Skip to main content

cvkg_render_gpu/kvasir/
graph_cache.rs

1//! Render graph execution plan caching.
2//!
3//! Stores compiled execution plans keyed by the active render pipeline settings
4//! to prevent redundant topological graph rebuilding on the hot frame loop.
5
6use crate::kvasir::graph::{KvasirGraph, NodeKey};
7
8/// Holds the configuration signature, render graph, and compiled pass sequence.
9pub struct CachedGraphPlan {
10    /// Whether backdrop glass blur pass is enabled.
11    pub has_glass: bool,
12    /// Whether bloom post-processing pass is enabled.
13    pub has_bloom: bool,
14    /// Whether accessibility color-blind simulation is enabled.
15    pub has_accessibility: bool,
16    /// Whether volumetric raymarching pass is enabled.
17    pub has_volumetric: bool,
18    /// Number of active offscreen compositor nodes.
19    pub active_offscreens_count: usize,
20    /// Content hash for offscreen effects (effect name, blend mode, args).
21    /// Changes when effect content changes even if count stays the same.
22    pub offscreen_content_hash: u64,
23    /// Number of active portal blur boundary regions.
24    pub portal_regions_count: usize,
25    /// Content hash for portal regions (rect coordinates).
26    /// Changes when portal positions/sizes change even if count stays the same.
27    pub portal_content_hash: u64,
28    /// Frame buffer width in physical pixels.
29    pub width: u32,
30    /// Frame buffer height in physical pixels.
31    pub height: u32,
32    /// Bits representation of scale factor float to allow exact comparison.
33    pub scale_bits: u32,
34    /// Content hash for material graph compilation results.
35    /// Changes when a material's WGSL output changes (e.g., a Custom
36    /// material node is modified) so the cached plan is invalidated
37    /// rather than reused with stale shader bindings.
38    /// P1-9 fix: previously the cache key did not include material
39    /// compilation, so a material change would silently produce stale
40    /// shader bindings on the next frame.
41    pub material_compilation_hash: u64,
42    /// The cached render graph DAG structure.
43    pub graph: KvasirGraph,
44    /// The compiled execution order of graph node keys.
45    pub plan: Vec<NodeKey>,
46}
47
48impl CachedGraphPlan {
49    /// Check if the cached graph configuration matches the incoming parameters.
50    #[allow(clippy::too_many_arguments)]
51    pub fn matches(
52        &self,
53        has_glass: bool,
54        has_bloom: bool,
55        has_accessibility: bool,
56        has_volumetric: bool,
57        active_offscreens_count: usize,
58        offscreen_content_hash: u64,
59        portal_regions_count: usize,
60        portal_content_hash: u64,
61        width: u32,
62        height: u32,
63        scale_bits: u32,
64        material_compilation_hash: u64,
65    ) -> bool {
66        self.has_glass == has_glass
67            && self.has_bloom == has_bloom
68            && self.has_accessibility == has_accessibility
69            && self.has_volumetric == has_volumetric
70            && self.active_offscreens_count == active_offscreens_count
71            && self.offscreen_content_hash == offscreen_content_hash
72            && self.portal_regions_count == portal_regions_count
73            && self.portal_content_hash == portal_content_hash
74            && self.width == width
75            && self.height == height
76            && self.scale_bits == scale_bits
77            && self.material_compilation_hash == material_compilation_hash
78    }
79}
80
81#[cfg(test)]
82mod p1_9_tests {
83    use super::*;
84
85    fn make_plan(material_hash: u64) -> CachedGraphPlan {
86        // We can't easily construct a KvasirGraph here, but matches() only
87        // looks at the simple fields. Provide a default KvasirGraph and
88        // empty plan -- matches() never reads graph/plan fields.
89        CachedGraphPlan {
90            has_glass: true,
91            has_bloom: false,
92            has_accessibility: false,
93            has_volumetric: false,
94            active_offscreens_count: 0,
95            offscreen_content_hash: 0,
96            portal_regions_count: 0,
97            portal_content_hash: 0,
98            width: 1280,
99            height: 720,
100            scale_bits: 1.0f32.to_bits(),
101            material_compilation_hash: material_hash,
102            graph: crate::kvasir::graph::KvasirGraph::new(),
103            plan: Vec::new(),
104        }
105    }
106
107    #[test]
108    fn matches_returns_true_when_material_hash_matches() {
109        let plan = make_plan(42);
110        assert!(plan.matches(
111            true,
112            false,
113            false,
114            false,
115            0,
116            0,
117            0,
118            0,
119            1280,
120            720,
121            1.0f32.to_bits(),
122            42,
123        ));
124    }
125
126    #[test]
127    fn matches_returns_false_when_material_hash_changes() {
128        // P1-9 regression: a material change must invalidate the cache,
129        // even if all other fields are identical.
130        let plan = make_plan(42);
131        assert!(!plan.matches(
132            true,
133            false,
134            false,
135            false,
136            0,
137            0,
138            0,
139            0,
140            1280,
141            720,
142            1.0f32.to_bits(),
143            43, // different material hash
144        ));
145    }
146}