cvkg-render-gpu 0.2.13

Cyber Viking Kvasir Graph (CVKG) - High-fidelity agentic UI framework
Documentation
//! P1-35: SVG Filter Render Graph Integration
//!
//! Integrates SVG filter execution into the Kvasir render graph so that
//! filter passes are scheduled alongside other render passes (geometry,
//! glass, bloom, composite) with proper resource dependencies.

use crate::kvasir::node::{ExecutionContext, KvasirNode};
use crate::kvasir::resource::ResourceId;
use crate::kvasir::nodes::RES_SCENE;

/// Resource IDs for SVG filter intermediate results.
pub const RES_FILTER_TEMP_A: ResourceId = ResourceId(100);
pub const RES_FILTER_TEMP_B: ResourceId = ResourceId(101);
pub const RES_FILTER_INPUT: ResourceId = ResourceId(102);

/// A Kvasir node that executes an SVG filter graph.
///
/// This node reads from the scene texture (or a specified input resource),
/// runs the SVG filter graph, and writes the result to an output resource.
/// Multiple filter nodes can be chained for complex filter graphs.
pub struct SvgFilterNode {
    /// Input resource (typically RES_SCENE or another filter's output).
    input: ResourceId,
    /// Output resource (typically RES_SCENE for the final filter, or a temp).
    output: ResourceId,
    /// Cached input slice for KvasirNode trait.
    input_slice: [ResourceId; 1],
    /// Cached output slice for KvasirNode trait.
    output_slice: [ResourceId; 1],
    /// Filter graph to execute.
    filter_graph: Option<crate::svg_filter_graph::SvgFilterGraph>,
    /// Label for debugging.
    label: &'static str,
}

impl SvgFilterNode {
    /// Create a new SVG filter node that reads from `input` and writes to `output`.
    pub fn new(input: ResourceId, output: ResourceId) -> Self {
        Self {
            input,
            output,
            input_slice: [input; 1],
            output_slice: [output; 1],
            filter_graph: None,
            label: "SvgFilter",
        }
    }

    /// Set the filter graph to execute.
    pub fn with_filter_graph(mut self, graph: crate::svg_filter_graph::SvgFilterGraph) -> Self {
        self.filter_graph = Some(graph);
        self
    }

    /// Set a custom label for debugging.
    pub fn with_label(mut self, label: &'static str) -> Self {
        self.label = label;
        self
    }
}

impl KvasirNode for SvgFilterNode {
    fn label(&self) -> &'static str {
        self.label
    }

    fn inputs(&self) -> &[ResourceId] {
        &self.input_slice
    }

    fn outputs(&self) -> &[ResourceId] {
        &self.output_slice
    }

    fn pass_id(&self) -> crate::kvasir::nodes::PassId {
        // Use PostProcess variant for filter passes
        crate::kvasir::nodes::PassId::PostProcess {
            pipeline_id: 0xF1_000,
        }
    }

    fn execute(&self, ctx: &mut ExecutionContext) {
        // In a full implementation, this would:
        // 1. Bind the input texture
        // 2. For each filter primitive in the graph:
        //    a. Set up the render/compute pass
        //    b. Bind input/output textures
        //    c. Dispatch the filter shader
        // 3. The final primitive writes to the output resource
        //
        // For now, this is a placeholder that copies input to output
        // (identity filter) to establish the graph wiring.
        if self.filter_graph.is_none() {
            return;
        }

        // TODO: Full filter execution via FilterEngine
        // This requires access to the FilterEngine from the renderer,
        // which would be passed through ExecutionContext.
        log::trace!(
            "[Kvasir] Executing SVG filter: {} -> {:?}",
            self.label,
            self.output
        );
    }
}

/// Builder for constructing SVG filter render graph nodes.
pub struct SvgFilterGraphBuilder {
    nodes: Vec<(ResourceId, ResourceId, Option<crate::svg_filter_graph::SvgFilterGraph>)>,
}

impl SvgFilterGraphBuilder {
    pub fn new() -> Self {
        Self { nodes: Vec::new() }
    }

    /// Add a filter pass that reads from `input` and writes to `output`.
    pub fn add_pass(
        mut self,
        input: ResourceId,
        output: ResourceId,
        graph: crate::svg_filter_graph::SvgFilterGraph,
    ) -> Self {
        self.nodes.push((input, output, Some(graph)));
        self
    }

    /// Add a pass that reads from the scene and writes to a temp buffer.
    pub fn add_scene_pass(self, output: ResourceId, graph: crate::svg_filter_graph::SvgFilterGraph) -> Self {
        self.add_pass(RES_SCENE, output, graph)
    }

    /// Add a pass that reads from a temp buffer and writes to the scene.
    pub fn add_final_pass(self, input: ResourceId, graph: crate::svg_filter_graph::SvgFilterGraph) -> Self {
        self.add_pass(input, RES_SCENE, graph)
    }

    /// Build the filter nodes for insertion into the Kvasir graph.
    pub fn build(self) -> Vec<SvgFilterNode> {
        self.nodes
            .into_iter()
            .enumerate()
            .map(|(i, (input, output, graph))| {
                let mut node = SvgFilterNode::new(input, output);
                if let Some(g) = graph {
                    node = node.with_filter_graph(g);
                }
                node.with_label(match i {
                    0 => "SvgFilter[0]",
                    1 => "SvgFilter[1]",
                    2 => "SvgFilter[2]",
                    _ => "SvgFilter[n]",
                })
            })
            .collect()
    }
}

impl Default for SvgFilterGraphBuilder {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod p1_35_filter_graph_tests {
    use super::*;

    #[test]
    fn filter_node_identity() {
        let node = SvgFilterNode::new(RES_SCENE, RES_FILTER_TEMP_A);
        assert_eq!(node.inputs(), &[RES_SCENE]);
        assert_eq!(node.outputs(), &[RES_FILTER_TEMP_A]);
        assert_eq!(node.label(), "SvgFilter");
    }

    #[test]
    fn filter_node_with_label() {
        let node = SvgFilterNode::new(RES_SCENE, RES_FILTER_TEMP_A).with_label("MyFilter");
        assert_eq!(node.label(), "MyFilter");
    }

    #[test]
    fn filter_node_pass_id_is_post_process() {
        let node = SvgFilterNode::new(RES_SCENE, RES_FILTER_TEMP_A);
        match node.pass_id() {
            crate::kvasir::nodes::PassId::PostProcess { pipeline_id } => {
                assert_eq!(pipeline_id, 0xF1_000);
            }
            other => panic!("expected PostProcess, got {:?}", other),
        }
    }

    #[test]
    fn filter_graph_builder_creates_nodes() {
        // We can't create a real SvgFilterGraph without the full type,
        // but we can test the builder pattern compiles and produces nodes.
        let builder = SvgFilterGraphBuilder::new();
        let nodes = builder.build();
        assert!(nodes.is_empty());
    }

    #[test]
    fn filter_node_without_filter_graph_is_noop() {
        // A node without a filter graph should not panic during execute
        let node = SvgFilterNode::new(RES_SCENE, RES_FILTER_TEMP_A);
        // We can't call execute without a real ExecutionContext,
        // but we verified the is_none() check in the source.
        assert!(node.filter_graph.is_none());
    }
}