use alloc::collections::BTreeSet;
use concinnity_memory::{Ledger, MemTag, Realm};
use concinnity_physics::{
BodyHandle, ContactHit, PhysicsBudget, PhysicsCounts, SensorCrossing, Simulation,
};
use crate::components::{
BodyDynamics, Camera3D, CharacterRig, Collider, PhysicsJoint, Transform, TriggerVolume,
};
use crate::ecs::asset_id::AssetId;
use crate::ecs::{Entity, EntityByName, PhysicsBudgetRecord, PipelineContext};
use super::props::{PropCollSnap, PropPhysics};
use super::rig::RigPhysics;
const CONTACTS_PER_DYNAMIC_BODY: usize = 4;
const CROSSINGS_PER_SENSOR: usize = 2;
pub(crate) fn budget_of(record: &PhysicsBudgetRecord) -> PhysicsBudget {
PhysicsBudget {
fixed: record.fixed,
dynamic: record.dynamic,
kinematic: record.kinematic,
sensors: record.sensors,
joints: record.joints,
anchors: record.anchors,
spawn_headroom: record.spawn_headroom,
}
}
#[cfg(test)]
pub(crate) fn record_of(budget: &PhysicsBudget) -> PhysicsBudgetRecord {
PhysicsBudgetRecord {
fixed: budget.fixed,
dynamic: budget.dynamic,
kinematic: budget.kinematic,
sensors: budget.sensors,
joints: budget.joints,
anchors: budget.anchors,
spawn_headroom: budget.spawn_headroom,
}
}
pub(crate) fn scan_counts(ctx: &PipelineContext) -> PhysicsCounts {
let bodies: BTreeSet<Entity> = ctx
.join2::<Collider, Transform>()
.map(|(e, ..)| e)
.collect();
let dynamics: BTreeSet<Entity> = ctx
.query_with_entity::<BodyDynamics>()
.map(|(e, _)| e)
.collect();
let dynamic_colliders = bodies.iter().filter(|e| dynamics.contains(e)).count() as u32;
let named = ctx.resource::<EntityByName>();
let has_body = |id: AssetId| {
named
.and_then(|index| index.0.get(&id))
.is_some_and(|entity| bodies.contains(entity))
};
let mut counts = PhysicsCounts {
static_colliders: bodies.len() as u32 - dynamic_colliders,
dynamic_colliders,
trigger_volumes: ctx.query::<TriggerVolume>().count() as u32,
rig_capsules: ctx.query::<CharacterRig>().count() as u32,
..PhysicsCounts::default()
};
for joint in ctx.query::<PhysicsJoint>() {
if !joint.body_a.is_some_and(has_body) {
continue;
}
match joint.body_b {
Some(body_b) if has_body(body_b) => counts.joints += 1,
Some(_) => continue,
None => {
counts.joints += 1;
counts.world_anchored_joints += 1;
}
}
}
let first_person = ctx.query::<Camera3D>().next().is_some_and(|camera| {
camera
.controller
.as_ref()
.is_none_or(|ctrl| ctrl.follow.is_none())
});
if first_person {
counts.player_capsules = 1;
}
counts
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct DriverCapacities {
pub(crate) props: usize,
pub(crate) refused: usize,
pub(crate) sampled: usize,
pub(crate) new_props: usize,
pub(crate) rigs: usize,
pub(crate) root_motions: usize,
pub(crate) contacts: usize,
pub(crate) contact_pairs: usize,
pub(crate) sensor_crossings: usize,
pub(crate) sensor_filters: usize,
}
impl DriverCapacities {
pub(crate) fn derive(budget: &PhysicsBudget) -> Self {
let statics = budget.fixed.saturating_sub(1) as usize;
let headroom = budget.spawn_headroom as usize;
let dynamic = budget.dynamic as usize + headroom;
Self {
props: statics + dynamic,
refused: headroom,
sampled: dynamic,
new_props: headroom,
rigs: budget.kinematic as usize,
root_motions: budget.kinematic as usize,
contacts: dynamic * CONTACTS_PER_DYNAMIC_BODY,
contact_pairs: dynamic * CONTACTS_PER_DYNAMIC_BODY,
sensor_crossings: budget.sensors as usize * CROSSINGS_PER_SENSOR,
sensor_filters: budget.sensors as usize,
}
}
}
fn vec_bytes<T>(capacity: usize) -> u64 {
(capacity as u64).saturating_mul(size_of::<T>() as u64)
}
pub(crate) fn reserved_bytes(budget: &PhysicsBudget) -> u64 {
let caps = DriverCapacities::derive(budget);
vec_bytes::<PropPhysics>(caps.props)
+ vec_bytes::<(BodyHandle, Entity)>(caps.props)
+ vec_bytes::<Entity>(caps.props)
+ vec_bytes::<Entity>(caps.refused)
+ vec_bytes::<(Entity, [f32; 3], [f32; 3])>(caps.sampled)
+ vec_bytes::<(Entity, PropCollSnap)>(caps.new_props)
+ vec_bytes::<RigPhysics>(caps.rigs)
+ vec_bytes::<crate::components::RootMotionEvent>(caps.root_motions)
+ vec_bytes::<ContactHit>(caps.contacts)
+ vec_bytes::<SensorCrossing>(caps.sensor_crossings)
+ vec_bytes::<((BodyHandle, BodyHandle), ContactHit)>(caps.contact_pairs)
+ vec_bytes::<((BodyHandle, BodyHandle), u64)>(caps.contact_pairs)
+ vec_bytes::<(u64, (AssetId, crate::components::TriggerFilter))>(caps.sensor_filters)
}
pub(crate) fn publish_reservation(ledger: &Ledger, budget: &PhysicsBudget, world: &Simulation) {
let driver = reserved_bytes(budget);
let simulation = world.reserved_bytes();
let bytes = driver.saturating_add(simulation);
ledger.set(MemTag::Physics, Realm::Host, bytes);
ledger.set_budget(MemTag::Physics, Realm::Host, Some(bytes));
}
#[cfg(test)]
mod tests {
use super::*;
fn budget(counts: PhysicsCounts, headroom: u32) -> PhysicsBudget {
PhysicsBudget::derive(&counts, headroom)
}
#[test]
fn a_record_round_trips_through_the_budget_it_describes() {
let original = budget(
PhysicsCounts {
static_colliders: 7,
dynamic_colliders: 3,
trigger_volumes: 2,
joints: 1,
world_anchored_joints: 1,
player_capsules: 1,
rig_capsules: 4,
},
12,
);
assert_eq!(budget_of(&record_of(&original)), original);
}
#[test]
fn capacities_hold_the_authored_content_plus_the_headroom() {
let caps = DriverCapacities::derive(&budget(
PhysicsCounts {
static_colliders: 5,
dynamic_colliders: 2,
trigger_volumes: 3,
player_capsules: 1,
rig_capsules: 2,
..PhysicsCounts::default()
},
10,
));
assert_eq!(caps.props, 5 + 2 + 10, "statics, dynamics, and spawns");
assert_eq!(caps.sampled, 2 + 10, "only dynamic props are written back");
assert_eq!(caps.refused, 10);
assert_eq!(caps.new_props, 10);
assert_eq!(caps.rigs, 3, "the player capsule plus two rigs");
assert_eq!(caps.sensor_filters, 3);
assert!(caps.contacts > 0 && caps.sensor_crossings > 0);
}
#[test]
fn an_empty_world_reserves_no_prop_capacity() {
let caps = DriverCapacities::derive(&budget(PhysicsCounts::default(), 0));
assert_eq!(caps.props, 0);
assert_eq!(caps.sampled, 0);
assert_eq!(caps.contacts, 0);
}
#[test]
fn reserved_bytes_grow_with_the_budget_they_describe() {
let empty = reserved_bytes(&budget(PhysicsCounts::default(), 0));
assert_eq!(empty, 0, "a floor-only world holds nothing per body");
let small = reserved_bytes(&budget(
PhysicsCounts {
static_colliders: 4,
..PhysicsCounts::default()
},
0,
));
assert!(small > 0);
let with_headroom = reserved_bytes(&budget(
PhysicsCounts {
static_colliders: 4,
..PhysicsCounts::default()
},
64,
));
assert!(
with_headroom > small,
"headroom is reserved, not borrowed later"
);
}
#[test]
fn the_published_row_counts_the_simulation_as_well_as_the_driver() {
let ledger = Ledger::new();
let budget = budget(
PhysicsCounts {
static_colliders: 8,
dynamic_colliders: 4,
..PhysicsCounts::default()
},
16,
);
let world = Simulation::with_capacity(budget.body_cap() as usize);
publish_reservation(&ledger, &budget, &world);
let driver = reserved_bytes(&budget);
let usage = ledger.usage(MemTag::Physics, Realm::Host);
assert!(driver > 0, "a world with bodies holds something");
assert_eq!(usage.bytes, driver + world.reserved_bytes());
assert!(
usage.bytes > driver,
"the simulation reserves against the same budget"
);
assert_eq!(
usage.budget,
Some(usage.bytes),
"everything is reserved at init, so usage is the ceiling"
);
}
#[test]
fn a_reservation_is_its_entries_times_their_size() {
assert_eq!(vec_bytes::<u32>(0), 0);
assert_eq!(vec_bytes::<u32>(10), 40);
assert_eq!(vec_bytes::<(u64, u64)>(8), 128);
}
}