Skip to content

Fix SpriteList pop with negative index, rescale, and lazy preload - #2906

Merged
pvcraven merged 1 commit into
developmentfrom
fix/spritelist-pop-rescale
Oct 1, 2026
Merged

pvcraven merged 1 commit into
developmentfrom
fix/spritelist-pop-rescale

Conversation

@pvcraven

@pvcraven pvcraven commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes three SpriteList bugs found while reviewing the sprite drawing code, plus two small consistency fixes. Each bug is reproduced by a new test that fails on development.

1. pop() with a negative index drew the wrong sprites

_sprite_index_data (the draw order sent to the GPU) is longer than the list, because it has spare capacity at the end. pop(index) passed the same index to both the sprite list and that array, so a negative index removed padding from the array instead of the sprite's entry:

list [a, b, c] → pop(-2) → list is [a, c], but the GPU draws [a, <b's freed slot>]

The removed sprite stayed on screen, c disappeared, and the next sprite appended reused b's slot. pop() and pop(-1) only worked by luck, since the entry being dropped happened to be the last one drawn.

Fix: convert the index to a positive one before touching either list. Out-of-range indexes now raise IndexError before anything changes. Before, a far-out-of-range negative index could have removed the wrong sprite after the check.

2. rescale() scaled around a moving center

rescale() called self.center inside its loop, after earlier sprites had already moved. Two sprites at x=0 and x=100 rescaled ×2 ended at -50 and 175, instead of -50 and 150. It also recalculated the center for every sprite, which is O(n²).

Fix: find the center once, before the loop. An early return for empty lists is needed now, since finding the center of an empty list would divide by zero.

3. preload_textures() on a lazy list raised AttributeError

It checked self.ctx, which only exists after the list is initialized, so a lazy list that hadn't been drawn yet raised AttributeError: 'SpriteList' object has no attribute 'ctx'.

Fix: if the list has no atlas yet, preload into the window's default atlas, which is the one the list gets when it initializes. Preloading doesn't initialize the list. Without a window, it raises the ValueError the code always intended.

Smaller fixes

  • The index array was created with typecode "i" (signed), but clear() and shuffle() recreate it as "I" (unsigned), matching the GPU's 32-bit unsigned index buffer. It's now "I" from the start.
  • The DEFAULT_TEXTURE_FILTER docs example for "linear filtering (smooth)" showed gl.NEAREST, gl.NEAREST; it now shows gl.LINEAR, gl.LINEAR.

Tests

The pop() tests read the draw order back from the GPU index buffer, like the existing shuffle test, so they check what's actually drawn.

  • test_pop_index: indexes 0–3 and -1 to -4 on a 4-sprite list. The draw order matches the list, including after a new sprite reuses the freed slot.
  • test_pop_repeatedly: repeated pop(-2) until the list is empty.
  • test_pop_out_of_range and test_pop_empty: IndexError, and nothing removed.
  • test_rescale_around_center: positions after rescaling around the center (50, 20), plus an empty list.
  • test_preload_textures_lazy: the texture is in the atlas, and the list isn't initialized.
  • test_index_buffer_type: "I" after creation, shuffle(), and clear().

On development, pop() with -2/-3/-4, the repeated pops, rescale, lazy preload, and index type tests fail. The non-negative and -1 pop cases pass there too, as expected.

Full suite on pyglet 3.0.dev11: 1382 passed. The 3 failures are the render tests that only fail on my machine. Ruff is clean, and mypy reports no errors in sprite_list.py.

🤖 Generated with Claude Code

- pop(): the index buffer array is longer than the list (it has spare
  capacity), so popping it with a negative index removed padding instead
  of the sprite's entry. After pop(-2) the removed sprite stayed on
  screen and the last sprite vanished. Make the index positive first,
  and raise IndexError for out-of-range indexes before changing
  anything.
- rescale(): self.center was recalculated for every sprite, after
  earlier sprites had moved. Find the center once.
- preload_textures(): raised AttributeError on a list that wasn't
  initialized yet, such as a lazy list. Use the default atlas it will
  get, and raise the intended ValueError only if there's no window.
- Create the index array as unsigned ("I"), matching clear(), shuffle()
  and the GPU buffer, and fix the DEFAULT_TEXTURE_FILTER docs example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pvcraven
pvcraven merged commit 474af84 into development Oct 1, 2026
7 checks passed
@pvcraven
pvcraven deleted the fix/spritelist-pop-rescale branch October 1, 2026 19:43
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