java-topology/defects/pygame/patch/pygame-0001-sprite-list-remove.patch

60 lines
3.1 KiB
Diff

# 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