mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-02 07:50:13 -04:00
795276935c
- Shared render: cache Font encode/width, skip unchanged palette/ShaderFX uniform sends, reuse quads and scratch tables, cheaper sprite palettes - Mods: fast path for loaded requires, memoized sandbox verdicts, fewer Hooks:call allocations, drop emptied hook chains - Audio: bulk PCM writes, cry/SFX prewarm on the worker, sliced sync fallback, cached Gen 3 SE sources - Gen 1: per-frame SGB battle palettes, hoisted overworld FX, cached menus - Gen 2: numeric attr grid, indexed anim cells, entity culling, memoized cache keys, palette shader set once per tile run - Gen 3: reuse native tile batches, per-cell metatile rebuilds, partial FRLG tile anim uploads, near-view ghost ticking, packed FRLG font atlas - Fixes: emergency quit timer, Gen 3 batch regression and stale sprite index, trainer card texture leak, missing pic re-reads, anim tint cache, Crystal ghost grass overdraw, refresh-rate dependent counters, pad swap
159 lines
7.0 KiB
Lua
159 lines
7.0 KiB
Lua
-- Presentation counters that draw code samples must advance at the fixed
|
|
-- 60Hz logic rate, not once per rendered frame:
|
|
-- * Player:pose's surf bob (a 144Hz display bobbed 2.4x too fast, and the
|
|
-- battle-transition wipe, which draws the player twice a frame, doubled
|
|
-- it) -- now counted in Game:step logic steps (Game.logicStep);
|
|
-- * the battle text box's ScrollTextUpOneLine offset (scrollPx), which
|
|
-- BattleState:drawTextArea / WideBattle counted down per draw -- now
|
|
-- BattleState:tickTextScroll, run once per update step.
|
|
-- Also pins the per-frame caching in the battle OAM colorizer: one
|
|
-- sgbBattlePals() per frame, shared color triples, and AnimPlayer's
|
|
-- uniform sends always matching the colors of the cell being painted.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.modkit")
|
|
|
|
-- ---------------------------------------------------------------- surf bob
|
|
do
|
|
local prevGame = package.loaded["src.core.Game"]
|
|
local fakeGame = { logicStep = 10 }
|
|
package.loaded["src.core.Game"] = fakeGame
|
|
local Player = require("src.world.Player")
|
|
local p = setmetatable({ surfing = true, px = 0, py = 0, facing = "down",
|
|
progress = 0, animClock = 0 }, Player)
|
|
-- the wipe's double draw (and any 120/144Hz frame without a new step)
|
|
p:pose(); p:pose(); p:pose()
|
|
T.eq(p.bobTimer, 1, "three draws inside one logic step advance the bob once")
|
|
fakeGame.logicStep = 11
|
|
p:pose(); p:pose()
|
|
T.eq(p.bobTimer, 2, "the next step advances it exactly once more")
|
|
fakeGame.logicStep = 14
|
|
p:pose()
|
|
T.eq(p.bobTimer, 5, "steps run between draws (fast-forward) all count")
|
|
-- the 32-step cycle: low for bob 0..15, one pixel down for 16..31
|
|
local lows, highs = 0, 0
|
|
for _ = 1, 32 do
|
|
fakeGame.logicStep = fakeGame.logicStep + 1
|
|
local _, _, y = p:pose()
|
|
local y2 = select(3, p:pose()) -- a second draw in the same step
|
|
T.eq(y2, y, "a repeat draw in the same step shows the same bob")
|
|
if y == 0 then lows = lows + 1 else highs = highs + 1 end
|
|
end
|
|
T.eq(lows, 16, "16 steps of the cycle sit on the baseline")
|
|
T.eq(highs, 16, "16 steps of the cycle sit a pixel down")
|
|
-- no running Game (headless callers): the old per-call tick
|
|
fakeGame.logicStep = nil
|
|
local before = p.bobTimer
|
|
p:pose(); p:pose()
|
|
T.eq(p.bobTimer, (before + 2) % 32, "without a step clock each pose ticks")
|
|
package.loaded["src.core.Game"] = prevGame
|
|
end
|
|
|
|
-- --------------------------------------------------------- battle scroll
|
|
local BattleState = require("src.battle.BattleState")
|
|
do
|
|
local b = setmetatable({ phase = "messages", current = {}, scrollPx = 8 },
|
|
{ __index = BattleState })
|
|
local seen = {}
|
|
for i = 1, 5 do
|
|
b:tickTextScroll()
|
|
seen[i] = tostring(b.scrollPx)
|
|
end
|
|
T.eq(table.concat(seen, ","), "6,4,2,nil,nil",
|
|
"2px per logic step, cleared at 0 (the draws saw 6,4,2,0 before)")
|
|
b.scrollPx = 8
|
|
b.phase = "menu"
|
|
b:tickTextScroll()
|
|
T.eq(b.scrollPx, 8, "no count-down while the message box is not shown")
|
|
b.phase, b.current, b.animPlaying, b.msgHold = "messages", nil, nil, true
|
|
b:tickTextScroll()
|
|
T.eq(b.scrollPx, 6, "msgHold keeps the box (and its scroll) live")
|
|
end
|
|
|
|
-- ------------------------------------------- battle OAM colorizer caching
|
|
do
|
|
local zoneA = { { 255, 255, 255 }, { 200, 0, 0 }, { 100, 0, 0 }, { 0, 0, 0 } }
|
|
local zoneB = { { 255, 255, 255 }, { 0, 200, 0 }, { 0, 100, 0 }, { 0, 0, 0 } }
|
|
local palsCalls = 0
|
|
local b = setmetatable({}, { __index = BattleState })
|
|
b.sgbBattlePals = function()
|
|
palsCalls = palsCalls + 1
|
|
return { [0] = zoneA, [1] = zoneB, [2] = zoneA, [3] = zoneB }
|
|
end
|
|
local PaletteFX = require("src.render.PaletteFX")
|
|
local realObp = PaletteFX.usesSpriteObp
|
|
PaletteFX.usesSpriteObp = function() return false end
|
|
local memo = {}
|
|
local c1 = b:animSpriteColors({ obp = "e4", x = 20, y = 20 }, 0, 0, memo)
|
|
local c2 = b:animSpriteColors({ obp = "e4", x = 20, y = 28 }, 0, 8, memo)
|
|
local c3 = b:animSpriteColors({ obp = "e4", x = 20, y = 20 }, 8, 8, memo)
|
|
T.eq(palsCalls, 1, "one sgbBattlePals per frame memo")
|
|
T.check(c1 == c2, "same zone + obp shares one triple table")
|
|
T.check(c3 ~= c1, "the enemy-HUD zone resolves its own triple")
|
|
T.eq(math.floor(c1[1][1] * 255 + 0.5), 200, "e4 color 1 is the zone's shade 1")
|
|
T.eq(math.floor(c3[1][2] * 255 + 0.5), 200, "enemy HUD wears zone 1's shade 1")
|
|
local fresh = b:animSpriteColors({ obp = "e4", x = 20, y = 20 }, 0, 0)
|
|
T.eq(palsCalls, 2, "without a memo it resolves the palettes itself")
|
|
T.same(fresh, c1, "memoized and unmemoized colors agree")
|
|
PaletteFX.usesSpriteObp = realObp
|
|
|
|
-- AnimPlayer.drawSprites: every blit must see the uniforms of the cell
|
|
-- it paints, whatever sends were skipped as redundant
|
|
local AnimPlayer = require("src.battle.AnimPlayer")
|
|
local g = love.graphics
|
|
local saved = {}
|
|
for _, k in ipairs({ "draw", "setShader", "getScissor", "intersectScissor",
|
|
"setScissor" }) do saved[k] = g[k] end
|
|
local shader = { u = {} }
|
|
function shader:send(name, v) self.u[name] = v end
|
|
local realShaderFn = PaletteFX.shader
|
|
PaletteFX.shader = function() return shader end
|
|
local scissor
|
|
local blits = {}
|
|
g.draw = function()
|
|
blits[#blits + 1] = { c1 = shader.u.c1, c2 = shader.u.c2,
|
|
c3 = shader.u.c3, clip = scissor }
|
|
end
|
|
g.setShader = function() end
|
|
g.getScissor = function()
|
|
if scissor then return scissor[1], scissor[2], scissor[3], scissor[4] end
|
|
end
|
|
g.intersectScissor = function(x, y, w, h) scissor = { x, y, w, h } end
|
|
g.setScissor = function(x, y, w, h) scissor = x and { x, y, w, h } or nil end
|
|
local tri = {}
|
|
local function colorAt(px, py)
|
|
-- a vertical zone boundary at x = 16
|
|
local key = px < 16 and "L" or "R"
|
|
tri[key] = tri[key] or { { px < 16 and 1 or 0, 0, 0 }, { 0, 0, 0 },
|
|
{ 0, 0, 0 } }
|
|
return tri[key]
|
|
end
|
|
local ap = setmetatable({ sheetImage = function() return {} end,
|
|
tileQuad = function() return {} end },
|
|
{ __index = AnimPlayer })
|
|
local sprites = {
|
|
{ x = 8 + 4, y = 16, tile = 1 }, -- rx 4: fully in L
|
|
{ x = 8 + 12, y = 16, tile = 1 }, -- rx 12: straddles L|R
|
|
{ x = 8 + 20, y = 16 + 3, tile = 1 }, -- rx 20, ry 3: R only (2 rows)
|
|
{ x = 8 + 5, y = 16, tile = 1 }, -- back to L
|
|
}
|
|
ap:drawSprites(sprites, function(_, px, py) return colorAt(px, py) end)
|
|
-- expected paint order: L, L+R(slice), R, R(slice, same colors -> no
|
|
-- repaint), L
|
|
local want = { "L", "L", "R", "R", "L" }
|
|
T.eq(#blits, #want, "one blit per tile plus one per differing cell")
|
|
for i, w in ipairs(want) do
|
|
local bl = blits[i]
|
|
if bl then
|
|
T.eq(bl.c1 and bl.c1[1], w == "L" and 1 or 0,
|
|
("blit %d paints with its cell's colors"):format(i))
|
|
end
|
|
end
|
|
T.check(blits[3] and blits[3].clip ~= nil,
|
|
"the straddling tile's second cell is scissored")
|
|
for k, v in pairs(saved) do g[k] = v end
|
|
PaletteFX.shader = realShaderFn
|
|
end
|
|
|
|
T.finish("refresh-rate-independent counters")
|