1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use crate::mpm::pipeline::MpmPipeline;
use crate::rbd::pipeline::RbdPipeline;
use crate::state::NexusState;
use khal::backend::{GpuBackend, GpuBackendError, GpuTimestamps};
bitflags::bitflags! {
/// A bit mask identifying nexus pipelines.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct NexusPipelineMask: u8 {
const RBD = 1 << 0;
const MPM = 1 << 1;
}
}
#[derive(Default)]
pub struct NexusPipeline {
pub rbd_pipeline: Option<RbdPipeline>,
pub mpm_pipeline: Option<MpmPipeline>,
}
impl NexusPipeline {
pub fn preload_pipelines(
&mut self,
backend: &GpuBackend,
pipelines: NexusPipelineMask,
) -> Result<(), GpuBackendError> {
if pipelines.contains(NexusPipelineMask::RBD) && self.rbd_pipeline.is_none() {
self.rbd_pipeline = Some(RbdPipeline::new(backend)?);
}
if pipelines.contains(NexusPipelineMask::MPM) && self.mpm_pipeline.is_none() {
self.mpm_pipeline = Some(MpmPipeline::new(backend)?);
}
Ok(())
}
/// Advances the physics simulation by one GPU timestep.
///
/// The compute pipelines are compiled lazily the first time their
/// sub-state is stepped, so the initial call is more expensive (shader
/// compilation) than the subsequent ones.
///
/// In addition, resources are loaded lazily on the GPU, so the first step
/// after inserting/removing entities can be slower too. Call `Self::finalize`
/// to pay that cost upfront.
pub async fn simulate(
&mut self,
backend: &GpuBackend,
state: &mut NexusState,
timestamps: Option<&mut GpuTimestamps>,
) -> Result<(), GpuBackendError> {
state.finalize(backend).await?;
let t0 = web_time::Instant::now();
// Profiling timestamps use a non-blocking readback (harvested in the
// viewer's `sync`). Only record a fresh frame while the previous
// readback has been consumed — otherwise we'd resolve into a staging
// buffer that's still mapped. This mirrors `auto_resize_buffers`'
// "request only when idle" rule, so timings simply update a frame or two
// apart instead of stalling the step on a blocking readback.
let mut timestamps = timestamps.filter(|ts| ts.is_idle());
// Rigid-bodies. `auto_resize_buffers` grows the collision-pair / coloring
// buffers when the previous step overflowed them.
if let Some(rbd) = state.rbd.as_mut() {
self.preload_pipelines(backend, NexusPipelineMask::RBD)?;
let pipeline = self.rbd_pipeline.as_mut().unwrap_or_else(|| unreachable!());
let steps = state.rbd_steps_per_frame.max(1);
for _ in 0..steps {
state.run_stats = pipeline.step(backend, rbd, timestamps.as_deref_mut())?;
}
pipeline.auto_resize_buffers(backend, rbd)?;
}
// MPM pipeline
if let Some(mpm) = state.mpm.as_mut() {
self.preload_pipelines(backend, NexusPipelineMask::MPM)?;
let pipeline = self.mpm_pipeline.as_mut().unwrap_or_else(|| unreachable!());
// MPM needs many small substeps per visible frame for stability.
// Upload the per-substep dt once, then run the substep loop.
let substeps = state.mpm_substeps.max(1);
let _ = mpm.write_substep_params(backend, substeps);
for _ in 0..substeps {
let _ = pipeline.step(backend, mpm, timestamps.as_deref_mut());
}
}
// MPM owns the pose of every body it is coupled to: it integrates its
// own copy each substep while the rigid-body pipeline treats those
// bodies as static. Push that copy back so rendering and the next
// step's broad phase see a boundary that actually moved.
// FIXME: the RBD pipeline should remain in charge of moving the bodies.
if let (Some(rbd), Some(mpm)) = (state.rbd.as_mut(), state.mpm.as_ref()) {
let pipeline = self.mpm_pipeline.as_ref().unwrap_or_else(|| unreachable!());
pipeline.writeback_body_poses(backend, mpm, rbd.body_poses_mut())?;
}
state.run_stats.encoding_time = t0.elapsed();
// If we recorded this frame, kick off the non-blocking readback of the
// resolved timestamps (the passes were already resolved + submitted in
// `step`). The viewer harvests it later with `try_take`, without ever
// blocking on the GPU.
if let Some(ts) = timestamps {
ts.request_read(backend);
}
Ok(())
}
}