1use std::collections::{HashMap, HashSet, VecDeque};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
17pub enum TaskPriority {
18 RealTime = 4,
20 High = 3,
22 #[default]
24 Normal = 2,
25 Low = 1,
27 Background = 0,
29}
30
31#[derive(Debug, Clone)]
37pub struct ComputeTask {
38 pub name: String,
40 pub workgroup_size: [u32; 3],
42 pub dispatch_count: [u32; 3],
44 pub dependencies: Vec<String>,
46 pub priority: TaskPriority,
48 pub estimated_ms: f64,
50}
51
52impl ComputeTask {
53 pub fn new_1d(name: impl Into<String>, dispatch_x: u32) -> Self {
55 Self {
56 name: name.into(),
57 workgroup_size: [64, 1, 1],
58 dispatch_count: [dispatch_x, 1, 1],
59 dependencies: vec![],
60 priority: TaskPriority::Normal,
61 estimated_ms: 1.0,
62 }
63 }
64
65 pub fn new_2d(name: impl Into<String>, dispatch_x: u32, dispatch_y: u32) -> Self {
67 Self {
68 name: name.into(),
69 workgroup_size: [8, 8, 1],
70 dispatch_count: [dispatch_x, dispatch_y, 1],
71 dependencies: vec![],
72 priority: TaskPriority::Normal,
73 estimated_ms: 1.0,
74 }
75 }
76
77 pub fn total_workgroups(&self) -> u64 {
79 self.dispatch_count[0] as u64
80 * self.dispatch_count[1] as u64
81 * self.dispatch_count[2] as u64
82 }
83
84 pub fn total_invocations(&self) -> u64 {
86 self.total_workgroups()
87 * self.workgroup_size[0] as u64
88 * self.workgroup_size[1] as u64
89 * self.workgroup_size[2] as u64
90 }
91
92 pub fn depends_on(mut self, dep: impl Into<String>) -> Self {
94 self.dependencies.push(dep.into());
95 self
96 }
97
98 pub fn with_priority(mut self, priority: TaskPriority) -> Self {
100 self.priority = priority;
101 self
102 }
103
104 pub fn with_estimated_ms(mut self, ms: f64) -> Self {
106 self.estimated_ms = ms;
107 self
108 }
109}
110
111#[derive(Debug, Clone, Default)]
117pub struct TaskGraph {
118 tasks: HashMap<String, ComputeTask>,
120}
121
122impl TaskGraph {
123 pub fn new() -> Self {
125 Self::default()
126 }
127
128 pub fn add_task(&mut self, task: ComputeTask) {
130 self.tasks.insert(task.name.clone(), task);
131 }
132
133 pub fn remove_task(&mut self, name: &str) {
135 self.tasks.remove(name);
136 }
137
138 pub fn len(&self) -> usize {
140 self.tasks.len()
141 }
142
143 pub fn is_empty(&self) -> bool {
145 self.tasks.is_empty()
146 }
147
148 pub fn topological_sort(&self) -> Result<Vec<String>, String> {
153 let mut in_degree: HashMap<&str, usize> = HashMap::new();
155 let mut rev: HashMap<&str, Vec<&str>> = HashMap::new(); for (name, task) in &self.tasks {
158 in_degree.entry(name.as_str()).or_insert(0);
159 for dep in &task.dependencies {
160 if !self.tasks.contains_key(dep.as_str()) {
161 continue;
163 }
164 rev.entry(dep.as_str()).or_default().push(name.as_str());
166 *in_degree.entry(name.as_str()).or_insert(0) += 1;
167 }
168 }
169
170 let mut queue: VecDeque<&str> = in_degree
171 .iter()
172 .filter(|(_, d)| **d == 0)
173 .map(|(&n, _)| n)
174 .collect();
175
176 let mut queue_vec: Vec<&str> = queue.drain(..).collect();
178 queue_vec.sort();
179 queue.extend(queue_vec);
180
181 let mut order = Vec::new();
182 while let Some(name) = queue.pop_front() {
183 order.push(name.to_owned());
184 if let Some(dependents) = rev.get(name) {
185 let mut next: Vec<&str> = dependents
186 .iter()
187 .filter_map(|&d| {
188 let deg = in_degree.get_mut(d)?;
189 *deg -= 1;
190 if *deg == 0 { Some(d) } else { None }
191 })
192 .collect();
193 next.sort();
194 queue.extend(next);
195 }
196 }
197
198 if order.len() != self.tasks.len() {
199 let cycle_node = self
201 .tasks
202 .keys()
203 .find(|n| !order.contains(*n))
204 .cloned()
205 .unwrap_or_else(|| "unknown".to_owned());
206 Err(cycle_node)
207 } else {
208 Ok(order)
209 }
210 }
211
212 pub fn critical_path(&self) -> Vec<String> {
216 let order = match self.topological_sort() {
217 Ok(o) => o,
218 Err(_) => return vec![],
219 };
220
221 let mut eft: HashMap<&str, f64> = HashMap::new();
223 let mut pred: HashMap<&str, &str> = HashMap::new();
224
225 for name in &order {
226 let task = &self.tasks[name.as_str()];
227 let dep_max = task
228 .dependencies
229 .iter()
230 .filter_map(|d| eft.get(d.as_str()).copied())
231 .fold(0.0f64, f64::max);
232 let ef = dep_max + task.estimated_ms;
233 eft.insert(name.as_str(), ef);
234 if let Some(best_pred) = task
236 .dependencies
237 .iter()
238 .filter_map(|d| {
239 let t = eft.get(d.as_str()).copied()?;
240 Some((d.as_str(), t))
241 })
242 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
243 .map(|(d, _)| d)
244 {
245 pred.insert(name.as_str(), best_pred);
246 }
247 }
248
249 let end = order.iter().max_by(|a, b| {
251 eft.get(a.as_str())
252 .unwrap_or(&0.0)
253 .partial_cmp(eft.get(b.as_str()).unwrap_or(&0.0))
254 .expect("operation should succeed")
255 });
256
257 let mut path = Vec::new();
258 let mut cur = match end {
259 Some(s) => s.as_str(),
260 None => return vec![],
261 };
262 loop {
263 path.push(cur.to_owned());
264 match pred.get(cur) {
265 Some(&p) => cur = p,
266 None => break,
267 }
268 }
269 path.reverse();
270 path
271 }
272
273 pub fn has_cycle(&self) -> bool {
275 self.topological_sort().is_err()
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum BarrierType {
286 ReadAfterWrite,
288 WriteAfterRead,
290 WriteAfterWrite,
292}
293
294#[derive(Debug, Clone)]
296pub struct ResourceBarrier {
297 pub producer: String,
299 pub consumer: String,
301 pub barrier_type: BarrierType,
303 pub resource: String,
305}
306
307impl ResourceBarrier {
308 pub fn raw(
310 producer: impl Into<String>,
311 consumer: impl Into<String>,
312 resource: impl Into<String>,
313 ) -> Self {
314 Self {
315 producer: producer.into(),
316 consumer: consumer.into(),
317 barrier_type: BarrierType::ReadAfterWrite,
318 resource: resource.into(),
319 }
320 }
321
322 pub fn war(
324 producer: impl Into<String>,
325 consumer: impl Into<String>,
326 resource: impl Into<String>,
327 ) -> Self {
328 Self {
329 producer: producer.into(),
330 consumer: consumer.into(),
331 barrier_type: BarrierType::WriteAfterRead,
332 resource: resource.into(),
333 }
334 }
335}
336
337#[derive(Debug, Default)]
343pub struct TaskScheduler {
344 pub barriers: Vec<ResourceBarrier>,
346}
347
348impl TaskScheduler {
349 pub fn new() -> Self {
351 Self::default()
352 }
353
354 pub fn add_barrier(&mut self, barrier: ResourceBarrier) {
356 self.barriers.push(barrier);
357 }
358
359 pub fn schedule(&self, graph: &TaskGraph) -> Result<Vec<String>, String> {
363 graph.topological_sort()
364 }
365
366 pub fn batch_schedule(&self, graph: &TaskGraph) -> Result<Vec<Vec<String>>, String> {
370 let order = self.schedule(graph)?;
371 let tasks = &graph.tasks;
372
373 let mut depth: HashMap<&str, usize> = HashMap::new();
375 for name in &order {
376 let task = &tasks[name.as_str()];
377 let d = task
378 .dependencies
379 .iter()
380 .filter_map(|dep| depth.get(dep.as_str()).copied())
381 .max()
382 .map(|m| m + 1)
383 .unwrap_or(0);
384 depth.insert(name.as_str(), d);
385 }
386
387 let max_depth = depth.values().copied().max().unwrap_or(0);
388 let mut batches: Vec<Vec<String>> = vec![vec![]; max_depth + 1];
389 for name in &order {
390 let d = *depth.get(name.as_str()).unwrap_or(&0);
391 batches[d].push(name.clone());
392 }
393 Ok(batches)
394 }
395}
396
397#[derive(Debug, Clone)]
403pub struct WorkloadBalancer {
404 pub budget_ms: f64,
406 pending: Vec<(ComputeTask, f64)>,
408}
409
410impl WorkloadBalancer {
411 pub fn new(budget_ms: f64) -> Self {
413 Self {
414 budget_ms,
415 pending: vec![],
416 }
417 }
418
419 pub fn submit(&mut self, task: ComputeTask) {
421 let cost = task.estimated_ms;
422 self.pending.push((task, cost));
423 }
424
425 pub fn extract_frame_work(&mut self) -> Vec<ComputeTask> {
430 self.pending.sort_by(|a, b| {
432 b.0.priority
433 .cmp(&a.0.priority)
434 .then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
435 });
436
437 let mut remaining = self.budget_ms;
438 let mut this_frame = Vec::new();
439 let mut leftover = Vec::new();
440
441 for (task, cost) in self.pending.drain(..) {
442 if cost <= remaining || this_frame.is_empty() {
443 remaining -= cost;
444 this_frame.push(task);
445 } else {
446 leftover.push((task, cost));
447 }
448 }
449 self.pending = leftover;
450 this_frame
451 }
452
453 pub fn pending_count(&self) -> usize {
455 self.pending.len()
456 }
457}
458
459#[derive(Debug, Clone, PartialEq, Eq)]
465pub enum AsyncState {
466 Pending,
468 Running,
470 Done,
472 Failed(String),
474}
475
476#[derive(Debug, Clone)]
478pub struct AsyncResult {
479 pub name: String,
481 pub state: AsyncState,
483 pub output: Vec<u8>,
485}
486
487impl AsyncResult {
488 pub fn is_complete(&self) -> bool {
490 matches!(self.state, AsyncState::Done | AsyncState::Failed(_))
491 }
492}
493
494#[derive(Debug, Default)]
496pub struct AsyncCompute {
497 results: Vec<AsyncResult>,
499}
500
501impl AsyncCompute {
502 pub fn new() -> Self {
504 Self::default()
505 }
506
507 pub fn submit(&mut self, task: &ComputeTask) -> usize {
509 let idx = self.results.len();
510 self.results.push(AsyncResult {
511 name: task.name.clone(),
512 state: AsyncState::Pending,
513 output: vec![],
514 });
515 idx
516 }
517
518 pub fn tick(&mut self) {
523 for r in &mut self.results {
524 match r.state {
525 AsyncState::Pending => r.state = AsyncState::Running,
526 AsyncState::Running => {
527 r.state = AsyncState::Done;
528 r.output = vec![0u8; 4]; }
530 _ => {}
531 }
532 }
533 }
534
535 pub fn poll(&self, idx: usize) -> Option<&AsyncResult> {
537 self.results.get(idx)
538 }
539
540 pub fn drain_completed(&mut self) -> Vec<AsyncResult> {
542 let mut done = Vec::new();
543 let mut remaining = Vec::new();
544 for r in self.results.drain(..) {
545 if r.is_complete() {
546 done.push(r);
547 } else {
548 remaining.push(r);
549 }
550 }
551 self.results = remaining;
552 done
553 }
554}
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum PipelineStage {
563 Top,
565 Vertex,
567 Fragment,
569 Compute,
571 Transfer,
573 ColorAttachment,
575 ShaderRead,
577 Bottom,
579}
580
581#[derive(Debug, Clone)]
583pub struct PipelineBarrier {
584 pub src_stage: PipelineStage,
586 pub dst_stage: PipelineStage,
588 pub label: String,
590 pub color_to_shader_read: bool,
592}
593
594impl PipelineBarrier {
595 pub fn color_attachment_to_shader_read(label: impl Into<String>) -> Self {
597 Self {
598 src_stage: PipelineStage::ColorAttachment,
599 dst_stage: PipelineStage::ShaderRead,
600 label: label.into(),
601 color_to_shader_read: true,
602 }
603 }
604
605 pub fn compute_to_compute(label: impl Into<String>) -> Self {
607 Self {
608 src_stage: PipelineStage::Compute,
609 dst_stage: PipelineStage::Compute,
610 label: label.into(),
611 color_to_shader_read: false,
612 }
613 }
614
615 pub fn is_compute_read_hazard(&self) -> bool {
617 self.src_stage == PipelineStage::Compute
618 && matches!(
619 self.dst_stage,
620 PipelineStage::ShaderRead | PipelineStage::Fragment
621 )
622 }
623}
624
625#[derive(Debug, Clone)]
631pub struct GpuTimestampQuery {
632 pub label: String,
634 pub start_ns: u64,
636 pub end_ns: u64,
638 active: bool,
640}
641
642impl GpuTimestampQuery {
643 pub fn new(label: impl Into<String>) -> Self {
645 Self {
646 label: label.into(),
647 start_ns: 0,
648 end_ns: 0,
649 active: false,
650 }
651 }
652
653 pub fn begin(&mut self, now_ns: u64) {
655 self.start_ns = now_ns;
656 self.active = true;
657 }
658
659 pub fn end(&mut self, now_ns: u64) {
661 self.end_ns = now_ns;
662 self.active = false;
663 }
664
665 pub fn elapsed_us(&self) -> f64 {
667 (self.end_ns.saturating_sub(self.start_ns)) as f64 / 1_000.0
668 }
669
670 pub fn elapsed_ms(&self) -> f64 {
672 self.elapsed_us() / 1_000.0
673 }
674
675 pub fn is_active(&self) -> bool {
677 self.active
678 }
679}
680
681#[derive(Debug, Default)]
683pub struct TimestampPool {
684 queries: Vec<GpuTimestampQuery>,
686}
687
688impl TimestampPool {
689 pub fn new() -> Self {
691 Self::default()
692 }
693
694 pub fn begin(&mut self, label: impl Into<String>, now_ns: u64) -> usize {
696 let mut q = GpuTimestampQuery::new(label);
697 q.begin(now_ns);
698 let idx = self.queries.len();
699 self.queries.push(q);
700 idx
701 }
702
703 pub fn end(&mut self, idx: usize, now_ns: u64) {
705 if let Some(q) = self.queries.get_mut(idx) {
706 q.end(now_ns);
707 }
708 }
709
710 pub fn elapsed_ms(&self, idx: usize) -> f64 {
712 self.queries.get(idx).map(|q| q.elapsed_ms()).unwrap_or(0.0)
713 }
714
715 pub fn total_ms(&self) -> f64 {
717 self.queries
718 .iter()
719 .filter(|q| !q.is_active())
720 .map(|q| q.elapsed_ms())
721 .sum()
722 }
723
724 pub fn reset(&mut self) {
726 self.queries.clear();
727 }
728}
729
730#[derive(Debug, Clone)]
736pub struct FrameResource {
737 pub name: String,
739 pub size: usize,
741 pub first_use: usize,
743 pub last_use: usize,
745 pub offset: usize,
747}
748
749#[derive(Debug, Clone)]
751pub struct FramePass {
752 pub name: String,
754 pub reads: Vec<String>,
756 pub writes: Vec<String>,
758 pub barriers: Vec<PipelineBarrier>,
760}
761
762impl FramePass {
763 pub fn new(name: impl Into<String>) -> Self {
765 Self {
766 name: name.into(),
767 reads: vec![],
768 writes: vec![],
769 barriers: vec![],
770 }
771 }
772
773 pub fn reads(mut self, res: impl Into<String>) -> Self {
775 self.reads.push(res.into());
776 self
777 }
778
779 pub fn writes(mut self, res: impl Into<String>) -> Self {
781 self.writes.push(res.into());
782 self
783 }
784
785 pub fn barrier(mut self, b: PipelineBarrier) -> Self {
787 self.barriers.push(b);
788 self
789 }
790}
791
792#[derive(Debug, Default)]
794pub struct FrameGraph {
795 passes: Vec<FramePass>,
797 resources: HashMap<String, FrameResource>,
799}
800
801impl FrameGraph {
802 pub fn new() -> Self {
804 Self::default()
805 }
806
807 pub fn add_pass(&mut self, pass: FramePass) {
809 let idx = self.passes.len();
810 for res in pass.reads.iter().chain(pass.writes.iter()) {
812 let e = self.resources.entry(res.clone()).or_insert(FrameResource {
813 name: res.clone(),
814 size: 0,
815 first_use: idx,
816 last_use: idx,
817 offset: 0,
818 });
819 if idx < e.first_use {
821 e.first_use = idx;
822 }
823 if idx > e.last_use {
824 e.last_use = idx;
825 }
826 }
827 self.passes.push(pass);
828 }
829
830 pub fn declare_resource(&mut self, name: impl Into<String>, size: usize) {
832 let name = name.into();
833 let e = self.resources.entry(name.clone()).or_insert(FrameResource {
834 name: name.clone(),
835 size: 0,
836 first_use: usize::MAX,
837 last_use: 0,
838 offset: 0,
839 });
840 e.size = size;
841 }
842
843 pub fn alias_resources(&mut self) {
846 let names: Vec<String> = {
848 let mut v: Vec<String> = self.resources.keys().cloned().collect();
849 v.sort();
850 v
851 };
852
853 let mut allocations: Vec<(usize, usize, usize)> = Vec::new(); let pass_count = self.passes.len();
856
857 for name in &names {
858 if let Some(res) = self.resources.get_mut(name) {
859 if res.first_use > pass_count {
860 continue;
861 }
862 let mut found = None;
864 for (off, end, sz) in &mut allocations {
865 if *end < res.first_use && *sz >= res.size {
866 found = Some(*off);
867 *end = res.last_use;
868 break;
869 }
870 }
871 if let Some(off) = found {
872 res.offset = off;
873 } else {
874 let off: usize = allocations.iter().map(|(o, _, s)| o + s).max().unwrap_or(0);
875 res.offset = off;
876 let (last_use, size) = (res.last_use, res.size);
877 allocations.push((off, last_use, size));
878 }
879 }
880 }
881 }
882
883 pub fn peak_memory(&self) -> usize {
885 self.resources
886 .values()
887 .map(|r| r.offset + r.size)
888 .max()
889 .unwrap_or(0)
890 }
891
892 pub fn pass_count(&self) -> usize {
894 self.passes.len()
895 }
896
897 pub fn barriers_for_pass(&self, idx: usize) -> &[PipelineBarrier] {
899 self.passes
900 .get(idx)
901 .map(|p| p.barriers.as_slice())
902 .unwrap_or(&[])
903 }
904
905 pub fn all_barriers(&self) -> Vec<&PipelineBarrier> {
907 self.passes.iter().flat_map(|p| p.barriers.iter()).collect()
908 }
909
910 pub fn resources_for_pass(&self, idx: usize) -> Vec<&str> {
912 if let Some(pass) = self.passes.get(idx) {
913 pass.reads
914 .iter()
915 .chain(pass.writes.iter())
916 .map(|s| s.as_str())
917 .collect::<HashSet<_>>()
918 .into_iter()
919 .collect()
920 } else {
921 vec![]
922 }
923 }
924}
925
926#[cfg(test)]
931mod tests {
932 use super::*;
933
934 #[test]
937 fn test_priority_ordering() {
938 assert!(TaskPriority::RealTime > TaskPriority::High);
939 assert!(TaskPriority::High > TaskPriority::Normal);
940 assert!(TaskPriority::Normal > TaskPriority::Low);
941 assert!(TaskPriority::Low > TaskPriority::Background);
942 }
943
944 #[test]
947 fn test_compute_task_invocations_1d() {
948 let t = ComputeTask::new_1d("particles", 100);
949 assert_eq!(t.total_invocations(), 6400);
951 }
952
953 #[test]
954 fn test_compute_task_invocations_2d() {
955 let t = ComputeTask::new_2d("shadows", 8, 8);
956 assert_eq!(t.total_invocations(), 4096);
958 }
959
960 #[test]
961 fn test_compute_task_depends_on() {
962 let t = ComputeTask::new_1d("B", 1).depends_on("A");
963 assert!(t.dependencies.contains(&"A".to_owned()));
964 }
965
966 #[test]
967 fn test_compute_task_priority() {
968 let t = ComputeTask::new_1d("t", 1).with_priority(TaskPriority::High);
969 assert_eq!(t.priority, TaskPriority::High);
970 }
971
972 #[test]
975 fn test_task_graph_topo_sort_simple() {
976 let mut g = TaskGraph::new();
977 g.add_task(ComputeTask::new_1d("A", 1));
978 g.add_task(ComputeTask::new_1d("B", 1).depends_on("A"));
979 g.add_task(ComputeTask::new_1d("C", 1).depends_on("B"));
980 let order = g.topological_sort().unwrap();
981 let pos: HashMap<&str, usize> = order
982 .iter()
983 .enumerate()
984 .map(|(i, s)| (s.as_str(), i))
985 .collect();
986 assert!(pos["A"] < pos["B"]);
987 assert!(pos["B"] < pos["C"]);
988 }
989
990 #[test]
991 fn test_task_graph_topo_sort_diamond() {
992 let mut g = TaskGraph::new();
993 g.add_task(ComputeTask::new_1d("A", 1));
994 g.add_task(ComputeTask::new_1d("B", 1).depends_on("A"));
995 g.add_task(ComputeTask::new_1d("C", 1).depends_on("A"));
996 g.add_task(ComputeTask::new_1d("D", 1).depends_on("B").depends_on("C"));
997 let order = g.topological_sort().unwrap();
998 assert_eq!(order.len(), 4);
999 }
1000
1001 #[test]
1002 fn test_task_graph_cycle_detection() {
1003 let mut g = TaskGraph::new();
1004 g.add_task(ComputeTask::new_1d("A", 1).depends_on("B"));
1005 g.add_task(ComputeTask::new_1d("B", 1).depends_on("A"));
1006 assert!(g.has_cycle());
1007 }
1008
1009 #[test]
1010 fn test_task_graph_critical_path() {
1011 let mut g = TaskGraph::new();
1012 g.add_task(ComputeTask::new_1d("A", 1).with_estimated_ms(1.0));
1013 g.add_task(
1014 ComputeTask::new_1d("B", 1)
1015 .depends_on("A")
1016 .with_estimated_ms(2.0),
1017 );
1018 g.add_task(ComputeTask::new_1d("C", 1).with_estimated_ms(10.0));
1019 let cp = g.critical_path();
1020 assert!(cp.contains(&"C".to_owned()));
1022 }
1023
1024 #[test]
1025 fn test_task_graph_empty_topo() {
1026 let g = TaskGraph::new();
1027 let order = g.topological_sort().unwrap();
1028 assert!(order.is_empty());
1029 }
1030
1031 #[test]
1034 fn test_scheduler_schedule() {
1035 let mut g = TaskGraph::new();
1036 g.add_task(ComputeTask::new_1d("X", 1));
1037 g.add_task(ComputeTask::new_1d("Y", 1).depends_on("X"));
1038 let sched = TaskScheduler::new();
1039 let order = sched.schedule(&g).unwrap();
1040 assert_eq!(order.len(), 2);
1041 }
1042
1043 #[test]
1044 fn test_scheduler_batch_schedule() {
1045 let mut g = TaskGraph::new();
1046 g.add_task(ComputeTask::new_1d("A", 1));
1047 g.add_task(ComputeTask::new_1d("B", 1));
1048 g.add_task(ComputeTask::new_1d("C", 1).depends_on("A").depends_on("B"));
1049 let sched = TaskScheduler::new();
1050 let batches = sched.batch_schedule(&g).unwrap();
1051 assert!(batches[0].len() >= 2);
1053 assert!(batches.len() >= 2);
1055 }
1056
1057 #[test]
1060 fn test_resource_barrier_raw() {
1061 let b = ResourceBarrier::raw("write_task", "read_task", "position_buffer");
1062 assert_eq!(b.barrier_type, BarrierType::ReadAfterWrite);
1063 assert_eq!(b.resource, "position_buffer");
1064 }
1065
1066 #[test]
1067 fn test_resource_barrier_war() {
1068 let b = ResourceBarrier::war("reader", "writer", "depth");
1069 assert_eq!(b.barrier_type, BarrierType::WriteAfterRead);
1070 }
1071
1072 #[test]
1075 fn test_workload_balancer_respects_budget() {
1076 let mut wb = WorkloadBalancer::new(10.0);
1077 wb.submit(ComputeTask::new_1d("A", 1).with_estimated_ms(3.0));
1078 wb.submit(ComputeTask::new_1d("B", 1).with_estimated_ms(4.0));
1079 wb.submit(ComputeTask::new_1d("C", 1).with_estimated_ms(6.0));
1080 let frame = wb.extract_frame_work();
1081 let total: f64 = frame.iter().map(|t| t.estimated_ms).sum();
1082 assert!(total <= 10.0 + 6.0);
1084 }
1085
1086 #[test]
1087 fn test_workload_balancer_priority_order() {
1088 let mut wb = WorkloadBalancer::new(5.0);
1089 wb.submit(
1090 ComputeTask::new_1d("low", 1)
1091 .with_priority(TaskPriority::Low)
1092 .with_estimated_ms(2.0),
1093 );
1094 wb.submit(
1095 ComputeTask::new_1d("rt", 1)
1096 .with_priority(TaskPriority::RealTime)
1097 .with_estimated_ms(2.0),
1098 );
1099 let frame = wb.extract_frame_work();
1100 assert_eq!(frame[0].name, "rt");
1102 }
1103
1104 #[test]
1105 fn test_workload_balancer_pending_count() {
1106 let mut wb = WorkloadBalancer::new(1.0);
1107 for i in 0..5 {
1108 wb.submit(ComputeTask::new_1d(format!("t{i}"), 1).with_estimated_ms(1.0));
1109 }
1110 wb.extract_frame_work();
1111 assert!(wb.pending_count() < 5);
1112 }
1113
1114 #[test]
1117 fn test_async_compute_submit_poll() {
1118 let mut ac = AsyncCompute::new();
1119 let task = ComputeTask::new_1d("sim", 64);
1120 let idx = ac.submit(&task);
1121 let r = ac.poll(idx).unwrap();
1122 assert_eq!(r.state, AsyncState::Pending);
1123 }
1124
1125 #[test]
1126 fn test_async_compute_tick_to_done() {
1127 let mut ac = AsyncCompute::new();
1128 let task = ComputeTask::new_1d("sim", 1);
1129 let idx = ac.submit(&task);
1130 ac.tick(); ac.tick(); assert_eq!(ac.poll(idx).unwrap().state, AsyncState::Done);
1133 }
1134
1135 #[test]
1136 fn test_async_compute_drain_completed() {
1137 let mut ac = AsyncCompute::new();
1138 let t = ComputeTask::new_1d("t", 1);
1139 ac.submit(&t);
1140 ac.tick();
1141 ac.tick();
1142 let done = ac.drain_completed();
1143 assert_eq!(done.len(), 1);
1144 assert!(ac.poll(0).is_none()); }
1146
1147 #[test]
1150 fn test_pipeline_barrier_color_to_shader_read() {
1151 let b = PipelineBarrier::color_attachment_to_shader_read("gbuffer");
1152 assert!(b.color_to_shader_read);
1153 assert_eq!(b.src_stage, PipelineStage::ColorAttachment);
1154 assert_eq!(b.dst_stage, PipelineStage::ShaderRead);
1155 }
1156
1157 #[test]
1158 fn test_pipeline_barrier_compute_to_compute() {
1159 let b = PipelineBarrier::compute_to_compute("particles");
1160 assert_eq!(b.src_stage, PipelineStage::Compute);
1161 assert!(!b.is_compute_read_hazard()); }
1163
1164 #[test]
1165 fn test_pipeline_barrier_compute_read_hazard() {
1166 let b = PipelineBarrier {
1167 src_stage: PipelineStage::Compute,
1168 dst_stage: PipelineStage::ShaderRead,
1169 label: "test".to_owned(),
1170 color_to_shader_read: false,
1171 };
1172 assert!(b.is_compute_read_hazard());
1173 }
1174
1175 #[test]
1178 fn test_timestamp_query_elapsed() {
1179 let mut q = GpuTimestampQuery::new("render");
1180 q.begin(1_000_000); q.end(2_000_000); assert!((q.elapsed_ms() - 1.0).abs() < 1e-6);
1183 }
1184
1185 #[test]
1186 fn test_timestamp_query_is_active() {
1187 let mut q = GpuTimestampQuery::new("x");
1188 assert!(!q.is_active());
1189 q.begin(0);
1190 assert!(q.is_active());
1191 q.end(100);
1192 assert!(!q.is_active());
1193 }
1194
1195 #[test]
1196 fn test_timestamp_pool_total() {
1197 let mut pool = TimestampPool::new();
1198 let i0 = pool.begin("a", 0);
1199 pool.end(i0, 1_000_000);
1200 let i1 = pool.begin("b", 0);
1201 pool.end(i1, 2_000_000);
1202 let total = pool.total_ms();
1203 assert!((total - 3.0).abs() < 1e-6, "total={total}");
1204 }
1205
1206 #[test]
1207 fn test_timestamp_pool_reset() {
1208 let mut pool = TimestampPool::new();
1209 pool.begin("x", 0);
1210 pool.reset();
1211 assert!((pool.total_ms()).abs() < 1e-10);
1212 }
1213
1214 #[test]
1217 fn test_frame_graph_add_pass() {
1218 let mut fg = FrameGraph::new();
1219 fg.add_pass(FramePass::new("gbuffer").writes("color").writes("depth"));
1220 fg.add_pass(
1221 FramePass::new("lighting")
1222 .reads("color")
1223 .reads("depth")
1224 .writes("hdr"),
1225 );
1226 assert_eq!(fg.pass_count(), 2);
1227 }
1228
1229 #[test]
1230 fn test_frame_graph_resource_lifetime() {
1231 let mut fg = FrameGraph::new();
1232 fg.declare_resource("color", 1024 * 1024 * 4);
1233 fg.add_pass(FramePass::new("p0").writes("color"));
1234 fg.add_pass(FramePass::new("p1").reads("color"));
1235 let res = &fg.resources["color"];
1236 assert_eq!(res.first_use, 0);
1237 assert_eq!(res.last_use, 1);
1238 }
1239
1240 #[test]
1241 fn test_frame_graph_aliasing() {
1242 let mut fg = FrameGraph::new();
1243 fg.declare_resource("A", 1024);
1244 fg.declare_resource("B", 1024);
1245 fg.add_pass(FramePass::new("p0").writes("A"));
1246 fg.add_pass(FramePass::new("p1").reads("A"));
1247 fg.add_pass(FramePass::new("p2").writes("B"));
1248 fg.alias_resources();
1249 let peak = fg.peak_memory();
1251 assert!(peak > 0);
1252 }
1253
1254 #[test]
1255 fn test_frame_graph_barriers() {
1256 let mut fg = FrameGraph::new();
1257 fg.add_pass(
1258 FramePass::new("render")
1259 .barrier(PipelineBarrier::color_attachment_to_shader_read("test")),
1260 );
1261 let barriers = fg.barriers_for_pass(0);
1262 assert_eq!(barriers.len(), 1);
1263 }
1264
1265 #[test]
1266 fn test_frame_graph_all_barriers() {
1267 let mut fg = FrameGraph::new();
1268 fg.add_pass(FramePass::new("p0").barrier(PipelineBarrier::compute_to_compute("c0")));
1269 fg.add_pass(FramePass::new("p1").barrier(PipelineBarrier::compute_to_compute("c1")));
1270 assert_eq!(fg.all_barriers().len(), 2);
1271 }
1272}