use std::any::Any;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::num::NonZeroU64;
use std::ptr::NonNull;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
use oxijolt_sys::*;
use crate::character::CharacterEntry;
use crate::constraint::ConstraintEntry;
use crate::job_system::{create_caller_job_system, QueueContext};
use crate::jolt_assert;
use crate::limits;
use crate::listener::{first_payload, Listeners};
use crate::owned::{JoltObject, Owned};
use crate::ragdoll::RagdollEntry;
use crate::vehicle::VehicleEntry;
use crate::{CharacterContactListener, CollisionLayers, JobSystem, StepError, Vec3, WorldError};
mod rebase;
pub(crate) fn ensure_initialized() -> bool {
static INIT: OnceLock<bool> = OnceLock::new();
*INIT.get_or_init(|| {
jolt_assert::install();
unsafe { JPH_Init() }
})
}
fn lock_joltc_globals() -> MutexGuard<'static, ()> {
static GLOBALS: Mutex<()> = Mutex::new(());
GLOBALS.lock().unwrap_or_else(PoisonError::into_inner)
}
#[derive(Clone)]
enum JobSystemChoice {
ThreadPool(u32),
Caller(Arc<dyn JobSystem>),
}
impl fmt::Debug for JobSystemChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ThreadPool(workers) => f.debug_tuple("ThreadPool").field(workers).finish(),
Self::Caller(_) => f.write_str("Caller(JobSystem)"),
}
}
}
impl JobSystemChoice {
fn max_concurrency(&self) -> Result<u32, WorldError> {
match self {
Self::ThreadPool(workers) => Ok(workers + 1),
Self::Caller(job_system) => {
let value = job_system.max_concurrency();
if (1..=WorldSettings::MAX_CONCURRENCY).contains(&value) {
Ok(value)
} else {
Err(WorldError::InvalidValue(
"job system max_concurrency must be between 1 and 65",
))
}
}
}
}
}
#[derive(Clone, Debug)]
pub struct WorldSettings {
max_bodies: u32,
max_body_pairs: u32,
max_contact_constraints: u32,
jobs: JobSystemChoice,
temp_allocator_size: u32,
gravity: Vec3,
layers: CollisionLayers,
velocity_steps: u32,
position_steps: u32,
}
impl Default for WorldSettings {
fn default() -> Self {
Self {
max_bodies: 10240,
max_body_pairs: 65536,
max_contact_constraints: 10240,
jobs: JobSystemChoice::ThreadPool(1),
temp_allocator_size: 10 * 1024 * 1024,
gravity: Vec3::new(0.0, -9.81, 0.0),
layers: CollisionLayers::default(),
velocity_steps: 10,
position_steps: 2,
}
}
}
impl WorldSettings {
pub const MAX_BODIES: u32 = 1 << 23;
pub const MAX_WORKER_THREADS: u32 = 64;
pub const MAX_CONCURRENCY: u32 = Self::MAX_WORKER_THREADS + 1;
pub const MAX_CONTACT_CONSTRAINTS: u32 = 1 << 20;
pub const MIN_VELOCITY_STEPS: u32 = 2;
pub const MAX_SOLVER_STEPS: u32 = 255;
#[must_use]
pub fn max_bodies(mut self, value: u32) -> Self {
self.max_bodies = value;
self
}
#[must_use]
pub fn max_body_pairs(mut self, value: u32) -> Self {
self.max_body_pairs = value;
self
}
#[must_use]
pub fn max_contact_constraints(mut self, value: u32) -> Self {
self.max_contact_constraints = value;
self
}
#[must_use]
pub fn worker_threads(mut self, value: u32) -> Self {
self.jobs = JobSystemChoice::ThreadPool(value);
self
}
#[must_use]
pub fn job_system(mut self, value: Arc<dyn JobSystem>) -> Self {
self.jobs = JobSystemChoice::Caller(value);
self
}
#[must_use]
pub fn temp_allocator_size(mut self, value: u32) -> Self {
self.temp_allocator_size = value;
self
}
#[must_use]
pub fn gravity(mut self, value: Vec3) -> Self {
self.gravity = value;
self
}
#[must_use]
pub fn layers(mut self, value: CollisionLayers) -> Self {
self.layers = value;
self
}
#[must_use]
pub fn velocity_steps(mut self, value: u32) -> Self {
self.velocity_steps = value;
self
}
#[must_use]
pub fn position_steps(mut self, value: u32) -> Self {
self.position_steps = value;
self
}
pub(crate) fn validate(&self) -> Result<(), WorldError> {
let invalid = |what| Err(WorldError::InvalidValue(what));
if !(1..=Self::MAX_BODIES).contains(&self.max_bodies) {
return invalid("max_bodies must be between 1 and 2^23");
}
if self.max_body_pairs == 0 {
return invalid("max_body_pairs must be at least 1");
}
if !(1..=Self::MAX_CONTACT_CONSTRAINTS).contains(&self.max_contact_constraints) {
return invalid("max_contact_constraints must be between 1 and 2^20");
}
if let JobSystemChoice::ThreadPool(workers) = self.jobs {
if !(1..=Self::MAX_WORKER_THREADS).contains(&workers) {
return invalid("worker_threads must be between 1 and 64");
}
}
if self.temp_allocator_size == 0 {
return invalid("temp_allocator_size must be at least 1");
}
if !limits::is_acceleration(self.gravity) {
return invalid(limits::GRAVITY_RULE);
}
if !(Self::MIN_VELOCITY_STEPS..=Self::MAX_SOLVER_STEPS).contains(&self.velocity_steps) {
return invalid("velocity_steps must be between 2 and 255");
}
if self.position_steps > Self::MAX_SOLVER_STEPS {
return invalid("position_steps must be at most 255");
}
self.layers.validate()
}
}
impl JoltObject for JPH_TempAllocator {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_TempAllocator_Destroy(ptr) };
}
}
impl JoltObject for JPH_JobSystem {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_JobSystem_Destroy(ptr) };
}
}
enum WorldJobSystem {
ThreadPool(Owned<JPH_JobSystem>),
Caller(Arc<QueueContext>),
}
impl WorldJobSystem {
fn thread_pool(workers: u32) -> Result<Self, WorldError> {
let config = JobSystemThreadPoolConfig {
maxJobs: 0,
maxBarriers: 0,
numThreads: workers as i32,
};
let pool = unsafe { Owned::from_raw(JPH_JobSystemThreadPool_Create(&config)) }
.ok_or(WorldError::AllocationFailed("job system"))?;
Ok(Self::ThreadPool(pool))
}
fn as_ptr(&self) -> *mut JPH_JobSystem {
match self {
Self::ThreadPool(pool) => pool.as_ptr(),
Self::Caller(context) => context.as_ptr(),
}
}
fn finish_update(&self) -> Option<Box<dyn Any + Send>> {
match self {
Self::ThreadPool(_) => None,
Self::Caller(context) => context.finish_update(),
}
}
}
impl JoltObject for JPH_PhysicsSystem {
unsafe fn destroy(ptr: *mut Self) {
let _globals = lock_joltc_globals();
unsafe { JPH_PhysicsSystem_Destroy(ptr) };
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub(crate) struct WorldTag(NonZeroU64);
impl WorldTag {
pub(crate) fn next() -> Self {
static NEXT_WORLD: AtomicU64 = AtomicU64::new(1);
let value = NEXT_WORLD.fetch_add(1, Ordering::Relaxed);
Self(NonZeroU64::new(value).expect("world counter overflowed"))
}
}
pub(crate) fn advance_structure_epoch(epoch: &mut u64) {
*epoch = epoch.checked_add(1).expect("structure epoch overflowed");
}
pub struct PhysicsWorld {
pub(crate) characters: BTreeMap<u32, CharacterEntry>,
pub(crate) vehicles: BTreeMap<u32, VehicleEntry>,
pub(crate) ragdolls: BTreeMap<u32, RagdollEntry>,
pub(crate) constraints: BTreeMap<u32, ConstraintEntry>,
pub(crate) character_collision: Option<Owned<JPH_CharacterVsCharacterCollision>>,
pub(crate) system: Owned<JPH_PhysicsSystem>,
pub(crate) listeners: Listeners,
job_system: WorldJobSystem,
pub(crate) temp_allocator: Owned<JPH_TempAllocator>,
pub(crate) body_interface: NonNull<JPH_BodyInterface>,
pub(crate) body_lock_interface: NonNull<JPH_BodyLockInterface>,
pub(crate) narrow_phase_query: NonNull<JPH_NarrowPhaseQuery>,
pub(crate) broad_phase_query: NonNull<JPH_BroadPhaseQuery>,
pub(crate) object_layer_count: u32,
pub(crate) tag: WorldTag,
pub(crate) inner_bodies: BTreeSet<u32>,
pub(crate) next_character_id: u32,
pub(crate) vehicle_bodies: BTreeMap<u32, u32>,
pub(crate) next_vehicle_id: u32,
pub(crate) ragdoll_bodies: BTreeMap<u32, u32>,
pub(crate) next_ragdoll_id: u32,
pub(crate) constraint_bodies: BTreeMap<u32, BTreeSet<u32>>,
pub(crate) next_constraint_id: u32,
pub(crate) structure_epoch: u64,
pub(crate) pending_cache_invalidations: BTreeSet<u32>,
pub(crate) character_listener: Option<Arc<dyn CharacterContactListener>>,
}
impl fmt::Debug for PhysicsWorld {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PhysicsWorld")
.field("body_count", &self.body_count())
.field("constraint_count", &self.constraint_count())
.finish_non_exhaustive()
}
}
impl Drop for PhysicsWorld {
fn drop(&mut self) {
unsafe { self.listeners.detach(self.system.as_ptr()) };
self.remove_all_constraints();
self.remove_all_vehicles();
self.remove_all_ragdolls();
}
}
unsafe impl Send for PhysicsWorld {}
unsafe impl Sync for PhysicsWorld {}
unsafe fn physics_settings(system: *mut JPH_PhysicsSystem) -> JPH_PhysicsSettings {
let mut settings: JPH_PhysicsSettings = unsafe { std::mem::zeroed() };
unsafe { JPH_PhysicsSystem_GetPhysicsSettings(system, &mut settings) };
settings
}
pub(crate) const DELTA_TIME_RULE: &str =
"delta time must be finite and between MIN_DELTA_TIME and MAX_DELTA_TIME";
impl PhysicsWorld {
pub const MAX_DELTA_TIME: f32 = 1.0;
pub const MIN_DELTA_TIME: f32 = 1.0e-6;
pub(crate) fn is_valid_delta_time(delta_time: f32) -> bool {
delta_time.is_finite()
&& (Self::MIN_DELTA_TIME..=Self::MAX_DELTA_TIME).contains(&delta_time)
}
pub fn new(settings: WorldSettings) -> Result<Self, WorldError> {
settings.validate()?;
let max_concurrency = settings.jobs.max_concurrency()?;
if !ensure_initialized() {
return Err(WorldError::InitFailed);
}
let temp_allocator =
unsafe { Owned::from_raw(JPH_TempAllocator_Create(settings.temp_allocator_size)) }
.ok_or(WorldError::AllocationFailed("temp allocator"))?;
let job_system = match &settings.jobs {
JobSystemChoice::ThreadPool(workers) => WorldJobSystem::thread_pool(*workers)?,
JobSystemChoice::Caller(job_system) => WorldJobSystem::Caller(
create_caller_job_system(Arc::clone(job_system), max_concurrency)?,
),
};
let tables = unsafe { settings.layers.create_tables() }?;
let (broad_phase, pair_filter, object_vs_broad_phase) = tables.as_raw();
let system_settings = JPH_PhysicsSystemSettings {
maxBodies: settings.max_bodies,
numBodyMutexes: 0,
maxBodyPairs: settings.max_body_pairs,
maxContactConstraints: settings.max_contact_constraints,
_padding: 0,
broadPhaseLayerInterface: broad_phase,
objectLayerPairFilter: pair_filter,
objectVsBroadPhaseLayerFilter: object_vs_broad_phase,
};
let system = {
let _globals = lock_joltc_globals();
unsafe { Owned::from_raw(JPH_PhysicsSystem_Create(&system_settings)) }
};
let Some(system) = system else {
return Err(WorldError::AllocationFailed("physics system"));
};
tables.forget();
let gravity = settings.gravity.to_jph();
unsafe { JPH_PhysicsSystem_SetGravity(system.as_ptr(), &gravity) };
let mut physics = unsafe { physics_settings(system.as_ptr()) };
physics.numVelocitySteps = settings.velocity_steps;
physics.numPositionSteps = settings.position_steps;
unsafe { JPH_PhysicsSystem_SetPhysicsSettings(system.as_ptr(), &mut physics) };
let (body_interface, body_lock_interface, narrow_phase_query, broad_phase_query) = unsafe {
(
JPH_PhysicsSystem_GetBodyInterface(system.as_ptr()),
JPH_PhysicsSystem_GetBodyLockInterface(system.as_ptr()),
JPH_PhysicsSystem_GetNarrowPhaseQuery(system.as_ptr()),
JPH_PhysicsSystem_GetBroadPhaseQuery(system.as_ptr()),
)
};
let body_interface =
NonNull::new(body_interface).ok_or(WorldError::AllocationFailed("body interface"))?;
let body_lock_interface = NonNull::new(body_lock_interface.cast_mut())
.ok_or(WorldError::AllocationFailed("body lock interface"))?;
let narrow_phase_query = NonNull::new(narrow_phase_query.cast_mut())
.ok_or(WorldError::AllocationFailed("narrow phase query"))?;
let broad_phase_query = NonNull::new(broad_phase_query.cast_mut())
.ok_or(WorldError::AllocationFailed("broad phase query"))?;
Ok(Self {
characters: BTreeMap::new(),
vehicles: BTreeMap::new(),
ragdolls: BTreeMap::new(),
constraints: BTreeMap::new(),
character_collision: None,
system,
listeners: Listeners::default(),
job_system,
temp_allocator,
body_interface,
body_lock_interface,
narrow_phase_query,
broad_phase_query,
object_layer_count: settings.layers.object_layer_count(),
tag: WorldTag::next(),
inner_bodies: BTreeSet::new(),
next_character_id: 1,
vehicle_bodies: BTreeMap::new(),
next_vehicle_id: 1,
ragdoll_bodies: BTreeMap::new(),
next_ragdoll_id: 1,
constraint_bodies: BTreeMap::new(),
next_constraint_id: 1,
structure_epoch: 0,
pending_cache_invalidations: BTreeSet::new(),
character_listener: None,
})
}
pub(crate) fn note_structure_change(&mut self) {
advance_structure_epoch(&mut self.structure_epoch);
}
pub fn gravity(&self) -> Vec3 {
let mut gravity = Vec3::ZERO.to_jph();
unsafe { JPH_PhysicsSystem_GetGravity(self.system.as_ptr(), &mut gravity) };
Vec3::from_jph(gravity)
}
pub fn set_gravity(&mut self, gravity: Vec3) -> Result<(), WorldError> {
if !limits::is_acceleration(gravity) {
return Err(WorldError::InvalidValue(limits::GRAVITY_RULE));
}
let gravity = gravity.to_jph();
unsafe { JPH_PhysicsSystem_SetGravity(self.system.as_ptr(), &gravity) };
Ok(())
}
pub fn velocity_steps(&self) -> u32 {
unsafe { physics_settings(self.system.as_ptr()) }.numVelocitySteps
}
pub fn position_steps(&self) -> u32 {
unsafe { physics_settings(self.system.as_ptr()) }.numPositionSteps
}
pub fn body_count(&self) -> u32 {
unsafe { JPH_PhysicsSystem_GetNumBodies(self.system.as_ptr()) }
}
pub(crate) fn has_room_for_bodies(&self, count: usize) -> bool {
let max_bodies = unsafe { JPH_PhysicsSystem_GetMaxBodies(self.system.as_ptr()) };
max_bodies
.checked_sub(self.body_count())
.is_some_and(|room| count as u64 <= u64::from(room))
}
pub fn optimize_broad_phase(&mut self) {
unsafe { JPH_PhysicsSystem_OptimizeBroadPhase(self.system.as_ptr()) };
}
pub fn step(&mut self, delta_time: f32) -> Result<StepReport, StepError> {
if !Self::is_valid_delta_time(delta_time) {
return Err(StepError::InvalidDeltaTime);
}
self.listeners.begin_step();
let simulates = self.simulates();
if simulates {
self.apply_cache_invalidations();
}
let errors = unsafe {
JPH_PhysicsSystem_Update2(
self.system.as_ptr(),
delta_time,
1,
self.temp_allocator.as_ptr(),
self.job_system.as_ptr(),
)
};
if simulates {
self.pending_cache_invalidations.clear();
}
let job_panic = self.job_system.finish_update();
let listeners = self.listeners.finish_step();
if let Some(payload) = first_payload(job_panic, listeners.panic) {
std::panic::resume_unwind(payload);
}
Ok(StepReport {
manifold_cache_full: errors & JPH_PhysicsUpdateError_ManifoldCacheFull != 0,
body_pair_cache_full: errors & JPH_PhysicsUpdateError_BodyPairCacheFull != 0,
contact_constraints_full: errors & JPH_PhysicsUpdateError_ContactConstraintsFull != 0,
rejected_contact_settings: listeners.rejected_contact_settings,
})
}
}
impl PhysicsWorld {
fn simulates(&self) -> bool {
let active = unsafe {
JPH_PhysicsSystem_GetNumActiveBodies(self.system.as_ptr(), JPH_BodyType_Rigid)
+ JPH_PhysicsSystem_GetNumActiveBodies(self.system.as_ptr(), JPH_BodyType_Soft)
};
active > 0 || !self.vehicles.is_empty()
}
fn apply_cache_invalidations(&mut self) {
for &raw in &self.pending_cache_invalidations {
unsafe { JPH_BodyInterface_InvalidateContactCache(self.body_interface.as_ptr(), raw) };
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[must_use = "a step may have dropped contacts; check `is_complete`"]
#[non_exhaustive]
pub struct StepReport {
pub manifold_cache_full: bool,
pub body_pair_cache_full: bool,
pub contact_constraints_full: bool,
pub rejected_contact_settings: u32,
}
impl StepReport {
pub fn is_complete(&self) -> bool {
!(self.manifold_cache_full || self.body_pair_cache_full || self.contact_constraints_full)
}
}
#[cfg(test)]
mod tests;