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