The render loop is a thing you can get wrong invisibly, so it gets written down (#44)
1 parent
e0eb5c5089
commit
5f71a420bc
2 files changed
+38
-3
No files matched your search
+26
-2
@@ -90,8 +90,32 @@ api/ PHP backend: index.php (routing), lib/{Store,TextureStore
|
||||
tests/ Vitest unit tests tools/ seed-*, e2e (steps in tools/e2e/, rig in tools/e2e/lib/), smoke-*, lib/, assets/ (the walk demo's sheet)
|
||||
```
|
||||
|
||||
The toolbar's right edge shows the frame rate (frames counted over a 0.5 s window, updated
|
||||
twice a second) in edit and play mode alike; `__gm.fps()` exposes the same number to tests.
|
||||
**The picture is drawn when something changes, not sixty times a second** (`src/ui/render.ts`).
|
||||
An idle editor on the ticker costs ~75 % of a core, and ~64 of those points are the drawing of a
|
||||
picture nobody changed. `initRenderLoop()` takes `app.render` off the ticker; the ticker itself
|
||||
keeps running (~3 points), so the clock, a running world and the panels still get their frame —
|
||||
only the drawing is skipped. Idle then costs ~10 % of a core, which is the browser's own floor.
|
||||
|
||||
What wakes it is deliberately **not** a strict dirty flag. `requestRender()` opens a short
|
||||
trailing window (200 ms) rather than asking for exactly one frame, and it is called from the few
|
||||
places everything passes through: every pointer, wheel and keyboard event (once, on the window,
|
||||
in `initUI`), `Widget.invalidate()` and `Widget.resize()`, `Stack.relayout()`, `EditorState.emit()`,
|
||||
and a texture or font arriving. A repaint that lands a few milliseconds after its cause — a
|
||||
microtask, a texture upload, a font resolving, one of the 70-odd direct `redraw()` calls that
|
||||
never went through `invalidate()` — is drawn without anyone having had to declare it. What moves
|
||||
by itself cannot be woken by an event, so `Editor.tick` asks for the next frame for as long as
|
||||
play or an editor run is on, a Time shader is on screen, or the particles or animation preview is
|
||||
showing (`movesByItself()`).
|
||||
The reason for the trailing window rather than the exact frame: **a missing repaint is invisible
|
||||
to the checks.** `__gm.pixelAt` re-renders the stage on demand, so every colour check would pass
|
||||
just as well on a screen that froze a minute ago. The drawn-frame counter is the only witness —
|
||||
`__gm.renderStats()` gives it, and step 33 is the check that the editor really does go quiet and
|
||||
really does wake up again.
|
||||
|
||||
The toolbar's right edge shows the frame rate in edit and play mode alike; `__gm.fps()` exposes
|
||||
the same number to tests. It counts **drawn** frames over the time the renderer was **awake**:
|
||||
idle seconds are not slow seconds, so the readout answers "how smooth is it while it moves" and
|
||||
holds its last answer while nothing needs drawing, instead of falling to zero when all is well.
|
||||
|
||||
Rules that keep it organised: `core/` imports nothing from other layers; `ui/` never sees
|
||||
assets; position exists only on scene objects; overrides are merged in exactly one place
|
||||
|
||||
+12
-1
@@ -64,7 +64,7 @@ The e2e run is a list of steps in `tools/e2e/` (hierarchy, inspector, play, asse
|
||||
graph, parenting, colliders, containers, text/enabled, menus, textures, frames, overflow, aspect,
|
||||
container tab, run-in-editor, pause, nested behaviors, key repeat, list tab, struct variable,
|
||||
variable nodes, property nodes, animations, state machines, console, shaders, transforms, fonts,
|
||||
particles, shared scope) that build on each other's
|
||||
particles, shared scope, render on change) that build on each other's
|
||||
scene state and share the CDP driver in `tools/lib/harness.mjs` — `press`, `undo`, `play`,
|
||||
`object(id)`, `world()`, `mutate(js)` and friends, so a check reads like the interaction it
|
||||
exercises.
|
||||
@@ -80,6 +80,17 @@ nothing. A step picks a type with `pickAssetType(t, 'Font')` from the same file,
|
||||
first. The fourteenth type (Variables) pushed `Font` under that edge and took the font step down,
|
||||
because only the particles step had carried the workaround of its own.
|
||||
|
||||
**What no colour check can see: whether anything was drawn at all.** `__gm.pixelAt` re-renders the
|
||||
stage on demand, so it answers correctly on a screen that has been frozen for a minute — which is
|
||||
exactly the failure mode of drawing on change instead of every frame (see *Architecture*). The
|
||||
drawn-frame counter in `__gm.renderStats()` is the witness, and the render-on-change step is the
|
||||
only place the suite looks at it. Two rules there, both learned the hard way: measure idleness only
|
||||
after waiting for the trailing window of the last event to run out (otherwise the frames the
|
||||
previous click is still entitled to look like a leak), and take a step that does its measuring on a
|
||||
tab whose picture stands still — the scene viewport may be wearing a Time shader, and the particles
|
||||
and animation previews are moving pictures by definition. Screenshots ask for a frame first
|
||||
(`__gm.wake()` in `harness.mjs`) rather than trusting what the compositor still holds.
|
||||
|
||||
**A check that demands an exact number must be handed the number, not something it can be derived
|
||||
from.** The particle preview used to hold second 0.5 by computing the clock reading that *would* mean
|
||||
0.5 since the field started; the round trip through floating point misses by the last bit about one
|
||||
|
||||
Reference in new issue
Block a user