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    /// 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}