Files
gen1recomp/tests/game3_se_source_cache_test.lua
T
Shane McGovern 795276935c Performance pass: cut per-frame allocations and draw calls, fix frame-rate and batch bugs
- 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
2026-09-30 14:53:18 +00:00

133 lines
4.5 KiB
Lua

#!/usr/bin/env luajit
-- Gen 3 Audio.playSe caches the static Source per (id, pan, gain, mono,
-- loop): a repeat SE neither re-bakes nor rebuilds SoundData / Source, the
-- player-slot bookkeeping (_seSources/_seByPlayer/_seMeta) behaves as with a
-- fresh Source, and anything baked into the PCM (pan, volume) still misses.
-- luajit tests/game3_se_source_cache_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("game3 se source cache")
local check, eq = S.check, S.eq
love = require("tests.love_stub")
local built, sources = 0, 0
local Source = {}
Source.__index = Source
function Source:play() self.playing = true; self.plays = self.plays + 1 end
function Source:stop() self.playing = false; self.stops = self.stops + 1 end
function Source:isPlaying() return self.playing end
function Source:setVolume(v) self.volume = v end
function Source:getVolume() return self.volume or 1 end
function Source:setLooping(v) self.looping = v end
function Source:clone()
sources = sources + 1
return setmetatable({ sd = self.sd, plays = 0, stops = 0, clone = true },
Source)
end
love.audio = love.audio or {}
love.audio.newSource = function(sd)
sources = sources + 1
return setmetatable({ sd = sd, plays = 0, stops = 0 }, Source)
end
local realNewSoundData = love.sound.newSoundData
love.sound.newSoundData = function(...)
built = built + 1
return realNewSoundData(...)
end
local Player = require("src.core.game3.m4a_player")
local bakes = 0
Player.songInfo = function(_, id)
return { kind = "se", player = (id == 7) and 2 or 1 }
end
Player.start = function() return true end
Player.bakeSlot = function()
bakes = bakes + 1
local L, R = {}, {}
for i = 1, 64 do L[i] = math.sin(i) * 0.5; R[i] = math.cos(i) * 0.5 end
return L, R
end
local Audio = require("src.core.game3.audio")
Audio._ready = true
Audio._pack = {}
Audio._cache = {}
Audio._seRawClear()
local function live(id)
local n = 0
for _, src in ipairs(Audio._seSources) do
local meta = Audio._seMeta[src]
if meta and meta.id == id then n = n + 1 end
end
return n
end
check(Audio.playSe(5), "first play")
local first = Audio._seByPlayer[1]
eq(bakes, 1, "first play bakes")
eq(sources, 1, "first play builds one Source")
check(first and first.playing, "first play is sounding")
check(Audio.playSe(5), "repeat play")
eq(bakes, 1, "repeat does not re-bake")
eq(sources, 1, "repeat reuses the cached Source")
check(Audio._seByPlayer[1] == first, "same Source on its player slot")
check(first.playing and first.plays == 2, "cached Source restarted")
eq(live(5), 1, "one live entry for the SE (the previous play was replaced)")
check(Audio._seMeta[first] and Audio._seMeta[first].id == 5
and Audio._seMeta[first].player == 1, "meta rebuilt for the replay")
-- a different SE on the same player replaces it, then the first comes back
local builtBefore = built
Audio.playSe(6)
check(not first.playing, "player 1 SE replaced stops the cached Source")
check(Audio._seMeta[first] == nil, "replaced Source forgotten")
Audio.playSe(5)
check(Audio._seByPlayer[1] == first and first.playing,
"cached Source reused after being replaced")
eq(built - builtBefore, 1, "only the new SE built SoundData")
-- PCM-baked parameters miss the cache
local n = sources
Audio.playSe(5, { pan = 63 })
eq(sources, n + 1, "a different pan builds its own Source")
Audio.playSe(5, { pan = 63 })
eq(sources, n + 1, "and that one is cached too")
Audio.playSe(5, { volume = 0.5 })
eq(sources, n + 2, "a different volume builds its own Source")
-- stopSe still stops and forgets it
Audio.playSe(5)
local cached = Audio._seByPlayer[1]
Audio.stopSe(5)
check(not cached.playing and Audio._seMeta[cached] == nil
and Audio._seByPlayer[1] == nil, "stopSe stops and forgets a cached Source")
-- a cached Source still tracked live (not replaced) is cloned, not cut off
Audio._seSources[#Audio._seSources + 1] = cached
Audio._seMeta[cached] = { id = 5, player = 3 }
cached.playing = true
Audio.playSe(5)
local now = Audio._seByPlayer[1]
check(now ~= cached and now.clone and now.playing,
"live cached Source is cloned for the new play")
check(cached.playing, "the live one keeps playing")
Audio.stopSe()
-- a re-install / session end drops the cache with the raw memo
Audio._seRawClear()
n = sources
Audio.playSe(5)
eq(sources, n + 1, "cache cleared with the raw memo")
eq(bakes, 3, "and the SE is baked again (5 and 6 before)")
-- non-memoable plays (explicit loop/maxSec) never touch the cache
n = sources
Audio.playSe(5, { maxSec = 1 })
Audio.playSe(5, { maxSec = 1 })
eq(sources, n + 2, "maxSec plays build fresh Sources")
S.finish()