Shader: Performance messen (ein Render-Pass pro Sprite) #67

Closed
opened 2026-09-13 11:27:43 +02:00 by meleki · 1 comment
Owner

Befund

Jedes Sprite mit Shader bekommt einen eigenen Pixi-Filter (syncShader in src/viewport/symbols.ts), also einen Render-Pass in eine Zwischentextur und zurueck. Das Wiki (Shaders) sagt "fine for dozens, not for thousands"; das ist eine Schaetzung, keine Messung.

Aufgabe

Einmal messen und die Zahl ins Wiki schreiben:

  • Ein Tetris-Brett (oder eine Szene mit 200 Sprites), alle mit Adjust colors, FPS im Play ueber __gm.fps() gegen dieselbe Szene ohne Shader.
  • Auf der CI-GPU (Intel HD 630, Vulkan) und einmal lokal.

Je nach Ergebnis ein Folge-Issue: Sprites mit demselben Shader und denselben Param-Werten koennten sich einen Filter teilen, oder der Filter haengt am Container statt am Sprite.

## Befund Jedes Sprite mit Shader bekommt einen eigenen Pixi-`Filter` (`syncShader` in `src/viewport/symbols.ts`), also einen Render-Pass in eine Zwischentextur und zurueck. Das Wiki (*Shaders*) sagt "fine for dozens, not for thousands"; das ist eine Schaetzung, keine Messung. ## Aufgabe Einmal messen und die Zahl ins Wiki schreiben: - Ein Tetris-Brett (oder eine Szene mit 200 Sprites), alle mit `Adjust colors`, FPS im Play ueber `__gm.fps()` gegen dieselbe Szene ohne Shader. - Auf der CI-GPU (Intel HD 630, Vulkan) und einmal lokal. Je nach Ergebnis ein Folge-Issue: Sprites mit demselben Shader und denselben Param-Werten koennten sich einen Filter teilen, oder der Filter haengt am Container statt am Sprite.
meleki added the shader label 2026-09-13 11:27:43 +02:00
admin added this to the Development project 2026-09-13 13:29:59 +02:00
admin moved this to Next in Development on 2026-09-13 13:32:30 +02:00
Author
Owner

Gemessen statt geschaetzt. Das Wiki trug bisher die Schaetzung "fine for dozens, not for thousands" - sie war der Richtung nach richtig und am oberen Ende zwei Groessenordnungen zu optimistisch.

Die Messung

Intel HD 630 (dieselbe GPU, die CI benutzt), 48x48-Sprites in einer 640x360-Kamera, Adjust colors mit neutralen Werten, drei Laeufe auf 1 fps gleich:

beschattete Sprites 25 50 100 200 400 800
fps 58 56 54 48 34 20

Dieselbe Szene ohne Shader haelt bei jeder dieser Zahlen 60 fps, 800 eingeschlossen - der ganze Einbruch sind die Passes. Rund 20 us pro Pass bis 200, 40 us bei 800.

Der wichtigste Befund war nicht erwartet: vierfache Sprite-Flaeche (96x96) misst dasselbe - 51,6 / 49,5 / 34,2 gegen 53,2 / 48,5 / 34,5 bei 100 / 200 / 400. Es kostet der Wechsel des Render-Ziels, nicht das Rechnen pro Pixel.

Die Zahlen sind lastrobust: der Wiederholungslauf lief bei loadavg 1,2-1,5 und misst dieselben Werte auf 1 fps genau. Jede Zeile traegt die Last ihres Punktes im Protokoll; kein Punkt wurde waehrend eines CI-Jobs genommen (ein Waechter wartete auf das Verschwinden des Job-Containers, jeder Lauf startet erst unter loadavg 0,5).

Zwei Fallen am Messgeraet, beide haben zuerst eine falsche Antwort gekostet

Der Median der Frame-Zeiten ist hier wertlos. Der erste Lauf meldete "bei 200 Sprites kein Verlust" - Median 17,2 ms. In denselben 4 Sekunden waren aber nur 192 statt 239 Frames da: unter vsync ist die Verteilung bimodal (die meisten Frames treffen einen Takt, einige verpassen mehrere). Die Rate kommt jetzt aus Frames durch Messdauer, __gm.fps() bestaetigt sie unabhaengig.

Der 60-Hz-Deckel laesst sich in headless nicht abschalten. --disable-gpu-vsync und --disable-frame-rate-limit sind gemessen wirkungslos (57,6 fps mit wie ohne). Aufgedeckt hat das ein eigener Waechter im Werkzeug; der tote Schalter ist wieder ausgebaut. Gemessen wird deshalb der Einbruchspunkt, nicht die Passkosten im halbleeren Frame - das steht so im Skriptkopf.

Was im Repo steht

  • tools/perf-shaders.mjs - das Messwerkzeug. Laeuft bewusst nicht in CI, Frame-Zeiten sind maschinenabhaengig.
  • __gm.filterCount() in debugHook.ts zaehlt sichtbare Nodes mit Filter. Damit ist "ein Pass pro Sprite" gemessen und nicht behauptet: im Messlauf filters == n an jedem Punkt, 0 ohne Shader.
  • Wiki Shaders traegt die Tabelle statt der Schaetzung, Development nennt das Werkzeug und listet Schritt 28 endlich mit.

Beweise

./check.sh === CHECK EXIT 0 ===, 850 Tests
e2e Modul 28 ALL PASSED - 37 checks (vorher 36)
Gegenprobe Hook stumpf auf 0, neu gebaut -> genau der neue Check faellt ({"on":0,"off":0}), die anderen 36 bleiben gruen
Gueltigkeit nach dem Rueckbau ergibt der Neubau wieder index-0KKVUy-l.js, denselben Inhalts-Hash wie das Bundle des gruenen Laufs
Angesehen Screenshot der schwersten Szene im Play: 800 Sprites, Spiel laeuft (sie ueberlappen dort)

Die eingebauten Waechter des Werkzeugs beissen nachweislich: beim 96er-Durchgang fiel "the baseline holds the frame rate at every count" bei 400 Sprites (54,4 fps), weil dort schon die shaderlose Szene Takte verpasst und der Vergleich etwas anderes messen wuerde.

Folge-Issue

Der in diesem Issue angedachte Weg "Filter teilen" bringt nichts - das GLSL-Programm ist laengst geteilt (GlProgram.from cached nach Quelltext), geteilt wuerden nur die Filter-Objekte, der Pass bleibt. Der Weg, der die Passes wirklich beseitigt, ist #77: das Fragment in den Zeichenweg des Sprites statt in einen Filter.

Commit 82effb2, Wiki bd26824.

Gemessen statt geschaetzt. Das Wiki trug bisher die Schaetzung "fine for dozens, not for thousands" - sie war der Richtung nach richtig und am oberen Ende zwei Groessenordnungen zu optimistisch. ## Die Messung Intel HD 630 (dieselbe GPU, die CI benutzt), 48x48-Sprites in einer 640x360-Kamera, `Adjust colors` mit neutralen Werten, **drei Laeufe auf 1 fps gleich**: | beschattete Sprites | 25 | 50 | 100 | 200 | 400 | 800 | |---|---|---|---|---|---|---| | **fps** | 58 | 56 | 54 | 48 | 34 | 20 | Dieselbe Szene **ohne** Shader haelt bei jeder dieser Zahlen 60 fps, 800 eingeschlossen - der ganze Einbruch sind die Passes. Rund 20 us pro Pass bis 200, 40 us bei 800. **Der wichtigste Befund war nicht erwartet:** vierfache Sprite-Flaeche (96x96) misst *dasselbe* - 51,6 / 49,5 / 34,2 gegen 53,2 / 48,5 / 34,5 bei 100 / 200 / 400. Es kostet der Wechsel des Render-Ziels, nicht das Rechnen pro Pixel. Die Zahlen sind lastrobust: der Wiederholungslauf lief bei loadavg 1,2-1,5 und misst dieselben Werte auf 1 fps genau. Jede Zeile traegt die Last ihres Punktes im Protokoll; kein Punkt wurde waehrend eines CI-Jobs genommen (ein Waechter wartete auf das Verschwinden des Job-Containers, jeder Lauf startet erst unter loadavg 0,5). ## Zwei Fallen am Messgeraet, beide haben zuerst eine falsche Antwort gekostet **Der Median der Frame-Zeiten ist hier wertlos.** Der erste Lauf meldete "bei 200 Sprites kein Verlust" - Median 17,2 ms. In denselben 4 Sekunden waren aber nur 192 statt 239 Frames da: unter vsync ist die Verteilung bimodal (die meisten Frames treffen einen Takt, einige verpassen mehrere). Die Rate kommt jetzt aus **Frames durch Messdauer**, `__gm.fps()` bestaetigt sie unabhaengig. **Der 60-Hz-Deckel laesst sich in headless nicht abschalten.** `--disable-gpu-vsync` und `--disable-frame-rate-limit` sind gemessen **wirkungslos** (57,6 fps mit wie ohne). Aufgedeckt hat das ein eigener Waechter im Werkzeug; der tote Schalter ist wieder ausgebaut. Gemessen wird deshalb der **Einbruchspunkt**, nicht die Passkosten im halbleeren Frame - das steht so im Skriptkopf. ## Was im Repo steht - `tools/perf-shaders.mjs` - das Messwerkzeug. **Laeuft bewusst nicht in CI**, Frame-Zeiten sind maschinenabhaengig. - `__gm.filterCount()` in `debugHook.ts` zaehlt sichtbare Nodes mit Filter. Damit ist "ein Pass pro Sprite" gemessen und nicht behauptet: im Messlauf `filters == n` an jedem Punkt, 0 ohne Shader. - Wiki *Shaders* traegt die Tabelle statt der Schaetzung, *Development* nennt das Werkzeug und listet Schritt 28 endlich mit. ## Beweise | | | |---|---| | `./check.sh` | `=== CHECK EXIT 0 ===`, 850 Tests | | e2e Modul 28 | `ALL PASSED - 37 checks` (vorher 36) | | Gegenprobe | Hook stumpf auf 0, **neu gebaut** -> genau der neue Check faellt (`{"on":0,"off":0}`), die anderen 36 bleiben gruen | | Gueltigkeit | nach dem Rueckbau ergibt der Neubau wieder `index-0KKVUy-l.js`, denselben Inhalts-Hash wie das Bundle des gruenen Laufs | | Angesehen | Screenshot der schwersten Szene im Play: 800 Sprites, Spiel laeuft (sie ueberlappen dort) | Die eingebauten Waechter des Werkzeugs beissen nachweislich: beim 96er-Durchgang fiel "the baseline holds the frame rate at every count" bei 400 Sprites (54,4 fps), weil dort schon die *shaderlose* Szene Takte verpasst und der Vergleich etwas anderes messen wuerde. ## Folge-Issue Der in diesem Issue angedachte Weg "Filter teilen" bringt nichts - das GLSL-Programm ist laengst geteilt (`GlProgram.from` cached nach Quelltext), geteilt wuerden nur die Filter-Objekte, der Pass bleibt. Der Weg, der die Passes wirklich beseitigt, ist **#77**: das Fragment in den Zeichenweg des Sprites statt in einen Filter. Commit `82effb2`, Wiki `bd26824`.
admin moved this to Done in Development on 2026-09-13 14:42:44 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#67