use std::collections::BTreeMap;
use std::fmt;
use std::path::Path;
use bevy_ecs::prelude::{Resource, World};
use serde::{Deserialize, Serialize};
use super::{PhysicsBody, PhysicsSolver, SimulationClass};
#[derive(
Resource,
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
)]
pub enum DeterminismMode {
#[default]
Off,
Local,
CrossPlatform,
}
impl DeterminismMode {
#[must_use]
pub fn parse(name: &str) -> Option<Self> {
match name.to_ascii_lowercase().as_str() {
"off" => Some(Self::Off),
"local" => Some(Self::Local),
"crossplatform" | "cross_platform" | "cross-platform" => {
Some(Self::CrossPlatform)
}
_ => None,
}
}
}
pub const CPU_PHYSICS_PART: &str = "cpu_physics";
#[derive(Resource, Clone, Debug, Default, PartialEq, Eq)]
pub struct DeterminismSupport {
pub parts: BTreeMap<String, DeterminismMode>,
}
impl DeterminismSupport {
pub fn declare(&mut self, part: impl Into<String>, mode: DeterminismMode) {
self.parts
.entry(part.into())
.and_modify(|existing| *existing = (*existing).min(mode))
.or_insert(mode);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct DeterminismOffender {
pub part: String,
pub supports: DeterminismMode,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct DeterminismError {
pub mode: DeterminismMode,
pub offenders: Vec<DeterminismOffender>,
}
impl fmt::Display for DeterminismError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"determinism mode {:?} is not supported by",
self.mode
)?;
for (index, offender) in self.offenders.iter().enumerate() {
let separator = if index == 0 { " " } else { ", " };
write!(
formatter,
"{separator}{} (supports {:?})",
offender.part, offender.supports
)?;
}
Ok(())
}
}
impl std::error::Error for DeterminismError {}
#[must_use]
pub fn body_part(
body: &PhysicsBody,
project_root: Option<&Path>,
) -> Option<(String, DeterminismMode)> {
let local = DeterminismMode::Local;
match body.simulation {
SimulationClass::None | SimulationClass::Static => None,
SimulationClass::Cpu => Some((CPU_PHYSICS_PART.into(), local)),
SimulationClass::Gpu => {
Some(match (body.solver, &body.custom_shader) {
(PhysicsSolver::Custom, Some(path)) => {
let source = project_root
.map_or_else(|| path.into(), |root| root.join(path));
let mode = std::fs::read_to_string(source)
.ok()
.and_then(|text| shader_determinism(&text))
.unwrap_or_default();
(format!("gpu_shader:{path}"), mode)
}
(solver, _) => (format!("gpu_solver:{solver:?}"), local),
})
}
}
}
#[must_use]
pub fn shader_determinism(source: &str) -> Option<DeterminismMode> {
source.lines().find_map(|line| {
let rest = line.trim().strip_prefix("//")?.trim();
let rest = rest.strip_prefix("rusting:")?.trim();
let value = rest.strip_prefix("determinism")?.trim();
DeterminismMode::parse(value.strip_prefix('=')?.trim())
})
}
pub fn check_parts(
mode: DeterminismMode,
parts: &BTreeMap<String, DeterminismMode>,
) -> Result<(), DeterminismError> {
let offenders: Vec<_> = parts
.iter()
.filter(|(_, supports)| **supports < mode)
.map(|(part, supports)| DeterminismOffender {
part: part.clone(),
supports: *supports,
})
.collect();
if offenders.is_empty() {
Ok(())
} else {
Err(DeterminismError { mode, offenders })
}
}
pub fn check_determinism(world: &mut World) -> Result<(), DeterminismError> {
let mode = world
.get_resource::<DeterminismMode>()
.copied()
.unwrap_or_default();
if mode == DeterminismMode::Off {
return Ok(());
}
let mut support = world
.get_resource::<DeterminismSupport>()
.cloned()
.unwrap_or_default();
let mut bodies = world.query::<&PhysicsBody>();
for body in bodies.iter(world) {
if let Some((part, supports)) = body_part(body, None) {
support.declare(part, supports);
}
}
check_parts(mode, &support.parts)
}