1use super::functions::*;
5use std::time::Instant;
6
7#[derive(Debug)]
12pub struct ComputeOverlapScheduler {
13 pub critical_count: usize,
15 pub background_count: usize,
17 pub(super) recorder: MultiQueueRecorder,
18}
19impl ComputeOverlapScheduler {
20 pub fn new() -> Self {
22 Self {
23 critical_count: 0,
24 background_count: 0,
25 recorder: MultiQueueRecorder::new(),
26 }
27 }
28 pub fn submit_critical(&mut self, batch: DispatchBatch) {
30 self.critical_count += 1;
31 self.recorder.submit(batch, QueueType::Main);
32 }
33 pub fn submit_background(&mut self, batch: DispatchBatch) {
35 self.background_count += 1;
36 self.recorder.submit(batch, QueueType::AsyncCompute);
37 }
38 pub fn end_frame(&mut self) -> usize {
40 let n = self.recorder.flush_all();
41 self.critical_count = 0;
42 self.background_count = 0;
43 n
44 }
45 pub fn has_pending(&self) -> bool {
47 self.recorder.pending_total() > 0
48 }
49}
50#[derive(Debug, Clone)]
52pub struct PipelineConfig {
53 pub enabled_stages: Vec<PipelineStage>,
55 pub substeps: u32,
57 pub use_gpu: bool,
59}
60impl PipelineConfig {
61 pub fn new() -> Self {
63 Self {
64 enabled_stages: PipelineStage::all_in_order().to_vec(),
65 substeps: 1,
66 use_gpu: false,
67 }
68 }
69 pub fn is_enabled(&self, stage: PipelineStage) -> bool {
71 self.enabled_stages.contains(&stage)
72 }
73}
74pub struct PhysicsPipeline {
79 pub config: PipelineConfig,
81 pub stats: PipelineStats,
83}
84impl PhysicsPipeline {
85 pub fn new(config: PipelineConfig) -> Self {
87 Self {
88 config,
89 stats: PipelineStats::default(),
90 }
91 }
92 pub fn step(&mut self, world_state: &mut WorldState, dt: f64) -> PipelineStats {
97 let step_start = Instant::now();
98 let mut step_stats = PipelineStats::default();
99 let sub_dt = if self.config.substeps > 0 {
100 dt / self.config.substeps as f64
101 } else {
102 dt
103 };
104 for _ in 0..self.config.substeps.max(1) {
105 let sub_stats = self.run_stages(world_state, sub_dt);
106 step_stats.accumulate(&sub_stats);
107 }
108 step_stats.total_time_ms = step_start.elapsed().as_secs_f64() * 1000.0;
109 self.stats.accumulate(&step_stats);
110 step_stats
111 }
112 fn run_stages(&self, world_state: &mut WorldState, dt: f64) -> PipelineStats {
114 let mut stats = PipelineStats::default();
115 for stage in PipelineStage::all_in_order() {
116 if !self.config.is_enabled(stage) {
117 continue;
118 }
119 let mut timer = StageTimer::start();
120 match stage {
121 PipelineStage::BroadPhase => {
122 let pairs = run_broadphase(world_state);
123 stats.collision_pairs += pairs as u32;
124 }
125 PipelineStage::NarrowPhase => {}
126 PipelineStage::ConstraintSolve => {
127 let solved = run_constraint_solve(world_state);
128 stats.solved_constraints += solved as u32;
129 }
130 PipelineStage::Integration => {
131 run_integration(world_state, dt);
132 }
133 PipelineStage::PostProcess => {
134 run_postprocess(world_state);
135 }
136 }
137 timer.stop();
138 match stage {
139 PipelineStage::BroadPhase => stats.broadphase_ms += timer.elapsed_ms,
140 PipelineStage::NarrowPhase => stats.narrowphase_ms += timer.elapsed_ms,
141 PipelineStage::ConstraintSolve => stats.constraint_ms += timer.elapsed_ms,
142 PipelineStage::Integration => stats.integration_ms += timer.elapsed_ms,
143 PipelineStage::PostProcess => stats.postprocess_ms += timer.elapsed_ms,
144 }
145 }
146 stats
147 }
148}
149#[derive(Debug, Clone)]
151pub struct ComputePipeline {
152 pub label: String,
154 pub shader_source: String,
156 pub entry_point: String,
158 pub workgroup_size: [u32; 3],
160}
161impl ComputePipeline {
162 pub fn new(label: &str, shader: &str, entry_point: &str) -> Self {
167 Self {
168 label: label.to_owned(),
169 shader_source: shader.to_owned(),
170 entry_point: entry_point.to_owned(),
171 workgroup_size: [64, 1, 1],
172 }
173 }
174 pub fn workgroups_needed(&self, n_items: u32) -> [u32; 3] {
179 let x = n_items.div_ceil(self.workgroup_size[0]);
180 [x, 1, 1]
181 }
182}
183#[derive(Debug, Clone, PartialEq)]
190pub struct ResourceBarrier {
191 pub src_stage: PipelineStage,
193 pub dst_stage: PipelineStage,
195 pub resource_name: String,
197}
198impl ResourceBarrier {
199 pub fn new(src: PipelineStage, dst: PipelineStage, name: &str) -> Self {
201 Self {
202 src_stage: src,
203 dst_stage: dst,
204 resource_name: name.to_owned(),
205 }
206 }
207 pub fn is_valid_order(&self) -> bool {
210 self.src_stage < self.dst_stage
211 }
212}
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub struct ResourceHandle {
219 pub offset: usize,
221 pub size: usize,
223}
224impl ResourceHandle {
225 pub fn from_alloc(alloc: (usize, usize)) -> Self {
227 Self {
228 offset: alloc.0,
229 size: alloc.1,
230 }
231 }
232}
233#[derive(Debug, Clone)]
238pub struct FrameGraphPass {
239 pub name: String,
241 pub reads: Vec<String>,
243 pub writes: Vec<String>,
245 pub dependencies: Vec<String>,
247 pub queue: QueueType,
249}
250impl FrameGraphPass {
251 pub fn new(name: impl Into<String>, queue: QueueType) -> Self {
253 Self {
254 name: name.into(),
255 reads: Vec::new(),
256 writes: Vec::new(),
257 dependencies: Vec::new(),
258 queue,
259 }
260 }
261 pub fn reads(mut self, resource: impl Into<String>) -> Self {
263 self.reads.push(resource.into());
264 self
265 }
266 pub fn writes(mut self, resource: impl Into<String>) -> Self {
268 self.writes.push(resource.into());
269 self
270 }
271 pub fn depends_on(mut self, pass: impl Into<String>) -> Self {
273 self.dependencies.push(pass.into());
274 self
275 }
276}
277#[derive(Debug, Default)]
279pub struct FrameGraph {
280 pub(super) passes: Vec<FrameGraphPass>,
281}
282impl FrameGraph {
283 pub fn new() -> Self {
285 Self::default()
286 }
287 pub fn add_pass(&mut self, pass: FrameGraphPass) {
289 self.passes.push(pass);
290 }
291 pub fn pass_names(&self) -> Vec<&str> {
293 self.passes.iter().map(|p| p.name.as_str()).collect()
294 }
295 pub fn writers_of(&self, resource: &str) -> Vec<&FrameGraphPass> {
297 self.passes
298 .iter()
299 .filter(|p| p.writes.iter().any(|w| w == resource))
300 .collect()
301 }
302 pub fn readers_of(&self, resource: &str) -> Vec<&FrameGraphPass> {
304 self.passes
305 .iter()
306 .filter(|p| p.reads.iter().any(|r| r == resource))
307 .collect()
308 }
309 pub fn validate_dependencies(&self) -> Vec<String> {
312 let names: std::collections::HashSet<&str> =
313 self.passes.iter().map(|p| p.name.as_str()).collect();
314 let mut errors = Vec::new();
315 for pass in &self.passes {
316 for dep in &pass.dependencies {
317 if !names.contains(dep.as_str()) {
318 errors.push(format!("{}: unknown dependency '{}'", pass.name, dep));
319 }
320 }
321 }
322 errors
323 }
324 pub fn async_pass_count(&self) -> usize {
326 self.passes
327 .iter()
328 .filter(|p| p.queue == QueueType::AsyncCompute)
329 .count()
330 }
331}
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
337pub enum PipelineStage {
338 BroadPhase,
340 NarrowPhase,
342 ConstraintSolve,
344 Integration,
346 PostProcess,
348}
349impl PipelineStage {
350 pub fn all_in_order() -> [PipelineStage; 5] {
352 [
353 PipelineStage::BroadPhase,
354 PipelineStage::NarrowPhase,
355 PipelineStage::ConstraintSolve,
356 PipelineStage::Integration,
357 PipelineStage::PostProcess,
358 ]
359 }
360}
361#[derive(Debug)]
363pub struct DispatchBatch {
364 pub pipeline: ComputePipeline,
366 pub bindings: Vec<CpuBuffer>,
368 pub dispatch_dims: [u32; 3],
370}
371impl DispatchBatch {
372 pub fn new(pipeline: ComputePipeline, workitems: u32) -> Self {
375 let dispatch_dims = pipeline.workgroups_needed(workitems);
376 Self {
377 pipeline,
378 bindings: Vec::new(),
379 dispatch_dims,
380 }
381 }
382 pub fn bind(&mut self, buffer: CpuBuffer) {
384 self.bindings.push(buffer);
385 }
386}
387pub struct AsyncComputeQueue {
393 pub(super) queue: std::collections::VecDeque<DispatchBatch>,
394 pub total_enqueued: usize,
396 pub total_executed: usize,
398}
399impl AsyncComputeQueue {
400 pub fn new() -> Self {
402 Self {
403 queue: std::collections::VecDeque::new(),
404 total_enqueued: 0,
405 total_executed: 0,
406 }
407 }
408 pub fn submit(&mut self, batch: DispatchBatch) {
410 self.total_enqueued += 1;
411 self.queue.push_back(batch);
412 }
413 pub fn flush(&mut self) -> usize {
418 let n = self.queue.len();
419 self.total_executed += n;
420 self.queue.clear();
421 n
422 }
423 pub fn pending(&self) -> usize {
425 self.queue.len()
426 }
427 pub fn is_idle(&self) -> bool {
429 self.queue.is_empty()
430 }
431}
432#[derive(Debug)]
434pub struct MultiQueueBatch {
435 pub batch: DispatchBatch,
437 pub queue: QueueType,
439 pub wait_frame: u64,
441}
442#[derive(Debug, Clone, Default)]
446pub struct WorldState {
447 pub positions: Vec<f64>,
449 pub velocities: Vec<f64>,
451 pub inverse_masses: Vec<f64>,
453}
454impl WorldState {
455 pub fn body_count(&self) -> usize {
457 self.inverse_masses.len()
458 }
459}
460#[derive(Debug, Default)]
465pub struct PipelineProfiler {
466 pub(super) samples: std::collections::HashMap<String, Vec<f64>>,
467}
468impl PipelineProfiler {
469 pub fn new() -> Self {
471 Self::default()
472 }
473 pub fn record(&mut self, stage_name: &str, ms: f64) {
475 self.samples
476 .entry(stage_name.to_owned())
477 .or_default()
478 .push(ms);
479 }
480 pub fn summary(&self, stage_name: &str) -> Option<(f64, f64, usize)> {
483 let v = self.samples.get(stage_name)?;
484 if v.is_empty() {
485 return None;
486 }
487 let n = v.len() as f64;
488 let mean = v.iter().sum::<f64>() / n;
489 let variance = v.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n;
490 Some((mean, variance.sqrt(), v.len()))
491 }
492 pub fn stage_names(&self) -> Vec<&str> {
494 let mut names: Vec<&str> = self.samples.keys().map(String::as_str).collect();
495 names.sort_unstable();
496 names
497 }
498 pub fn total_samples(&self) -> usize {
500 self.samples.values().map(Vec::len).sum()
501 }
502 pub fn reset(&mut self) {
504 self.samples.clear();
505 }
506}
507#[derive(Debug, Clone, PartialEq, Eq)]
513pub enum QueueType {
514 Main,
516 AsyncCompute,
518 Transfer,
520}
521pub struct StageTimer {
523 pub(super) start: Instant,
524 pub elapsed_ms: f64,
526}
527impl StageTimer {
528 pub fn start() -> Self {
530 Self {
531 start: Instant::now(),
532 elapsed_ms: 0.0,
533 }
534 }
535 pub fn stop(&mut self) {
537 self.elapsed_ms = self.start.elapsed().as_secs_f64() * 1000.0;
538 }
539}
540#[derive(Debug, Clone, Default)]
545pub struct PipelineStatistics {
546 pub cs_invocations: u64,
548 pub workgroups_dispatched: u64,
550 pub flops: u64,
552 pub bytes_read: u64,
554 pub bytes_written: u64,
556}
557impl PipelineStatistics {
558 pub fn arithmetic_intensity(&self) -> f64 {
560 let bytes = self.bytes_read + self.bytes_written;
561 if bytes == 0 {
562 return 0.0;
563 }
564 self.flops as f64 / bytes as f64
565 }
566 pub fn bandwidth_utilization(&self, peak_bw_bytes_s: f64, elapsed_s: f64) -> f64 {
568 if peak_bw_bytes_s <= 0.0 || elapsed_s <= 0.0 {
569 return 0.0;
570 }
571 let used = (self.bytes_read + self.bytes_written) as f64 / elapsed_s;
572 (used / peak_bw_bytes_s).min(1.0)
573 }
574}
575#[derive(Debug, Default)]
577pub struct MultiQueueRecorder {
578 pub main_queue: Vec<DispatchBatch>,
580 pub async_queue: Vec<DispatchBatch>,
582 pub transfer_queue: Vec<DispatchBatch>,
584 pub total_recorded: usize,
586}
587impl MultiQueueRecorder {
588 pub fn new() -> Self {
590 Self::default()
591 }
592 pub fn submit(&mut self, batch: DispatchBatch, queue: QueueType) {
594 self.total_recorded += 1;
595 match queue {
596 QueueType::Main => self.main_queue.push(batch),
597 QueueType::AsyncCompute => self.async_queue.push(batch),
598 QueueType::Transfer => self.transfer_queue.push(batch),
599 }
600 }
601 pub fn flush_all(&mut self) -> usize {
603 let n = self.main_queue.len() + self.async_queue.len() + self.transfer_queue.len();
604 self.main_queue.clear();
605 self.async_queue.clear();
606 self.transfer_queue.clear();
607 n
608 }
609 pub fn pending_total(&self) -> usize {
611 self.main_queue.len() + self.async_queue.len() + self.transfer_queue.len()
612 }
613}
614#[derive(Debug)]
624pub struct GpuMemoryPool {
625 pub capacity: usize,
627 pub allocated: usize,
629 pub(super) free_list: Vec<(usize, usize)>,
631}
632impl GpuMemoryPool {
633 pub fn new(capacity: usize) -> Self {
635 Self {
636 capacity,
637 allocated: 0,
638 free_list: vec![(0, capacity)],
639 }
640 }
641 pub fn alloc(&mut self, size: usize) -> Option<(usize, usize)> {
646 for i in 0..self.free_list.len() {
647 let (off, avail) = self.free_list[i];
648 if avail >= size {
649 let alloc_off = off;
650 if avail == size {
651 self.free_list.remove(i);
652 } else {
653 self.free_list[i] = (off + size, avail - size);
654 }
655 self.allocated += size;
656 return Some((alloc_off, size));
657 }
658 }
659 None
660 }
661 pub fn free(&mut self, offset: usize, size: usize) -> Result<(), &'static str> {
665 if offset + size > self.capacity {
666 return Err("block out of bounds");
667 }
668 if self.allocated < size {
669 return Err("double-free: allocated count underflow");
670 }
671 self.allocated -= size;
672 self.free_list.push((offset, size));
673 self.free_list.sort_by_key(|&(off, _)| off);
674 let mut merged: Vec<(usize, usize)> = Vec::new();
675 for &(off, sz) in &self.free_list {
676 if let Some(last) = merged.last_mut()
677 && last.0 + last.1 == off
678 {
679 last.1 += sz;
680 continue;
681 }
682 merged.push((off, sz));
683 }
684 self.free_list = merged;
685 Ok(())
686 }
687 pub fn free_space(&self) -> usize {
689 self.capacity - self.allocated
690 }
691 pub fn is_fully_free(&self) -> bool {
693 self.allocated == 0
694 }
695 pub fn fragmentation_count(&self) -> usize {
697 self.free_list.len()
698 }
699 pub fn alloc_buffer(
702 &mut self,
703 label: &str,
704 n: usize,
705 usage: BufferUsage,
706 ) -> Option<(CpuBuffer, (usize, usize))> {
707 let handle = self.alloc(n)?;
708 let buf = CpuBuffer::new_zeros(label, n, usage);
709 Some((buf, handle))
710 }
711}
712#[derive(Debug, Clone, Default)]
714pub struct TimestampQuerySet {
715 pub(super) queries: Vec<TimestampQuery>,
716}
717impl TimestampQuerySet {
718 pub fn new() -> Self {
720 Self::default()
721 }
722 pub fn record(&mut self, query: TimestampQuery) {
724 self.queries.push(query);
725 }
726 pub fn queries(&self) -> &[TimestampQuery] {
728 &self.queries
729 }
730 pub fn slowest_pass(&self) -> Option<&TimestampQuery> {
732 self.queries.iter().max_by(|a, b| {
733 a.elapsed_ms()
734 .partial_cmp(&b.elapsed_ms())
735 .unwrap_or(std::cmp::Ordering::Equal)
736 })
737 }
738 pub fn total_elapsed_ms(&self) -> f64 {
740 self.queries.iter().map(|q| q.elapsed_ms()).sum()
741 }
742 pub fn clear(&mut self) {
744 self.queries.clear();
745 }
746}
747#[derive(Debug, Clone, Default)]
749pub struct PipelineStats {
750 pub broadphase_ms: f64,
752 pub narrowphase_ms: f64,
754 pub constraint_ms: f64,
756 pub integration_ms: f64,
758 pub postprocess_ms: f64,
760 pub total_time_ms: f64,
762 pub collision_pairs: u32,
764 pub solved_constraints: u32,
766}
767impl PipelineStats {
768 pub fn accumulate(&mut self, other: &PipelineStats) {
770 self.broadphase_ms += other.broadphase_ms;
771 self.narrowphase_ms += other.narrowphase_ms;
772 self.constraint_ms += other.constraint_ms;
773 self.integration_ms += other.integration_ms;
774 self.postprocess_ms += other.postprocess_ms;
775 self.total_time_ms += other.total_time_ms;
776 self.collision_pairs += other.collision_pairs;
777 self.solved_constraints += other.solved_constraints;
778 }
779 pub fn stage_total_ms(&self) -> f64 {
781 self.broadphase_ms
782 + self.narrowphase_ms
783 + self.constraint_ms
784 + self.integration_ms
785 + self.postprocess_ms
786 }
787}
788#[derive(Debug, Default)]
793pub struct BarrierOptimizer;
794impl BarrierOptimizer {
795 pub fn optimize(barriers: &[ResourceBarrier]) -> BarrierSet {
798 let mut seen: std::collections::HashMap<
799 (PipelineStage, PipelineStage, &str),
800 &ResourceBarrier,
801 > = std::collections::HashMap::new();
802 for b in barriers {
803 seen.insert((b.src_stage, b.dst_stage, b.resource_name.as_str()), b);
804 }
805 let mut out = BarrierSet::new();
806 for b in seen.values() {
807 out.add(ResourceBarrier::new(
808 b.src_stage,
809 b.dst_stage,
810 &b.resource_name,
811 ));
812 }
813 out
814 }
815 pub fn savings(barriers: &[ResourceBarrier]) -> usize {
818 let optimized = Self::optimize(barriers);
819 barriers.len().saturating_sub(optimized.len())
820 }
821}
822pub struct PipelineBuilder {
837 pub(super) config: PipelineConfig,
838}
839impl PipelineBuilder {
840 pub fn new() -> Self {
842 Self {
843 config: PipelineConfig::new(),
844 }
845 }
846 pub fn substeps(mut self, n: u32) -> Self {
848 self.config.substeps = n;
849 self
850 }
851 pub fn use_gpu(mut self, gpu: bool) -> Self {
853 self.config.use_gpu = gpu;
854 self
855 }
856 pub fn enable_stage(mut self, stage: PipelineStage) -> Self {
858 if !self.config.enabled_stages.contains(&stage) {
859 self.config.enabled_stages.push(stage);
860 self.config.enabled_stages.sort();
861 }
862 self
863 }
864 pub fn disable_stage(mut self, stage: PipelineStage) -> Self {
866 self.config.enabled_stages.retain(|&s| s != stage);
867 self
868 }
869 pub fn build(self) -> PhysicsPipeline {
871 PhysicsPipeline::new(self.config)
872 }
873}
874#[derive(Debug, Clone, Default)]
877pub struct BarrierSet {
878 pub(super) barriers: Vec<ResourceBarrier>,
879}
880impl BarrierSet {
881 pub fn new() -> Self {
883 Self::default()
884 }
885 pub fn add(&mut self, barrier: ResourceBarrier) {
887 self.barriers.push(barrier);
888 }
889 pub fn barriers_from(&self, stage: PipelineStage) -> Vec<&ResourceBarrier> {
891 self.barriers
892 .iter()
893 .filter(|b| b.src_stage == stage)
894 .collect()
895 }
896 pub fn barriers_to(&self, stage: PipelineStage) -> Vec<&ResourceBarrier> {
898 self.barriers
899 .iter()
900 .filter(|b| b.dst_stage == stage)
901 .collect()
902 }
903 pub fn len(&self) -> usize {
905 self.barriers.len()
906 }
907 pub fn is_empty(&self) -> bool {
909 self.barriers.is_empty()
910 }
911 pub fn validate(&self) -> Vec<&ResourceBarrier> {
914 self.barriers
915 .iter()
916 .filter(|b| !b.is_valid_order())
917 .collect()
918 }
919}
920#[derive(Debug, Clone)]
926pub struct TimestampQuery {
927 pub label: String,
929 pub begin_ms: f64,
931 pub end_ms: f64,
933}
934impl TimestampQuery {
935 pub fn new(label: impl Into<String>, begin_ms: f64, end_ms: f64) -> Self {
937 Self {
938 label: label.into(),
939 begin_ms,
940 end_ms,
941 }
942 }
943 pub fn elapsed_ms(&self) -> f64 {
945 self.end_ms - self.begin_ms
946 }
947}
948#[derive(Debug, Default)]
953pub struct ResourceAliasingTracker {
954 pub(super) aliases: std::collections::HashMap<(usize, usize), Vec<String>>,
957}
958impl ResourceAliasingTracker {
959 pub fn new() -> Self {
961 Self::default()
962 }
963 pub fn track(&mut self, resource_name: impl Into<String>, offset: usize, size: usize) {
965 self.aliases
966 .entry((offset, size))
967 .or_default()
968 .push(resource_name.into());
969 }
970 pub fn aliases_for(&self, offset: usize, size: usize) -> &[String] {
972 self.aliases
973 .get(&(offset, size))
974 .map(Vec::as_slice)
975 .unwrap_or(&[])
976 }
977 pub fn are_aliased(&self, a: &str, b: &str) -> bool {
979 for names in self.aliases.values() {
980 if names.contains(&a.to_string()) && names.contains(&b.to_string()) {
981 return true;
982 }
983 }
984 false
985 }
986 pub fn allocation_count(&self) -> usize {
988 self.aliases.len()
989 }
990 pub fn total_resource_registrations(&self) -> usize {
992 self.aliases.values().map(Vec::len).sum()
993 }
994}
995#[derive(Debug, Clone)]
997pub struct CpuBuffer {
998 pub label: String,
1000 pub data: Vec<f32>,
1002 pub usage: BufferUsage,
1004}
1005impl CpuBuffer {
1006 pub fn new_f32(label: &str, data: Vec<f32>, usage: BufferUsage) -> Self {
1008 Self {
1009 label: label.to_owned(),
1010 data,
1011 usage,
1012 }
1013 }
1014 pub fn new_zeros(label: &str, n: usize, usage: BufferUsage) -> Self {
1016 Self {
1017 label: label.to_owned(),
1018 data: vec![0.0_f32; n],
1019 usage,
1020 }
1021 }
1022 pub fn len(&self) -> usize {
1024 self.data.len()
1025 }
1026 pub fn is_empty(&self) -> bool {
1028 self.data.is_empty()
1029 }
1030}
1031#[derive(Debug, Clone, Copy, PartialEq)]
1033pub enum BufferUsage {
1034 Storage,
1036 Uniform,
1038 StorageReadOnly,
1040}