1use super::types::WorldState;
6
7pub(super) fn run_broadphase(world: &WorldState) -> usize {
9 let n = world.body_count();
10 n.saturating_sub(1) * n / 2
11}
12pub(super) fn run_constraint_solve(world: &WorldState) -> usize {
14 let n = world.body_count();
15 n.saturating_sub(1) * n / 2
16}
17pub(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}
35pub(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}