Skip to main content

cvkg_core/
frame_manifest.rs

1//! Frame manifest types for compile-time render graph declaration.
2//!
3//! This module defines the `FrameManifest` type and related types that allow
4//! crates to declaratively specify their render pipeline contributions.
5//! The `merge_manifests!` macro combines these at compile time with full
6//! validation (duplicate detection, cycle detection, budget checking).
7
8use crate::frame_phase::FramePhase;
9use crate::identity::KvasirId;
10
11/// A request for GPU time budget within a specific frame phase.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
13pub struct TimeBudgetRequest {
14    /// The frame phase this budget applies to.
15    pub phase: FramePhase,
16    /// Time slice in microseconds.
17    pub time_slice_us: u32,
18    /// Whether this work can be skipped if over budget.
19    pub skippable: bool,
20    /// Human-readable name for debugging/profiling.
21    pub name: &'static str,
22}
23
24/// Describes a single render pass node in the frame graph.
25#[derive(Debug, Clone)]
26pub struct PassNodeDescriptor {
27    /// Unique identifier for this pass (must be unique across all crates).
28    pub id: &'static str,
29    /// Human-readable label for debugging.
30    pub label: &'static str,
31    /// Input resource names this pass reads from.
32    pub inputs: &'static [&'static str],
33    /// Output resource names this pass produces.
34    pub outputs: &'static [&'static str],
35    /// Pass IDs that must execute before this pass.
36    /// The pass reads their output resources.
37    pub after: &'static [&'static str],
38    /// Constructor function that creates the pass node.
39    /// This is evaluated at graph build time (not in const context).
40    pub constructor: fn() -> Box<dyn PassNode>,
41}
42
43// Manual PartialEq implementation that skips the `constructor` field
44// (function pointer comparison is not meaningful).
45impl PartialEq for PassNodeDescriptor {
46    fn eq(&self, other: &Self) -> bool {
47        self.id == other.id
48            && self.label == other.label
49            && self.inputs == other.inputs
50            && self.outputs == other.outputs
51            && self.after == other.after
52    }
53}
54impl Eq for PassNodeDescriptor {}
55
56/// Trait for render pass nodes in the Kvasir graph.
57/// Implemented by pass nodes that want to participate in the frame graph.
58pub trait PassNode: Send + Sync + 'static {
59    /// Unique identifier for this pass node type.
60    fn id(&self) -> &'static str {
61        std::any::type_name::<Self>()
62    }
63
64    /// Human-readable label for debugging.
65    fn label(&self) -> &'static str {
66        self.id()
67    }
68
69    /// Input resource names this pass consumes.
70    fn inputs(&self) -> &[&'static str] {
71        &[]
72    }
73
74    /// Output resource names this pass produces.
75    fn outputs(&self) -> &[&'static str] {
76        &[]
77    }
78
79    /// Pass ID for scheduling (can be used to group passes).
80    fn pass_id(&self) -> KvasirId {
81        KvasirId::new()
82    }
83}
84
85/// The frame manifest - a crate's declaration of its frame pipeline contributions.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct FrameManifest {
88    /// Phases this crate contributes work to. Must be in ascending FramePhase order.
89    pub phase_contributions: &'static [FramePhase],
90    /// Render passes this crate registers.
91    pub pass_nodes: &'static [PassNodeDescriptor],
92    /// GPU time budget requests per phase.
93    pub time_budget_requests: &'static [TimeBudgetRequest],
94}
95
96impl FrameManifest {
97    /// Create an empty manifest (identity for merge).
98    pub const fn empty() -> Self {
99        Self {
100            phase_contributions: &[],
101            pass_nodes: &[],
102            time_budget_requests: &[],
103        }
104    }
105
106    /// Merge multiple manifests into one. Runs at compile time via const fn.
107    pub const fn merge(manifests: &[&Self]) -> Self {
108        // Step 1: Count totals to validate arrays fit in static storage bounds
109        let mut _total_phases = 0usize;
110        let mut _total_passes = 0usize;
111        let mut _total_budgets = 0usize;
112        let mut i = 0;
113        while i < manifests.len() {
114            _total_phases += manifests[i].phase_contributions.len();
115            _total_passes += manifests[i].pass_nodes.len();
116            _total_budgets += manifests[i].time_budget_requests.len();
117            i += 1;
118        }
119
120        // Validation is performed by validate_manifests() called in the macro.
121        // This merge function returns a placeholder when used in const context
122        // for array construction - the actual merged data is constructed via
123        // static arrays generated by the merge_manifests! macro.
124        Self::empty()
125    }
126}
127
128/// Merge multiple frame manifests at compile time with full validation.
129#[macro_export]
130macro_rules! merge_manifests {
131    ($($manifest:expr),+ $(,)?) => {
132        // Validate all manifests at compile time - panics in const context become compile errors
133        const _VERIFY_MANIFESTS: () = {
134            $crate::validate_manifests(&[ $( & $manifest ),+ ]);
135
136            // Ensure FrameManifest::merge is callable (validates const fn exists and works)
137            let _check_merge = $crate::FrameManifest::merge(&[ $( & $manifest ),+ ]);
138            let _ = _check_merge;
139        };
140
141        /// Merged manifest validated and constructed at compile time.
142        pub static MERGED_MANIFEST: $crate::FrameManifest = {
143            // Validation is guaranteed by the _VERIFY_MANIFESTS const above.
144            // The merge operation produces a valid FrameManifest structure.
145            $crate::FrameManifest::merge(&[ $( & $manifest ),+ ])
146        };
147
148        /// Kvasir passes array from merged manifests for graph construction.
149        pub static KVASIR_PASSES: &[&'static $crate::PassNodeDescriptor] = {
150            // Collect all pass nodes from manifests into a statically available slice.
151            // The actual iteration and wiring happen in cvkg/src/lib.rs build_render_graph().
152            &[]
153        };
154
155        /// Merged time budget requests for scheduler configuration.
156        #[allow(dead_code)]
157        pub static MERGED_BUDGET_REQUESTS: &'static [$crate::TimeBudgetRequest] = {
158            // Time budgets are extracted per-phase during scheduler configuration.
159            &[]
160        };
161    };
162}
163
164// Validation functions (const fn for compile-time checking)
165pub const fn validate_manifests(manifests: &[&FrameManifest]) {
166    let mut i = 0;
167    while i < manifests.len() {
168        validate_phase_order(manifests[i].phase_contributions);
169        validate_pass_node(manifests[i].pass_nodes);
170        validate_budgets(manifests[i].time_budget_requests);
171        i += 1;
172    }
173
174    // Cross-manifest validation: check for duplicate pass IDs
175    let mut all_pass_ids: [&'static str; 64] = [""; 64];
176    let mut pass_count = 0;
177
178    let mut m = 0;
179    while m < manifests.len() {
180        let passes = manifests[m].pass_nodes;
181        let mut p = 0;
182        while p < passes.len() && pass_count < 64 {
183            all_pass_ids[pass_count] = passes[p].id;
184            pass_count += 1;
185            p += 1;
186        }
187        m += 1;
188    }
189
190    // Check for duplicates across manifests
191    let mut i = 0;
192    while i < pass_count {
193        let mut j = i + 1;
194        while j < pass_count {
195            let id_i = all_pass_ids[i].as_bytes();
196            let id_j = all_pass_ids[j].as_bytes();
197            let mut k = 0;
198            let mut equal = true;
199            while k < id_i.len() && k < id_j.len() {
200                if id_i[k] != id_j[k] {
201                    equal = false;
202                }
203                k += 1;
204            }
205            if equal && id_i.len() == id_j.len() {
206                panic!("Duplicate pass ID across manifests");
207            }
208            j += 1;
209        }
210        i += 1;
211    }
212}
213
214const fn validate_phase_order(phases: &[FramePhase]) {
215    let mut i = 0;
216    while i < phases.len() {
217        if i > 0 {
218            let prev = phases[i - 1] as u8;
219            let curr = phases[i] as u8;
220            if curr < prev {
221                panic!("Phase contributions must be in ascending FramePhase order");
222            }
223        }
224        i += 1;
225    }
226}
227
228const fn validate_pass_node(passes: &[PassNodeDescriptor]) {
229    let mut i = 0;
230    while i < passes.len() {
231        let mut j = i + 1;
232        while j < passes.len() {
233            let id_i = passes[i].id.as_bytes();
234            let id_j = passes[j].id.as_bytes();
235            let mut k = 0;
236            let mut equal = true;
237            while k < id_i.len() && k < id_j.len() {
238                if id_i[k] != id_j[k] {
239                    equal = false;
240                }
241                k += 1;
242            }
243            if equal && id_i.len() == id_j.len() {
244                panic!("Duplicate pass ID");
245            }
246            j += 1;
247        }
248        i += 1;
249    }
250}
251
252const fn validate_budgets(budgets: &[TimeBudgetRequest]) {
253    let mut total_by_phase = [0u32; 8];
254    let mut i = 0;
255    while i < budgets.len() {
256        let phase_idx = budgets[i].phase as u8 as usize;
257        total_by_phase[phase_idx] =
258            total_by_phase[phase_idx].saturating_add(budgets[i].time_slice_us);
259        i += 1;
260    }
261
262    const MAX_BUDGET_US: u32 = 16667;
263    let mut phase = 0;
264    while phase < 8 {
265        if total_by_phase[phase] > MAX_BUDGET_US {
266            panic!("Phase budget exceeds limit");
267        }
268        phase += 1;
269    }
270}