Skip to content

Create WebGL sprite data textures in rows of 256 - #2938

Merged
pvcraven merged 1 commit into
developmentfrom
fix/webgl-sprite-capacity
Oct 7, 2026
Merged

pvcraven merged 1 commit into
developmentfrom
fix/webgl-sprite-capacity

Conversation

@pvcraven

@pvcraven pvcraven commented Oct 7, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #2909: on WebGL, a SpriteList whose storage was created with a capacity over 256 only drew its first 256 sprites.

The problem

On WebGL, sprite data is stored in textures (SpriteListTextureData) instead of buffers:

  • The shader reads them as rows of 256 sprites (lib/sprite.glsl: texelFetch(..., ivec2(index % 256, index / 256), 0)), and so does the GPU collision shader.
  • The grow_* methods resize them to (256, rows).
  • __init__ created them as one row as wide as the capacity. So with a capacity over 256, sprite i >= 256 was read from row i // 256, which didn't exist.

That happens with:

  • SpriteList(capacity=1000);
  • clear() on a list that had grown past 256 sprites, since it keeps the capacity and creates the storage again;
  • a lazy list, or a list created before the window, with more than 256 sprites before it's first drawn.

The fix

Create the five textures as (256, capacity // 256), the layout the shader reads and the grow methods keep. The capacity is always a multiple of 256, and I align it anyway in case a subclass passes one that isn't. Uploads are whole-texture, row-major writes, so they don't change.

Verified

I reproduced it on desktop OpenGL by making SpriteList use the WebGL storage, drawing 1-pixel sprites into a framebuffer, and counting lit pixels:

Case Before After
Created at 256, grown by appending 600 / 600 600 / 600
Created at capacity 1024 256 / 600 600 / 600
Grown past 256, clear(), then 400 more 256 / 400 400 / 400

Tests

New tests/unit/spritelist/test_spritelist_texture_storage.py. It runs on desktop GL in CI by monkeypatching SpriteList to use SpriteListTextureData:

  • test_created_with_capacity with capacities 100, 256, 1000, 1024 and 3000, drawing 600 sprites;
  • test_clear_after_growing;
  • test_lazy_list_grown_before_first_draw;
  • test_storage_is_rows_of_256.

On development, 6 of the 8 fail. Capacities 100 and 256 pass, since they round up to a single 256-wide row either way.

Full suite: 1732 passed. Ruff is clean, and mypy has no new errors. I haven't run it in an actual browser build. A quick check there would be worth doing, but the shader and texture code are the same as the desktop path the tests use.

The changelog has an entry under Unreleased → Fixes.

🤖 Generated with Claude Code

On WebGL, a SpriteList stores its sprite data in textures, which the
shader reads as rows of 256 sprites and the grow methods resize to
(256, rows). SpriteListTextureData created them as one row as wide as the
capacity, so with a capacity over 256, sprites past the first 256 were
read from rows that didn't exist and weren't drawn: SpriteList(capacity=
1000), clear() on a list that had grown past 256, or a lazy list with
more than 256 sprites before its first draw.

Create them as (256, capacity // 256). Uploads are row-major whole
texture writes, so they don't change.

Fixes #2909

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pvcraven
pvcraven merged commit 04fb717 into development Oct 7, 2026
7 checks passed
@pvcraven
pvcraven deleted the fix/webgl-sprite-capacity branch October 7, 2026 17:44
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.

WebGL: SpriteList created with capacity over 256 only draws 256 sprites

1 participant