Skip to content

Commit 7fab334

Browse files
committed
Improve performance in physics path
1 parent 536a883 commit 7fab334

3 files changed

Lines changed: 124 additions & 62 deletions

File tree

‎spec/zerodotthreedotone.tex‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,8 @@ \subsection{Contacts and worlds}
943943
\item each body satisfies its body invariant,
944944
\item body ids are strictly increasing in storage order,
945945
\item every body AABB lies within bounds,
946-
\item every contact satisfies \code{a < b}.
946+
\item every contact satisfies \code{a < b},
947+
\item contacts are strictly increasing in lexicographic key order \code{(a,b)}.
947948
\end{enumerate}
948949

949950
Adding a body requires ascending id order and panics otherwise. After adding or moving a body, clamping is exact: if body half-extents are $h_x,h_y$ and bounds are $[m_x,M_x]\times[m_y,M_y]$, then the center is clamped to
@@ -955,7 +956,7 @@ \subsection{Contacts and worlds}
955956

956957
\code{step()} increments the world tick by $1$ and refreshes contacts.
957958

958-
\code{has\_contact(a,b)} normalizes the id pair into sorted order and linearly scans the cached contact list.
959+
\code{has\_contact(a,b)} normalizes the id pair into sorted order and performs membership query over the sorted cached contact vector by key \code{(a,b)}.
959960

960961
\subsection{Contact refresh algorithm}
961962

@@ -964,7 +965,7 @@ \subsection{Contact refresh algorithm}
964965
\ell_{\min x}\le r_{\max x} \land \ell_{\max x}\ge r_{\min x} \land \ell_{\min y}\le r_{\max y} \land \ell_{\max y}\ge r_{\min y}.
965966
\]
966967

967-
When the number of bodies is at most $12$, contact refresh computes AABBs for active bodies, enumerates every active pair, and appends a contact for each intersecting pair in increasing storage-pair order.
968+
When the number of bodies is at most $12$, contact refresh computes AABBs for active bodies, enumerates every active pair, appends a contact for each intersecting pair in increasing storage-pair order, then sorts the resulting contact vector by key \code{(a,b)}.
968969

969970
When the number of bodies exceeds $12$, refresh performs sweep-and-prune on $x$ intervals:
970971
\begin{enumerate}
@@ -979,7 +980,7 @@ \subsection{Trigger helpers}
979980

980981
\code{set\_trigger\_mask\_deferred(world,first\_trigger\_id,trigger\_count,active\_mask)} requires \code{trigger\_count <= 64}. For each index $i$ in $[0,\code{trigger\_count})$ it sets body \code{first\_trigger\_id+i} active iff bit $i$ of \code{active\_mask} is $1$.
981982

982-
\code{collect\_actor\_trigger\_contacts(world,actor\_id,first\_trigger\_id,trigger\_count,remaining\_mask)} also requires \code{trigger\_count <= 64}. For each trigger bit still present in \code{remaining\_mask}, if the world has contact between the actor and that trigger body, the helper clears the bit in \code{remaining\_mask} and increments the collected count. It does \emph{not} mutate body activity or refresh contacts during the scan. Caller-visible trigger activity is updated afterward in one batch. It returns the number of collected triggers as \code{u8}.
983+
\code{collect\_actor\_trigger\_contacts(world,actor\_id,first\_trigger\_id,trigger\_count,remaining\_mask)} also requires \code{trigger\_count <= 64}. It reads the cached contacts, computes which trigger ids in the contiguous range \code{[first\_trigger\_id, first\_trigger\_id + trigger\_count)} are currently in contact with \code{actor\_id}, intersects that set with the bits still present in \code{remaining\_mask}, clears exactly those intersecting bits in \code{remaining\_mask}, and returns the number of cleared bits as \code{u8}. It does \emph{not} mutate body activity or refresh contacts during the scan. Caller-visible trigger activity is updated afterward in one batch.
983984

984985
\section{Builtin game registry}
985986

‎src/builtin/platformer/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ impl Platformer {
304304
let was_non_terminal = state.remaining_berries != 0;
305305
let mut remaining = u64::from(state.remaining_berries);
306306
let collected = collect_actor_trigger_contacts(
307-
&mut state.world,
307+
&state.world,
308308
PLAYER_BODY_ID,
309309
FIRST_BERRY_BODY_ID,
310310
BERRY_COUNT,

‎src/physics.rs‎

Lines changed: 118 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ impl<const BODIES: usize, const CONTACTS: usize> PhysicsWorld2d<BODIES, CONTACTS
136136
if contact.a >= contact.b {
137137
return false;
138138
}
139+
if contact_index > 0 {
140+
let previous = contacts[contact_index - 1];
141+
if (previous.a, previous.b) >= (contact.a, contact.b) {
142+
return false;
143+
}
144+
}
139145
contact_index += 1;
140146
}
141147
true
@@ -158,21 +164,21 @@ impl<const BODIES: usize, const CONTACTS: usize> PhysicsWorld2d<BODIES, CONTACTS
158164
self.bodies
159165
.push(body)
160166
.expect("physics body capacity exceeded");
161-
self.clamp_body(body.id);
167+
let last_index = self.bodies.len() - 1;
168+
self.clamp_body_index(last_index);
169+
}
170+
171+
fn body_index(&self, id: u16) -> Option<usize> {
172+
self.bodies
173+
.as_slice()
174+
.binary_search_by_key(&id, |body| body.id)
175+
.ok()
162176
}
163177

164178
/// Returns immutable body by id.
165179
pub fn body(&self, id: u16) -> Option<&PhysicsBody2d> {
166-
let bodies = self.bodies.as_slice();
167-
let mut index = 0usize;
168-
while index < bodies.len() {
169-
let body = &bodies[index];
170-
if body.id == id {
171-
return Some(body);
172-
}
173-
index += 1;
174-
}
175-
None
180+
self.body_index(id)
181+
.map(|index| &self.bodies.as_slice()[index])
176182
}
177183

178184
/// Returns immutable body by id or panics if missing.
@@ -182,15 +188,8 @@ impl<const BODIES: usize, const CONTACTS: usize> PhysicsWorld2d<BODIES, CONTACTS
182188

183189
/// Returns mutable body by id.
184190
pub fn body_mut(&mut self, id: u16) -> Option<&mut PhysicsBody2d> {
185-
let bodies = self.bodies.as_mut_slice();
186-
let mut index = 0usize;
187-
while index < bodies.len() {
188-
if bodies[index].id == id {
189-
return Some(&mut bodies[index]);
190-
}
191-
index += 1;
192-
}
193-
None
191+
let index = self.body_index(id)?;
192+
Some(&mut self.bodies.as_mut_slice()[index])
194193
}
195194

196195
/// Sets activity flag and refreshes contact cache.
@@ -212,10 +211,10 @@ impl<const BODIES: usize, const CONTACTS: usize> PhysicsWorld2d<BODIES, CONTACTS
212211
}
213212

214213
pub(crate) fn set_body_position_deferred(&mut self, id: u16, position: Vec2<StrictF64>) {
215-
if let Some(body) = self.body_mut(id) {
216-
body.position = position;
214+
if let Some(index) = self.body_index(id) {
215+
self.bodies.as_mut_slice()[index].position = position;
216+
self.clamp_body_index(index);
217217
}
218-
self.clamp_body(id);
219218
}
220219

221220
/// Translates body and refreshes contact cache.
@@ -225,10 +224,10 @@ impl<const BODIES: usize, const CONTACTS: usize> PhysicsWorld2d<BODIES, CONTACTS
225224
}
226225

227226
pub(crate) fn translate_body_deferred(&mut self, id: u16, delta: Vec2<StrictF64>) {
228-
if let Some(body) = self.body_mut(id) {
229-
body.position += delta;
227+
if let Some(index) = self.body_index(id) {
228+
self.bodies.as_mut_slice()[index].position += delta;
229+
self.clamp_body_index(index);
230230
}
231-
self.clamp_body(id);
232231
}
233232

234233
/// Advances world tick and recomputes contacts.
@@ -240,28 +239,21 @@ impl<const BODIES: usize, const CONTACTS: usize> PhysicsWorld2d<BODIES, CONTACTS
240239
/// Returns whether bodies `a` and `b` currently overlap.
241240
pub fn has_contact(&self, a: u16, b: u16) -> bool {
242241
let (left, right) = if a <= b { (a, b) } else { (b, a) };
243-
let contacts = self.contacts.as_slice();
244-
let mut index = 0usize;
245-
while index < contacts.len() {
246-
let contact = contacts[index];
247-
if contact.a == left && contact.b == right {
248-
return true;
249-
}
250-
index += 1;
251-
}
252-
false
242+
self.contacts
243+
.as_slice()
244+
.binary_search_by_key(&(left, right), |contact| (contact.a, contact.b))
245+
.is_ok()
253246
}
254247

255-
fn clamp_body(&mut self, id: u16) {
248+
fn clamp_body_index(&mut self, index: usize) {
256249
let bounds = self.bounds;
257-
if let Some(body) = self.body_mut(id) {
258-
let min_x = bounds.min.x + body.half_extents.x;
259-
let max_x = bounds.max.x - body.half_extents.x;
260-
let min_y = bounds.min.y + body.half_extents.y;
261-
let max_y = bounds.max.y - body.half_extents.y;
262-
body.position.x = body.position.x.clamp(min_x, max_x);
263-
body.position.y = body.position.y.clamp(min_y, max_y);
264-
}
250+
let body = &mut self.bodies.as_mut_slice()[index];
251+
let min_x = bounds.min.x + body.half_extents.x;
252+
let max_x = bounds.max.x - body.half_extents.x;
253+
let min_y = bounds.min.y + body.half_extents.y;
254+
let max_y = bounds.max.y - body.half_extents.y;
255+
body.position.x = body.position.x.clamp(min_x, max_x);
256+
body.position.y = body.position.y.clamp(min_y, max_y);
265257
}
266258

267259
pub(crate) fn refresh_contacts(&mut self) {
@@ -301,6 +293,9 @@ impl<const BODIES: usize, const CONTACTS: usize> PhysicsWorld2d<BODIES, CONTACTS
301293
}
302294
left += 1;
303295
}
296+
self.contacts
297+
.as_mut_slice()
298+
.sort_by_key(|contact| (contact.a, contact.b));
304299
return;
305300
}
306301

@@ -420,7 +415,7 @@ pub fn set_trigger_mask_deferred<const BODIES: usize, const CONTACTS: usize>(
420415
/// triggers. Caller code is responsible for applying any deferred trigger-body
421416
/// activity synchronization after collection.
422417
pub fn collect_actor_trigger_contacts<const BODIES: usize, const CONTACTS: usize>(
423-
world: &mut PhysicsWorld2d<BODIES, CONTACTS>,
418+
world: &PhysicsWorld2d<BODIES, CONTACTS>,
424419
actor_id: u16,
425420
first_trigger_id: u16,
426421
trigger_count: usize,
@@ -430,18 +425,34 @@ pub fn collect_actor_trigger_contacts<const BODIES: usize, const CONTACTS: usize
430425
trigger_count <= u64::BITS as usize,
431426
"trigger_count {trigger_count} exceeds 64-bit trigger mask capacity"
432427
);
433-
let mut collected = 0u8;
434-
let mut index = 0usize;
435-
while index < trigger_count {
436-
let bit = 1u64 << index;
437-
let trigger_id = first_trigger_id + index as u16;
438-
if (*remaining_mask & bit) != 0 && world.has_contact(actor_id, trigger_id) {
439-
*remaining_mask &= !bit;
440-
collected += 1;
428+
if *remaining_mask == 0 || trigger_count == 0 {
429+
return 0;
430+
}
431+
432+
let first_trigger_id: u32 = u32::from(first_trigger_id);
433+
let trigger_count_u32: u32 =
434+
u32::try_from(trigger_count).expect("trigger_count bounded to 64 fits in u32");
435+
let last_trigger_id_exclusive = first_trigger_id + trigger_count_u32;
436+
437+
let mut contacted_mask = 0u64;
438+
for contact in world.contacts.as_slice() {
439+
let other_id = if contact.a == actor_id {
440+
u32::from(contact.b)
441+
} else if contact.b == actor_id {
442+
u32::from(contact.a)
443+
} else {
444+
continue;
445+
};
446+
if other_id < first_trigger_id || other_id >= last_trigger_id_exclusive {
447+
continue;
441448
}
442-
index += 1;
449+
let bit_index = other_id - first_trigger_id;
450+
contacted_mask |= 1u64 << bit_index;
443451
}
444-
collected
452+
453+
let collected_mask = *remaining_mask & contacted_mask;
454+
*remaining_mask &= !collected_mask;
455+
collected_mask.count_ones() as u8
445456
}
446457

447458
fn intersects(left: Aabb2<StrictF64>, right: Aabb2<StrictF64>) -> bool {
@@ -505,7 +516,10 @@ mod proofs {
505516

506517
#[cfg(test)]
507518
mod tests {
508-
use super::{BodyKind, Contact2d, PhysicsBody2d, PhysicsOracleView2d, PhysicsWorld2d};
519+
use super::{
520+
BodyKind, Contact2d, PhysicsBody2d, PhysicsOracleView2d, PhysicsWorld2d,
521+
collect_actor_trigger_contacts,
522+
};
509523
use crate::math::{Aabb2, StrictF64, Vec2};
510524

511525
fn sample_body(id: u16, x: f64, y: f64) -> PhysicsBody2d {
@@ -531,6 +545,9 @@ mod tests {
531545
world.add_body(sample_body(4, 4.4, 4.0));
532546
world.set_body_active(4, false);
533547
assert_eq!(world.contacts.as_slice(), &[Contact2d { a: 1, b: 2 }]);
548+
assert!(world.has_contact(1, 2));
549+
assert!(world.has_contact(2, 1));
550+
assert!(!world.has_contact(1, 4));
534551
}
535552

536553
#[test]
@@ -552,4 +569,48 @@ mod tests {
552569
&[Contact2d { a: 1, b: 2 }]
553570
);
554571
}
572+
573+
#[test]
574+
fn collect_actor_trigger_contacts_only_collects_trigger_range_contacts() {
575+
let bounds = Aabb2::new(
576+
Vec2::new(StrictF64::new(0.0), StrictF64::new(0.0)),
577+
Vec2::new(StrictF64::new(16.0), StrictF64::new(16.0)),
578+
);
579+
let mut world = PhysicsWorld2d::<5, 10>::new(bounds);
580+
world.add_body(sample_body(1, 1.0, 1.0));
581+
world.add_body(sample_body(5, 1.0, 1.0));
582+
world.add_body(sample_body(10, 1.0, 1.0));
583+
world.add_body(sample_body(11, 4.0, 4.0));
584+
world.add_body(sample_body(12, 1.0, 1.0));
585+
586+
let mut remaining_mask = 0b111u64;
587+
let collected = collect_actor_trigger_contacts(&world, 1, 10, 3, &mut remaining_mask);
588+
assert_eq!(collected, 2);
589+
assert_eq!(remaining_mask, 0b010);
590+
591+
let collected_again = collect_actor_trigger_contacts(&world, 1, 10, 3, &mut remaining_mask);
592+
assert_eq!(collected_again, 0);
593+
assert_eq!(remaining_mask, 0b010);
594+
}
595+
596+
#[test]
597+
fn invariant_rejects_unsorted_contacts() {
598+
let bounds = Aabb2::new(
599+
Vec2::new(StrictF64::new(0.0), StrictF64::new(0.0)),
600+
Vec2::new(StrictF64::new(8.0), StrictF64::new(8.0)),
601+
);
602+
let mut world = PhysicsWorld2d::<2, 2>::new(bounds);
603+
world.add_body(sample_body(1, 1.0, 1.0));
604+
world.add_body(sample_body(2, 2.0, 2.0));
605+
world.contacts.clear();
606+
world
607+
.contacts
608+
.push(Contact2d { a: 2, b: 3 })
609+
.expect("contact capacity must allow first manual insert");
610+
world
611+
.contacts
612+
.push(Contact2d { a: 1, b: 2 })
613+
.expect("contact capacity must allow second manual insert");
614+
assert!(!world.invariant());
615+
}
555616
}

0 commit comments

Comments
 (0)