cvkg_compositor/engine.rs
1//! # Compositor Engine
2//!
3//! The compositor engine maintains the `LayerTree` across frames and provides:
4//! - **Material Routing**: Flattens the layer tree into GPU pass buckets.
5//! - **Damage Tracking**: Tracks which layers changed to avoid re-recording.
6//! - **Z-Sorting**: Ensures correct painter's order within each pass.
7//!
8//! The engine produces three command buckets that feed into the
9//! Backdrop Capture Architecture in `cvkg-render-gpu`:
10//! 1. `scene_commands` -- Opaque/standard UI (Scene Capture pass)
11//! 2. `glass_commands` -- Glassmorphism (Material Composite pass)
12//! 3. `overlay_commands` -- Foreground UI (Top-Level pass)
13
14use crate::layer::{DrawCommand, Layer, LayerId, LayerTree, Material};
15use cvkg_core::Rect;
16use log::warn;
17
18/// Draw command tagged with its source layer material.
19/// This is the output of the compositor's routing phase.
20#[derive(Debug, Clone)]
21pub struct RoutedDrawCommand {
22 /// The draw command itself.
23 pub command: DrawCommand,
24 /// The material this command belongs to.
25 pub material: Material,
26 /// The layer this command originated from.
27 pub source_layer: LayerId,
28 /// Z-order index for sorting within the same material pass.
29 pub z_index: u32,
30 /// Explicit draw order for fine-grained sorting within the same z-pass.
31 /// Higher values render later (on top). Default: 0.
32 /// Convention: 0 = background, 100 = UI chrome, 200 = SVG content, 300 = overlays.
33 pub draw_order: i32,
34}
35
36/// A command emitted by the compositor to control the GPU rendering pipeline.
37#[derive(Debug, Clone)]
38pub enum RenderCommand {
39 /// Standard geometry draw call.
40 Draw(RoutedDrawCommand),
41 /// Instructs the GPU to bind an offscreen texture for the subsequent commands.
42 PushOffscreen {
43 source_layer: LayerId,
44 material: Material,
45 bounds: Rect,
46 },
47 /// Instructs the GPU to unbind the offscreen texture, and draw it.
48 PopOffscreen,
49}
50
51/// Segmented command buckets produced by flatten_and_route().
52/// Each bucket corresponds to a GPU pass in the Backdrop Capture Architecture.
53#[derive(Debug, Default)]
54pub struct CommandBuckets {
55 /// Commands for the Scene Capture pass (opaque background + standard UI).
56 pub scene_commands: Vec<RenderCommand>,
57 /// Commands for the Material Composite pass (glass elements sampling blur pyramid).
58 pub glass_commands: Vec<RenderCommand>,
59 /// Commands for the Top-Level / Foreground pass (crisp text, icons, edge lighting).
60 pub overlay_commands: Vec<RenderCommand>,
61}
62
63impl CommandBuckets {
64 /// Returns the total number of commands across all buckets.
65 pub fn total_count(&self) -> usize {
66 self.scene_commands.len() + self.glass_commands.len() + self.overlay_commands.len()
67 }
68
69 /// Returns true if all buckets are empty.
70 pub fn is_empty(&self) -> bool {
71 self.scene_commands.is_empty()
72 && self.glass_commands.is_empty()
73 && self.overlay_commands.is_empty()
74 }
75
76 /// Clears all command buckets.
77 pub fn clear(&mut self) {
78 self.scene_commands.clear();
79 self.glass_commands.clear();
80 self.overlay_commands.clear();
81 }
82}
83
84/// Damage tracking information for a single frame.
85#[derive(Debug, Default)]
86pub struct DamageInfo {
87 /// IDs of layers that were modified this frame.
88 pub dirty_layers: Vec<LayerId>,
89 /// The frame generation these changes belong to.
90 pub frame_generation: u64,
91 /// True if the entire tree needs re-flattening (structural changes).
92 pub full_rebuild_needed: bool,
93}
94
95/// The compositor engine that orchestrates the retained-mode layer tree.
96pub struct CompositorEngine {
97 /// The retained layer tree.
98 layer_tree: LayerTree,
99 /// Reusable buffer for flattening (avoids per-frame allocation).
100 flatten_buffer: Vec<RenderCommand>,
101 /// The last frame generation that was flattened.
102 last_flatten_generation: u64,
103 /// Damage information for the current frame.
104 current_damage: DamageInfo,
105 /// Z-order counter during flattening.
106 z_counter: u32,
107 /// True if the current tree contains an active ShaderEffect
108 has_active_shaders: bool,
109}
110
111impl Default for CompositorEngine {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117impl CompositorEngine {
118 /// Creates a new compositor engine with an empty layer tree.
119 pub fn new() -> Self {
120 Self {
121 layer_tree: LayerTree::new(),
122 flatten_buffer: Vec::new(),
123 last_flatten_generation: 0,
124 current_damage: DamageInfo::default(),
125 z_counter: 0,
126 has_active_shaders: false,
127 }
128 }
129
130 /// Returns a reference to the layer tree.
131 pub fn layer_tree(&self) -> &LayerTree {
132 &self.layer_tree
133 }
134
135 /// Returns a mutable reference to the layer tree.
136 pub fn layer_tree_mut(&mut self) -> &mut LayerTree {
137 &mut self.layer_tree
138 }
139
140 /// Creates a new layer and inserts it into the tree.
141 /// Returns the new layer's ID.
142 pub fn create_layer(&mut self, layer: Layer) -> LayerId {
143 let id = layer.id;
144 self.layer_tree.insert_layer(layer);
145 self.current_damage.dirty_layers.push(id);
146 self.current_damage.full_rebuild_needed = true;
147 id
148 }
149
150 /// Removes a layer from the tree.
151 pub fn remove_layer(&mut self, id: LayerId) -> Option<Layer> {
152 self.current_damage.dirty_layers.push(id);
153 self.current_damage.full_rebuild_needed = true;
154 self.layer_tree.remove_layer(id)
155 }
156
157 /// Marks a layer as dirty (its content changed).
158 pub fn mark_dirty(&mut self, id: LayerId) {
159 if self.layer_tree.get_layer(id).is_some() {
160 self.layer_tree.mark_dirty(id);
161 self.current_damage.dirty_layers.push(id);
162 }
163 }
164
165 /// Returns the damage information for the current frame.
166 pub fn damage_info(&self) -> &DamageInfo {
167 &self.current_damage
168 }
169
170 /// Flattens the layer tree and routes draw calls into three command buckets
171 /// based on their material type.
172 ///
173 /// This is the core routing method that feeds the GPU's multi-pass pipeline:
174 /// - Scene Capture pass: All opaque draw calls
175 /// - Material Composite pass: Glass draw calls (sample blur pyramid)
176 /// - Foreground pass: Overlay draw calls (crisp on top)
177 ///
178 /// The tree is traversed depth-first, back-to-front (painter's algorithm),
179 /// producing correctly Z-ordered commands within each bucket.
180 pub fn flatten_and_route(&mut self) -> CommandBuckets {
181 let mut buckets = CommandBuckets::default();
182
183 if self.layer_tree.is_empty() {
184 return buckets;
185 }
186
187 // Use the reusable buffer to avoid per-frame allocation.
188 self.flatten_buffer.clear();
189 self.z_counter = 0;
190 self.has_active_shaders = false;
191
192 // Flatten the tree depth-first, back-to-front.
193 let roots = self.layer_tree.roots().to_vec();
194 Self::flatten_tree(
195 &mut self.layer_tree,
196 &roots,
197 &mut self.flatten_buffer,
198 &mut self.z_counter,
199 &mut self.has_active_shaders,
200 );
201
202 // Route into buckets by material — use retain-like approach to avoid clones.
203 // Instead of cloning each command, we drain from flatten_buffer.
204 for cmd in self.flatten_buffer.drain(..) {
205 match &cmd {
206 RenderCommand::Draw(routed) => match routed.material {
207 Material::Glass { .. } => {
208 buckets.glass_commands.push(cmd);
209 }
210 Material::Overlay => {
211 buckets.overlay_commands.push(cmd);
212 }
213 _ => {
214 buckets.scene_commands.push(cmd);
215 }
216 },
217 _ => {
218 buckets.scene_commands.push(cmd);
219 }
220 }
221 }
222
223 // Update bookkeeping.
224 self.last_flatten_generation = self.layer_tree.generation();
225 self.current_damage.frame_generation = self.last_flatten_generation;
226 self.current_damage.dirty_layers.clear();
227 self.current_damage.full_rebuild_needed = false;
228
229 buckets
230 }
231
232 /// Recursively flattens a layer and its children into the command buffer.
233 ///
234 /// Children are processed back-to-front (reverse order) so that
235 /// the frontmost child is drawn last (painter's algorithm).
236 fn flatten_tree(
237 layer_tree: &mut LayerTree,
238 layer_ids: &[LayerId],
239 buffer: &mut Vec<RenderCommand>,
240 z_counter: &mut u32,
241 has_active_shaders: &mut bool,
242 ) {
243 for layer_id in layer_ids {
244 Self::flatten_layer(layer_tree, *layer_id, buffer, z_counter, has_active_shaders);
245 }
246 }
247
248 fn flatten_layer(
249 layer_tree: &mut LayerTree,
250 layer_id: LayerId,
251 buffer: &mut Vec<RenderCommand>,
252 z_counter: &mut u32,
253 has_active_shaders: &mut bool,
254 ) {
255 // Extract layer data first to avoid borrow conflicts with recursive calls.
256 let (material, draw_list, children, bounds, visible) = match layer_tree.get_layer(layer_id) {
257 Some(layer) => {
258 if !layer.visible {
259 return;
260 }
261 (
262 layer.material.clone(),
263 layer.draw_list.clone(),
264 layer.children.clone(),
265 layer.bounds,
266 true,
267 )
268 }
269 None => {
270 warn!(
271 "CompositorEngine: referenced layer {:?} not found in tree",
272 layer_id
273 );
274 return;
275 }
276 };
277
278 let is_offscreen = matches!(material, Material::Isolated | Material::ShaderEffect { .. });
279
280 if is_offscreen {
281 buffer.push(RenderCommand::PushOffscreen {
282 source_layer: layer_id,
283 material: material.clone(),
284 bounds,
285 });
286
287 if matches!(material, Material::ShaderEffect { .. }) {
288 *has_active_shaders = true;
289 }
290 }
291
292 // Push draw commands.
293 for cmd in &draw_list {
294 buffer.push(RenderCommand::Draw(RoutedDrawCommand {
295 command: cmd.clone(),
296 material: material.clone(),
297 source_layer: layer_id,
298 z_index: *z_counter,
299 draw_order: 0,
300 }));
301 *z_counter += 1;
302 }
303
304 // Process children back-to-front (reverse order for painter's algorithm).
305 for child_id in children.iter().rev() {
306 Self::flatten_layer(layer_tree, *child_id, buffer, z_counter, has_active_shaders);
307 }
308
309 if is_offscreen {
310 buffer.push(RenderCommand::PopOffscreen);
311 }
312 }
313
314 /// Returns true if the layer tree has been modified since the last flatten.
315 pub fn needs_reflatten(&self) -> bool {
316 // Only force re-flatten every frame if there are active shaders.
317 // For static content, skip flattening entirely.
318 if self.has_active_shaders {
319 return true;
320 }
321 // Check if any layers were structurally modified or marked dirty.
322 if self.current_damage.full_rebuild_needed {
323 return true;
324 }
325 // Check per-layer dirty stamps — only re-flatten if something actually changed.
326 if !self.current_damage.dirty_layers.is_empty() {
327 return true;
328 }
329 self.layer_tree.generation() > self.last_flatten_generation
330 }
331
332 /// Advances the frame. Call once per frame after rendering.
333 pub fn end_frame(&mut self) {
334 self.layer_tree.advance_generation();
335 }
336
337 /// Clears all layers and resets the engine state.
338 pub fn clear(&mut self) {
339 self.layer_tree.clear();
340 self.flatten_buffer.clear();
341 self.last_flatten_generation = 0;
342 self.current_damage = DamageInfo::default();
343 self.z_counter = 0;
344 self.has_active_shaders = false;
345 }
346}