mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-02 15:50:39 -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
320 lines
12 KiB
Lua
320 lines
12 KiB
Lua
-- Hot-path caches and the bugs fixed alongside them: Font's encode/width
|
|
-- memo, PaletteFX/GbcPalette last-sent uniform detection, SpriteRenderer's
|
|
-- obp bake lookup, Hooks' unwrap of the last link, Sandbox's per-name
|
|
-- verdict, Input's recycled step tables and stale-pad rebuild, and
|
|
-- TextBox's scroll slide running on the logic step instead of draw.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
love = love or require("tests.love_stub")
|
|
|
|
package.loaded["src.core.Logger"] = {
|
|
info = function() end, warn = function() end, error = function() end,
|
|
debug = function() end,
|
|
}
|
|
|
|
-- ------------------------------------------------------------- Font
|
|
do
|
|
local Font = require("src.render.Font")
|
|
local a = Font.encode("ABC")
|
|
a[1], a[4] = -1, -2
|
|
local b = Font.encode("ABC")
|
|
eq(#b, 3, "encode hands out a copy: a caller's append does not stick")
|
|
check(b[1] ~= -1, "encode hands out a copy: a caller's write does not stick")
|
|
check(a ~= b, "each encode is a fresh table")
|
|
eq(Font.glyphCount("ABC"), #Font.split("ABC"), "glyphCount matches split")
|
|
local w = Font.width("ABC")
|
|
eq(Font.width("ABC"), w, "cached width is stable")
|
|
local rev = Font.revision
|
|
Font.setFrame(Font.frameIndex())
|
|
eq(Font.revision, rev, "re-selecting the same frame keeps the caches")
|
|
Font.setFrame(Font.frameIndex() + 1)
|
|
check(Font.revision > rev, "a frame change drops cached widths")
|
|
Font.setFrame(1)
|
|
end
|
|
|
|
-- ------------------------------------------------------------- PaletteFX
|
|
local function recordingShader()
|
|
local sh = { sends = {} }
|
|
function sh.send(self, name, value)
|
|
self.sends[#self.sends + 1] = { name = name, value = { value[1], value[2], value[3] } }
|
|
end
|
|
return sh
|
|
end
|
|
|
|
do
|
|
local PaletteFX = require("src.render.PaletteFX")
|
|
PaletteFX.pickerActive = function() return false end
|
|
local pal = { { 255, 0, 0 }, { 0, 255, 0 }, { 0, 0, 255 }, { 0, 0, 0 } }
|
|
local sh = recordingShader()
|
|
|
|
PaletteFX.mode = "gbc"
|
|
PaletteFX.sendColors(sh, pal)
|
|
eq(#sh.sends, 4, "first send sets all four colors")
|
|
PaletteFX.sendColors(sh, pal)
|
|
eq(#sh.sends, 4, "identical colors to the same shader are not re-sent")
|
|
local changed = { pal[1], pal[2], { 10, 20, 30 }, pal[4] }
|
|
PaletteFX.sendColors(sh, changed)
|
|
eq(#sh.sends, 5, "only the changed uniform is re-sent")
|
|
eq(sh.sends[5].name, "c2", "and it is the one that changed")
|
|
|
|
local other = recordingShader()
|
|
PaletteFX.sendColors(other, changed)
|
|
eq(#other.sends, 4, "the record is per shader")
|
|
|
|
PaletteFX.forgetSent(sh)
|
|
PaletteFX.sendColors(sh, changed)
|
|
eq(#sh.sends, 9, "forgetSent makes the next send go out in full")
|
|
|
|
PaletteFX.sendShades(sh, pal)
|
|
PaletteFX.sendColors(sh, changed)
|
|
eq(sh.sends[#sh.sends].name, "c2", "sendShades shares the record with sendColors")
|
|
|
|
-- the index arithmetic must land on exactly what effectiveColors resolves
|
|
local function lastFour(s)
|
|
local out = {}
|
|
for i = #s.sends, 1, -1 do
|
|
local e = s.sends[i]
|
|
local k = tonumber(e.name:sub(2)) + 1
|
|
if not out[k] then out[k] = e.value end
|
|
end
|
|
return out
|
|
end
|
|
for _, mode in ipairs({ "gbc", "gbc_inv", "og", "og_inv", "classic" }) do
|
|
for _, map in ipairs({ false, PaletteFX.DARK_BGP, PaletteFX.POISON_BGP }) do
|
|
PaletteFX.mode = mode
|
|
PaletteFX.setShadeMap(map or nil)
|
|
local s = recordingShader()
|
|
PaletteFX.sendColors(s, pal)
|
|
local want = PaletteFX.effectiveColors(pal)
|
|
local got = lastFour(s)
|
|
local same = true
|
|
for i = 1, 4 do
|
|
for c = 1, 3 do
|
|
if math.abs(got[i][c] - want[i][c] / 255) > 1e-12 then same = false end
|
|
end
|
|
end
|
|
check(same, ("sendColors matches effectiveColors (%s, map %s)")
|
|
:format(mode, tostring(map and true)))
|
|
end
|
|
end
|
|
PaletteFX.mode = "gbc"
|
|
PaletteFX.setShadeMap(nil)
|
|
end
|
|
|
|
-- ------------------------------------------------------------- GbcPalette
|
|
do
|
|
local GbcPalette = require("src.render.GbcPalette")
|
|
local sh = recordingShader()
|
|
love.graphics.newShader = function() return sh end
|
|
love.graphics.setShader = function() end
|
|
local pal = { { 255, 255, 255 }, { 170, 170, 170 }, { 85, 85, 85 }, { 0, 0, 0 } }
|
|
check(GbcPalette.useRaw(pal), "useRaw binds")
|
|
eq(#sh.sends, 4, "useRaw sends pal0..pal3 once")
|
|
GbcPalette.useRaw(pal)
|
|
eq(#sh.sends, 4, "an unchanged palette is not re-sent")
|
|
GbcPalette.useRaw({ pal[1], { 1, 2, 3 }, pal[3], pal[4] })
|
|
eq(#sh.sends, 5, "one changed entry is one send")
|
|
eq(sh.sends[5].name, "pal1", "to the changed uniform")
|
|
end
|
|
|
|
-- ------------------------------------------------------------- SpriteRenderer
|
|
do
|
|
local Assets = require("src.render.Assets")
|
|
local loads = 0
|
|
Assets.image = function() loads = loads + 1; return { tag = loads } end
|
|
local SpriteRenderer = require("src.render.SpriteRenderer")
|
|
SpriteRenderer.invalidate()
|
|
local savedImage = love.image
|
|
love.image = nil -- headless bake path
|
|
local colors = { {}, {}, {}, {} }
|
|
local one = SpriteRenderer.obpImage("sheet.png", colors, 1)
|
|
eq(SpriteRenderer.obpImage("sheet.png", colors, 1), one, "a cache hit returns the bake")
|
|
eq(SpriteRenderer.obpImage("sheet.png", colors, "1"), one,
|
|
"number and string groups share a key, as the concatenated key did")
|
|
check(SpriteRenderer.obpImage("sheet.png", colors, "dark") ~= nil, "a new group bakes")
|
|
SpriteRenderer.invalidate()
|
|
check(SpriteRenderer.obpImage("sheet.png", colors, 1) ~= one, "invalidate drops the lookup too")
|
|
love.image = savedImage
|
|
end
|
|
|
|
-- ------------------------------------------------------------- Hooks
|
|
do
|
|
local Hooks = require("src.mods.Hooks")
|
|
local h = Hooks.new()
|
|
local unwrap = h:wrap("x", function(next, v) return next(v + 1) end)
|
|
eq(h:call("x", function(v) return v * 2 end, 1), 4, "a single link runs")
|
|
unwrap()
|
|
eq(h.chains.x, nil, "unwrapping the last link drops the chain (wantsHook false)")
|
|
eq(h:call("x", function(v) return v * 2 end, 1), 2, "and the call is plain vanilla")
|
|
local u1 = h:wrap("y", function(next, ...) return next(...) end)
|
|
local u2 = h:wrap("y", function(next, ...) return next(...) end)
|
|
u1()
|
|
check(h.chains.y ~= nil, "a chain with links left stays")
|
|
u2()
|
|
eq(h.chains.y, nil, "and goes once empty")
|
|
local stale = h:wrap("z", function(next, ...) return next(...) end)
|
|
h:removeOwner(nil)
|
|
h.chains.z = nil
|
|
local fresh = h:wrap("z", function(next, ...) return next(...) end)
|
|
stale()
|
|
check(h.chains.z ~= nil, "a stale unwrap never drops a newer chain")
|
|
fresh()
|
|
local ok, err = pcall(function()
|
|
return h:call("none", function() error("boom", 0) end)
|
|
end)
|
|
check(not ok and err == "boom", "vanilla errors propagate without a chain")
|
|
h:wrap("w", function(next, ...) return next(...) end)
|
|
ok, err = pcall(function()
|
|
return h:call("w", function() error("deep", 0) end)
|
|
end)
|
|
check(not ok and err == "deep", "vanilla errors propagate through a chain unwrapped")
|
|
local n, a, b, c = select("#", h:call("w", function() return 1, nil, 3 end)),
|
|
h:call("w", function() return 1, nil, 3 end)
|
|
eq(n, 3, "trailing results keep their count")
|
|
check(a == 1 and b == nil and c == 3, "and their values")
|
|
end
|
|
|
|
-- ------------------------------------------------------------- Sandbox
|
|
do
|
|
local Sandbox = require("src.mods.Sandbox")
|
|
for _ = 1, 2 do
|
|
check(Sandbox.moduleDenial("io") ~= nil, "io denied")
|
|
check(Sandbox.moduleDenial("love.filesystem") ~= nil, "love.* denied")
|
|
eq(Sandbox.moduleDenial("love"), nil, "bare love allowed")
|
|
check(Sandbox.moduleDenial("socket.http") ~= nil, "network needs the permission")
|
|
eq(Sandbox.moduleDenial("socket.http", { network = true }), nil, "and passes with it")
|
|
eq(Sandbox.moduleDenial("src.core.Game"), nil, "engine modules allowed")
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------- Input
|
|
do
|
|
local Input = require("src.core.Input")
|
|
Input:init()
|
|
Input:keypressed("up")
|
|
Input:step()
|
|
local pressed, queue = Input.pressed, Input.pressQueue
|
|
check(Input:wasPressed("up"), "press edges")
|
|
Input:keyreleased("up")
|
|
Input:step()
|
|
eq(Input.pressed, pressed, "step recycles its own pressed table")
|
|
eq(Input.pressQueue, queue, "and its own queue")
|
|
check(not Input:wasPressed("up"), "cleared in place")
|
|
local mine = { a = true }
|
|
Input.pressed = mine
|
|
Input:step()
|
|
check(Input.pressed ~= mine and mine.a == true,
|
|
"a table a test installed is replaced, never emptied")
|
|
|
|
-- a pad swapped for another between two polls, same count
|
|
local function pad(name, connected)
|
|
return { name = name, connected = connected,
|
|
isGamepadDown = function() return false end,
|
|
isConnected = function(self) return self.connected end,
|
|
isGamepad = function() return true end,
|
|
getName = function(self) return self.name end,
|
|
getGUID = function() return "" end,
|
|
getID = function() return 1 end,
|
|
getButtonCount = function() return 12 end,
|
|
getAxisCount = function() return 4 end }
|
|
end
|
|
local p1, p2 = pad("one", true), pad("two", true)
|
|
local current = { p1 }
|
|
love.joystick = {
|
|
getJoystickCount = function() return #current end,
|
|
getJoysticks = function() return current end,
|
|
}
|
|
local GamepadMap = require("src.core.GamepadMap")
|
|
GamepadMap.ignoreRawForJoystick = function() return true end
|
|
GamepadMap.isAccelerometer = function() return false end
|
|
Input:pollPads()
|
|
eq(Input._pollPads[1], p1, "first poll caches the pad")
|
|
p1.connected = false
|
|
current = { p2 }
|
|
Input:pollPads()
|
|
eq(Input._pollPads[1], p2, "a disconnected cached pad forces a rebuild")
|
|
current = { p1 }
|
|
p1.connected = true
|
|
Input:joysticksChanged()
|
|
Input:pollPads()
|
|
eq(Input._pollPads[1], p1, "joysticksChanged drops the cache")
|
|
love.joystick = nil
|
|
end
|
|
|
|
-- ------------------------------------------------------------- TextBox
|
|
do
|
|
local TextBox = require("src.render.TextBox")
|
|
local input = {
|
|
wasPressed = function() return false end,
|
|
isDown = function() return false end,
|
|
}
|
|
local game = { data = { text = {} }, input = input,
|
|
save = { generation = 2, options = { textSpeed = 1 } } }
|
|
local box = TextBox.new(game, "a\nb\nc")
|
|
local steps = 0
|
|
while not box.scrollPx and steps < 200 do
|
|
box:update(1 / 60)
|
|
steps = steps + 1
|
|
end
|
|
eq(box.scrollPx, 6, "the frame that scrolls ends 2px into the slide")
|
|
local drew = pcall(box.draw, box)
|
|
if drew then
|
|
pcall(box.draw, box)
|
|
eq(box.scrollPx, 6, "draw no longer advances the slide")
|
|
end
|
|
box:update(1 / 60)
|
|
eq(box.scrollPx, 4, "each logic step moves it 2px")
|
|
box:update(1 / 60)
|
|
box:update(1 / 60)
|
|
eq(box.scrollPx, nil, "and it ends after four steps")
|
|
end
|
|
|
|
-- ------------------------------------------------------------- ShaderFX
|
|
do
|
|
love.filesystem.write = function() return true end
|
|
love.graphics.getRendererInfo = function() return "OpenGL", "4.1 stub", "stub", "stub" end
|
|
love.graphics.getSupported = function() return { glsl3 = true } end
|
|
love.graphics.flushBatch = function() end
|
|
love.graphics.validateShader = function() return true end
|
|
love.timer = love.timer or {}
|
|
love.timer.getDelta = love.timer.getDelta or function() return 1 / 60 end
|
|
local sends = {}
|
|
love.graphics.newShader = function(frag, vert)
|
|
local sh = { frag = frag, vert = vert }
|
|
function sh.send(self, name, value) sends[#sends + 1] = name end
|
|
return sh
|
|
end
|
|
local quads = 0
|
|
love.graphics.newQuad = function(x, y, w, h, sw, sh)
|
|
quads = quads + 1
|
|
local q = { x = x, y = y, w = w, h = h }
|
|
function q.setViewport(self, nx, ny, nw, nh) self.x, self.y, self.w, self.h = nx, ny, nw, nh end
|
|
return q
|
|
end
|
|
local ShaderFX = require("src.render.ShaderFX")
|
|
local ok = ShaderFX.activate("main", { name = "lcd3x.slangp",
|
|
fullPath = "tests/data/shaderfx/gl/lcd3x.slangp", converted = true })
|
|
eq(ok, true, "lcd3x activates under the stub")
|
|
local canvas = setmetatable({ w = 800, h = 720 }, { __index = {
|
|
getWidth = function(self) return self.w end,
|
|
getHeight = function(self) return self.h end,
|
|
getPixelWidth = function(self) return self.w end,
|
|
getPixelHeight = function(self) return self.h end,
|
|
} })
|
|
local rect = { x = 0, y = 0, w = 800, h = 720, scale = 5 }
|
|
ShaderFX.render(canvas, rect, { w = 160, h = 144 }, 1, 1)
|
|
local first = #sends
|
|
check(first > 0, "the first frame sends the pass uniforms")
|
|
ShaderFX.render(canvas, rect, { w = 160, h = 144 }, 1, 1)
|
|
eq(#sends, first, "an identical frame re-sends nothing")
|
|
eq(quads, 1, "the crop quad is re-aimed, not rebuilt")
|
|
ShaderFX.render(canvas, { x = 0, y = 0, w = 640, h = 576, scale = 4 },
|
|
{ w = 160, h = 144 }, 1, 1)
|
|
check(#sends > first, "a size change re-sends what changed")
|
|
ShaderFX.deactivate("main")
|
|
end
|
|
|
|
T.finish("render_hotpath_caches")
|