Skip to main content

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