Skip to main content

oxiphysics_gpu/pipeline/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4use super::functions::*;
5use std::time::Instant;
6
7/// Schedules compute dispatches to maximise GPU utilisation through overlapping.
8///
9/// Classifies dispatches as either "latency-critical" (must finish before the
10/// next frame) or "background" (can run async on a secondary queue).
11#[derive(Debug)]
12pub struct ComputeOverlapScheduler {
13    /// Number of latency-critical dispatches submitted this frame.
14    pub critical_count: usize,
15    /// Number of background dispatches submitted this frame.
16    pub background_count: usize,
17    pub(super) recorder: MultiQueueRecorder,
18}
19impl ComputeOverlapScheduler {
20    /// Create a new scheduler.
21    pub fn new() -> Self {
22        Self {
23            critical_count: 0,
24            background_count: 0,
25            recorder: MultiQueueRecorder::new(),
26        }
27    }
28    /// Submit a latency-critical dispatch (→ main queue).
29    pub fn submit_critical(&mut self, batch: DispatchBatch) {
30        self.critical_count += 1;
31        self.recorder.submit(batch, QueueType::Main);
32    }
33    /// Submit a background dispatch (→ async compute queue).
34    pub fn submit_background(&mut self, batch: DispatchBatch) {
35        self.background_count += 1;
36        self.recorder.submit(batch, QueueType::AsyncCompute);
37    }
38    /// Execute all pending work and reset frame counters.
39    pub fn end_frame(&mut self) -> usize {
40        let n = self.recorder.flush_all();
41        self.critical_count = 0;
42        self.background_count = 0;
43        n
44    }
45    /// Returns `true` if there is any pending work.
46    pub fn has_pending(&self) -> bool {
47        self.recorder.pending_total() > 0
48    }
49}
50/// Configuration for the high-level physics pipeline.
51#[derive(Debug, Clone)]
52pub struct PipelineConfig {
53    /// Which stages are active.  Stages not in this list are skipped.
54    pub enabled_stages: Vec<PipelineStage>,
55    /// Number of sub-steps per call to [`PhysicsPipeline::step`].
56    pub substeps: u32,
57    /// Whether a GPU backend should be preferred over the CPU fallback.
58    pub use_gpu: bool,
59}
60impl PipelineConfig {
61    /// Default config: all stages enabled, 1 substep, CPU backend.
62    pub fn new() -> Self {
63        Self {
64            enabled_stages: PipelineStage::all_in_order().to_vec(),
65            substeps: 1,
66            use_gpu: false,
67        }
68    }
69    /// Returns `true` if `stage` is in the enabled list.
70    pub fn is_enabled(&self, stage: PipelineStage) -> bool {
71        self.enabled_stages.contains(&stage)
72    }
73}
74/// Orchestrates a complete simulation step through all enabled pipeline stages.
75///
76/// The stage execution order is guaranteed:
77/// `BroadPhase → NarrowPhase → ConstraintSolve → Integration → PostProcess`
78pub struct PhysicsPipeline {
79    /// Pipeline configuration.
80    pub config: PipelineConfig,
81    /// Cumulative stats across all steps since creation.
82    pub stats: PipelineStats,
83}
84impl PhysicsPipeline {
85    /// Create a new pipeline with the given configuration.
86    pub fn new(config: PipelineConfig) -> Self {
87        Self {
88            config,
89            stats: PipelineStats::default(),
90        }
91    }
92    /// Run one full simulation step (all enabled stages, for `substeps`
93    /// iterations) over `world_state` with time step `dt`.
94    ///
95    /// Returns the [`PipelineStats`] for this step.
96    pub fn step(&mut self, world_state: &mut WorldState, dt: f64) -> PipelineStats {
97        let step_start = Instant::now();
98        let mut step_stats = PipelineStats::default();
99        let sub_dt = if self.config.substeps > 0 {
100            dt / self.config.substeps as f64
101        } else {
102            dt
103        };
104        for _ in 0..self.config.substeps.max(1) {
105            let sub_stats = self.run_stages(world_state, sub_dt);
106            step_stats.accumulate(&sub_stats);
107        }
108        step_stats.total_time_ms = step_start.elapsed().as_secs_f64() * 1000.0;
109        self.stats.accumulate(&step_stats);
110        step_stats
111    }
112    /// Run all enabled stages once for a single sub-step.
113    fn run_stages(&self, world_state: &mut WorldState, dt: f64) -> PipelineStats {
114        let mut stats = PipelineStats::default();
115        for stage in PipelineStage::all_in_order() {
116            if !self.config.is_enabled(stage) {
117                continue;
118            }
119            let mut timer = StageTimer::start();
120            match stage {
121                PipelineStage::BroadPhase => {
122                    let pairs = run_broadphase(world_state);
123                    stats.collision_pairs += pairs as u32;
124                }
125                PipelineStage::NarrowPhase => {}
126                PipelineStage::ConstraintSolve => {
127                    let solved = run_constraint_solve(world_state);
128                    stats.solved_constraints += solved as u32;
129                }
130                PipelineStage::Integration => {
131                    run_integration(world_state, dt);
132                }
133                PipelineStage::PostProcess => {
134                    run_postprocess(world_state);
135                }
136            }
137            timer.stop();
138            match stage {
139                PipelineStage::BroadPhase => stats.broadphase_ms += timer.elapsed_ms,
140                PipelineStage::NarrowPhase => stats.narrowphase_ms += timer.elapsed_ms,
141                PipelineStage::ConstraintSolve => stats.constraint_ms += timer.elapsed_ms,
142                PipelineStage::Integration => stats.integration_ms += timer.elapsed_ms,
143                PipelineStage::PostProcess => stats.postprocess_ms += timer.elapsed_ms,
144            }
145        }
146        stats
147    }
148}
149/// GPU compute pipeline descriptor (wgpu-agnostic, usable in unit tests).
150#[derive(Debug, Clone)]
151pub struct ComputePipeline {
152    /// Human-readable label for debugging.
153    pub label: String,
154    /// Raw WGSL shader source.
155    pub shader_source: String,
156    /// Name of the entry-point function inside the shader.
157    pub entry_point: String,
158    /// Workgroup size declared in the shader `[X, Y, Z]`.
159    pub workgroup_size: [u32; 3],
160}
161impl ComputePipeline {
162    /// Create a new pipeline descriptor.
163    ///
164    /// `workgroup_size` defaults to `[64, 1, 1]` (matching the shaders in
165    /// [`crate::shaders`]).
166    pub fn new(label: &str, shader: &str, entry_point: &str) -> Self {
167        Self {
168            label: label.to_owned(),
169            shader_source: shader.to_owned(),
170            entry_point: entry_point.to_owned(),
171            workgroup_size: [64, 1, 1],
172        }
173    }
174    /// Compute the dispatch grid dimensions needed to cover `n_items` work
175    /// items.
176    ///
177    /// Returns `[ceil(n_items / workgroup_size[0\]), 1, 1]`.
178    pub fn workgroups_needed(&self, n_items: u32) -> [u32; 3] {
179        let x = n_items.div_ceil(self.workgroup_size[0]);
180        [x, 1, 1]
181    }
182}
183/// Describes a memory/execution barrier between two pipeline stages.
184///
185/// On a real GPU this would translate to a `vkCmdPipelineBarrier` or
186/// `wgpu::CommandEncoder::insert_debug_marker`.  Here it is a pure data
187/// type used to validate that the user has not forgotten to insert barriers
188/// between dependent passes.
189#[derive(Debug, Clone, PartialEq)]
190pub struct ResourceBarrier {
191    /// The stage that *writes* the resource.
192    pub src_stage: PipelineStage,
193    /// The stage that *reads* the resource.
194    pub dst_stage: PipelineStage,
195    /// Human-readable name of the protected resource.
196    pub resource_name: String,
197}
198impl ResourceBarrier {
199    /// Create a new barrier descriptor.
200    pub fn new(src: PipelineStage, dst: PipelineStage, name: &str) -> Self {
201        Self {
202            src_stage: src,
203            dst_stage: dst,
204            resource_name: name.to_owned(),
205        }
206    }
207    /// Returns `true` if the barrier is between stages in the correct
208    /// dependency order (src must come before dst).
209    pub fn is_valid_order(&self) -> bool {
210        self.src_stage < self.dst_stage
211    }
212}
213/// An opaque handle to a pool-allocated GPU resource.
214///
215/// Stores the `(offset, size)` pair returned by [`GpuMemoryPool::alloc`] so
216/// that the caller can later free the resource.
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub struct ResourceHandle {
219    /// Offset into the pool's backing store.
220    pub offset: usize,
221    /// Size of the allocation in `f32` elements.
222    pub size: usize,
223}
224impl ResourceHandle {
225    /// Create a handle from a raw `(offset, size)` pair.
226    pub fn from_alloc(alloc: (usize, usize)) -> Self {
227        Self {
228            offset: alloc.0,
229            size: alloc.1,
230        }
231    }
232}
233/// A simple frame-graph pass descriptor.
234///
235/// Frame graphs describe the rendering / compute workload as a DAG of passes,
236/// enabling automatic barrier insertion and resource aliasing.
237#[derive(Debug, Clone)]
238pub struct FrameGraphPass {
239    /// Unique name for this pass.
240    pub name: String,
241    /// Names of resources this pass reads.
242    pub reads: Vec<String>,
243    /// Names of resources this pass writes.
244    pub writes: Vec<String>,
245    /// Names of passes that must complete before this one.
246    pub dependencies: Vec<String>,
247    /// Which queue this pass runs on.
248    pub queue: QueueType,
249}
250impl FrameGraphPass {
251    /// Create a new pass with no dependencies.
252    pub fn new(name: impl Into<String>, queue: QueueType) -> Self {
253        Self {
254            name: name.into(),
255            reads: Vec::new(),
256            writes: Vec::new(),
257            dependencies: Vec::new(),
258            queue,
259        }
260    }
261    /// Add a resource read.
262    pub fn reads(mut self, resource: impl Into<String>) -> Self {
263        self.reads.push(resource.into());
264        self
265    }
266    /// Add a resource write.
267    pub fn writes(mut self, resource: impl Into<String>) -> Self {
268        self.writes.push(resource.into());
269        self
270    }
271    /// Add a pass dependency.
272    pub fn depends_on(mut self, pass: impl Into<String>) -> Self {
273        self.dependencies.push(pass.into());
274        self
275    }
276}
277/// A minimal frame graph that can validate pass dependencies.
278#[derive(Debug, Default)]
279pub struct FrameGraph {
280    pub(super) passes: Vec<FrameGraphPass>,
281}
282impl FrameGraph {
283    /// Create an empty frame graph.
284    pub fn new() -> Self {
285        Self::default()
286    }
287    /// Add a pass.
288    pub fn add_pass(&mut self, pass: FrameGraphPass) {
289        self.passes.push(pass);
290    }
291    /// Return all pass names in insertion order.
292    pub fn pass_names(&self) -> Vec<&str> {
293        self.passes.iter().map(|p| p.name.as_str()).collect()
294    }
295    /// Return all passes that write to `resource`.
296    pub fn writers_of(&self, resource: &str) -> Vec<&FrameGraphPass> {
297        self.passes
298            .iter()
299            .filter(|p| p.writes.iter().any(|w| w == resource))
300            .collect()
301    }
302    /// Return all passes that read from `resource`.
303    pub fn readers_of(&self, resource: &str) -> Vec<&FrameGraphPass> {
304        self.passes
305            .iter()
306            .filter(|p| p.reads.iter().any(|r| r == resource))
307            .collect()
308    }
309    /// Validate that all declared dependencies reference existing passes.
310    /// Returns a list of invalid dependency references.
311    pub fn validate_dependencies(&self) -> Vec<String> {
312        let names: std::collections::HashSet<&str> =
313            self.passes.iter().map(|p| p.name.as_str()).collect();
314        let mut errors = Vec::new();
315        for pass in &self.passes {
316            for dep in &pass.dependencies {
317                if !names.contains(dep.as_str()) {
318                    errors.push(format!("{}: unknown dependency '{}'", pass.name, dep));
319                }
320            }
321        }
322        errors
323    }
324    /// Count how many passes run on the async compute queue.
325    pub fn async_pass_count(&self) -> usize {
326        self.passes
327            .iter()
328            .filter(|p| p.queue == QueueType::AsyncCompute)
329            .count()
330    }
331}
332/// A distinct stage in the physics simulation pipeline.
333///
334/// Stages run in dependency order:
335/// `BroadPhase → NarrowPhase → ConstraintSolve → Integration → PostProcess`
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
337pub enum PipelineStage {
338    /// Broad-phase collision detection (AABB / BVH).
339    BroadPhase,
340    /// Narrow-phase contact generation.
341    NarrowPhase,
342    /// Constraint / impulse solver.
343    ConstraintSolve,
344    /// Velocity and position integration.
345    Integration,
346    /// Post-processing (sleeping, user callbacks, …).
347    PostProcess,
348}
349impl PipelineStage {
350    /// Ordered list of all stages, from first to last.
351    pub fn all_in_order() -> [PipelineStage; 5] {
352        [
353            PipelineStage::BroadPhase,
354            PipelineStage::NarrowPhase,
355            PipelineStage::ConstraintSolve,
356            PipelineStage::Integration,
357            PipelineStage::PostProcess,
358        ]
359    }
360}
361/// A single compute dispatch pass: one pipeline plus its bound buffers.
362#[derive(Debug)]
363pub struct DispatchBatch {
364    /// The pipeline to dispatch.
365    pub pipeline: ComputePipeline,
366    /// Buffers bound to this pass, in binding-slot order.
367    pub bindings: Vec<CpuBuffer>,
368    /// The dispatch grid dimensions `[X, Y, Z]`.
369    pub dispatch_dims: [u32; 3],
370}
371impl DispatchBatch {
372    /// Create a new dispatch batch, computing the required workgroup grid from
373    /// `workitems`.
374    pub fn new(pipeline: ComputePipeline, workitems: u32) -> Self {
375        let dispatch_dims = pipeline.workgroups_needed(workitems);
376        Self {
377            pipeline,
378            bindings: Vec::new(),
379            dispatch_dims,
380        }
381    }
382    /// Append `buffer` to the binding list (next available slot).
383    pub fn bind(&mut self, buffer: CpuBuffer) {
384        self.bindings.push(buffer);
385    }
386}
387/// Models an async compute queue: a FIFO of [`DispatchBatch`]es that can be
388/// submitted independently of the graphics queue.
389///
390/// On a GPU this enables overlapping async compute with graphics work.
391/// Here it is a simple in-memory queue used to test scheduling logic.
392pub struct AsyncComputeQueue {
393    pub(super) queue: std::collections::VecDeque<DispatchBatch>,
394    /// Total number of batches ever enqueued (for profiling).
395    pub total_enqueued: usize,
396    /// Total number of batches ever drained (executed).
397    pub total_executed: usize,
398}
399impl AsyncComputeQueue {
400    /// Create an empty async compute queue.
401    pub fn new() -> Self {
402        Self {
403            queue: std::collections::VecDeque::new(),
404            total_enqueued: 0,
405            total_executed: 0,
406        }
407    }
408    /// Enqueue a dispatch batch.
409    pub fn submit(&mut self, batch: DispatchBatch) {
410        self.total_enqueued += 1;
411        self.queue.push_back(batch);
412    }
413    /// Execute (drain) all pending batches and return the count executed.
414    ///
415    /// In a real backend this would flush the command buffer to the GPU.
416    /// Here it simply clears the queue and updates counters.
417    pub fn flush(&mut self) -> usize {
418        let n = self.queue.len();
419        self.total_executed += n;
420        self.queue.clear();
421        n
422    }
423    /// Number of batches currently waiting in the queue.
424    pub fn pending(&self) -> usize {
425        self.queue.len()
426    }
427    /// Returns `true` when there are no pending batches.
428    pub fn is_idle(&self) -> bool {
429        self.queue.is_empty()
430    }
431}
432/// A dispatched batch annotated with its target queue.
433#[derive(Debug)]
434pub struct MultiQueueBatch {
435    /// The dispatch batch.
436    pub batch: DispatchBatch,
437    /// Which queue to submit to.
438    pub queue: QueueType,
439    /// Semaphore/fence dependency: must wait for this frame's signal.
440    pub wait_frame: u64,
441}
442/// Minimal world state passed to [`PhysicsPipeline::step`].
443///
444/// Uses pure f64 arrays (no nalgebra) compatible with GPU buffer uploads.
445#[derive(Debug, Clone, Default)]
446pub struct WorldState {
447    /// Flat position array `[x0, y0, z0, x1, y1, z1, ...]`.
448    pub positions: Vec<f64>,
449    /// Flat velocity array `[vx0, vy0, vz0, ...]`.
450    pub velocities: Vec<f64>,
451    /// Inverse masses `[inv_m0, inv_m1, ...]`.
452    pub inverse_masses: Vec<f64>,
453}
454impl WorldState {
455    /// Number of bodies in the world state.
456    pub fn body_count(&self) -> usize {
457        self.inverse_masses.len()
458    }
459}
460/// Records per-stage timing samples and provides basic statistics.
461///
462/// Each call to [`PipelineProfiler::record`] appends one sample for the
463/// named stage.  [`PipelineProfiler::summary`] returns mean ± stddev.
464#[derive(Debug, Default)]
465pub struct PipelineProfiler {
466    pub(super) samples: std::collections::HashMap<String, Vec<f64>>,
467}
468impl PipelineProfiler {
469    /// Create a new, empty profiler.
470    pub fn new() -> Self {
471        Self::default()
472    }
473    /// Append a timing sample (milliseconds) for `stage_name`.
474    pub fn record(&mut self, stage_name: &str, ms: f64) {
475        self.samples
476            .entry(stage_name.to_owned())
477            .or_default()
478            .push(ms);
479    }
480    /// Returns `(mean_ms, stddev_ms, sample_count)` for `stage_name`, or
481    /// `None` if no samples have been recorded.
482    pub fn summary(&self, stage_name: &str) -> Option<(f64, f64, usize)> {
483        let v = self.samples.get(stage_name)?;
484        if v.is_empty() {
485            return None;
486        }
487        let n = v.len() as f64;
488        let mean = v.iter().sum::<f64>() / n;
489        let variance = v.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n;
490        Some((mean, variance.sqrt(), v.len()))
491    }
492    /// Return all stage names that have recorded samples.
493    pub fn stage_names(&self) -> Vec<&str> {
494        let mut names: Vec<&str> = self.samples.keys().map(String::as_str).collect();
495        names.sort_unstable();
496        names
497    }
498    /// Total number of samples across all stages.
499    pub fn total_samples(&self) -> usize {
500        self.samples.values().map(Vec::len).sum()
501    }
502    /// Clear all recorded samples.
503    pub fn reset(&mut self) {
504        self.samples.clear();
505    }
506}
507/// Models multiple compute queues (graphics queue + async compute queue).
508///
509/// On Vulkan/Metal/DX12 certain hardware can overlap work on the graphics
510/// queue and an async compute queue.  This mock records which queue each
511/// dispatch was submitted to.
512#[derive(Debug, Clone, PartialEq, Eq)]
513pub enum QueueType {
514    /// Main graphics/compute queue.
515    Main,
516    /// Async compute queue (can overlap with graphics).
517    AsyncCompute,
518    /// Transfer/copy queue.
519    Transfer,
520}
521/// Wraps an [`Instant`] and records the elapsed duration of a stage.
522pub struct StageTimer {
523    pub(super) start: Instant,
524    /// Elapsed time recorded by [`StageTimer::stop`].
525    pub elapsed_ms: f64,
526}
527impl StageTimer {
528    /// Create a new timer and start the clock.
529    pub fn start() -> Self {
530        Self {
531            start: Instant::now(),
532            elapsed_ms: 0.0,
533        }
534    }
535    /// Stop the timer and record elapsed time in milliseconds.
536    pub fn stop(&mut self) {
537        self.elapsed_ms = self.start.elapsed().as_secs_f64() * 1000.0;
538    }
539}
540/// Low-level GPU pipeline statistics for one dispatch (mock).
541///
542/// Mirrors the statistics provided by `VkQueryPool` with
543/// `VK_QUERY_TYPE_PIPELINE_STATISTICS` or Metal/D3D equivalents.
544#[derive(Debug, Clone, Default)]
545pub struct PipelineStatistics {
546    /// Number of invocations of the compute shader.
547    pub cs_invocations: u64,
548    /// Number of work-groups dispatched.
549    pub workgroups_dispatched: u64,
550    /// Estimated FLOP count (mock value).
551    pub flops: u64,
552    /// Estimated bytes read from global memory.
553    pub bytes_read: u64,
554    /// Estimated bytes written to global memory.
555    pub bytes_written: u64,
556}
557impl PipelineStatistics {
558    /// Compute the arithmetic intensity (FLOPs per byte).
559    pub fn arithmetic_intensity(&self) -> f64 {
560        let bytes = self.bytes_read + self.bytes_written;
561        if bytes == 0 {
562            return 0.0;
563        }
564        self.flops as f64 / bytes as f64
565    }
566    /// Estimated memory bandwidth utilization as a fraction of `peak_bw_bytes_s`.
567    pub fn bandwidth_utilization(&self, peak_bw_bytes_s: f64, elapsed_s: f64) -> f64 {
568        if peak_bw_bytes_s <= 0.0 || elapsed_s <= 0.0 {
569            return 0.0;
570        }
571        let used = (self.bytes_read + self.bytes_written) as f64 / elapsed_s;
572        (used / peak_bw_bytes_s).min(1.0)
573    }
574}
575/// Multi-queue command recorder: tracks which batches go to which queue.
576#[derive(Debug, Default)]
577pub struct MultiQueueRecorder {
578    /// Pending batches on the main queue.
579    pub main_queue: Vec<DispatchBatch>,
580    /// Pending batches on the async compute queue.
581    pub async_queue: Vec<DispatchBatch>,
582    /// Pending batches on the transfer queue.
583    pub transfer_queue: Vec<DispatchBatch>,
584    /// Total batches ever recorded.
585    pub total_recorded: usize,
586}
587impl MultiQueueRecorder {
588    /// Create an empty recorder.
589    pub fn new() -> Self {
590        Self::default()
591    }
592    /// Submit a batch to the specified queue.
593    pub fn submit(&mut self, batch: DispatchBatch, queue: QueueType) {
594        self.total_recorded += 1;
595        match queue {
596            QueueType::Main => self.main_queue.push(batch),
597            QueueType::AsyncCompute => self.async_queue.push(batch),
598            QueueType::Transfer => self.transfer_queue.push(batch),
599        }
600    }
601    /// Flush all queues and return total batches executed.
602    pub fn flush_all(&mut self) -> usize {
603        let n = self.main_queue.len() + self.async_queue.len() + self.transfer_queue.len();
604        self.main_queue.clear();
605        self.async_queue.clear();
606        self.transfer_queue.clear();
607        n
608    }
609    /// Number of pending batches across all queues.
610    pub fn pending_total(&self) -> usize {
611        self.main_queue.len() + self.async_queue.len() + self.transfer_queue.len()
612    }
613}
614/// A simple slab-style GPU memory pool that sub-allocates [`CpuBuffer`]s from
615/// a fixed-capacity backing store.
616///
617/// The pool pre-allocates a large buffer and hands out non-overlapping slices
618/// (as [`CpuBuffer`] views backed by offsets).  On a real GPU this avoids per-
619/// allocation overhead from `vkAllocateMemory`.
620///
621/// This CPU-side mock tracks allocations as `(offset, size)` pairs and
622/// simulates fragmentation/free-list behaviour.
623#[derive(Debug)]
624pub struct GpuMemoryPool {
625    /// Total capacity of the pool in `f32` elements.
626    pub capacity: usize,
627    /// Currently allocated `f32` elements.
628    pub allocated: usize,
629    /// Free-list entries `(offset_in_elements, size_in_elements)`.
630    pub(super) free_list: Vec<(usize, usize)>,
631}
632impl GpuMemoryPool {
633    /// Create a pool with the given capacity in `f32` elements.
634    pub fn new(capacity: usize) -> Self {
635        Self {
636            capacity,
637            allocated: 0,
638            free_list: vec![(0, capacity)],
639        }
640    }
641    /// Attempt to allocate `size` `f32` elements from the pool.
642    ///
643    /// Returns `Some((offset, size))` on success, `None` if the pool is full.
644    /// Uses a first-fit strategy.
645    pub fn alloc(&mut self, size: usize) -> Option<(usize, usize)> {
646        for i in 0..self.free_list.len() {
647            let (off, avail) = self.free_list[i];
648            if avail >= size {
649                let alloc_off = off;
650                if avail == size {
651                    self.free_list.remove(i);
652                } else {
653                    self.free_list[i] = (off + size, avail - size);
654                }
655                self.allocated += size;
656                return Some((alloc_off, size));
657            }
658        }
659        None
660    }
661    /// Free a previously allocated block `(offset, size)`.
662    ///
663    /// Returns `Err` if the block was never allocated (invalid free).
664    pub fn free(&mut self, offset: usize, size: usize) -> Result<(), &'static str> {
665        if offset + size > self.capacity {
666            return Err("block out of bounds");
667        }
668        if self.allocated < size {
669            return Err("double-free: allocated count underflow");
670        }
671        self.allocated -= size;
672        self.free_list.push((offset, size));
673        self.free_list.sort_by_key(|&(off, _)| off);
674        let mut merged: Vec<(usize, usize)> = Vec::new();
675        for &(off, sz) in &self.free_list {
676            if let Some(last) = merged.last_mut()
677                && last.0 + last.1 == off
678            {
679                last.1 += sz;
680                continue;
681            }
682            merged.push((off, sz));
683        }
684        self.free_list = merged;
685        Ok(())
686    }
687    /// Remaining free `f32` elements.
688    pub fn free_space(&self) -> usize {
689        self.capacity - self.allocated
690    }
691    /// Returns `true` if the pool has no outstanding allocations.
692    pub fn is_fully_free(&self) -> bool {
693        self.allocated == 0
694    }
695    /// Number of fragmented free-list entries (1 = perfectly contiguous).
696    pub fn fragmentation_count(&self) -> usize {
697        self.free_list.len()
698    }
699    /// Allocate a named [`CpuBuffer`] from the pool, returning the buffer
700    /// and the pool allocation handle `(offset, size)`.
701    pub fn alloc_buffer(
702        &mut self,
703        label: &str,
704        n: usize,
705        usage: BufferUsage,
706    ) -> Option<(CpuBuffer, (usize, usize))> {
707        let handle = self.alloc(n)?;
708        let buf = CpuBuffer::new_zeros(label, n, usage);
709        Some((buf, handle))
710    }
711}
712/// A set of [`TimestampQuery`]s collected during one frame / step.
713#[derive(Debug, Clone, Default)]
714pub struct TimestampQuerySet {
715    pub(super) queries: Vec<TimestampQuery>,
716}
717impl TimestampQuerySet {
718    /// Create an empty query set.
719    pub fn new() -> Self {
720        Self::default()
721    }
722    /// Record a timestamp query.
723    pub fn record(&mut self, query: TimestampQuery) {
724        self.queries.push(query);
725    }
726    /// Return all queries.
727    pub fn queries(&self) -> &[TimestampQuery] {
728        &self.queries
729    }
730    /// Find the query with the longest elapsed time.
731    pub fn slowest_pass(&self) -> Option<&TimestampQuery> {
732        self.queries.iter().max_by(|a, b| {
733            a.elapsed_ms()
734                .partial_cmp(&b.elapsed_ms())
735                .unwrap_or(std::cmp::Ordering::Equal)
736        })
737    }
738    /// Sum of all elapsed times.
739    pub fn total_elapsed_ms(&self) -> f64 {
740        self.queries.iter().map(|q| q.elapsed_ms()).sum()
741    }
742    /// Clear all recorded queries.
743    pub fn clear(&mut self) {
744        self.queries.clear();
745    }
746}
747/// Per-step statistics produced by [`PhysicsPipeline::step`].
748#[derive(Debug, Clone, Default)]
749pub struct PipelineStats {
750    /// Time spent in the broad-phase stage (ms).
751    pub broadphase_ms: f64,
752    /// Time spent in the narrow-phase stage (ms).
753    pub narrowphase_ms: f64,
754    /// Time spent in the constraint-solve stage (ms).
755    pub constraint_ms: f64,
756    /// Time spent in the integration stage (ms).
757    pub integration_ms: f64,
758    /// Time spent in the post-process stage (ms).
759    pub postprocess_ms: f64,
760    /// Total wall-clock time for the step (ms).
761    pub total_time_ms: f64,
762    /// Number of collision pairs found in broad-phase.
763    pub collision_pairs: u32,
764    /// Number of constraints solved.
765    pub solved_constraints: u32,
766}
767impl PipelineStats {
768    /// Accumulate another stats snapshot into `self` (used for sub-step sums).
769    pub fn accumulate(&mut self, other: &PipelineStats) {
770        self.broadphase_ms += other.broadphase_ms;
771        self.narrowphase_ms += other.narrowphase_ms;
772        self.constraint_ms += other.constraint_ms;
773        self.integration_ms += other.integration_ms;
774        self.postprocess_ms += other.postprocess_ms;
775        self.total_time_ms += other.total_time_ms;
776        self.collision_pairs += other.collision_pairs;
777        self.solved_constraints += other.solved_constraints;
778    }
779    /// Sum of all per-stage times.
780    pub fn stage_total_ms(&self) -> f64 {
781        self.broadphase_ms
782            + self.narrowphase_ms
783            + self.constraint_ms
784            + self.integration_ms
785            + self.postprocess_ms
786    }
787}
788/// Optimises a sequence of resource barriers by removing redundant ones.
789///
790/// Two barriers are redundant when a later barrier subsumes an earlier one
791/// (same `src_stage → dst_stage` pair for the same resource).
792#[derive(Debug, Default)]
793pub struct BarrierOptimizer;
794impl BarrierOptimizer {
795    /// Deduplicate `barriers`: for each `(src, dst, resource)` triple,
796    /// keep only the last occurrence.  Returns the optimised set.
797    pub fn optimize(barriers: &[ResourceBarrier]) -> BarrierSet {
798        let mut seen: std::collections::HashMap<
799            (PipelineStage, PipelineStage, &str),
800            &ResourceBarrier,
801        > = std::collections::HashMap::new();
802        for b in barriers {
803            seen.insert((b.src_stage, b.dst_stage, b.resource_name.as_str()), b);
804        }
805        let mut out = BarrierSet::new();
806        for b in seen.values() {
807            out.add(ResourceBarrier::new(
808                b.src_stage,
809                b.dst_stage,
810                &b.resource_name,
811            ));
812        }
813        out
814    }
815    /// Count how many barriers would be removed from `before` to produce
816    /// the optimised set.
817    pub fn savings(barriers: &[ResourceBarrier]) -> usize {
818        let optimized = Self::optimize(barriers);
819        barriers.len().saturating_sub(optimized.len())
820    }
821}
822/// Fluent builder for [`PhysicsPipeline`].
823///
824/// ```no_run
825/// use oxiphysics_gpu::pipeline::{PipelineBuilder, PipelineStage};
826///
827/// let pipeline = PipelineBuilder::new()
828///     .substeps(2)
829///     .use_gpu(false)
830///     .disable_stage(PipelineStage::PostProcess)
831///     .build();
832///
833/// assert_eq!(pipeline.config.substeps, 2);
834/// assert!(!pipeline.config.is_enabled(PipelineStage::PostProcess));
835/// ```
836pub struct PipelineBuilder {
837    pub(super) config: PipelineConfig,
838}
839impl PipelineBuilder {
840    /// Create a builder with a default config (all stages, 1 substep, CPU).
841    pub fn new() -> Self {
842        Self {
843            config: PipelineConfig::new(),
844        }
845    }
846    /// Set the number of sub-steps.
847    pub fn substeps(mut self, n: u32) -> Self {
848        self.config.substeps = n;
849        self
850    }
851    /// Set whether to prefer a GPU backend.
852    pub fn use_gpu(mut self, gpu: bool) -> Self {
853        self.config.use_gpu = gpu;
854        self
855    }
856    /// Add a stage to the enabled list (idempotent).
857    pub fn enable_stage(mut self, stage: PipelineStage) -> Self {
858        if !self.config.enabled_stages.contains(&stage) {
859            self.config.enabled_stages.push(stage);
860            self.config.enabled_stages.sort();
861        }
862        self
863    }
864    /// Remove a stage from the enabled list.
865    pub fn disable_stage(mut self, stage: PipelineStage) -> Self {
866        self.config.enabled_stages.retain(|&s| s != stage);
867        self
868    }
869    /// Consume the builder and produce a [`PhysicsPipeline`].
870    pub fn build(self) -> PhysicsPipeline {
871        PhysicsPipeline::new(self.config)
872    }
873}
874/// A collection of [`ResourceBarrier`]s that form a complete barrier schedule
875/// for one pipeline configuration.
876#[derive(Debug, Clone, Default)]
877pub struct BarrierSet {
878    pub(super) barriers: Vec<ResourceBarrier>,
879}
880impl BarrierSet {
881    /// Create an empty barrier set.
882    pub fn new() -> Self {
883        Self::default()
884    }
885    /// Append a barrier.
886    pub fn add(&mut self, barrier: ResourceBarrier) {
887        self.barriers.push(barrier);
888    }
889    /// Return all barriers whose `src_stage` matches `stage`.
890    pub fn barriers_from(&self, stage: PipelineStage) -> Vec<&ResourceBarrier> {
891        self.barriers
892            .iter()
893            .filter(|b| b.src_stage == stage)
894            .collect()
895    }
896    /// Return all barriers whose `dst_stage` matches `stage`.
897    pub fn barriers_to(&self, stage: PipelineStage) -> Vec<&ResourceBarrier> {
898        self.barriers
899            .iter()
900            .filter(|b| b.dst_stage == stage)
901            .collect()
902    }
903    /// Number of barriers registered.
904    pub fn len(&self) -> usize {
905        self.barriers.len()
906    }
907    /// Returns `true` if no barriers are registered.
908    pub fn is_empty(&self) -> bool {
909        self.barriers.is_empty()
910    }
911    /// Validate that every barrier has `src < dst` (i.e., no backwards
912    /// dependencies).  Returns the list of invalid barriers.
913    pub fn validate(&self) -> Vec<&ResourceBarrier> {
914        self.barriers
915            .iter()
916            .filter(|b| !b.is_valid_order())
917            .collect()
918    }
919}
920/// A timestamp query pair `(begin, end)` for a named pass.
921///
922/// On a real GPU this maps to `VkQueryPool` or `wgpu::QuerySet` writes at the
923/// start and end of a render/compute pass.  Here we store wall-clock
924/// `f64` timestamps in milliseconds.
925#[derive(Debug, Clone)]
926pub struct TimestampQuery {
927    /// Human-readable label for this pass.
928    pub label: String,
929    /// Begin timestamp in milliseconds (relative to some epoch).
930    pub begin_ms: f64,
931    /// End timestamp in milliseconds.
932    pub end_ms: f64,
933}
934impl TimestampQuery {
935    /// Create a new timestamp query pair.
936    pub fn new(label: impl Into<String>, begin_ms: f64, end_ms: f64) -> Self {
937        Self {
938            label: label.into(),
939            begin_ms,
940            end_ms,
941        }
942    }
943    /// Elapsed duration in milliseconds.
944    pub fn elapsed_ms(&self) -> f64 {
945        self.end_ms - self.begin_ms
946    }
947}
948/// Tracks resource aliasing: two logical resources that share the same physical
949/// memory (pool allocation) at non-overlapping lifetimes.
950///
951/// This is important for reducing peak GPU memory usage.
952#[derive(Debug, Default)]
953pub struct ResourceAliasingTracker {
954    /// Maps physical allocation handle `(offset, size)` to a list of logical
955    /// resource names currently using it.
956    pub(super) aliases: std::collections::HashMap<(usize, usize), Vec<String>>,
957}
958impl ResourceAliasingTracker {
959    /// Create an empty tracker.
960    pub fn new() -> Self {
961        Self::default()
962    }
963    /// Record that `resource_name` uses physical allocation `(offset, size)`.
964    pub fn track(&mut self, resource_name: impl Into<String>, offset: usize, size: usize) {
965        self.aliases
966            .entry((offset, size))
967            .or_default()
968            .push(resource_name.into());
969    }
970    /// Return all resource names sharing the physical allocation `(offset, size)`.
971    pub fn aliases_for(&self, offset: usize, size: usize) -> &[String] {
972        self.aliases
973            .get(&(offset, size))
974            .map(Vec::as_slice)
975            .unwrap_or(&[])
976    }
977    /// Return `true` if two named resources share any physical allocation.
978    pub fn are_aliased(&self, a: &str, b: &str) -> bool {
979        for names in self.aliases.values() {
980            if names.contains(&a.to_string()) && names.contains(&b.to_string()) {
981                return true;
982            }
983        }
984        false
985    }
986    /// Total number of physical allocations tracked.
987    pub fn allocation_count(&self) -> usize {
988        self.aliases.len()
989    }
990    /// Total number of logical resource registrations across all allocations.
991    pub fn total_resource_registrations(&self) -> usize {
992        self.aliases.values().map(Vec::len).sum()
993    }
994}
995/// Simulated GPU buffer backed by CPU memory for unit testing without a GPU.
996#[derive(Debug, Clone)]
997pub struct CpuBuffer {
998    /// Human-readable label for debugging.
999    pub label: String,
1000    /// Buffer contents as 32-bit floats.
1001    pub data: Vec<f32>,
1002    /// Intended usage of this buffer.
1003    pub usage: BufferUsage,
1004}
1005impl CpuBuffer {
1006    /// Create a buffer pre-filled with the given data.
1007    pub fn new_f32(label: &str, data: Vec<f32>, usage: BufferUsage) -> Self {
1008        Self {
1009            label: label.to_owned(),
1010            data,
1011            usage,
1012        }
1013    }
1014    /// Create a buffer of `n` zeros.
1015    pub fn new_zeros(label: &str, n: usize, usage: BufferUsage) -> Self {
1016        Self {
1017            label: label.to_owned(),
1018            data: vec![0.0_f32; n],
1019            usage,
1020        }
1021    }
1022    /// Number of `f32` elements in the buffer.
1023    pub fn len(&self) -> usize {
1024        self.data.len()
1025    }
1026    /// Returns `true` if the buffer contains no elements.
1027    pub fn is_empty(&self) -> bool {
1028        self.data.is_empty()
1029    }
1030}
1031/// Describes how a [`CpuBuffer`] will be used by the pipeline.
1032#[derive(Debug, Clone, Copy, PartialEq)]
1033pub enum BufferUsage {
1034    /// Read-write storage buffer.
1035    Storage,
1036    /// Uniform (read-only, small) buffer.
1037    Uniform,
1038    /// Read-only storage buffer.
1039    StorageReadOnly,
1040}