Skip to main content

oxijolt/
collision_group.rs

1//! Collision groups (Jolt `CollisionGroup` and `GroupFilterTable`): which bodies the solver lets
2//! collide, beyond their object layers.
3//!
4//! A [`GroupFilterTable`] says which of its sub groups collide with each other; it is built once
5//! with a [`GroupFilterTableBuilder`] and never changes afterwards, so bodies and Jolt's worker
6//! threads can share it. A [`CollisionGroup`] puts a body into a group of a table, given to the
7//! body at creation with [`BodySettings::collision_group`](crate::BodySettings::collision_group)
8//! or [`SoftBodySettings::collision_group`](crate::SoftBodySettings::collision_group).
9
10use std::fmt;
11use std::sync::Arc;
12
13use oxijolt_sys::*;
14
15use crate::owned::{JoltObject, Owned};
16use crate::world::ensure_initialized;
17use crate::CollisionGroupError;
18
19/// A group filter table, owned with one reference: the one `JPH_GroupFilterTable_Create`
20/// returns. Creation settings that use the table hold their own reference while they exist, and
21/// each body created with it holds one for its life.
22impl JoltObject for JPH_GroupFilterTable {
23    unsafe fn destroy(ptr: *mut Self) {
24        // SAFETY: the owner holds one reference (trait contract). `GroupFilterTable` derives from
25        // `GroupFilter` with single inheritance, as joltc's header notes, so the pointer is the
26        // `GroupFilter` that `Release` acts on.
27        unsafe { JPH_GroupFilter_Destroy(ptr.cast()) };
28    }
29}
30
31/// Builds a [`GroupFilterTable`]: which of its sub groups collide with each other.
32///
33/// ```
34/// use oxijolt::{CollisionGroup, GroupFilterTableBuilder};
35///
36/// # fn main() -> Result<(), oxijolt::CollisionGroupError> {
37/// // A chain of three links in which neighbours do not collide.
38/// let mut builder = GroupFilterTableBuilder::new(3)?;
39/// builder.disable_collision(0, 1)?;
40/// builder.disable_collision(1, 2)?;
41/// let table = builder.build();
42/// assert!(table.is_collision_enabled(0, 2)?);
43/// let links: Vec<CollisionGroup> = (0..3)
44///     .map(|link| CollisionGroup::new(&table, 1, link))
45///     .collect::<Result<_, _>>()?;
46/// assert!(!links[0].can_collide(&links[1]));
47/// assert!(links[0].can_collide(&links[2]));
48/// # Ok(())
49/// # }
50/// ```
51pub struct GroupFilterTableBuilder {
52    table: Owned<JPH_GroupFilterTable>,
53    sub_groups: u32,
54}
55
56impl GroupFilterTableBuilder {
57    /// A table of `sub_groups` sub groups (ids `0..sub_groups`) in which every pair of different
58    /// sub groups collides; a sub group never collides with itself. Refused for 0 sub groups and
59    /// for more than [`GroupFilterTable::MAX_SUB_GROUPS`].
60    pub fn new(sub_groups: u32) -> Result<Self, CollisionGroupError> {
61        if sub_groups == 0 {
62            return Err(CollisionGroupError::NoSubGroups);
63        }
64        if sub_groups > GroupFilterTable::MAX_SUB_GROUPS {
65            return Err(CollisionGroupError::TooManySubGroups(sub_groups));
66        }
67        if !ensure_initialized() {
68            return Err(CollisionGroupError::InitFailed);
69        }
70        // SAFETY: Jolt is initialised (its allocation hooks are set). The handle takes over the
71        // one reference joltc's `JPH_GroupFilterTable_Create` returns.
72        let table = unsafe { Owned::from_raw(JPH_GroupFilterTable_Create(sub_groups)) }
73            .unwrap_or_else(|| unreachable!("joltc `new`s the table"));
74        Ok(Self { table, sub_groups })
75    }
76
77    /// Makes the sub groups `a` and `b` (in either order) not collide.
78    pub fn disable_collision(&mut self, a: u32, b: u32) -> Result<(), CollisionGroupError> {
79        self.check_pair(a, b)?;
80        // SAFETY: the table is live and only this builder references it; both sub groups are
81        // below its size and differ, as `GroupFilterTable::GetBit` asserts.
82        unsafe { JPH_GroupFilterTable_DisableCollision(self.table.as_ptr(), a, b) };
83        Ok(())
84    }
85
86    /// Makes the sub groups `a` and `b` (in either order) collide again.
87    pub fn enable_collision(&mut self, a: u32, b: u32) -> Result<(), CollisionGroupError> {
88        self.check_pair(a, b)?;
89        // SAFETY: as in `disable_collision`.
90        unsafe { JPH_GroupFilterTable_EnableCollision(self.table.as_ptr(), a, b) };
91        Ok(())
92    }
93
94    /// The table, which no longer changes.
95    pub fn build(self) -> GroupFilterTable {
96        GroupFilterTable(Arc::new(TableInner {
97            table: self.table,
98            sub_groups: self.sub_groups,
99        }))
100    }
101
102    fn check_pair(&self, a: u32, b: u32) -> Result<(), CollisionGroupError> {
103        check_pair(self.sub_groups, a, b)
104    }
105}
106
107impl fmt::Debug for GroupFilterTableBuilder {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.debug_struct("GroupFilterTableBuilder")
110            .field("sub_groups", &self.sub_groups)
111            .finish_non_exhaustive()
112    }
113}
114
115/// Both sub groups below `sub_groups`, and different.
116fn check_pair(sub_groups: u32, a: u32, b: u32) -> Result<(), CollisionGroupError> {
117    check_sub_group(sub_groups, a)?;
118    check_sub_group(sub_groups, b)?;
119    if a == b {
120        return Err(CollisionGroupError::SameSubGroup(a));
121    }
122    Ok(())
123}
124
125fn check_sub_group(sub_groups: u32, id: u32) -> Result<(), CollisionGroupError> {
126    if id >= sub_groups {
127        return Err(CollisionGroupError::SubGroupOutOfRange(id));
128    }
129    Ok(())
130}
131
132// SAFETY: the builder owns its native table alone (no group or body references it before
133// `build`), and Jolt's `GroupFilterTable` has no thread affinity; `&mut self` serialises edits.
134unsafe impl Send for GroupFilterTableBuilder {}
135
136/// Which sub groups of a collision group collide with each other (Jolt `GroupFilterTable`),
137/// built with a [`GroupFilterTableBuilder`] and fixed from then on. Cloning shares the table.
138///
139/// Two tables are equal only when they are the same table, as Jolt compares group filters by
140/// pointer: two groups with the same group id but different tables never collide.
141#[derive(Clone)]
142pub struct GroupFilterTable(Arc<TableInner>);
143
144/// The native table and its size.
145struct TableInner {
146    table: Owned<JPH_GroupFilterTable>,
147    sub_groups: u32,
148}
149
150// SAFETY: the native table is never written after `GroupFilterTableBuilder::build`; Jolt's
151// `RefTarget` count is atomic, and Jolt's own worker threads call the const `CanCollide`
152// concurrently during a step (`Body.inl:74-76`, `PhysicsSystem.cpp:1992`).
153unsafe impl Send for TableInner {}
154// SAFETY: as for `Send`: every access after `build` only reads the table or changes its atomic
155// reference count.
156unsafe impl Sync for TableInner {}
157
158impl GroupFilterTable {
159    /// The most sub groups a table may have, an oxijolt bound: the table takes about n²/16
160    /// bytes, and Jolt's `int` bit index (`GroupFilterTable::GetBit`) overflows above 65 536.
161    /// See [docs/limits.md#group-filter-table-size].
162    ///
163    /// [docs/limits.md#group-filter-table-size]: https://github.com/pockerhead/oxijolt/blob/main/docs/limits.md#group-filter-table-size
164    pub const MAX_SUB_GROUPS: u32 = 4096;
165
166    /// How many sub groups the table has; their ids are `0..sub_groups()`.
167    pub fn sub_groups(&self) -> u32 {
168        self.0.sub_groups
169    }
170
171    /// Whether the sub groups `a` and `b` collide; a sub group never collides with itself.
172    /// Refused for a sub group not in the table.
173    pub fn is_collision_enabled(&self, a: u32, b: u32) -> Result<bool, CollisionGroupError> {
174        check_sub_group(self.0.sub_groups, a)?;
175        check_sub_group(self.0.sub_groups, b)?;
176        if a == b {
177            return Ok(false);
178        }
179        // SAFETY: the table is live and no longer written; both sub groups are below its size
180        // and differ. joltc's parameter is mutable but the call is Jolt's const
181        // `IsCollisionEnabled`.
182        Ok(unsafe { JPH_GroupFilterTable_IsCollisionEnabled(self.as_ptr(), a, b) })
183    }
184
185    fn as_ptr(&self) -> *mut JPH_GroupFilterTable {
186        self.0.table.as_ptr()
187    }
188}
189
190impl PartialEq for GroupFilterTable {
191    fn eq(&self, other: &Self) -> bool {
192        Arc::ptr_eq(&self.0, &other.0)
193    }
194}
195
196impl Eq for GroupFilterTable {}
197
198impl fmt::Debug for GroupFilterTable {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        f.debug_struct("GroupFilterTable")
201            .field("sub_groups", &self.0.sub_groups)
202            .finish_non_exhaustive()
203    }
204}
205
206/// The collision group of a body (Jolt `CollisionGroup`): a group id and a sub group of a
207/// [`GroupFilterTable`].
208///
209/// Jolt's rule for two bodies that both have a group: different group ids collide; the same
210/// group id with different tables never collides; within one table the same sub group never
211/// collides, and different sub groups collide when the table says so. A body without a group
212/// collides by its object layer alone. The group only filters what the solver collides: scene
213/// queries, character movement and vehicle wheel casts do not look at it.
214#[derive(Clone, PartialEq, Eq, Debug)]
215pub struct CollisionGroup {
216    table: GroupFilterTable,
217    group_id: u32,
218    sub_group_id: u32,
219}
220
221impl CollisionGroup {
222    /// The largest group id a caller may use. The ids above it belong to ragdolls: the parts of
223    /// a ragdoll use group id `2^31 +` [`RagdollId::to_raw`](crate::RagdollId::to_raw).
224    pub const MAX_GROUP_ID: u32 = (1 << 31) - 1;
225
226    /// The group `group_id` with sub group `sub_group_id` of `table`. Refused for a group id
227    /// above [`MAX_GROUP_ID`](Self::MAX_GROUP_ID) and a sub group not in the table.
228    pub fn new(
229        table: &GroupFilterTable,
230        group_id: u32,
231        sub_group_id: u32,
232    ) -> Result<Self, CollisionGroupError> {
233        if group_id > Self::MAX_GROUP_ID {
234            return Err(CollisionGroupError::GroupIdOutOfRange(group_id));
235        }
236        check_sub_group(table.sub_groups(), sub_group_id)?;
237        Ok(Self {
238            table: table.clone(),
239            group_id,
240            sub_group_id,
241        })
242    }
243
244    /// The table of the group.
245    pub fn table(&self) -> &GroupFilterTable {
246        &self.table
247    }
248
249    /// The group id.
250    pub fn group_id(&self) -> u32 {
251        self.group_id
252    }
253
254    /// The sub group, below the table's [`sub_groups`](GroupFilterTable::sub_groups).
255    pub fn sub_group_id(&self) -> u32 {
256        self.sub_group_id
257    }
258
259    /// Whether bodies of the two groups collide, by Jolt's rule (`CollisionGroup::CanCollide`).
260    pub fn can_collide(&self, other: &CollisionGroup) -> bool {
261        let (a, b) = (self.to_jph(), other.to_jph());
262        // SAFETY: both tables are live, and `a` and `b` are live locals whose sub groups are
263        // below their tables' sizes (`new`). joltc builds two temporary Jolt groups, each adding
264        // and releasing a reference to its table within the call; the table is only read.
265        unsafe { JPH_GroupFilter_CanCollide(self.table.as_ptr().cast(), &a, &b) }
266    }
267
268    /// The group as joltc's value; the table pointer stays owned by `self`.
269    pub(crate) fn to_jph(&self) -> JPH_CollisionGroup {
270        JPH_CollisionGroup {
271            // `GroupFilterTable` derives from `GroupFilter` with single inheritance.
272            groupFilter: self.table.as_ptr().cast_const().cast(),
273            groupID: self.group_id,
274            subGroupID: self.sub_group_id,
275        }
276    }
277}