Skip to main content

oxiphysics_gpu/pipeline/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::types::WorldState;
6
7/// Mock broad-phase: returns number of potential collision pairs.
8pub(super) fn run_broadphase(world: &WorldState) -> usize {
9    let n = world.body_count();
10    n.saturating_sub(1) * n / 2
11}
12/// Mock constraint solve: returns number of solved constraints (= collision pairs).
13pub(super) fn run_constraint_solve(world: &WorldState) -> usize {
14    let n = world.body_count();
15    n.saturating_sub(1) * n / 2
16}
17/// Mock integration: semi-implicit Euler with gravity `[0, -9.81, 0]`.
18pub(super) fn run_integration(world: &mut WorldState, dt: f64) {
19    let n = world.body_count();
20    for i in 0..n {
21        let inv_m = world.inverse_masses[i];
22        if inv_m == 0.0 {
23            continue;
24        }
25        if world.velocities.len() > i * 3 + 1 {
26            world.velocities[i * 3 + 1] -= 9.81 * dt;
27        }
28        if world.positions.len() >= (i + 1) * 3 && world.velocities.len() >= (i + 1) * 3 {
29            world.positions[i * 3] += world.velocities[i * 3] * dt;
30            world.positions[i * 3 + 1] += world.velocities[i * 3 + 1] * dt;
31            world.positions[i * 3 + 2] += world.velocities[i * 3 + 2] * dt;
32        }
33    }
34}
35/// Mock post-process: no-op for an empty world; just validates invariants.
36pub(super) fn run_postprocess(_world: &mut WorldState) {}
37#[cfg(test)]
38mod tests {
39
40    use crate::pipeline::AsyncComputeQueue;
41
42    use crate::pipeline::BarrierSet;
43    use crate::pipeline::BufferUsage;
44
45    use crate::pipeline::ComputePipeline;
46    use crate::pipeline::CpuBuffer;
47    use crate::pipeline::DispatchBatch;
48
49    use crate::pipeline::GpuMemoryPool;
50
51    use crate::pipeline::PhysicsPipeline;
52    use crate::pipeline::PipelineBuilder;
53    use crate::pipeline::PipelineConfig;
54    use crate::pipeline::PipelineProfiler;
55    use crate::pipeline::PipelineStage;
56
57    use crate::pipeline::PipelineStats;
58
59    use crate::pipeline::ResourceBarrier;
60    use crate::pipeline::ResourceHandle;
61
62    use crate::pipeline::WorldState;
63    #[test]
64    fn test_pipeline_workgroups() {
65        let p = ComputePipeline::new("test", "", "main");
66        assert_eq!(p.workgroups_needed(200), [4, 1, 1]);
67        assert_eq!(p.workgroups_needed(128), [2, 1, 1]);
68        assert_eq!(p.workgroups_needed(1), [1, 1, 1]);
69    }
70    #[test]
71    fn test_cpu_buffer_zeros() {
72        let buf = CpuBuffer::new_zeros("zeros", 10, BufferUsage::Storage);
73        assert_eq!(buf.len(), 10);
74        assert!(buf.data.iter().all(|&v| v == 0.0));
75        assert!(!buf.is_empty());
76    }
77    #[test]
78    fn test_dispatch_batch_bind() {
79        let pipeline = ComputePipeline::new("batch", "", "main");
80        let mut batch = DispatchBatch::new(pipeline, 64);
81        batch.bind(CpuBuffer::new_zeros("a", 64, BufferUsage::Storage));
82        batch.bind(CpuBuffer::new_zeros("b", 64, BufferUsage::StorageReadOnly));
83        batch.bind(CpuBuffer::new_f32("c", vec![1.0; 4], BufferUsage::Uniform));
84        assert_eq!(batch.bindings.len(), 3);
85    }
86    #[test]
87    fn default_config_has_all_stages() {
88        let cfg = PipelineConfig::new();
89        for stage in PipelineStage::all_in_order() {
90            assert!(cfg.is_enabled(stage), "stage {stage:?} should be enabled");
91        }
92        assert_eq!(cfg.substeps, 1);
93        assert!(!cfg.use_gpu);
94    }
95    #[test]
96    fn config_disable_stage() {
97        let mut cfg = PipelineConfig::new();
98        cfg.enabled_stages
99            .retain(|&s| s != PipelineStage::PostProcess);
100        assert!(!cfg.is_enabled(PipelineStage::PostProcess));
101        assert!(cfg.is_enabled(PipelineStage::BroadPhase));
102    }
103    #[test]
104    fn builder_pattern_substeps_and_gpu_flag() {
105        let p = PipelineBuilder::new().substeps(4).use_gpu(true).build();
106        assert_eq!(p.config.substeps, 4);
107        assert!(p.config.use_gpu);
108    }
109    #[test]
110    fn builder_disable_stage() {
111        let p = PipelineBuilder::new()
112            .disable_stage(PipelineStage::NarrowPhase)
113            .build();
114        assert!(!p.config.is_enabled(PipelineStage::NarrowPhase));
115        assert!(p.config.is_enabled(PipelineStage::BroadPhase));
116    }
117    #[test]
118    fn builder_enable_stage_idempotent() {
119        let p = PipelineBuilder::new()
120            .enable_stage(PipelineStage::BroadPhase)
121            .enable_stage(PipelineStage::BroadPhase)
122            .build();
123        let count = p
124            .config
125            .enabled_stages
126            .iter()
127            .filter(|&&s| s == PipelineStage::BroadPhase)
128            .count();
129        assert_eq!(count, 1);
130    }
131    #[test]
132    fn stage_order_is_canonical() {
133        let order = PipelineStage::all_in_order();
134        for w in order.windows(2) {
135            assert!(
136                w[0] < w[1],
137                "stage order violated: {:?} should be < {:?}",
138                w[0],
139                w[1]
140            );
141        }
142    }
143    #[test]
144    fn empty_world_step_no_panic() {
145        let mut pipeline = PhysicsPipeline::new(PipelineConfig::new());
146        let mut world = WorldState::default();
147        let stats = pipeline.step(&mut world, 1.0 / 60.0);
148        assert_eq!(stats.collision_pairs, 0);
149        assert_eq!(stats.solved_constraints, 0);
150    }
151    #[test]
152    fn stats_accumulate_sums_fields() {
153        let mut a = PipelineStats {
154            broadphase_ms: 1.0,
155            narrowphase_ms: 2.0,
156            constraint_ms: 3.0,
157            integration_ms: 4.0,
158            postprocess_ms: 5.0,
159            total_time_ms: 6.0,
160            collision_pairs: 10,
161            solved_constraints: 5,
162        };
163        let b = PipelineStats {
164            broadphase_ms: 0.5,
165            narrowphase_ms: 0.5,
166            constraint_ms: 0.5,
167            integration_ms: 0.5,
168            postprocess_ms: 0.5,
169            total_time_ms: 1.0,
170            collision_pairs: 3,
171            solved_constraints: 2,
172        };
173        a.accumulate(&b);
174        assert!((a.broadphase_ms - 1.5).abs() < 1e-12);
175        assert_eq!(a.collision_pairs, 13);
176        assert_eq!(a.solved_constraints, 7);
177    }
178    #[test]
179    fn pipeline_cumulative_stats_grow() {
180        let mut pipeline = PhysicsPipeline::new(PipelineConfig::new());
181        let mut world = WorldState {
182            positions: vec![0.0; 6],
183            velocities: vec![0.0; 6],
184            inverse_masses: vec![1.0, 1.0],
185        };
186        pipeline.step(&mut world, 0.016);
187        pipeline.step(&mut world, 0.016);
188        assert!(pipeline.stats.total_time_ms >= 0.0);
189    }
190    #[test]
191    fn integration_stage_moves_body_downward() {
192        let cfg = PipelineBuilder::new()
193            .disable_stage(PipelineStage::BroadPhase)
194            .disable_stage(PipelineStage::NarrowPhase)
195            .disable_stage(PipelineStage::ConstraintSolve)
196            .disable_stage(PipelineStage::PostProcess)
197            .build()
198            .config;
199        let mut pipeline = PhysicsPipeline::new(cfg);
200        let mut world = WorldState {
201            positions: vec![0.0, 10.0, 0.0],
202            velocities: vec![0.0, 0.0, 0.0],
203            inverse_masses: vec![1.0],
204        };
205        pipeline.step(&mut world, 1.0);
206        assert!(
207            world.positions[1] < 10.0,
208            "y should decrease due to gravity"
209        );
210    }
211    #[test]
212    fn barrier_valid_order() {
213        let b = ResourceBarrier::new(
214            PipelineStage::BroadPhase,
215            PipelineStage::NarrowPhase,
216            "pair_buffer",
217        );
218        assert!(b.is_valid_order());
219    }
220    #[test]
221    fn barrier_invalid_reverse_order() {
222        let b = ResourceBarrier::new(PipelineStage::NarrowPhase, PipelineStage::BroadPhase, "bad");
223        assert!(!b.is_valid_order());
224    }
225    #[test]
226    fn barrier_set_validate_catches_bad() {
227        let mut bs = BarrierSet::new();
228        bs.add(ResourceBarrier::new(
229            PipelineStage::BroadPhase,
230            PipelineStage::NarrowPhase,
231            "ok",
232        ));
233        bs.add(ResourceBarrier::new(
234            PipelineStage::PostProcess,
235            PipelineStage::Integration,
236            "backwards",
237        ));
238        assert_eq!(bs.validate().len(), 1);
239    }
240    #[test]
241    fn barrier_set_filter_by_stage() {
242        let mut bs = BarrierSet::new();
243        bs.add(ResourceBarrier::new(
244            PipelineStage::BroadPhase,
245            PipelineStage::NarrowPhase,
246            "pair_buf",
247        ));
248        bs.add(ResourceBarrier::new(
249            PipelineStage::NarrowPhase,
250            PipelineStage::ConstraintSolve,
251            "contact_buf",
252        ));
253        assert_eq!(bs.barriers_from(PipelineStage::BroadPhase).len(), 1);
254        assert_eq!(bs.barriers_to(PipelineStage::ConstraintSolve).len(), 1);
255        assert_eq!(bs.len(), 2);
256    }
257    #[test]
258    fn async_queue_submit_and_flush() {
259        let mut q = AsyncComputeQueue::new();
260        assert!(q.is_idle());
261        let p = ComputePipeline::new("test", "", "main");
262        q.submit(DispatchBatch::new(p.clone(), 64));
263        q.submit(DispatchBatch::new(p, 128));
264        assert_eq!(q.pending(), 2);
265        let executed = q.flush();
266        assert_eq!(executed, 2);
267        assert!(q.is_idle());
268        assert_eq!(q.total_enqueued, 2);
269        assert_eq!(q.total_executed, 2);
270    }
271    #[test]
272    fn async_queue_flush_empty_returns_zero() {
273        let mut q = AsyncComputeQueue::new();
274        assert_eq!(q.flush(), 0);
275    }
276    #[test]
277    fn async_queue_pending_decrements_on_flush() {
278        let mut q = AsyncComputeQueue::new();
279        let p = ComputePipeline::new("t", "", "main");
280        for _ in 0..5 {
281            q.submit(DispatchBatch::new(p.clone(), 32));
282        }
283        assert_eq!(q.pending(), 5);
284        q.flush();
285        assert_eq!(q.pending(), 0);
286    }
287    #[test]
288    fn profiler_record_and_summary() {
289        let mut prof = PipelineProfiler::new();
290        prof.record("broadphase", 1.0);
291        prof.record("broadphase", 3.0);
292        let (mean, _std, n) = prof.summary("broadphase").unwrap();
293        assert!((mean - 2.0).abs() < 1e-10);
294        assert_eq!(n, 2);
295    }
296    #[test]
297    fn profiler_summary_unknown_stage_is_none() {
298        let prof = PipelineProfiler::new();
299        assert!(prof.summary("nonexistent").is_none());
300    }
301    #[test]
302    fn profiler_total_samples() {
303        let mut prof = PipelineProfiler::new();
304        prof.record("a", 1.0);
305        prof.record("a", 2.0);
306        prof.record("b", 3.0);
307        assert_eq!(prof.total_samples(), 3);
308    }
309    #[test]
310    fn profiler_stage_names_sorted() {
311        let mut prof = PipelineProfiler::new();
312        prof.record("zzz", 1.0);
313        prof.record("aaa", 1.0);
314        prof.record("mmm", 1.0);
315        let names = prof.stage_names();
316        assert_eq!(names, vec!["aaa", "mmm", "zzz"]);
317    }
318    #[test]
319    fn profiler_reset_clears_all() {
320        let mut prof = PipelineProfiler::new();
321        prof.record("broadphase", 1.5);
322        prof.reset();
323        assert_eq!(prof.total_samples(), 0);
324        assert!(prof.stage_names().is_empty());
325    }
326    #[test]
327    fn profiler_stddev_uniform_values() {
328        let mut prof = PipelineProfiler::new();
329        for _ in 0..4 {
330            prof.record("stage", 5.0);
331        }
332        let (mean, std, _) = prof.summary("stage").unwrap();
333        assert!((mean - 5.0).abs() < 1e-10);
334        assert!(std < 1e-10, "stddev should be ~0 for identical samples");
335    }
336    #[test]
337    fn pool_alloc_basic() {
338        let mut pool = GpuMemoryPool::new(1024);
339        let h = pool.alloc(256).expect("alloc should succeed");
340        assert_eq!(h.1, 256);
341        assert_eq!(pool.allocated, 256);
342        assert_eq!(pool.free_space(), 768);
343    }
344    #[test]
345    fn pool_alloc_and_free_round_trip() {
346        let mut pool = GpuMemoryPool::new(1024);
347        let h = pool.alloc(512).unwrap();
348        pool.free(h.0, h.1).expect("free should succeed");
349        assert!(pool.is_fully_free());
350        assert_eq!(pool.fragmentation_count(), 1);
351    }
352    #[test]
353    fn pool_alloc_exhaustion_returns_none() {
354        let mut pool = GpuMemoryPool::new(100);
355        let _ = pool.alloc(100).unwrap();
356        assert!(
357            pool.alloc(1).is_none(),
358            "pool exhausted → alloc should fail"
359        );
360    }
361    #[test]
362    fn pool_multiple_allocs_and_frees() {
363        let mut pool = GpuMemoryPool::new(1024);
364        let h1 = pool.alloc(256).unwrap();
365        let h2 = pool.alloc(256).unwrap();
366        pool.free(h1.0, h1.1).unwrap();
367        pool.free(h2.0, h2.1).unwrap();
368        assert!(pool.is_fully_free());
369    }
370    #[test]
371    fn pool_alloc_buffer_returns_correct_size() {
372        let mut pool = GpuMemoryPool::new(2048);
373        let (buf, handle) = pool
374            .alloc_buffer("positions", 512, BufferUsage::Storage)
375            .expect("alloc_buffer should succeed");
376        assert_eq!(buf.len(), 512);
377        assert_eq!(handle.1, 512);
378    }
379    #[test]
380    fn resource_handle_from_alloc() {
381        let handle = ResourceHandle::from_alloc((64, 128));
382        assert_eq!(handle.offset, 64);
383        assert_eq!(handle.size, 128);
384    }
385    #[test]
386    fn pool_coalesces_free_blocks() {
387        let mut pool = GpuMemoryPool::new(300);
388        let h1 = pool.alloc(100).unwrap();
389        let h2 = pool.alloc(100).unwrap();
390        let h3 = pool.alloc(100).unwrap();
391        pool.free(h1.0, h1.1).unwrap();
392        pool.free(h2.0, h2.1).unwrap();
393        pool.free(h3.0, h3.1).unwrap();
394        assert_eq!(pool.fragmentation_count(), 1, "all blocks should coalesce");
395    }
396}
397#[cfg(test)]
398mod extended_pipeline_tests {
399
400    use crate::pipeline::BarrierOptimizer;
401
402    use crate::pipeline::ComputeOverlapScheduler;
403    use crate::pipeline::ComputePipeline;
404
405    use crate::pipeline::DispatchBatch;
406    use crate::pipeline::FrameGraph;
407    use crate::pipeline::FrameGraphPass;
408    use crate::pipeline::GpuMemoryPool;
409    use crate::pipeline::MultiQueueRecorder;
410
411    use crate::pipeline::PipelineStage;
412    use crate::pipeline::PipelineStatistics;
413    use crate::pipeline::PipelineStats;
414    use crate::pipeline::QueueType;
415    use crate::pipeline::ResourceAliasingTracker;
416    use crate::pipeline::ResourceBarrier;
417
418    use crate::pipeline::StageTimer;
419    use crate::pipeline::TimestampQuery;
420    use crate::pipeline::TimestampQuerySet;
421    use crate::pipeline::WorldState;
422    #[test]
423    fn timestamp_query_elapsed() {
424        let q = TimestampQuery::new("broadphase", 1.0, 3.5);
425        assert!((q.elapsed_ms() - 2.5).abs() < 1e-10);
426    }
427    #[test]
428    fn timestamp_query_zero_duration() {
429        let q = TimestampQuery::new("instant", 5.0, 5.0);
430        assert!((q.elapsed_ms()).abs() < 1e-10);
431    }
432    #[test]
433    fn query_set_slowest_pass() {
434        let mut qs = TimestampQuerySet::new();
435        qs.record(TimestampQuery::new("a", 0.0, 1.0));
436        qs.record(TimestampQuery::new("b", 0.0, 5.0));
437        qs.record(TimestampQuery::new("c", 0.0, 2.0));
438        let slowest = qs.slowest_pass().unwrap();
439        assert_eq!(slowest.label, "b");
440    }
441    #[test]
442    fn query_set_total_elapsed() {
443        let mut qs = TimestampQuerySet::new();
444        qs.record(TimestampQuery::new("x", 0.0, 1.0));
445        qs.record(TimestampQuery::new("y", 0.0, 2.0));
446        assert!((qs.total_elapsed_ms() - 3.0).abs() < 1e-10);
447    }
448    #[test]
449    fn query_set_empty_slowest_none() {
450        let qs = TimestampQuerySet::new();
451        assert!(qs.slowest_pass().is_none());
452    }
453    #[test]
454    fn query_set_clear() {
455        let mut qs = TimestampQuerySet::new();
456        qs.record(TimestampQuery::new("a", 0.0, 1.0));
457        qs.clear();
458        assert!(qs.queries().is_empty());
459    }
460    #[test]
461    fn pipeline_stats_arithmetic_intensity() {
462        let s = PipelineStatistics {
463            cs_invocations: 1024,
464            workgroups_dispatched: 16,
465            flops: 2048,
466            bytes_read: 512,
467            bytes_written: 512,
468        };
469        let ai = s.arithmetic_intensity();
470        assert!((ai - 2.0).abs() < 1e-10, "ai = {ai}");
471    }
472    #[test]
473    fn pipeline_stats_zero_bytes() {
474        let s = PipelineStatistics::default();
475        assert!((s.arithmetic_intensity()).abs() < 1e-10);
476    }
477    #[test]
478    fn pipeline_stats_bandwidth_utilisation() {
479        let s = PipelineStatistics {
480            bytes_read: 500_000_000,
481            bytes_written: 500_000_000,
482            ..Default::default()
483        };
484        let util = s.bandwidth_utilization(1_000_000_000.0, 1.0);
485        assert!((util - 1.0).abs() < 1e-6, "util = {util}");
486    }
487    #[test]
488    fn multi_queue_recorder_submit_and_flush() {
489        let mut rec = MultiQueueRecorder::new();
490        let p = ComputePipeline::new("t", "", "main");
491        rec.submit(DispatchBatch::new(p.clone(), 64), QueueType::Main);
492        rec.submit(DispatchBatch::new(p.clone(), 64), QueueType::AsyncCompute);
493        rec.submit(DispatchBatch::new(p, 64), QueueType::Transfer);
494        assert_eq!(rec.pending_total(), 3);
495        let flushed = rec.flush_all();
496        assert_eq!(flushed, 3);
497        assert_eq!(rec.pending_total(), 0);
498    }
499    #[test]
500    fn multi_queue_recorder_total_recorded() {
501        let mut rec = MultiQueueRecorder::new();
502        let p = ComputePipeline::new("t", "", "main");
503        for _ in 0..5 {
504            rec.submit(DispatchBatch::new(p.clone(), 32), QueueType::Main);
505        }
506        assert_eq!(rec.total_recorded, 5);
507    }
508    #[test]
509    fn barrier_optimizer_removes_duplicates() {
510        let barriers = vec![
511            ResourceBarrier::new(PipelineStage::BroadPhase, PipelineStage::NarrowPhase, "buf"),
512            ResourceBarrier::new(PipelineStage::BroadPhase, PipelineStage::NarrowPhase, "buf"),
513        ];
514        let opt = BarrierOptimizer::optimize(&barriers);
515        assert_eq!(opt.len(), 1, "duplicate should be removed");
516        assert_eq!(BarrierOptimizer::savings(&barriers), 1);
517    }
518    #[test]
519    fn barrier_optimizer_preserves_different_resources() {
520        let barriers = vec![
521            ResourceBarrier::new(
522                PipelineStage::BroadPhase,
523                PipelineStage::NarrowPhase,
524                "buf_a",
525            ),
526            ResourceBarrier::new(
527                PipelineStage::BroadPhase,
528                PipelineStage::NarrowPhase,
529                "buf_b",
530            ),
531        ];
532        let opt = BarrierOptimizer::optimize(&barriers);
533        assert_eq!(opt.len(), 2);
534    }
535    #[test]
536    fn barrier_optimizer_empty_input() {
537        let opt = BarrierOptimizer::optimize(&[]);
538        assert!(opt.is_empty());
539        assert_eq!(BarrierOptimizer::savings(&[]), 0);
540    }
541    #[test]
542    fn aliasing_tracker_detects_shared_allocation() {
543        let mut t = ResourceAliasingTracker::new();
544        t.track("shadow_map", 0, 1024);
545        t.track("gbuffer_depth", 0, 1024);
546        assert!(t.are_aliased("shadow_map", "gbuffer_depth"));
547    }
548    #[test]
549    fn aliasing_tracker_no_alias() {
550        let mut t = ResourceAliasingTracker::new();
551        t.track("a", 0, 512);
552        t.track("b", 512, 512);
553        assert!(!t.are_aliased("a", "b"));
554    }
555    #[test]
556    fn aliasing_tracker_aliases_for() {
557        let mut t = ResourceAliasingTracker::new();
558        t.track("x", 100, 200);
559        t.track("y", 100, 200);
560        let aliases = t.aliases_for(100, 200);
561        assert_eq!(aliases.len(), 2);
562    }
563    #[test]
564    fn aliasing_tracker_counts() {
565        let mut t = ResourceAliasingTracker::new();
566        t.track("a", 0, 256);
567        t.track("b", 256, 256);
568        t.track("c", 0, 256);
569        assert_eq!(t.allocation_count(), 2);
570        assert_eq!(t.total_resource_registrations(), 3);
571    }
572    #[test]
573    fn overlap_scheduler_critical_and_background() {
574        let mut sched = ComputeOverlapScheduler::new();
575        let p = ComputePipeline::new("t", "", "main");
576        sched.submit_critical(DispatchBatch::new(p.clone(), 64));
577        sched.submit_background(DispatchBatch::new(p, 64));
578        assert!(sched.has_pending());
579        let n = sched.end_frame();
580        assert_eq!(n, 2);
581        assert!(!sched.has_pending());
582        assert_eq!(sched.critical_count, 0);
583    }
584    #[test]
585    fn overlap_scheduler_empty_frame() {
586        let mut sched = ComputeOverlapScheduler::new();
587        assert!(!sched.has_pending());
588        let n = sched.end_frame();
589        assert_eq!(n, 0);
590    }
591    #[test]
592    fn frame_graph_writers_and_readers() {
593        let mut fg = FrameGraph::new();
594        fg.add_pass(FrameGraphPass::new("shadow_pass", QueueType::Main).writes("shadow_map"));
595        fg.add_pass(FrameGraphPass::new("lighting_pass", QueueType::Main).reads("shadow_map"));
596        let writers = fg.writers_of("shadow_map");
597        assert_eq!(writers.len(), 1);
598        assert_eq!(writers[0].name, "shadow_pass");
599        let readers = fg.readers_of("shadow_map");
600        assert_eq!(readers.len(), 1);
601        assert_eq!(readers[0].name, "lighting_pass");
602    }
603    #[test]
604    fn frame_graph_validate_valid_deps() {
605        let mut fg = FrameGraph::new();
606        fg.add_pass(FrameGraphPass::new("pass_a", QueueType::Main));
607        fg.add_pass(FrameGraphPass::new("pass_b", QueueType::Main).depends_on("pass_a"));
608        assert!(
609            fg.validate_dependencies().is_empty(),
610            "valid deps should produce no errors"
611        );
612    }
613    #[test]
614    fn frame_graph_validate_invalid_dep() {
615        let mut fg = FrameGraph::new();
616        fg.add_pass(FrameGraphPass::new("pass_b", QueueType::Main).depends_on("nonexistent"));
617        let errors = fg.validate_dependencies();
618        assert_eq!(errors.len(), 1);
619        assert!(errors[0].contains("nonexistent"));
620    }
621    #[test]
622    fn frame_graph_async_pass_count() {
623        let mut fg = FrameGraph::new();
624        fg.add_pass(FrameGraphPass::new("main1", QueueType::Main));
625        fg.add_pass(FrameGraphPass::new("async1", QueueType::AsyncCompute));
626        fg.add_pass(FrameGraphPass::new("async2", QueueType::AsyncCompute));
627        assert_eq!(fg.async_pass_count(), 2);
628    }
629    #[test]
630    fn frame_graph_pass_names_order() {
631        let mut fg = FrameGraph::new();
632        fg.add_pass(FrameGraphPass::new("z", QueueType::Main));
633        fg.add_pass(FrameGraphPass::new("a", QueueType::Main));
634        let names = fg.pass_names();
635        assert_eq!(names, vec!["z", "a"]);
636    }
637    #[test]
638    fn pool_free_out_of_bounds_returns_err() {
639        let mut pool = GpuMemoryPool::new(100);
640        assert!(
641            pool.free(200, 10).is_err(),
642            "out-of-bounds free should fail"
643        );
644    }
645    #[test]
646    fn pool_fragmentation_after_partial_free() {
647        let mut pool = GpuMemoryPool::new(300);
648        let h1 = pool.alloc(100).unwrap();
649        let _h2 = pool.alloc(100).unwrap();
650        let _h3 = pool.alloc(100).unwrap();
651        pool.free(h1.0, h1.1).unwrap();
652        assert!(pool.fragmentation_count() >= 1);
653    }
654    #[test]
655    fn stage_timer_records_elapsed() {
656        let mut timer = StageTimer::start();
657        timer.stop();
658        assert!(timer.elapsed_ms >= 0.0, "elapsed should be non-negative");
659    }
660    #[test]
661    fn pipeline_stats_stage_total() {
662        let s = PipelineStats {
663            broadphase_ms: 1.0,
664            narrowphase_ms: 2.0,
665            constraint_ms: 3.0,
666            integration_ms: 4.0,
667            postprocess_ms: 5.0,
668            ..Default::default()
669        };
670        assert!((s.stage_total_ms() - 15.0).abs() < 1e-10);
671    }
672    #[test]
673    fn world_state_body_count() {
674        let w = WorldState {
675            positions: vec![0.0; 9],
676            velocities: vec![0.0; 9],
677            inverse_masses: vec![1.0, 1.0, 1.0],
678        };
679        assert_eq!(w.body_count(), 3);
680    }
681}