Shaders draw in the sprite's own pass: a Mesh with the graph's fragment, no filter (#77)
game-maker / suite (push) Successful in 8m24s

A sprite's picture is now a Mesh (unit quad, sibling of its Graphics node, which keeps
only the hit area); a worn shader replaces Pixi's fragment in that draw, so there is no
intermediate texture and no extra render pass. Measured on the HD 630: 400 shaded
sprites hold 60 fps (was 34), 800 hold 56 (was 20).

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; and MeshPipe caches whether a mesh batches and
re-checks it only in a frame without structure changes, so switching a shader on or
off calls mesh.unload().

The console panel mirrors console.group/groupCollapsed/groupEnd and fills the %s %d
%o %c directives, so Pixi's deprecation warning shows its head, not only its stack.
E2E 12 guards against the addChild-on-Graphics warning, 28 counts shaders instead of
filters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-13 15:31:08 +02:00
1 parent 82effb2c03
commit 0e5e5ef925
15 files changed
+328 -88

No files matched your search

+14 -11
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env node
/**
* What a shader costs: the same scene of N sprites drawn with and without `Adjust colors`, measured
* as frame time in the running game. Every sprite wearing a shader gets its own Pixi filter, so it
* gets its own render pass into an intermediate texture and back (#67).
* as frame time in the running game. Up to #67 every sprite wearing a shader got its own Pixi
* filter, so its own render pass into an intermediate texture and back; since #77 the fragment
* runs in the sprite's own draw, which leaves the batch – one draw call per shaded sprite, no pass.
*
* The values are neutral (1), so both passes put the very same pixels on screen – the only
* difference between the two runs is the pass itself.
* The values are neutral (1), so both runs put the very same pixels on screen – the only
* difference between them is how the sprite is drawn.
*
* Usage: node tools/seed-e2e.mjs && node tools/perf-shaders.mjs
* GM_PERF_COUNTS=... sprite counts, GM_PERF_SIZE=... edge length of one sprite
@@ -89,6 +90,7 @@ await runChecks({ projectId, baseUrl, port: 9347, shotPrefix: 'gm-perf' }, async
await sleep(SAMPLE_MS);
const frames = await json('(window.__perfOn = false, window.__perfSamples.slice(1))');
const reported = await json('__gm.fps()');
const shaders = await json('__gm.shaderCount()');
const filters = await json('__gm.filterCount()');
const elapsed = frames.reduce((a, b) => a + b, 0);
return {
@@ -98,6 +100,7 @@ await runChecks({ projectId, baseUrl, port: 9347, shotPrefix: 'gm-perf' }, async
p95: round(percentile(frames, 0.95)),
fps: round((frames.length * 1000) / elapsed, 1),
reported,
shaders,
filters,
};
};
@@ -114,15 +117,15 @@ await runChecks({ projectId, baseUrl, port: 9347, shotPrefix: 'gm-perf' }, async
await stop();
}
check(
`${n} sprites: a filter per sprite with the shader on, none without`,
run.on.filters === n && run.off.filters === 0,
{ on: run.on.filters, off: run.off.filters },
`${n} sprites: a shader per sprite and no filter with the shader on, neither without`,
run.on.shaders === n && run.on.filters === 0 && run.off.shaders === 0 && run.off.filters === 0,
{ on: run.on, off: run.off },
);
rows.push({ n, ...run });
console.log(
` n=${String(n).padStart(4)} off ${String(run.off.mean).padStart(6)} ms (${run.off.fps} fps)` +
` on ${String(run.on.mean).padStart(6)} ms (${run.on.fps} fps)` +
` +${round(((run.on.mean - run.off.mean) * 1000) / n, 1)} us/pass load ${run.off.load} / ${run.on.load}`,
` +${round(((run.on.mean - run.off.mean) * 1000) / n, 1)} us/sprite load ${run.off.load} / ${run.on.load}`,
);
}
@@ -145,12 +148,12 @@ await runChecks({ projectId, baseUrl, port: 9347, shotPrefix: 'gm-perf' }, async
`\nsprite ${SIZE}x${SIZE}, ${SAMPLE_MS} ms per point, neutral values, ` +
`60 Hz cap (cannot be lifted in headless)`,
);
console.log('| sprites | no shader | with shader | per pass | filters |');
console.log('| --- | --- | --- | --- | --- |');
console.log('| sprites | no shader | with shader | per sprite | shaders | filters |');
console.log('| --- | --- | --- | --- | --- | --- |');
for (const r of rows)
console.log(
`| ${r.n} | ${r.off.mean} ms / ${r.off.fps} fps | ${r.on.mean} ms / ${r.on.fps} fps |` +
` ${round(((r.on.mean - r.off.mean) * 1000) / r.n, 1)} us | ${r.on.filters} |`,
` ${round(((r.on.mean - r.off.mean) * 1000) / r.n, 1)} us | ${r.on.shaders} | ${r.on.filters} |`,
);
console.log(JSON.stringify(rows));
});