Skip to content

feat(rendering): geometry pixel snapping on the GPU — closes R2#389

Merged
Exoridus merged 6 commits into
mainfrom
worktree-infinite-maps-slice2
Jul 23, 2026
Merged

feat(rendering): geometry pixel snapping on the GPU — closes R2#389
Exoridus merged 6 commits into
mainfrom
worktree-infinite-maps-slice2

Conversation

@Exoridus

Copy link
Copy Markdown
Owner

Second and final PR of the GPU pixel-snapping track. PR 1 (#388) made the enum break and moved position snapping into the vertex shaders. This PR moves geometry boundary snapping there too, then deletes the entire CPU snap path and the last retained-recording exceptions — closing roadmap R2 completely (snapping is now one GPU mechanism).

What changes

  • Boundary snap in the vertex shaders (Sprite, NineSlice, Repeating; both backends). Each local quad boundary is rounded to the device grid (floor(local · scale + 0.5) / scale) from the same composed pipeline the origin snap uses, gated on the row flag (m1.z == 2) and only when the composed transform is axis-aligned (decided in-shader from the linear part's cross-terms). Because a shared edge is the same absolute bit value and the round is pure, seams stay closed with no instance-format change.
  • Repeating destW/destH derived from the snapped corners (shader strategy), so the tile period stays aligned to the snapped destination width.
  • Demolition. Deleted the CPU snap path (snapQuadsInto, snapBoundsInto, buildPixelSnapContext, snapLocalBoundary, resolveEffectivePixelSnapMode, the render-quad helpers) plus Sprite.getRenderBounds / NineSlice/Repeating getRenderQuads/getRenderBounds and the dead _getSnapPixelSize backend methods. pixelSnap.ts now exports only PixelSnapMode + isPixelSnapMode.
  • Geometry joins retained recording. Dropped the geometry clause from the recordability predicate and the direct-child patch bailout, and removed the 8 renderer poison guards (sprite/nine-slice/repeating × 2 backends + both tile-chunk renderers). Geometry-snapped rows/quads are now view-independent, so geometry-snapped sprites and tilemaps are fully recordable.
  • Tilemaps were already GPU after PR 1 — they snap only the chunk origin and keep integer chunk-local tile pitch, so their geometry guards simply drop; no per-tile boundary snap is added.

Testing

  • Browser pixel tests (both backends): new Repeating cases — shader-strategy edge snap under fractional zoom + geometry-strategy seam closure; each verified RED→GREEN (disabling the shader block blends the fractional far edge).
  • rendering-perf structural gates: geometry-snapped sprites and a geometry-snapped tilemap inside a RetainedContainer now reach the recorded tier under a camera pan (zero instance/transform re-upload); the old "geometry still poisons" gate is replaced.
  • Full local gate green: tsc (engine + tilemap), eslint, prettier, unit + perf, and the full WebGL2 (262) and WebGPU (186) browser suites.
  • API-docs regeneration is a no-op (all deleted symbols were @internal).

Behavior note: the once-per-frame geometry downgraded to position dev-warn is removed with the CPU path. The downgrade still happens, now silently in-shader.

@Exoridus
Exoridus enabled auto-merge (squash) July 19, 2026 10:36
@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 292.59kB (1.23%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
exo-esm-esm 850.36kB 32.94kB (4.03%) ⬆️
exo-esm-modules-esm 3.74MB 150.09kB (4.18%) ⬆️
exo-iife-min-Exo-iife 857.98kB 33.14kB (4.02%) ⬆️
exojs-react-esm 33.9kB 1.38kB (4.25%) ⬆️
exo-full-iife-Exo-iife 2.72MB 75.31kB (2.85%) ⬆️
exojs-physics-esm 331.13kB 963 bytes (0.29%) ⬆️
exojs-tilemap-esm 369.75kB -1.23kB (-0.33%) ⬇️

Affected Assets, Files, and Routes:

view changes for bundle: exo-iife-min-Exo-iife

Assets Changed:

Asset Name Size Change Total Size Change (%)
exo.iife.min.js 33.14kB 857.98kB 4.02%

Files in exo.iife.min.js:

  • ./src/rendering/sprite/NineSliceSprite.ts → Total Size: 6.37kB

  • ./src/rendering/sprite/Sprite.ts → Total Size: 15.95kB

  • ./src/rendering/sprite/RepeatingSprite.ts → Total Size: 7.82kB

  • ./src/rendering/webgl2/WebGl2Backend.ts → Total Size: 76.66kB

  • ./src/rendering/webgl2/WebGl2NineSliceSpriteRenderer.ts → Total Size: 24.17kB

  • ./src/rendering/pixelSnap.ts → Total Size: 5.22kB

  • ./src/rendering/webgpu/WebGpuBackend.ts → Total Size: 89.49kB

  • ./src/rendering/webgpu/WebGpuRepeatingSpriteRenderer.ts → Total Size: 47.18kB

  • ./src/rendering/webgpu/WebGpuSpriteRenderer.ts → Total Size: 67.63kB

  • ./src/rendering/plan/RetainedInstructionSet.ts → Total Size: 7.96kB

  • ./src/rendering/webgl2/WebGl2SpriteRenderer.ts → Total Size: 32.1kB

  • ./src/rendering/webgl2/WebGl2RepeatingSpriteRenderer.ts → Total Size: 38.13kB

  • ./src/rendering/webgl2/glsl/sprite.vert → Total Size: 5.29kB

  • ./src/rendering/webgpu/WebGpuNineSliceSpriteRenderer.ts → Total Size: 35.39kB

  • ./src/rendering/RetainedContainer.ts → Total Size: 29.7kB

  • ./src/rendering/sprite/spriteMaterialSources.ts → Total Size: 11.08kB

view changes for bundle: exo-esm-modules-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
rendering/webgpu/WebGpuBackend.js -460 bytes 91.52kB -0.5%
rendering/webgl2/WebGl2Backend.js -605 bytes 78.21kB -0.77%
rendering/webgpu/WebGpuSpriteRenderer.js 1.07kB 68.82kB 1.58%
rendering/webgpu/WebGpuRepeatingSpriteRenderer.js 1.82kB 48.02kB 3.93%
core/Application.js 1.38kB 42.97kB 3.31%
rendering/webgl2/WebGl2RepeatingSpriteRenderer.js 2.87kB 38.7kB 8.02% ⚠️
input/InteractionManager.js 1.31kB 38.56kB 3.53%
rendering/webgpu/WebGpuNineSliceSpriteRenderer.js 1.47kB 36.13kB 4.25%
rendering/webgl2/WebGl2SpriteRenderer.js -276 bytes 32.84kB -0.83%
rendering/RetainedContainer.js -237 bytes 30.38kB -0.77%
input/InputManager.js 265 bytes 29.92kB 0.89%
core/SceneDirector.js (New) 25.44kB 25.44kB 100.0% 🚀
rendering/webgl2/WebGl2NineSliceSpriteRenderer.js 1.33kB 24.66kB 5.72% ⚠️
rendering/webgpu/WebGpuBackend.d.ts -462 bytes 23.01kB -1.97%
core/Application.d.ts 1.72kB 22.87kB 8.15% ⚠️
audio/BaseVoice.js 8.46kB 20.34kB 71.27% ⚠️
rendering/webgl2/WebGl2Backend.d.ts -462 bytes 19.46kB -2.32%
audio/Sound.js -3.35kB 16.81kB -16.61%
rendering/RenderingContext.js 265 bytes 16.76kB 1.61%
rendering/sprite/Sprite.js -1.67kB 16.19kB -9.35%
core/Scene.js 470 bytes 15.62kB 3.1%
core/Scene.d.ts 952 bytes 13.96kB 7.32% ⚠️
input/InteractionManager.d.ts 933 bytes 13.86kB 7.22% ⚠️
core/SceneScope.js (New) 13.66kB 13.66kB 100.0% 🚀
core/SceneDirector.d.ts (New) 13.65kB 13.65kB 100.0% 🚀
rendering/webgpu/WebGpuRepeatingSpriteRenderer.d.ts 2.71kB 12.7kB 27.14% ⚠️
index.js 321 bytes 11.58kB 2.85%
rendering/sprite/spriteMaterialSources.d.ts 3.22kB 11.38kB 39.43% ⚠️
rendering/RenderingContext.d.ts 181 bytes 11.21kB 1.64%
rendering/sprite/spriteMaterialSources.js 3.15kB 11.19kB 39.22% ⚠️
core/scene/SceneAudio.js (New) 10.0kB 10.0kB 100.0% 🚀
rendering/webgpu/WebGpuNineSliceSpriteRenderer.d.ts 1.95kB 9.87kB 24.7% ⚠️
rendering/plan/RetainedInstructionSet.d.ts -12 bytes 9.81kB -0.12%
rendering/webgpu/WebGpuSpriteRenderer.d.ts 30 bytes 9.49kB 0.32%
input/InputManager.d.ts 171 bytes 9.33kB 1.87%
debug/RenderPassInspectorLayer.js 1 bytes 9.31kB 0.01%
audio/Playable.d.ts 3.49kB 9.09kB 62.3% ⚠️
core/SystemRegistry.js 5.3kB 8.77kB 152.75% ⚠️
audio/AudioManager.js 186 bytes 8.75kB 2.17%
audio/Sound.d.ts -1.97kB 8.69kB -18.52%
audio/BaseVoice.d.ts 3.29kB 8.51kB 62.92% ⚠️
extensions/Extension.d.ts 825 bytes 8.46kB 10.81% ⚠️
rendering/plan/RetainedInstructionSet.js -114 bytes 8.29kB -1.36%
rendering/sprite/RepeatingSprite.js -2.73kB 8.14kB -25.12%
debug/PerformanceLayer.js 1 bytes 7.43kB 0.01%
core/scene/SceneTweens.js (New) 7.02kB 7.02kB 100.0% 🚀
core/scene/SceneInputs.js (New) 6.82kB 6.82kB 100.0% 🚀
core/SceneScope.d.ts (New) 6.78kB 6.78kB 100.0% 🚀
rendering/sprite/NineSliceSprite.js -1.72kB 6.69kB -20.43%
core/SceneTypes.d.ts (New) 6.41kB 6.41kB 100.0% 🚀
audio/AudioGenerator.js 115 bytes 6.24kB 1.88%
input/FocusManager.js 1 bytes 6.12kB 0.02%
audio/SoundVoice.js 199 bytes 6.05kB 3.4%
audio/spatial-*.d.ts 2.74kB 6.04kB 83.12% ⚠️
audio/spatial-*.d.ts (New) 632 bytes 632 bytes 100.0% 🚀
rendering/sprite/Sprite.d.ts -846 bytes 5.98kB -12.39%
core/scene/SceneInteraction.js (New) 5.97kB 5.97kB 100.0% 🚀
debug/PointerStackLayer.js 1 bytes 5.64kB 0.02%
audio/AudioManager.d.ts 92 bytes 5.57kB 1.68%
audio/AudioListener.js 718 bytes 5.56kB 14.84% ⚠️
animation/TweenManager.js 224 bytes 5.49kB 4.25%
audio/spatial-*.js -1.23kB 1.99kB -38.15%
audio/spatial-*.js (New) 5.45kB 5.45kB 100.0% 🚀
rendering/webgl2/glsl/sprite.vert.js 1.8kB 5.38kB 50.46% ⚠️
audio/AudioStreamVoice.js 719 bytes 5.33kB 15.6% ⚠️
rendering/pixelSnap.js -6.37kB 5.3kB -54.55%
core/SceneTypes.js (New) 5.28kB 5.28kB 100.0% 🚀
rendering/pixelSnap.d.ts -5.57kB 5.01kB -52.66%
rendering/webgl2/WebGl2RepeatingSpriteRenderer.d.ts -34 bytes 4.85kB -0.7%
core/scene/SceneAudio.d.ts (New) 4.83kB 4.83kB 100.0% 🚀
rendering/webgl2/WebGl2SpriteRenderer.d.ts 30 bytes 4.64kB 0.65%
core/scene/SceneInputs.d.ts (New) 4.5kB 4.5kB 100.0% 🚀
debug/HitTestLayer.js 1 bytes 4.37kB 0.02%
audio/AudioStream.js 115 bytes 4.37kB 2.7%
core/SystemRegistry.d.ts 2.56kB 4.29kB 147.89% ⚠️
core/scene/SceneInteraction.d.ts (New) 4.11kB 4.11kB 100.0% 🚀
animation/TweenManager.d.ts 130 bytes 4.01kB 3.35%
rendering/sprite/RepeatingSprite.d.ts -1.08kB 3.99kB -21.27%
debug/BoundingBoxesLayer.js 1 bytes 3.94kB 0.03%
extensions/materialize.js 577 bytes 3.81kB 17.84% ⚠️
core/scene/SceneTweens.d.ts (New) 3.69kB 3.69kB 100.0% 🚀
extensions/snapshot.js 236 bytes 3.37kB 7.53% ⚠️
audio/AudioStreamVoice.d.ts 509 bytes 2.9kB 21.32% ⚠️
core/index.d.ts 654 bytes 2.65kB 32.68% ⚠️
audio/SoundVoice.d.ts 54 bytes 2.53kB 2.18%
rendering/sprite/NineSliceSprite.d.ts -912 bytes 2.43kB -27.3%
audio/AudioListener.d.ts 294 bytes 2.42kB 13.8% ⚠️
core/System.d.ts 1.41kB 2.39kB 143.28% ⚠️
core/SceneState.js (New) 2.23kB 2.23kB 100.0% 🚀
core/scene/SceneLoader.d.ts (New) 2.2kB 2.2kB 100.0% 🚀
core/SceneState.d.ts (New) 1.99kB 1.99kB 100.0% 🚀
ui/UIRoot.js 1 bytes 1.93kB 0.05%
extensions/materialize.d.ts 517 bytes 1.74kB 42.31% ⚠️
ui/UIRoot.d.ts 1 bytes 1.49kB 0.07%
extensions/snapshot.d.ts 85 bytes 1.38kB 6.55% ⚠️
core/SystemOrder.js (New) 1.06kB 1.06kB 100.0% 🚀
core/scene/SceneLoader.js (New) 927 bytes 927 bytes 100.0% 🚀
core/SystemOrder.d.ts (New) 832 bytes 832 bytes 100.0% 🚀
extensions/index.d.ts 26 bytes 246 bytes 11.82% ⚠️
core/SceneManager.js (Deleted) -12.65kB 0 bytes -100.0% 🗑️
core/SceneManager.d.ts (Deleted) -3.62kB 0 bytes -100.0% 🗑️

Files in rendering/webgpu/WebGpuBackend.js:

  • ./src/rendering/webgpu/WebGpuBackend.ts → Total Size: 89.41kB

Files in rendering/webgl2/WebGl2Backend.js:

  • ./src/rendering/webgl2/WebGl2Backend.ts → Total Size: 76.61kB

Files in rendering/webgpu/WebGpuSpriteRenderer.js:

  • ./src/rendering/webgpu/WebGpuSpriteRenderer.ts → Total Size: 67.56kB

Files in rendering/webgpu/WebGpuRepeatingSpriteRenderer.js:

  • ./src/rendering/webgpu/WebGpuRepeatingSpriteRenderer.ts → Total Size: 47.1kB

Files in rendering/webgl2/WebGl2RepeatingSpriteRenderer.js:

  • ./src/rendering/webgl2/WebGl2RepeatingSpriteRenderer.ts → Total Size: 37.96kB

Files in rendering/webgpu/WebGpuNineSliceSpriteRenderer.js:

  • ./src/rendering/webgpu/WebGpuNineSliceSpriteRenderer.ts → Total Size: 35.29kB

Files in rendering/webgl2/WebGl2SpriteRenderer.js:

  • ./src/rendering/webgl2/WebGl2SpriteRenderer.ts → Total Size: 31.99kB

Files in rendering/RetainedContainer.js:

  • ./src/rendering/RetainedContainer.ts → Total Size: 29.7kB

Files in rendering/webgl2/WebGl2NineSliceSpriteRenderer.js:

  • ./src/rendering/webgl2/WebGl2NineSliceSpriteRenderer.ts → Total Size: 24.07kB

Files in rendering/sprite/Sprite.js:

  • ./src/rendering/sprite/Sprite.ts → Total Size: 15.82kB

Files in rendering/sprite/spriteMaterialSources.js:

  • ./src/rendering/sprite/spriteMaterialSources.ts → Total Size: 11.08kB

Files in rendering/plan/RetainedInstructionSet.js:

  • ./src/rendering/plan/RetainedInstructionSet.ts → Total Size: 7.96kB

Files in rendering/sprite/RepeatingSprite.js:

  • ./src/rendering/sprite/RepeatingSprite.ts → Total Size: 7.82kB

Files in rendering/sprite/NineSliceSprite.js:

  • ./src/rendering/sprite/NineSliceSprite.ts → Total Size: 6.37kB

Files in rendering/webgl2/glsl/sprite.vert.js:

  • ./src/rendering/webgl2/glsl/sprite.vert → Total Size: 5.29kB

Files in rendering/pixelSnap.js:

  • ./src/rendering/pixelSnap.ts → Total Size: 5.17kB
view changes for bundle: exojs-react-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
Scenes.js 472 bytes 4.94kB 10.57% ⚠️
useScene.js 602 bytes 4.03kB 17.57% ⚠️
Scenes.d.ts 101 bytes 2.69kB 3.91%
useScene.d.ts 207 bytes 1.75kB 13.46% ⚠️
view changes for bundle: exo-esm-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
exo.esm.js 32.94kB 850.36kB 4.03%

Files in exo.esm.js:

  • ./src/rendering/RetainedContainer.ts → Total Size: 29.7kB

  • ./src/rendering/sprite/Sprite.ts → Total Size: 15.82kB

  • ./src/rendering/plan/RetainedInstructionSet.ts → Total Size: 7.96kB

  • ./src/rendering/webgpu/WebGpuNineSliceSpriteRenderer.ts → Total Size: 35.34kB

  • ./src/rendering/webgpu/WebGpuSpriteRenderer.ts → Total Size: 67.57kB

  • ./src/rendering/webgpu/WebGpuRepeatingSpriteRenderer.ts → Total Size: 47.12kB

  • ./src/rendering/webgl2/WebGl2Backend.ts → Total Size: 76.61kB

  • ./src/rendering/sprite/RepeatingSprite.ts → Total Size: 7.82kB

  • ./src/rendering/webgl2/WebGl2RepeatingSpriteRenderer.ts → Total Size: 38.02kB

  • ./src/rendering/webgpu/WebGpuBackend.ts → Total Size: 89.41kB

  • ./src/rendering/pixelSnap.ts → Total Size: 5.17kB

  • ./src/rendering/sprite/spriteMaterialSources.ts → Total Size: 11.08kB

  • ./src/rendering/webgl2/glsl/sprite.vert → Total Size: 5.29kB

  • ./src/rendering/webgl2/WebGl2SpriteRenderer.ts → Total Size: 32.05kB

  • ./src/rendering/sprite/NineSliceSprite.ts → Total Size: 6.37kB

  • ./src/rendering/webgl2/WebGl2NineSliceSpriteRenderer.ts → Total Size: 24.12kB

view changes for bundle: exojs-physics-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
PhysicsWorld.js 553 bytes 36.21kB 1.55%
PhysicsWorld.d.ts 416 bytes 19.02kB 2.24%
Aabb.js -6 bytes 775 bytes -0.77%
view changes for bundle: exojs-tilemap-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
webgpu/WebGpuTileChunkRenderer.js -621 bytes 28.57kB -2.13%
webgl2/WebGl2TileChunkRenderer.js -613 bytes 24.42kB -2.45%

Files in webgpu/WebGpuTileChunkRenderer.js:

  • ./src/webgpu/WebGpuTileChunkRenderer.ts → Total Size: 28.08kB

Files in webgl2/WebGl2TileChunkRenderer.js:

  • ./src/webgl2/WebGl2TileChunkRenderer.ts → Total Size: 24.04kB
view changes for bundle: exo-full-iife-Exo-iife

Assets Changed:

Asset Name Size Change Total Size Change (%)
exo.full.iife.js 75.31kB 2.72MB 2.85%

Files in exo.full.iife.js:

  • ./packages/exojs-tilemap/src/webgl2/WebGl2TileChunkRenderer.ts → Total Size: 19.42kB

  • ./packages/exojs-tilemap/src/webgpu/WebGpuTileChunkRenderer.ts → Total Size: 22.75kB

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

…-snapped drawables

The "CPU snap path deleted" commit (2295709) intentionally made both
pixel-snap modes fully GPU-resolved and therefore recordable — the
isRetainedFragmentRecordable() predicate no longer inspects pixelSnapMode
at all. These two tests still asserted the pre-change expectation
(geometry-snap = non-recordable / poisons the recording), which the CI
run for this PR has been failing on since 2026-07-19.
@Exoridus
Exoridus merged commit 65861c9 into main Jul 23, 2026
17 of 18 checks passed
@Exoridus
Exoridus deleted the worktree-infinite-maps-slice2 branch July 23, 2026 08:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant