60 lines
3.1 KiB
Diff
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
|