From 66b60cece91be4dbcaeaea6522297ed10c997716 Mon Sep 17 00:00:00 2001 From: Exoridus <1218727+Exoridus@users.noreply.github.com> Date: Sun, 27 Sep 2026 04:18:23 +0200 Subject: [PATCH 1/2] fix(rendering): unify scalable sprite integrity --- packages/exojs-bench/docs/harness.md | 1 + .../src/webgl2/WebGl2TileChunkRenderer.ts | 6 +- .../src/components/BenchComparisonTable.astro | 30 +- site/src/components/BenchResultCard.astro | 47 +- .../src/components/pages/BenchmarksPage.astro | 3 +- .../guide/debugging/renderer-sdk-contract.mdx | 2 + site/src/lib/bench-cards.ts | 22 +- site/src/lib/bench-profiles.ts | 22 - src/rendering/coreRendererBindings.ts | 19 +- src/rendering/plan/DerivedRootProduct.ts | 4 +- src/rendering/plan/DerivedSelectionState.ts | 24 +- src/rendering/plan/RenderPlanBuilder.ts | 25 +- src/rendering/webgl2/WebGl2Backend.ts | 12 +- src/rendering/webgl2/WebGl2MeshRenderer.ts | 6 +- .../webgl2/WebGl2NineSliceSpriteRenderer.ts | 534 ------------ .../webgl2/WebGl2RetainedGroupResources.ts | 8 +- ...rer.ts => WebGl2ScalableSpriteRenderer.ts} | 69 +- src/rendering/webgl2/WebGl2SpriteRenderer.ts | 6 +- src/rendering/webgl2/WebGl2TextRenderer.ts | 8 +- src/rendering/webgl2/shaders/nine-slice.frag | 16 - src/rendering/webgl2/shaders/nine-slice.vert | 94 --- .../webgpu/WebGpuNineSliceSpriteRenderer.ts | 777 ------------------ ...rer.ts => WebGpuScalableSpriteRenderer.ts} | 45 +- src/rendering/webgpu/WebGpuTextRenderer.ts | 2 +- src/rendering/webgpu/shaders/nine-slice.wgsl | 112 --- .../retained-instruction-webgl2.test.ts | 62 +- .../structural-scalable-sprite.test.ts | 35 + ...webgl2-mobile-precision-regression.test.ts | 19 +- .../webgl2-scalable-sprite-state.test.ts | 32 + .../browser/webgl2-shader-compile.test.ts | 1 - ...2-text-retained-instruction-replay.test.ts | 4 +- .../browser/webgpu-shader-compile.test.ts | 6 +- .../rendering/derived-selection-state.test.ts | 34 +- .../persistent-slot-live-marks.test.ts | 32 + .../webgpu-affine-packing-parity.test.ts | 6 +- .../rendering/webgpu-pipeline-prewarm.test.ts | 58 +- .../webgpu-retained-record-replay.test.ts | 70 +- test/rendering/wgsl-naga-validation.test.ts | 6 +- test/site/bench-invalid-values.test.ts | 9 +- test/site/bench-opening-selection.test.ts | 1 - 40 files changed, 427 insertions(+), 1842 deletions(-) delete mode 100644 src/rendering/webgl2/WebGl2NineSliceSpriteRenderer.ts rename src/rendering/webgl2/{WebGl2RepeatingSpriteRenderer.ts => WebGl2ScalableSpriteRenderer.ts} (93%) delete mode 100644 src/rendering/webgl2/shaders/nine-slice.frag delete mode 100644 src/rendering/webgl2/shaders/nine-slice.vert delete mode 100644 src/rendering/webgpu/WebGpuNineSliceSpriteRenderer.ts rename src/rendering/webgpu/{WebGpuRepeatingSpriteRenderer.ts => WebGpuScalableSpriteRenderer.ts} (96%) delete mode 100644 src/rendering/webgpu/shaders/nine-slice.wgsl create mode 100644 test/perf/rendering/structural-scalable-sprite.test.ts create mode 100644 test/rendering/browser/webgl2-scalable-sprite-state.test.ts diff --git a/packages/exojs-bench/docs/harness.md b/packages/exojs-bench/docs/harness.md index 1e45c4f03..3fee35c0c 100644 --- a/packages/exojs-bench/docs/harness.md +++ b/packages/exojs-bench/docs/harness.md @@ -256,6 +256,7 @@ Rules the generator enforces rather than merely intends: - Every row names the mechanism behind its difference, drawn from the structural counters. A row whose mechanism cannot be evidenced is not published - it is listed under Omissions with the reason, so a dropped row stays auditable. - One column per competitor, no "best competitor" composite. Phaser occupies its own WebGL1 block, CPU time only, explicitly carrying no mechanism. - Cells where ExoJS loses are published exactly like the cells where it wins. +- A scenario whose arms turn out not to do the same work is repaired and re-measured. It is never published with a "not comparable" label in place of its comparison, and never hidden: a label would keep a known-unfair row on the page, and hiding it would let the choice of what to show depend on the outcome. ### Machine profiles diff --git a/packages/exojs-tilemap/src/webgl2/WebGl2TileChunkRenderer.ts b/packages/exojs-tilemap/src/webgl2/WebGl2TileChunkRenderer.ts index 07129c847..7425b94aa 100644 --- a/packages/exojs-tilemap/src/webgl2/WebGl2TileChunkRenderer.ts +++ b/packages/exojs-tilemap/src/webgl2/WebGl2TileChunkRenderer.ts @@ -408,7 +408,7 @@ export class WebGl2TileChunkRenderer extends AbstractWebGl2Renderer { {row.load} )} - {/* - * A row whose arms are known not to do the same work - * publishes their times and no comparison: the factor, - * the bar and the winner all go, the same way they do - * for a pair the clock could not separate. - */} {row.entries.map((entry, column) => { const cell = entry.cell; - const withheld = withheldScenario(row.archetype) !== undefined; if (cell === null) { const reason = @@ -248,7 +240,7 @@ const winnerOf = (cell: ProfileCell): string => { const mixed = outcome === 'unstable'; const level = outcome === 'level'; // A pair the clock never separated publishes neither a figure nor a bar; only its two times survive. - const unresolved = withheld || outcome === 'timer-limited' || outcome === 'timer-unknown'; + const unresolved = outcome === 'timer-limited' || outcome === 'timer-unknown'; const band = mixed ? ratioBand(cell) : null; const factor = mixed ? pooledFactor(cell) : cell.verdict.factor; const ratio = cell.verdict.ratio; @@ -279,16 +271,8 @@ const winnerOf = (cell: ProfileCell): string => { {/* A pair inside the noise band is named, not scored: its factor is pinned to 1 by the ladder, so printing 1.00x beside two medians that differ would claim a precision the ladder never measured. */} {unresolved ? '-' : mixed ? (factor === null ? '-' : formatApproximate(factor)) : level ? 'similar' : formatFactor(factor)} {/* The word alone; what it means is stated once in the legend rather than in every cell that carries it. */} - - {withheld - ? 'not comparable' - : unresolved - ? OUTCOME_STATUS[outcome] - : mixed - ? OUTCOME_STATUS.unstable - : level - ? '' - : winnerOf(cell)} + + {unresolved ? OUTCOME_STATUS[outcome] : mixed ? OUTCOME_STATUS.unstable : level ? '' : winnerOf(cell)} @@ -308,7 +292,6 @@ const winnerOf = (cell: ProfileCell): string => {
{/* The row already names its archetype; the label repeats it only where a reader reaches the summary without that context. */} Measurement detail - {withheldScenario(row.archetype) !== undefined &&

{withheldScenario(row.archetype)}

}
{row.entries.map((entry, column) => { const cell = entry.cell; @@ -796,13 +779,6 @@ const winnerOf = (cell: ProfileCell): string => { border-bottom: 1px solid var(--line-soft); } - .row-withheld { - margin: 0.4rem 0 0.6rem; - font-size: 0.8rem; - line-height: 1.45; - color: var(--fg-muted); - } - .row-more summary { cursor: pointer; font-family: var(--f-mono); diff --git a/site/src/components/BenchResultCard.astro b/site/src/components/BenchResultCard.astro index d032476d1..77a3c3e85 100644 --- a/site/src/components/BenchResultCard.astro +++ b/site/src/components/BenchResultCard.astro @@ -172,23 +172,15 @@ const statesOf = (load: BenchCard['loads'][number]): readonly CellOutcome[] => [ {card.loads.map(load => (