Skip to content

Commit 1fd892f

Browse files
committed
Fix lock bug in separate callback #246
1 parent 083171e commit 1fd892f

File tree

5 files changed

+164
-76
lines changed

5 files changed

+164
-76
lines changed

CHANGELOG.rst

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Changelog
99
Changes:
1010
1111
- Build/run MacOs ARM on ARM github runners.
12+
- Fix lock bug in separate callback
13+
- Improve documentation
1214
1315
1416
Pymunk 6.8.0 (2024-05-10)

TODO.txt

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ Chipmunk improvements
5151
- https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/ccds.pdf
5252
- collision detection:
5353
- r-tree https://www.sebastiansylvan.com/post/r-trees--adapting-out-of-core-techniques-to-modern-memory-architectures/
54+
- ideas from https://github.com/mtsamis/box2d-optimized
55+
5456

5557
Typing a existing project - learnings
5658
-------------------------------------

benchmarks/chipmunk.py

+38-1
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,42 @@ def update(self, dt):
206206
self.m_count += 1
207207

208208

209+
class Multifixture(Benchmark):
210+
def __init__(self, size):
211+
super().__init__()
212+
213+
s1 = pymunk.Segment(self.space.static_body, (-35, 0), (35, 0), 1)
214+
s2 = pymunk.Segment(self.space.static_body, (-36, -50), (-36, 0), 1)
215+
s3 = pymunk.Segment(self.space.static_body, (36, -50), (36, 0), 1)
216+
217+
self.space.add(s1, s2, s3)
218+
219+
for i in range(size):
220+
b = pymunk.Body()
221+
b.position = -20 + (i % 6) * 7 + i / 10, 1 + (i / 6) * 5
222+
c = int(50 - i / 2)
223+
for z in range(c):
224+
bs = 2 / 2
225+
ps = 2 * z * bs + bs
226+
227+
top = pymunk.Poly.create_box_bb(
228+
b,
229+
pymunk.BB(-3, -3, 3, -4),
230+
)
231+
left = pymunk.Poly.create_box_bb(
232+
b,
233+
pymunk.BB(-3, -3, -2, 2),
234+
)
235+
right = pymunk.Poly.create_box_bb(
236+
b,
237+
pymunk.BB(2, -3, 3, 2),
238+
)
239+
top.density = 2
240+
left.density = 2
241+
242+
self.space.add(top, left, right, b)
243+
244+
209245
tests = [SlowExplosion, FallingSquares]
210246

211247
screen = pygame.display.set_mode((600, 600))
@@ -218,12 +254,13 @@ def update(self, dt):
218254

219255
# FallingSquares(space, 200)
220256
# FallingCircles(space, 200)
257+
# sim = Tumbler(1000)
258+
sim = Multifixture(100)
221259
# MildN2(space, 50)
222260
# N2(space, 500)
223261
# Diagonal(space, 100)
224262
# SlowExplosion(space, 5000)
225263

226-
sim = Tumbler(1000)
227264

228265
translation = pymunk.Transform()
229266
scaling = 2

pymunk/_callbacks.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -217,14 +217,16 @@ def ext_cpCollisionSeparateFunc(
217217
_arb: ffi.CData, _space: ffi.CData, data: ffi.CData
218218
) -> None:
219219
handler = ffi.from_handle(data)
220+
221+
orig_locked = handler._space._locked
222+
handler._space._locked = True
220223
try:
221224
# this try is needed since a separate callback will be called
222225
# if a colliding object is removed, regardless if its in a
223-
# step or not.
224-
handler._space._locked = True
226+
# step or not. Meaning the unlock must succeed
225227
handler._separate(Arbiter(_arb, handler._space), handler._space, handler.data)
226228
finally:
227-
handler._space._locked = False
229+
handler._space._locked = orig_locked
228230

229231

230232
# body.py

pymunk/tests/test_space.py

+117-72
Original file line numberDiff line numberDiff line change
@@ -133,64 +133,6 @@ def testAddRemove(self) -> None:
133133
self.assertEqual(s.bodies, [b])
134134
self.assertEqual(s.shapes, [c2])
135135

136-
def testAddRemoveInStep(self) -> None:
137-
s = p.Space()
138-
139-
b1 = p.Body(1, 2)
140-
c1 = p.Circle(b1, 2)
141-
142-
b2 = p.Body(1, 2)
143-
c2 = p.Circle(b2, 2)
144-
145-
s.add(b1, b2, c1, c2)
146-
147-
b = p.Body(1, 2)
148-
c = p.Circle(b, 2)
149-
150-
def pre_solve_add(arb: p.Arbiter, space: p.Space, data: Any) -> bool:
151-
space.add(b, c)
152-
space.add(c, b)
153-
self.assertTrue(b not in s.bodies)
154-
self.assertTrue(c not in s.shapes)
155-
return True
156-
157-
def pre_solve_remove(arb: p.Arbiter, space: p.Space, data: Any) -> bool:
158-
space.remove(b, c)
159-
space.remove(c, b)
160-
self.assertTrue(b in s.bodies)
161-
self.assertTrue(c in s.shapes)
162-
return True
163-
164-
s.add_collision_handler(0, 0).pre_solve = pre_solve_add
165-
166-
s.step(0.1)
167-
return
168-
self.assertTrue(b in s.bodies)
169-
self.assertTrue(c in s.shapes)
170-
171-
s.add_collision_handler(0, 0).pre_solve = pre_solve_remove
172-
173-
s.step(0.1)
174-
175-
self.assertTrue(b not in s.bodies)
176-
self.assertTrue(c not in s.shapes)
177-
178-
def testRemoveInStep(self) -> None:
179-
self._setUp()
180-
s = self.s
181-
182-
def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool:
183-
space.remove(*arb.shapes)
184-
return True
185-
186-
s.add_collision_handler(0, 0).pre_solve = pre_solve
187-
188-
s.step(0.1)
189-
190-
self.assertTrue(self.s1 not in s.shapes)
191-
self.assertTrue(self.s2 not in s.shapes)
192-
self._tearDown()
193-
194136
def testAddShapeAsserts(self) -> None:
195137
s1 = p.Space()
196138
s2 = p.Space()
@@ -705,6 +647,112 @@ def separate(*_: Any) -> None:
705647
s.step(1)
706648
s.remove(c1)
707649

650+
def testCollisionHandlerRemoveAfterSeparate(self) -> None:
651+
# In this test the separate must happen before post_solve in the same step()
652+
print()
653+
space = p.Space()
654+
655+
shape1 = p.Circle(space.static_body, 1)
656+
shape1.collision_type = 1
657+
658+
body2 = p.Body()
659+
shape2 = p.Circle(body2, 1)
660+
shape2.density = 1
661+
shape2.collision_type = 2
662+
663+
body3 = p.Body(body_type=p.Body.KINEMATIC)
664+
shape3 = p.Circle(body3, 1)
665+
shape3.collision_type = 3
666+
667+
space.add(shape1, body2, shape2, shape3, body3)
668+
print("START", shape1, shape2, shape3)
669+
670+
def separate(arbiter: p.Arbiter, space: p.Space, data):
671+
print("SEP", arbiter.shapes)
672+
self.separate_occurred = True
673+
674+
def post_solve(arbiter: p.Arbiter, space: p.Space, data):
675+
print("POST", arbiter.shapes)
676+
if self.separate_occurred:
677+
print("POST REMOVE", arbiter.shapes)
678+
space.remove(*arbiter.shapes)
679+
680+
space.add_collision_handler(1, 2).post_solve = post_solve
681+
space.add_collision_handler(3, 2).separate = separate
682+
683+
print(1)
684+
self.separate_occurred = False
685+
space.step(1)
686+
print(2)
687+
body3.position = 10, 0
688+
# self.assertEqual(len(space.shapes), 3)
689+
690+
self.separate_occurred = False
691+
space.step(1)
692+
print(3)
693+
space.remove(shape3)
694+
# self.assertEqual(len(space.shapes), 1)
695+
print(4)
696+
# space.remove(shape3)
697+
698+
def testCollisionHandlerAddRemoveInStep(self) -> None:
699+
s = p.Space()
700+
701+
b1 = p.Body(1, 2)
702+
c1 = p.Circle(b1, 2)
703+
704+
b2 = p.Body(1, 2)
705+
c2 = p.Circle(b2, 2)
706+
707+
s.add(b1, b2, c1, c2)
708+
709+
b = p.Body(1, 2)
710+
c = p.Circle(b, 2)
711+
712+
def pre_solve_add(arb: p.Arbiter, space: p.Space, data: Any) -> bool:
713+
space.add(b, c)
714+
space.add(c, b)
715+
self.assertTrue(b not in s.bodies)
716+
self.assertTrue(c not in s.shapes)
717+
return True
718+
719+
def pre_solve_remove(arb: p.Arbiter, space: p.Space, data: Any) -> bool:
720+
space.remove(b, c)
721+
space.remove(c, b)
722+
self.assertTrue(b in s.bodies)
723+
self.assertTrue(c in s.shapes)
724+
return True
725+
726+
s.add_collision_handler(0, 0).pre_solve = pre_solve_add
727+
728+
s.step(0.1)
729+
return
730+
self.assertTrue(b in s.bodies)
731+
self.assertTrue(c in s.shapes)
732+
733+
s.add_collision_handler(0, 0).pre_solve = pre_solve_remove
734+
735+
s.step(0.1)
736+
737+
self.assertTrue(b not in s.bodies)
738+
self.assertTrue(c not in s.shapes)
739+
740+
def testCollisionHandlerRemoveInStep(self) -> None:
741+
self._setUp()
742+
s = self.s
743+
744+
def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool:
745+
space.remove(*arb.shapes)
746+
return True
747+
748+
s.add_collision_handler(0, 0).pre_solve = pre_solve
749+
750+
s.step(0.1)
751+
752+
self.assertTrue(self.s1 not in s.shapes)
753+
self.assertTrue(self.s2 not in s.shapes)
754+
self._tearDown()
755+
708756
def testCollisionHandlerKeyOrder(self) -> None:
709757
s = p.Space()
710758
h1 = s.add_collision_handler(1, 2)
@@ -960,7 +1008,7 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None:
9601008

9611009
def testPickleCachedArbiters(self) -> None:
9621010
s = p.Space()
963-
1011+
9641012
b1 = p.Body()
9651013
b2 = p.Body()
9661014

@@ -970,32 +1018,31 @@ def testPickleCachedArbiters(self) -> None:
9701018
c1.mass = 1
9711019
c2.mass = 1
9721020

973-
b2.position = 1,2
974-
s.add(c1,c2, b1, b2)
975-
1021+
b2.position = 1, 2
1022+
s.add(c1, c2, b1, b2)
1023+
9761024
s.step(0.1)
9771025
# print("\nOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO")
9781026
s_copy = s.copy()
979-
1027+
9801028
# a1 = [p.arbiter._arbiter_to_dict(_arb, s) for _arb in s._get_arbiters()]
9811029
# a2 = [p.arbiter._arbiter_to_dict(_arb, s_copy) for _arb in s_copy._get_arbiters()]
982-
1030+
9831031
# print("a1", a1)
9841032
# print("a2", a2)
9851033
# print("XXXX")
986-
1034+
9871035
# print("s.bodies.position:")
9881036
# print([b.position for b in s.bodies])
9891037
# print("s_copy.bodies.position:")
9901038
# print([b.position for b in s_copy.bodies])
9911039

992-
9931040
s.step(0.1)
9941041
s_copy.step(0.1)
995-
1042+
9961043
# a1 = [p.arbiter._arbiter_to_dict(_arb, s) for _arb in s._get_arbiters()]
9971044
# a2 = [p.arbiter._arbiter_to_dict(_arb, s_copy) for _arb in s_copy._get_arbiters()]
998-
1045+
9991046
# print("a1", a1)
10001047
# print("a2", a2)
10011048
# print("XXXX")
@@ -1009,7 +1056,7 @@ def testPickleCachedArbiters(self) -> None:
10091056

10101057
# a1 = [p.arbiter._arbiter_to_dict(_arb, s) for _arb in s._get_arbiters()]
10111058
# a2 = [p.arbiter._arbiter_to_dict(_arb, s_copy) for _arb in s_copy._get_arbiters()]
1012-
1059+
10131060
# print("a1", a1)
10141061
# print("a2", a2)
10151062
# print("XXXX")
@@ -1019,14 +1066,12 @@ def testPickleCachedArbiters(self) -> None:
10191066
# print("s_copy.bodies.position:")
10201067
# print([b.position for b in s_copy.bodies])
10211068

1022-
# TODO: to assert that everything is working as it should all
1069+
# TODO: to assert that everything is working as it should all
10231070
# properties on the cached the arbiters should be asserted.
10241071

1025-
10261072
self.assertAlmostEqual(s.bodies[0].position.x, s_copy.bodies[0].position.x)
10271073
self.assertAlmostEqual(s.bodies[0].position.y, s_copy.bodies[0].position.y)
10281074

10291075

1030-
10311076
def f1(*args: Any, **kwargs: Any) -> None:
10321077
pass

0 commit comments

Comments
 (0)