Shaders: replace the performance estimate with measured numbers (#67)

Intel HD 630, 48x48 sprites, 640x360 camera, neutral values. The estimate
"fine for dozens, not for thousands" was right in spirit and two orders of
magnitude too optimistic at the upper end. Development names the new tool
and finally lists e2e step 28.
root committed 2026-09-13 14:41:46 +02:00
1 parent 46d30db966
commit bd26824b01
2 files changed
+17 -4

No files matched your search

+4 -3
@@ -12,13 +12,14 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
# code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
node tools/seed-e2e.mjs && node tools/e2e.mjs # 727 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 728 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs # 24 checks: the example game, played end to end
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs # 19 checks: the Tetris example, played end to end
node tools/seed-actor.mjs && node tools/smoke-actor.mjs # 21 checks: the walk demo (the seed uploads its own sheet)
node tools/seed-e2e.mjs && node tools/perf-shaders.mjs # what a shader costs per sprite; a measuring tool, not a check
```
**A run needs a working GPU.** An idle editor page keeps rendering a picture that never changes, and
@@ -62,7 +63,7 @@ to touch it.
The e2e run is a list of steps in `tools/e2e/` (hierarchy, inspector, play, assets, drawing,
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) that build on each other's
variable nodes, property nodes, animations, state machines, console, shaders) 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.
@@ -150,4 +151,4 @@ widths to find click points. The **render device is not in this repo** — `/dev
and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which
applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets
WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`,
WebGL 2.0, and the same 727 checks as here.
WebGL 2.0, and the same 728 checks as here.
+13 -1
@@ -83,7 +83,19 @@ deviations while their shader is not worn.
## What it costs, and where it stops
A shader is a Pixi filter: the sprite is rendered into an intermediate texture and drawn back with
the fragment, one pass per sprite wearing one. Fine for dozens, not for thousands. The pass runs at
the fragment, one pass per sprite wearing one. Measured with `tools/perf-shaders.mjs` on an Intel
HD 630 (the GPU CI uses as well), sprites of 48x48 inside a 640x360 camera, neutral values, three
runs agreeing to within 1 fps:
| shaded sprites | 25 | 50 | 100 | 200 | 400 | 800 |
| --- | --- | --- | --- | --- | --- | --- |
| frames per second | 58 | 56 | 54 | 48 | 34 | 20 |
The very same scene without shaders holds 60 fps at every one of those counts, 800 included, so the
whole drop is the passes: about 20 microseconds each up to 200 sprites, 40 at 800. Sprite size
hardly matters — four times the area (96x96) measures the same, so what costs is the pass, not the
pixels in it. A few dozen shaded sprites are free in practice; a few hundred cost half the frame
rate. The pass runs at
screen resolution, so pixel art stays sharp: measured in play at a fractional camera scale (1.43
screen pixels per world unit), a *Nearest* texture keeps its hard border pixel for pixel with a
neutral shader on, and brightness 0.5 darkens every one of those pixels — the pass runs there, and