Skip to content

SpriteList.insert() does not set _sprite_index_changed: inserted sprite is not drawn until another list operation triggers a sync #2863

Description

@Tanner85

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions