java-topology/defects/pygame/patch/pygame-0001-sprite-list-remove.patch
russell@unturf.com 6bafc7f5e9 sfml/angelscript/threejs/pygame: unit tests + patches + whitepaper (115 sites, 48 ecosystems)
SFML 5 defects: VideoMode dedup x3 (139x), allWindows erase (1001x), GL ext (149x)
AngelScript 3 defects: FindNewOwner (100x), CompileSwitch (250x)
Three.js 5 defects: WebGL binding (22x), StackNode filter (1875x), NodeBuilder (517x)
pygame 4 defects: remove_internal (3001x), spritecollide dokill (3001x), switch_layer (3001x)

Whitepaper: 98→115 sites, 44→48 ecosystems
All unit tests pass: 6/6 each
2026-03-27 11:57:12 -04:00

59 lines
3.1 KiB
Diff

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