mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-03 08:07:07 -04:00
performance enhancements 1 and decoration names
This commit is contained in:
@@ -21,6 +21,8 @@ end
|
||||
-- version session (save editor or game). GPU release runs before soft
|
||||
-- invalidate via installLoader(nil).
|
||||
function SessionLifecycle.endMountedSession(version)
|
||||
local stream = package.loaded["src.core.game3.asset_stream"]
|
||||
if stream then stream.shutdown() end
|
||||
local Assets = require("src.render.Assets")
|
||||
if Assets.releaseSession then Assets.releaseSession() end
|
||||
if version then
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
-- CPU-only preparation shared by the render thread and the asset worker.
|
||||
local Pack = require("src.import.gba.native_pack")
|
||||
local Palette = require("src.core.game3.palette")
|
||||
local OwExtract = require("src.import.gba.ow_extract")
|
||||
local D = {}
|
||||
|
||||
function D.imageData(rgba, w, h)
|
||||
if not (love and love.image) then return nil, "love.image unavailable" end
|
||||
local ok, data = pcall(love.image.newImageData, w, h, "rgba8", rgba)
|
||||
if ok then return data end
|
||||
data = love.image.newImageData(w, h)
|
||||
local i = 1
|
||||
for y = 0, h - 1 do
|
||||
for x = 0, w - 1 do
|
||||
data:setPixel(x, y, (rgba:byte(i) or 0) / 255, (rgba:byte(i + 1) or 0) / 255,
|
||||
(rgba:byte(i + 2) or 0) / 255, (rgba:byte(i + 3) or 0) / 255)
|
||||
i = i + 4
|
||||
end
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
function D.pair(cache, root, pair)
|
||||
local path = root .. "/" .. pair
|
||||
local blob, pals = cache:read(path .. "/mids.idx"), cache:read(path .. "/palettes.bin")
|
||||
if not blob or not pals then return nil, "missing native blobs for " .. pair end
|
||||
local idx, err = Pack.decodeIdx(blob)
|
||||
if not idx then return nil, err end
|
||||
local rgb, bgr = Palette.load(pals)
|
||||
if not rgb then return nil, bgr end
|
||||
local overBlob = cache:read(path .. "/mids_over.idx")
|
||||
local out = { pair = pair, cols = idx.atlasCols, rows = idx.atlasRows, midCount = idx.midCount,
|
||||
idxBlob = blob, bgr = bgr, midToSlot = {}, layers = {} }
|
||||
for i, mid in ipairs(idx.midIds) do out.midToSlot[mid] = i - 1 end
|
||||
local function layer(key, dataKey, indexed, source, tag, transparent)
|
||||
local hash = Palette.hash(bgr, source)
|
||||
local rel = path .. "/atlas_" .. tag .. "_" .. hash .. ".rgba"
|
||||
local w, h = indexed.atlasCols * 16, indexed.atlasRows * 16
|
||||
local rgba = cache:read(rel)
|
||||
if not rgba or #rgba ~= w * h * 4 then
|
||||
rgba = Pack.bakeRgba(indexed, rgb, { transparentZero = transparent, cancelled = cache.cancelled })
|
||||
if not rgba then return nil, "asset preparation cancelled" end
|
||||
if cache.write then pcall(cache.write, cache, rel, rgba) end
|
||||
end
|
||||
local data, why = D.imageData(rgba, w, h)
|
||||
if not data then return nil, why end
|
||||
out.layers[#out.layers + 1] = { imageKey = key, dataKey = dataKey, data = data, bytes = w * h * 4 }
|
||||
return true
|
||||
end
|
||||
local ok, why = layer("image", "imageData", idx, blob, overBlob and "u" or "flat", false)
|
||||
if not ok then return nil, why end
|
||||
local over = overBlob and Pack.decodeIdx(overBlob)
|
||||
if over then
|
||||
ok, why = layer("overImage", "overImageData", over, overBlob, "o", true)
|
||||
if not ok then return nil, why end
|
||||
out.overBlob, out.layered = overBlob, true
|
||||
end
|
||||
if overBlob then
|
||||
local midBlob = cache:read(path .. "/mids_mid.idx")
|
||||
local mid = midBlob and Pack.decodeIdx(midBlob)
|
||||
if mid then
|
||||
ok, why = layer("midImage", "midImageData", mid, midBlob, "m", true)
|
||||
if not ok then return nil, why end
|
||||
end
|
||||
end
|
||||
out.animFiles = {}
|
||||
local manifestRel = path .. "/anim_manifest.lua"
|
||||
local manifestBlob = cache:read(manifestRel)
|
||||
out.animFiles[manifestRel] = manifestBlob
|
||||
local chunk = manifestBlob and load(manifestBlob, "@anim_manifest.lua", "t", {})
|
||||
local man = chunk and chunk()
|
||||
if man then
|
||||
local function read(file)
|
||||
if file then out.animFiles[path .. "/" .. file] = cache:read(path .. "/" .. file) end
|
||||
end
|
||||
out.animFiles[path .. "/palettes.bin"] = pals
|
||||
for _, row in ipairs(man.banks or {}) do read(row.file); read(row.overFile) end
|
||||
for _, kind in ipairs({ "water", "sand", "flower" }) do
|
||||
if man[kind] then read("anim_" .. kind .. ".rgba") end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function D.sprite(cache, root, gid)
|
||||
local metaBlob, rgba = cache:read(root .. "/" .. gid .. ".meta"), cache:read(root .. "/" .. gid .. ".rgba")
|
||||
local meta = metaBlob and OwExtract.decodeMeta(metaBlob)
|
||||
if not meta or not rgba then return nil, "missing OW sheet " .. tostring(gid) end
|
||||
local w, h, n = meta.width, meta.height, meta.frameCount
|
||||
if w < 1 or h < 1 or n < 1 or w > 256 or h > 256 or n > 512 then return nil, "bad OW dimensions" end
|
||||
local bytes = w * h * n * 4
|
||||
if #rgba ~= bytes then
|
||||
if #rgba < w * h * 4 then return nil, "truncated OW sheet" end
|
||||
rgba = rgba:sub(1, bytes) .. string.rep("\0", math.max(0, bytes - #rgba))
|
||||
end
|
||||
local data, err = D.imageData(rgba, w, h * n)
|
||||
if not data then return nil, err end
|
||||
meta.imageData, meta.bytes = data, bytes
|
||||
return meta
|
||||
end
|
||||
|
||||
-- Snapshots never consult a later GameVersion, cache prefix, or mod mount.
|
||||
function D.cache(spec, cancelSignal)
|
||||
local cache = { cancelled = cancelSignal and function() return cancelSignal:getCount() > 0 end or nil }
|
||||
local function checkCancel()
|
||||
if cache.cancelled and cache.cancelled() then error("asset preparation cancelled") end
|
||||
end
|
||||
local function path(rel) return (spec.prefix or "") .. rel end
|
||||
function cache:read(rel)
|
||||
checkCancel()
|
||||
if spec.directory then
|
||||
local f = io.open(spec.directory .. "/" .. path(rel), "rb")
|
||||
if not f then return nil end
|
||||
local bytes = f:read("*a"); f:close(); return bytes
|
||||
end
|
||||
return love.filesystem.read(path(rel))
|
||||
end
|
||||
function cache:write(rel, bytes)
|
||||
checkCancel()
|
||||
-- No directory creation or mounting in a worker; derived caches are optional.
|
||||
if spec.directory then
|
||||
local f = io.open(spec.directory .. "/" .. path(rel), "wb")
|
||||
if not f then return false end
|
||||
f:write(bytes); f:close(); return true
|
||||
end
|
||||
return love.filesystem.write(path(rel), bytes)
|
||||
end
|
||||
return cache
|
||||
end
|
||||
|
||||
return D
|
||||
@@ -0,0 +1,189 @@
|
||||
-- One CPU worker, bounded decoded residency, and a shared upload budget.
|
||||
local Decode = require("src.core.game3.asset_decode")
|
||||
local Stream = {}
|
||||
Stream.UPLOAD_BUDGET = .002
|
||||
Stream.MAX_DECODED = 4
|
||||
Stream.MAX_PENDING = 64
|
||||
local queue, jobs, serial = {}, {}, 0
|
||||
local thread, input, output, active
|
||||
local function cancel(job)
|
||||
job.cancelled = true
|
||||
if job.cancelSignal then job.cancelSignal:push(true) end
|
||||
end
|
||||
local clock = function() return love and love.timer and love.timer.getTime() or os.clock() end
|
||||
local function record(job, phase, seconds, route)
|
||||
if Stream.onTiming then Stream.onTiming(job.kind, job.key, phase, seconds, route) end
|
||||
if os.getenv("POKEPORT_ASSET_PROFILE") == "1" then
|
||||
print(string.format("[game3/asset] %s=%s %s=%.3fms route=%s", job.kind, job.key, phase, seconds * 1000, route))
|
||||
end
|
||||
end
|
||||
local function startWorker()
|
||||
if thread then return true end
|
||||
if not (love and love.thread and love.thread.newThread and love.thread.newChannel) then return false end
|
||||
local ok, value = pcall(love.thread.newThread, "src/core/game3/asset_worker.lua")
|
||||
if not ok then return false end
|
||||
input, output = love.thread.newChannel(), love.thread.newChannel()
|
||||
local started = pcall(value.start, value, input, output)
|
||||
if not started then return false end
|
||||
thread = value
|
||||
return true
|
||||
end
|
||||
function Stream.shutdown()
|
||||
Stream.cancelPending()
|
||||
if thread then
|
||||
input:clear(); input:push({ stop = true }); thread:wait()
|
||||
end
|
||||
thread, input, output, active = nil, nil, nil, nil
|
||||
queue, jobs = {}, {}
|
||||
Stream._uploadedThisFrame = false
|
||||
Stream.workerFailed = false
|
||||
end
|
||||
function Stream.cancelPending()
|
||||
for _, job in pairs(jobs) do
|
||||
cancel(job)
|
||||
job.owner.pending[job.key] = nil
|
||||
end
|
||||
jobs = {}
|
||||
end
|
||||
function Stream.frameComplete() Stream._uploadedThisFrame = false end
|
||||
local Lifecycle = require("src.core.SessionLifecycle")
|
||||
if not Lifecycle._game3AssetShutdownRegistered then
|
||||
Lifecycle._game3AssetShutdownRegistered = true
|
||||
Lifecycle.registerProcessShutdown(function()
|
||||
local current = package.loaded["src.core.game3.asset_stream"]
|
||||
if current then current.shutdown() end
|
||||
end)
|
||||
end
|
||||
|
||||
function Stream.new(kind, cache, root, upload, publish)
|
||||
local s = { kind = kind, cache = cache, root = root, upload = upload, publish = publish, pending = {} }
|
||||
function s:cancel()
|
||||
self.cancelled = true
|
||||
for _, job in pairs(self.pending) do cancel(job); jobs[job.id] = nil end
|
||||
self.pending = {}
|
||||
end
|
||||
function s:decoded(key)
|
||||
local job = self.pending[key]
|
||||
return job and job.data ~= nil
|
||||
end
|
||||
function s:retain(wanted)
|
||||
for key, job in pairs(self.pending) do
|
||||
if not wanted[key] then
|
||||
cancel(job); jobs[job.id], self.pending[key] = nil, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
function s:prefetch(key, priority)
|
||||
if self.cancelled then return end
|
||||
if self.pending[key] then self.pending[key].priority = priority or self.pending[key].priority; return end
|
||||
local count = 0
|
||||
for _ in pairs(self.pending) do count = count + 1 end
|
||||
if count >= Stream.MAX_PENDING then return end
|
||||
serial = serial + 1
|
||||
local spec = self.cache and self.cache.assetWorkerSpec and self.cache:assetWorkerSpec(root, kind, key)
|
||||
local job = { id = serial, kind = kind, key = key, root = root, owner = self, spec = spec, priority = priority or 1 }
|
||||
self.pending[key], jobs[serial] = job, job
|
||||
queue[#queue + 1] = job
|
||||
end
|
||||
function s:get(key)
|
||||
Stream.poll()
|
||||
local job = self.pending[key]
|
||||
local data = job and job.data
|
||||
if not data then
|
||||
if job and job.cancelSignal then job.cancelSignal:push(true) end
|
||||
local start = clock()
|
||||
local err
|
||||
data, err = Decode[kind](cache, root, key)
|
||||
record({ kind = kind, key = key }, "decode", clock() - start, "sync")
|
||||
if not data then return nil, err end
|
||||
end
|
||||
local start = clock()
|
||||
local co = job and job.co or coroutine.create(function() return upload(data) end)
|
||||
local value
|
||||
repeat
|
||||
local ok, res = coroutine.resume(co)
|
||||
if not ok then
|
||||
if job then job.cancelled = true; jobs[job.id] = nil; self.pending[key] = nil end
|
||||
return nil, tostring(res)
|
||||
end
|
||||
value = res
|
||||
until coroutine.status(co) == "dead"
|
||||
if job then job.cancelled = true; jobs[job.id] = nil; self.pending[key] = nil end
|
||||
publish(key, value)
|
||||
record({ kind = kind, key = key }, "upload", clock() - start, data == (job and job.data) and "decoded" or "sync")
|
||||
return value
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
function Stream.poll()
|
||||
if output then
|
||||
local result = output:pop()
|
||||
if result then
|
||||
if result.fatal then
|
||||
if active then active.spec, active.sent = nil, false end
|
||||
Stream.workerFailed, active = true, nil
|
||||
print("[game3/asset] worker unavailable: " .. tostring(result.fatal))
|
||||
end
|
||||
local job = jobs[result.id]
|
||||
if active and active.id == result.id then active = nil end
|
||||
if job and not job.cancelled then
|
||||
job.data, job.error, job.done = result.data, result.error, true
|
||||
record(job, "decode", result.seconds, "worker")
|
||||
end
|
||||
end
|
||||
end
|
||||
if thread and thread:getError() then
|
||||
if active then active.spec, active.sent = nil, false end
|
||||
thread, input, output, active = nil, nil, nil, nil
|
||||
Stream.workerFailed = true
|
||||
end
|
||||
if active then return end
|
||||
local decoded = 0
|
||||
for _, job in ipairs(queue) do if not job.cancelled and job.data then decoded = decoded + 1 end end
|
||||
if decoded >= Stream.MAX_DECODED then return end
|
||||
local nextJob
|
||||
for _, job in ipairs(queue) do
|
||||
if not job.cancelled and not job.done and job ~= active and not job.sent then
|
||||
if job.spec and (not nextJob or job.priority < nextJob.priority) then nextJob = job end
|
||||
end
|
||||
end
|
||||
if nextJob and not Stream.workerFailed and startWorker() then
|
||||
nextJob.sent, active = true, nextJob
|
||||
nextJob.cancelSignal = love.thread.newChannel()
|
||||
input:push({ id = nextJob.id, kind = nextJob.kind, key = nextJob.key, root = nextJob.root,
|
||||
spec = nextJob.spec, cancelSignal = nextJob.cancelSignal })
|
||||
end
|
||||
-- Unknown/custom caches prepare synchronously only when explicitly consumed.
|
||||
end
|
||||
|
||||
function Stream.update()
|
||||
Stream.poll()
|
||||
if Stream._uploadedThisFrame then return end
|
||||
local start = clock()
|
||||
for i = #queue, 1, -1 do if queue[i].cancelled then table.remove(queue, i) end end
|
||||
local nextJob
|
||||
for _, job in ipairs(queue) do
|
||||
if job.data and (not nextJob or job.priority < nextJob.priority) then nextJob = job end
|
||||
end
|
||||
local job = nextJob
|
||||
if job then
|
||||
Stream._uploadedThisFrame = true
|
||||
if not job.co then job.co = coroutine.create(function() return job.owner.upload(job.data) end) end
|
||||
local t = clock()
|
||||
local ok, value = coroutine.resume(job.co)
|
||||
if not ok then
|
||||
job.error, job.done, job.data = tostring(value), true, nil
|
||||
job.co = nil
|
||||
elseif coroutine.status(job.co) == "dead" then
|
||||
job.owner.publish(job.key, value)
|
||||
job.owner.pending[job.key], jobs[job.id], job.cancelled = nil, nil, true
|
||||
end
|
||||
record(job, "upload_step", clock() - t, "warm")
|
||||
-- A texture upload (including animation catch-up on publication) is
|
||||
-- atomic. One coroutine slice per render frame prevents catch-up ticks
|
||||
-- and a second ready asset from silently exceeding the upload budget.
|
||||
if clock() - start > Stream.UPLOAD_BUDGET then Stream.lastOverBudget = clock() - start end
|
||||
end
|
||||
end
|
||||
return Stream
|
||||
@@ -0,0 +1,22 @@
|
||||
local input, output = ...
|
||||
local running, fatal = pcall(function()
|
||||
require("love.filesystem")
|
||||
require("love.image")
|
||||
require("love.data")
|
||||
require("love.timer")
|
||||
table.insert(package.searchers or package.loaders, 1, function(name)
|
||||
local path = name:gsub("%.", "/") .. ".lua"
|
||||
if love.filesystem.getInfo(path) then return assert(love.filesystem.load(path)) end
|
||||
end)
|
||||
local D = require("src.core.game3.asset_decode")
|
||||
while true do
|
||||
local job = input:demand()
|
||||
if job.stop then break end
|
||||
local start = love.timer.getTime()
|
||||
local ok, data, err = pcall(D[job.kind], D.cache(job.spec, job.cancelSignal), job.root, job.key)
|
||||
output:push({ id = job.id, data = ok and data or nil, error = ok and err or tostring(data),
|
||||
seconds = love.timer.getTime() - start })
|
||||
end
|
||||
|
||||
end)
|
||||
if not running then output:push({ fatal = tostring(fatal) }) end
|
||||
@@ -65,6 +65,16 @@ end
|
||||
|
||||
local function loveCache()
|
||||
return {
|
||||
assetWorkerSpec = function(_, root, kind, key)
|
||||
local rel = root .. "/" .. tostring(key) .. (kind == "pair" and "/mids.idx" or ".meta")
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local prefix = require("src.core.GameVersion").cachePrefix()
|
||||
-- Only a version-qualified cache can be read independently of mounted
|
||||
-- overlays. Overrides/custom readers retain the synchronous fallback.
|
||||
if Dataset.cacheRootOverride or os.getenv("POKEPORT_GBA_CACHE") then return nil end
|
||||
if not CacheFs.existsAt(prefix .. rel) then return nil end
|
||||
return { prefix = prefix, directory = CacheFs.root() }
|
||||
end,
|
||||
read = function(_, rel)
|
||||
local ok, CacheFs = pcall(require, "src.import.CacheFs")
|
||||
if ok and CacheFs and CacheFs.readActive then
|
||||
|
||||
@@ -231,13 +231,16 @@ local function drawFieldPlane(game, vw, vh, Renderer)
|
||||
local transitioning = Transition and Transition.isActive and Transition.isActive()
|
||||
Oam.resetFrame()
|
||||
local prev = Oam.setLayer("world")
|
||||
local function exchange(current, replacement)
|
||||
return Renderer and Renderer:exchangeWorldCanvas(current, replacement)
|
||||
end
|
||||
if Tilt.active() and not transitioning and Renderer and Renderer.beginUprightPass then
|
||||
FieldView.draw(game, vw, vh, { skipActors = true })
|
||||
FieldView.draw(game, vw, vh, { skipActors = true, exchangeCanvas = exchange })
|
||||
Renderer:beginUprightPass()
|
||||
FieldView.draw(game, vw, vh, { actorsOnly = true, billboard = true })
|
||||
Renderer:endUprightPass()
|
||||
else
|
||||
FieldView.draw(game, vw, vh)
|
||||
FieldView.draw(game, vw, vh, { exchangeCanvas = exchange })
|
||||
end
|
||||
Oam.setLayer(prev)
|
||||
Oam.animateSprites("world")
|
||||
@@ -439,6 +442,8 @@ end
|
||||
Display.presentFlat = presentFlat
|
||||
|
||||
function Display.present(game, winW, winH)
|
||||
local Stream = package.loaded["src.core.game3.asset_stream"]
|
||||
if Stream then Stream.frameComplete() end
|
||||
if not Display.planesBroken then
|
||||
local ok, err = pcall(presentPlanes, game)
|
||||
if ok then return true end
|
||||
|
||||
@@ -147,6 +147,8 @@ function Field.start(mod, game, session)
|
||||
end
|
||||
|
||||
function Field.stop()
|
||||
local Stream = package.loaded["src.core.game3.asset_stream"]
|
||||
if Stream then Stream.cancelPending() end
|
||||
lazyReq("src.world.game3.Follower").reset()
|
||||
Field.running = false
|
||||
Field._session = nil
|
||||
@@ -213,9 +215,9 @@ function Field.update(_dt)
|
||||
local Ghosts = lazyReq("src.core.game3.ghosts")
|
||||
Ghosts.sync()
|
||||
Ghosts.update(game)
|
||||
-- pre-load tileset pairs queued by a seamless world refresh, one per frame
|
||||
-- Prepare nearby assets on a worker; share a main-thread texture budget.
|
||||
local MapMod = package.loaded["src.core.game3.map"]
|
||||
if MapMod and MapMod._warmQueue and MapMod.stepWarm then MapMod.stepWarm() end
|
||||
if MapMod and MapMod.stepWarm then MapMod.stepWarm(game) end
|
||||
|
||||
Field.pollMapChange(game)
|
||||
-- pokefirered/src/safari_zone.c:60 CB2_EndSafariBattle
|
||||
|
||||
@@ -1780,7 +1780,7 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
end
|
||||
local FieldWeather = modFieldWeather()
|
||||
if FieldWeather and FieldWeather.draw then
|
||||
FieldWeather.draw(camX, camY, canvasW, canvasH)
|
||||
FieldWeather.draw(camX, camY, canvasW, canvasH, opts.exchangeCanvas)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1833,10 +1833,21 @@ function FieldView.invalidate()
|
||||
if OwSprites and OwSprites.invalidate then
|
||||
OwSprites.invalidate()
|
||||
end
|
||||
local WeatherRse = package.loaded["src.core.game3.field_weather_rse"]
|
||||
if WeatherRse and WeatherRse.invalidate then WeatherRse.invalidate() end
|
||||
local FieldEffects = modFieldEffects()
|
||||
if FieldEffects and FieldEffects.invalidate then
|
||||
FieldEffects.invalidate()
|
||||
end
|
||||
end
|
||||
|
||||
local Assets = require("src.render.Assets")
|
||||
if Assets.register and not Assets._game3FieldInvalidatorRegistered then
|
||||
Assets._game3FieldInvalidatorRegistered = true
|
||||
Assets.register(function()
|
||||
local current = package.loaded["src.core.game3.field_view"]
|
||||
if current then current.invalidate() end
|
||||
end)
|
||||
end
|
||||
|
||||
return FieldView
|
||||
|
||||
@@ -130,11 +130,11 @@ function FieldWeather.drawBelow(camX, camY, canvasW, canvasH)
|
||||
if E then E.drawBelow(camX or 0, camY or 0, canvasW or W, canvasH or H) end
|
||||
end
|
||||
|
||||
function FieldWeather.draw(camX, camY, canvasW, canvasH)
|
||||
function FieldWeather.draw(camX, camY, canvasW, canvasH, exchangeCanvas)
|
||||
if Weather.isSuspended() then return end
|
||||
local E = Weather.rseEngine()
|
||||
if E then
|
||||
E.draw(camX or 0, camY or 0, canvasW or W, canvasH or H)
|
||||
E.draw(camX or 0, camY or 0, canvasW or W, canvasH or H, exchangeCanvas)
|
||||
return
|
||||
end
|
||||
local w = FieldWeather._current
|
||||
|
||||
@@ -1931,6 +1931,39 @@ local function drawTiled(sheet, frame, baseX, baseY, ox, oy, cw, ch)
|
||||
local sy = oy + (baseY % 64) - 64
|
||||
while sx > -64 do sx = sx - 64 end
|
||||
while sy > -64 do sy = sy - 64 end
|
||||
if love.graphics.newSpriteBatch then
|
||||
local r, g, b, a = love.graphics.getColor()
|
||||
local bucket = r == 1 and g == 1 and b == 1 and a == 1 and "tiledBase" or "tiledTint"
|
||||
local store = sheet[bucket] or {}; sheet[bucket] = store
|
||||
local key = table.concat({ frame, sx, sy, cw, ch, r, g, b, a }, ":")
|
||||
if store.key ~= key then
|
||||
local q = sheetQuad(sheet, frame, 8, 8)
|
||||
local count = (math.floor((cw - sx) / 64) + 1) * (math.floor((ch - sy) / 64) + 1)
|
||||
count = count * (type(q) == "table" and #q or 1)
|
||||
if not store.batch or store.capacity < count then
|
||||
store.batch = love.graphics.newSpriteBatch(sheet.image, count, "dynamic")
|
||||
store.capacity = count
|
||||
end
|
||||
local batch = store.batch
|
||||
batch:clear()
|
||||
-- Image draws store their colour as bytes in the vertex buffer. Use
|
||||
-- the same byte conversion here, including fractional EVA/16, instead
|
||||
-- of applying a float constant colour to the finished SpriteBatch.
|
||||
batch:setColor(r, g, b, a)
|
||||
for y = sy, ch, 64 do
|
||||
for x = sx, cw, 64 do
|
||||
if type(q) == "table" then
|
||||
for _, part in ipairs(q) do batch:add(part[1], x + part[2], y + part[3]) end
|
||||
else batch:add(q, x, y) end
|
||||
end
|
||||
end
|
||||
store.key = key
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(store.batch)
|
||||
love.graphics.setColor(r, g, b, a)
|
||||
return
|
||||
end
|
||||
for y = sy, ch, 64 do
|
||||
for x = sx, cw, 64 do
|
||||
drawSheetFrame(sheet, frame, 8, 8, x, y)
|
||||
@@ -1988,17 +2021,40 @@ local function drawPriority1(A, ox, oy, cw, ch)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/field_weather.c:459 ApplyColorMap
|
||||
local function colorMapPass(A, cw, ch)
|
||||
local function colorMapPass(A, cw, ch, exchangeCanvas)
|
||||
if S.colorMapIndex == 0 or not A.shader then return end
|
||||
local cur = love.graphics.getCanvas()
|
||||
if not cur then return end
|
||||
cw, ch = cur:getWidth(), cur:getHeight()
|
||||
local tmp = R._tmpCanvas
|
||||
if not tmp or tmp:getWidth() ~= cw or tmp:getHeight() ~= ch then
|
||||
tmp = love.graphics.newCanvas(cw, ch)
|
||||
if tmp and tmp.release then tmp:release() end
|
||||
tmp = love.graphics.newCanvas(cw, ch, { dpiscale = cur.getDPIScale and cur:getDPIScale() or 1,
|
||||
format = cur.getFormat and cur:getFormat() or "normal" })
|
||||
tmp:setFilter("nearest", "nearest")
|
||||
R._tmpCanvas = tmp
|
||||
end
|
||||
if exchangeCanvas and not love.graphics.getScissor() and exchangeCanvas(cur, tmp) then
|
||||
local shader = love.graphics.getShader()
|
||||
local blend, alpha = love.graphics.getBlendMode()
|
||||
local r, g, b, a = love.graphics.getColor()
|
||||
-- A transform-only push leaves ownership of the new target with the
|
||||
-- caller. push("all") would restore the old canvas and switch twice more.
|
||||
love.graphics.push()
|
||||
love.graphics.origin()
|
||||
love.graphics.setCanvas(tmp)
|
||||
love.graphics.setBlendMode("replace", "premultiplied")
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
colorMapUniforms(A.shader, COLOR_MAP_DARK_CONTRAST)
|
||||
love.graphics.setShader(A.shader)
|
||||
love.graphics.draw(cur, 0, 0)
|
||||
love.graphics.pop()
|
||||
love.graphics.setShader(shader)
|
||||
love.graphics.setBlendMode(blend, alpha)
|
||||
love.graphics.setColor(r, g, b, a)
|
||||
R._tmpCanvas = cur
|
||||
return tmp
|
||||
end
|
||||
love.graphics.push("all")
|
||||
love.graphics.origin()
|
||||
love.graphics.setShader()
|
||||
@@ -2028,7 +2084,7 @@ function R.drawBelow(camX, camY, canvasW, canvasH)
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
function R.draw(camX, camY, canvasW, canvasH)
|
||||
function R.draw(camX, camY, canvasW, canvasH, exchangeCanvas)
|
||||
R.setCamera(camX, camY)
|
||||
if not S.started or not S.initialized then return end
|
||||
local A = loadAssets()
|
||||
@@ -2041,7 +2097,9 @@ function R.draw(camX, camY, canvasW, canvasH)
|
||||
if R._belowFrame ~= S.frames then
|
||||
drawPriority2(A, camX, camY, ox, oy, canvasW, canvasH)
|
||||
end
|
||||
colorMapPass(A, canvasW, canvasH)
|
||||
love.graphics.pop()
|
||||
colorMapPass(A, canvasW, canvasH, exchangeCanvas)
|
||||
love.graphics.push("all")
|
||||
drawPriority1(A, ox, oy, canvasW, canvasH)
|
||||
love.graphics.pop()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
+93
-6
@@ -156,7 +156,7 @@ function Map.refreshWorld(game, reachW, reachH, rootId)
|
||||
local queue, seen, entries = {}, {}, {}
|
||||
for _, entry in ipairs(Map.world) do
|
||||
local pair = entry.def and (entry.def.pair or (entry.def.midLayout and entry.def.midLayout.pair))
|
||||
if sync and pair and Map.warmNear(entry, x0, y0, x1, y1) then
|
||||
if sync and pair and not NativeTileset.prefetch and Map.warmNear(entry, x0, y0, x1, y1) then
|
||||
if NativeTileset.ready(pair) then pcall(NativeTileset.get, pair) end
|
||||
elseif type(pair) == "string" and not (NativeTileset._pairs and NativeTileset._pairs[pair]) then
|
||||
if not seen[pair] then
|
||||
@@ -170,6 +170,12 @@ function Map.refreshWorld(game, reachW, reachH, rootId)
|
||||
end
|
||||
Map._warmQueue = queue[1] and queue or nil
|
||||
Map._warmEntries = queue[1] and entries or nil
|
||||
if NativeTileset._stream then
|
||||
local root = maps[rootId]
|
||||
local pair = root and (root.pair or (root.midLayout and root.midLayout.pair))
|
||||
if pair then seen[pair] = true end
|
||||
NativeTileset._stream:retain(seen)
|
||||
end
|
||||
end
|
||||
Map._worldRoot = rootId
|
||||
Map._worldReachW = reachW
|
||||
@@ -190,10 +196,27 @@ function Map.warmNow(game, rootId)
|
||||
local vw = (FieldView and FieldView._viewW) or (Display and Display.W) or 240
|
||||
local vh = (FieldView and FieldView._viewH) or (Display and Display.H) or 160
|
||||
Map._warmPairs = nil
|
||||
return Map.refreshWorld(game, math.ceil(vw / 16), math.ceil(vh / 16), rootId)
|
||||
local world = Map.refreshWorld(game, math.ceil(vw / 16), math.ceil(vh / 16), rootId)
|
||||
local Native = package.loaded["src.core.game3.tileset_native"]
|
||||
local def = host_map_def(game, rootId)
|
||||
local pair = def and (def.pair or (def.midLayout and def.midLayout.pair))
|
||||
if Native and pair and Native.ready(pair) then pcall(Native.get, pair) end
|
||||
local Ow = package.loaded["src.core.game3.ow_sprites"]
|
||||
if Ow and Ow.get and Ow.playerGraphicsId then
|
||||
local gid = Ow.playerGraphicsId(game)
|
||||
if gid then pcall(Ow.get, gid) end
|
||||
local Objects = package.loaded["src.core.game3.objects"]
|
||||
local x0, y0, x1, y1 = Map.warmRect()
|
||||
for _, eo in ipairs(Objects and Objects.forDraw and Objects.forDraw() or {}) do
|
||||
local x, y = eo.cellX or 0, eo.cellY or 0
|
||||
if eo.graphicsId and (not x0 or (x >= x0 and x <= x1 and y >= y0 and y <= y1)) then pcall(Ow.get, eo.graphicsId) end
|
||||
end
|
||||
end
|
||||
Map.stepWarm(game)
|
||||
return world
|
||||
end
|
||||
|
||||
function Map.warmRect()
|
||||
function Map.warmRect(margin)
|
||||
local P = package.loaded["src.core.game3.player"]
|
||||
local px, py = P and tonumber(P.cellX), P and tonumber(P.cellY)
|
||||
if not (px and py) then return nil end
|
||||
@@ -202,8 +225,9 @@ function Map.warmRect()
|
||||
local vw = (FieldView and FieldView._viewW) or (Display and Display.W) or 240
|
||||
local vh = (FieldView and FieldView._viewH) or (Display and Display.H) or 160
|
||||
local cols, rows = math.ceil(vw / 16), math.ceil(vh / 16)
|
||||
local mx = math.ceil(cols / 2) + 1 + cols * Map.WARM_MARGIN_SCREENS
|
||||
local my = math.ceil(rows / 2) + 1 + rows * Map.WARM_MARGIN_SCREENS
|
||||
margin = margin or Map.WARM_MARGIN_SCREENS
|
||||
local mx = math.ceil(cols / 2) + 1 + cols * margin
|
||||
local my = math.ceil(rows / 2) + 1 + rows * margin
|
||||
return px - mx, py - my, px + mx, py + my
|
||||
end
|
||||
|
||||
@@ -216,7 +240,70 @@ function Map.warmNear(entry, x0, y0, x1, y1)
|
||||
return ox + w > x0 and ox <= x1 and oy + h > y0 and oy <= y1
|
||||
end
|
||||
|
||||
function Map.stepWarm()
|
||||
function Map.stepWarm(game)
|
||||
local Stream = package.loaded["src.core.game3.asset_stream"]
|
||||
local Native = package.loaded["src.core.game3.tileset_native"]
|
||||
if Native and Native.prefetch then
|
||||
local x0, y0, x1, y1 = Map.warmRect()
|
||||
Map._warmScanTick = ((Map._warmScanTick or 0) + 1) % 4
|
||||
if Map._warmScanTick ~= 0 and Map._warmScanWorld == Map.world
|
||||
and Map._warmScanX == x0 and Map._warmScanY == y0
|
||||
and Map._warmScanX1 == x1 and Map._warmScanY1 == y1 then
|
||||
if Stream then Stream.update() end
|
||||
return true
|
||||
end
|
||||
Map._warmScanWorld = Map.world
|
||||
Map._warmScanX, Map._warmScanY, Map._warmScanX1, Map._warmScanY1 = x0, y0, x1, y1
|
||||
local vx0, vy0, vx1, vy1 = Map.warmRect(0)
|
||||
local wantedPairs = {}
|
||||
local priorities = {}
|
||||
local def = Map.currentDef()
|
||||
local currentPair = def and (def.pair or (def.midLayout and def.midLayout.pair))
|
||||
if currentPair then wantedPairs[currentPair], priorities[currentPair] = true, 0 end
|
||||
local queue = Map._warmQueue or {}
|
||||
for i = #queue, 1, -1 do
|
||||
local pair = queue[i]
|
||||
local near = not (Map._warmEntries and Map._warmEntries[pair])
|
||||
for _, entry in ipairs(Map._warmEntries and Map._warmEntries[pair] or {}) do
|
||||
if Map.warmNear(entry, x0, y0, x1, y1) then near = true end
|
||||
if Map.warmNear(entry, vx0, vy0, vx1, vy1) then priorities[pair] = 0 end
|
||||
end
|
||||
if Native._pairs[pair] then table.remove(queue, i)
|
||||
elseif near then wantedPairs[pair] = true end
|
||||
end
|
||||
if not queue[1] then Map._warmQueue = nil end
|
||||
if Native._stream then Native._stream:retain(wantedPairs) end
|
||||
for pair in pairs(wantedPairs) do Native.prefetch(pair, priorities[pair] or 1) end
|
||||
local Ow = package.loaded["src.core.game3.ow_sprites"]
|
||||
if game and Ow and Ow.prefetch then
|
||||
local wanted = {}
|
||||
local function actor(eo, ox, oy)
|
||||
if not (eo.visible and not eo.hidden and not eo.invisible) then return end
|
||||
local x, y = (eo.cellX or 0) + (ox or 0), (eo.cellY or 0) + (oy or 0)
|
||||
if not x0 or (x >= x0 and x <= x1 and y >= y0 and y <= y1) then
|
||||
local gid = tonumber(eo.graphicsId)
|
||||
if gid then
|
||||
local visible = not vx0 or (x >= vx0 and x <= vx1 and y >= vy0 and y <= vy1)
|
||||
wanted[gid] = math.min(wanted[gid] or 1, visible and 0 or 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
local Obj = package.loaded["src.core.game3.objects"]
|
||||
for _, eo in ipairs(Obj and Obj.forDraw and Obj.forDraw() or {}) do actor(eo) end
|
||||
local Ghosts = package.loaded["src.core.game3.ghosts"]
|
||||
for _, entry in ipairs(Map.world or {}) do
|
||||
if Map.warmNear(entry, x0, y0, x1, y1) then
|
||||
for _, eo in ipairs(Ghosts and Ghosts.forDraw(entry.id) or {}) do actor(eo, entry.ox, entry.oy) end
|
||||
end
|
||||
end
|
||||
local gid = Ow.playerGraphicsId(game)
|
||||
if gid then wanted[gid] = 0 end
|
||||
if Ow._stream then Ow._stream:retain(wanted) end
|
||||
for id, priority in pairs(wanted) do Ow.prefetch(id, priority) end
|
||||
end
|
||||
if Stream then Stream.update() end
|
||||
return true
|
||||
end
|
||||
local queue = Map._warmQueue
|
||||
if not queue then return false end
|
||||
local NativeTileset = package.loaded["src.core.game3.tileset_native"]
|
||||
|
||||
@@ -4,7 +4,13 @@ local Extract = require("src.import.gba.extract_island1")
|
||||
local OwExtract = require("src.import.gba.ow_extract")
|
||||
local Versions = require("src.import.gba.versions")
|
||||
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local OwSprites = {}
|
||||
local makeStream
|
||||
local function resetStream()
|
||||
if OwSprites._stream then OwSprites._stream:cancel() end
|
||||
OwSprites._stream = OwSprites._cache and makeStream() or nil
|
||||
end
|
||||
|
||||
OwSprites._cache = nil
|
||||
OwSprites._manifest = nil
|
||||
@@ -34,22 +40,35 @@ local function owRoot()
|
||||
return (Extract.CACHE_ROOT or "data/generated/gba") .. "/ow"
|
||||
end
|
||||
|
||||
local function loadManifest()
|
||||
OwSprites._manifestAttempted = true
|
||||
local src = OwSprites._cache and OwSprites._cache:read(owRoot() .. "/manifest.lua")
|
||||
local chunk = src and load(src, "@ow/manifest.lua", "t", {})
|
||||
OwSprites._manifest = chunk and chunk() or nil
|
||||
end
|
||||
|
||||
local function resetPalettes(changed)
|
||||
if changed then OwSprites._overrides = {} end
|
||||
for _, o in pairs(OwSprites._overrides or {}) do o.spr = nil end
|
||||
end
|
||||
|
||||
function OwSprites.install(cache)
|
||||
resetPalettes(OwSprites._cache ~= cache or OwSprites._root ~= owRoot())
|
||||
OwSprites._root = owRoot()
|
||||
OwSprites._cache = cache
|
||||
OwSprites._loaded = {}
|
||||
resetStream()
|
||||
OwSprites._reflectionLoaded = {}
|
||||
OwSprites._manifest = nil
|
||||
OwSprites._logged = false
|
||||
if not cache then return end
|
||||
local src = cache:read(owRoot() .. "/manifest.lua")
|
||||
if src then
|
||||
local chunk = load(src, "@ow/manifest.lua", "t", {})
|
||||
if chunk then OwSprites._manifest = chunk() end
|
||||
end
|
||||
loadManifest()
|
||||
end
|
||||
|
||||
function OwSprites.invalidate()
|
||||
resetPalettes(false)
|
||||
OwSprites._manifestAttempted = false
|
||||
OwSprites._loaded = {}
|
||||
resetStream()
|
||||
OwSprites._reflectionLoaded = {}
|
||||
OwSprites._manifest = nil
|
||||
OwSprites._logged = false
|
||||
@@ -62,69 +81,33 @@ function OwSprites.ready()
|
||||
return OwExtract.ready(cache, Extract.CACHE_ROOT or "data/generated/gba")
|
||||
end
|
||||
|
||||
local function load_one(gid)
|
||||
local cache = OwSprites._cache
|
||||
if not cache then return nil end
|
||||
local root = owRoot()
|
||||
local metaBlob = cache:read(root .. "/" .. gid .. ".meta")
|
||||
local rgba = cache:read(root .. "/" .. gid .. ".rgba")
|
||||
if not metaBlob or not rgba then return nil end
|
||||
local meta = OwExtract.decodeMeta(metaBlob)
|
||||
if not meta then return nil end
|
||||
local function uploadSprite(meta)
|
||||
local image = love.graphics.newImage(meta.imageData)
|
||||
image:setFilter("nearest", "nearest")
|
||||
coroutine.yield("texture")
|
||||
local w, h, n = meta.width, meta.height, meta.frameCount
|
||||
-- decodeMeta reads these as u16 without validating, so a corrupt .meta can
|
||||
-- carry 65535 for any of them and `aw * ah` then reaches ~4.3e9 pixels on a
|
||||
-- cache file in the user-writable save directory. Bound them before sizing.
|
||||
local MAX_FRAME_DIM, MAX_FRAMES = 256, 512
|
||||
if type(w) ~= "number" or type(h) ~= "number" or type(n) ~= "number"
|
||||
or w < 1 or h < 1 or n < 1
|
||||
or w > MAX_FRAME_DIM or h > MAX_FRAME_DIM or n > MAX_FRAMES then
|
||||
return nil
|
||||
end
|
||||
local aw, ah = w, h * n
|
||||
if #rgba ~= aw * ah * 4 then
|
||||
if #rgba < aw * h * 4 then return nil end
|
||||
end
|
||||
if not (love and love.image and love.graphics) then return nil end
|
||||
|
||||
local ok, imageData = pcall(love.image.newImageData, aw, ah, "rgba8", rgba)
|
||||
if not ok or not imageData then
|
||||
imageData = love.image.newImageData(aw, ah)
|
||||
local i = 1
|
||||
for y = 0, ah - 1 do
|
||||
for x = 0, aw - 1 do
|
||||
local r = (rgba:byte(i) or 0) / 255
|
||||
local g = (rgba:byte(i + 1) or 0) / 255
|
||||
local b = (rgba:byte(i + 2) or 0) / 255
|
||||
local a = (rgba:byte(i + 3) or 0) / 255
|
||||
imageData:setPixel(x, y, r, g, b, a)
|
||||
i = i + 4
|
||||
end
|
||||
end
|
||||
end
|
||||
local image = love.graphics.newImage(imageData)
|
||||
if image.setFilter then image:setFilter("nearest", "nearest") end
|
||||
local quads = {}
|
||||
for fi = 0, n - 1 do
|
||||
quads[fi] = love.graphics.newQuad(0, fi * h, w, h, aw, ah)
|
||||
quads[fi] = love.graphics.newQuad(0, fi * h, w, h, w, h * n)
|
||||
if fi % 32 == 31 then coroutine.yield("quads") end
|
||||
end
|
||||
return {
|
||||
image = image,
|
||||
-- Kept so setObjectPalette can recolour the sheet: LOVE 11 has no
|
||||
-- Image:newImageData, so the decoded pixels are the only CPU-side source.
|
||||
imageData = imageData,
|
||||
quads = quads,
|
||||
width = w,
|
||||
height = h,
|
||||
frameCount = n,
|
||||
inanimate = meta.inanimate,
|
||||
paletteTag = meta.paletteTag,
|
||||
reflectionPaletteTag = meta.reflectionPaletteTag,
|
||||
reflectionPaletteMappedTag = meta.reflectionPaletteMappedTag,
|
||||
palette = meta.palette,
|
||||
reflectionPalette = meta.reflectionPalette,
|
||||
mappedReflectionPalette = meta.mappedReflectionPalette,
|
||||
}
|
||||
meta.image, meta.quads = image, quads
|
||||
meta.bytes = nil
|
||||
return meta
|
||||
end
|
||||
makeStream = function()
|
||||
return Stream.new("sprite", OwSprites._cache, owRoot(), uploadSprite, function(gid, spr)
|
||||
OwSprites._loaded[gid] = spr
|
||||
if not OwSprites._logged then
|
||||
print(string.format("[game3/ow] sprites ready (%s sheets)", tostring(OwSprites._manifest and OwSprites._manifest.count or "?")))
|
||||
OwSprites._logged = true
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function OwSprites.prefetch(gid, priority)
|
||||
gid = tonumber(gid)
|
||||
if gid and not OwSprites._loaded[gid] and OwSprites._stream then OwSprites._stream:prefetch(gid, priority) end
|
||||
end
|
||||
|
||||
function OwSprites.get(graphicsId)
|
||||
@@ -132,20 +115,8 @@ function OwSprites.get(graphicsId)
|
||||
if graphicsId == nil then return nil end
|
||||
local cached = OwSprites._loaded[graphicsId]
|
||||
if cached then return cached end
|
||||
if not OwSprites._manifest then
|
||||
OwSprites.install(OwSprites._cache)
|
||||
end
|
||||
local spr = load_one(graphicsId)
|
||||
if spr then
|
||||
OwSprites._loaded[graphicsId] = spr
|
||||
if not OwSprites._logged then
|
||||
local n = OwSprites._manifest and OwSprites._manifest.count
|
||||
print(string.format("[game3/ow] FRLG sprites ready (%s sheets)",
|
||||
tostring(n or "?")))
|
||||
OwSprites._logged = true
|
||||
end
|
||||
end
|
||||
return spr
|
||||
if not OwSprites._manifestAttempted then loadManifest() end
|
||||
return OwSprites._stream and OwSprites._stream:get(graphicsId) or nil
|
||||
end
|
||||
|
||||
-- src/data/object_events/object_event_anims.h:633
|
||||
@@ -325,7 +296,7 @@ function OwSprites.setObjectPalette(graphicsId, key, colours, sourceColours)
|
||||
graphicsId = tonumber(graphicsId)
|
||||
if graphicsId == nil or type(key) ~= "string" then return false end
|
||||
local current = OwSprites._overrides[graphicsId]
|
||||
if current and current.key == key then return true end
|
||||
if current and current.key == key and current.spr then return true end
|
||||
if type(colours) ~= "table" or #colours == 0 then return false end
|
||||
|
||||
local base = OwSprites.get(graphicsId)
|
||||
@@ -337,7 +308,7 @@ function OwSprites.setObjectPalette(graphicsId, key, colours, sourceColours)
|
||||
end
|
||||
local spr = recolour_sprite(base, from, colours)
|
||||
if not spr then return false end
|
||||
OwSprites._overrides[graphicsId] = { key = key, spr = spr }
|
||||
OwSprites._overrides[graphicsId] = { key = key, spr = spr, colours = colours, sourceColours = from }
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -426,6 +397,10 @@ function OwSprites.getDraw(graphicsId)
|
||||
graphicsId = tonumber(graphicsId)
|
||||
if graphicsId == nil then return nil end
|
||||
local ov = OwSprites._overrides and OwSprites._overrides[graphicsId]
|
||||
if ov and not ov.spr then
|
||||
OwSprites.setObjectPalette(graphicsId, ov.key, ov.colours, ov.sourceColours)
|
||||
ov = OwSprites._overrides[graphicsId]
|
||||
end
|
||||
if ov and ov.spr then return ov.spr end
|
||||
return OwSprites.get(graphicsId)
|
||||
end
|
||||
@@ -493,9 +468,7 @@ function OwSprites.draw(graphicsId, px, py, camX, camY, facing, walkPhase, stepF
|
||||
end
|
||||
|
||||
function OwSprites.avatars()
|
||||
if not OwSprites._manifest and OwSprites._cache then
|
||||
OwSprites.install(OwSprites._cache)
|
||||
end
|
||||
if not OwSprites._manifestAttempted and OwSprites._cache then loadManifest() end
|
||||
local m = OwSprites._manifest
|
||||
return m and m.avatars or nil
|
||||
end
|
||||
|
||||
@@ -27,6 +27,17 @@ local function stdString(id)
|
||||
end
|
||||
Adapters.stdString = stdString
|
||||
|
||||
-- pokeemerald/src/scrcmd.c:1599 StringCopy(..., gDecorations[decorId].name)
|
||||
local function decorationName(src)
|
||||
local ok, DecorInv = pcall(require, "src.core.game3.rse.decoration_inventory")
|
||||
if not ok or type(DecorInv) ~= "table" or not DecorInv.info then return nil end
|
||||
local okInfo, info = pcall(DecorInv.info, src)
|
||||
if okInfo and type(info) == "table" and type(info.name) == "string" and info.name ~= "" then
|
||||
return info.name
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/event_object_movement.c:5208 GetOppositeDirection
|
||||
local OPPOSITE_DIR = { down = "up", up = "down", left = "right", right = "left" }
|
||||
|
||||
@@ -161,6 +172,9 @@ function Adapters.stub(opts)
|
||||
if op == "bufferstdstring" then
|
||||
return stdString(src)
|
||||
end
|
||||
if op == "bufferdecorationname" then
|
||||
return decorationName(src)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
a.leadMonName = opts.leadMonName or function() return "POKéMON" end
|
||||
@@ -1527,6 +1541,9 @@ function Adapters.host(mod, game, world)
|
||||
if op == "bufferstdstring" then
|
||||
return stdString(src) or tostring(src)
|
||||
end
|
||||
if op == "bufferdecorationname" then
|
||||
return decorationName(src)
|
||||
end
|
||||
if op == "bufferpartymonnick" then
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local party = Runtime and Runtime.session and Runtime.session.party
|
||||
|
||||
@@ -295,11 +295,11 @@ local function load_bank(cache, pair, kind, info)
|
||||
end
|
||||
|
||||
-- pokefirered/src/tileset_anims.c:223
|
||||
function TilesetAnim.bindPair(pair, atlas)
|
||||
function TilesetAnim.bindPair(pair, atlas, prepared)
|
||||
if not Versions.NATIVE_RENDER or Versions.TILESET_ANIM == false then
|
||||
return false
|
||||
end
|
||||
local cache = TilesetAnim._cache
|
||||
local cache = prepared and { read = function(_, rel) return prepared[rel] end } or TilesetAnim._cache
|
||||
if not cache or not pair or not atlas then return false end
|
||||
local entry = TilesetAnim._pairs[pair]
|
||||
if entry == false then return false end
|
||||
@@ -326,9 +326,13 @@ function TilesetAnim.bindPair(pair, atlas)
|
||||
end
|
||||
TilesetAnim._visible[pair] = true
|
||||
if entry.rse then return true end
|
||||
TilesetAnim._applyKind(entry, "water", TilesetAnim._waterFrame)
|
||||
TilesetAnim._applyKind(entry, "sand", TilesetAnim._sandFrame)
|
||||
TilesetAnim._applyKind(entry, "flower", TilesetAnim._flowerFrame)
|
||||
local dirty = false
|
||||
if TilesetAnim._applyKind(entry, "water", TilesetAnim._waterFrame, true) then dirty = true end
|
||||
if TilesetAnim._applyKind(entry, "sand", TilesetAnim._sandFrame, true) then dirty = true end
|
||||
if TilesetAnim._applyKind(entry, "flower", TilesetAnim._flowerFrame, true) then dirty = true end
|
||||
-- Initial/catch-up preparation uploads once after all CPU patches. This
|
||||
-- also keeps a warming pair's final step within one texture submission.
|
||||
if dirty then NativeTileset().flush(atlas, true, false) end
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -370,7 +374,7 @@ local function get_frame_piece(bank, frame, mi, srcOff)
|
||||
return nil
|
||||
end
|
||||
|
||||
function TilesetAnim._applyKind(entry, kind, frame)
|
||||
function TilesetAnim._applyKind(entry, kind, frame, deferUpload)
|
||||
local bank = entry.banks[kind]
|
||||
if not bank or bank.frames < 1 then return end
|
||||
frame = frame % bank.frames
|
||||
@@ -411,6 +415,7 @@ function TilesetAnim._applyKind(entry, kind, frame)
|
||||
end
|
||||
end
|
||||
end
|
||||
if deferUpload then return true end
|
||||
if partial then
|
||||
NativeTileset().flush(ts, true, false)
|
||||
elseif ts.image and love and love.graphics then
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
local Extract = require("src.import.gba.extract_island1")
|
||||
local NativePack = require("src.import.gba.native_pack")
|
||||
local Palette = require("src.core.game3.palette")
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local Versions = require("src.import.gba.versions")
|
||||
|
||||
local NativeTileset = {}
|
||||
@@ -13,7 +13,12 @@ NativeTileset._cache = nil
|
||||
NativeTileset._logged = {}
|
||||
NativeTileset._ready = {}
|
||||
|
||||
local NATIVE = Extract.NATIVE_ROOT or (Extract.CACHE_ROOT .. "/native")
|
||||
local makeStream
|
||||
local function nativeRoot() return Extract.NATIVE_ROOT or (Extract.CACHE_ROOT .. "/native") end
|
||||
local function resetStream()
|
||||
if NativeTileset._stream then NativeTileset._stream:cancel() end
|
||||
NativeTileset._stream = NativeTileset._cache and makeStream() or nil
|
||||
end
|
||||
|
||||
local function log(msg)
|
||||
print("[game3/native] " .. tostring(msg))
|
||||
@@ -24,6 +29,7 @@ function NativeTileset.install(cache, _bundle)
|
||||
NativeTileset._pairs = {}
|
||||
NativeTileset._logged = {}
|
||||
NativeTileset._ready = {}
|
||||
resetStream()
|
||||
local okA, TilesetAnim = pcall(require, "src.core.game3.tileset_anim")
|
||||
if okA and TilesetAnim and TilesetAnim.install then
|
||||
TilesetAnim.install(cache)
|
||||
@@ -34,134 +40,21 @@ function NativeTileset.invalidate()
|
||||
NativeTileset._pairs = {}
|
||||
NativeTileset._logged = {}
|
||||
NativeTileset._ready = {}
|
||||
resetStream()
|
||||
local okA, TilesetAnim = pcall(require, "src.core.game3.tileset_anim")
|
||||
if okA and TilesetAnim and TilesetAnim.invalidate then
|
||||
TilesetAnim.invalidate()
|
||||
end
|
||||
end
|
||||
|
||||
local function rgba_to_image(rgba, w, h)
|
||||
if not (love and love.image and love.image.newImageData) then
|
||||
return nil, nil, "love.image unavailable"
|
||||
end
|
||||
local ok, imageData = pcall(love.image.newImageData, w, h, "rgba8", rgba)
|
||||
if not ok or not imageData then
|
||||
ok, imageData = pcall(function()
|
||||
local id = love.image.newImageData(w, h)
|
||||
if id.setString then
|
||||
id:setString(rgba)
|
||||
end
|
||||
return id
|
||||
end)
|
||||
end
|
||||
if not ok or not imageData then
|
||||
imageData = love.image.newImageData(w, h)
|
||||
local i = 1
|
||||
for y = 0, h - 1 do
|
||||
for x = 0, w - 1 do
|
||||
local r = rgba:byte(i) or 0
|
||||
local g = rgba:byte(i + 1) or 0
|
||||
local b = rgba:byte(i + 2) or 0
|
||||
local a = rgba:byte(i + 3) or 255
|
||||
imageData:setPixel(x, y, r / 255, g / 255, b / 255, a / 255)
|
||||
i = i + 4
|
||||
end
|
||||
end
|
||||
end
|
||||
local image = love.graphics.newImage(imageData)
|
||||
if image.setFilter then image:setFilter("nearest", "nearest") end
|
||||
return image, imageData
|
||||
end
|
||||
|
||||
local function bake_or_load(cache, pair, idxTbl, rgb, hash, tag, transparentZero)
|
||||
local w = (idxTbl.atlasCols or 16) * 16
|
||||
local h = (idxTbl.atlasRows or 1) * 16
|
||||
local rgbaRel = NATIVE .. "/" .. pair .. "/atlas_" .. tag .. "_" .. hash .. ".rgba"
|
||||
local rgba = cache:read(rgbaRel)
|
||||
if not rgba or #rgba ~= w * h * 4 then
|
||||
rgba, w, h = NativePack.bakeRgba(idxTbl, rgb, { transparentZero = transparentZero })
|
||||
pcall(function() cache:write(rgbaRel, rgba) end)
|
||||
end
|
||||
return rgba_to_image(rgba, w, h)
|
||||
end
|
||||
|
||||
local function load_pair(cache, pair)
|
||||
local idxBlob = cache:read(NATIVE .. "/" .. pair .. "/mids.idx")
|
||||
local palBlob = cache:read(NATIVE .. "/" .. pair .. "/palettes.bin")
|
||||
if not idxBlob or not palBlob then
|
||||
return nil, "missing native blobs for " .. tostring(pair)
|
||||
end
|
||||
local idxTbl, ierr = NativePack.decodeIdx(idxBlob)
|
||||
if not idxTbl then return nil, ierr end
|
||||
local rgb, bgr = Palette.load(palBlob)
|
||||
if not rgb then return nil, bgr end
|
||||
|
||||
local hash = Palette.hash(bgr, idxBlob)
|
||||
local layered = cache:exists(NATIVE .. "/" .. pair .. "/mids_over.idx")
|
||||
local tag = layered and "u" or "flat"
|
||||
local image, imageData, err = bake_or_load(cache, pair, idxTbl, rgb, hash, tag, false)
|
||||
if not image then return nil, err or "bake under failed" end
|
||||
|
||||
local midToSlot = {}
|
||||
for i, mid in ipairs(idxTbl.midIds or {}) do
|
||||
midToSlot[mid] = i - 1
|
||||
end
|
||||
|
||||
local cols = idxTbl.atlasCols or 16
|
||||
if (idxTbl.atlasRows or 0) * 16 > 2048 then
|
||||
log("WARNING atlas rows large for " .. pair)
|
||||
end
|
||||
|
||||
local ts = {
|
||||
pair = pair,
|
||||
image = image,
|
||||
imageData = imageData,
|
||||
midToSlot = midToSlot,
|
||||
cols = cols,
|
||||
rows = idxTbl.atlasRows or 1,
|
||||
midCount = idxTbl.midCount or 0,
|
||||
quads = {},
|
||||
layered = false,
|
||||
overImage = nil,
|
||||
overImageData = nil,
|
||||
overQuads = {},
|
||||
idxBlob = idxBlob,
|
||||
overBlob = nil,
|
||||
bgr = bgr,
|
||||
slotPix = {},
|
||||
}
|
||||
|
||||
if layered then
|
||||
local overBlob = cache:read(NATIVE .. "/" .. pair .. "/mids_over.idx")
|
||||
local overTbl = overBlob and NativePack.decodeIdx(overBlob)
|
||||
if overTbl then
|
||||
local overHash = Palette.hash(bgr, overBlob)
|
||||
local oImg, oData = bake_or_load(cache, pair, overTbl, rgb, overHash, "o", true)
|
||||
if oImg then
|
||||
ts.layered = true
|
||||
ts.overImage = oImg
|
||||
ts.overImageData = oData
|
||||
ts.overBlob = overBlob
|
||||
end
|
||||
end
|
||||
local midBlob = cache:read(NATIVE .. "/" .. pair .. "/mids_mid.idx")
|
||||
local midTbl = midBlob and NativePack.decodeIdx(midBlob)
|
||||
if midTbl then
|
||||
ts.midImage = bake_or_load(cache, pair, midTbl, rgb, Palette.hash(bgr, midBlob), "m", true)
|
||||
end
|
||||
end
|
||||
|
||||
return ts
|
||||
end
|
||||
|
||||
function NativeTileset.ready(pair)
|
||||
if not Versions.NATIVE_RENDER then return false end
|
||||
if not pair then return false end
|
||||
if NativeTileset._pairs[pair] or NativeTileset._ready[pair] then return true end
|
||||
local cache = NativeTileset._cache
|
||||
if not cache then return false end
|
||||
local ok = cache:exists(NATIVE .. "/" .. pair .. "/mids.idx")
|
||||
and cache:exists(NATIVE .. "/" .. pair .. "/palettes.bin")
|
||||
local ok = cache:exists(nativeRoot() .. "/" .. pair .. "/mids.idx")
|
||||
and cache:exists(nativeRoot() .. "/" .. pair .. "/palettes.bin")
|
||||
if ok then NativeTileset._ready[pair] = true end
|
||||
return ok
|
||||
end
|
||||
@@ -175,40 +68,55 @@ local function tilesetAnim()
|
||||
return animMod or nil
|
||||
end
|
||||
|
||||
local function bind_anim(pair, atlas)
|
||||
local function bind_anim(pair, atlas, prepared)
|
||||
local TilesetAnim = tilesetAnim()
|
||||
if TilesetAnim and TilesetAnim.bindPair then
|
||||
TilesetAnim.bindPair(pair, atlas)
|
||||
TilesetAnim.bindPair(pair, atlas, prepared)
|
||||
end
|
||||
end
|
||||
|
||||
local function uploadPair(data)
|
||||
local ts = { pair = data.pair, midToSlot = data.midToSlot, cols = data.cols, rows = data.rows,
|
||||
midCount = data.midCount, layered = data.layered or false, idxBlob = data.idxBlob,
|
||||
overBlob = data.overBlob, bgr = data.bgr, quads = {}, overQuads = {}, slotPix = {}, preparedAnim = data.animFiles }
|
||||
for _, layer in ipairs(data.layers) do
|
||||
local image = love.graphics.newImage(layer.data)
|
||||
image:setFilter("nearest", "nearest")
|
||||
ts[layer.imageKey], ts[layer.dataKey] = image, layer.data
|
||||
coroutine.yield("texture")
|
||||
end
|
||||
return ts
|
||||
end
|
||||
makeStream = function()
|
||||
return Stream.new("pair", NativeTileset._cache, nativeRoot(), uploadPair, function(pair, ts)
|
||||
NativeTileset._pairs[pair] = ts
|
||||
bind_anim(pair, ts, ts.preparedAnim)
|
||||
ts.preparedAnim = nil
|
||||
if not NativeTileset._logged[pair] then
|
||||
log(string.format("atlas ready pair=%s mids=%d %dx%d layered=%s", pair,
|
||||
ts.midCount, ts.cols * 16, ts.rows * 16, tostring(ts.layered)))
|
||||
NativeTileset._logged[pair] = true
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function NativeTileset.prefetch(pair, priority)
|
||||
if type(pair) == "string" and not NativeTileset._pairs[pair] and NativeTileset._stream then
|
||||
NativeTileset._stream:prefetch(pair, priority)
|
||||
end
|
||||
end
|
||||
|
||||
function NativeTileset.get(pair)
|
||||
if not pair then return nil end
|
||||
local cached = NativeTileset._pairs[pair]
|
||||
if cached then
|
||||
-- Cheap for a bound pair (no manifest read): marks it visible and
|
||||
-- catches its atlas up to the current anim frames before a draw.
|
||||
-- FieldView reads cached atlases directly and does not come here per frame.
|
||||
bind_anim(pair, cached)
|
||||
return cached
|
||||
end
|
||||
local cache = NativeTileset._cache
|
||||
if not cache then return nil end
|
||||
local ts, err = load_pair(cache, pair)
|
||||
if not ts then
|
||||
if not NativeTileset._logged[pair] then
|
||||
log("load failed " .. tostring(pair) .. ": " .. tostring(err))
|
||||
NativeTileset._logged[pair] = true
|
||||
end
|
||||
return nil
|
||||
end
|
||||
NativeTileset._pairs[pair] = ts
|
||||
if not NativeTileset._logged[pair] then
|
||||
log(string.format("atlas ready pair=%s mids=%d %dx%d layered=%s",
|
||||
pair, ts.midCount, ts.cols * 16, ts.rows * 16, tostring(ts.layered)))
|
||||
if cached then bind_anim(pair, cached); return cached end
|
||||
local stream = NativeTileset._stream
|
||||
if not stream then return nil end
|
||||
local ts, err = stream:get(pair)
|
||||
if not ts and not NativeTileset._logged[pair] then
|
||||
log("load failed " .. tostring(pair) .. ": " .. tostring(err))
|
||||
NativeTileset._logged[pair] = true
|
||||
end
|
||||
bind_anim(pair, ts)
|
||||
return ts
|
||||
end
|
||||
|
||||
|
||||
@@ -235,6 +235,7 @@ function NativePack.bakeRgba(idxTbl, rgbPals, opts)
|
||||
local pixels = idxTbl.pixels or {}
|
||||
local rowChunks = {}
|
||||
for ay = 0, h - 1 do
|
||||
if ay % 16 == 0 and opts.cancelled and opts.cancelled() then return nil, w, h end
|
||||
local midRow = math.floor(ay / 16)
|
||||
local py = ay % 16
|
||||
local line = {}
|
||||
|
||||
@@ -496,6 +496,15 @@ function Renderer:endWorldPass()
|
||||
love.graphics.setCanvas(self.canvas)
|
||||
end
|
||||
|
||||
-- A field compositor may consume the current world into another compatible
|
||||
-- target. Its caller continues drawing into the replacement until endWorldPass.
|
||||
function Renderer:exchangeWorldCanvas(current, replacement)
|
||||
if self.worldCanvas ~= current or not self.worldActive then return false end
|
||||
if current:getWidth() ~= replacement:getWidth() or current:getHeight() ~= replacement:getHeight() then return false end
|
||||
self.worldCanvas = replacement
|
||||
return true
|
||||
end
|
||||
|
||||
-- Tilt mode's upright pass: standing things (sprites, tall-grass feet
|
||||
-- overdraw, screen-anchored FX) draw here instead of into the ground
|
||||
-- world canvas, each already projected to its ground anchor and colorized
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local function stats(v)
|
||||
table.sort(v)
|
||||
return string.format("n=%d p99=%.3f max=%.3f", #v, v[math.max(1, math.ceil(#v * .99))] or 0, v[#v] or 0)
|
||||
end
|
||||
return function(game)
|
||||
for _ = 1, 900 do if game.boot then break end U.wait(1) end
|
||||
local Native = require("src.core.game3.tileset_native")
|
||||
local Ow = require("src.core.game3.ow_sprites")
|
||||
local Map = require("src.core.game3.map")
|
||||
local family = require("src.core.GameVersion").get()
|
||||
local records = {}
|
||||
for _, spec in ipairs({ { Native, "get", "pair" }, { Ow, "get", "sprite" }, { Map, "load", "map" } }) do
|
||||
local owner, key, label = unpack(spec)
|
||||
local fn = owner[key]
|
||||
owner[key] = function(...)
|
||||
local args = { ... }
|
||||
local t = love.timer.getTime()
|
||||
local a, b = fn(...)
|
||||
local ms = (love.timer.getTime() - t) * 1000
|
||||
if ms > 1 then print(string.format("ASSET_STALL %s id=%s ms=%.3f", label, tostring(args[label == "map" and 3 or 1]), ms)) end
|
||||
records[label] = records[label] or {}; records[label][#records[label] + 1] = ms
|
||||
return a, b
|
||||
end
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "STREAM", gender = 0 })
|
||||
U.wait(60)
|
||||
local map = family == "emerald" and "EM_ROUTE111" or "FR_CELADON_CITY"
|
||||
Map.load(nil, game, map, { x = 10, y = 10 })
|
||||
U.wait(120)
|
||||
local pairsToTest = {}
|
||||
for pair in pairs(Native._pairs) do pairsToTest[#pairsToTest + 1] = pair end
|
||||
table.sort(pairsToTest)
|
||||
local spritesToTest = {}
|
||||
for gid in pairs(Ow._loaded) do spritesToTest[#spritesToTest + 1] = gid end
|
||||
table.sort(spritesToTest)
|
||||
local base = Native._cache
|
||||
local cold = { read = function(_, rel) if rel:find("/atlas_", 1, true) then return nil end return base:read(rel) end,
|
||||
write = function() return true end, exists = function(_, rel) return base:exists(rel) end }
|
||||
local savedUpdate, savedDraw = game.update, game.draw
|
||||
game.update, game.draw = function() end, function() end
|
||||
if os.getenv("ASSET_COLD_WORKER") == "1" and Native.prefetch then
|
||||
assert(love.filesystem.getIdentity() == os.getenv("POKEPORT_IDENTITY"), "use a disposable driver identity")
|
||||
Native.install(base)
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
for _, pair in ipairs(pairsToTest) do
|
||||
local root = require("src.import.gba.extract_island1").NATIVE_ROOT
|
||||
local spec = assert(base:assetWorkerSpec(root, "pair", pair))
|
||||
assert(not spec.directory, "cold worker benchmark requires a disposable save-dir cache")
|
||||
local dir = spec.prefix .. root .. "/" .. pair
|
||||
for _, file in ipairs(love.filesystem.getDirectoryItems(dir)) do
|
||||
if file:match("^atlas_.*%.rgba$") then assert(love.filesystem.remove(dir .. "/" .. file)) end
|
||||
end
|
||||
Native.prefetch(pair)
|
||||
local deadline = love.timer.getTime() + 20
|
||||
while not Native._stream:decoded(pair) and love.timer.getTime() < deadline do Stream.poll(); U.wait(1) end
|
||||
assert(Native._stream:decoded(pair), pair)
|
||||
local data = Native._stream.pending[pair].data
|
||||
local reference = assert(require("src.core.game3.asset_decode").pair(base, root, pair))
|
||||
for i, layer in ipairs(data.layers) do
|
||||
assert(layer.data:getString() == reference.layers[i].data:getString(), "worker RGBA mismatch " .. pair)
|
||||
end
|
||||
print("PASS worker cold RGBA parity " .. pair)
|
||||
while not Native._pairs[pair] do Stream.frameComplete(); Stream.update(); U.wait(1) end
|
||||
end
|
||||
end
|
||||
for _, mode in ipairs({ "cold_rgba_cache", "disk_rgba_cache", "decoded", "gpu_warm" }) do
|
||||
local v = {}
|
||||
if mode ~= "gpu_warm" then Native.install(mode == "cold_rgba_cache" and cold or base) end
|
||||
for _, pair in ipairs(pairsToTest) do
|
||||
if mode == "decoded" and Native.prefetch then
|
||||
Native.prefetch(pair)
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local deadline = love.timer.getTime() + 20
|
||||
while not Native._stream:decoded(pair) and love.timer.getTime() < deadline do Stream.poll(); U.wait(1) end
|
||||
assert(Native._stream:decoded(pair), "worker failed to prepare " .. pair)
|
||||
end
|
||||
local t = love.timer.getTime(); assert(Native.get(pair), pair)
|
||||
local ms = (love.timer.getTime() - t) * 1000
|
||||
v[#v + 1] = ms
|
||||
print(string.format("ASSET_PAIR mode=%s id=%s ms=%.3f", mode, pair, ms))
|
||||
end
|
||||
print("ASSET_PROFILE " .. mode .. " " .. stats(v))
|
||||
end
|
||||
local spriteCache = Ow._cache
|
||||
for _, mode in ipairs({ "disk_rgba_cache", "decoded", "gpu_warm" }) do
|
||||
if mode ~= "gpu_warm" then Ow.install(spriteCache) end
|
||||
local v = {}
|
||||
for _, gid in ipairs(spritesToTest) do
|
||||
if mode == "decoded" and Ow.prefetch then
|
||||
Ow.prefetch(gid)
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local deadline = love.timer.getTime() + 20
|
||||
while not Ow._stream:decoded(gid) and love.timer.getTime() < deadline do Stream.poll(); U.wait(1) end
|
||||
assert(Ow._stream:decoded(gid), "worker failed to prepare sprite " .. gid)
|
||||
end
|
||||
local t = love.timer.getTime(); assert(Ow.get(gid), gid)
|
||||
local ms = (love.timer.getTime() - t) * 1000
|
||||
v[#v + 1] = ms
|
||||
print(string.format("ASSET_SPRITE mode=%s id=%s ms=%.3f", mode, gid, ms))
|
||||
end
|
||||
print("ASSET_PROFILE sprite_" .. mode .. " " .. stats(v))
|
||||
end
|
||||
game.update, game.draw = savedUpdate, savedDraw
|
||||
for label, v in pairs(records) do print("ASSET_PROFILE " .. label .. " " .. stats(v)) end
|
||||
print("PASS game3_asset_stream_profile")
|
||||
love.event.quit(0)
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
local run = require("tests.drivers.game3_seam_walk_stress")
|
||||
local function stats(v)
|
||||
table.sort(v)
|
||||
local sum = 0; for _, n in ipairs(v) do sum = sum + n end
|
||||
return string.format("n=%d mean=%.3f p99=%.3f max=%.3f", #v, sum / math.max(1, #v), v[math.ceil(#v * .99)] or 0, v[#v] or 0)
|
||||
end
|
||||
return function(game)
|
||||
local samples = { update = {}, draw = {}, seam = {}, pair = {}, sprite = {} }
|
||||
local function wrap(owner, key, label, onlySeams)
|
||||
local old = owner[key]
|
||||
owner[key] = function(...)
|
||||
local t = love.timer.getTime()
|
||||
local a, b, c = old(...)
|
||||
local ms = (love.timer.getTime() - t) * 1000
|
||||
local args = { ... }
|
||||
if not onlySeams or (args[4] and args[4].seamless) then
|
||||
local v = samples[label]; v[#v + 1] = ms
|
||||
if (label == "pair" or label == "sprite") and ms > 1 then
|
||||
print(string.format("SEAM_ASSET_STALL %s id=%s ms=%.3f", label, tostring(args[1]), ms))
|
||||
end
|
||||
end
|
||||
return a, b, c
|
||||
end
|
||||
end
|
||||
wrap(game, "update", "update")
|
||||
wrap(game, "draw", "draw")
|
||||
wrap(require("src.core.game3.map"), "load", "seam", true)
|
||||
wrap(require("src.core.game3.tileset_native"), "get", "pair")
|
||||
wrap(require("src.core.game3.ow_sprites"), "get", "sprite")
|
||||
run(game)
|
||||
for label, v in pairs(samples) do print("SEAM_PROFILE " .. label .. " " .. stats(v)) end
|
||||
end
|
||||
@@ -335,6 +335,9 @@ return function(game)
|
||||
patch(Field, "tryWalkIntoSign", function() return false end)
|
||||
patch(TrainerSight, "check", function() return false end)
|
||||
patch(ForcedMovement, "onStepFinished", function() return false end)
|
||||
-- Keep Cycling Road's automatic downhill steps from restarting movement
|
||||
-- during stepOnce's settling window, like the other forced paths above.
|
||||
patch(Player, "cyclingRoadPull", function() return false end)
|
||||
patch(StepEvents, "onStepTaken", function() return false end)
|
||||
patch(Encounters, "onStep", function() return nil end)
|
||||
patch(Encounters, "noteGrass", function() return nil end)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local function stats(v)
|
||||
table.sort(v)
|
||||
local sum = 0; for _, n in ipairs(v) do sum = sum + n end
|
||||
return string.format("n=%d mean=%.3f p99=%.3f max=%.3f", #v, sum / #v, v[math.ceil(#v * .99)], v[#v])
|
||||
end
|
||||
return function(game)
|
||||
for _ = 1, 900 do if game.boot then break end U.wait(1) end
|
||||
game:_handleBootAction({ action = "new_game", name = "WEATHER", gender = 0 })
|
||||
U.wait(60)
|
||||
local Map = require("src.core.game3.map")
|
||||
Map.load(nil, game, "EM_PETALBURG_CITY", { x = 17, y = 13 })
|
||||
U.wait(90)
|
||||
local R = require("src.core.game3.field_weather_rse")
|
||||
local baselinePath = os.getenv("WEATHER_BASELINE")
|
||||
local baseline = baselinePath and assert(love.filesystem.load(baselinePath))() or nil
|
||||
local Field = require("src.core.game3.field_view")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local Rng = require("src.core.game3.rng")
|
||||
local Oam = require("src.core.game3.oam")
|
||||
local failures = 0
|
||||
local function check(ok, label) print((ok and "PASS " or "FAIL ") .. label); if not ok then failures = failures + 1 end end
|
||||
local saveUpdate, saveDraw = game.update, game.draw
|
||||
game.update, game.draw = function() end, function() end
|
||||
local function weather(engine, id, frames)
|
||||
engine.restart(); Rng._value = 42
|
||||
engine.setCurrentAndNextWeatherNoDelay(id)
|
||||
for _ = 1, frames do engine.update() end
|
||||
end
|
||||
local function render(engine, w, h, swap)
|
||||
package.loaded["src.core.game3.field_weather_rse"] = engine
|
||||
Oam.resetFrame()
|
||||
local canvas = Renderer.worldCanvas
|
||||
if not canvas or canvas:getWidth() ~= w or canvas:getHeight() ~= h then
|
||||
canvas = love.graphics.newCanvas(w, h, { dpiscale = 1 })
|
||||
end
|
||||
Renderer.worldCanvas, Renderer.worldActive = canvas, true
|
||||
love.graphics.setCanvas(canvas); love.graphics.clear(.2, .3, .4, 1)
|
||||
local switches, oldSet = 0, love.graphics.setCanvas
|
||||
love.graphics.setCanvas = function(...) switches = switches + 1; return oldSet(...) end
|
||||
local t = love.timer.getTime()
|
||||
Field.draw(game, w, h, { exchangeCanvas = swap and function(cur, next) return Renderer:exchangeWorldCanvas(cur, next) end or nil })
|
||||
love.graphics.setCanvas = oldSet
|
||||
local cpu = (love.timer.getTime() - t) * 1000
|
||||
oldSet()
|
||||
local image = Renderer.worldCanvas:newImageData()
|
||||
local sync = (love.timer.getTime() - t) * 1000
|
||||
return image, cpu, sync, switches
|
||||
end
|
||||
for _, size in ipairs({ { "fit", 240, 160 }, { "survey", 960, 640 } }) do
|
||||
for _, spec in ipairs({ { "clear", 2, 180 }, { "shade", 11, 180 }, { "rain", 3, 180 },
|
||||
{ "drought", 12, 180 }, { "fog", 6, 180 }, { "sandstorm", 8, 180 }, { "sandstorm_blend", 8, 2 } }) do
|
||||
weather(R, spec[2], spec[3])
|
||||
local copy = render(R, size[2], size[3], false)
|
||||
local swapped = render(R, size[2], size[3], true)
|
||||
check(copy:getString() == swapped:getString(), size[1] .. " " .. spec[1] .. " copy vs exchange pixels")
|
||||
if baseline then
|
||||
weather(baseline, spec[2], spec[3])
|
||||
render(baseline, size[2], size[3], false)
|
||||
local original = render(baseline, size[2], size[3], false)
|
||||
if original:getString() ~= swapped:getString() then
|
||||
local a, b, count, max = original:getString(), swapped:getString(), 0, 0
|
||||
for i = 1, #a do local d = math.abs(a:byte(i) - b:byte(i)); if d > 0 then count = count + 1; max = math.max(max, d) end end
|
||||
print(string.format("WEATHER_DIFF %s %s channels=%d max=%d eva=%d evb=%d", size[1], spec[1], count, max, R.state.currBlendEVA, R.state.currBlendEVB))
|
||||
end
|
||||
check(original:getString() == swapped:getString(), size[1] .. " " .. spec[1] .. " original vs batched exchange pixels")
|
||||
end
|
||||
for _, mode in ipairs({ { "copy", baseline or R, false }, { "exchange", R, true } }) do
|
||||
weather(mode[2], spec[2], spec[3])
|
||||
local cpus, synced, switches = {}, {}, {}
|
||||
for _ = 1, 60 do
|
||||
local _, cpu, sync, count = render(mode[2], size[2], size[3], mode[3])
|
||||
cpus[#cpus + 1], synced[#synced + 1], switches[#switches + 1] = cpu, sync, count
|
||||
end
|
||||
print(string.format("WEATHER_PROFILE %s %s %s cpu %s completion_with_readback %s switches=%d", size[1], spec[1], mode[1],
|
||||
stats(cpus), stats(synced), switches[#switches]))
|
||||
end
|
||||
if os.getenv("POKEPORT_SHOT_DIR") then
|
||||
swapped:encode("png", "weather-preview.png")
|
||||
local bytes = love.filesystem.read("weather-preview.png")
|
||||
local f = assert(io.open(os.getenv("POKEPORT_SHOT_DIR") .. "/" .. size[1] .. "_" .. spec[1] .. ".png", "wb")); f:write(bytes); f:close()
|
||||
love.filesystem.remove("weather-preview.png")
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Exercise Display's actual target exchange, upright actors, and UI planes.
|
||||
local Zoom, Tilt = require("src.render.Zoom"), require("src.render.Tilt")
|
||||
local uprightCalls, beginUpright = 0, Renderer.beginUprightPass
|
||||
Renderer.beginUprightPass = function(self, ...)
|
||||
uprightCalls = uprightCalls + 1
|
||||
return beginUpright(self, ...)
|
||||
end
|
||||
Zoom.allowSurvey = true
|
||||
if baseline then
|
||||
for _, zoom in ipairs({ 0, Zoom.offsetRange(Renderer:fitScale()) }) do
|
||||
game.options.zoom = zoom; Zoom.offset = zoom
|
||||
for _, tilt in ipairs({ 0, 2 }) do
|
||||
game.options.tilt = tilt; Tilt.setLevel(tilt); Tilt.update(1)
|
||||
for _, id in ipairs({ 2, 3, 6, 11, 12, 8 }) do
|
||||
local function planes(engine)
|
||||
package.loaded["src.core.game3.field_weather_rse"] = engine
|
||||
weather(engine, id, 180)
|
||||
local before = uprightCalls
|
||||
saveDraw(game); saveDraw(game)
|
||||
if tilt > 0 then check(uprightCalls == before + 2, "upright pass rendered") end
|
||||
local out = {}
|
||||
for _, key in ipairs({ "worldCanvas", "uprightCanvas", "canvas" }) do
|
||||
local target = Renderer[key]
|
||||
if target then out[key] = target:newImageData():getString() end
|
||||
end
|
||||
return out
|
||||
end
|
||||
local a, b = planes(baseline), planes(R)
|
||||
for key, bytes in pairs(a) do
|
||||
check(bytes == b[key], string.format("display zoom=%d tilt=%d weather=%d %s pixels", zoom, tilt, id, key))
|
||||
end
|
||||
end
|
||||
if os.getenv("POKEPORT_SHOT_DIR") then
|
||||
game.draw = saveDraw
|
||||
U.still(game, os.getenv("POKEPORT_SHOT_DIR") .. string.format("/display_zoom%d_tilt%d.png", zoom, tilt))
|
||||
game.draw = function() end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Renderer.beginUprightPass = beginUpright
|
||||
package.loaded["src.core.game3.field_weather_rse"] = R
|
||||
game.update, game.draw = saveUpdate, saveDraw
|
||||
love.graphics.setCanvas()
|
||||
print((failures == 0 and "PASS" or "FAIL") .. " game3_weather_compositor_profile failures=" .. failures)
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
love = require("tests.love_stub")
|
||||
local channels, worker = {}, {}
|
||||
love.thread = {
|
||||
newChannel = function()
|
||||
local ch = { q = {} }
|
||||
function ch:push(v) self.q[#self.q + 1] = v end
|
||||
function ch:pop() return table.remove(self.q, 1) end
|
||||
function ch:clear() self.q = {} end
|
||||
channels[#channels + 1] = ch
|
||||
return ch
|
||||
end,
|
||||
newThread = function()
|
||||
function worker:start(input, output) self.input, self.output = input, output end
|
||||
function worker:getError() return self.error end
|
||||
function worker:wait() self.joined = true end
|
||||
return worker
|
||||
end,
|
||||
}
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local published, uploads = {}, 0
|
||||
local cache = { assetWorkerSpec = function() return { prefix = "emerald/" } end }
|
||||
local function owner()
|
||||
return Stream.new("pair", cache, "ROOT", function(data)
|
||||
for _ = 1, data.layers do uploads = uploads + 1; coroutine.yield("texture") end
|
||||
return data
|
||||
end, function(key, data) published[key] = data end)
|
||||
end
|
||||
local s = owner()
|
||||
s:prefetch("A"); s:prefetch("A"); Stream.poll()
|
||||
eq(#worker.input.q, 1, "duplicate requests are coalesced")
|
||||
local request = worker.input:pop()
|
||||
eq(request.spec.prefix, "emerald/", "worker receives an immutable cartridge namespace")
|
||||
eq(uploads, 0, "queuing CPU work does not touch graphics")
|
||||
worker.output:push({ id = request.id, data = { layers = 3 }, seconds = .01 })
|
||||
Stream.update()
|
||||
eq(uploads, 1, "only one texture layer uploads in an update")
|
||||
Stream.update(); eq(uploads, 1, "fixed-step catchup cannot upload another layer in the same render frame")
|
||||
Stream.frameComplete(); Stream.update(); eq(uploads, 2, "next render frame admits the next layer")
|
||||
check(published.A == nil, "partially uploaded atlases are never published")
|
||||
local result = s:get("A")
|
||||
eq(uploads, 3, "unexpected demand finishes the existing partial upload without duplicating textures")
|
||||
check(published.A == result and result.layers == 3, "fallback publishes the complete atlas")
|
||||
|
||||
s:prefetch("STALE"); Stream.poll(); request = worker.input:pop()
|
||||
s:cancel()
|
||||
eq(#request.cancelSignal.q, 1, "cache reset signals active CPU preparation to stop")
|
||||
worker.output:push({ id = request.id, data = { layers = 1 }, seconds = .01 })
|
||||
Stream.frameComplete(); Stream.update()
|
||||
check(published.STALE == nil, "cache reset rejects a late result")
|
||||
eq(uploads, 3, "stale data does not allocate a texture")
|
||||
local nextOwner = owner()
|
||||
nextOwner:prefetch("OUTSIDE"); Stream.poll(); request = worker.input:pop()
|
||||
nextOwner:retain({})
|
||||
eq(#request.cancelSignal.q, 1, "leaving the neighborhood interrupts active preparation")
|
||||
worker.output:push({ id = request.id, data = { layers = 1 }, seconds = .01 })
|
||||
Stream.frameComplete(); Stream.update()
|
||||
check(published.OUTSIDE == nil, "leaving the warm neighborhood cancels queued work")
|
||||
for i = 1, 100 do nextOwner:prefetch("LIMIT" .. i) end
|
||||
local count = 0; for _ in pairs(nextOwner.pending) do count = count + 1 end
|
||||
eq(count, Stream.MAX_PENDING, "pending assets have a residency bound")
|
||||
Stream.cancelPending()
|
||||
check(next(nextOwner.pending) == nil, "field stop cancels all pending requests")
|
||||
Stream.shutdown()
|
||||
check(worker.joined, "process teardown joins the worker before filesystem teardown")
|
||||
local priorityOwner = owner()
|
||||
priorityOwner:prefetch("NEAR", 1)
|
||||
priorityOwner:prefetch("VISIBLE", 0)
|
||||
Stream.poll(); request = worker.input:pop()
|
||||
eq(request.key, "VISIBLE", "visible assets decode before nearby assets")
|
||||
worker.output:push({ id = request.id, data = { layers = 1 }, seconds = .01 })
|
||||
Stream.poll(); request = worker.input:pop()
|
||||
eq(request.key, "NEAR", "nearby preparation follows visible preparation")
|
||||
worker.output:push({ id = request.id, data = { layers = 1 }, seconds = .01 })
|
||||
Stream.poll()
|
||||
Stream.frameComplete(); Stream.update()
|
||||
eq(coroutine.status(priorityOwner.pending.VISIBLE.co), "suspended", "visible decoded assets upload first")
|
||||
check(priorityOwner.pending.NEAR.co == nil, "nearby uploads wait for the visible upload slice")
|
||||
Stream.shutdown()
|
||||
T.finish("game3_asset_stream_test")
|
||||
@@ -0,0 +1,53 @@
|
||||
-- pokeemerald/src/scrcmd.c:1599 bufferdecorationname copies gDecorations[id].name.
|
||||
-- Lanette's givedecoration DECOR_LOTAD_DOLL (99) was printing "99" because
|
||||
-- bufferName never asked the decoration cache.
|
||||
-- luajit tests/engine/game3_bufferdecorationname_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local eq = T.eq
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local DecorInv = require("src.core.game3.rse.decoration_inventory")
|
||||
local Adapters = require("src.core.game3.scripting.adapters")
|
||||
local Vm = require("src.core.game3.scripting.vm")
|
||||
local Ops = require("src.core.game3.scripting.ops_a")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
|
||||
DecorInv.install({
|
||||
decorations = {
|
||||
[99] = { id = 99, name = "LOTAD DOLL", category = 6 },
|
||||
},
|
||||
})
|
||||
|
||||
local stub = Adapters.stub({})
|
||||
local host = Adapters.host(nil, nil, nil)
|
||||
|
||||
eq(stub.bufferName("bufferdecorationname", 99), "LOTAD DOLL", "stub names decoration 99")
|
||||
eq(host.bufferName("bufferdecorationname", 99), "LOTAD DOLL", "host names decoration 99")
|
||||
eq(stub.bufferName("bufferdecorationname", 1), nil, "unknown decoration stays unresolved")
|
||||
|
||||
local vm = Vm.new({
|
||||
store = Flags.newStore(),
|
||||
scripts = { t = { { op = "end" } } },
|
||||
adapters = stub,
|
||||
})
|
||||
vm:start("t")
|
||||
Ops.dispatch(vm, { op = "setvar", [1] = 0x8000, [2] = 99 })
|
||||
Ops.dispatch(vm, { op = "bufferdecorationname", dest = 1, src = 0x8000 })
|
||||
eq(vm.ctx.stringVars[2], "LOTAD DOLL", "VAR_0x8000 decoration name fills STR_VAR_2")
|
||||
|
||||
local home = os.getenv("HOME")
|
||||
local cache = home and (home .. "/Library/Application Support/LOVE/pokemon-love2d/emerald/data/generated/gba/decorations/decorations.lua")
|
||||
local f = cache and io.open(cache, "rb")
|
||||
if f then
|
||||
local src = f:read("*a")
|
||||
f:close()
|
||||
DecorInv.reset()
|
||||
local pack = assert(load(src, "@decorations.lua", "t", {}))()
|
||||
DecorInv.install(pack)
|
||||
eq(stub.bufferName("bufferdecorationname", 99), "LOTAD DOLL", "emerald cache decoration 99 is LOTAD DOLL")
|
||||
end
|
||||
|
||||
T.finish("game3_bufferdecorationname_test")
|
||||
Reference in New Issue
Block a user