diff --git a/src/ui/gen2/Chrome.lua b/src/ui/gen2/Chrome.lua index 12f8a802..6a147077 100644 --- a/src/ui/gen2/Chrome.lua +++ b/src/ui/gen2/Chrome.lua @@ -138,11 +138,39 @@ function Chrome.printThrough(text, tx, ty, palette, invert) local paper = pal[1] or { 255, 255, 255 } love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1) love.graphics.rectangle("fill", tx * 8, ty * 8, width, 8) - love.graphics.setColor(1, 1, 1, 1) + -- Per glyph, not per string: a TTF-mod build still keeps the multi-byte + -- charmap sequences (the naming screen's /, the 'd/'l/'s ligatures) + -- and anything the mod names in ttf.tiles (Font.lua's own note on keeping + -- digits tile-based for column alignment) on their ROM tiles, so a string + -- can freely mix the two kinds of glyph. + local ink = pal[4] or { 0, 0, 0 } local previous = love.graphics.getShader() - GbcPalette.useRaw(pal) - Font.draw(text, tx * 8, ty * 8) - love.graphics.setShader(previous) + local shaded = false + local pen = tx * 8 + for _, code in ipairs(Font.encode(text)) do + if code >= Font.TTF_BASE then + -- The shader below recovers a shade from the RED CHANNEL of an already + -- flat-shaded 2bpp tile sheet -- exactly what a TTF glyph is not. + -- LÖVE's font rasterizer stores glyph coverage as alpha over a plain + -- white texture, which this shader reads back as shade 0 no matter how + -- solid the glyph looks, painting the character the SAME colour as the + -- paper rect just drawn above it: invisible (reported against a real + -- Gold build with a TTF translation mod active, gen1recomp#1642). A TTF + -- glyph has no discrete shade to recover in the first place, so skip + -- the shader for it and tint it with the palette's own ink colour + -- (shade 3, the same entry `rgb = pal3` would have mapped a black tile + -- pixel to) directly. + if shaded then love.graphics.setShader(previous); shaded = false end + love.graphics.setColor(ink[1] / 255, ink[2] / 255, ink[3] / 255, 1) + elseif not shaded then + love.graphics.setColor(1, 1, 1, 1) + GbcPalette.useRaw(pal) + shaded = true + end + Font.drawCode(code, pen, ty * 8) + pen = pen + Font.advanceOf(code) + end + if shaded then love.graphics.setShader(previous) end love.graphics.setColor(0, 0, 0, 1) return width end diff --git a/tests/engine/gen2_chrome_print_through_ttf_test.lua b/tests/engine/gen2_chrome_print_through_ttf_test.lua new file mode 100644 index 00000000..068d0a2f --- /dev/null +++ b/tests/engine/gen2_chrome_print_through_ttf_test.lua @@ -0,0 +1,133 @@ +-- Chrome.printThrough (src/ui/gen2/Chrome.lua) used to run every string +-- through the GbcPalette shade-remap shader whenever a palette was given, +-- with no regard for whether the glyph it was about to draw came from a +-- tile page or from a TTF. The shader recovers a shade by reading the +-- RED CHANNEL of an already-rasterized 2bpp tile pixel (SHADER_SOURCE in +-- src/render/GbcPalette.lua); a TTF glyph is LÖVE's own anti-aliased +-- coverage mask, drawn as plain white with the current tint carrying the +-- ink colour, which that same channel read always reports as shade 0 -- +-- painting every character the SAME colour as the paper rect printThrough +-- had just drawn behind it, i.e. invisible. Reported against a real Gold +-- build running a TTF translation mod: the naming screen's keyboard, +-- Diploma and Pokegear text all vanish, since all three draw through this +-- one routine (gen1recomp#1642). +-- +-- The switch is per GLYPH, not per string: a TTF-mod build still keeps +-- multi-byte charmap sequences (the naming screen's own / cells, +-- the 'd/'l/'s ligatures) and anything a mod names in ttf.tiles on their ROM +-- tiles (src/render/Font.lua's Font.split), so one call can mix both kinds +-- of glyph and each must take its own path. +-- +-- No real shader runs headless (love_stub does not stub newShader), so this +-- cannot check a rendered pixel. Font.encode/drawCode/advanceOf/width are +-- replaced with fakes that hand printThrough a fixed list of glyph codes, +-- so what is checked is the two things that decide the outcome: which +-- glyphs skip the shader, and what colour is active when each one draws. +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") + +love = require("tests.love_stub") + +require("src.core.Logger").warn = function() end + +local Chrome = require("src.ui.gen2.Chrome") +local GbcPalette = require("src.render.GbcPalette") +local Font = require("src.render.Font") + +-- Palette arrays are 1-indexed shade 0..3, same order GbcPalette.useRaw +-- reads (src/render/GbcPalette.lua channel()). +local PALETTE = { { 200, 220, 255 }, { 150, 170, 220 }, { 90, 100, 160 }, { 10, 10, 30 } } + +local TILE_CODE = 0x80 +local TTF_CODE = Font.TTF_BASE + 65 -- 'A', were it decoded + +-- "text" is a plain list of glyph codes for this suite; Font.width/encode +-- pass it straight through, matching how a real caller's string would +-- decode to a list of codes. +Font.width = function(codes) return #codes * 8 end +Font.encode = function(codes) return codes end +Font.advanceOf = function(_code) return 8 end + +local drawn +Font.drawCode = function(code, x, y) + drawn[#drawn + 1] = { code = code, x = x, y = y, color = { love.graphics.getColor() } } +end + +local useRawCalls +local realUseRaw = GbcPalette.useRaw +GbcPalette.useRaw = function(...) + useRawCalls = useRawCalls + 1 + return realUseRaw(...) +end + +GbcPalette.available = function() return true end + +local function colorsEq(a, b) + return math.abs(a[1] - b[1]) < 1e-9 and math.abs(a[2] - b[2]) < 1e-9 + and math.abs(a[3] - b[3]) < 1e-9 +end + +-- ------------------------------------------------------- all tile glyphs +do + useRawCalls = 0 + drawn = {} + Chrome.printThrough({ TILE_CODE, TILE_CODE }, 0, 0, PALETTE) + T.eq(useRawCalls, 1, "one shaded run binds the shader once, not per glyph") + T.eq(#drawn, 2, "both glyphs drew") + for i, d in ipairs(drawn) do + T.check(colorsEq(d.color, { 1, 1, 1, 1 }), + ("tile glyph %d is tinted white, letting the shader pick the colour"):format(i)) + end +end + +-- -------------------------------------------------------- all TTF glyphs +do + useRawCalls = 0 + drawn = {} + Chrome.printThrough({ TTF_CODE, TTF_CODE }, 0, 0, PALETTE) + T.eq(useRawCalls, 0, "a TTF glyph never binds the shade-remap shader") + T.eq(#drawn, 2, "both glyphs drew") + local ink = Chrome.throughPalette(PALETTE, false)[4] + for i, d in ipairs(drawn) do + T.check(colorsEq(d.color, { ink[1] / 255, ink[2] / 255, ink[3] / 255, 1 }), + ("TTF glyph %d is tinted with the palette's own ink colour"):format(i)) + end +end + +-- --------------------------------------------- mixed: tile, TTF, then tile +do + useRawCalls = 0 + drawn = {} + Chrome.printThrough({ TILE_CODE, TTF_CODE, TILE_CODE }, 0, 0, PALETTE) + T.eq(useRawCalls, 2, + "the shader re-binds once per return to a tile glyph, not once for the whole string") + T.eq(#drawn, 3, "all three glyphs drew") + local ink = Chrome.throughPalette(PALETTE, false)[4] + T.check(colorsEq(drawn[1].color, { 1, 1, 1, 1 }), "1st (tile) glyph: white/shaded") + T.check(colorsEq(drawn[2].color, { ink[1] / 255, ink[2] / 255, ink[3] / 255, 1 }), + "2nd (TTF) glyph, mid-string, still gets the ink tint") + T.check(colorsEq(drawn[3].color, { 1, 1, 1, 1 }), "3rd (tile) glyph: shaded again") +end + +-- ---------------------------------------------------- inverted TTF ink +do + drawn = {} + Chrome.printThrough({ TTF_CODE }, 0, 0, PALETTE, true) + local ink = Chrome.throughPalette(PALETTE, true)[4] + T.check(colorsEq(drawn[1].color, { ink[1] / 255, ink[2] / 255, ink[3] / 255, 1 }), + "an inverted call tints TTF ink with the inverted palette's own shade-3 entry") +end + +-- ---------------------------------------------- DMG mode's own TTF ink +do + GbcPalette.setMode("dmg") + drawn = {} + Chrome.printThrough({ TTF_CODE }, 0, 0, PALETTE) + local ink = Chrome.throughPalette(PALETTE, false)[4] + T.check(colorsEq(drawn[1].color, { ink[1] / 255, ink[2] / 255, ink[3] / 255, 1 }), + "DMG mode's own resolved palette (four grey hardware shades) still reaches the TTF ink") + GbcPalette.setMode("gbc") +end + +T.finish("gen2_chrome_print_through_ttf_test")