Skip to content

Track every texture that shares an atlas name - #2955

Merged
pvcraven merged 1 commit into
developmentfrom
fix/atlas-shared-texture-tracking
Oct 8, 2026
Merged

pvcraven merged 1 commit into
developmentfrom
fix/atlas-shared-texture-tracking

Conversation

@pvcraven

@pvcraven pvcraven commented Oct 8, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two DefaultTextureAtlas bugs with textures that share an image. One of them is likely behind an occasional CI failure: PR #2952's first run failed on Python 3.12, and the rerun passed.

FAILED tests/unit/sprite/test_sprite_collision.py::test_sprites_at_point
  - Exception: Texture 'sprite_solid_color|(0, 1, 2, 3)' not found in UVData

Background

Separate textures with the same image and vertex order share an atlas name and slot. For example, every SpriteSolidColor size shares one white image. The atlas counts the textures using each name, and frees the slot from a finalizer when the count reaches 0.

It kept only the first texture for each name in _unique_textures[name], though, so that set could disagree with the count. That causes two bugs.

Bug 1: resize() raises (reproduced every time)

  • Trigger: the first texture with a given image is freed while another texture with that image is still in use. The set is then empty but the name stays.
  • Effect: unique_textures raises RuntimeError: Empty set in unique textures, so resize() fails. Resizing happens whenever the atlas fills up. It also fails partway through, after the atlas's size and texture have already changed.
  • Reproduction: add a 10×10 and a 20×20 SpriteSolidColor to an atlas, free the first, then resize. On development that raises every time.

Bug 2: a freed name is left behind (the CI failure)

  • Trigger: the finalizer removed the name only if the set was already empty. If it runs before the set's own weak reference callback has dropped the dying texture, the slot is freed but the name stays.
  • Effect: the next texture with that image sees the name, skips allocation, and raises Texture '...' not found in UVData. That's the CI error.
  • What I could show: I recreated that state directly, and on development the next add raises exactly that message. I couldn't recreate the real sequence that led to it on CI, even with Python 3.12 and pymunk, many runs, very frequent garbage collection, and a randomized stress test of creating, freeing, rebuilding and resizing. This fix doesn't depend on that sequence.

The fix

  1. Every texture that shares a name is added to its set. The set is then empty only when none of them are alive. Their finalizers are created before they're added to the set, so the set's callback runs first, as for the first texture.
  2. The reference count decides. When it reaches 0, the name is removed even if the set still holds the dying texture.
  3. unique_textures skips a set whose last texture is dying, instead of raising.

Textures sharing a name share one slot, so resize() updating one live texture per name updates them all.

Tests

New tests/unit/atlas/test_shared_textures.py:

  • Resize after the first shared texture dies: works, and the survivor's coordinates follow its image. On development it raises Empty set in unique textures.
  • The name is removed only when the last shared texture dies.
  • The name is removed even if the set still holds a texture. This simulates the finalizer running first, and the next texture with that image can be added. On development this fails, and the add would raise the CI error.

Full suite: 1814 passed on Python 3.13, and 1814 passed on 3.12 with pymunk. The stress test finds no stale entries. ruff, mypy and pyright all pass.

The changelog has an entry under Unreleased → Fixes.

🤖 Generated with Claude Code

Textures with the same image and vertex order share an atlas name and
slot, such as every SpriteSolidColor size. The atlas kept only the
first of them in _unique_textures[name], so its records could disagree
with the reference count:
- When the first texture died while another was still used, the set was
  empty but the name stayed. unique_textures then raised "Empty set in
  unique textures", so resize(), which runs when the atlas fills up,
  failed and left the atlas half-resized.
- The finalizer only removed the name if the set was already empty. If
  it ran before the set's own weak reference callback, the name stayed
  after its slot was freed, and the next texture with that name raised
  "Texture '...' not found in UVData", as in an occasional CI failure.

Every texture sharing a name is now added to the set, the name is
removed whenever the reference count reaches 0, and unique_textures
skips a set whose last texture is dying.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pvcraven
pvcraven merged commit 7949ab0 into development Oct 8, 2026
7 checks passed
@pvcraven
pvcraven deleted the fix/atlas-shared-texture-tracking branch October 8, 2026 19:27
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