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