cvkg-core 0.3.1

Cyber Viking Kvasir Graph (CVKG) - High-fidelity agentic UI framework
Documentation
//! 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;
    }
}