use std::any::Any;
use std::cell::Cell;
use std::ffi::c_void;
use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
use std::ptr::{null, NonNull};
use std::sync::OnceLock;
use oxijolt_sys::*;
use crate::owned::{JoltObject, Owned};
use crate::shape::compound_sub_shape_of;
use crate::{BodyId, ObjectLayer, PhysicsWorld, QueryError, SubShapeId};
#[derive(Clone, Copy, Debug, Default)]
pub struct QueryFilter<'a> {
object_layers: Option<&'a [ObjectLayer]>,
child_groups: Option<u32>,
excluded_body: Option<BodyId>,
}
impl<'a> QueryFilter<'a> {
pub const fn new() -> Self {
Self {
object_layers: None,
child_groups: None,
excluded_body: None,
}
}
#[must_use]
pub fn object_layers(mut self, layers: &'a [ObjectLayer]) -> Self {
self.object_layers = Some(layers);
self
}
#[must_use]
pub fn child_groups(mut self, mask: u32) -> Self {
self.child_groups = Some(mask);
self
}
#[must_use]
pub fn exclude_body(mut self, body: BodyId) -> Self {
self.excluded_body = Some(body);
self
}
pub(crate) fn validate(&self, world: &PhysicsWorld) -> Result<(), QueryError> {
let layers = self.object_layers.unwrap_or_default();
if let Some(&layer) = layers
.iter()
.find(|layer| layer.get() >= world.object_layer_count)
{
return Err(QueryError::UnknownObjectLayer(layer));
}
match self.excluded_body {
Some(body) if body.world != world.tag => Err(QueryError::WrongWorld(body)),
_ => Ok(()),
}
}
pub(crate) fn keeps_group(&self, user_data: u32) -> bool {
self.child_groups
.is_none_or(|mask| user_data < 32 && mask & (1 << user_data) != 0)
}
pub(crate) fn keeps_object_layer(&self, layer: ObjectLayer) -> bool {
self.object_layers
.is_none_or(|layers| layers.contains(&layer))
}
pub(crate) fn keeps_body(&self, body: BodyId) -> bool {
self.excluded_body
.is_none_or(|excluded| excluded.to_raw() != body.to_raw())
}
}
impl JoltObject for JPH_ObjectLayerFilter {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_ObjectLayerFilter_Destroy(ptr) };
}
}
impl JoltObject for JPH_BodyFilter {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_BodyFilter_Destroy(ptr) };
}
}
impl JoltObject for JPH_ShapeFilter {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_ShapeFilter_Destroy(ptr) };
}
}
static OBJECT_LAYER_PROCS: JPH_ObjectLayerFilter_Procs = JPH_ObjectLayerFilter_Procs {
ShouldCollide: Some(object_layer_should_collide),
};
static BODY_PROCS: JPH_BodyFilter_Procs = JPH_BodyFilter_Procs {
ShouldCollide: Some(body_should_collide),
ShouldCollideLocked: None,
};
static SHAPE_PROCS: JPH_ShapeFilter_Procs = JPH_ShapeFilter_Procs {
ShouldCollide: Some(shape_should_collide),
ShouldCollide2: Some(shape_should_collide2),
};
fn install_procs() {
static INSTALLED: OnceLock<()> = OnceLock::new();
INSTALLED.get_or_init(|| {
unsafe {
JPH_ObjectLayerFilter_SetProcs(&OBJECT_LAYER_PROCS);
JPH_BodyFilter_SetProcs(&BODY_PROCS);
JPH_ShapeFilter_SetProcs(&SHAPE_PROCS);
}
});
}
pub(crate) struct FilterState<'q> {
world: &'q PhysicsWorld,
filter: &'q QueryFilter<'q>,
shape_filter: Cell<*mut JPH_ShapeFilter>,
root_cache: Cell<Option<(JPH_BodyID, NonNull<JPH_Shape>)>>,
panic: Cell<Option<Box<dyn Any + Send>>>,
}
impl<'q> FilterState<'q> {
fn new(world: &'q PhysicsWorld, filter: &'q QueryFilter<'q>) -> Self {
Self {
world,
filter,
shape_filter: Cell::new(std::ptr::null_mut()),
root_cache: Cell::new(None),
panic: Cell::new(None),
}
}
fn has_panicked(&self) -> bool {
let payload = self.panic.take();
let panicked = payload.is_some();
self.panic.set(payload);
panicked
}
pub(crate) fn guarded<R>(&self, reject: R, f: impl FnOnce() -> R) -> R {
if self.has_panicked() {
return reject;
}
match catch_unwind(AssertUnwindSafe(f)) {
Ok(value) => value,
Err(payload) => {
self.panic.set(Some(payload));
reject
}
}
}
fn current_root_shape(&self) -> Option<NonNull<JPH_Shape>> {
let body = unsafe { JPH_ShapeFilter_GetBodyID2(self.shape_filter.get()) };
if let Some((cached, root)) = self.root_cache.get() {
if cached == body {
return Some(root);
}
}
let root = unsafe { JPH_BodyInterface_GetShape(self.world.body_interface.as_ptr(), body) };
let root = NonNull::new(root.cast_mut())?;
self.root_cache.set(Some((body, root)));
Some(root)
}
fn keeps_child(&self, shape2: *const JPH_Shape, id2: JPH_SubShapeID) -> bool {
if self.filter.child_groups.is_none() {
return true;
}
let Some(root) = self.current_root_shape() else {
return false;
};
if shape2 == root.as_ptr().cast_const() {
return true;
}
let sub_type = unsafe { JPH_Shape_GetSubType(root.as_ptr()) };
if sub_type != JPH_ShapeSubType_StaticCompound
&& sub_type != JPH_ShapeSubType_MutableCompound
{
return true;
}
match unsafe { compound_sub_shape_of(root, SubShapeId::new(id2)) } {
Some(child) => self.filter.keeps_group(child.user_data),
None => false,
}
}
}
pub(crate) struct RawFilters {
pub(crate) object_layer: *const JPH_ObjectLayerFilter,
pub(crate) body: *const JPH_BodyFilter,
pub(crate) shape: *const JPH_ShapeFilter,
}
unsafe fn create_filter<T: JoltObject>(
wanted: bool,
create: unsafe extern "C" fn(*mut c_void) -> *mut T,
user_data: *mut c_void,
) -> Result<Option<Owned<T>>, QueryError> {
if !wanted {
return Ok(None);
}
unsafe { Owned::from_raw(create(user_data)) }
.map(Some)
.ok_or(QueryError::AllocationFailed)
}
pub(crate) fn with_query_filters<R>(
world: &PhysicsWorld,
filter: &QueryFilter<'_>,
run: impl FnOnce(&RawFilters, &FilterState<'_>) -> R,
) -> Result<R, QueryError> {
let state = FilterState::new(world, filter);
let result = {
install_procs();
let user_data = (&state as *const FilterState<'_>)
.cast_mut()
.cast::<c_void>();
let object_layer = unsafe {
create_filter(
filter.object_layers.is_some(),
JPH_ObjectLayerFilter_Create,
user_data,
)
}?;
let body = unsafe {
create_filter(
filter.excluded_body.is_some(),
JPH_BodyFilter_Create,
user_data,
)
}?;
let shape = unsafe {
create_filter(
filter.child_groups.is_some(),
JPH_ShapeFilter_Create,
user_data,
)
}?;
if let Some(shape) = &shape {
state.shape_filter.set(shape.as_ptr());
}
let raw = RawFilters {
object_layer: object_layer
.as_ref()
.map_or(null(), |f| f.as_ptr().cast_const()),
body: body.as_ref().map_or(null(), |f| f.as_ptr().cast_const()),
shape: shape.as_ref().map_or(null(), |f| f.as_ptr().cast_const()),
};
run(&raw, &state)
};
if let Some(payload) = state.panic.take() {
resume_unwind(payload);
}
Ok(result)
}
unsafe fn filter_state<'s>(user_data: *mut c_void) -> &'s FilterState<'s> {
unsafe { &*user_data.cast::<FilterState<'s>>() }
}
unsafe extern "C" fn object_layer_should_collide(
user_data: *mut c_void,
layer: JPH_ObjectLayer,
) -> bool {
let state = unsafe { filter_state(user_data) };
state.guarded(false, || {
#[cfg(test)]
tests::panic_if_injected(tests::Callback::ObjectLayer);
state.filter.keeps_object_layer(ObjectLayer::new(layer))
})
}
unsafe extern "C" fn body_should_collide(user_data: *mut c_void, body: JPH_BodyID) -> bool {
let state = unsafe { filter_state(user_data) };
state.guarded(false, || {
#[cfg(test)]
tests::panic_if_injected(tests::Callback::Body);
state.filter.keeps_body(BodyId::new(body, state.world.tag))
})
}
unsafe extern "C" fn shape_should_collide(
user_data: *mut c_void,
shape2: *const JPH_Shape,
sub_shape_id2: *const JPH_SubShapeID,
) -> bool {
let (state, id2) = unsafe { (filter_state(user_data), *sub_shape_id2) };
state.guarded(false, || {
#[cfg(test)]
tests::panic_if_injected(tests::Callback::Shape);
state.keeps_child(shape2, id2)
})
}
unsafe extern "C" fn shape_should_collide2(
user_data: *mut c_void,
_shape1: *const JPH_Shape,
_sub_shape_id1: *const JPH_SubShapeID,
shape2: *const JPH_Shape,
sub_shape_id2: *const JPH_SubShapeID,
) -> bool {
let (state, id2) = unsafe { (filter_state(user_data), *sub_shape_id2) };
state.guarded(false, || {
#[cfg(test)]
tests::panic_if_injected(tests::Callback::Shape2);
state.keeps_child(shape2, id2)
})
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{
BodySettings, BroadPhaseLayer, CharacterSettings, CollideShape, CollisionLayers,
CompoundChild, ExtendedUpdateSettings, Quat, RVec3, RayCast, Shape, ShapeCast, Vec3,
WorldSettings,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Callback {
ObjectLayer,
Body,
Shape,
Shape2,
}
thread_local! {
static INJECTED_PANIC: Cell<Option<Callback>> = const { Cell::new(None) };
}
pub(crate) fn inject_panic(callback: Option<Callback>) {
INJECTED_PANIC.set(callback);
}
pub(super) fn panic_if_injected(callback: Callback) {
if INJECTED_PANIC.get() == Some(callback) {
panic!("injected {callback:?} panic");
}
}
fn world() -> PhysicsWorld {
PhysicsWorld::new(WorldSettings::default()).unwrap()
}
#[test]
fn guarded_keeps_the_first_panic_and_short_circuits() {
let world = world();
let filter = QueryFilter::new();
let state = FilterState::new(&world, &filter);
assert_eq!(state.guarded(0, || 7), 7);
assert_eq!(state.guarded(0, || panic!("first")), 0);
let mut ran = false;
assert_eq!(
state.guarded(0, || {
ran = true;
7
}),
0
);
assert!(!ran);
let payload = state.panic.take().expect("the panic is kept");
assert_eq!(payload.downcast_ref::<&str>(), Some(&"first"));
}
#[test]
fn with_query_filters_re_raises_after_the_query() {
let world = world();
let layers = [ObjectLayer::NON_MOVING];
let filter = QueryFilter::new()
.object_layers(&layers)
.child_groups(1)
.exclude_body(BodyId::new(0, world.tag));
let result = catch_unwind(AssertUnwindSafe(|| {
with_query_filters(&world, &filter, |raw, state| {
assert!(!raw.object_layer.is_null());
assert!(!raw.body.is_null());
assert!(!raw.shape.is_null());
state.guarded((), || panic!("in a callback"));
})
}));
let payload = result.expect_err("the panic is re-raised");
assert_eq!(payload.downcast_ref::<&str>(), Some(&"in a callback"));
}
#[derive(Clone, Copy, Debug)]
enum Query {
Ray,
ShapeCast,
Collide,
Point,
}
fn found_body(
world: &PhysicsWorld,
filter: &QueryFilter<'_>,
query: Query,
ball: &Shape,
) -> Option<BodyId> {
let above = RVec3::new(0.0, 5.0, 0.0);
let down = Vec3::new(0.0, -10.0, 0.0);
match query {
Query::Ray => world
.cast_ray(&RayCast::new(above, down), filter)
.unwrap()
.map(|hit| hit.body),
Query::ShapeCast => world
.cast_shape(&ShapeCast::new(ball, above, Quat::IDENTITY, down), filter)
.unwrap()
.map(|hit| hit.body),
Query::Collide => {
let overlapping = RVec3::new(0.0, 0.5, 0.0);
world
.collide_shape(
&CollideShape::new(ball, overlapping, Quat::IDENTITY),
filter,
)
.unwrap()
.first()
.map(|hit| hit.body)
}
Query::Point => world
.collide_point(RVec3::new(0.0, 0.2, 0.0), filter)
.unwrap()
.first()
.map(|hit| hit.body),
}
}
#[test]
fn a_panic_in_a_filter_callback_resumes_after_the_query() {
let mut world = world();
let unit_box = Shape::new_box(Vec3::new(0.5, 0.5, 0.5)).unwrap();
let compound = Shape::new_compound(&[
CompoundChild {
shape: &unit_box,
position: Vec3::ZERO,
rotation: Quat::IDENTITY,
user_data: 1,
},
CompoundChild {
shape: &unit_box,
position: Vec3::new(3.0, 0.0, 0.0),
rotation: Quat::IDENTITY,
user_data: 2,
},
])
.unwrap();
let target = world
.create_body(&compound, &BodySettings::new_static())
.unwrap();
let ball = Shape::new_sphere(0.25).unwrap();
let excluded = world
.create_body(
&ball,
&BodySettings::new_static().position(RVec3::new(50.0, 0.0, 0.0)),
)
.unwrap();
let layers = [ObjectLayer::NON_MOVING];
let filter = QueryFilter::new()
.object_layers(&layers)
.child_groups(1 << 1)
.exclude_body(excluded);
let cases = [
(Query::Ray, Callback::ObjectLayer),
(Query::Ray, Callback::Body),
(Query::Ray, Callback::Shape),
(Query::ShapeCast, Callback::ObjectLayer),
(Query::ShapeCast, Callback::Body),
(Query::ShapeCast, Callback::Shape2),
(Query::Collide, Callback::ObjectLayer),
(Query::Collide, Callback::Body),
(Query::Collide, Callback::Shape2),
(Query::Point, Callback::ObjectLayer),
(Query::Point, Callback::Body),
(Query::Point, Callback::Shape),
];
for (query, callback) in cases {
INJECTED_PANIC.set(Some(callback));
let result = catch_unwind(AssertUnwindSafe(|| {
found_body(&world, &filter, query, &ball)
}));
INJECTED_PANIC.set(None);
let payload = result.expect_err("joltc returned and the panic resumed");
assert_eq!(
payload.downcast_ref::<String>().map(String::as_str),
Some(format!("injected {callback:?} panic").as_str()),
"{query:?}"
);
assert_eq!(
found_body(&world, &filter, query, &ball),
Some(target),
"{query:?} after a {callback:?} panic"
);
}
world.remove_body(target).unwrap();
for query in [Query::Ray, Query::ShapeCast, Query::Collide, Query::Point] {
assert_eq!(found_body(&world, &filter, query, &ball), None, "{query:?}");
}
}
#[test]
fn a_panic_in_a_filter_callback_resumes_after_a_character_update_or_refresh() {
let mut world = world();
let unit_box = Shape::new_box(Vec3::new(0.5, 0.5, 0.5)).unwrap();
let floor = Shape::new_compound(&[
CompoundChild {
shape: &unit_box,
position: Vec3::new(0.0, -0.5, 0.0),
rotation: Quat::IDENTITY,
user_data: 1,
},
CompoundChild {
shape: &unit_box,
position: Vec3::new(3.0, -0.5, 0.0),
rotation: Quat::IDENTITY,
user_data: 2,
},
])
.unwrap();
world
.create_body(&floor, &BodySettings::new_static())
.unwrap();
let ball = Shape::new_sphere(0.25).unwrap();
let excluded = world
.create_body(
&ball,
&BodySettings::new_static().position(RVec3::new(50.0, 0.0, 0.0)),
)
.unwrap();
let capsule = Shape::new_capsule(0.5, 0.3).unwrap();
let id = world
.create_character(
&CharacterSettings::new(&capsule).shape_offset(Vec3::new(0.0, 0.8, 0.0)),
RVec3::new(0.0, 0.05, 0.0),
Quat::IDENTITY,
)
.unwrap();
let layers = [ObjectLayer::NON_MOVING];
let filter = QueryFilter::new()
.object_layers(&layers)
.child_groups(1 << 1)
.exclude_body(excluded);
let update = |world: &mut PhysicsWorld| {
world
.character_mut(id)
.unwrap()
.set_linear_velocity(Vec3::new(0.5, -1.0, 0.0))
.unwrap();
world.update_character(
id,
1.0 / 60.0,
Vec3::new(0.0, -9.81, 0.0),
&ExtendedUpdateSettings::default(),
&filter,
)
};
for callback in [Callback::ObjectLayer, Callback::Body, Callback::Shape2] {
INJECTED_PANIC.set(Some(callback));
let result = catch_unwind(AssertUnwindSafe(|| update(&mut world)));
INJECTED_PANIC.set(None);
let payload = result.expect_err("joltc returned and the panic resumed");
assert_eq!(
payload.downcast_ref::<String>().map(String::as_str),
Some(format!("injected {callback:?} panic").as_str()),
);
assert_eq!(update(&mut world), Ok(()), "after a {callback:?} panic");
}
for callback in [Callback::ObjectLayer, Callback::Body, Callback::Shape2] {
INJECTED_PANIC.set(Some(callback));
let result = catch_unwind(AssertUnwindSafe(|| {
world.refresh_character_contacts(id, &filter)
}));
INJECTED_PANIC.set(None);
let payload = result.expect_err("joltc returned and the panic resumed");
assert_eq!(
payload.downcast_ref::<String>().map(String::as_str),
Some(format!("injected {callback:?} panic").as_str()),
);
assert_eq!(
world.refresh_character_contacts(id, &filter),
Ok(()),
"after a {callback:?} panic"
);
}
world.remove_body(excluded).unwrap();
}
#[test]
fn unset_parts_create_no_filters() {
let world = world();
let created = with_query_filters(&world, &QueryFilter::new(), |raw, _| {
[
raw.object_layer.is_null(),
raw.body.is_null(),
raw.shape.is_null(),
]
});
assert_eq!(created, Ok([true; 3]));
}
#[test]
fn validate_checks_layers_and_worlds() {
let other = world();
let mut layers = CollisionLayers::new(1);
let only = layers.add_object_layer(BroadPhaseLayer::new(0));
let world = PhysicsWorld::new(WorldSettings::default().layers(layers)).unwrap();
let known = [only];
assert_eq!(
QueryFilter::new().object_layers(&known).validate(&world),
Ok(())
);
let unknown = [ObjectLayer::new(1)];
assert_eq!(
QueryFilter::new().object_layers(&unknown).validate(&world),
Err(QueryError::UnknownObjectLayer(unknown[0]))
);
let foreign = BodyId::new(0, other.tag);
assert_eq!(
QueryFilter::new().exclude_body(foreign).validate(&world),
Err(QueryError::WrongWorld(foreign))
);
let removed = BodyId::new(0, world.tag);
assert!(!world.contains_body(removed));
assert_eq!(
QueryFilter::new().exclude_body(removed).validate(&world),
Ok(())
);
}
#[test]
fn group_mask_selects_user_data_values() {
let filter = QueryFilter::new().child_groups(1 << 2 | 1 << 31);
assert!(filter.keeps_group(2));
assert!(filter.keeps_group(31));
assert!(!filter.keeps_group(3));
assert!(!filter.keeps_group(32));
assert!(!filter.keeps_group(u32::MAX));
assert!(QueryFilter::new().keeps_group(u32::MAX));
}
}