Bug description
SpriteList.insert() updates the CPU-side index data (_sprite_index_data) but never sets self._sprite_index_changed = True. All the other mutating methods (append, remove, pop, swap, reverse, sort, clear) do set it.
Since _write_sprite_buffers_to_gpu() only uploads the index buffer when that flag is set, a sprite added via insert() is fully present in the list (it is updated, collides, can be removed) but is never rendered. It silently becomes visible later, as soon as any other operation on the same list (e.g. an append or remove) sets the flag and forces a full sync.
This makes the bug very hard to diagnose in a real game: the sprite "exists but is invisible", and the symptom is intermittent because unrelated list operations mask it. (Found while debugging "ghost NPCs" in my game BUMS: NPCs inserted with insert(1, ...) to control render order would walk across the whole screen invisibly for ~6 seconds until their own despawn remove() re-synced the buffer for the next spawn.)
Affected: arcade 3.3.2 and current development (checked 2026-07-04, insert() body is unchanged).
How to reproduce
Offscreen render + pixel readback, no window interaction needed:
import arcade
W, H = 200, 100
win = arcade.Window(W, H, "test", visible=False)
ctx = win.ctx
sl = arcade.SpriteList()
a = arcade.SpriteSolidColor(20, 20, color=arcade.color.RED)
a.position = (30, 50)
b = arcade.SpriteSolidColor(20, 20, color=arcade.color.GREEN)
b.position = (100, 50)
sl.append(a)
sl.append(b)
tex = ctx.texture((W, H))
fb = ctx.framebuffer(color_attachments=[tex])
def render_and_sample():
with fb.activate():
fb.clear()
sl.draw()
data = fb.read(components=4)
def px(x, y):
i = (y * W + x) * 4
return tuple(data[i:i + 4])
return {"red(30,50)": px(30, 50), "green(100,50)": px(100, 50), "blue(160,50)": px(160, 50)}
print("PHASE 1 - after 2 appends and first draw (initial sync):")
print(" ", render_and_sample())
c = arcade.SpriteSolidColor(20, 20, color=arcade.color.BLUE)
c.position = (160, 50)
sl.insert(1, c)
print(f"PHASE 2 - after insert(1, blue): len(sl)={len(sl)}, blue in list: {c in sl}")
print(" flag _sprite_index_changed:", sl._sprite_index_changed)
print(" ", render_and_sample())
sl.remove(a) # any flag-setting operation "heals" the list
print("PHASE 3 - after remove(red) which sets the flag, redraw:")
print(" ", render_and_sample())
Output on arcade 3.3.2 (Linux, Python 3.10):
PHASE 1 - after 2 appends and first draw (initial sync):
{'red(30,50)': (255, 0, 0, 255), 'green(100,50)': (0, 255, 0, 255), 'blue(160,50)': (0, 0, 0, 0)}
PHASE 2 - after insert(1, blue): len(sl)=3, blue in list: True
flag _sprite_index_changed: False
{'red(30,50)': (255, 0, 0, 255), 'green(100,50)': (0, 255, 0, 255), 'blue(160,50)': (0, 0, 0, 0)}
PHASE 3 - after remove(red) which sets the flag, redraw:
{'red(30,50)': (0, 0, 0, 0), 'green(100,50)': (0, 255, 0, 255), 'blue(160,50)': (0, 0, 255, 255)}
In PHASE 2 the blue sprite is in the list and drawn, but the pixels show it is not rendered. In PHASE 3 a mere remove() of another sprite makes it appear.
Expected behavior
A sprite added with insert() should be rendered on the next draw(), exactly like one added with append().
Suggested fix
Add the missing flag at the end of SpriteList.insert() (mirroring append):
self._sprite_index_data.insert(index, slot)
self._sprite_index_data.pop()
self._sprite_index_changed = True # <-- missing
Possibly related history: #1021 fixed a different insert() bug (texture not loaded); the index-buffer flag was never added.
Environment
- arcade: 3.3.2 (bug also present in
development as of 2026-07-04)
- Python: 3.10
- OS: Linux (Ubuntu, X11)
Bug description
SpriteList.insert()updates the CPU-side index data (_sprite_index_data) but never setsself._sprite_index_changed = True. All the other mutating methods (append,remove,pop,swap,reverse,sort,clear) do set it.Since
_write_sprite_buffers_to_gpu()only uploads the index buffer when that flag is set, a sprite added viainsert()is fully present in the list (it is updated, collides, can be removed) but is never rendered. It silently becomes visible later, as soon as any other operation on the same list (e.g. anappendorremove) sets the flag and forces a full sync.This makes the bug very hard to diagnose in a real game: the sprite "exists but is invisible", and the symptom is intermittent because unrelated list operations mask it. (Found while debugging "ghost NPCs" in my game BUMS: NPCs inserted with
insert(1, ...)to control render order would walk across the whole screen invisibly for ~6 seconds until their own despawnremove()re-synced the buffer for the next spawn.)Affected: arcade 3.3.2 and current
development(checked 2026-07-04,insert()body is unchanged).How to reproduce
Offscreen render + pixel readback, no window interaction needed:
Output on arcade 3.3.2 (Linux, Python 3.10):
In PHASE 2 the blue sprite is in the list and drawn, but the pixels show it is not rendered. In PHASE 3 a mere
remove()of another sprite makes it appear.Expected behavior
A sprite added with
insert()should be rendered on the nextdraw(), exactly like one added withappend().Suggested fix
Add the missing flag at the end of
SpriteList.insert()(mirroringappend):Possibly related history: #1021 fixed a different
insert()bug (texture not loaded); the index-buffer flag was never added.Environment
developmentas of 2026-07-04)