Skip to content

Keep a shared texture alive while adding another with its name - #2978

Merged
pvcraven merged 1 commit into
developmentfrom
atlas-gc-race
Oct 9, 2026
Merged

pvcraven merged 1 commit into
developmentfrom
atlas-gc-race

Conversation

@pvcraven

@pvcraven pvcraven commented Oct 9, 2026

Copy link
Copy Markdown
Member

Fixes the KeyError that made #2977 fail on Python 3.14 five times in a row:

tests/unit/tilemap/test_rotation_flip.py::test_object_rotation_placement
arcade/texture_atlas/atlas_default.py:305: KeyError: '52901d76…|(0, 2, 1, 3)'
  load_tilemap → SpriteList.append → DefaultTextureAtlas.add → _add

Cause

When a texture is added whose atlas name (image + transform) another texture already has, _add takes the fast path and shares that texture's slot:

if self.has_unique_texture(texture):
    if not self.has_texture(texture):
        self._add_texture_ref(texture, ...)
        self._unique_textures[texture.atlas_name].add(texture)   # KeyError

Say the only other texture with that name is garbage stuck in a reference cycle, but not collected yet. If the cycle collector runs inside _add_texture_ref (its WeakSet.add and finalize() both allocate), that texture's finalizer drops the name's count to 0, pops the name, and frees its region and slot. The new texture then has no slot. It's the same family as #2955, but a different moment.

Python 3.14's garbage collector runs at different times, which is presumably why only 3.14 hit it, and only once #2977 changed when sprites in a tile map are freed. I couldn't reproduce the CI failure itself here (Windows, Python 3.13 and 3.14 both pass), but the new test reproduces the same KeyError deterministically.

Fix

_add takes a strong reference to a live texture from _unique_textures[name] and holds it while adding the new one, so that texture can't be collected in the middle. If no texture with the name is alive, it uses the full add path instead of the fast path.

Test

test_add_while_last_shared_texture_is_collected makes a texture garbage inside a reference cycle, then runs gc.collect() at the moment _add_texture_ref starts. Without the fix it raises the same KeyError as CI. With it, the new texture gets the shared slot.

The full test suite (1852), ruff, mypy and pyright pass. The atlas and tile map tests pass on Python 3.14 locally.

To check it against #2977 before either merges, I pushed a temporary branch with #2977 plus this fix and started the PyTest workflow on it. I'll delete that branch afterwards.

🤖 Generated with Claude Code

When a texture is added whose name another texture already has, the
atlas reuses that texture's slot. If the garbage collector ran inside
_add_texture_ref and freed the last other texture with the name, its
finalizer popped the name and freed the slot, and the add raised
KeyError. This failed reliably on Python 3.14 in CI when loading a tile
map. _add now holds a strong reference to a live texture with the name
while it adds the new one, and uses the full add path if none is alive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pvcraven
pvcraven merged commit 5561a22 into development Oct 9, 2026
7 checks passed
@pvcraven
pvcraven deleted the atlas-gc-race branch October 9, 2026 21:25
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