use std::collections::{BTreeMap, HashMap};
use std::hash::{Hash, Hasher};
use bevy_ecs::change_detection::DetectChanges;
use bevy_ecs::component::Component;
use bevy_ecs::entity::Entity;
use bevy_ecs::lifecycle::RemovedComponents;
use bevy_ecs::prelude::{
Changed, Commands, IntoScheduleConfigs, Query, Ref, ResMut, Resource, World,
};
use bytemuck::{Pod, Zeroable};
use serde::{Deserialize, Serialize};
use super::{App, AppError, EventQueue, Plugin, ScheduleStage};
use crate::Transform;
pub const MAX_GPU_CONDITION_INSTRUCTIONS: usize = 64;
#[repr(C)]
#[derive(
Component, Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Pod, Zeroable,
)]
pub struct PhysicsId {
pub slot: u32,
pub generation: u32,
}
#[derive(Clone, Copy, Debug)]
struct PhysicsSlot {
generation: u32,
entity: Option<Entity>,
}
#[derive(Resource, Clone, Debug, Default)]
pub struct PhysicsIdRegistry {
slots: Vec<PhysicsSlot>,
free_slots: Vec<u32>,
entity_ids: HashMap<Entity, PhysicsId>,
}
impl PhysicsIdRegistry {
pub(crate) fn hash_state(&self, hasher: &mut super::StateHasher) {
for slot in &self.slots {
hasher.word(u64::from(slot.generation));
hasher.word(slot.entity.map_or(u64::MAX, Entity::to_bits));
}
for slot in &self.free_slots {
hasher.word(u64::from(*slot));
}
}
pub fn assign(&mut self, entity: Entity) -> PhysicsId {
if let Some(id) = self.entity_ids.get(&entity) {
return *id;
}
let id = if let Some(slot) = self.free_slots.pop() {
let entry = &mut self.slots[slot as usize];
entry.entity = Some(entity);
PhysicsId {
slot,
generation: entry.generation,
}
} else {
let slot = self.slots.len() as u32;
let generation = 1;
self.slots.push(PhysicsSlot {
generation,
entity: Some(entity),
});
PhysicsId { slot, generation }
};
self.entity_ids.insert(entity, id);
id
}
pub fn release(&mut self, entity: Entity) -> Option<PhysicsId> {
let old_id = self.entity_ids.remove(&entity)?;
let entry = &mut self.slots[old_id.slot as usize];
entry.entity = None;
entry.generation = next_generation(entry.generation);
self.free_slots.push(old_id.slot);
Some(old_id)
}
#[must_use]
pub fn resolve(&self, id: PhysicsId) -> Option<Entity> {
let entry = self.slots.get(id.slot as usize)?;
(entry.generation == id.generation)
.then_some(entry.entity)
.flatten()
}
#[must_use]
pub fn id_for(&self, entity: Entity) -> Option<PhysicsId> {
self.entity_ids.get(&entity).copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.entity_ids.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entity_ids.is_empty()
}
}
fn next_generation(generation: u32) -> u32 {
let next = generation.wrapping_add(1);
if next == 0 {
1
} else {
next
}
}
#[repr(transparent)]
#[derive(
Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Hash, Serialize,
)]
pub struct GpuEventId(pub u32);
#[derive(Resource, Clone, Debug, Default)]
pub struct GpuEventRegistry {
names: Vec<String>,
ids: HashMap<String, GpuEventId>,
}
impl GpuEventRegistry {
pub fn register(&mut self, name: impl Into<String>) -> GpuEventId {
let name = name.into();
if let Some(id) = self.ids.get(&name) {
return *id;
}
let id = GpuEventId((self.names.len() as u32).saturating_add(1));
self.names.push(name.clone());
self.ids.insert(name, id);
id
}
#[must_use]
pub fn id(&self, name: &str) -> Option<GpuEventId> {
self.ids.get(name).copied()
}
#[must_use]
pub fn name(&self, id: GpuEventId) -> Option<&str> {
let index = id.0.checked_sub(1)? as usize;
self.names.get(index).map(String::as_str)
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub enum GpuStateField {
PositionX,
PositionY,
PositionZ,
VelocityX,
VelocityY,
VelocityZ,
AngularVelocityX,
AngularVelocityY,
AngularVelocityZ,
ScaleX,
ScaleY,
ScaleZ,
Mass,
GravityScale,
Speed,
Custom(u8),
}
impl GpuStateField {
fn gpu_code(self) -> u32 {
match self {
Self::PositionX => 0,
Self::PositionY => 1,
Self::PositionZ => 2,
Self::VelocityX => 3,
Self::VelocityY => 4,
Self::VelocityZ => 5,
Self::AngularVelocityX => 6,
Self::AngularVelocityY => 7,
Self::AngularVelocityZ => 8,
Self::ScaleX => 9,
Self::ScaleY => 10,
Self::ScaleZ => 11,
Self::Mass => 12,
Self::GravityScale => 13,
Self::Speed => 14,
Self::Custom(index) => 0x100 + u32::from(index),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub enum GpuComparison {
Less,
LessOrEqual,
Greater,
GreaterOrEqual,
Equal,
NotEqual,
}
impl GpuComparison {
fn gpu_code(self) -> u32 {
match self {
Self::Less => 0,
Self::LessOrEqual => 1,
Self::Greater => 2,
Self::GreaterOrEqual => 3,
Self::Equal => 4,
Self::NotEqual => 5,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
enum GpuConditionNode {
Compare {
field: GpuStateField,
comparison: GpuComparison,
value: f32,
},
Range {
field: GpuStateField,
minimum: f32,
maximum: f32,
},
Colliding,
Sleeping,
TimerElapsed(f32),
And(Box<Self>, Box<Self>),
Or(Box<Self>, Box<Self>),
Not(Box<Self>),
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct GpuCondition {
node: GpuConditionNode,
}
impl GpuCondition {
#[must_use]
pub fn field(field: GpuStateField) -> GpuFieldCondition {
GpuFieldCondition { field }
}
#[must_use]
pub fn position_y() -> GpuFieldCondition {
Self::field(GpuStateField::PositionY)
}
#[must_use]
pub fn velocity_y() -> GpuFieldCondition {
Self::field(GpuStateField::VelocityY)
}
#[must_use]
pub fn custom(index: u8) -> GpuFieldCondition {
Self::field(GpuStateField::Custom(index))
}
#[must_use]
pub fn colliding() -> Self {
Self {
node: GpuConditionNode::Colliding,
}
}
#[must_use]
pub fn sleeping() -> Self {
Self {
node: GpuConditionNode::Sleeping,
}
}
#[must_use]
pub fn timer_elapsed(seconds: f32) -> Self {
Self {
node: GpuConditionNode::TimerElapsed(seconds.max(0.0)),
}
}
#[must_use]
pub fn and(self, other: Self) -> Self {
Self {
node: GpuConditionNode::And(
Box::new(self.node),
Box::new(other.node),
),
}
}
#[must_use]
pub fn or(self, other: Self) -> Self {
Self {
node: GpuConditionNode::Or(
Box::new(self.node),
Box::new(other.node),
),
}
}
#[must_use]
pub fn inverted(self) -> Self {
Self {
node: GpuConditionNode::Not(Box::new(self.node)),
}
}
pub fn compile(
&self,
) -> Result<Vec<GpuConditionInstruction>, ConditionError> {
let mut instructions = Vec::new();
compile_node(&self.node, &mut instructions)?;
if instructions.len() > MAX_GPU_CONDITION_INSTRUCTIONS {
return Err(ConditionError::TooManyInstructions {
count: instructions.len(),
maximum: MAX_GPU_CONDITION_INSTRUCTIONS,
});
}
Ok(instructions)
}
}
impl std::ops::Not for GpuCondition {
type Output = Self;
fn not(self) -> Self::Output {
self.inverted()
}
}
#[derive(Clone, Copy, Debug)]
pub struct GpuFieldCondition {
field: GpuStateField,
}
impl GpuFieldCondition {
fn compare(self, comparison: GpuComparison, value: f32) -> GpuCondition {
GpuCondition {
node: GpuConditionNode::Compare {
field: self.field,
comparison,
value,
},
}
}
#[must_use]
pub fn less_than(self, value: f32) -> GpuCondition {
self.compare(GpuComparison::Less, value)
}
#[must_use]
pub fn less_or_equal(self, value: f32) -> GpuCondition {
self.compare(GpuComparison::LessOrEqual, value)
}
#[must_use]
pub fn greater_than(self, value: f32) -> GpuCondition {
self.compare(GpuComparison::Greater, value)
}
#[must_use]
pub fn greater_or_equal(self, value: f32) -> GpuCondition {
self.compare(GpuComparison::GreaterOrEqual, value)
}
#[must_use]
pub fn equal_to(self, value: f32) -> GpuCondition {
self.compare(GpuComparison::Equal, value)
}
#[must_use]
pub fn not_equal_to(self, value: f32) -> GpuCondition {
self.compare(GpuComparison::NotEqual, value)
}
#[must_use]
pub fn inside(self, minimum: f32, maximum: f32) -> GpuCondition {
GpuCondition {
node: GpuConditionNode::Range {
field: self.field,
minimum: minimum.min(maximum),
maximum: minimum.max(maximum),
},
}
}
}
mod condition_opcode {
pub const COMPARE: u32 = 1;
pub const RANGE: u32 = 2;
pub const COLLIDING: u32 = 3;
pub const SLEEPING: u32 = 4;
pub const TIMER_ELAPSED: u32 = 5;
pub const AND: u32 = 16;
pub const OR: u32 = 17;
pub const NOT: u32 = 18;
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
pub struct GpuConditionInstruction {
pub opcode: u32,
pub operand: u32,
pub flags: u32,
pub reserved: u32,
pub values: [f32; 4],
}
fn compile_node(
node: &GpuConditionNode,
output: &mut Vec<GpuConditionInstruction>,
) -> Result<(), ConditionError> {
let instruction = match node {
GpuConditionNode::Compare {
field,
comparison,
value,
} => GpuConditionInstruction {
opcode: condition_opcode::COMPARE,
operand: field.gpu_code(),
flags: comparison.gpu_code(),
values: [*value, 0.0, 0.0, 0.0],
..Default::default()
},
GpuConditionNode::Range {
field,
minimum,
maximum,
} => GpuConditionInstruction {
opcode: condition_opcode::RANGE,
operand: field.gpu_code(),
values: [*minimum, *maximum, 0.0, 0.0],
..Default::default()
},
GpuConditionNode::Colliding => GpuConditionInstruction {
opcode: condition_opcode::COLLIDING,
..Default::default()
},
GpuConditionNode::Sleeping => GpuConditionInstruction {
opcode: condition_opcode::SLEEPING,
..Default::default()
},
GpuConditionNode::TimerElapsed(seconds) => GpuConditionInstruction {
opcode: condition_opcode::TIMER_ELAPSED,
values: [*seconds, 0.0, 0.0, 0.0],
..Default::default()
},
GpuConditionNode::And(left, right) => {
compile_node(left, output)?;
compile_node(right, output)?;
GpuConditionInstruction {
opcode: condition_opcode::AND,
..Default::default()
}
}
GpuConditionNode::Or(left, right) => {
compile_node(left, output)?;
compile_node(right, output)?;
GpuConditionInstruction {
opcode: condition_opcode::OR,
..Default::default()
}
}
GpuConditionNode::Not(inner) => {
compile_node(inner, output)?;
GpuConditionInstruction {
opcode: condition_opcode::NOT,
..Default::default()
}
}
};
output.push(instruction);
if output.len() > MAX_GPU_CONDITION_INSTRUCTIONS {
return Err(ConditionError::TooManyInstructions {
count: output.len(),
maximum: MAX_GPU_CONDITION_INSTRUCTIONS,
});
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConditionError {
TooManyInstructions { count: usize, maximum: usize },
}
impl std::fmt::Display for ConditionError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooManyInstructions { count, maximum } => write!(
formatter,
"GPU condition has {count} instructions; maximum is {maximum}"
),
}
}
}
impl std::error::Error for ConditionError {}
#[repr(u32)]
#[derive(
Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize,
)]
pub enum GpuEventMode {
#[default]
OnEnter = 0,
OnExit = 1,
WhileTrue = 2,
Once = 3,
}
#[repr(u32)]
#[derive(
Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize,
)]
pub enum GpuEventPayload {
#[default]
None = 0,
Position = 1,
Velocity = 2,
AngularVelocity = 3,
Contact = 4,
Custom = 5,
}
#[derive(
Component, Clone, Debug, Default, Deserialize, PartialEq, Serialize,
)]
pub struct GpuPhysicsWatch {
pub rules: Vec<GpuPhysicsRule>,
}
#[derive(Resource, Clone, Debug, Default, PartialEq)]
pub struct GpuPhysicsClassWatches {
pub classes: BTreeMap<String, Vec<GpuPhysicsRule>>,
}
impl GpuPhysicsClassWatches {
pub fn add(&mut self, class: impl Into<String>, rule: GpuPhysicsRule) {
let class = class.into();
let class = class.trim();
if class.is_empty() {
return;
}
let rules = self.classes.entry(class.to_owned()).or_default();
if !rules.contains(&rule) {
rules.push(rule);
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct GpuPhysicsRule {
pub event: String,
pub condition: GpuCondition,
pub mode: GpuEventMode,
pub payload: GpuEventPayload,
pub cooldown_seconds: f32,
}
impl GpuPhysicsRule {
#[must_use]
pub fn new(event: impl Into<String>, condition: GpuCondition) -> Self {
Self {
event: event.into(),
condition,
mode: GpuEventMode::OnEnter,
payload: GpuEventPayload::None,
cooldown_seconds: 0.0,
}
}
#[must_use]
pub fn mode(mut self, mode: GpuEventMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn payload(mut self, payload: GpuEventPayload) -> Self {
self.payload = payload;
self
}
#[must_use]
pub fn cooldown(mut self, seconds: f32) -> Self {
self.cooldown_seconds = seconds.max(0.0);
self
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
pub struct RawGpuPhysicsEvent {
pub body_slot: u32,
pub body_generation: u32,
pub event_id: u32,
pub flags: u32,
pub tick_low: u32,
pub tick_high: u32,
pub payload_kind: u32,
pub reserved: u32,
pub payload: [f32; 4],
}
impl RawGpuPhysicsEvent {
#[must_use]
pub fn physics_id(self) -> PhysicsId {
PhysicsId {
slot: self.body_slot,
generation: self.body_generation,
}
}
#[must_use]
pub fn tick(self) -> u64 {
u64::from(self.tick_low) | (u64::from(self.tick_high) << 32)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum GpuBodyCommand {
Teleport(Transform),
SetVelocity {
linear: [f32; 3],
angular: [f32; 3],
},
Impulse([f32; 3]),
Force([f32; 3]),
SetCustomValues([f32; 4]),
ReadState,
}
#[derive(Resource, Clone, Debug, Default, PartialEq)]
pub struct GpuPhysicsCommands {
pub commands: Vec<(PhysicsId, GpuBodyCommand)>,
pub apply_ticks: Vec<u64>,
pub read_all_states: bool,
pub reset_to_authored: bool,
}
impl GpuPhysicsCommands {
pub fn push(&mut self, body: PhysicsId, command: GpuBodyCommand) {
self.commands.push((body, command));
}
pub fn restore(&mut self, body: PhysicsId, state: &GpuStateMirror) {
self.push(body, GpuBodyCommand::Teleport(state.transform));
self.push(
body,
GpuBodyCommand::SetVelocity {
linear: state.linear_velocity,
angular: state.angular_velocity,
},
);
if let Some(values) = state.custom_values {
self.push(body, GpuBodyCommand::SetCustomValues(values));
}
}
}
pub(super) fn stamp_gpu_commands(world: &mut World) {
let tick = world.resource::<super::FrameTime>().fixed_tick + 1;
if let Some(mut commands) = world.get_resource_mut::<GpuPhysicsCommands>() {
let length = commands.commands.len();
if commands.apply_ticks.len() != length {
commands.apply_ticks.resize(length, tick);
}
}
}
pub fn request_gpu_class_snapshot(world: &mut World, class: &str) -> usize {
let members = world
.query::<(&PhysicsId, &super::ObjectClasses)>()
.iter(world)
.filter(|(_, classes)| classes.contains(class))
.map(|(id, _)| *id)
.collect::<Vec<_>>();
let mut commands = world.resource_mut::<GpuPhysicsCommands>();
for &id in &members {
commands.push(id, GpuBodyCommand::ReadState);
}
members.len()
}
pub const GPU_PHYSICS_ABI_VERSION: u32 = 1;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct GpuConditionShader {
pub events: Vec<String>,
pub glsl: String,
}
#[derive(Resource, Clone, Debug, Default, PartialEq)]
pub struct GpuConditionShaders(pub Vec<GpuConditionShader>);
impl GpuConditionShader {
#[must_use]
pub fn resolve(&self, registry: &mut GpuEventRegistry) -> String {
let ids = self
.events
.iter()
.map(|name| format!("{}u", registry.register(name.clone()).0))
.collect::<Vec<_>>();
let table = if ids.is_empty() {
String::new()
} else {
format!(
"const uint EVENTS[{}] = uint[]({});\n",
ids.len(),
ids.join(", ")
)
};
format!("{table}#line 1\n{}", self.glsl)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GpuPhysicsEventsLost {
pub count: u64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GpuPhysicsEvent {
pub entity: Entity,
pub physics_id: PhysicsId,
pub event_id: GpuEventId,
pub tick: u64,
pub flags: u32,
pub payload_kind: u32,
pub payload: [f32; 4],
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GpuEventRouteReport {
pub delivered: usize,
pub stale: usize,
pub unknown_event: usize,
}
#[derive(
Component,
Clone,
Copy,
Debug,
Default,
Deserialize,
PartialEq,
Eq,
Hash,
Serialize,
)]
pub enum PhysicsSyncMode {
None,
#[default]
Events,
SelectedState,
FullState,
}
impl PhysicsSyncMode {
pub const STATE_READBACK_BYTES: u64 = 144;
#[must_use]
pub fn reads_back_state(self) -> bool {
matches!(self, Self::SelectedState | Self::FullState)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GpuStateSample {
pub physics_id: PhysicsId,
pub tick: u64,
pub transform: Transform,
pub linear_velocity: [f32; 3],
pub angular_velocity: [f32; 3],
pub custom_values: Option<[f32; 4]>,
}
#[derive(Component, Clone, Copy, Debug, PartialEq)]
pub struct GpuStateMirror {
pub tick: u64,
pub transform: Transform,
pub linear_velocity: [f32; 3],
pub angular_velocity: [f32; 3],
pub custom_values: Option<[f32; 4]>,
}
impl GpuStateMirror {
#[must_use]
pub fn age_ticks(&self, current_tick: u64) -> u64 {
current_tick.saturating_sub(self.tick)
}
}
#[derive(
Component,
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
Serialize,
Deserialize,
)]
pub struct AutoSimulation {
#[serde(skip)]
pub decision: Option<AllocationDecision>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AllocationDecision {
pub class: super::SimulationClass,
pub reason: AllocationReason,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AllocationReason {
NotDynamic,
NoGpuBackend,
CustomSolver,
Kinematic,
CpuOnlyCollider,
ReadsStateEveryTick,
FewBodies,
ManyBodies,
CpuOverBudget,
}
#[derive(Resource, Clone, Copy, Debug, PartialEq)]
pub struct AutoAllocationPolicy {
pub gpu_min_bodies: usize,
pub cpu_physics_budget: std::time::Duration,
}
impl Default for AutoAllocationPolicy {
fn default() -> Self {
Self {
gpu_min_bodies: 256,
cpu_physics_budget: std::time::Duration::from_millis(4),
}
}
}
fn required_class(
body: &super::PhysicsBody,
rigid: Option<&super::RigidBody>,
collider: Option<&super::Collider>,
sync: Option<&PhysicsSyncMode>,
gpu: bool,
) -> Option<AllocationDecision> {
use super::SimulationClass::{Cpu, Gpu};
let decide = |class, reason| Some(AllocationDecision { class, reason });
if !body.participates_in_dynamic_simulation() {
return decide(body.simulation, AllocationReason::NotDynamic);
}
if !gpu {
return decide(Cpu, AllocationReason::NoGpuBackend);
}
if body.solver == super::PhysicsSolver::Custom {
return decide(Gpu, AllocationReason::CustomSolver);
}
if rigid.is_some_and(|rigid| rigid.kind == super::RigidBodyKind::Kinematic)
{
return decide(Cpu, AllocationReason::Kinematic);
}
if collider.is_some_and(|collider| {
collider.sensor
|| matches!(
collider.shape,
super::ColliderShape::ConvexMesh
| super::ColliderShape::TriangleMesh
)
}) {
return decide(Cpu, AllocationReason::CpuOnlyCollider);
}
if sync.is_some_and(|sync| sync.reads_back_state()) {
return decide(Cpu, AllocationReason::ReadsStateEveryTick);
}
None
}
pub fn allocate_auto_simulation(world: &mut World) {
use super::SimulationClass::{Cpu, Gpu};
let policy = world
.get_resource::<AutoAllocationPolicy>()
.copied()
.unwrap_or_default();
let gpu = world
.get_resource::<super::PhysicsBackendStatus>()
.is_some_and(|status| status.gpu_dynamic_available);
let over_budget = world
.get_resource::<super::CpuFrameTimings>()
.is_some_and(|timings| timings.physics > policy.cpu_physics_budget);
let mut query = world.query::<(
Entity,
&AutoSimulation,
&super::PhysicsBody,
Option<&super::RigidBody>,
Option<&super::Collider>,
Option<&PhysicsSyncMode>,
)>();
let mut flexible = 0;
let mut pending = Vec::new();
for (entity, auto, body, rigid, collider, sync) in query.iter(world) {
let required = if auto.decision.is_none() {
required_class(body, rigid, collider, sync, gpu)
} else {
auto.decision.filter(|decision| {
decision.reason != AllocationReason::FewBodies
&& decision.reason != AllocationReason::ManyBodies
&& decision.reason != AllocationReason::CpuOverBudget
})
};
flexible += usize::from(required.is_none());
if auto.decision.is_none() {
pending.push((entity, required));
}
}
for (entity, required) in pending {
let decision = required.unwrap_or(if over_budget {
AllocationDecision {
class: Gpu,
reason: AllocationReason::CpuOverBudget,
}
} else if flexible >= policy.gpu_min_bodies {
AllocationDecision {
class: Gpu,
reason: AllocationReason::ManyBodies,
}
} else {
AllocationDecision {
class: Cpu,
reason: AllocationReason::FewBodies,
}
});
let mut entity = world.entity_mut(entity);
if let Some(mut auto) = entity.get_mut::<AutoSimulation>() {
auto.decision = Some(decision);
}
if let Some(mut body) = entity.get_mut::<super::PhysicsBody>() {
if body.simulation != decision.class {
body.simulation = decision.class;
}
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq)]
pub struct GpuQueryProxy {
pub collider: super::Collider,
pub layers: super::CollisionLayers,
pub place_on: GpuEventId,
pub remove_on: Option<GpuEventId>,
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct GpuProxyOf(pub Entity);
fn sync_gpu_query_proxies(world: &mut World) {
let mut proxies = world
.query::<(Entity, &GpuProxyOf)>()
.iter(world)
.map(|(proxy, owner)| (owner.0, proxy))
.collect::<HashMap<_, _>>();
proxies.retain(|owner, proxy| {
let alive = world.get::<GpuQueryProxy>(*owner).is_some();
if !alive {
world.despawn(*proxy);
}
alive
});
let events = world
.resource::<EventQueue<GpuPhysicsEvent>>()
.iter()
.copied()
.collect::<Vec<_>>();
for event in events {
let Some(settings) = world.get::<GpuQueryProxy>(event.entity).copied()
else {
continue;
};
if Some(event.event_id) == settings.remove_on {
if let Some(proxy) = proxies.remove(&event.entity) {
world.despawn(proxy);
}
continue;
}
if event.event_id != settings.place_on
|| event.payload_kind != GpuEventPayload::Position as u32
{
continue;
}
let mut transform = world
.get::<Transform>(event.entity)
.copied()
.unwrap_or_default();
transform.position =
[event.payload[0], event.payload[1], event.payload[2]];
let parts = (
transform,
settings.collider,
settings.layers,
super::PhysicsBody {
simulation: super::SimulationClass::Static,
..super::PhysicsBody::default()
},
GpuProxyOf(event.entity),
);
match proxies.get(&event.entity) {
Some(&proxy) => {
world.entity_mut(proxy).insert(parts);
}
None => {
let proxy = world.spawn(parts).id();
proxies.insert(event.entity, proxy);
}
}
}
}
pub fn apply_gpu_state_samples(
world: &mut World,
samples: &[GpuStateSample],
) -> GpuEventRouteReport {
let mut report = GpuEventRouteReport::default();
for sample in samples {
let entity = world
.resource::<PhysicsIdRegistry>()
.resolve(sample.physics_id);
let Some(mut entity) =
entity.and_then(|entity| world.get_entity_mut(entity).ok())
else {
report.stale += 1;
continue;
};
if entity
.get::<GpuStateMirror>()
.is_some_and(|mirror| mirror.tick > sample.tick)
{
continue;
}
entity.insert(GpuStateMirror {
tick: sample.tick,
transform: sample.transform,
linear_velocity: sample.linear_velocity,
angular_velocity: sample.angular_velocity,
custom_values: sample.custom_values,
});
report.delivered += 1;
}
report
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExtractedGpuPhysicsRule {
pub event_id: GpuEventId,
pub instructions: Vec<GpuConditionInstruction>,
pub mode: GpuEventMode,
pub payload: GpuEventPayload,
pub cooldown_seconds: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExtractedGpuPhysicsBody {
pub entity: Entity,
pub physics_id: PhysicsId,
pub transform: Transform,
pub rigid_body: super::RigidBody,
pub solver: super::PhysicsSolver,
pub collider: Option<(super::Collider, super::CollisionLayers)>,
pub custom_shader: Option<String>,
pub rules: Vec<ExtractedGpuPhysicsRule>,
pub sync: PhysicsSyncMode,
}
#[must_use]
pub fn custom_solver_id(path: &str) -> u32 {
let hash = path.bytes().fold(0x811c_9dc5_u32, |hash, byte| {
(hash ^ u32::from(byte)).wrapping_mul(0x0100_0193)
});
(hash & 0x00ff_ffff).max(1)
}
#[must_use]
pub fn custom_solver_source(path: &str, glsl: &str) -> String {
format!(
"const uint SOLVER_ID = {}u;\n#line 1\n{glsl}\n\
void condition(inout PhysicsState body) {{\n\
if (uint(body.properties.w) == SOLVER_ID) solve(body);\n}}\n",
custom_solver_id(path)
)
}
pub(super) fn simple_gpu_physics_signature(world: &mut World) -> u64 {
let mut hasher = super::FastHasher::default();
world
.resource_ref::<GpuPhysicsClassWatches>()
.last_changed()
.get()
.hash(&mut hasher);
let mut count = 0_u64;
let mut query = world.query::<(
Entity,
&PhysicsId,
&Transform,
&super::PhysicsBody,
Option<&super::RigidBody>,
Option<Ref<super::ObjectClasses>>,
Option<Ref<GpuPhysicsWatch>>,
Option<&PhysicsSyncMode>,
Option<Ref<super::Collider>>,
Option<Ref<super::CollisionLayers>>,
)>();
for (
entity,
id,
transform,
body,
rigid_body,
classes,
watch,
sync,
collider,
layers,
) in query.iter(world)
{
if !body.uses_gpu() {
continue;
}
count += 1;
sync.copied().unwrap_or_default().hash(&mut hasher);
classes
.map(|classes| classes.last_changed().get())
.hash(&mut hasher);
watch
.map(|watch| watch.last_changed().get())
.hash(&mut hasher);
collider
.map(|collider| collider.last_changed().get())
.hash(&mut hasher);
layers
.map(|layers| layers.last_changed().get())
.hash(&mut hasher);
entity.to_bits().hash(&mut hasher);
id.hash(&mut hasher);
for value in transform
.position
.into_iter()
.chain(transform.rotation)
.chain(transform.scale)
{
value.to_bits().hash(&mut hasher);
}
let solver = match body.solver {
super::PhysicsSolver::Full => 0_u8,
super::PhysicsSolver::Simplified => 1,
super::PhysicsSolver::NoCollision => 2,
super::PhysicsSolver::Custom => 3,
super::PhysicsSolver::Space => 4,
};
solver.hash(&mut hasher);
body.custom_shader.hash(&mut hasher);
let rigid_body = rigid_body.copied().unwrap_or_default();
(rigid_body.kind as u8).hash(&mut hasher);
rigid_body.mass.to_bits().hash(&mut hasher);
rigid_body.gravity_scale.to_bits().hash(&mut hasher);
for value in rigid_body
.linear_velocity
.into_iter()
.chain(rigid_body.angular_velocity)
{
value.to_bits().hash(&mut hasher);
}
}
count.hash(&mut hasher);
hasher.finish()
}
pub(super) fn extract_gpu_physics_bodies(
world: &mut World,
) -> Vec<ExtractedGpuPhysicsBody> {
let class_watches = world.resource::<GpuPhysicsClassWatches>().clone();
let needs_class_lookup = !class_watches.classes.is_empty();
let raw = {
let mut query = world.query::<(
Entity,
&PhysicsId,
&Transform,
&super::PhysicsBody,
Option<&super::RigidBody>,
Option<&super::ObjectClasses>,
Option<&GpuPhysicsWatch>,
Option<&PhysicsSyncMode>,
Option<&super::Collider>,
Option<&super::CollisionLayers>,
)>();
query
.iter(world)
.filter(|(_, _, _, body, ..)| body.uses_gpu())
.map(
|(
entity,
id,
transform,
body,
rigid_body,
classes,
watch,
sync,
collider,
layers,
)| {
let sync = sync.copied().unwrap_or_default();
let watched = sync != PhysicsSyncMode::None;
(
entity,
*id,
*transform,
body.solver,
(body.solver == super::PhysicsSolver::Custom)
.then(|| body.custom_shader.clone())
.flatten(),
rigid_body.copied().unwrap_or_default(),
(needs_class_lookup && watched)
.then(|| classes.cloned().unwrap_or_default()),
watch.filter(|_| watched).cloned().unwrap_or_default(),
sync,
collider.map(|collider| {
(*collider, layers.copied().unwrap_or_default())
}),
)
},
)
.collect::<Vec<_>>()
};
let mut extracted = Vec::with_capacity(raw.len());
world.resource_scope(
|_, mut events: bevy_ecs::prelude::Mut<GpuEventRegistry>| {
for (
entity,
physics_id,
transform,
solver,
custom_shader,
rigid_body,
classes,
watch,
sync,
collider,
) in raw
{
let mut authored_rules = watch.rules;
if let Some(classes) = classes {
for class in classes.names {
if let Some(class_rules) =
class_watches.classes.get(&class)
{
for rule in class_rules {
if !authored_rules.contains(rule) {
authored_rules.push(rule.clone());
}
}
}
}
}
let rules = authored_rules
.into_iter()
.filter_map(|rule| {
let instructions = rule.condition.compile().ok()?;
Some(ExtractedGpuPhysicsRule {
event_id: events.register(rule.event),
instructions,
mode: rule.mode,
payload: rule.payload,
cooldown_seconds: rule.cooldown_seconds,
})
})
.collect();
extracted.push(ExtractedGpuPhysicsBody {
entity,
physics_id,
transform,
rigid_body,
solver,
collider,
custom_shader,
rules,
sync,
});
}
},
);
extracted.sort_by_key(|body| body.physics_id.slot);
extracted
}
pub fn route_gpu_physics_events(
world: &mut World,
raw_events: &[RawGpuPhysicsEvent],
) -> GpuEventRouteReport {
let resolved = {
let ids = world.resource::<PhysicsIdRegistry>();
raw_events
.iter()
.map(|raw| (*raw, ids.resolve(raw.physics_id())))
.collect::<Vec<_>>()
};
let known_events = world.resource::<GpuEventRegistry>();
let mut routed = Vec::with_capacity(resolved.len());
let mut report = GpuEventRouteReport::default();
for (raw, entity) in resolved {
let Some(entity) = entity else {
report.stale += 1;
continue;
};
let event_id = GpuEventId(raw.event_id);
if known_events.name(event_id).is_none() {
report.unknown_event += 1;
continue;
}
routed.push(GpuPhysicsEvent {
entity,
physics_id: raw.physics_id(),
event_id,
tick: raw.tick(),
flags: raw.flags,
payload_kind: raw.payload_kind,
payload: raw.payload,
});
report.delivered += 1;
}
routed.sort_by_key(|event| {
(
event.tick,
event.physics_id.slot,
event.physics_id.generation,
event.event_id.0,
event.flags,
)
});
let mut events = world.resource_mut::<EventQueue<GpuPhysicsEvent>>();
for event in routed {
events.send(event);
}
report
}
#[derive(Clone, Copy, Debug, Default)]
pub struct HybridPhysicsPlugin;
impl Plugin for HybridPhysicsPlugin {
fn build(&self, app: &mut App) -> Result<(), AppError> {
if !app.world().contains_resource::<PhysicsIdRegistry>() {
app.insert_resource(PhysicsIdRegistry::default());
}
if !app.world().contains_resource::<GpuEventRegistry>() {
app.insert_resource(GpuEventRegistry::default());
}
if !app.world().contains_resource::<GpuPhysicsClassWatches>() {
app.insert_resource(GpuPhysicsClassWatches::default());
}
if !app.world().contains_resource::<GpuPhysicsCommands>() {
app.insert_resource(GpuPhysicsCommands::default());
}
if !app.world().contains_resource::<GpuConditionShaders>() {
app.insert_resource(GpuConditionShaders::default());
}
if !app.world().contains_resource::<AutoAllocationPolicy>() {
app.insert_resource(AutoAllocationPolicy::default());
}
app.add_event::<GpuPhysicsEvent>()
.add_event::<GpuPhysicsEventsLost>()
.add_systems(
ScheduleStage::PostUpdate,
(allocate_auto_simulation, maintain_gpu_physics_ids).chain(),
)
.add_systems(ScheduleStage::Update, sync_gpu_query_proxies);
Ok(())
}
}
fn maintain_gpu_physics_ids(
mut commands: Commands,
changed_bodies: Query<
(Entity, &super::PhysicsBody),
Changed<super::PhysicsBody>,
>,
mut removed_bodies: RemovedComponents<super::PhysicsBody>,
mut registry: ResMut<PhysicsIdRegistry>,
) {
for entity in removed_bodies.read() {
registry.release(entity);
}
for (entity, body) in &changed_bodies {
if body.uses_gpu() {
let id = registry.assign(entity);
commands.entity(entity).insert(id);
} else {
registry.release(entity);
commands.entity(entity).remove::<PhysicsId>();
}
}
}