Skip to main content

oxiphysics_gpu/
scheduler.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! GPU workload scheduler.
5//!
6//! Provides task graphs, topological scheduling, resource barriers, async
7//! compute simulation, frame graphs, and timestamp queries — all CPU-side.
8
9use std::collections::{HashMap, HashSet, VecDeque};
10
11// ---------------------------------------------------------------------------
12// TaskPriority
13// ---------------------------------------------------------------------------
14
15/// Priority level for a compute task.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
17pub enum TaskPriority {
18    /// Must complete within the current frame; highest urgency.
19    RealTime = 4,
20    /// High-importance work that should not be deferred.
21    High = 3,
22    /// Standard work.
23    #[default]
24    Normal = 2,
25    /// Can be deferred to future frames if time is tight.
26    Low = 1,
27    /// Background processing with no frame deadline.
28    Background = 0,
29}
30
31// ---------------------------------------------------------------------------
32// ComputeTask
33// ---------------------------------------------------------------------------
34
35/// A single GPU compute dispatch.
36#[derive(Debug, Clone)]
37pub struct ComputeTask {
38    /// Unique task name.
39    pub name: String,
40    /// Workgroup size in (x, y, z).
41    pub workgroup_size: [u32; 3],
42    /// Dispatch count in (x, y, z).
43    pub dispatch_count: [u32; 3],
44    /// Names of tasks that must complete before this one.
45    pub dependencies: Vec<String>,
46    /// Priority for the scheduler.
47    pub priority: TaskPriority,
48    /// Estimated execution time in milliseconds.
49    pub estimated_ms: f64,
50}
51
52impl ComputeTask {
53    /// Create a simple 1-D compute task.
54    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    /// Create a 2-D compute task.
66    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    /// Total number of workgroup invocations.
78    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    /// Total number of shader invocations.
85    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    /// Add a dependency by name.
93    pub fn depends_on(mut self, dep: impl Into<String>) -> Self {
94        self.dependencies.push(dep.into());
95        self
96    }
97
98    /// Set priority.
99    pub fn with_priority(mut self, priority: TaskPriority) -> Self {
100        self.priority = priority;
101        self
102    }
103
104    /// Set estimated execution time.
105    pub fn with_estimated_ms(mut self, ms: f64) -> Self {
106        self.estimated_ms = ms;
107        self
108    }
109}
110
111// ---------------------------------------------------------------------------
112// TaskGraph
113// ---------------------------------------------------------------------------
114
115/// A directed acyclic graph of compute tasks.
116#[derive(Debug, Clone, Default)]
117pub struct TaskGraph {
118    /// All tasks keyed by name.
119    tasks: HashMap<String, ComputeTask>,
120}
121
122impl TaskGraph {
123    /// Create an empty task graph.
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Add a task to the graph.  Replaces any existing task with the same name.
129    pub fn add_task(&mut self, task: ComputeTask) {
130        self.tasks.insert(task.name.clone(), task);
131    }
132
133    /// Remove a task by name.
134    pub fn remove_task(&mut self, name: &str) {
135        self.tasks.remove(name);
136    }
137
138    /// Number of tasks.
139    pub fn len(&self) -> usize {
140        self.tasks.len()
141    }
142
143    /// True when there are no tasks.
144    pub fn is_empty(&self) -> bool {
145        self.tasks.is_empty()
146    }
147
148    /// Topological sort using Kahn's algorithm.
149    ///
150    /// Returns `Ok(order)` where `order` is a valid execution order, or
151    /// `Err(cycle)` naming one task involved in a cycle.
152    pub fn topological_sort(&self) -> Result<Vec<String>, String> {
153        // Build adjacency and in-degree maps
154        let mut in_degree: HashMap<&str, usize> = HashMap::new();
155        let mut rev: HashMap<&str, Vec<&str>> = HashMap::new(); // task -> tasks that depend on it
156
157        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                    // Unknown dependency — skip
162                    continue;
163                }
164                // dep -> name (name depends on dep)
165                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        // Sort for determinism
177        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            // Find a node still in a cycle
200            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    /// Compute the critical path (longest chain by estimated_ms).
213    ///
214    /// Returns the list of task names on the critical path.
215    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        // Compute earliest finish times
222        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            // Track predecessor that provides dep_max
235            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        // Walk back from the task with maximum eft
250        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    /// Check whether the graph contains a cycle.
274    pub fn has_cycle(&self) -> bool {
275        self.topological_sort().is_err()
276    }
277}
278
279// ---------------------------------------------------------------------------
280// ResourceBarrier
281// ---------------------------------------------------------------------------
282
283/// Type of resource access ordering barrier.
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum BarrierType {
286    /// Read-after-write: a reader must wait for a preceding writer.
287    ReadAfterWrite,
288    /// Write-after-read: a writer must wait for a preceding reader.
289    WriteAfterRead,
290    /// Write-after-write: serialise two writers.
291    WriteAfterWrite,
292}
293
294/// A resource barrier between two tasks.
295#[derive(Debug, Clone)]
296pub struct ResourceBarrier {
297    /// Name of the task that writes / produces.
298    pub producer: String,
299    /// Name of the task that reads / consumes.
300    pub consumer: String,
301    /// The kind of hazard this barrier prevents.
302    pub barrier_type: BarrierType,
303    /// Resource being protected (e.g. buffer name).
304    pub resource: String,
305}
306
307impl ResourceBarrier {
308    /// Create a read-after-write barrier.
309    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    /// Create a write-after-read barrier.
323    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// ---------------------------------------------------------------------------
338// TaskScheduler
339// ---------------------------------------------------------------------------
340
341/// Schedules a task graph into an execution order.
342#[derive(Debug, Default)]
343pub struct TaskScheduler {
344    /// Barriers to inject between tasks.
345    pub barriers: Vec<ResourceBarrier>,
346}
347
348impl TaskScheduler {
349    /// Create a new scheduler.
350    pub fn new() -> Self {
351        Self::default()
352    }
353
354    /// Add a resource barrier.
355    pub fn add_barrier(&mut self, barrier: ResourceBarrier) {
356        self.barriers.push(barrier);
357    }
358
359    /// Schedule a task graph.
360    ///
361    /// Returns the topological execution order, or an error if a cycle exists.
362    pub fn schedule(&self, graph: &TaskGraph) -> Result<Vec<String>, String> {
363        graph.topological_sort()
364    }
365
366    /// Schedule and group independent tasks into parallel batches.
367    ///
368    /// Each inner `Vec` contains tasks that can run concurrently.
369    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        // Compute the depth of each task (longest dependency chain)
374        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// ---------------------------------------------------------------------------
398// WorkloadBalancer
399// ---------------------------------------------------------------------------
400
401/// Splits large dispatches across frames to stay within a time budget.
402#[derive(Debug, Clone)]
403pub struct WorkloadBalancer {
404    /// GPU time budget per frame in milliseconds.
405    pub budget_ms: f64,
406    /// Accumulated pending tasks with their estimated costs.
407    pending: Vec<(ComputeTask, f64)>,
408}
409
410impl WorkloadBalancer {
411    /// Create a new balancer with the given budget.
412    pub fn new(budget_ms: f64) -> Self {
413        Self {
414            budget_ms,
415            pending: vec![],
416        }
417    }
418
419    /// Submit a task for scheduling.
420    pub fn submit(&mut self, task: ComputeTask) {
421        let cost = task.estimated_ms;
422        self.pending.push((task, cost));
423    }
424
425    /// Extract tasks that fit within this frame's budget.
426    ///
427    /// Higher-priority tasks are selected first.  Returns the tasks to
428    /// execute this frame.
429    pub fn extract_frame_work(&mut self) -> Vec<ComputeTask> {
430        // Sort by descending priority then descending estimated_ms
431        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    /// Number of pending tasks.
454    pub fn pending_count(&self) -> usize {
455        self.pending.len()
456    }
457}
458
459// ---------------------------------------------------------------------------
460// AsyncCompute
461// ---------------------------------------------------------------------------
462
463/// State of an async compute task.
464#[derive(Debug, Clone, PartialEq, Eq)]
465pub enum AsyncState {
466    /// Waiting to be dispatched.
467    Pending,
468    /// Currently running.
469    Running,
470    /// Completed successfully.
471    Done,
472    /// Failed with an error message.
473    Failed(String),
474}
475
476/// A promise-like result from an async compute submission.
477#[derive(Debug, Clone)]
478pub struct AsyncResult {
479    /// Task name.
480    pub name: String,
481    /// Current state.
482    pub state: AsyncState,
483    /// Simulated output data (bytes).
484    pub output: Vec<u8>,
485}
486
487impl AsyncResult {
488    /// True when the task has finished (successfully or not).
489    pub fn is_complete(&self) -> bool {
490        matches!(self.state, AsyncState::Done | AsyncState::Failed(_))
491    }
492}
493
494/// Simulated async compute queue.
495#[derive(Debug, Default)]
496pub struct AsyncCompute {
497    /// All submitted tasks.
498    results: Vec<AsyncResult>,
499}
500
501impl AsyncCompute {
502    /// Create a new async compute queue.
503    pub fn new() -> Self {
504        Self::default()
505    }
506
507    /// Submit a task for async execution.  Returns an index into the result list.
508    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    /// Advance all pending tasks by one simulated tick.
519    ///
520    /// - Pending → Running
521    /// - Running → Done (with placeholder output)
522    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]; // placeholder
529                }
530                _ => {}
531            }
532        }
533    }
534
535    /// Get the result for submission index `idx`.
536    pub fn poll(&self, idx: usize) -> Option<&AsyncResult> {
537        self.results.get(idx)
538    }
539
540    /// Drain completed results.
541    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// ---------------------------------------------------------------------------
557// PipelineBarrier
558// ---------------------------------------------------------------------------
559
560/// Describes a memory barrier stage.
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum PipelineStage {
563    /// Beginning of the pipeline.
564    Top,
565    /// Vertex shading.
566    Vertex,
567    /// Fragment / pixel shading.
568    Fragment,
569    /// Compute dispatch.
570    Compute,
571    /// Transfer / copy operations.
572    Transfer,
573    /// Color attachment output.
574    ColorAttachment,
575    /// Shader read.
576    ShaderRead,
577    /// End of pipeline.
578    Bottom,
579}
580
581/// A pipeline memory barrier between passes.
582#[derive(Debug, Clone)]
583pub struct PipelineBarrier {
584    /// Stage that must complete before the barrier.
585    pub src_stage: PipelineStage,
586    /// Stage that must wait after the barrier.
587    pub dst_stage: PipelineStage,
588    /// Human-readable label.
589    pub label: String,
590    /// Whether this is a color attachment → shader read transition.
591    pub color_to_shader_read: bool,
592}
593
594impl PipelineBarrier {
595    /// Create a color attachment output → shader read barrier.
596    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    /// Create a compute → compute barrier (for storage buffer hazards).
606    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    /// True when the barrier crosses a compute → read hazard.
616    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// ---------------------------------------------------------------------------
626// GpuTimestampQuery
627// ---------------------------------------------------------------------------
628
629/// A single GPU timestamp query pair.
630#[derive(Debug, Clone)]
631pub struct GpuTimestampQuery {
632    /// Label for this query.
633    pub label: String,
634    /// Simulated start time (nanoseconds).
635    pub start_ns: u64,
636    /// Simulated end time (nanoseconds).
637    pub end_ns: u64,
638    /// Whether `begin` has been called.
639    active: bool,
640}
641
642impl GpuTimestampQuery {
643    /// Create a new timestamp query.
644    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    /// Record the start timestamp.
654    pub fn begin(&mut self, now_ns: u64) {
655        self.start_ns = now_ns;
656        self.active = true;
657    }
658
659    /// Record the end timestamp.
660    pub fn end(&mut self, now_ns: u64) {
661        self.end_ns = now_ns;
662        self.active = false;
663    }
664
665    /// Elapsed time in microseconds.
666    pub fn elapsed_us(&self) -> f64 {
667        (self.end_ns.saturating_sub(self.start_ns)) as f64 / 1_000.0
668    }
669
670    /// Elapsed time in milliseconds.
671    pub fn elapsed_ms(&self) -> f64 {
672        self.elapsed_us() / 1_000.0
673    }
674
675    /// True when a `begin` is outstanding.
676    pub fn is_active(&self) -> bool {
677        self.active
678    }
679}
680
681/// A pool of timestamp query pairs for profiling a frame.
682#[derive(Debug, Default)]
683pub struct TimestampPool {
684    /// All queries.
685    queries: Vec<GpuTimestampQuery>,
686}
687
688impl TimestampPool {
689    /// Create an empty pool.
690    pub fn new() -> Self {
691        Self::default()
692    }
693
694    /// Allocate and begin a new timestamp query.  Returns its index.
695    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    /// End the query at `idx`.
704    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    /// Get elapsed ms for query `idx`.
711    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    /// Total elapsed ms across all finished queries.
716    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    /// Reset all queries.
725    pub fn reset(&mut self) {
726        self.queries.clear();
727    }
728}
729
730// ---------------------------------------------------------------------------
731// FrameGraph
732// ---------------------------------------------------------------------------
733
734/// A transient resource in the frame graph.
735#[derive(Debug, Clone)]
736pub struct FrameResource {
737    /// Resource name.
738    pub name: String,
739    /// Size in bytes.
740    pub size: usize,
741    /// First pass index that uses this resource.
742    pub first_use: usize,
743    /// Last pass index that uses this resource.
744    pub last_use: usize,
745    /// Allocated byte offset (set during aliasing).
746    pub offset: usize,
747}
748
749/// A render pass in the frame graph.
750#[derive(Debug, Clone)]
751pub struct FramePass {
752    /// Pass name.
753    pub name: String,
754    /// Resources read by this pass.
755    pub reads: Vec<String>,
756    /// Resources written by this pass.
757    pub writes: Vec<String>,
758    /// Pipeline barriers to inject before this pass.
759    pub barriers: Vec<PipelineBarrier>,
760}
761
762impl FramePass {
763    /// Create a new frame pass.
764    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    /// Declare a resource read.
774    pub fn reads(mut self, res: impl Into<String>) -> Self {
775        self.reads.push(res.into());
776        self
777    }
778
779    /// Declare a resource write.
780    pub fn writes(mut self, res: impl Into<String>) -> Self {
781        self.writes.push(res.into());
782        self
783    }
784
785    /// Add a pipeline barrier.
786    pub fn barrier(mut self, b: PipelineBarrier) -> Self {
787        self.barriers.push(b);
788        self
789    }
790}
791
792/// A full-frame resource graph with transient resource aliasing.
793#[derive(Debug, Default)]
794pub struct FrameGraph {
795    /// All passes in submission order.
796    passes: Vec<FramePass>,
797    /// Declared transient resources.
798    resources: HashMap<String, FrameResource>,
799}
800
801impl FrameGraph {
802    /// Create an empty frame graph.
803    pub fn new() -> Self {
804        Self::default()
805    }
806
807    /// Add a render pass.
808    pub fn add_pass(&mut self, pass: FramePass) {
809        let idx = self.passes.len();
810        // Track resource lifetimes
811        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            // Update first_use if this is earlier (handles declare_resource + add_pass order)
820            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    /// Declare a transient resource with its size.
831    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    /// Run a simple aliasing pass: resources that do not overlap in lifetime
844    /// share the same memory offset.
845    pub fn alias_resources(&mut self) {
846        // Greedy aliasing: sort by first_use, then assign offsets
847        let names: Vec<String> = {
848            let mut v: Vec<String> = self.resources.keys().cloned().collect();
849            v.sort();
850            v
851        };
852
853        // Track which offsets are "free" at each pass
854        let mut allocations: Vec<(usize, usize, usize)> = Vec::new(); // (offset, end_pass, size)
855        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                // Find a free slot
863                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    /// Compute the peak memory required (maximum end of any allocation).
884    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    /// Number of passes.
893    pub fn pass_count(&self) -> usize {
894        self.passes.len()
895    }
896
897    /// Get all barriers for a given pass by index.
898    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    /// Collect all pipeline barriers across the frame in order.
906    pub fn all_barriers(&self) -> Vec<&PipelineBarrier> {
907        self.passes.iter().flat_map(|p| p.barriers.iter()).collect()
908    }
909
910    /// Find all resources used by pass at index `idx`.
911    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// ---------------------------------------------------------------------------
927// Tests
928// ---------------------------------------------------------------------------
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933
934    // --- TaskPriority tests ---
935
936    #[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    // --- ComputeTask tests ---
945
946    #[test]
947    fn test_compute_task_invocations_1d() {
948        let t = ComputeTask::new_1d("particles", 100);
949        // dispatch 100 × 1 × 1, workgroup 64 × 1 × 1 → 6400 invocations
950        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        // dispatch 8×8×1, workgroup 8×8×1 → 8*8*8*8 = 4096
957        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    // --- TaskGraph tests ---
973
974    #[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        // C alone is the critical path (10ms vs A+B = 3ms)
1021        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    // --- TaskScheduler tests ---
1032
1033    #[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        // A and B should be in the same batch (depth 0)
1052        assert!(batches[0].len() >= 2);
1053        // C in a later batch
1054        assert!(batches.len() >= 2);
1055    }
1056
1057    // --- ResourceBarrier tests ---
1058
1059    #[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    // --- WorkloadBalancer tests ---
1073
1074    #[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        // At most budget + one overflow (for non-empty guarantee)
1083        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        // RealTime should be first
1101        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    // --- AsyncCompute tests ---
1115
1116    #[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(); // Pending → Running
1131        ac.tick(); // Running → Done
1132        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()); // drained
1145    }
1146
1147    // --- PipelineBarrier tests ---
1148
1149    #[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()); // dst is also Compute
1162    }
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    // --- GpuTimestampQuery tests ---
1176
1177    #[test]
1178    fn test_timestamp_query_elapsed() {
1179        let mut q = GpuTimestampQuery::new("render");
1180        q.begin(1_000_000); // 1 ms in ns
1181        q.end(2_000_000); // 2 ms in ns
1182        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    // --- FrameGraph tests ---
1215
1216    #[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        // B's lifetime starts after A ends, so they may share memory
1250        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}