mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-27 23:35:16 -04:00
Pack Ruby/Sapphire font sheets into one atlas
FireRed draws each string from a packed fg+shadow atlas so LOVE's autobatcher keeps it in one draw call. Ruby/Sapphire's native faces (rs/font_faces.lua) skipped that and drew every glyph from three textures (shadow, fg, embedded colours), flushing the batch on every layer of every glyph. Stack all three sheets in one texture and draw from it.
This commit is contained in:
@@ -1448,12 +1448,17 @@ function FrlgFont.draw(text, x, y, opts)
|
||||
set_col(curCol.bg)
|
||||
love.graphics.rectangle("fill", dx, dy, adv, pitch)
|
||||
end
|
||||
local aq = gat and gat.fg[aid]
|
||||
local aq, ash = gat and gat.fg[aid], gat and gat.sh[aid]
|
||||
-- R/S native faces pack fg, shadow and embedded colours into one
|
||||
-- texture (rs/font_faces.lua), keyed by the source quad
|
||||
local nq = nativeFace and nativeFace.atlas and nativeFace.atlas.of[q]
|
||||
local aimg = gat and gat.image
|
||||
if nq then aq, ash, aimg = nq.fg, nq.sh, nativeFace.atlas.image end
|
||||
-- Draw shadow
|
||||
if gsh and curCol.shadow and (not curCol.shadow[4] or curCol.shadow[4] > 0) then
|
||||
set_col(curCol.shadow)
|
||||
if aq then
|
||||
love.graphics.draw(gat.image, gat.sh[aid], dx, dy)
|
||||
love.graphics.draw(aimg, ash, dx, dy)
|
||||
else
|
||||
love.graphics.draw(gsh, q, dx, dy)
|
||||
end
|
||||
@@ -1462,14 +1467,18 @@ function FrlgFont.draw(text, x, y, opts)
|
||||
if curCol.fg and (not curCol.fg[4] or curCol.fg[4] > 0) then
|
||||
set_col(curCol.fg)
|
||||
if aq then
|
||||
love.graphics.draw(gat.image, aq, dx, dy)
|
||||
love.graphics.draw(aimg, aq, dx, dy)
|
||||
else
|
||||
love.graphics.draw(gfg, q, dx, dy)
|
||||
end
|
||||
end
|
||||
if nativeFace and nativeFace.fixed then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(nativeFace.fixed, q, dx, dy)
|
||||
if nq and nq.fixed then
|
||||
love.graphics.draw(aimg, nq.fixed, dx, dy)
|
||||
else
|
||||
love.graphics.draw(nativeFace.fixed, q, dx, dy)
|
||||
end
|
||||
end
|
||||
end
|
||||
penX = penX + japanese_step(id, adv, minW, jpn, activeOpts, useSmall)
|
||||
|
||||
@@ -5,6 +5,44 @@ local function read(path)
|
||||
return Cache.readActive(path)
|
||||
end
|
||||
|
||||
-- Stack the fg, shadow and embedded-colour sheets in one texture so a
|
||||
-- string's glyphs share it and LOVE's autobatcher keeps the run in one draw
|
||||
-- call, as frlg_font's pack_atlas does for the FRLG sheets. `of` maps each
|
||||
-- source quad to its { fg, sh, fixed } quads in the atlas; pixels are copied
|
||||
-- verbatim. nil (draw from the separate sheets) when the data is missing.
|
||||
local ATLAS_GAP = 16
|
||||
function Faces.pack(fgData, shData, fixedData, quads, boundaryQuads)
|
||||
if not (fgData and shData and love and love.image and love.graphics) then return nil end
|
||||
local ok, atlas = pcall(function()
|
||||
local w, h = fgData:getDimensions()
|
||||
local sw, shh = shData:getDimensions()
|
||||
if sw ~= w or shh ~= h then return nil end
|
||||
local fmt = fgData:getFormat()
|
||||
if shData:getFormat() ~= fmt or (fixedData and fixedData:getFormat() ~= fmt) then return nil end
|
||||
local off = h + ATLAS_GAP
|
||||
local data = love.image.newImageData(w, (fixedData and 3 or 2) * off - ATLAS_GAP, fmt)
|
||||
data:paste(fgData, 0, 0, 0, 0, w, h)
|
||||
data:paste(shData, 0, off, 0, 0, w, h)
|
||||
if fixedData then data:paste(fixedData, 0, 2 * off, 0, 0, w, h) end
|
||||
local img = love.graphics.newImage(data)
|
||||
img:setFilter("nearest", "nearest")
|
||||
local aw, ah = img:getDimensions()
|
||||
local of = {}
|
||||
local function add(q)
|
||||
local qx, qy, qw, qh = q:getViewport()
|
||||
of[q] = {
|
||||
fg = love.graphics.newQuad(qx, qy, qw, qh, aw, ah),
|
||||
sh = love.graphics.newQuad(qx, qy + off, qw, qh, aw, ah),
|
||||
fixed = fixedData and love.graphics.newQuad(qx, qy + 2 * off, qw, qh, aw, ah) or nil,
|
||||
}
|
||||
end
|
||||
for _, q in pairs(quads) do add(q) end
|
||||
for _, q in pairs(boundaryQuads) do add(q) end
|
||||
return { image = img, of = of }
|
||||
end)
|
||||
return ok and atlas or nil
|
||||
end
|
||||
|
||||
-- pokeruby/src/text.c:2676
|
||||
function Faces.load(spec, name, language, loadImage, loadTable, palette, textMode)
|
||||
local id = assert(spec.faces[name], "unknown RS font face").id
|
||||
@@ -33,9 +71,10 @@ function Faces.load(spec, name, language, loadImage, loadTable, palette, textMod
|
||||
fixed[p] = "\0\0\0\0"
|
||||
end
|
||||
end
|
||||
local fixedImage
|
||||
local fixedImage, fixedData
|
||||
if present then
|
||||
fixedImage = love.graphics.newImage(love.image.newImageData(info.width, info.height, "rgba8", table.concat(fixed)))
|
||||
fixedData = love.image.newImageData(info.width, info.height, "rgba8", table.concat(fixed))
|
||||
fixedImage = love.graphics.newImage(fixedData)
|
||||
fixedImage:setFilter("nearest", "nearest")
|
||||
end
|
||||
local quads, boundaryQuads = {}, {}
|
||||
@@ -53,6 +92,7 @@ function Faces.load(spec, name, language, loadImage, loadTable, palette, textMod
|
||||
fg = fg, sh = sh, fixed = fixedImage, quads = quads, boundaryQuads = boundaryQuads, widths = widths,
|
||||
height = 16, pitch = 16, letterSpacing = 0, glyphs = info.glyphs,
|
||||
fgData = fgData, shData = shData,
|
||||
atlas = Faces.pack(fgData, shData, fixedData, quads, boundaryQuads),
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
-- Minimal LOVE stand-ins: ImageData with paste, Images, Quads with viewports.
|
||||
local Data = {}
|
||||
Data.__index = Data
|
||||
local function newData(w, h, fmt)
|
||||
return setmetatable({ w = w, h = h, fmt = fmt or "rgba8", px = {} }, Data)
|
||||
end
|
||||
function Data:getDimensions() return self.w, self.h end
|
||||
function Data:getFormat() return self.fmt end
|
||||
function Data:paste(src, dx, dy, sx, sy, sw, sh)
|
||||
for y = 0, sh - 1 do
|
||||
for x = 0, sw - 1 do
|
||||
self.px[(dy + y) * self.w + dx + x] = src.px[(sy + y) * src.w + sx + x]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local Image = {}
|
||||
Image.__index = Image
|
||||
function Image:getDimensions() return self.data.w, self.data.h end
|
||||
function Image:setFilter() end
|
||||
|
||||
local Quad = {}
|
||||
Quad.__index = Quad
|
||||
function Quad:getViewport() return self.x, self.y, self.w, self.h end
|
||||
|
||||
local draws = {}
|
||||
_G.love = {
|
||||
image = { newImageData = function(w, h, fmt) return newData(w, h, fmt) end },
|
||||
graphics = {
|
||||
newImage = function(data) return setmetatable({ data = data }, Image) end,
|
||||
newQuad = function(x, y, w, h) return setmetatable({ x = x, y = y, w = w, h = h }, Quad) end,
|
||||
draw = function(img, q) draws[#draws + 1] = { img = img, q = q } end,
|
||||
setColor = function() end,
|
||||
rectangle = function() end,
|
||||
},
|
||||
}
|
||||
|
||||
local Faces = require("src.ui.game3.rs.font_faces")
|
||||
|
||||
local function sheet(w, h, tag)
|
||||
local d = newData(w, h)
|
||||
for i = 0, w * h - 1 do d.px[i] = tag .. i end
|
||||
return d
|
||||
end
|
||||
|
||||
local W, H = 32, 32
|
||||
local fg, sh, fixed = sheet(W, H, "f"), sheet(W, H, "s"), sheet(W, H, "x")
|
||||
local q0 = love.graphics.newQuad(0, 0, 8, 16)
|
||||
local q1 = love.graphics.newQuad(16, 0, 6, 16)
|
||||
local b1 = love.graphics.newQuad(16, 0, 3, 16)
|
||||
|
||||
local atlas = Faces.pack(fg, sh, fixed, { [0] = q0, [1] = q1 }, { [1] = b1 })
|
||||
check(atlas ~= nil, "pack builds an atlas from fg, shadow and embedded-colour sheets")
|
||||
local off = H + 16
|
||||
local img = atlas.image.data
|
||||
eq(img.h, 3 * off - 16, "three layers stacked with a gap between them")
|
||||
eq(img.px[0], "f0", "fg layer copied verbatim at the top")
|
||||
eq(img.px[off * W + 5], "s5", "shadow layer copied verbatim below the fg")
|
||||
eq(img.px[2 * off * W + 7], "x7", "embedded-colour layer copied verbatim at the bottom")
|
||||
|
||||
local function vp(q) return { q:getViewport() } end
|
||||
local e = atlas.of[q1]
|
||||
check(e ~= nil and atlas.of[b1] ~= nil and atlas.of[q0] ~= nil, "every normal and boundary quad is mapped")
|
||||
local f, s, x = vp(e.fg), vp(e.sh), vp(e.fixed)
|
||||
check(f[1] == 16 and f[2] == 0 and f[3] == 6 and f[4] == 16, "fg quad keeps the source viewport")
|
||||
check(s[1] == 16 and s[2] == off and s[3] == 6, "shadow quad is offset by one layer")
|
||||
check(x[1] == 16 and x[2] == 2 * off and x[3] == 6, "embedded-colour quad is offset by two layers")
|
||||
eq(vp(atlas.of[b1].fg)[3], 3, "boundary quads keep their 3-px width")
|
||||
|
||||
local two = Faces.pack(fg, sh, nil, { [0] = q0 }, {})
|
||||
check(two and two.image.data.h == 2 * off - 16 and two.of[q0].fixed == nil,
|
||||
"a font without embedded colours packs two layers")
|
||||
check(Faces.pack(fg, sheet(16, 16, "s"), nil, { [0] = q0 }, {}) == nil,
|
||||
"mismatched sheets fall back to separate textures")
|
||||
local other = sheet(W, H, "s")
|
||||
other.fmt = "r8"
|
||||
check(Faces.pack(fg, other, nil, { [0] = q0 }, {}) == nil,
|
||||
"mismatched formats fall back to separate textures")
|
||||
|
||||
-- Every layer of every glyph in a string draws from the one atlas texture.
|
||||
local FrlgFont = require("src.ui.game3.frlg_font")
|
||||
local saved = {
|
||||
spec = FrlgFont._spec, ver = FrlgFont._syncVersion, faces = FrlgFont._faces,
|
||||
}
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local GV = require("src.core.GameVersion")
|
||||
local v = (Runtime and type(Runtime.session) == "table" and Runtime.session.version) or GV.current or ""
|
||||
FrlgFont._syncVersion, FrlgFont._spec = v, { nativeLayout = "rs" }
|
||||
local face = {
|
||||
name = "normal", nativeIndexed = true,
|
||||
fg = love.graphics.newImage(fg), sh = love.graphics.newImage(sh), fixed = love.graphics.newImage(fixed),
|
||||
quads = {}, boundaryQuads = {}, widths = {}, height = 16, pitch = 16, letterSpacing = 0,
|
||||
}
|
||||
for id = 0, 255 do
|
||||
face.quads[id] = love.graphics.newQuad(0, 0, 8, 16)
|
||||
face.widths[id] = 6
|
||||
end
|
||||
face.atlas = Faces.pack(fg, sh, fixed, face.quads, face.boundaryQuads)
|
||||
FrlgFont._faces = { normal = face }
|
||||
|
||||
local colors = { fg = { 1, 1, 1, 1 }, shadow = { 0, 0, 0, 1 } }
|
||||
FrlgFont.draw("AB", 0, 0, { colors = colors })
|
||||
check(#draws == 6, "two glyphs draw shadow, fg and embedded colours: " .. #draws)
|
||||
local allAtlas = true
|
||||
for _, d in ipairs(draws) do
|
||||
if d.img ~= face.atlas.image then allAtlas = false end
|
||||
end
|
||||
check(allAtlas, "every glyph layer draws from the packed atlas (one texture, batchable)")
|
||||
|
||||
draws = {}
|
||||
face.atlas = nil
|
||||
FrlgFont.draw("A", 0, 0, { colors = colors })
|
||||
check(#draws == 3 and draws[1].img == face.sh and draws[2].img == face.fg and draws[3].img == face.fixed,
|
||||
"without an atlas the separate sheets still draw")
|
||||
|
||||
FrlgFont._spec, FrlgFont._syncVersion, FrlgFont._faces = saved.spec, saved.ver, saved.faces
|
||||
T.finish("game3_rs_font_atlas_test")
|
||||
Reference in New Issue
Block a user