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}