1use super::types::WorldState;
6
7#[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#[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#[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#[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}