Render on change #44

Closed
opened 2026-09-12 15:03:59 +02:00 by meleki · 2 comments
Owner

Editor nur bei Änderungen neu zeichnen, im Play weiter durchgehend.

Stand: ein stehendes Bild kostet im Editor gemessen 69 % eines Kerns (Lüfter, Akku). Seit dem Low-Latency-Modus (desynchronized) verlängert ein Frame auf Abruf die Reaktionszeit nicht mehr.

Abzuklären: was alles ein Neuzeichnen auslösen muss (Eingabe, Hover, Laden einer Textur, Perlen/Live-Werte im Graph während Play).


Übernommen aus todo-notes/render-on-change.md beim Umzug auf das Issue-System (2026-09-12).

  • Angelegt: 2026-09-11 · bf41919 — Add todo notes for the next round of features, gaps and upkeep
  • Gemeldet von: Claude (Assistent)
Editor nur bei Änderungen neu zeichnen, im Play weiter durchgehend. Stand: ein stehendes Bild kostet im Editor gemessen 69 % eines Kerns (Lüfter, Akku). Seit dem Low-Latency-Modus (`desynchronized`) verlängert ein Frame auf Abruf die Reaktionszeit nicht mehr. Abzuklären: was alles ein Neuzeichnen auslösen muss (Eingabe, Hover, Laden einer Textur, Perlen/Live-Werte im Graph während Play). --- *Übernommen aus `todo-notes/render-on-change.md` beim Umzug auf das Issue-System (2026-09-12).* - **Angelegt:** 2026-09-11 · `bf41919` — Add todo notes for the next round of features, gaps and upkeep - **Gemeldet von:** Claude (Assistent)
meleki added the todo-note label 2026-09-12 15:03:59 +02:00
admin added the low-priority label 2026-09-12 18:14:18 +02:00
admin added this to the Development project 2026-09-13 13:30:08 +02:00
admin removed this from the Development project 2026-09-13 13:30:59 +02:00
admin added this to the Development project 2026-09-13 13:45:43 +02:00
admin moved this to Next in Development on 2026-09-14 14:09:36 +02:00

Anmerkung: ich habe mitbekommen, dass ein redraw beim shader gezielt ausgelöst wird, vorallem wenn Time variable vorhanden. Ich weißt nicht wie relevant diese info ist. wenn es nichts mit dem issue zu tun hat, dann meine anmerkung hier ignorieren.

Anmerkung: ich habe mitbekommen, dass ein redraw beim shader gezielt ausgelöst wird, vorallem wenn Time variable vorhanden. Ich weißt nicht wie relevant diese info ist. wenn es nichts mit dem issue zu tun hat, dann meine anmerkung hier ignorieren.
Author
Owner

Implemented in bc57f61, wiki in 5f71a42.

It pays off. Measured on the built stand, 20 s per variant, CPU of all eleven chromium processes read from /proc, real GPU (ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630, Mesa)), order probed by repeating the first variant last:

CPU frames drawn
kept awake (old behaviour) 75.5 % of a core 1201 (60 fps)
render on change, idle 8.8 % 0
awake again (order probe) 83.9 % 1199

So ~89 % off the idle cost, and the zero says nothing is drawn at all rather than drawn more cheaply. Splitting it further: the ticker itself costs ~3 points, drawing ~64, and ~9-12 % is browser floor that stays. The 69 % in the note is the same order of magnitude.

What it does. initRenderLoop() in src/ui/render.ts takes app.render off the ticker; the ticker keeps running, so the clock, a live world and the panels still get their step. requestRender() opens a 200 ms trailing window rather than scheduling exactly one frame.

The window is the point. Strict dirty tracking would have to be right at ~70 direct redraw() calls that never pass through invalidate(), ~70 position mutations and ~70 addChild/removeChild — that is the road to the bug you notice three days later. A trailing window also catches the repaint an event only starts: a microtask, a texture upload, a resolved font. The idle gain is the same, because after 200 ms it is quiet either way.

Woken at the places everything passes through: pointer/wheel/keyboard once centrally on window in initUI, Widget.invalidate()/resize(), Stack.relayout(), EditorState.emit(), and textureLibrary.onChange/fontLibrary.onChange. Editor.movesByItself() asks for every frame while Play or an editor run is on, a time shader is on screen, or the particle/animation preview is showing.

FPS counter now counts drawn frames over the time the renderer was awake. Idle seconds are not slow seconds, so it answers "how smooth while it moves" and holds its last answer instead of falling to 0 at rest.

Testing. No colour check can see a missing repaint: pixelAt extracts from the stage on demand (debugHook.ts) and is just as green on a screen frozen a minute ago — of 868 checks the 24 that read pixels are all blind here. Step 33 render-on-change watches the new __gm.renderStats() counter instead (7 checks). Two traps that cost me a run and are now in Development: let the last click's trailing window expire before measuring, or its legitimate frames look like a leak; and measure on a tab whose picture is actually still. Harness screenshots ask for a frame first via __gm.wake().

Ran locally: npm run check 1005 tests, the full e2e ALL PASSED - 875 checks, smokes 24/19/21. Full run rather than the affected module, because a change to the frame loop has no affected module.

One thing left open, not covered by any test here: whether a surface that is not redrawn for seconds stays stable on real hardware with our own desynchronized context and no preserveDrawingBuffer is up to the driver. Headless in the container does not answer that, and neither does the e2e run. ?vsync rules it out immediately if it ever shows.

Implemented in `bc57f61`, wiki in `5f71a42`. **It pays off.** Measured on the built stand, 20 s per variant, CPU of all eleven chromium processes read from `/proc`, real GPU (`ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630, Mesa)`), order probed by repeating the first variant last: | | CPU | frames drawn | |---|---|---| | kept awake (old behaviour) | 75.5 % of a core | 1201 (60 fps) | | render on change, idle | **8.8 %** | **0** | | awake again (order probe) | 83.9 % | 1199 | So ~89 % off the idle cost, and the zero says nothing is drawn at all rather than drawn more cheaply. Splitting it further: the ticker itself costs ~3 points, drawing ~64, and ~9-12 % is browser floor that stays. The 69 % in the note is the same order of magnitude. **What it does.** `initRenderLoop()` in `src/ui/render.ts` takes `app.render` off the ticker; the ticker keeps running, so the clock, a live world and the panels still get their step. `requestRender()` opens a **200 ms trailing window** rather than scheduling exactly one frame. The window is the point. Strict dirty tracking would have to be right at ~70 direct `redraw()` calls that never pass through `invalidate()`, ~70 position mutations and ~70 `addChild`/`removeChild` — that is the road to the bug you notice three days later. A trailing window also catches the repaint an event only *starts*: a microtask, a texture upload, a resolved font. The idle gain is the same, because after 200 ms it is quiet either way. Woken at the places everything passes through: pointer/wheel/keyboard once centrally on `window` in `initUI`, `Widget.invalidate()`/`resize()`, `Stack.relayout()`, `EditorState.emit()`, and `textureLibrary.onChange`/`fontLibrary.onChange`. `Editor.movesByItself()` asks for every frame while Play or an editor run is on, a time shader is on screen, or the particle/animation preview is showing. **FPS counter** now counts drawn frames over the time the renderer was *awake*. Idle seconds are not slow seconds, so it answers "how smooth while it moves" and holds its last answer instead of falling to 0 at rest. **Testing.** No colour check can see a missing repaint: `pixelAt` extracts from the stage on demand (`debugHook.ts`) and is just as green on a screen frozen a minute ago — of 868 checks the 24 that read pixels are all blind here. Step 33 `render-on-change` watches the new `__gm.renderStats()` counter instead (7 checks). Two traps that cost me a run and are now in `Development`: let the last click's trailing window expire before measuring, or its legitimate frames look like a leak; and measure on a tab whose picture is actually still. Harness screenshots ask for a frame first via `__gm.wake()`. Ran locally: `npm run check` 1005 tests, the **full** e2e `ALL PASSED - 875 checks`, smokes 24/19/21. Full run rather than the affected module, because a change to the frame loop has no affected module. **One thing left open, not covered by any test here:** whether a surface that is not redrawn for seconds stays stable on real hardware with our own `desynchronized` context and no `preserveDrawingBuffer` is up to the driver. Headless in the container does not answer that, and neither does the e2e run. `?vsync` rules it out immediately if it ever shows.
admin moved this to Done in Development on 2026-09-14 21:43:58 +02:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#44