Speed up SpriteList swap, insert, setitem, and buffer uploads - #2908
Merged
Merged
Conversation
- swap(): the index buffer is in the same order as the sprite list, so use the given positions instead of searching it with .index(). O(1) instead of O(n). - insert() and __setitem__: check membership with the sprite_slot dict instead of scanning the list. __setitem__ now also accepts setting a negative index to the sprite already there. - write_sprite_buffers_to_gpu(): new optional slot_count and index_count arguments. The buffer backend writes only the slots in use, through zero-copy memoryview slices, instead of the whole capacity. The texture (WebGL) backend still writes everything. Measured: swap at the end of a 10k list 222 -> 0.22 us, setitem 65 -> 1.5 us, insert+pop 77 -> 23 us, move one sprite and draw 1.6-2.3x faster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Speeds up
SpriteList.swap(),insert(),__setitem__, and GPU buffer uploads. These were found while reviewing the sprite drawing code.swap(): O(n) → O(1)swap()found each sprite's place in the draw order with_sprite_index_data.index(slot), a linear search, even though it's given the positions. The index buffer is always in the same order as the sprite list, so it now swapsindex_data[index_1]andindex_data[index_2]directly. Negative indexes are made positive first, because the index buffer is longer than the list.insert()and__setitem__: dictionary membership instead of a list scaninsert()checkedsprite in self.sprite_list, and__setitem__calledself.sprite_list.index(sprite). Both now check thesprite_slotdict, the same wayappend()does.__setitem__also used to compare the existing position with the given index, sosprite_list[-1] = sprite_list[-1]raised "already in the list". It now checks whether the sprite is the one being replaced, which also fixes that.Upload only the slots in use
Any change to a sprite re-uploaded the whole capacity of the affected buffer, so cost grew with capacity rather than sprite count. Capacity doubles as lists grow and never shrinks after
pop()orremove().write_sprite_buffers_to_gpu()gets two optional arguments,slot_countandindex_count. The buffer backend uses them to write zero-copymemoryviewslices of just the slots in use.The rest of each orphaned buffer stays undefined, which is safe: the index buffer only refers to slots below
slot_count. The texture (WebGL) backend accepts the arguments but still writes whole textures. Both arguments default toNone(write everything), so existing callers of this method don't change.Benchmarks
Old and new code alternated, 3 rounds, best result with the range in brackets:
swap(-1, -2)on a 10,000 sprite listsl[9000] = spriteon a 10,000 sprite listinsert(5000)+pop(5000)on a 10,000 sprite listThe last row is the normal "everything moves" case. The ranges overlap, so I'd call it unchanged within noise, but it's not slower.
insert()is still O(n) because inserting into a Python list and array is, but the extra scan is gone.Tests
test_swap_draw_order: swaps including negative, mixed, and same-index cases. It checks the list and the draw order read back from the GPU index buffer.test_swap_out_of_range:IndexError, and nothing changes.test_setitem_negative_index: setting a sprite to its own position, adding a sprite that's already elsewhere (raises), an index out of range (raises), and replacing at a negative index.test_insert_already_in_list.test_gpu_buffers_match_after_changes: 200 random pops, appends that reuse freed slots, inserts, swaps, and moves, with buffer growth, drawing every 10 steps. At the end, the draw order and each sprite's position, depth, angle, and size are compared against what was uploaded to the GPU. With the position upload deliberately cut one slot short, this test fails.On
development, onlytest_setitem_negative_indexfails, since that's the one behavior change. Full suite on pyglet 3.0.dev11: 1419 passed. The 3 failures are the render tests that only fail on my machine. Ruff is clean, and mypy reports no errors insprite_list.py.🤖 Generated with Claude Code