From bd26824b0126ad576bfa5cc3222796ad84328be5 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 13 Sep 2026 14:41:46 +0200 Subject: [PATCH] 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. --- Development.md | 7 ++++--- Shaders.md | 14 +++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Development.md b/Development.md index ec2a847..b9bdadf 100644 --- a/Development.md +++ b/Development.md @@ -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. diff --git a/Shaders.md b/Shaders.md index 4631da9..8b2ede3 100644 --- a/Shaders.md +++ b/Shaders.md @@ -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