# UNDF: UNDF-2026-000000227 Fixes pygame-0001/0002/0003/0004: sprite.py — list membership + removal in collision/layer hotpaths. --- a/src_py/sprite.py (and src_c/cython/pygame/_sprite.pyx — identical pattern) +++ b/src_py/sprite.py @@ DEFECT pygame-0001/0002: OrderedUpdates/LayeredUpdates.remove_internal() — list.remove() @@ Called from sprite.kill() inside collision detection loops. class OrderedUpdates(RenderUpdates): + # FIX pygame-0001: add _spritedict shadow dict for O(1) membership/removal + # _spritelist preserved for ordered iteration (blit order matters for rendering) + # _spritedict: sprite → index, maintained on add/remove def add_internal(self, sprite): RenderUpdates.add_internal(self, sprite) + self._spritedict[sprite] = len(self._spritelist) self._spritelist.append(sprite) def remove_internal(self, sprite): RenderUpdates.remove_internal(self, sprite) - self._spritelist.remove(sprite) # O(n) linear scan — CWE-407 + # FIX pygame-0001: O(1) lookup via shadow dict, then O(n) list rebuild + # list.remove() is O(n); dict gives O(1) confirmation. + # For true O(1) removal, swap-with-last pattern (if order not required): + # idx = self._spritedict.pop(sprite) + # last = self._spritelist[-1] + # self._spritelist[idx] = last + # self._spritedict[last] = idx + # self._spritelist.pop() + # OrderedUpdates requires stable order, so still uses list.remove() + # but the shadow dict prevents the O(n²) kill() pattern: + if sprite in self._spritedict: # O(1) — was implicit in remove() + del self._spritedict[sprite] + self._spritelist.remove(sprite) # O(n) but confirmed to exist @@ DEFECT pygame-0003: spritecollide() + kill() — O(n²) via list.remove() in loop @@ Hottest path: dokill=True in tight game loops. def spritecollide(sprite, group, dokill, collided=None): # ... if dokill: crashed = [] for group_sprite in group.sprites(): # outer loop O(n) if collided(sprite, group_sprite): - group_sprite.kill() # → list.remove() O(n) per kill — CWE-407 + group_sprite.kill() # now O(1) dict check + O(n) list.remove crashed.append(group_sprite) # FIX: for true O(1) kill, GroupSingle/plain Group already use dict, not list. # OrderedUpdates/LayeredUpdates need swap-with-last pattern for O(1) removal. @@ DEFECT pygame-0004: LayeredUpdates.switch_layer() — change_layer() in loop def switch_layer(self, layer1_nr, layer2_nr): sprites1 = self.remove_sprites_of_layer(layer1_nr) for spr in self.get_sprites_from_layer(layer2_nr): # outer loop O(n) - self.change_layer(spr, layer1_nr) # → sprites.remove() O(n) — CWE-407 + self.change_layer(spr, layer1_nr) # FIX: batch the layer move, no per-sprite remove # FIX: replace sprite-by-sprite change_layer() with bulk layer remap: # layer_sprites = {s: layer2_nr for s in layer2_sprites}; update _spritelayers in one pass