diff --git a/Development.md b/Development.md index b9bdadf..8ec3e77 100644 --- a/Development.md +++ b/Development.md @@ -12,7 +12,7 @@ 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 # 728 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 730 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 diff --git a/Shaders.md b/Shaders.md index 8b2ede3..2834cd3 100644 --- a/Shaders.md +++ b/Shaders.md @@ -14,7 +14,7 @@ a bad property name, ...). A node that keeps the graph from compiling shows the and a red frame. A shader graph is compiled on every change. A sprite wearing a graph that does not compile is drawn -without a filter, as if it wore none. +plain, as if it wore none. ## The nodes @@ -66,8 +66,8 @@ saturation 0 gives `#7c7c7c` — all byte-exact against the formula. ## Wearing a shader -The sprite's `Shader` row is a picker over the project's shader assets (`None` is no shader and no -extra render pass). The rows below it are the shader's `Property` nodes, in node order; they are +The sprite's `Shader` row is a picker over the project's shader assets (`None` is no shader: the +sprite draws in Pixi's batch like any other). The rows below it are the shader's `Property` nodes, in node order; they are ordinary sprite properties: overridable per placed object, reset (↺) and apply (↑) like any other, readable and writable by a `Properties` node at runtime — a behavior can fade a sprite by writing its `brightness` every frame. That node's `+ Property` list (under *Type: Sprite*) offers the @@ -82,24 +82,26 @@ 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. 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: +A shader runs in the sprite's own draw: the mesh is drawn once, with the graph's fragment in place +of Pixi's, so there is no intermediate texture and no extra pass. What it costs is the batch — a +sprite drawing with a shader of its own cannot share a draw call with the plain ones, so every +shaded sprite is one draw call. 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 2 fps: -| shaded sprites | 25 | 50 | 100 | 200 | 400 | 800 | -| --- | --- | --- | --- | --- | --- | --- | -| frames per second | 58 | 56 | 54 | 48 | 34 | 20 | +| sprites | 50 | 100 | 200 | 400 | 800 | 1600 | 3200 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| all wearing the shader | 60 | 60 | 60 | 60 | 56 | 34 | 17 | +| the same scene, no shader | 60 | 60 | 60 | 60 | 60 | 39 | 21 | -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 +Up to 400 shaded sprites nothing shows; at 800 the shader costs about 1.5 microseconds per sprite, +from 1600 on about 3 — and there the plain scene misses ticks by itself. For comparison, as a +filter pass per sprite (up to #77) the same counts measured 56 / 54 / 48 / 34 / 20 fps from 50 to +800: what cost then was the change of render target, not the pixels. The shader samples the +texture itself, 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 -it does not resample. Only sprites wear shaders; text objects do not. The +neutral shader on, and brightness 0.5 darkens every one of those pixels — the shader draws there, +and it does not resample. Only sprites wear shaders; text objects do not. The node set is deliberately small — no channel split, no texture sampling at other coordinates, no time input; each is one node in `src/core/shader/nodes.ts` (a `ShaderNodeDef` with an `emit`) and one row in the table above. @@ -108,8 +110,15 @@ one row in the table above. `src/core/shader/nodes.ts` is the catalogue, `src/core/shader/compile.ts` turns a graph into the fragment (every node reachable from the Output becomes one local per output, in dependency order; -every Property a `uniform float u_`), `src/viewport/shaders.ts` wraps the result in a filter -and `src/viewport/ShaderLibrary.ts` caches it per asset. The rows come from +every Property a `uniform float u_`), `src/viewport/shaders.ts` wraps the result in a Pixi +`Shader` for the sprite's mesh (`GraphShader`, with a vertex stage of its own) and +`src/viewport/ShaderLibrary.ts` caches the compiled graph per asset. A sprite's picture is a `Mesh` +(a unit quad sized by its scale, `symbols.ts`), a sibling of the sprite's `Graphics` node under the +same parent: Pixi 8 lets only plain containers have children, and the node keeps just the hit area. +Two Pixi facts the code leans on: a mesh's texture is bound only for Pixi's own shader, so +`GraphShader.wear` hands it over itself; and Pixi caches whether a mesh batches and re-checks that +only in a frame without structure changes, so switching a shader on or off calls `mesh.unload()` +to drop that cache — otherwise a mesh that ever drew batched keeps drawing plain, shader ignored. The rows come from `shaderParamDefs(graph)` in `src/core/properties/shaders.ts`; `schemaForAsset` and `schemaForObject` (`PropertySchema.ts`) append them to a sprite's schema, which is why both take the project.