Skip to content

fix(video): batchers + texture-cache bug-hunt batch — 15 confirmed findings#1546

Merged
obiot merged 5 commits into
masterfrom
fix/batcher-texture-cache-bug-hunt
Jul 9, 2026
Merged

fix(video): batchers + texture-cache bug-hunt batch — 15 confirmed findings#1546
obiot merged 5 commits into
masterfrom
fix/batcher-texture-cache-bug-hunt

Conversation

@obiot

@obiot obiot commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Beat 2 of the adversarial bug hunt: batchers + texture cache (~4,200 lines swept). One deep-reading finder agent produced 14 findings; all 14 were hand-verified against source with traced call paths before any fix, and a 15th (mesh GL buffers dead after context restore) surfaced during verification. All 15 are fixed here, in two cluster commits.

Atlas / parser cluster (commit 1 — pure JS, 11 failing-first tests)

Fix Symptom
aseprite parser read trimmed/spriteSourceSize/sourceSize/pivot/rotated one level too deep trimmed exports jitter per frame; sourceSize missing
aseprite tag speed was 10×(frameCount−1) ms/frame 2-frame tag ran at 100 fps; authored per-frame durations ignored
addUVs alias key used texture dims, not region dims (#1281 workaround) sub-region lookups duplicated regions; a frame at (0,0) hijacked full-image draws
no-arg createAnimationFromName/getAnimationSettings iterated alias keys + anims frames doubled; NaN frame sizes on aseprite atlases
addRegion on unsized video divided by width = 0 Infinity/NaN UVs — invisible quad (WebGL-only parity bug)
padded-spritesheet truncation rewrote the UV divisor frames sampled shifted under WebGL (Canvas fine)
atlases.length === 0 on a Map "format not supported" unreachable; malformed atlases constructed empty

GL-state cluster (commit 2 — 9 tests, key fixes mutation-verified)

Fix Symptom
lit normal-map units never reserved in the unit allocator (HIGH) with a Light2d + normal-mapped sprite + ≥9 distinct textures: unlit sprites render the normal map, or lit sprites shade with color data as normals. Reservation is lazy (first lit bind) so unlit games keep the full pool; ShaderEffect.setTexture samplers skip reserved units
per-batcher tracking of global gl.activeTexture video force-re-upload after a mesh pass overwrote the mesh texture with video frames. Active unit now tracked once per renderer; uploads force-activate their unit
unit-0 invalidation only reached the current batcher under camera post-effects, another batcher's unit-0 texture rendered opaque black. New renderer.invalidateTextureUnit() reaches all batchers
no chunking in PrimitiveBatcher.drawVertices fillEllipse at radius ≳ 435 px: GL errors, shape (and batch-mates) vanish. Chunked on primitive boundaries, exact for all modes
NoiseTexture2d.destroy() leaked its GL normal-map texture + canvas unbounded growth on per-level noise maps. New event.TEXTURE2D_DESTROYED + lit-batcher eviction
module-level mesh depth-clear flag two Applications on one page could steal/re-arm each other's depth clear. Per-renderer flag; RENDER_TARGET_CHANGED now carries the emitting renderer
GL buffer lifetime quad index buffer leaked per GAME_RESET; mesh index/vertex buffers stayed bound to the lost context after restore (uploads silently failed). Delete + recreate on reset

Verification

  • Cluster 1 tests written failing-first: 11/11 fail on master, 11/11 pass here
  • Cluster 2: 9/9 pass; 3 subtlest fixes verified by mutation (each mutation fails exactly its matching test)
  • Full suite: 4773 passed | 15 skipped, 0 failed; pnpm lint + biome clean; pnpm build green
  • Existing suites guarding the touched paths (webgl_mesh_depth, drawVertices, all 5 shader specs, tmxlayer-shader, texture-resource, texture, sprite, aseprite-parser) all untouched-green

Known-open, deliberately not in scope: #1489 (framewidth refinement no-op) and the anchorPoint first-wins atlas contamination — both are shared-atlas semantics questions for the #1410 refactor rather than point fixes.

🤖 Generated with Claude Code

https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A

Copilot AI review requested due to automatic review settings July 9, 2026 08:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

obiot and others added 4 commits July 9, 2026 16:55
…liases, video regions)

Seven confirmed findings from the batchers + texture-cache adversarial
sweep, atlas/parser cluster — all reproduced by failing-first tests in
the new texture-atlas-parsers.spec.js (11 tests):

- aseprite parser read trimmed/spriteSourceSize/sourceSize/pivot/rotated
  off the frame rect instead of the frame entry (siblings, as in the
  TexturePacker layout) — trim silently ignored, sourceSize missing
- aseprite animation speed was 10*(frameCount-1) ms/frame; tags now
  carry the authored per-frame `duration` as frame-object delays
- addUVs coordinate-alias key used texture dims instead of region dims:
  sub-region lookups never hit (duplicate regions), and a frame at (0,0)
  poisoned full-image lookups (drew frame 0 stretched)
- no-arg createAnimationFromName/getAnimationSettings iterated alias
  keys (every frame twice) and the aseprite `anims` entry (NaN sizes)
- addRegion on an unsized video divided by width=0 → Infinity/NaN UVs
  (videoWidth fallback, matching uploadTexture/TextureCache.get)
- padded spritesheet "truncation" rewrote the UV divisor while the GL
  texture keeps its physical size — frames sampled shifted under WebGL
- `atlases.length === 0` dead guard on a Map — unsupported-format error
  was unreachable, malformed atlases constructed empty

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
…c, buffer lifetime)

Eight confirmed findings from the batchers + texture-cache adversarial
sweep, GL-state cluster — covered by the new webgl_batcher_state.spec.js
(9 tests, key fixes verified by mutation):

- lit normal maps bound at maxBatchTextures+slot by fixed arithmetic
  while the unit allocator handed those same units to color textures —
  whoever bound second clobbered the other (unlit sprites showing the
  normal map / lit sprites shading with color data). LitQuadBatcher now
  reserves its normal range on first bind (lazily — unlit games keep the
  full pool); ShaderEffect extra samplers skip units reserved by others
- per-batcher tracking of the global gl.activeTexture state desynced
  after another batcher/FBO moved it, landing texImage2D re-uploads on
  foreign textures (video frames overwriting a mesh texture). The active
  unit now lives on the renderer (shared accessor); createTexture2D
  force-activates its target unit before uploading
- post-effect FBO setup + blitTexture null unit 0's binding but only
  invalidated the current batcher — other batchers sampled an unbound
  texture (opaque black). New renderer.invalidateTextureUnit() reaches
  every batcher
- PrimitiveBatcher.drawVertices had no chunking: a filled ellipse at
  radius ≳ 435px overflowed the 4096-vertex buffer (silently-dropped
  typed-array writes + GL errors, nothing rendered). Oversized shapes
  now chunk on primitive boundaries; thick-line expansion checks
  capacity per pair
- NoiseTexture2d.destroy() leaked the lit batcher's cached GL texture +
  baked canvas (strong-keyed map, only emptied on full reset). New
  event.TEXTURE2D_DESTROYED emitted by Texture2d.destroy(); the lit
  batcher evicts in response
- module-level mesh depth-clear flag was shared across renderer
  instances; now per-renderer, and RENDER_TARGET_CHANGED carries the
  emitting renderer so subscribers ignore foreign broadcasts
- quad batchers leaked a GL index buffer on every GAME_RESET (recreate
  without delete); mesh batchers kept lost-context index/vertex buffers
  after webglcontextrestored (uploads silently failed). Both families
  now delete + recreate on reset (WebGLIndexBuffer.destroy/recreate)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
…with #1543)

#1543 widened Texture2d.getTexture() to the new Texture2dSource union
(GPU-resident backings incl. ImageBitmap); the event signature added in
this branch predated that and was too narrow to typecheck.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
@obiot obiot force-pushed the fix/batcher-texture-cache-bug-hunt branch from bb62cdf to 6e93a06 Compare July 9, 2026 09:02
Copilot AI review requested due to automatic review settings July 9, 2026 09:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…overlay

Since the 19.5 examples-shell restyle (#1454), #screen is a fixed overlay
covering everything below the topbar; the tiledMapLoader/spine selectors
were floated above it (absolute + zIndex 1000) but the aseprite one stayed
in normal flow and has been painted over ever since. Float it the same way,
match the house control styling, and default the select to "run front" so
it agrees with the animation the entity actually starts with.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
Copilot AI review requested due to automatic review settings July 9, 2026 10:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@obiot obiot merged commit 962690d into master Jul 9, 2026
6 checks passed
@obiot obiot deleted the fix/batcher-texture-cache-bug-hunt branch July 9, 2026 23:24
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.

2 participants