Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/frame-profile-sections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Frame-profile sections for world, presentation and DS audio

F11 enables the existing `FrameProfile` report. These additional sections use
its shared no-op closer when profiling is disabled; there is no new setting.
All labels begin with spaces, so nested timings do not get counted again as
top-level frame time. Multiple spans with the same label accumulate.

| Label | Work measured |
| --- | --- |
| `world: tiles` | Initial map/neighbour draws, declined-pipeline fallback and the flat upper layer |
| `world: entities` | Flat ghosts, main and above-layer entities, or tilted billboards |
| `world: FX` | Flat/tilted effect groups, pipeline `ctx.drawFx` and final map overlays |
| `tiles: ensureWindow` | Window validation/refill, including cached and missing-art returns |
| `renderer: zone blit` | Actual `blitCanvas` calls, including the unshaded fallback |
| `renderer: present pipeline` | Present processing and final native output |
| `voxel: sun pass` | Shadow preparation, including cache-hit/rate-limit decisions |
| `voxel: terrain` | Main and neighbouring terrain submission, per eye |
| `voxel: water` | Water draw-list construction, planar reflection and water submission, per eye |
| `voxel: cards` | Main character and staged-battle card submission, per eye |
| `audio: Nitro render` | One DS mixer block, including SoundData creation |

These are wall-clock submission timings, not GPU execution times. Reflection
copies of cards contribute to the water section; the cards section measures
the visible scene's card pass. The world labels describe selected drawing
phases rather than an exhaustive partition of `drawWorld`; setup, sorting and
other work remain visible in the enclosing section. Voxel timings run inside
the selected world pipeline. Hook names, arguments and dispatch order are
unchanged. An exception still takes its existing cleanup/error path; an
unfinished span is discarded by the existing profiler contract.

A1 adds observability and makes no performance claim. Desktop before/after
frame timings and ARM/device timings: **not measured**. Capture the same scene,
viewport, camera, mod configuration and profile window when measuring later
optimizations. Profiling itself is disabled by F11 / `FrameProfile.setEnabled(false)`.
11 changes: 11 additions & 0 deletions mods/DRAMATIC_SHAPE/lib/VoxelScene.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ local V = ...

local Mat4 = V.require("Mat4")
local Perf = V.require("Perf")
local FrameProfile = require("src.core.FrameProfile")
-- The PERFORMANCE tier's ceilings (lib/Tier.lua). Top-level here rather
-- than lazily as in ShadowMap/Water/AntiAlias: Tier requires nothing from
-- this mod, so there is no load order to protect, and the sun's rate
Expand Down Expand Up @@ -2669,21 +2670,26 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
-- reaches far north and barely south, which is right for every rung
-- but a head free to face south.
local shCx, shCy = FirstPerson.shadowCenter(cx, cy, vh)
local closeSun = FrameProfile.section(" voxel: sun pass")
castShadows(state, terrain, nbMesh, posed, shCx, shCy, vw, vh, atlasFor,
water, nbWater, battleCards, battleToken)
closeSun()

-- Everything between beginScene and endScene, as one function: the flat
-- path runs it once, a VR frame runs it once PER EYE -- same posed
-- list, same shadow map, same glint, so the two eyes can never disagree
-- about anything but their viewpoint.
local function drawScene()

local closeTerrain = FrameProfile.section(" voxel: terrain")
Voxel3D.draw(terrain, atlasFor(state.map), nil)
for i, nb in ipairs(state.neighbors or {}) do
Voxel3D.draw(nbMesh[i], atlasFor(nb.map),
Mat4.translate(nb.ox, 0, nb.oy))
end

closeTerrain()

-- Without a shadow map (headless, or a driver that could not make the
-- canvas) the old flat decals stand in: ground-only, characters only,
-- but better than a world with nothing under anybody. They go down
Expand Down Expand Up @@ -2768,6 +2774,7 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
-- lake would otherwise wear one as a black smear. Water covers them,
-- which is the same answer the shadow map's own pass gives (see
-- ShadowMap.sprites) -- people do not shadow water either way.
local closeWater = FrameProfile.section(" voxel: water")
local waterDraws = {}
if water then
waterDraws[#waterDraws + 1] = { water, atlasFor(state.map), nil }
Expand Down Expand Up @@ -2803,6 +2810,8 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
planarTex, planarY)
end

closeWater()

-- AND THE RIPPLE RINGS ON TOP OF IT, BEFORE ANYBODY IS STANDING IN IT.
--
-- THIS LINE'S POSITION IS THE WHOLE FIX. Reported from play: "the ripples
Expand Down Expand Up @@ -2864,10 +2873,12 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
-- drawEntity resolves the lean-over-the-wall-in-front case, and a
-- character genuinely behind a building is far deeper and loses the
-- test, so buildings and trees really occlude.
local closeCards = FrameProfile.section(" voxel: cards")
drawCast(state, posed, atlasFor)
-- and the staged fight's mons over them, exactly as they were painted
-- into the water's reflection copy a few lines up (see drawBattleCards)
drawBattleCards()
closeCards()
-- tall grass last, pulled camera-ward exactly as far as the characters
-- were (same per-vertex shader bias, so grass never drifts either):
-- relative depth between a walker and the tuft row south of their feet
Expand Down
3 changes: 3 additions & 0 deletions src/audio/NitroAudio.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
-- Nintendo DS SSEQ transport over the ROM's SBNK/SWAR instruments.
-- PCM/ADPCM mixing is local; no replacement MIDI soundfont is used.
local FrameProfile = require("src.core.FrameProfile")
local Sdat=require('src.import.Gen4Sdat')
local Audio={};local Player={};Player.__index=Player
local archives,players={},setmetatable({}, {__mode="k"})
Expand Down Expand Up @@ -194,6 +195,7 @@ function Player:tick()
for _,v in ipairs(self.voices) do v.ticks=v.ticks-1;if v.ticks<=0 and v.release==0 then v.release=math.floor(RATE*0.04) end end
end
function Player:render()
local closeRender=FrameProfile.section(" audio: Nitro render")
local sound=love.sound.newSoundData(BLOCK,RATE,16,2)
for i=0,BLOCK-1 do
self.tickPhase=self.tickPhase+self.tempo*48/60/RATE
Expand All @@ -215,6 +217,7 @@ function Player:render()
sound:setSample(i,1,math.max(-1,math.min(1,left)))
sound:setSample(i,2,math.max(-1,math.min(1,right)))
end
closeRender()
return sound
end
function Player:update()
Expand Down
6 changes: 6 additions & 0 deletions src/render/Renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
-- neighbor filtering.
-- Spec: docs/new-features.md (survey zoom)

local FrameProfile = require("src.core.FrameProfile")
local Zoom = require("src.render.Zoom")
local Tilt = require("src.render.Tilt")
local PaletteFX = require("src.render.PaletteFX")
Expand Down Expand Up @@ -686,11 +687,13 @@ end
-- render.compose mod can composite the world/UI canvases into its own layout.
function Renderer:blitCanvas(canvas, sx, sy, zoneList, zoneSx, zoneSy,
bx, by, boxX, boxY, boxW, boxH, dpiX, dpiY)
local closeBlit = FrameProfile.section(" renderer: zone blit")
local shader = zoneList and zoneList[1] and PaletteFX.shader() or nil
if not shader then
love.graphics.setScissor(boxX, boxY, boxW, boxH)
love.graphics.draw(canvas, bx, by, 0, sx, sy)
love.graphics.setScissor()
closeBlit()
return
end
love.graphics.setShader(shader)
Expand All @@ -713,6 +716,7 @@ function Renderer:blitCanvas(canvas, sx, sy, zoneList, zoneSx, zoneSy,
end
love.graphics.setScissor()
love.graphics.setShader()
closeBlit()
end

-- THE BARS, FILLED WITH THE EDGE THEY FRAME.
Expand Down Expand Up @@ -1393,6 +1397,7 @@ function Renderer:endFrame(zones, worldZones)
end

if present then
local closePresent = FrameProfile.section(" renderer: present pipeline")
love.graphics.setCanvas()
-- Post-process pipelines run over the finished composite -- world, UI
-- and all -- and before GBC FX, so a blur or colour grade is what the
Expand All @@ -1410,6 +1415,7 @@ function Renderer:endFrame(zones, worldZones)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(composed, 0, 0)
end
closePresent()
end
self.worldActive = false
self.uprightActive = false
Expand Down
6 changes: 6 additions & 0 deletions src/render/TileRenderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
-- a single static SpriteBatch covering the map plus a border-block ring
-- (the ring plays the role of the GB border blocks around small maps).

local FrameProfile = require("src.core.FrameProfile")
local Assets = require("src.render.Assets")
local Logger = require("src.core.Logger")
local Probe = require("src.core.Probe")
Expand Down Expand Up @@ -1793,6 +1794,7 @@ function TileRenderer:setWin(tx0, ty0, tx1, ty1)
end

function TileRenderer:ensureWindow(camX, camY, vw, vh)
local closeWindow = FrameProfile.section(" tiles: ensureWindow")
local W, H = self.bodyTilesW, self.bodyTilesH
vw = vw or W * 8 -- a nil view (headless draw) means the whole body
vh = vh or H * 8
Expand Down Expand Up @@ -1844,6 +1846,7 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
end
if win and tx0 >= win.tx0 and ty0 >= win.ty0
and tx1 <= win.tx1 and ty1 <= win.ty1 then
closeWindow()
return -- still inside the last fill
end
-- refill with margin so the next few scrolled pixels stay covered
Expand Down Expand Up @@ -1920,6 +1923,7 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
cells, drew, W, H, tostring(self.gen3.slots))
end
self:setWin(tx0, ty0, tx1, ty1)
closeWindow()
return
end

Expand All @@ -1928,6 +1932,7 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
-- player, and leave the reason in the log (gen3SheetsFor already logged it).
if not self.image then
self:setWin(tx0, ty0, tx1, ty1)
closeWindow()
return
end
if not self.winBatch then
Expand Down Expand Up @@ -1970,6 +1975,7 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
end
end
self:setWin(tx0, ty0, tx1, ty1)
closeWindow()
end

-- draw the static tile window, then its animated overdraw, at the camera offset
Expand Down
25 changes: 25 additions & 0 deletions src/world/OverworldController.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
-- surfing, Cut trees, trainer sight lines, and dispatches interactions to
-- map scripts (data/scripts/), marts, nurses or extracted text.

local FrameProfile = require("src.core.FrameProfile")
local Assets = require("src.render.Assets")
local Camera = require("src.render.Camera")
local Collision = require("src.world.Collision")
Expand Down Expand Up @@ -16678,6 +16679,7 @@ function OverworldState:drawWorld()
-- path below on the vanilla flat/tilt draw
local override
if not pipelineId then
local closeTiles = FrameProfile.section(" world: tiles")
self.map.renderer:drawBorderFill(cam.x, bgY, vw, vh)
self.map.renderer:draw(cam.x, bgY, vw, vh)
for _, nb in ipairs(self.neighbors) do
Expand All @@ -16687,6 +16689,7 @@ function OverworldState:drawWorld()
if self.g3MirageTower then
require("src.world.Gen3MirageTower").drawTower(self, cam)
end
closeTiles()
end
-- per-billboard SGB palette source; only needed (and only paid for) when
-- tilting. nil headless / on stale palettes -> billboards go uncolorized.
Expand Down Expand Up @@ -17401,6 +17404,7 @@ function OverworldState:drawWorld()
-- Deliberately unscaled by depth, like :billboard: an effect keeps its
-- crisp authored size and only its anchor moves.
ctx.drawFx = function(project, scale)
local closeFx = FrameProfile.section(" world: FX")
scale = scale or pscale
local colors = ctx.spriteColors()
local function at(drawFn, wx, wy)
Expand Down Expand Up @@ -17471,6 +17475,7 @@ function OverworldState:drawWorld()
if pt.active then radarOne = pt; at(fxRadarOne, pt.x * 16 + 8, pt.y * 16 + 16) end
end
radarOne = nil
closeFx()
end
override = Pipelines.drawWorld(pipelineId, ctx)
-- world post-processes (a miniature-diorama blur, a colour grade) fold
Expand All @@ -17484,11 +17489,13 @@ function OverworldState:drawWorld()
-- was retired). The ground pass was skipped on its behalf above, so
-- draw it now and fall through to the flat path below rather than
-- compositing an empty canvas.
local closeTiles = FrameProfile.section(" world: tiles")
self.map.renderer:drawBorderFill(cam.x, bgY, vw, vh)
self.map.renderer:draw(cam.x, bgY, vw, vh)
for _, nb in ipairs(self.neighbors) do
nb.map.renderer:drawMapOnly(cam.x - nb.ox, bgY - nb.oy, vw, vh)
end
closeTiles()
end
end

Expand Down Expand Up @@ -17581,6 +17588,7 @@ function OverworldState:drawWorld()
-- The COLLISION GRID IS UNTOUCHED by any of this. It is in map pixels and
-- stays there; only the picture is projected, which is why a compressed
-- world still walks the same.
local closeGhosts = FrameProfile.section(" world: entities")
for _, g in ipairs(self.ghosts) do
-- A GHOST GOES THROUGH THE FREE CAMERA TOO.
--
Expand All @@ -17606,6 +17614,7 @@ function OverworldState:drawWorld()
end
if not drawn then g.npc:draw(cam.x - g.ox, cam.y - g.oy) end
end
closeGhosts()
-- HIDDEN MEANS HIDDEN, for the player as much as for anybody.
--
-- Two Gen 3 things write this and neither had a reader: the
Expand Down Expand Up @@ -17909,6 +17918,7 @@ function OverworldState:drawWorld()
and groundForRise:beginSprites() then
spriteDepthGround = groundForRise
end
local closeEntities = FrameProfile.section(" world: entities")
for _, e in ipairs(self.entities) do
if self:gen3AboveTopLayer(e) then
onTop = onTop or {}
Expand All @@ -17925,6 +17935,7 @@ function OverworldState:drawWorld()
spriteDepthGround:endSprites()
spriteDepthGround = nil
end
closeEntities()
-- The Gen 3 top layer goes on AFTER the entity pass and before the
-- field effects: it is the half of every metatile the player walks
-- behind -- treetops, upper storeys, the far rail of a bridge. On a
Expand Down Expand Up @@ -17956,6 +17967,7 @@ function OverworldState:drawWorld()
self:eachEmote(fxEmote)
emoteEarly = true
end
local closeTiles = FrameProfile.section(" world: tiles")
aim(self.map.renderer, 0, 0)
self.map.renderer:drawAbove(cam.x, bgY, vw, vh)
aim(self.map.renderer)
Expand All @@ -17966,8 +17978,12 @@ function OverworldState:drawWorld()
aim(nb.map.renderer)
end
end
closeTiles()
closeEntities = FrameProfile.section(" world: entities")
for _, e in ipairs(onTop or {}) do drawEntity(e) end
closeEntities()

local closeFx = FrameProfile.section(" world: FX")
fxHeal()
fxDust()
fxCutTree()
Expand All @@ -17988,6 +18004,7 @@ function OverworldState:drawWorld()
fxRod()
-- Smoke keeps the template's priority 0, above the ship and map layers.
fxSSAnneSmoke()
closeFx()
else
-- === TILT PATH: ground-hugging FX stay on the projected ground, all
-- standing things billboard upright over it in a separate pass. ======
Expand All @@ -17997,13 +18014,15 @@ function OverworldState:drawWorld()
-- draws them last, over the sprites, in the same canvas; here the two
-- layers are separate and composited ground-under-upright, so drawing
-- them now into the still-active ground canvas is order-equivalent.
local closeFx = FrameProfile.section(" world: FX")
fxHeal()
fxDust()
fxCutTree()
fxWater()
-- ...and the gates with them: they are behind every upright sprite (see
-- drawGen3Gates), so the ground canvas is exactly where they belong
self:drawGen3Gates(cam)
closeFx()

Game.renderer:beginUprightPass()

Expand All @@ -18024,6 +18043,7 @@ function OverworldState:drawWorld()
end
table.sort(items, function(a, b) return a.y < b.y end)

local closeEntities = FrameProfile.section(" world: entities")
for _, it in ipairs(items) do
if it.kind == "ghost" then
-- ghosts billboard just like real entities (foot offset folds in the
Expand Down Expand Up @@ -18059,6 +18079,8 @@ function OverworldState:drawWorld()
end
end

closeEntities()
closeFx = FrameProfile.section(" world: FX")
-- Standing world FX: each billboards at the ground foot of the
-- character it belongs to, so it stays upright over the tilted ground.
-- emote bubble -> the spotting NPC's foot (rides above its head)
Expand Down Expand Up @@ -18100,13 +18122,15 @@ function OverworldState:drawWorld()
radarOne = nil

Game.renderer:endUprightPass()
closeFx()
end

-- LAST IN THE WORLD PASS, over everything the cave contains: the black
-- window with the circle cut out of it. It has to be here rather than in
-- drawUI because it is anchored to the PLAYER, not to the screen -- the
-- cartridge can put it at a fixed (120,80) because its camera never lets
-- the player leave the middle, and this port's does at a map edge.
local closeFx = FrameProfile.section(" world: FX")
self:drawGen3Flash(cam, vw, vh)
-- the Mirage Tower's falling fossil and ceiling crumbs: OAM priority 0
if self.g3MirageTower or self.g3MirageCrumbles then
Expand All @@ -18118,6 +18142,7 @@ function OverworldState:drawWorld()
-- in the world pass rather than the UI pass for the same reason the flash
-- is: it is anchored to a CELL, not to the screen.
self:drawGen3Decorate(cam)
closeFx()
end

-- The cursor the DECORATE row hands the player: the decoration's own
Expand Down