Skip to main content

fission_shell_desktop/
pipeline.rs

1use anyhow::Result;
2use fission_core::diff::diff_ir;
3use fission_core::env::{VideoStateMap, WebStateMap, Env};
4use fission_core::lowering::{build_layout_tree, LoweringContext};
5use fission_core::{LayoutPoint, ScrollStateMap};
6use fission_diagnostics::{SnapshotBlob, SnapshotKind, SnapshotProvider};
7use fission_diagnostics::prelude as diag;
8use fission_ir::{CoreIR, NodeId, Op, PaintOp, LayoutOp, EmbedKind, WidgetNodeId};
9use fission_layout::{LayoutEngine, LayoutRect, LayoutSnapshot, LayoutUnit, LayoutSize};
10use fission_render::{
11    BoxShadow, Color as RenderColor, DisplayList, DisplayOp, Fill, ImageFit, Renderer, Stroke,
12};
13use fission_shell::VideoSurfaceFrame;
14use serde::{Deserialize, Serialize};
15use std::collections::{HashMap, HashSet};
16use std::fs::File;
17use std::io::Write;
18use std::sync::Arc;
19
20/// The render pipeline that manages incremental IR diffing, layout computation,
21/// paint caching, and display list generation.
22///
23/// Each frame, the pipeline receives a new [`CoreIR`] tree, diffs it against the
24/// previous frame, recomputes layout for dirty subtrees, generates a display list,
25/// and hands it to the renderer.
26pub struct Pipeline {
27    pub prev_ir: Option<CoreIR>, 
28    pub last_snapshot: Option<LayoutSnapshot>,
29    pub paint_cache: HashMap<NodeId, (u64, Vec<DisplayOp>)>,
30    pub video_surfaces: Vec<VideoSurfaceFrame>,
31    pub last_viewport: Option<LayoutRect>,
32    pub layout_invariant_violation_count: u32,
33    pub layout_full_rebuild_count: u32,
34}
35
36/// Per-frame statistics from the render pipeline.
37pub struct PipelineStats {
38    pub dirty_nodes: usize,
39    pub layout_updates: usize,
40    pub paint_misses: usize,
41    pub paint_hits: usize,
42    pub video_surfaces: usize,
43}
44
45impl Pipeline {
46    pub fn new() -> Self {
47        Self {
48            prev_ir: None,
49            last_snapshot: None,
50            paint_cache: HashMap::new(),
51            video_surfaces: Vec::new(),
52            last_viewport: None,
53            layout_invariant_violation_count: 0,
54            layout_full_rebuild_count: 0,
55        }
56    }
57
58    pub fn take_video_surfaces(&mut self) -> Vec<VideoSurfaceFrame> {
59        std::mem::take(&mut self.video_surfaces)
60    }
61
62    pub fn render(
63        &mut self,
64        next_ir: CoreIR,
65        viewport_size: LayoutSize,
66        layout_engine: &mut LayoutEngine,
67        scroll_map: &ScrollStateMap,
68        renderer: &mut dyn Renderer,
69        video_map: &VideoStateMap,
70        web_map: &WebStateMap,
71        env: &Env,
72    ) -> Result<PipelineStats> {
73        let viewport = LayoutRect::new(0.0, 0.0, viewport_size.width, viewport_size.height);
74        let stats = self.update(next_ir, viewport, layout_engine, env, scroll_map, video_map, web_map)?;
75        
76        let snapshot = self.last_snapshot.take().expect("snapshot missing after update");
77        let ir = self.prev_ir.take().expect("ir missing after update");
78        
79        let display_list = self.generate_display_list(&ir, &snapshot, scroll_map, video_map, web_map, viewport);
80        
81        self.last_snapshot = Some(snapshot);
82        self.prev_ir = Some(ir);
83        
84        renderer.render(&display_list)?;
85        Ok(stats)
86    }
87
88    fn generate_display_list(&mut self, ir: &CoreIR, snapshot: &LayoutSnapshot, scroll_map: &ScrollStateMap, video_map: &VideoStateMap, web_map: &WebStateMap, viewport: LayoutRect) -> DisplayList {
89        let mut display_list = DisplayList {
90            ops: Vec::new(),
91            bounds: viewport,
92        };
93
94        let mut visited = HashSet::new();
95        let mut flyout_contents = HashSet::new();
96        for (id, node) in &ir.nodes {
97            if let Op::Layout(LayoutOp::Flyout { content, .. }) = &node.op {
98                flyout_contents.insert(*content);
99            }
100        }
101
102        if let Some(root) = ir.root {
103            self.generate_display_list_recursive_with_visited(
104                root,
105                ir,
106                snapshot,
107                scroll_map,
108                &mut display_list,
109                &mut 0, 
110                &mut 0,
111                video_map,
112                web_map,
113                LayoutPoint::ZERO,
114                &mut visited,
115                &flyout_contents,
116            );
117        }
118        display_list
119    }
120
121    pub fn update(
122        &mut self,
123        next_ir: CoreIR,
124        viewport: LayoutRect,
125        layout_engine: &mut LayoutEngine,
126        env: &Env,
127        scroll_map: &ScrollStateMap,
128        video_map: &VideoStateMap,
129        web_map: &WebStateMap,
130    ) -> Result<PipelineStats> {
131        let mut stats = PipelineStats {
132            dirty_nodes: 0,
133            layout_updates: 0,
134            paint_misses: 0,
135            paint_hits: 0,
136            video_surfaces: 0,
137        };
138
139        let viewport_changed = self.last_viewport.map(|v| v != viewport).unwrap_or(true);
140        self.last_viewport = Some(viewport);
141
142        let mut use_full = false;
143        let mut layout_dirty_closure = HashSet::new();
144
145        if let Some(last_ir) = &self.prev_ir {
146            let diff = fission_core::diff::diff_ir(last_ir, &next_ir);
147            layout_dirty_closure = diff.dirty_structural;
148            stats.dirty_nodes = layout_dirty_closure.len();
149        } else {
150            use_full = true;
151        }
152
153        let needs_layout = !layout_dirty_closure.is_empty() || use_full || viewport_changed;
154
155        // Always clear paint cache — it doesn't track child subtree changes
156        // or layout position changes, so stale entries cause incorrect rendering
157        // after resize, scroll, animation, or any child content change.
158        self.paint_cache.clear();
159
160        if needs_layout {
161            let start_layout = std::time::Instant::now();
162            let layout_input_nodes = build_layout_tree(&next_ir, env);
163            let mut layout_dirty_count = layout_dirty_closure.len();
164
165            if viewport_changed || use_full {
166                let full: HashSet<NodeId> = layout_input_nodes.iter().map(|n| n.id).collect();
167                layout_engine.update(&layout_input_nodes, &full);
168                use_full = true;
169                layout_dirty_count = full.len();
170            } else {
171                layout_engine.update(&layout_input_nodes, &layout_dirty_closure);
172            }
173
174            let root_id = next_ir.root.expect("no root in IR");
175            let snapshot =
176                layout_engine.compute_layout(&layout_input_nodes, root_id, viewport.size, &|id| {
177                    scroll_map.get_offset(id)
178                })?;
179            self.last_snapshot = Some(snapshot);
180            
181            let duration = start_layout.elapsed().as_nanos() as u64;
182            diag::emit(
183                diag::DiagCategory::Layout,
184                diag::DiagLevel::Debug,
185                diag::DiagEventKind::LayoutSummary {
186                    nodes: layout_input_nodes.len() as u32,
187                    dirty_count: layout_dirty_count as u32,
188                    full_rebuild: use_full,
189                    duration_ns: duration,
190                },
191            );
192        }
193
194        let snapshot = self.last_snapshot.take().expect("layout snapshot missing");
195        self.video_surfaces.clear();
196        self.collect_video_surfaces(next_ir.root.expect("root missing"), &next_ir, &snapshot, video_map, scroll_map, LayoutPoint::ZERO);
197        self.last_snapshot = Some(snapshot);
198
199        self.prev_ir = Some(next_ir);
200        stats.video_surfaces = self.video_surfaces.len();
201
202        Ok(stats)
203    }
204
205    fn generate_display_list_recursive_with_visited(
206        &mut self,
207        node_id: NodeId,
208        ir: &CoreIR,
209        snapshot: &LayoutSnapshot,
210        scroll_map: &ScrollStateMap,
211        out_list: &mut DisplayList,
212        miss_count: &mut usize,
213        hit_count: &mut usize,
214        video_map: &VideoStateMap,
215        web_map: &WebStateMap,
216        accumulated_offset: LayoutPoint,
217        visited: &mut HashSet<NodeId>,
218        flyout_contents: &HashSet<NodeId>,
219    ) {
220        if !visited.insert(node_id) {
221            return;
222        }
223
224        if let (Some(node), Some(geom)) = (ir.nodes.get(&node_id), snapshot.nodes.get(&node_id)) {
225            let mut hasher = std::collections::hash_map::DefaultHasher::new();
226            use std::hash::{Hash, Hasher};
227            node.hash.hash(&mut hasher);
228            
229            // Simplified hashing for paint cache: include scroll offset if it's a scroll node
230            if matches!(node.op, Op::Layout(LayoutOp::Scroll { .. })) {
231                let offset = scroll_map.get_offset(node_id);
232                offset.to_bits().hash(&mut hasher);
233            }
234            let hash = hasher.finish();
235
236            if let Some((cached_hash, cached_ops)) = self.paint_cache.get(&node_id) {
237                if *cached_hash == hash {
238                    *hit_count += 1;
239                    out_list.ops.extend(cached_ops.clone());
240                    return;
241                }
242            }
243
244            *miss_count += 1;
245            let mut segment = Vec::new();
246            let mut pushed_state = false;
247            let mut child_offset = accumulated_offset;
248
249            match &node.op {
250                fission_ir::Op::Layout(fission_ir::LayoutOp::Scroll { direction, .. }) => {
251                    let offset = scroll_map.get_offset(node_id);
252                    segment.push(DisplayOp::Save);
253                    segment.push(DisplayOp::ClipRect(geom.rect));
254                    match direction {
255                        fission_ir::FlexDirection::Row => {
256                            segment.push(DisplayOp::Translate(LayoutPoint::new(-offset, 0.0)));
257                            child_offset =
258                                LayoutPoint::new(accumulated_offset.x - offset, accumulated_offset.y);
259                        }
260                        fission_ir::FlexDirection::Column => {
261                            segment.push(DisplayOp::Translate(LayoutPoint::new(0.0, -offset)));
262                            child_offset =
263                                LayoutPoint::new(accumulated_offset.x, accumulated_offset.y - offset);
264                        }
265                    }
266                    pushed_state = true;
267                }
268                fission_ir::Op::Layout(fission_ir::LayoutOp::Clip { path: _ }) => {
269                    segment.push(DisplayOp::Save);
270                    segment.push(DisplayOp::ClipRect(geom.rect));
271                    pushed_state = true;
272                }
273                fission_ir::Op::Layout(fission_ir::LayoutOp::Transform { transform }) => {
274                    segment.push(DisplayOp::Save);
275                    segment.push(DisplayOp::Transform(*transform));
276                    pushed_state = true;
277                }
278                fission_ir::Op::Paint(fission_ir::PaintOp::DrawRect {
279                    fill,
280                    stroke,
281                    corner_radius,
282                    shadow,
283                }) => {
284                    segment.push(DisplayOp::DrawRect {
285                        rect: geom.rect,
286                        fill: fill.as_ref().map(|f| Fill {
287                            color: RenderColor {
288                                r: f.color.r,
289                                g: f.color.g,
290                                b: f.color.b,
291                                a: f.color.a,
292                            },
293                        }),
294                        stroke: stroke.as_ref().map(|s| Stroke {
295                            color: RenderColor {
296                                r: s.color.r,
297                                g: s.color.g,
298                                b: s.color.b,
299                                a: s.color.a,
300                            },
301                            width: s.width,
302                        }),
303                        corner_radius: *corner_radius,
304                        shadow: shadow.as_ref().map(|s| BoxShadow {
305                            color: RenderColor {
306                                r: s.color.r,
307                                g: s.color.g,
308                                b: s.color.b,
309                                a: s.color.a,
310                            },
311                            blur_radius: s.blur_radius,
312                            offset: s.offset,
313                        }),
314                        bounds: geom.rect,
315                        node_id: Some(node_id),
316                    });
317                }
318                fission_ir::Op::Paint(fission_ir::PaintOp::DrawText {
319                    text,
320                    size,
321                    color,
322                    underline,
323                    caret_index,
324                }) => {
325                    segment.push(DisplayOp::DrawText {
326                        text: text.clone(),
327                        position: geom.rect.origin,
328                        size: *size,
329                        color: fission_render::Color {
330                            r: color.r,
331                            g: color.g,
332                            b: color.b,
333                            a: color.a,
334                        },
335                        bounds: geom.rect,
336                        node_id: Some(node_id),
337                        underline: *underline,
338                        caret_index: *caret_index,
339                    });
340                }
341                fission_ir::Op::Paint(fission_ir::PaintOp::DrawRichText { runs, caret_index }) => {
342                    let render_runs = runs
343                        .iter()
344                        .map(|r| fission_render::TextRun {
345                            text: r.text.clone(),
346                            style: fission_render::TextStyle {
347                                font_size: r.style.font_size,
348                                color: fission_render::Color {
349                                    r: r.style.color.r,
350                                    g: r.style.color.g,
351                                    b: r.style.color.b,
352                                    a: r.style.color.a,
353                                },
354                                underline: r.style.underline,
355                                background_color: r.style.background_color.map(|c| fission_render::Color {
356                                    r: c.r, g: c.g, b: c.b, a: c.a,
357                                }),
358                            },
359                        })
360                        .collect();
361
362                    segment.push(DisplayOp::DrawRichText {
363                        runs: render_runs,
364                        position: geom.rect.origin,
365                        bounds: geom.rect,
366                        node_id: Some(node_id),
367                        caret_index: *caret_index,
368                    });
369                }
370                fission_ir::Op::Paint(fission_ir::PaintOp::DrawPath { path, fill, stroke }) => {
371                    segment.push(DisplayOp::DrawPath {
372                        path: path.clone(),
373                        fill: fill.as_ref().map(|f| Fill {
374                            color: RenderColor {
375                                r: f.color.r,
376                                g: f.color.g,
377                                b: f.color.b,
378                                a: f.color.a,
379                            },
380                        }),
381                        stroke: stroke.as_ref().map(|s| Stroke {
382                            color: RenderColor {
383                                r: s.color.r,
384                                g: s.color.g,
385                                b: s.color.b,
386                                a: s.color.a,
387                            },
388                            width: s.width,
389                        }),
390                        bounds: geom.rect,
391                        node_id: Some(node_id),
392                    });
393                }
394                fission_ir::Op::Paint(fission_ir::PaintOp::DrawSvg { content, fill, stroke }) => {
395                    segment.push(DisplayOp::DrawSvg {
396                        content: content.clone(),
397                        fill: fill.as_ref().map(|f| Fill {
398                            color: RenderColor {
399                                r: f.color.r,
400                                g: f.color.g,
401                                b: f.color.b,
402                                a: f.color.a,
403                            },
404                        }),
405                        stroke: stroke.as_ref().map(|s| Stroke {
406                            color: RenderColor {
407                                r: s.color.r,
408                                g: s.color.g,
409                                b: s.color.b,
410                                a: s.color.a,
411                            },
412                            width: s.width,
413                        }),
414                        bounds: geom.rect,
415                        node_id: Some(node_id),
416                    });
417                }
418                _ => {}
419            }
420
421            let mut temp_dl = DisplayList {
422                ops: Vec::new(),
423                bounds: out_list.bounds,
424            };
425
426            for child in &node.children {
427                self.generate_display_list_recursive_with_visited(
428                    *child,
429                    ir,
430                    snapshot,
431                    scroll_map,
432                    &mut temp_dl,
433                    miss_count,
434                    hit_count,
435                    video_map,
436                    web_map,
437                    child_offset,
438                    visited,
439                    flyout_contents,
440                );
441            }
442
443            segment.extend(temp_dl.ops);
444
445            if pushed_state {
446                segment.push(DisplayOp::Restore);
447            }
448
449            self.paint_cache.insert(node_id, (hash, segment.clone()));
450            out_list.ops.extend(segment);
451        }
452    }
453
454    fn push_video_surface(
455        &mut self,
456        widget_id: WidgetNodeId,
457        rect: LayoutRect,
458        video_map: &VideoStateMap,
459    ) {
460        if let Some(state) = video_map.states.get(&widget_id) {
461            let surface_id = state.surface_id.unwrap_or(0);
462            self.video_surfaces.push(VideoSurfaceFrame {
463                widget_id,
464                surface_id,
465                rect,
466            });
467        }
468    }
469
470    fn collect_video_surfaces(
471        &mut self,
472        node_id: NodeId,
473        ir: &CoreIR,
474        snapshot: &LayoutSnapshot,
475        video_map: &VideoStateMap,
476        scroll_map: &ScrollStateMap,
477        accumulated_offset: LayoutPoint,
478    ) {
479        let mut visited = std::collections::HashSet::new();
480        self.collect_video_surfaces_with_visited(
481            node_id,
482            ir,
483            snapshot,
484            video_map,
485            scroll_map,
486            accumulated_offset,
487            &mut visited,
488        );
489    }
490
491    fn collect_video_surfaces_with_visited(
492        &mut self,
493        node_id: NodeId,
494        ir: &CoreIR,
495        snapshot: &LayoutSnapshot,
496        video_map: &VideoStateMap,
497        scroll_map: &ScrollStateMap,
498        accumulated_offset: LayoutPoint,
499        visited: &mut std::collections::HashSet<NodeId>,
500    ) {
501        if !visited.insert(node_id) {
502            return;
503        }
504        if let (Some(node), Some(geom)) = (ir.nodes.get(&node_id), snapshot.nodes.get(&node_id)) {
505            let mut child_offset = accumulated_offset;
506            if let fission_ir::Op::Layout(fission_ir::LayoutOp::Scroll { .. }) = &node.op {
507                let offset = scroll_map.get_offset(node_id);
508                child_offset =
509                    LayoutPoint::new(accumulated_offset.x, accumulated_offset.y - offset);
510            }
511
512            if let fission_ir::Op::Layout(fission_ir::LayoutOp::Embed {
513                kind: EmbedKind::Video,
514                widget_id,
515                ..
516            }) = &node.op
517            {
518                let translated_rect = translate_rect(geom.rect, accumulated_offset);
519                self.push_video_surface(*widget_id, translated_rect, video_map);
520            }
521
522            for child in &node.children {
523                self.collect_video_surfaces_with_visited(
524                    *child,
525                    ir,
526                    snapshot,
527                    video_map,
528                    scroll_map,
529                    child_offset,
530                    visited,
531                );
532            }
533        }
534    }
535}
536
537impl SnapshotProvider for Pipeline {
538    fn snapshot(&self, kind: SnapshotKind) -> Option<SnapshotBlob> {
539        match kind {
540            SnapshotKind::Layout => self.last_snapshot.as_ref().and_then(|snap| {
541                serde_json::to_string_pretty(snap)
542                    .ok()
543                    .map(|json| SnapshotBlob { kind, json })
544            }),
545        }
546    }
547}
548
549fn translate_rect(rect: LayoutRect, offset: LayoutPoint) -> LayoutRect {
550    LayoutRect {
551        origin: LayoutPoint::new(rect.origin.x + offset.x, rect.origin.y + offset.y),
552        size: rect.size,
553    }
554}