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
20pub 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
36pub 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 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 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}