1use crate::web_backend::WebSurfaceFrame;
2use anyhow::Result;
3use fission_core::diff::diff_ir;
4use fission_core::env::{AnimationStateMap, Env, VideoStateMap, WebStateMap};
5use fission_core::lowering::build_layout_tree;
6use fission_core::registry::AnimationPropertyId;
7use fission_core::scrollbar::scrollbar_geometry_for_node;
8use fission_core::ui::custom_render::downcast_render_object;
9use fission_core::{LayoutPoint, ScrollStateMap};
10use fission_diagnostics::prelude as diag;
11use fission_diagnostics::{SnapshotBlob, SnapshotKind, SnapshotProvider};
12use fission_ir::{
13 CompositeScalar, CoreIR, EmbedKind, FlexDirection, LayoutOp, NodeId, Op, WidgetNodeId,
14};
15use fission_layout::{LayoutEngine, LayoutInputNode, LayoutRect, LayoutSize, LayoutSnapshot};
16use fission_render::{
17 embed_surface_id, BoxShadow, Color as RenderColor, DisplayList, DisplayOp, Fill, LayerClip,
18 RenderLayer, RenderNode, RenderScene, Renderer, Stroke,
19};
20use fission_shell::VideoSurfaceFrame;
21use serde::Serialize;
22use std::collections::{HashMap, HashSet};
23use std::hash::{Hash, Hasher};
24#[cfg(not(target_arch = "wasm32"))]
25use std::time::Instant;
26#[cfg(target_arch = "wasm32")]
27use web_time::Instant;
28
29fn render_trace_enabled() -> bool {
30 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
31 *ENABLED.get_or_init(|| std::env::var("FISSION_RENDER_TRACE").is_ok())
32}
33
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub struct InvalidationSet {
36 pub build: bool,
37 pub layout: bool,
38 pub paint: bool,
39 pub composite: bool,
40}
41
42impl InvalidationSet {
43 pub fn mark_build(&mut self) {
44 self.build = true;
45 self.layout = true;
46 self.paint = true;
47 self.composite = true;
48 }
49
50 pub fn mark_layout(&mut self) {
51 self.layout = true;
52 self.paint = true;
53 self.composite = true;
54 }
55
56 pub fn mark_paint(&mut self) {
57 self.paint = true;
58 self.composite = true;
59 }
60
61 pub fn mark_composite(&mut self) {
62 self.composite = true;
63 }
64
65 pub fn merge(&mut self, other: Self) {
66 self.build |= other.build;
67 self.layout |= other.layout;
68 self.paint |= other.paint;
69 self.composite |= other.composite;
70 }
71
72 pub fn any(self) -> bool {
73 self.build || self.layout || self.paint || self.composite
74 }
75
76 pub fn highest_class(self) -> &'static str {
77 if self.build {
78 "build"
79 } else if self.layout {
80 "layout"
81 } else if self.paint {
82 "paint"
83 } else if self.composite {
84 "composite"
85 } else {
86 "none"
87 }
88 }
89
90 pub fn labels(self) -> Vec<&'static str> {
91 let mut labels = Vec::new();
92 if self.build {
93 labels.push("build");
94 }
95 if self.layout {
96 labels.push("layout");
97 }
98 if self.paint {
99 labels.push("paint");
100 }
101 if self.composite {
102 labels.push("composite");
103 }
104 if labels.is_empty() {
105 labels.push("none");
106 }
107 labels
108 }
109}
110
111#[derive(Debug, Clone)]
112struct BoundaryCacheEntry {
113 hash: u64,
114 layer: RenderLayer,
115}
116
117#[derive(Debug, Clone)]
118struct OpacityBinding {
119 layer_path: Vec<usize>,
120 scalar: CompositeScalar,
121}
122
123#[derive(Debug, Clone)]
124struct TransformBinding {
125 layer_path: Vec<usize>,
126 rect: LayoutRect,
127 layout_transform: Option<[f32; 16]>,
128 scroll: Option<ScrollTransform>,
129 translate_x: Option<CompositeScalar>,
130 translate_y: Option<CompositeScalar>,
131 scale: Option<CompositeScalar>,
132 rotation: Option<CompositeScalar>,
133}
134
135#[derive(Debug, Clone)]
136struct ScrollbarBinding {
137 node_path: Vec<usize>,
138 node_id: NodeId,
139}
140
141#[derive(Debug, Clone)]
142struct ScrollTransform {
143 node_id: NodeId,
144 direction: FlexDirection,
145}
146
147#[derive(Debug, Clone, Default)]
148struct RetainedDynamicOps {
149 opacity: Vec<OpacityBinding>,
150 transform: Vec<TransformBinding>,
151 scrollbar: Vec<ScrollbarBinding>,
152}
153
154#[derive(Debug, Clone)]
155pub struct CompositorTexturePlan {
156 pub key: u64,
157 pub bounds: LayoutRect,
158 pub scene: Option<RenderScene>,
159 pub scene_cache_key: Option<u64>,
160 pub content_key: u64,
161 pub local_dynamic: bool,
162 pub composite_dynamic: bool,
163 pub opacity: f32,
164 pub transform: Option<[f32; 16]>,
165 pub transform_clip: bool,
166 pub clip: Option<LayerClip>,
167 pub children: Vec<CompositorTexturePlan>,
168 pub source_layer_path: Option<Vec<usize>>,
169}
170
171pub struct Pipeline {
172 pub prev_ir: Option<CoreIR>,
173 pub last_snapshot: Option<LayoutSnapshot>,
174 pub paint_cache: HashMap<NodeId, (u64, DisplayList)>,
175 boundary_cache: HashMap<NodeId, BoundaryCacheEntry>,
176 pub last_scroll_offsets: HashMap<NodeId, u32>,
177 pub video_surfaces: Vec<VideoSurfaceFrame>,
178 pub web_surfaces: Vec<WebSurfaceFrame>,
179 pub scene_3d_surfaces: Vec<(WidgetNodeId, LayoutRect, Vec<u8>)>,
180 pub last_viewport: Option<LayoutRect>,
181 pub layout_invariant_violation_count: u32,
182 pub layout_full_rebuild_count: u32,
183 retained_scene: Option<RenderScene>,
184 retained_dynamic_ops: RetainedDynamicOps,
185 layout_input_nodes: Vec<LayoutInputNode>,
186 pending_layout_dirty_nodes: HashSet<NodeId>,
187 pending_layout_invalidated: bool,
188 pending_layout_full: bool,
189 compositor_animation_keys: HashSet<(WidgetNodeId, AnimationPropertyId)>,
190 runtime_dynamic_nodes: HashSet<NodeId>,
191 scroll_nodes: HashSet<NodeId>,
192 runtime_dynamic_subtrees: HashMap<NodeId, bool>,
193 retained_texture_plans: Vec<CompositorTexturePlan>,
194 retained_texture_root_transform: Option<[f32; 16]>,
195}
196
197pub struct PipelineStats {
198 pub dirty_nodes: usize,
199 pub layout_updates: usize,
200 pub paint_misses: usize,
201 pub paint_hits: usize,
202 pub video_surfaces: usize,
203}
204
205impl Pipeline {
206 pub fn new() -> Self {
207 Self {
208 prev_ir: None,
209 last_snapshot: None,
210 paint_cache: HashMap::new(),
211 boundary_cache: HashMap::new(),
212 last_scroll_offsets: HashMap::new(),
213 video_surfaces: Vec::new(),
214 web_surfaces: Vec::new(),
215 scene_3d_surfaces: Vec::new(),
216 last_viewport: None,
217 layout_invariant_violation_count: 0,
218 layout_full_rebuild_count: 0,
219 retained_scene: None,
220 retained_dynamic_ops: RetainedDynamicOps::default(),
221 layout_input_nodes: Vec::new(),
222 pending_layout_dirty_nodes: HashSet::new(),
223 pending_layout_invalidated: false,
224 pending_layout_full: true,
225 compositor_animation_keys: HashSet::new(),
226 runtime_dynamic_nodes: HashSet::new(),
227 scroll_nodes: HashSet::new(),
228 runtime_dynamic_subtrees: HashMap::new(),
229 retained_texture_plans: Vec::new(),
230 retained_texture_root_transform: None,
231 }
232 }
233
234 pub fn take_video_surfaces(&mut self) -> Vec<VideoSurfaceFrame> {
235 std::mem::take(&mut self.video_surfaces)
236 }
237
238 pub fn take_web_surfaces(&mut self) -> Vec<WebSurfaceFrame> {
239 std::mem::take(&mut self.web_surfaces)
240 }
241
242 pub fn invalidate_layout_all(&mut self) {
243 self.pending_layout_full = true;
244 self.pending_layout_dirty_nodes.clear();
245 }
246
247 pub fn replace_ir(&mut self, next_ir: CoreIR, env: &Env) -> InvalidationSet {
248 let mut invalidation = InvalidationSet::default();
249 let mut rebuild_layout_tree = self.prev_ir.is_none();
250
251 if let Some(prev_ir) = &self.prev_ir {
252 let diff = diff_ir(prev_ir, &next_ir);
253 if !diff.dirty_layout.is_empty() {
254 invalidation.mark_layout();
255 self.pending_layout_invalidated = true;
256 self.pending_layout_dirty_nodes.extend(diff.dirty_layout);
257 }
258 if !diff.dirty_paint.is_empty() {
259 invalidation.mark_paint();
260 }
261 if !diff.dirty_composite.is_empty() {
262 invalidation.mark_composite();
263 }
264 rebuild_layout_tree = rebuild_layout_tree || invalidation.layout;
265 } else {
266 invalidation.mark_build();
267 self.pending_layout_full = true;
268 self.pending_layout_dirty_nodes.clear();
269 }
270
271 if rebuild_layout_tree {
272 self.layout_input_nodes = build_layout_tree(&next_ir, env);
273 }
274
275 if invalidation.layout {
276 self.pending_layout_full |= self.prev_ir.is_none();
277 self.clear_render_caches();
278 } else if invalidation.paint || invalidation.composite {
279 self.clear_render_caches();
280 }
281
282 self.prev_ir = Some(next_ir);
283 self.refresh_retained_metadata();
284 invalidation
285 }
286
287 pub fn classify_animation_updates(
288 &self,
289 changed: &[(WidgetNodeId, AnimationPropertyId)],
290 ) -> InvalidationSet {
291 let mut invalidation = InvalidationSet::default();
292 for key in changed {
293 if self.compositor_animation_keys.contains(key) {
294 invalidation.mark_composite();
295 } else {
296 invalidation.mark_build();
297 }
298 }
299 invalidation
300 }
301
302 pub fn ensure_layout(
303 &mut self,
304 viewport: LayoutRect,
305 layout_engine: &mut LayoutEngine,
306 scroll_map: &ScrollStateMap,
307 ) -> Result<usize> {
308 let viewport_changed = self.last_viewport.map(|v| v != viewport).unwrap_or(true);
309 let needs_full =
310 self.pending_layout_full || self.last_snapshot.is_none() || viewport_changed;
311
312 if !needs_full && !self.pending_layout_invalidated {
313 self.last_viewport = Some(viewport);
314 return Ok(0);
315 }
316
317 let start_layout = Instant::now();
318 let dirty_layout_nodes = if needs_full {
319 self.layout_input_nodes.len()
320 } else {
321 self.pending_layout_dirty_nodes.len()
322 };
323 let (snapshot, full_rebuild) = if needs_full {
324 self.layout_full_rebuild_count = self.layout_full_rebuild_count.saturating_add(1);
325 layout_engine.update(&self.layout_input_nodes);
326 let root_id = self
327 .prev_ir
328 .as_ref()
329 .and_then(|ir| ir.root)
330 .expect("no root in IR");
331 (
332 layout_engine.compute_layout(
333 &self.layout_input_nodes,
334 root_id,
335 viewport.size,
336 &|id| scroll_map.get_offset(id),
337 )?,
338 true,
339 )
340 } else {
341 layout_engine.update(&self.layout_input_nodes);
342 let root_id = self
343 .prev_ir
344 .as_ref()
345 .and_then(|ir| ir.root)
346 .expect("no root in IR");
347 (
348 layout_engine.compute_layout_incremental(
349 &self.layout_input_nodes,
350 root_id,
351 viewport.size,
352 &|id| scroll_map.get_offset(id),
353 self.last_snapshot
354 .as_ref()
355 .expect("incremental layout requires a prior snapshot"),
356 &self.pending_layout_dirty_nodes,
357 )?,
358 false,
359 )
360 };
361 self.last_snapshot = Some(snapshot);
362 self.last_viewport = Some(viewport);
363 self.pending_layout_dirty_nodes.clear();
364 self.pending_layout_invalidated = false;
365 self.pending_layout_full = false;
366 self.clear_render_caches();
367
368 let duration = start_layout.elapsed().as_nanos() as u64;
369 diag::emit(
370 diag::DiagCategory::Layout,
371 diag::DiagLevel::Debug,
372 diag::DiagEventKind::LayoutSummary {
373 nodes: self.layout_input_nodes.len() as u32,
374 dirty_count: dirty_layout_nodes as u32,
375 full_rebuild,
376 duration_ns: duration,
377 },
378 );
379
380 Ok(dirty_layout_nodes)
381 }
382
383 pub fn prepare_current(
384 &mut self,
385 render_viewport_size: LayoutSize,
386 layout_viewport_size: LayoutSize,
387 resize_preview: bool,
388 scroll_map: &ScrollStateMap,
389 animation_map: &AnimationStateMap,
390 video_map: &VideoStateMap,
391 web_map: &WebStateMap,
392 ) -> Result<PipelineStats> {
393 let render_viewport = LayoutRect::new(
394 0.0,
395 0.0,
396 render_viewport_size.width,
397 render_viewport_size.height,
398 );
399 let mut stats = PipelineStats {
400 dirty_nodes: if self.pending_layout_full || self.pending_layout_invalidated {
401 if self.pending_layout_full {
402 self.layout_input_nodes.len()
403 } else {
404 self.pending_layout_dirty_nodes.len()
405 }
406 } else {
407 0
408 },
409 layout_updates: 0,
410 paint_misses: 0,
411 paint_hits: 0,
412 video_surfaces: 0,
413 };
414
415 let ir = self.prev_ir.as_ref().expect("ir missing before render");
416 let snapshot = self
417 .last_snapshot
418 .as_ref()
419 .expect("snapshot missing before render");
420
421 self.video_surfaces.clear();
422 self.web_surfaces.clear();
423 self.scene_3d_surfaces.clear();
424 if let Some(root) = ir.root {
425 collect_video_surfaces(
426 root,
427 ir,
428 snapshot,
429 video_map,
430 web_map,
431 scroll_map,
432 LayoutPoint::ZERO,
433 &mut self.video_surfaces,
434 &mut self.web_surfaces,
435 &mut self.scene_3d_surfaces,
436 );
437 }
438 stats.video_surfaces = self.video_surfaces.len();
439
440 if self.retained_scene.is_none() {
441 if render_trace_enabled() {
442 eprintln!("[pipeline] rebuilding retained render scene");
443 }
444 if let Some(root) = ir.root {
445 let mut visited = HashSet::new();
446 let mut bindings = RetainedDynamicOps::default();
447 let content_root = generate_render_layer_recursive(
448 root,
449 ir,
450 snapshot,
451 scroll_map,
452 animation_map,
453 &mut self.paint_cache,
454 &mut self.boundary_cache,
455 &self.runtime_dynamic_subtrees,
456 &mut stats.paint_misses,
457 &mut stats.paint_hits,
458 true,
459 &mut visited,
460 &mut bindings,
461 vec![0, 0],
462 );
463 if let Some(content_root) = content_root {
464 let mut presentation_root = RenderLayer::new(render_viewport);
465 presentation_root.style.clip = Some(LayerClip::Rect(render_viewport));
466 presentation_root
467 .children
468 .push(RenderNode::Layer(content_root));
469
470 let mut scene = RenderScene::new(render_viewport);
471 scene.roots.push(RenderNode::Layer(presentation_root));
472 self.retained_scene = Some(scene);
473 self.retained_dynamic_ops = bindings;
474 }
475 }
476 }
477
478 self.patch_retained_scene(
479 render_viewport_size,
480 layout_viewport_size,
481 resize_preview,
482 scroll_map,
483 animation_map,
484 );
485 let scene = self
486 .retained_scene
487 .as_ref()
488 .expect("retained render scene missing before render");
489 self.retained_texture_root_transform = scene.roots.first().and_then(|root| match root {
490 RenderNode::Layer(layer) => layer.style.transform,
491 RenderNode::Paint(_) => None,
492 });
493 if self.retained_texture_plans.is_empty() {
494 self.retained_texture_plans = self.build_texture_compositor_plans(scene);
495 } else {
496 patch_texture_compositor_plans(&mut self.retained_texture_plans, scene);
497 }
498
499 diag::emit(
500 diag::DiagCategory::Layout,
501 diag::DiagLevel::Debug,
502 diag::DiagEventKind::PaintSummary {
503 segments_reused: stats.paint_hits as u32,
504 segments_regenerated: stats.paint_misses as u32,
505 paint_ops_total: count_render_paint_ops(scene) as u32,
506 },
507 );
508
509 self.last_scroll_offsets = scroll_map
510 .offsets
511 .iter()
512 .map(|(id, offset)| (*id, offset.to_bits()))
513 .collect();
514
515 Ok(stats)
516 }
517
518 pub fn render_current(
519 &mut self,
520 render_viewport_size: LayoutSize,
521 layout_viewport_size: LayoutSize,
522 resize_preview: bool,
523 renderer: &mut dyn Renderer,
524 scroll_map: &ScrollStateMap,
525 animation_map: &AnimationStateMap,
526 video_map: &VideoStateMap,
527 web_map: &WebStateMap,
528 ) -> Result<PipelineStats> {
529 let stats = self.prepare_current(
530 render_viewport_size,
531 layout_viewport_size,
532 resize_preview,
533 scroll_map,
534 animation_map,
535 video_map,
536 web_map,
537 )?;
538 let scene = self
539 .retained_scene
540 .as_ref()
541 .expect("retained render scene missing before render");
542 renderer.render_scene(scene)?;
543 Ok(stats)
544 }
545
546 pub fn render(
547 &mut self,
548 next_ir: CoreIR,
549 viewport_size: LayoutSize,
550 layout_engine: &mut LayoutEngine,
551 scroll_map: &ScrollStateMap,
552 renderer: &mut dyn Renderer,
553 video_map: &VideoStateMap,
554 web_map: &WebStateMap,
555 env: &Env,
556 ) -> Result<PipelineStats> {
557 self.replace_ir(next_ir, env);
558 let viewport = LayoutRect::new(0.0, 0.0, viewport_size.width, viewport_size.height);
559 let layout_updates = self.ensure_layout(viewport, layout_engine, scroll_map)?;
560 let mut stats = self.render_current(
561 viewport_size,
562 viewport_size,
563 false,
564 renderer,
565 scroll_map,
566 &AnimationStateMap::default(),
567 video_map,
568 web_map,
569 )?;
570 stats.layout_updates = layout_updates;
571 Ok(stats)
572 }
573
574 fn refresh_retained_metadata(&mut self) {
575 self.compositor_animation_keys.clear();
576 self.runtime_dynamic_nodes.clear();
577 self.scroll_nodes.clear();
578 self.runtime_dynamic_subtrees.clear();
579 self.boundary_cache.clear();
580
581 let Some(ir) = self.prev_ir.as_ref() else {
582 return;
583 };
584
585 for node in ir.nodes.values() {
586 let mut node_is_runtime_dynamic =
587 matches!(node.op, Op::Layout(LayoutOp::Scroll { .. }));
588 if matches!(node.op, Op::Layout(LayoutOp::Scroll { .. })) {
589 self.scroll_nodes.insert(node.id);
590 }
591 if ir
592 .custom_render_objects
593 .get(&node.id)
594 .and_then(downcast_render_object)
595 .is_some_and(|render_object| render_object.is_runtime_dynamic())
596 {
597 node_is_runtime_dynamic = true;
598 }
599 if let Some(target) = node
600 .composite
601 .opacity
602 .as_ref()
603 .and_then(|value| value.animation_target)
604 {
605 self.compositor_animation_keys
606 .insert((target, AnimationPropertyId::Opacity));
607 node_is_runtime_dynamic = true;
608 }
609 if let Some(target) = node
610 .composite
611 .translate_x
612 .as_ref()
613 .and_then(|value| value.animation_target)
614 {
615 self.compositor_animation_keys
616 .insert((target, AnimationPropertyId::TranslateX));
617 node_is_runtime_dynamic = true;
618 }
619 if let Some(target) = node
620 .composite
621 .translate_y
622 .as_ref()
623 .and_then(|value| value.animation_target)
624 {
625 self.compositor_animation_keys
626 .insert((target, AnimationPropertyId::TranslateY));
627 node_is_runtime_dynamic = true;
628 }
629 if let Some(target) = node
630 .composite
631 .scale
632 .as_ref()
633 .and_then(|value| value.animation_target)
634 {
635 self.compositor_animation_keys
636 .insert((target, AnimationPropertyId::Scale));
637 node_is_runtime_dynamic = true;
638 }
639 if let Some(target) = node
640 .composite
641 .rotation
642 .as_ref()
643 .and_then(|value| value.animation_target)
644 {
645 self.compositor_animation_keys
646 .insert((target, AnimationPropertyId::Rotation));
647 node_is_runtime_dynamic = true;
648 }
649 if node_is_runtime_dynamic {
650 self.runtime_dynamic_nodes.insert(node.id);
651 }
652 }
653
654 if let Some(root) = ir.root {
655 let mut memo = HashMap::new();
656 let _ = self.compute_runtime_dynamic_subtree(root, ir, &mut memo);
657 self.runtime_dynamic_subtrees = memo;
658 }
659 }
660
661 fn compute_runtime_dynamic_subtree(
662 &self,
663 node_id: NodeId,
664 ir: &CoreIR,
665 memo: &mut HashMap<NodeId, bool>,
666 ) -> bool {
667 if let Some(cached) = memo.get(&node_id) {
668 return *cached;
669 }
670
671 let Some(node) = ir.nodes.get(&node_id) else {
672 memo.insert(node_id, false);
673 return false;
674 };
675
676 let mut dynamic = matches!(node.op, Op::Layout(LayoutOp::Scroll { .. }));
677 dynamic |= node
678 .composite
679 .opacity
680 .as_ref()
681 .and_then(|value| value.animation_target)
682 .is_some();
683 dynamic |= node
684 .composite
685 .translate_x
686 .as_ref()
687 .and_then(|value| value.animation_target)
688 .is_some();
689 dynamic |= node
690 .composite
691 .translate_y
692 .as_ref()
693 .and_then(|value| value.animation_target)
694 .is_some();
695 dynamic |= node
696 .composite
697 .scale
698 .as_ref()
699 .and_then(|value| value.animation_target)
700 .is_some();
701 dynamic |= node
702 .composite
703 .rotation
704 .as_ref()
705 .and_then(|value| value.animation_target)
706 .is_some();
707
708 for child in &node.children {
709 dynamic |= self.compute_runtime_dynamic_subtree(*child, ir, memo);
710 }
711
712 memo.insert(node_id, dynamic);
713 dynamic
714 }
715
716 fn clear_render_caches(&mut self) {
717 if render_trace_enabled() {
718 eprintln!(
719 "[pipeline] clear_render_caches layout_full={} layout_invalidated={} retained_was_present={}",
720 self.pending_layout_full,
721 self.pending_layout_invalidated,
722 self.retained_scene.is_some()
723 );
724 }
725 self.paint_cache.clear();
726 self.boundary_cache.clear();
727 self.retained_scene = None;
728 self.retained_dynamic_ops = RetainedDynamicOps::default();
729 self.retained_texture_plans.clear();
730 self.retained_texture_root_transform = None;
731 }
732
733 fn patch_retained_scene(
734 &mut self,
735 render_viewport_size: LayoutSize,
736 layout_viewport_size: LayoutSize,
737 resize_preview: bool,
738 scroll_map: &ScrollStateMap,
739 animation_map: &AnimationStateMap,
740 ) {
741 let Some(scene) = self.retained_scene.as_mut() else {
742 return;
743 };
744
745 scene.bounds = LayoutRect::new(
746 0.0,
747 0.0,
748 render_viewport_size.width,
749 render_viewport_size.height,
750 );
751 let scene_bounds = scene.bounds;
752 if let Some(presentation_layer) = layer_mut_at_path(scene, &[0]) {
753 presentation_layer.bounds = scene_bounds;
754 presentation_layer.style.clip = Some(LayerClip::Rect(scene_bounds));
755 presentation_layer.style.transform = presentation_transform_matrix(
756 render_viewport_size,
757 layout_viewport_size,
758 resize_preview,
759 );
760 }
761
762 for binding in &self.retained_dynamic_ops.opacity {
763 let alpha =
764 resolve_scalar_value(&binding.scalar, animation_map, AnimationPropertyId::Opacity);
765 if let Some(layer) = layer_mut_at_path(scene, &binding.layer_path) {
766 layer.style.opacity = alpha;
767 }
768 }
769
770 for binding in &self.retained_dynamic_ops.transform {
771 if let Some(layer) = layer_mut_at_path(scene, &binding.layer_path) {
772 layer.style.transform =
773 compose_dynamic_layer_transform(binding, scroll_map, animation_map);
774 }
775 }
776
777 let Some(ir) = self.prev_ir.as_ref() else {
778 return;
779 };
780 let Some(snapshot) = self.last_snapshot.as_ref() else {
781 return;
782 };
783 for binding in &self.retained_dynamic_ops.scrollbar {
784 let Some(scrollbar) = build_scrollbar_paint(ir, binding.node_id, snapshot, scroll_map)
785 else {
786 continue;
787 };
788 if let Some(RenderNode::Paint(list)) =
789 render_node_mut_at_path(scene, &binding.node_path)
790 {
791 *list = scrollbar;
792 }
793 }
794 }
795
796 pub fn retained_scene(&self) -> Option<&RenderScene> {
797 self.retained_scene.as_ref()
798 }
799
800 pub fn texture_compositor_plans(&self) -> &[CompositorTexturePlan] {
801 &self.retained_texture_plans
802 }
803
804 pub fn texture_compositor_root_transform(&self) -> Option<[f32; 16]> {
805 self.retained_texture_root_transform
806 }
807
808 fn build_texture_compositor_plans(&self, scene: &RenderScene) -> Vec<CompositorTexturePlan> {
809 let Some(split_layer_path) = find_texture_compositor_split_layer_path(scene) else {
810 return Vec::new();
811 };
812 let Some(split_layer) = layer_ref_at_path(scene, &split_layer_path) else {
813 return Vec::new();
814 };
815 let mut plans = Vec::new();
816 for (child_index, child) in split_layer.children.iter().enumerate() {
817 let mut child_path = split_layer_path.clone();
818 child_path.push(child_index);
819 if let Some(plan) = build_texture_plan_from_node(
820 child,
821 &child_path,
822 true,
823 &self.runtime_dynamic_nodes,
824 &self.scroll_nodes,
825 &self.runtime_dynamic_subtrees,
826 ) {
827 plans.push(plan);
828 }
829 }
830 if render_trace_enabled() {
831 for plan in &plans {
832 log_texture_plan(plan, 0);
833 }
834 }
835 plans
836 }
837}
838
839fn log_texture_plan(plan: &CompositorTexturePlan, depth: usize) {
840 let indent = " ".repeat(depth);
841 eprintln!(
842 "[pipeline] {}plan key={} bounds=({}, {}, {}x{}) scene={} clip={} transform=({:.1},{:.1}) transform_clip={} children={}",
843 indent,
844 plan.key,
845 plan.bounds.origin.x,
846 plan.bounds.origin.y,
847 plan.bounds.size.width,
848 plan.bounds.size.height,
849 plan.scene.is_some(),
850 plan.clip.is_some(),
851 plan.transform.map(|m| m[12]).unwrap_or(0.0),
852 plan.transform.map(|m| m[13]).unwrap_or(0.0),
853 plan.transform_clip,
854 plan.children.len()
855 );
856 for child in &plan.children {
857 log_texture_plan(child, depth + 1);
858 }
859}
860
861fn layer_mut_at_path<'a>(
862 scene: &'a mut RenderScene,
863 path: &[usize],
864) -> Option<&'a mut RenderLayer> {
865 let (root_index, tail) = path.split_first()?;
866 let node = scene.roots.get_mut(*root_index)?;
867 layer_mut_in_node(node, tail)
868}
869
870fn render_node_mut_at_path<'a>(
871 scene: &'a mut RenderScene,
872 path: &[usize],
873) -> Option<&'a mut RenderNode> {
874 let (root_index, tail) = path.split_first()?;
875 let node = scene.roots.get_mut(*root_index)?;
876 render_node_mut_in_node(node, tail)
877}
878
879fn render_node_mut_in_node<'a>(
880 node: &'a mut RenderNode,
881 path: &[usize],
882) -> Option<&'a mut RenderNode> {
883 if path.is_empty() {
884 return Some(node);
885 }
886 match node {
887 RenderNode::Layer(layer) => {
888 let (child_index, tail) = path.split_first()?;
889 let child = layer.children.get_mut(*child_index)?;
890 render_node_mut_in_node(child, tail)
891 }
892 RenderNode::Paint(_) => None,
893 }
894}
895
896fn layer_mut_in_node<'a>(node: &'a mut RenderNode, path: &[usize]) -> Option<&'a mut RenderLayer> {
897 match node {
898 RenderNode::Layer(layer) => {
899 if path.is_empty() {
900 return Some(layer);
901 }
902 let (child_index, tail) = path.split_first()?;
903 let child = layer.children.get_mut(*child_index)?;
904 layer_mut_in_node(child, tail)
905 }
906 RenderNode::Paint(_) => None,
907 }
908}
909
910fn layer_ref_at_path<'a>(scene: &'a RenderScene, path: &[usize]) -> Option<&'a RenderLayer> {
911 let (root_index, tail) = path.split_first()?;
912 let node = scene.roots.get(*root_index)?;
913 layer_ref_in_node(node, tail)
914}
915
916fn layer_ref_in_node<'a>(node: &'a RenderNode, path: &[usize]) -> Option<&'a RenderLayer> {
917 match node {
918 RenderNode::Layer(layer) => {
919 if path.is_empty() {
920 return Some(layer);
921 }
922 let (child_index, tail) = path.split_first()?;
923 let child = layer.children.get(*child_index)?;
924 layer_ref_in_node(child, tail)
925 }
926 RenderNode::Paint(_) => None,
927 }
928}
929
930fn count_render_paint_ops(scene: &RenderScene) -> usize {
931 scene.roots.iter().map(count_render_node_paint_ops).sum()
932}
933
934fn count_render_node_paint_ops(node: &RenderNode) -> usize {
935 match node {
936 RenderNode::Paint(list) => list.ops.len(),
937 RenderNode::Layer(layer) => layer.children.iter().map(count_render_node_paint_ops).sum(),
938 }
939}
940
941fn render_node_bounds(node: &RenderNode) -> LayoutRect {
942 match node {
943 RenderNode::Paint(list) => list.bounds,
944 RenderNode::Layer(layer) => layer.bounds,
945 }
946}
947
948fn find_texture_compositor_split_layer_path(scene: &RenderScene) -> Option<Vec<usize>> {
949 let Some(RenderNode::Layer(presentation_root)) = scene.roots.first() else {
950 return None;
951 };
952 if presentation_root.children.len() != 1 {
953 return None;
954 }
955 let Some(RenderNode::Layer(layer)) = presentation_root.children.first() else {
956 return None;
957 };
958 let mut layer = layer;
959 let mut path = vec![0, 0];
960 loop {
961 let only_child = match layer.children.as_slice() {
962 [RenderNode::Layer(child)] => Some(child),
963 _ => None,
964 };
965 let is_plain_wrapper = layer.style.clip.is_none()
966 && (layer.style.opacity - 1.0).abs() <= 0.001
967 && layer.style.transform.is_none();
968 if let (true, Some(child)) = (is_plain_wrapper, only_child) {
969 layer = child;
970 path.push(0);
971 } else {
972 return Some(path);
973 }
974 }
975}
976
977#[derive(Debug)]
978struct TexturePlanCandidate<'a> {
979 node: &'a RenderNode,
980 path: Vec<usize>,
981}
982
983fn build_texture_plan_from_node(
984 node: &RenderNode,
985 node_path: &[usize],
986 force: bool,
987 runtime_dynamic_nodes: &HashSet<NodeId>,
988 scroll_nodes: &HashSet<NodeId>,
989 runtime_dynamic_subtrees: &HashMap<NodeId, bool>,
990) -> Option<CompositorTexturePlan> {
991 let candidate = find_nested_texture_plan_candidate(
992 node,
993 node_path,
994 force,
995 runtime_dynamic_nodes,
996 scroll_nodes,
997 runtime_dynamic_subtrees,
998 )?;
999 let bounds = render_node_bounds(candidate.node);
1000 if bounds.size.width <= 0.0 || bounds.size.height <= 0.0 {
1001 return None;
1002 }
1003
1004 match candidate.node {
1005 RenderNode::Paint(list) => {
1006 let scene = localized_scene_for_compositor_children(
1007 vec![RenderNode::Paint(list.clone())],
1008 bounds,
1009 );
1010 let scene_cache_key = scene_cache_key(&scene);
1011 let content_key = plan_content_key(Some(scene_cache_key), &[]);
1012 Some(CompositorTexturePlan {
1013 key: texture_plan_key_for_paint(list),
1014 bounds,
1015 scene: Some(scene),
1016 scene_cache_key: Some(scene_cache_key),
1017 content_key,
1018 local_dynamic: false,
1019 composite_dynamic: false,
1020 opacity: 1.0,
1021 transform: None,
1022 transform_clip: true,
1023 clip: None,
1024 children: Vec::new(),
1025 source_layer_path: None,
1026 })
1027 }
1028 RenderNode::Layer(layer) => {
1029 let wrapper_only_scroll_plan = !layer.style.transform_clip;
1030 let mut child_plans = Vec::new();
1031 let mut local_children = Vec::new();
1032 for (child_index, child) in layer.children.iter().enumerate() {
1033 let mut child_path = candidate.path.clone();
1034 child_path.push(child_index);
1035 if wrapper_only_scroll_plan {
1036 child_plans.extend(build_descending_wrapper_plans(
1037 child,
1038 &child_path,
1039 runtime_dynamic_nodes,
1040 scroll_nodes,
1041 runtime_dynamic_subtrees,
1042 ));
1043 } else {
1044 if let Some(child_plan) = build_texture_plan_from_node(
1045 child,
1046 &child_path,
1047 false,
1048 runtime_dynamic_nodes,
1049 scroll_nodes,
1050 runtime_dynamic_subtrees,
1051 ) {
1052 child_plans.push(child_plan);
1053 } else {
1054 local_children.push(child.clone());
1055 }
1056 }
1057 }
1058
1059 let local_dynamic = local_children
1060 .iter()
1061 .any(|child| render_node_or_subtree_is_dynamic(child, runtime_dynamic_subtrees));
1062 let scene = if local_children.is_empty() {
1063 None
1064 } else {
1065 Some(localized_scene_for_compositor_children(
1066 local_children,
1067 bounds,
1068 ))
1069 };
1070 let scene_cache_key = if scene.is_none() {
1071 None
1072 } else {
1073 layer
1074 .style
1075 .content_cache_key
1076 .or(layer.style.cache_key)
1077 .or_else(|| scene.as_ref().map(scene_cache_key))
1078 };
1079 let content_key = plan_content_key(scene_cache_key, &child_plans);
1080 let composite_dynamic = layer
1081 .node_id
1082 .map(|id| runtime_dynamic_nodes.contains(&id))
1083 .unwrap_or(false);
1084 Some(CompositorTexturePlan {
1085 key: texture_plan_key_for_layer(layer),
1086 bounds,
1087 scene,
1088 scene_cache_key,
1089 content_key,
1090 local_dynamic,
1091 composite_dynamic,
1092 opacity: layer.style.opacity,
1093 transform: layer.style.transform,
1094 transform_clip: layer.style.transform_clip,
1095 clip: layer.style.clip.clone(),
1096 children: child_plans,
1097 source_layer_path: Some(candidate.path),
1098 })
1099 }
1100 }
1101}
1102
1103fn build_descending_wrapper_plans(
1104 node: &RenderNode,
1105 node_path: &[usize],
1106 runtime_dynamic_nodes: &HashSet<NodeId>,
1107 scroll_nodes: &HashSet<NodeId>,
1108 runtime_dynamic_subtrees: &HashMap<NodeId, bool>,
1109) -> Vec<CompositorTexturePlan> {
1110 match node {
1111 RenderNode::Paint(_) => build_texture_plan_from_node(
1112 node,
1113 node_path,
1114 true,
1115 runtime_dynamic_nodes,
1116 scroll_nodes,
1117 runtime_dynamic_subtrees,
1118 )
1119 .into_iter()
1120 .collect(),
1121 RenderNode::Layer(layer) => {
1122 let mut children = Vec::new();
1123 for (child_index, child) in layer.children.iter().enumerate() {
1124 let mut child_path = node_path.to_vec();
1125 child_path.push(child_index);
1126 children.extend(build_descending_wrapper_plans(
1127 child,
1128 &child_path,
1129 runtime_dynamic_nodes,
1130 scroll_nodes,
1131 runtime_dynamic_subtrees,
1132 ));
1133 }
1134
1135 if children.is_empty() {
1136 return build_texture_plan_from_node(
1137 node,
1138 node_path,
1139 true,
1140 runtime_dynamic_nodes,
1141 scroll_nodes,
1142 runtime_dynamic_subtrees,
1143 )
1144 .into_iter()
1145 .collect();
1146 }
1147
1148 let composite_dynamic = layer
1149 .node_id
1150 .map(|id| runtime_dynamic_nodes.contains(&id))
1151 .unwrap_or(false);
1152 vec![CompositorTexturePlan {
1153 key: texture_plan_key_for_layer(layer),
1154 bounds: layer.bounds,
1155 scene: None,
1156 scene_cache_key: None,
1157 content_key: plan_content_key(None, &children),
1158 local_dynamic: false,
1159 composite_dynamic,
1160 opacity: layer.style.opacity,
1161 transform: layer.style.transform,
1162 transform_clip: layer.style.transform_clip,
1163 clip: layer.style.clip.clone(),
1164 children,
1165 source_layer_path: Some(node_path.to_vec()),
1166 }]
1167 }
1168 }
1169}
1170
1171fn find_nested_texture_plan_candidate<'a>(
1172 node: &'a RenderNode,
1173 node_path: &[usize],
1174 force: bool,
1175 runtime_dynamic_nodes: &HashSet<NodeId>,
1176 scroll_nodes: &HashSet<NodeId>,
1177 runtime_dynamic_subtrees: &HashMap<NodeId, bool>,
1178) -> Option<TexturePlanCandidate<'a>> {
1179 match node {
1180 RenderNode::Paint(_) => force.then_some(TexturePlanCandidate {
1181 node,
1182 path: node_path.to_vec(),
1183 }),
1184 RenderNode::Layer(layer) => {
1185 if !force {
1186 if let Some(child) = descend_through_plain_wrapper(layer) {
1187 let mut child_path = node_path.to_vec();
1188 child_path.push(0);
1189 return find_nested_texture_plan_candidate(
1190 child,
1191 &child_path,
1192 false,
1193 runtime_dynamic_nodes,
1194 scroll_nodes,
1195 runtime_dynamic_subtrees,
1196 );
1197 }
1198 }
1199
1200 let subtree_dynamic = render_node_or_subtree_is_dynamic(node, runtime_dynamic_subtrees);
1201 let own_dynamic = layer
1202 .node_id
1203 .map(|id| runtime_dynamic_nodes.contains(&id))
1204 .unwrap_or(false);
1205 let is_scroll_node = layer
1206 .node_id
1207 .map(|id| scroll_nodes.contains(&id))
1208 .unwrap_or(false);
1209 if force
1210 || layer_should_extract_as_plan(layer, subtree_dynamic, own_dynamic, is_scroll_node)
1211 {
1212 Some(TexturePlanCandidate {
1213 node,
1214 path: node_path.to_vec(),
1215 })
1216 } else {
1217 for (child_index, child) in layer.children.iter().enumerate() {
1218 let mut child_path = node_path.to_vec();
1219 child_path.push(child_index);
1220 if let Some(candidate) = find_nested_texture_plan_candidate(
1221 child,
1222 &child_path,
1223 false,
1224 runtime_dynamic_nodes,
1225 scroll_nodes,
1226 runtime_dynamic_subtrees,
1227 ) {
1228 return Some(candidate);
1229 }
1230 }
1231 None
1232 }
1233 }
1234 }
1235}
1236
1237fn descend_through_plain_wrapper<'a>(layer: &'a RenderLayer) -> Option<&'a RenderNode> {
1238 let only_child = match layer.children.as_slice() {
1239 [child] => Some(child),
1240 _ => None,
1241 }?;
1242 if layer.style.clip.is_none()
1243 && (layer.style.opacity - 1.0).abs() <= 0.001
1244 && layer.style.transform.is_none()
1245 {
1246 match only_child {
1247 RenderNode::Layer(_) => Some(only_child),
1248 RenderNode::Paint(_) => None,
1249 }
1250 } else {
1251 None
1252 }
1253}
1254
1255fn layer_should_extract_as_plan(
1256 layer: &RenderLayer,
1257 subtree_dynamic: bool,
1258 own_dynamic: bool,
1259 is_scroll_node: bool,
1260) -> bool {
1261 const MIN_PLAN_AREA: f32 = 64.0 * 64.0;
1262 if layer.children.is_empty() {
1263 return false;
1264 }
1265 if is_scroll_node {
1266 return false;
1267 }
1268 if own_dynamic {
1269 return true;
1270 }
1271 if !subtree_dynamic {
1272 return false;
1273 }
1274 let has_style = layer.style.clip.is_some()
1275 || (layer.style.opacity - 1.0).abs() > 0.001
1276 || layer.style.transform.is_some();
1277 let has_local_paint = layer
1278 .children
1279 .iter()
1280 .any(|child| matches!(child, RenderNode::Paint(_)));
1281 let has_multiple_children = layer.children.len() > 1;
1282 (has_style || has_local_paint || has_multiple_children)
1283 && layer.bounds.size.width * layer.bounds.size.height >= MIN_PLAN_AREA
1284}
1285
1286fn localized_scene_for_compositor_children(
1287 children: Vec<RenderNode>,
1288 bounds: LayoutRect,
1289) -> RenderScene {
1290 let local_bounds = LayoutRect::new(0.0, 0.0, bounds.size.width, bounds.size.height);
1291 let mut root = RenderLayer::new(local_bounds);
1292 root.style.transform = Some(translation_matrix(-bounds.origin.x, -bounds.origin.y));
1293 root.children.extend(children);
1294
1295 let mut scene = RenderScene::new(local_bounds);
1296 scene.roots.push(RenderNode::Layer(root));
1297 scene
1298}
1299
1300fn render_node_or_subtree_is_dynamic(
1301 node: &RenderNode,
1302 runtime_dynamic_subtrees: &HashMap<NodeId, bool>,
1303) -> bool {
1304 match node {
1305 RenderNode::Paint(_) => false,
1306 RenderNode::Layer(layer) => {
1307 layer
1308 .node_id
1309 .and_then(|id| runtime_dynamic_subtrees.get(&id).copied())
1310 .unwrap_or(false)
1311 || layer
1312 .children
1313 .iter()
1314 .any(|child| render_node_or_subtree_is_dynamic(child, runtime_dynamic_subtrees))
1315 }
1316 }
1317}
1318
1319fn texture_plan_key_for_layer(layer: &RenderLayer) -> u64 {
1320 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1321 layer.node_id.hash(&mut hasher);
1322 layer.bounds.size.width.to_bits().hash(&mut hasher);
1323 layer.bounds.size.height.to_bits().hash(&mut hasher);
1324 hash_serde_value(&layer.style.clip, &mut hasher);
1325 hasher.finish()
1326}
1327
1328fn texture_plan_key_for_paint(list: &DisplayList) -> u64 {
1329 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1330 list.bounds.size.width.to_bits().hash(&mut hasher);
1331 list.bounds.size.height.to_bits().hash(&mut hasher);
1332 hash_serde_value(list, &mut hasher);
1333 hasher.finish()
1334}
1335
1336fn scene_cache_key(scene: &RenderScene) -> u64 {
1337 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1338 hash_serde_value(scene, &mut hasher);
1339 hasher.finish()
1340}
1341
1342fn plan_content_key(scene_cache_key: Option<u64>, children: &[CompositorTexturePlan]) -> u64 {
1343 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1344 scene_cache_key.hash(&mut hasher);
1345 for child in children {
1346 child.key.hash(&mut hasher);
1347 child.content_key.hash(&mut hasher);
1348 child.bounds.origin.x.to_bits().hash(&mut hasher);
1349 child.bounds.origin.y.to_bits().hash(&mut hasher);
1350 child.bounds.size.width.to_bits().hash(&mut hasher);
1351 child.bounds.size.height.to_bits().hash(&mut hasher);
1352 child.opacity.to_bits().hash(&mut hasher);
1353 hash_serde_value(&child.transform, &mut hasher);
1354 hash_serde_value(&child.clip, &mut hasher);
1355 }
1356 hasher.finish()
1357}
1358
1359fn patch_texture_compositor_plans(plans: &mut [CompositorTexturePlan], scene: &RenderScene) {
1360 for plan in plans {
1361 patch_texture_compositor_plan(plan, scene);
1362 }
1363}
1364
1365fn patch_texture_compositor_plan(plan: &mut CompositorTexturePlan, scene: &RenderScene) {
1366 for child in &mut plan.children {
1367 patch_texture_compositor_plan(child, scene);
1368 }
1369
1370 if let Some(path) = plan.source_layer_path.as_deref() {
1371 if let Some(layer) = layer_ref_at_path(scene, path) {
1372 plan.bounds = layer.bounds;
1373 plan.opacity = layer.style.opacity;
1374 plan.transform = layer.style.transform;
1375 plan.transform_clip = layer.style.transform_clip;
1376 plan.clip = layer.style.clip.clone();
1377 }
1378 }
1379
1380 plan.content_key = plan_content_key(plan.scene_cache_key, &plan.children);
1381}
1382
1383fn hash_serde_value<T: Serialize, H: Hasher>(value: &T, hasher: &mut H) {
1384 if let Ok(bytes) = bincode::serialize(value) {
1385 bytes.hash(hasher);
1386 }
1387}
1388
1389fn presentation_transform_matrix(
1390 render_viewport_size: LayoutSize,
1391 layout_viewport_size: LayoutSize,
1392 resize_preview: bool,
1393) -> Option<[f32; 16]> {
1394 if !resize_preview
1395 || render_viewport_size.width <= 0.0
1396 || render_viewport_size.height <= 0.0
1397 || layout_viewport_size.width <= 0.0
1398 || layout_viewport_size.height <= 0.0
1399 {
1400 return None;
1401 }
1402
1403 None
1407}
1408
1409fn compose_dynamic_layer_transform(
1410 binding: &TransformBinding,
1411 scroll_map: &ScrollStateMap,
1412 animation_map: &AnimationStateMap,
1413) -> Option<[f32; 16]> {
1414 let mut matrix: Option<[f32; 16]> = None;
1415
1416 if let Some(scroll) = &binding.scroll {
1417 let offset = scroll_map.get_offset(scroll.node_id);
1418 let scroll_matrix = match scroll.direction {
1419 FlexDirection::Row => translation_matrix(-offset, 0.0),
1420 FlexDirection::Column => translation_matrix(0.0, -offset),
1421 };
1422 matrix = append_transform(matrix, scroll_matrix);
1423 }
1424
1425 if let Some(layout_transform) = binding.layout_transform {
1426 matrix = append_transform(matrix, layout_transform);
1427 }
1428
1429 let translate_x = binding
1430 .translate_x
1431 .as_ref()
1432 .map(|scalar| resolve_scalar_value(scalar, animation_map, AnimationPropertyId::TranslateX))
1433 .unwrap_or(0.0);
1434 let translate_y = binding
1435 .translate_y
1436 .as_ref()
1437 .map(|scalar| resolve_scalar_value(scalar, animation_map, AnimationPropertyId::TranslateY))
1438 .unwrap_or(0.0);
1439 let scale = binding
1440 .scale
1441 .as_ref()
1442 .map(|scalar| resolve_scalar_value(scalar, animation_map, AnimationPropertyId::Scale))
1443 .unwrap_or(1.0);
1444 let rotation = binding
1445 .rotation
1446 .as_ref()
1447 .map(|scalar| resolve_scalar_value(scalar, animation_map, AnimationPropertyId::Rotation))
1448 .unwrap_or(0.0);
1449
1450 let has_composite_transform = translate_x.abs() > 0.001
1451 || translate_y.abs() > 0.001
1452 || (scale - 1.0).abs() > 0.001
1453 || rotation.abs() > 0.001;
1454 if has_composite_transform {
1455 matrix = append_transform(
1456 matrix,
1457 composite_transform_matrix(binding.rect, translate_x, translate_y, scale, rotation),
1458 );
1459 }
1460
1461 matrix.filter(|value| !is_identity_matrix(value))
1462}
1463
1464fn append_transform(current: Option<[f32; 16]>, next: [f32; 16]) -> Option<[f32; 16]> {
1465 Some(match current {
1466 Some(existing) => multiply_matrix(existing, next),
1467 None => next,
1468 })
1469}
1470
1471fn generate_render_layer_recursive(
1472 node_id: NodeId,
1473 ir: &CoreIR,
1474 snapshot: &LayoutSnapshot,
1475 scroll_map: &ScrollStateMap,
1476 animation_map: &AnimationStateMap,
1477 paint_cache: &mut HashMap<NodeId, (u64, DisplayList)>,
1478 boundary_cache: &mut HashMap<NodeId, BoundaryCacheEntry>,
1479 runtime_dynamic_subtrees: &HashMap<NodeId, bool>,
1480 miss_count: &mut usize,
1481 hit_count: &mut usize,
1482 scene_cache_allowed: bool,
1483 visited: &mut HashSet<NodeId>,
1484 bindings: &mut RetainedDynamicOps,
1485 layer_path: Vec<usize>,
1486) -> Option<RenderLayer> {
1487 if !visited.insert(node_id) {
1488 return None;
1489 }
1490
1491 let (Some(node), Some(geom)) = (ir.nodes.get(&node_id), snapshot.nodes.get(&node_id)) else {
1492 return None;
1493 };
1494
1495 let rect = geom.rect;
1496 let can_use_boundary_cache = !runtime_dynamic_subtrees
1497 .get(&node_id)
1498 .copied()
1499 .unwrap_or(false);
1500
1501 let scene_cache_key = boundary_hash(node, rect);
1502 let can_cache_scene = scene_cache_allowed && can_use_boundary_cache && node.parent.is_some();
1503 if can_cache_scene {
1504 if let Some(entry) = boundary_cache.get(&node_id) {
1505 if entry.hash == scene_cache_key {
1506 *hit_count += 1;
1507 return Some(entry.layer.clone());
1508 }
1509 }
1510 } else if can_use_boundary_cache {
1511 if let Some(entry) = boundary_cache.get(&node_id) {
1512 if entry.hash == scene_cache_key {
1513 *hit_count += 1;
1514 return Some(entry.layer.clone());
1515 }
1516 }
1517 }
1518
1519 let composite_opacity = resolve_composite_scalar(
1520 node.composite.opacity.as_ref(),
1521 animation_map,
1522 AnimationPropertyId::Opacity,
1523 );
1524 let composite_tx = resolve_composite_scalar(
1525 node.composite.translate_x.as_ref(),
1526 animation_map,
1527 AnimationPropertyId::TranslateX,
1528 );
1529 let composite_ty = resolve_composite_scalar(
1530 node.composite.translate_y.as_ref(),
1531 animation_map,
1532 AnimationPropertyId::TranslateY,
1533 );
1534 let composite_scale = resolve_composite_scalar(
1535 node.composite.scale.as_ref(),
1536 animation_map,
1537 AnimationPropertyId::Scale,
1538 )
1539 .unwrap_or(1.0);
1540 let composite_rotation = resolve_composite_scalar(
1541 node.composite.rotation.as_ref(),
1542 animation_map,
1543 AnimationPropertyId::Rotation,
1544 )
1545 .unwrap_or(0.0);
1546
1547 let _has_composite_transform = composite_tx.unwrap_or(0.0).abs() > 0.001
1548 || composite_ty.unwrap_or(0.0).abs() > 0.001
1549 || (composite_scale - 1.0).abs() > 0.001
1550 || composite_rotation.abs() > 0.001;
1551 let has_opacity_layer = composite_opacity
1552 .map(|value| (value - 1.0).abs() > 0.001)
1553 .unwrap_or(false);
1554 let needs_dynamic_opacity = node
1555 .composite
1556 .opacity
1557 .as_ref()
1558 .and_then(|value| value.animation_target)
1559 .is_some();
1560 let needs_dynamic_transform = node
1561 .composite
1562 .translate_x
1563 .as_ref()
1564 .and_then(|value| value.animation_target)
1565 .is_some()
1566 || node
1567 .composite
1568 .translate_y
1569 .as_ref()
1570 .and_then(|value| value.animation_target)
1571 .is_some()
1572 || node
1573 .composite
1574 .scale
1575 .as_ref()
1576 .and_then(|value| value.animation_target)
1577 .is_some()
1578 || node
1579 .composite
1580 .rotation
1581 .as_ref()
1582 .and_then(|value| value.animation_target)
1583 .is_some();
1584 let emit_opacity_layer = has_opacity_layer || needs_dynamic_opacity;
1585 let has_runtime_clip = node.composite.clip_to_bounds;
1586 let scroll = match &node.op {
1587 Op::Layout(LayoutOp::Scroll { direction, .. }) => Some(ScrollTransform {
1588 node_id,
1589 direction: *direction,
1590 }),
1591 _ => None,
1592 };
1593 let layout_transform = match &node.op {
1594 Op::Layout(LayoutOp::Transform { transform }) => Some(*transform),
1595 _ => None,
1596 };
1597 let has_own_transform = needs_dynamic_transform || layout_transform.is_some();
1598 let has_dynamic_transform = has_own_transform || scroll.is_some();
1599 let has_dynamic_style = emit_opacity_layer || has_dynamic_transform || has_runtime_clip;
1600 let has_dynamic_children = node.children.iter().any(|child| {
1601 runtime_dynamic_subtrees
1602 .get(child)
1603 .copied()
1604 .unwrap_or(false)
1605 });
1606 let mut layer = RenderLayer::new(rect);
1607 layer.node_id = Some(node_id);
1608 if can_cache_scene {
1609 layer.style.cache_key = Some(scene_cache_key);
1610 } else if has_dynamic_style && !has_dynamic_children {
1611 layer.style.content_cache_key = Some(scene_cache_key ^ 0x9E37_79B9_7F4A_7C15);
1612 }
1613
1614 layer.style.clip = match &node.op {
1615 Op::Layout(LayoutOp::Scroll { .. }) | Op::Layout(LayoutOp::Clip { .. }) => {
1616 Some(LayerClip::Rect(rect))
1617 }
1618 _ if has_runtime_clip => Some(LayerClip::Rect(rect)),
1619 _ => None,
1620 };
1621 if emit_opacity_layer {
1622 layer.style.opacity = composite_opacity.unwrap_or(1.0);
1623 }
1624
1625 if let Some(transform) = compose_dynamic_layer_transform(
1626 &TransformBinding {
1627 layer_path: layer_path.clone(),
1628 rect,
1629 layout_transform,
1630 scroll: None,
1631 translate_x: node.composite.translate_x.clone(),
1632 translate_y: node.composite.translate_y.clone(),
1633 scale: node.composite.scale.clone(),
1634 rotation: node.composite.rotation.clone(),
1635 },
1636 scroll_map,
1637 animation_map,
1638 ) {
1639 layer.style.transform = Some(transform);
1640 }
1641
1642 let local_hash = local_paint_hash(node);
1643 let local_paint = if let Some((cached_hash, cached_ops)) = paint_cache.get(&node_id) {
1644 if *cached_hash == local_hash {
1645 *hit_count += 1;
1646 Some(cached_ops.clone())
1647 } else {
1648 *miss_count += 1;
1649 let ops = build_local_paint_list(ir, node_id, node, rect);
1650 if let Some(ops) = ops.clone() {
1651 paint_cache.insert(node_id, (local_hash, ops));
1652 } else {
1653 paint_cache.remove(&node_id);
1654 }
1655 ops
1656 }
1657 } else {
1658 *miss_count += 1;
1659 let ops = build_local_paint_list(ir, node_id, node, rect);
1660 if let Some(ops) = ops.clone() {
1661 paint_cache.insert(node_id, (local_hash, ops));
1662 }
1663 ops
1664 };
1665
1666 if let Some(local_paint) = local_paint {
1667 layer.children.push(RenderNode::Paint(local_paint));
1668 }
1669
1670 if needs_dynamic_opacity {
1671 if let Some(scalar) = node.composite.opacity.as_ref() {
1672 bindings.opacity.push(OpacityBinding {
1673 layer_path: layer_path.clone(),
1674 scalar: scalar.clone(),
1675 });
1676 }
1677 }
1678 if has_own_transform {
1679 bindings.transform.push(TransformBinding {
1680 layer_path: layer_path.clone(),
1681 rect,
1682 layout_transform,
1683 scroll: None,
1684 translate_x: node.composite.translate_x.clone(),
1685 translate_y: node.composite.translate_y.clone(),
1686 scale: node.composite.scale.clone(),
1687 rotation: node.composite.rotation.clone(),
1688 });
1689 }
1690
1691 if let Some(scroll) = scroll {
1692 let content_index = layer.children.len();
1693 let mut content_path = layer_path.clone();
1694 content_path.push(content_index);
1695 let mut content_layer = RenderLayer::new(rect);
1696 content_layer.style.transform = compose_dynamic_layer_transform(
1697 &TransformBinding {
1698 layer_path: content_path.clone(),
1699 rect,
1700 layout_transform: None,
1701 scroll: Some(scroll.clone()),
1702 translate_x: None,
1703 translate_y: None,
1704 scale: None,
1705 rotation: None,
1706 },
1707 scroll_map,
1708 animation_map,
1709 );
1710 content_layer.style.transform_clip = false;
1711 bindings.transform.push(TransformBinding {
1712 layer_path: content_path.clone(),
1713 rect,
1714 layout_transform: None,
1715 scroll: Some(scroll),
1716 translate_x: None,
1717 translate_y: None,
1718 scale: None,
1719 rotation: None,
1720 });
1721
1722 for child in &node.children {
1723 let child_index = content_layer.children.len();
1724 let mut child_path = content_path.clone();
1725 child_path.push(child_index);
1726 if let Some(child_layer) = generate_render_layer_recursive(
1727 *child,
1728 ir,
1729 snapshot,
1730 scroll_map,
1731 animation_map,
1732 paint_cache,
1733 boundary_cache,
1734 runtime_dynamic_subtrees,
1735 miss_count,
1736 hit_count,
1737 scene_cache_allowed,
1738 visited,
1739 bindings,
1740 child_path,
1741 ) {
1742 content_layer.children.push(RenderNode::Layer(child_layer));
1743 }
1744 }
1745
1746 if !content_layer.children.is_empty() {
1747 layer.children.push(RenderNode::Layer(content_layer));
1748 }
1749 } else {
1750 for child in &node.children {
1751 let child_index = layer.children.len();
1752 let mut child_path = layer_path.clone();
1753 child_path.push(child_index);
1754 if let Some(child_layer) = generate_render_layer_recursive(
1755 *child,
1756 ir,
1757 snapshot,
1758 scroll_map,
1759 animation_map,
1760 paint_cache,
1761 boundary_cache,
1762 runtime_dynamic_subtrees,
1763 miss_count,
1764 hit_count,
1765 scene_cache_allowed,
1766 visited,
1767 bindings,
1768 child_path,
1769 ) {
1770 layer.children.push(RenderNode::Layer(child_layer));
1771 }
1772 }
1773 }
1774
1775 if let Some(scrollbar) = build_scrollbar_paint(ir, node_id, snapshot, scroll_map) {
1776 let mut scrollbar_path = layer_path.clone();
1777 scrollbar_path.push(layer.children.len());
1778 layer.children.push(RenderNode::Paint(scrollbar));
1779 bindings.scrollbar.push(ScrollbarBinding {
1780 node_path: scrollbar_path,
1781 node_id,
1782 });
1783 }
1784
1785 if can_use_boundary_cache {
1786 boundary_cache.insert(
1787 node_id,
1788 BoundaryCacheEntry {
1789 hash: scene_cache_key,
1790 layer: layer.clone(),
1791 },
1792 );
1793 }
1794
1795 Some(layer)
1796}
1797
1798fn push_video_surface(
1799 video_surfaces: &mut Vec<VideoSurfaceFrame>,
1800 widget_id: WidgetNodeId,
1801 rect: LayoutRect,
1802 video_map: &VideoStateMap,
1803) {
1804 if let Some(state) = video_map.states.get(&widget_id) {
1805 let surface_id = state.surface_id.unwrap_or(0);
1806 video_surfaces.push(VideoSurfaceFrame {
1807 widget_id,
1808 surface_id,
1809 rect,
1810 });
1811 }
1812}
1813
1814fn push_web_surface(
1815 web_surfaces: &mut Vec<WebSurfaceFrame>,
1816 widget_id: WidgetNodeId,
1817 rect: LayoutRect,
1818 web_map: &WebStateMap,
1819) {
1820 if let Some(state) = web_map.states.get(&widget_id) {
1821 if !state.url.trim().is_empty() {
1822 web_surfaces.push(WebSurfaceFrame {
1823 widget_id,
1824 url: state.url.clone(),
1825 user_agent: state.user_agent.clone(),
1826 rect,
1827 });
1828 }
1829 }
1830}
1831
1832fn collect_video_surfaces(
1833 node_id: NodeId,
1834 ir: &CoreIR,
1835 snapshot: &LayoutSnapshot,
1836 video_map: &VideoStateMap,
1837 web_map: &WebStateMap,
1838 scroll_map: &ScrollStateMap,
1839 accumulated_offset: LayoutPoint,
1840 video_surfaces: &mut Vec<VideoSurfaceFrame>,
1841 web_surfaces: &mut Vec<WebSurfaceFrame>,
1842 scene_3d_surfaces: &mut Vec<(WidgetNodeId, LayoutRect, Vec<u8>)>,
1843) {
1844 let mut visited = HashSet::new();
1845 collect_video_surfaces_with_visited(
1846 node_id,
1847 ir,
1848 snapshot,
1849 video_map,
1850 web_map,
1851 scroll_map,
1852 accumulated_offset,
1853 video_surfaces,
1854 web_surfaces,
1855 scene_3d_surfaces,
1856 &mut visited,
1857 );
1858}
1859
1860fn collect_video_surfaces_with_visited(
1861 node_id: NodeId,
1862 ir: &CoreIR,
1863 snapshot: &LayoutSnapshot,
1864 video_map: &VideoStateMap,
1865 web_map: &WebStateMap,
1866 scroll_map: &ScrollStateMap,
1867 accumulated_offset: LayoutPoint,
1868 video_surfaces: &mut Vec<VideoSurfaceFrame>,
1869 web_surfaces: &mut Vec<WebSurfaceFrame>,
1870 scene_3d_surfaces: &mut Vec<(WidgetNodeId, LayoutRect, Vec<u8>)>,
1871 visited: &mut HashSet<NodeId>,
1872) {
1873 if !visited.insert(node_id) {
1874 return;
1875 }
1876 if let (Some(node), Some(geom)) = (ir.nodes.get(&node_id), snapshot.nodes.get(&node_id)) {
1877 let mut child_offset = accumulated_offset;
1878 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1879 let offset = scroll_map.get_offset(node_id);
1880 child_offset = match direction {
1881 fission_ir::FlexDirection::Row => {
1882 LayoutPoint::new(accumulated_offset.x - offset, accumulated_offset.y)
1883 }
1884 fission_ir::FlexDirection::Column => {
1885 LayoutPoint::new(accumulated_offset.x, accumulated_offset.y - offset)
1886 }
1887 };
1888 }
1889
1890 if let Op::Layout(LayoutOp::Embed {
1891 kind: EmbedKind::Video,
1892 widget_id,
1893 ..
1894 }) = &node.op
1895 {
1896 let translated_rect = translate_rect(geom.rect, accumulated_offset);
1897 push_video_surface(video_surfaces, *widget_id, translated_rect, video_map);
1898 } else if let Op::Layout(LayoutOp::Embed {
1899 kind: EmbedKind::Web,
1900 widget_id,
1901 ..
1902 }) = &node.op
1903 {
1904 let translated_rect = translate_rect(geom.rect, accumulated_offset);
1905 push_web_surface(web_surfaces, *widget_id, translated_rect, web_map);
1906 } else if let Op::Layout(LayoutOp::Embed {
1907 kind: EmbedKind::Custom(payload),
1908 widget_id,
1909 ..
1910 }) = &node.op
1911 {
1912 let translated_rect = translate_rect(geom.rect, accumulated_offset);
1913 scene_3d_surfaces.push((*widget_id, translated_rect, payload.clone()));
1914 }
1915
1916 for child in &node.children {
1917 collect_video_surfaces_with_visited(
1918 *child,
1919 ir,
1920 snapshot,
1921 video_map,
1922 web_map,
1923 scroll_map,
1924 child_offset,
1925 video_surfaces,
1926 web_surfaces,
1927 scene_3d_surfaces,
1928 visited,
1929 );
1930 }
1931 }
1932}
1933
1934fn local_paint_hash(node: &fission_ir::CoreNode) -> u64 {
1935 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1936 node.op.hash(&mut hasher);
1937 hasher.finish()
1938}
1939
1940fn boundary_hash(node: &fission_ir::CoreNode, rect: LayoutRect) -> u64 {
1941 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1942 node.hash.hash(&mut hasher);
1943 rect.origin.x.to_bits().hash(&mut hasher);
1944 rect.origin.y.to_bits().hash(&mut hasher);
1945 rect.size.width.to_bits().hash(&mut hasher);
1946 rect.size.height.to_bits().hash(&mut hasher);
1947 hasher.finish()
1948}
1949
1950fn build_local_paint_list(
1951 ir: &CoreIR,
1952 node_id: NodeId,
1953 node: &fission_ir::CoreNode,
1954 rect: LayoutRect,
1955) -> Option<DisplayList> {
1956 let mut list = DisplayList::new(rect);
1957 match &node.op {
1958 Op::Paint(fission_ir::PaintOp::DrawRect {
1959 fill,
1960 stroke,
1961 corner_radius,
1962 shadow,
1963 }) => {
1964 list.push(DisplayOp::DrawRect {
1965 rect,
1966 fill: fill.as_ref().map(map_fill),
1967 stroke: stroke.as_ref().map(map_stroke),
1968 corner_radius: *corner_radius,
1969 shadow: shadow.as_ref().map(|s| BoxShadow {
1970 color: RenderColor {
1971 r: s.color.r,
1972 g: s.color.g,
1973 b: s.color.b,
1974 a: s.color.a,
1975 },
1976 blur_radius: s.blur_radius,
1977 offset: s.offset,
1978 }),
1979 bounds: rect,
1980 node_id: Some(node_id),
1981 });
1982 }
1983 Op::Paint(fission_ir::PaintOp::DrawText {
1984 text,
1985 size,
1986 color,
1987 underline,
1988 wrap,
1989 caret_index,
1990 caret_color,
1991 caret_width,
1992 caret_height,
1993 caret_radius,
1994 paragraph_style,
1995 }) => {
1996 list.push(DisplayOp::DrawText {
1997 text: text.clone(),
1998 position: rect.origin,
1999 size: *size,
2000 color: RenderColor {
2001 r: color.r,
2002 g: color.g,
2003 b: color.b,
2004 a: color.a,
2005 },
2006 bounds: rect,
2007 node_id: Some(node_id),
2008 underline: *underline,
2009 wrap: *wrap,
2010 caret_index: *caret_index,
2011 caret_color: caret_color.map(|color| RenderColor {
2012 r: color.r,
2013 g: color.g,
2014 b: color.b,
2015 a: color.a,
2016 }),
2017 caret_width: *caret_width,
2018 caret_height: *caret_height,
2019 caret_radius: *caret_radius,
2020 paragraph_style: *paragraph_style,
2021 });
2022 }
2023 Op::Paint(fission_ir::PaintOp::DrawRichText {
2024 runs,
2025 wrap,
2026 caret_index,
2027 caret_color,
2028 caret_width,
2029 caret_height,
2030 caret_radius,
2031 paragraph_style,
2032 }) => {
2033 let annotations = ir
2034 .custom_render_objects
2035 .get(&node_id)
2036 .and_then(|sidecar| {
2037 sidecar.downcast_ref::<Vec<fission_ir::op::RichTextAnnotation>>()
2038 })
2039 .cloned()
2040 .unwrap_or_default();
2041 let render_runs = runs
2042 .iter()
2043 .map(|r| fission_render::TextRun {
2044 text: r.text.clone(),
2045 style: fission_render::TextStyle {
2046 font_size: r.style.font_size,
2047 color: RenderColor {
2048 r: r.style.color.r,
2049 g: r.style.color.g,
2050 b: r.style.color.b,
2051 a: r.style.color.a,
2052 },
2053 underline: r.style.underline,
2054 font_family: r.style.font_family.clone(),
2055 locale: r.style.locale.clone(),
2056 font_weight: r.style.font_weight,
2057 font_style: r.style.font_style,
2058 line_height: r.style.line_height,
2059 letter_spacing: r.style.letter_spacing,
2060 background_color: r.style.background_color.map(|c| RenderColor {
2061 r: c.r,
2062 g: c.g,
2063 b: c.b,
2064 a: c.a,
2065 }),
2066 },
2067 })
2068 .collect();
2069
2070 list.push(DisplayOp::DrawRichText {
2071 runs: render_runs,
2072 position: rect.origin,
2073 bounds: rect,
2074 node_id: Some(node_id),
2075 wrap: *wrap,
2076 caret_index: *caret_index,
2077 caret_color: caret_color.map(|color| RenderColor {
2078 r: color.r,
2079 g: color.g,
2080 b: color.b,
2081 a: color.a,
2082 }),
2083 caret_width: *caret_width,
2084 caret_height: *caret_height,
2085 caret_radius: *caret_radius,
2086 paragraph_style: *paragraph_style,
2087 annotations,
2088 });
2089 }
2090 Op::Paint(fission_ir::PaintOp::DrawImage {
2091 request,
2092 fit,
2093 alignment,
2094 }) => {
2095 list.push(DisplayOp::DrawImage {
2096 rect,
2097 request: request.clone(),
2098 fit: match fit {
2099 fission_ir::op::ImageFit::Contain => fission_render::ImageFit::Contain,
2100 fission_ir::op::ImageFit::Cover => fission_render::ImageFit::Cover,
2101 fission_ir::op::ImageFit::Fill => fission_render::ImageFit::Fill,
2102 fission_ir::op::ImageFit::None => fission_render::ImageFit::None,
2103 },
2104 alignment: *alignment,
2105 bounds: rect,
2106 node_id: Some(node_id),
2107 });
2108 }
2109 Op::Paint(fission_ir::PaintOp::DrawPath { path, fill, stroke }) => {
2110 list.push(DisplayOp::DrawPath {
2111 path: path.clone(),
2112 fill: fill.as_ref().map(map_fill),
2113 stroke: stroke.as_ref().map(map_stroke),
2114 bounds: rect,
2115 node_id: Some(node_id),
2116 });
2117 }
2118 Op::Paint(fission_ir::PaintOp::DrawSvg {
2119 content,
2120 fill,
2121 stroke,
2122 }) => {
2123 list.push(DisplayOp::DrawSvg {
2124 content: content.clone(),
2125 fill: fill.as_ref().map(map_fill),
2126 stroke: stroke.as_ref().map(map_stroke),
2127 bounds: rect,
2128 node_id: Some(node_id),
2129 });
2130 }
2131 Op::Layout(LayoutOp::Embed {
2132 kind, widget_id, ..
2133 }) => {
2134 list.push(DisplayOp::DrawSurface {
2135 rect,
2136 surface_id: embed_surface_id(kind, *widget_id),
2137 position: 0,
2138 bounds: rect,
2139 node_id: Some(node_id),
2140 });
2141 }
2142 _ => {}
2143 }
2144 if list.ops.is_empty() {
2145 None
2146 } else {
2147 Some(list)
2148 }
2149}
2150
2151fn build_scrollbar_paint(
2152 ir: &CoreIR,
2153 node_id: NodeId,
2154 snapshot: &LayoutSnapshot,
2155 scroll_map: &ScrollStateMap,
2156) -> Option<DisplayList> {
2157 let geometry = scrollbar_geometry_for_node(ir, snapshot, scroll_map, node_id)?;
2158 let rail_fill = Some(Fill::Solid(RenderColor {
2159 r: 160,
2160 g: 168,
2161 b: 180,
2162 a: 80,
2163 }));
2164 let thumb_fill = Some(Fill::Solid(RenderColor {
2165 r: 82,
2166 g: 91,
2167 b: 108,
2168 a: 190,
2169 }));
2170 let mut list = DisplayList::new(geometry.rail_rect);
2171 let corner_radius = fission_core::scrollbar::SCROLLBAR_THICKNESS / 2.0;
2172
2173 list.push(DisplayOp::DrawRect {
2174 rect: geometry.rail_rect,
2175 fill: rail_fill,
2176 stroke: None,
2177 corner_radius,
2178 shadow: None,
2179 bounds: geometry.rail_rect,
2180 node_id: Some(node_id),
2181 });
2182 list.push(DisplayOp::DrawRect {
2183 rect: geometry.thumb_rect,
2184 fill: thumb_fill,
2185 stroke: None,
2186 corner_radius,
2187 shadow: None,
2188 bounds: geometry.thumb_rect,
2189 node_id: Some(node_id),
2190 });
2191
2192 Some(list)
2193}
2194
2195fn resolve_composite_scalar(
2196 scalar: Option<&fission_ir::CompositeScalar>,
2197 animation_map: &AnimationStateMap,
2198 property: AnimationPropertyId,
2199) -> Option<f32> {
2200 let scalar = scalar?;
2201 Some(resolve_scalar_value(scalar, animation_map, property))
2202}
2203
2204fn resolve_scalar_value(
2205 scalar: &fission_ir::CompositeScalar,
2206 animation_map: &AnimationStateMap,
2207 property: AnimationPropertyId,
2208) -> f32 {
2209 scalar
2210 .animation_target
2211 .and_then(|target| animation_map.values.get(&(target, property)).copied())
2212 .unwrap_or(scalar.base)
2213}
2214
2215fn composite_transform_matrix(
2216 rect: LayoutRect,
2217 translate_x: f32,
2218 translate_y: f32,
2219 scale: f32,
2220 rotation: f32,
2221) -> [f32; 16] {
2222 let center_x = rect.origin.x + rect.size.width * 0.5;
2223 let center_y = rect.origin.y + rect.size.height * 0.5;
2224
2225 let to_center = translation_matrix(center_x, center_y);
2226 let from_center = translation_matrix(-center_x, -center_y);
2227 let scale_matrix = scale_matrix(scale);
2228 let rotation_matrix = rotation_z_matrix(rotation);
2229 let animated_translate = translation_matrix(translate_x, translate_y);
2230
2231 multiply_matrix(
2232 animated_translate,
2233 multiply_matrix(
2234 to_center,
2235 multiply_matrix(rotation_matrix, multiply_matrix(scale_matrix, from_center)),
2236 ),
2237 )
2238}
2239
2240fn translation_matrix(tx: f32, ty: f32) -> [f32; 16] {
2241 [
2242 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, tx, ty, 0.0, 1.0,
2243 ]
2244}
2245
2246fn scale_matrix(scale: f32) -> [f32; 16] {
2247 [
2248 scale, 0.0, 0.0, 0.0, 0.0, scale, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
2249 ]
2250}
2251
2252fn rotation_z_matrix(radians: f32) -> [f32; 16] {
2253 let sin = radians.sin();
2254 let cos = radians.cos();
2255 [
2256 cos, sin, 0.0, 0.0, -sin, cos, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
2257 ]
2258}
2259
2260fn multiply_matrix(a: [f32; 16], b: [f32; 16]) -> [f32; 16] {
2261 let mut out = [0.0; 16];
2262 for row in 0..4 {
2263 for col in 0..4 {
2264 let mut sum = 0.0;
2265 for k in 0..4 {
2266 sum += a[row * 4 + k] * b[k * 4 + col];
2267 }
2268 out[row * 4 + col] = sum;
2269 }
2270 }
2271 out
2272}
2273
2274fn is_identity_matrix(matrix: &[f32; 16]) -> bool {
2275 const IDENTITY: [f32; 16] = [
2276 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
2277 ];
2278 matrix
2279 .iter()
2280 .zip(IDENTITY.iter())
2281 .all(|(lhs, rhs)| (*lhs - *rhs).abs() <= 0.000_1)
2282}
2283
2284#[cfg(test)]
2285fn scroll_offsets_changed(prev: &HashMap<NodeId, u32>, scroll_map: &ScrollStateMap) -> bool {
2286 if prev.len() != scroll_map.offsets.len() {
2287 return true;
2288 }
2289
2290 scroll_map
2291 .offsets
2292 .iter()
2293 .any(|(id, offset)| prev.get(id).copied() != Some(offset.to_bits()))
2294}
2295
2296impl SnapshotProvider for Pipeline {
2297 fn snapshot(&self, kind: SnapshotKind) -> Option<SnapshotBlob> {
2298 match kind {
2299 SnapshotKind::Layout => self.last_snapshot.as_ref().and_then(|snap| {
2300 serde_json::to_string_pretty(snap)
2301 .ok()
2302 .map(|json| SnapshotBlob { kind, json })
2303 }),
2304 }
2305 }
2306}
2307
2308fn map_fill(f: &fission_ir::op::Fill) -> Fill {
2309 match f {
2310 fission_ir::op::Fill::Solid(c) => Fill::Solid(RenderColor {
2311 r: c.r,
2312 g: c.g,
2313 b: c.b,
2314 a: c.a,
2315 }),
2316 fission_ir::op::Fill::LinearGradient { start, end, stops } => Fill::LinearGradient {
2317 start: *start,
2318 end: *end,
2319 stops: stops
2320 .iter()
2321 .map(|(o, c)| {
2322 (
2323 *o,
2324 RenderColor {
2325 r: c.r,
2326 g: c.g,
2327 b: c.b,
2328 a: c.a,
2329 },
2330 )
2331 })
2332 .collect(),
2333 },
2334 fission_ir::op::Fill::RadialGradient {
2335 center,
2336 radius,
2337 stops,
2338 } => Fill::RadialGradient {
2339 center: *center,
2340 radius: *radius,
2341 stops: stops
2342 .iter()
2343 .map(|(o, c)| {
2344 (
2345 *o,
2346 RenderColor {
2347 r: c.r,
2348 g: c.g,
2349 b: c.b,
2350 a: c.a,
2351 },
2352 )
2353 })
2354 .collect(),
2355 },
2356 }
2357}
2358
2359fn map_stroke(s: &fission_ir::op::Stroke) -> Stroke {
2360 Stroke {
2361 fill: map_fill(&s.fill),
2362 width: s.width,
2363 dash_array: s.dash_array.clone(),
2364 line_cap: match s.line_cap {
2365 fission_ir::op::LineCap::Butt => fission_render::LineCap::Butt,
2366 fission_ir::op::LineCap::Round => fission_render::LineCap::Round,
2367 fission_ir::op::LineCap::Square => fission_render::LineCap::Square,
2368 },
2369 line_join: match s.line_join {
2370 fission_ir::op::LineJoin::Miter => fission_render::LineJoin::Miter,
2371 fission_ir::op::LineJoin::Round => fission_render::LineJoin::Round,
2372 fission_ir::op::LineJoin::Bevel => fission_render::LineJoin::Bevel,
2373 },
2374 }
2375}
2376
2377fn translate_rect(rect: LayoutRect, offset: LayoutPoint) -> LayoutRect {
2378 LayoutRect {
2379 origin: LayoutPoint::new(rect.origin.x + offset.x, rect.origin.y + offset.y),
2380 size: rect.size,
2381 }
2382}
2383
2384#[cfg(test)]
2385mod tests {
2386 use super::{build_local_paint_list, scroll_offsets_changed, InvalidationSet, Pipeline};
2387 use fission_core::env::Env;
2388 use fission_core::registry::AnimationPropertyId;
2389 use fission_core::ScrollStateMap;
2390 use fission_ir::op::{
2391 Color, Fill, ImageAlignment, ImageFit, ImageRequest, ImageSource, RichTextAnnotation,
2392 TextRun, TextStyle,
2393 };
2394 use fission_ir::semantics::ActionTrigger;
2395 use fission_ir::{
2396 ActionEntry, CompositeScalar, CompositeStyle, CoreIR, EmbedKind, LayoutOp, NodeId, Op,
2397 PaintOp, WidgetNodeId,
2398 };
2399 use fission_layout::{LayoutEngine, LayoutRect, LayoutSize};
2400 use fission_render::{DisplayOp, RenderScene, Renderer};
2401 use std::collections::HashMap;
2402 use std::sync::Arc;
2403
2404 struct NullRenderer;
2405
2406 impl Renderer for NullRenderer {
2407 fn render_scene(&mut self, _scene: &RenderScene) -> anyhow::Result<()> {
2408 Ok(())
2409 }
2410 }
2411
2412 fn two_child_layout_ir(second_width: f32) -> CoreIR {
2413 let root = NodeId::derived(50, &[0]);
2414 let first = NodeId::derived(50, &[1]);
2415 let second = NodeId::derived(50, &[2]);
2416 let mut ir = CoreIR::new();
2417 ir.add_node(
2418 first,
2419 Op::Layout(LayoutOp::Box {
2420 width: Some(40.0),
2421 height: Some(20.0),
2422 min_width: None,
2423 max_width: None,
2424 min_height: None,
2425 max_height: None,
2426 padding: [0.0; 4],
2427 flex_grow: 0.0,
2428 flex_shrink: 1.0,
2429 aspect_ratio: None,
2430 }),
2431 vec![],
2432 );
2433 ir.add_node(
2434 second,
2435 Op::Layout(LayoutOp::Box {
2436 width: Some(second_width),
2437 height: Some(20.0),
2438 min_width: None,
2439 max_width: None,
2440 min_height: None,
2441 max_height: None,
2442 padding: [0.0; 4],
2443 flex_grow: 0.0,
2444 flex_shrink: 1.0,
2445 aspect_ratio: None,
2446 }),
2447 vec![],
2448 );
2449 ir.add_node(
2450 root,
2451 Op::Layout(LayoutOp::Flex {
2452 direction: fission_ir::FlexDirection::Column,
2453 wrap: fission_ir::op::FlexWrap::NoWrap,
2454 flex_grow: 0.0,
2455 flex_shrink: 1.0,
2456 padding: [0.0; 4],
2457 gap: Some(4.0),
2458 align_items: fission_ir::op::AlignItems::Start,
2459 justify_content: fission_ir::op::JustifyContent::Start,
2460 }),
2461 vec![first, second],
2462 );
2463 ir.set_root(root);
2464 ir
2465 }
2466
2467 #[test]
2468 fn unchanged_scroll_offsets_do_not_invalidate_cache() {
2469 let id = NodeId::derived(1, &[0]);
2470 let mut prev = HashMap::new();
2471 prev.insert(id, 12.5f32.to_bits());
2472 let mut scroll = ScrollStateMap::default();
2473 scroll.set_offset(id, 12.5);
2474 assert!(!scroll_offsets_changed(&prev, &scroll));
2475 }
2476
2477 #[test]
2478 fn changed_scroll_offsets_invalidate_cache() {
2479 let id = NodeId::derived(2, &[0]);
2480 let mut prev = HashMap::new();
2481 prev.insert(id, 0.0f32.to_bits());
2482 let mut scroll = ScrollStateMap::default();
2483 scroll.set_offset(id, 4.0);
2484 assert!(scroll_offsets_changed(&prev, &scroll));
2485 }
2486
2487 #[test]
2488 fn incremental_layout_keeps_rebuild_telemetry_honest() {
2489 let mut pipeline = Pipeline::new();
2490 let mut layout_engine = LayoutEngine::new();
2491 let scroll = ScrollStateMap::default();
2492
2493 pipeline.replace_ir(two_child_layout_ir(60.0), &Env::default());
2494 let first_pass = pipeline
2495 .ensure_layout(
2496 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2497 &mut layout_engine,
2498 &scroll,
2499 )
2500 .expect("initial layout");
2501 assert_eq!(first_pass, pipeline.layout_input_nodes.len());
2502 assert_eq!(pipeline.layout_full_rebuild_count, 1);
2503
2504 pipeline.replace_ir(two_child_layout_ir(90.0), &Env::default());
2505 let second_pass = pipeline
2506 .ensure_layout(
2507 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2508 &mut layout_engine,
2509 &scroll,
2510 )
2511 .expect("incremental layout");
2512
2513 assert_eq!(second_pass, 1);
2514 assert_eq!(pipeline.layout_full_rebuild_count, 1);
2515 assert!(pipeline.pending_layout_dirty_nodes.is_empty());
2516 }
2517
2518 #[test]
2519 fn rich_text_annotations_flow_into_display_ops() {
2520 let node_id = NodeId::derived(9, &[0]);
2521 let mut ir = CoreIR::new();
2522 ir.add_node(
2523 node_id,
2524 Op::Paint(PaintOp::DrawRichText {
2525 runs: vec![TextRun {
2526 text: "docs".into(),
2527 style: TextStyle {
2528 font_size: 14.0,
2529 color: Color::BLACK,
2530 underline: false,
2531 font_family: None,
2532 locale: None,
2533 font_weight: 400,
2534 font_style: fission_ir::op::FontStyle::Normal,
2535 line_height: None,
2536 letter_spacing: 0.0,
2537 background_color: None,
2538 },
2539 }],
2540 wrap: true,
2541 caret_index: None,
2542 caret_color: None,
2543 caret_width: None,
2544 caret_height: None,
2545 caret_radius: None,
2546 paragraph_style: None,
2547 }),
2548 vec![],
2549 );
2550 ir.custom_render_objects.insert(
2551 node_id,
2552 Arc::new(vec![RichTextAnnotation {
2553 range: 0..4,
2554 semantics_label: Some("Documentation".into()),
2555 semantics_identifier: Some("docs-link".into()),
2556 spell_out: Some(true),
2557 mouse_cursor: Some(fission_ir::op::MouseCursor::Pointer),
2558 actions: vec![ActionEntry {
2559 trigger: ActionTrigger::Default,
2560 action_id: 7,
2561 payload_data: Some(vec![1, 2, 3]),
2562 }],
2563 }]),
2564 );
2565
2566 let node = ir.nodes.get(&node_id).expect("paint node");
2567 let list =
2568 build_local_paint_list(&ir, node_id, node, LayoutRect::new(0.0, 0.0, 160.0, 40.0))
2569 .expect("display list");
2570 match list.ops.first() {
2571 Some(DisplayOp::DrawRichText { annotations, .. }) => {
2572 assert_eq!(annotations.len(), 1);
2573 assert_eq!(annotations[0].range, 0..4);
2574 assert_eq!(
2575 annotations[0].semantics_identifier.as_deref(),
2576 Some("docs-link")
2577 );
2578 }
2579 other => panic!("expected rich text op, got {other:?}"),
2580 }
2581 }
2582
2583 #[test]
2584 fn draw_image_paint_ops_flow_into_display_ops() {
2585 let node_id = NodeId::derived(12, &[0]);
2586 let request = ImageRequest {
2587 source: ImageSource::Network {
2588 url: "https://example.com/product.webp".into(),
2589 headers: Vec::new(),
2590 cache_policy: Default::default(),
2591 },
2592 cache_width: Some(220),
2593 cache_height: Some(160),
2594 semantic_label: Some("Product thumbnail".into()),
2595 ..Default::default()
2596 };
2597 let mut ir = CoreIR::new();
2598 ir.add_node(
2599 node_id,
2600 Op::Paint(PaintOp::DrawImage {
2601 request: request.clone(),
2602 fit: ImageFit::Cover,
2603 alignment: ImageAlignment::Center,
2604 }),
2605 vec![],
2606 );
2607
2608 let node = ir.nodes.get(&node_id).expect("image node");
2609 let rect = LayoutRect::new(24.0, 32.0, 220.0, 160.0);
2610 let list = build_local_paint_list(&ir, node_id, node, rect).expect("display list");
2611
2612 match list.ops.first() {
2613 Some(DisplayOp::DrawImage {
2614 rect: image_rect,
2615 request: image_request,
2616 fit,
2617 alignment,
2618 bounds,
2619 node_id: Some(image_node_id),
2620 }) => {
2621 assert_eq!(*image_rect, rect);
2622 assert_eq!(image_request, &request);
2623 assert_eq!(*fit, fission_render::ImageFit::Cover);
2624 assert_eq!(*alignment, ImageAlignment::Center);
2625 assert_eq!(*bounds, rect);
2626 assert_eq!(*image_node_id, node_id);
2627 }
2628 other => panic!("expected image display op, got {other:?}"),
2629 }
2630 }
2631
2632 #[test]
2633 fn retained_pipeline_scene_keeps_draw_image_ops() {
2634 let image_id = NodeId::derived(13, &[0]);
2635 let root_id = NodeId::derived(13, &[1]);
2636 let request = ImageRequest {
2637 source: ImageSource::Network {
2638 url: "https://example.com/catalog/thumbnail.webp".into(),
2639 headers: Vec::new(),
2640 cache_policy: Default::default(),
2641 },
2642 semantic_label: Some("Catalog thumbnail".into()),
2643 ..Default::default()
2644 };
2645 let mut ir = CoreIR::new();
2646 ir.add_node(
2647 image_id,
2648 Op::Paint(PaintOp::DrawImage {
2649 request: request.clone(),
2650 fit: ImageFit::Cover,
2651 alignment: ImageAlignment::Center,
2652 }),
2653 vec![],
2654 );
2655 ir.add_node(
2656 root_id,
2657 Op::Layout(LayoutOp::Box {
2658 width: Some(220.0),
2659 height: Some(160.0),
2660 min_width: None,
2661 max_width: None,
2662 min_height: None,
2663 max_height: None,
2664 padding: [0.0, 0.0, 0.0, 0.0],
2665 flex_grow: 0.0,
2666 flex_shrink: 0.0,
2667 aspect_ratio: None,
2668 }),
2669 vec![image_id],
2670 );
2671 ir.set_root(root_id);
2672
2673 let mut pipeline = Pipeline::new();
2674 let mut layout_engine = LayoutEngine::new();
2675 let scroll = ScrollStateMap::default();
2676 pipeline.replace_ir(ir, &Env::default());
2677 pipeline
2678 .ensure_layout(
2679 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2680 &mut layout_engine,
2681 &scroll,
2682 )
2683 .unwrap();
2684 pipeline
2685 .prepare_current(
2686 LayoutSize {
2687 width: 320.0,
2688 height: 240.0,
2689 },
2690 LayoutSize {
2691 width: 320.0,
2692 height: 240.0,
2693 },
2694 false,
2695 &scroll,
2696 &Default::default(),
2697 &Default::default(),
2698 &Default::default(),
2699 )
2700 .unwrap();
2701
2702 let display_list = pipeline.retained_scene().expect("retained scene").flatten();
2703 let image_op = display_list.ops.iter().find_map(|op| match op {
2704 DisplayOp::DrawImage {
2705 rect,
2706 request: image_request,
2707 fit,
2708 alignment,
2709 ..
2710 } => Some((rect, image_request, fit, alignment)),
2711 _ => None,
2712 });
2713
2714 let Some((rect, image_request, fit, alignment)) = image_op else {
2715 panic!("retained scene dropped DrawImage op");
2716 };
2717 assert_eq!(image_request, &request);
2718 assert_eq!(*fit, fission_render::ImageFit::Cover);
2719 assert_eq!(*alignment, ImageAlignment::Center);
2720 assert_eq!(rect.size.width, 220.0);
2721 assert_eq!(rect.size.height, 160.0);
2722 }
2723
2724 #[test]
2725 fn embed_layout_ops_flow_into_surface_display_ops() {
2726 let node_id = NodeId::derived(14, &[0]);
2727 let widget_id = WidgetNodeId::explicit("embed.surface");
2728 let mut ir = CoreIR::new();
2729 ir.add_node(
2730 node_id,
2731 Op::Layout(LayoutOp::Embed {
2732 kind: EmbedKind::Web,
2733 widget_id,
2734 width: Some(320.0),
2735 height: Some(180.0),
2736 }),
2737 vec![],
2738 );
2739
2740 let node = ir.nodes.get(&node_id).expect("embed node");
2741 let rect = LayoutRect::new(12.0, 24.0, 320.0, 180.0);
2742 let list = build_local_paint_list(&ir, node_id, node, rect).expect("display list");
2743
2744 match list.ops.first() {
2745 Some(DisplayOp::DrawSurface {
2746 rect: surface_rect,
2747 bounds,
2748 node_id: Some(surface_node_id),
2749 ..
2750 }) => {
2751 assert_eq!(*surface_rect, rect);
2752 assert_eq!(*bounds, rect);
2753 assert_eq!(*surface_node_id, node_id);
2754 }
2755 other => panic!("expected surface display op, got {other:?}"),
2756 }
2757 }
2758
2759 #[test]
2760 fn compositor_bound_opacity_animation_is_composite_only() {
2761 let mut ir = CoreIR::new();
2762 let child = NodeId::derived(10, &[1]);
2763 let root = NodeId::derived(10, &[0]);
2764 ir.add_node(child, Op::Layout(LayoutOp::AbsoluteFill), vec![]);
2765 ir.add_node_with_composite(
2766 root,
2767 Op::Structural(fission_ir::StructuralOp::Group { stable_hash: 1 }),
2768 CompositeStyle {
2769 opacity: Some(CompositeScalar::new(0.0).animated(WidgetNodeId::explicit("fade"))),
2770 ..Default::default()
2771 },
2772 vec![child],
2773 );
2774 ir.set_root(root);
2775
2776 let mut pipeline = Pipeline::new();
2777 pipeline.replace_ir(ir, &Env::default());
2778 let invalidation = pipeline.classify_animation_updates(&[(
2779 WidgetNodeId::explicit("fade"),
2780 AnimationPropertyId::Opacity,
2781 )]);
2782 assert_eq!(
2783 invalidation,
2784 InvalidationSet {
2785 build: false,
2786 layout: false,
2787 paint: false,
2788 composite: true,
2789 }
2790 );
2791 }
2792
2793 #[test]
2794 fn unbound_custom_animation_requires_build() {
2795 let pipeline = Pipeline::new();
2796 let invalidation = pipeline.classify_animation_updates(&[(
2797 WidgetNodeId::explicit("custom"),
2798 AnimationPropertyId::custom("phase"),
2799 )]);
2800 assert!(invalidation.build);
2801 assert!(invalidation.layout);
2802 }
2803
2804 #[test]
2805 fn compositor_bound_translate_animation_is_composite_only() {
2806 let mut ir = CoreIR::new();
2807 let child = NodeId::derived(11, &[1]);
2808 let root = NodeId::derived(11, &[0]);
2809 ir.add_node(
2810 child,
2811 Op::Paint(PaintOp::DrawRect {
2812 fill: Some(Fill::Solid(Color {
2813 r: 0,
2814 g: 0,
2815 b: 0,
2816 a: 255,
2817 })),
2818 stroke: None,
2819 corner_radius: 0.0,
2820 shadow: None,
2821 }),
2822 vec![],
2823 );
2824 ir.add_node_with_composite(
2825 root,
2826 Op::Layout(LayoutOp::Box {
2827 width: Some(120.0),
2828 height: Some(64.0),
2829 min_width: None,
2830 max_width: None,
2831 min_height: None,
2832 max_height: None,
2833 padding: [0.0, 0.0, 0.0, 0.0],
2834 flex_grow: 0.0,
2835 flex_shrink: 0.0,
2836 aspect_ratio: None,
2837 }),
2838 CompositeStyle {
2839 translate_x: Some(
2840 CompositeScalar::new(12.0).animated(WidgetNodeId::explicit("slide")),
2841 ),
2842 ..Default::default()
2843 },
2844 vec![child],
2845 );
2846 ir.set_root(root);
2847
2848 let mut pipeline = Pipeline::new();
2849 pipeline.replace_ir(ir, &Env::default());
2850 let invalidation = pipeline.classify_animation_updates(&[(
2851 WidgetNodeId::explicit("slide"),
2852 AnimationPropertyId::TranslateX,
2853 )]);
2854 assert_eq!(
2855 invalidation,
2856 InvalidationSet {
2857 build: false,
2858 layout: false,
2859 paint: false,
2860 composite: true,
2861 }
2862 );
2863 }
2864
2865 #[test]
2866 fn dynamic_layer_with_static_contents_gets_content_cache_key() {
2867 let mut ir = CoreIR::new();
2868 let child = NodeId::derived(12, &[1]);
2869 let root = NodeId::derived(12, &[0]);
2870 ir.add_node(
2871 child,
2872 Op::Paint(PaintOp::DrawRect {
2873 fill: Some(Fill::Solid(Color {
2874 r: 20,
2875 g: 40,
2876 b: 60,
2877 a: 255,
2878 })),
2879 stroke: None,
2880 corner_radius: 8.0,
2881 shadow: None,
2882 }),
2883 vec![],
2884 );
2885 ir.add_node_with_composite(
2886 root,
2887 Op::Layout(LayoutOp::Box {
2888 width: Some(160.0),
2889 height: Some(72.0),
2890 min_width: None,
2891 max_width: None,
2892 min_height: None,
2893 max_height: None,
2894 padding: [0.0, 0.0, 0.0, 0.0],
2895 flex_grow: 0.0,
2896 flex_shrink: 0.0,
2897 aspect_ratio: None,
2898 }),
2899 CompositeStyle {
2900 opacity: Some(
2901 CompositeScalar::new(0.4).animated(WidgetNodeId::explicit("fade-cache")),
2902 ),
2903 ..Default::default()
2904 },
2905 vec![child],
2906 );
2907 ir.set_root(root);
2908
2909 let mut pipeline = Pipeline::new();
2910 let mut layout_engine = LayoutEngine::new();
2911 let mut renderer = NullRenderer;
2912 let scroll = ScrollStateMap::default();
2913 pipeline.replace_ir(ir, &Env::default());
2914 pipeline
2915 .ensure_layout(
2916 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2917 &mut layout_engine,
2918 &scroll,
2919 )
2920 .unwrap();
2921 pipeline
2922 .render_current(
2923 LayoutSize {
2924 width: 320.0,
2925 height: 240.0,
2926 },
2927 LayoutSize {
2928 width: 320.0,
2929 height: 240.0,
2930 },
2931 false,
2932 &mut renderer,
2933 &scroll,
2934 &Default::default(),
2935 &Default::default(),
2936 &Default::default(),
2937 )
2938 .unwrap();
2939
2940 let scene = pipeline
2941 .retained_scene
2942 .as_ref()
2943 .expect("retained scene missing");
2944 let presentation_root = match scene.roots.first() {
2945 Some(fission_render::RenderNode::Layer(layer)) => layer,
2946 _ => panic!("missing presentation layer"),
2947 };
2948 let animated_layer = match presentation_root.children.first() {
2949 Some(fission_render::RenderNode::Layer(layer)) => layer,
2950 _ => panic!("missing animated layer"),
2951 };
2952
2953 assert!(animated_layer.style.cache_key.is_none());
2954 assert!(animated_layer.style.content_cache_key.is_some());
2955 }
2956
2957 #[test]
2958 fn nested_dynamic_descendant_becomes_child_texture_plan() {
2959 let mut ir = CoreIR::new();
2960 let left_paint = NodeId::derived(13, &[0]);
2961 let animated_paint = NodeId::derived(13, &[1]);
2962 let animated_wrapper = NodeId::derived(13, &[2]);
2963 let outer_static = NodeId::derived(13, &[3]);
2964 let outer_group = NodeId::derived(13, &[4]);
2965 let root = NodeId::derived(13, &[5]);
2966
2967 ir.add_node(
2968 left_paint,
2969 Op::Paint(PaintOp::DrawRect {
2970 fill: Some(Fill::Solid(Color {
2971 r: 10,
2972 g: 10,
2973 b: 10,
2974 a: 255,
2975 })),
2976 stroke: None,
2977 corner_radius: 0.0,
2978 shadow: None,
2979 }),
2980 vec![],
2981 );
2982 ir.add_node(
2983 animated_paint,
2984 Op::Paint(PaintOp::DrawRect {
2985 fill: Some(Fill::Solid(Color {
2986 r: 200,
2987 g: 40,
2988 b: 40,
2989 a: 255,
2990 })),
2991 stroke: None,
2992 corner_radius: 0.0,
2993 shadow: None,
2994 }),
2995 vec![],
2996 );
2997 ir.add_node_with_composite(
2998 animated_wrapper,
2999 Op::Layout(LayoutOp::Box {
3000 width: Some(96.0),
3001 height: Some(96.0),
3002 min_width: None,
3003 max_width: None,
3004 min_height: None,
3005 max_height: None,
3006 padding: [0.0, 0.0, 0.0, 0.0],
3007 flex_grow: 0.0,
3008 flex_shrink: 0.0,
3009 aspect_ratio: None,
3010 }),
3011 CompositeStyle {
3012 opacity: Some(
3013 CompositeScalar::new(0.4).animated(WidgetNodeId::explicit("nested-fade")),
3014 ),
3015 ..Default::default()
3016 },
3017 vec![animated_paint],
3018 );
3019 ir.add_node(
3020 outer_static,
3021 Op::Paint(PaintOp::DrawRect {
3022 fill: Some(Fill::Solid(Color {
3023 r: 20,
3024 g: 100,
3025 b: 180,
3026 a: 255,
3027 })),
3028 stroke: None,
3029 corner_radius: 8.0,
3030 shadow: None,
3031 }),
3032 vec![],
3033 );
3034 ir.add_node(
3035 outer_group,
3036 Op::Layout(LayoutOp::Box {
3037 width: Some(160.0),
3038 height: Some(120.0),
3039 min_width: None,
3040 max_width: None,
3041 min_height: None,
3042 max_height: None,
3043 padding: [0.0, 0.0, 0.0, 0.0],
3044 flex_grow: 0.0,
3045 flex_shrink: 0.0,
3046 aspect_ratio: None,
3047 }),
3048 vec![outer_static, animated_wrapper],
3049 );
3050 ir.add_node(
3051 root,
3052 Op::Layout(LayoutOp::Box {
3053 width: Some(320.0),
3054 height: Some(240.0),
3055 min_width: None,
3056 max_width: None,
3057 min_height: None,
3058 max_height: None,
3059 padding: [0.0, 0.0, 0.0, 0.0],
3060 flex_grow: 0.0,
3061 flex_shrink: 0.0,
3062 aspect_ratio: None,
3063 }),
3064 vec![left_paint, outer_group],
3065 );
3066 ir.set_root(root);
3067
3068 let mut pipeline = Pipeline::new();
3069 let mut layout_engine = LayoutEngine::new();
3070 let scroll = ScrollStateMap::default();
3071 pipeline.replace_ir(ir, &Env::default());
3072 pipeline
3073 .ensure_layout(
3074 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3075 &mut layout_engine,
3076 &scroll,
3077 )
3078 .unwrap();
3079 pipeline
3080 .prepare_current(
3081 LayoutSize {
3082 width: 320.0,
3083 height: 240.0,
3084 },
3085 LayoutSize {
3086 width: 320.0,
3087 height: 240.0,
3088 },
3089 false,
3090 &scroll,
3091 &Default::default(),
3092 &Default::default(),
3093 &Default::default(),
3094 )
3095 .unwrap();
3096
3097 let plans = pipeline.texture_compositor_plans();
3098 assert!(!plans.is_empty());
3099 assert!(
3100 plans.iter().any(|plan| !plan.children.is_empty()),
3101 "expected at least one retained texture plan to extract nested dynamic descendants"
3102 );
3103 }
3104
3105 #[test]
3106 fn resize_preview_keeps_texture_compositor_root_transform() {
3107 let mut ir = CoreIR::new();
3108 let left = NodeId::derived(14, &[0]);
3109 let right = NodeId::derived(14, &[1]);
3110 let root = NodeId::derived(14, &[2]);
3111
3112 ir.add_node(
3113 left,
3114 Op::Paint(PaintOp::DrawRect {
3115 fill: Some(Fill::Solid(Color {
3116 r: 80,
3117 g: 80,
3118 b: 80,
3119 a: 255,
3120 })),
3121 stroke: None,
3122 corner_radius: 0.0,
3123 shadow: None,
3124 }),
3125 vec![],
3126 );
3127 ir.add_node(
3128 right,
3129 Op::Paint(PaintOp::DrawRect {
3130 fill: Some(Fill::Solid(Color {
3131 r: 180,
3132 g: 180,
3133 b: 180,
3134 a: 255,
3135 })),
3136 stroke: None,
3137 corner_radius: 0.0,
3138 shadow: None,
3139 }),
3140 vec![],
3141 );
3142 ir.add_node(
3143 root,
3144 Op::Layout(LayoutOp::Box {
3145 width: Some(300.0),
3146 height: Some(200.0),
3147 min_width: None,
3148 max_width: None,
3149 min_height: None,
3150 max_height: None,
3151 padding: [0.0, 0.0, 0.0, 0.0],
3152 flex_grow: 0.0,
3153 flex_shrink: 0.0,
3154 aspect_ratio: None,
3155 }),
3156 vec![left, right],
3157 );
3158 ir.set_root(root);
3159
3160 let mut pipeline = Pipeline::new();
3161 let mut layout_engine = LayoutEngine::new();
3162 let scroll = ScrollStateMap::default();
3163 pipeline.replace_ir(ir, &Env::default());
3164 pipeline
3165 .ensure_layout(
3166 LayoutRect::new(0.0, 0.0, 300.0, 200.0),
3167 &mut layout_engine,
3168 &scroll,
3169 )
3170 .unwrap();
3171 pipeline
3172 .prepare_current(
3173 LayoutSize {
3174 width: 540.0,
3175 height: 360.0,
3176 },
3177 LayoutSize {
3178 width: 300.0,
3179 height: 200.0,
3180 },
3181 true,
3182 &scroll,
3183 &Default::default(),
3184 &Default::default(),
3185 &Default::default(),
3186 )
3187 .unwrap();
3188
3189 assert!(pipeline.texture_compositor_root_transform().is_none());
3190 assert!(!pipeline.texture_compositor_plans().is_empty());
3191 }
3192
3193 #[test]
3194 fn scroll_only_layers_patch_retained_transforms_after_offset_changes() {
3195 let mut ir = CoreIR::new();
3196 let content = NodeId::derived(15, &[0]);
3197 let scroll = NodeId::derived(15, &[1]);
3198 let root = NodeId::derived(15, &[2]);
3199
3200 ir.add_node(
3201 content,
3202 Op::Paint(PaintOp::DrawRect {
3203 fill: Some(Fill::Solid(Color {
3204 r: 120,
3205 g: 120,
3206 b: 220,
3207 a: 255,
3208 })),
3209 stroke: None,
3210 corner_radius: 0.0,
3211 shadow: None,
3212 }),
3213 vec![],
3214 );
3215 ir.add_node(
3216 scroll,
3217 Op::Layout(LayoutOp::Scroll {
3218 direction: fission_ir::FlexDirection::Column,
3219 show_scrollbar: true,
3220 width: Some(320.0),
3221 height: Some(240.0),
3222 min_width: None,
3223 max_width: None,
3224 min_height: None,
3225 max_height: None,
3226 padding: [0.0, 0.0, 0.0, 0.0],
3227 flex_grow: 0.0,
3228 flex_shrink: 0.0,
3229 }),
3230 vec![content],
3231 );
3232 ir.add_node(
3233 root,
3234 Op::Layout(LayoutOp::Box {
3235 width: Some(320.0),
3236 height: Some(240.0),
3237 min_width: None,
3238 max_width: None,
3239 min_height: None,
3240 max_height: None,
3241 padding: [0.0, 0.0, 0.0, 0.0],
3242 flex_grow: 0.0,
3243 flex_shrink: 0.0,
3244 aspect_ratio: None,
3245 }),
3246 vec![scroll],
3247 );
3248 ir.set_root(root);
3249
3250 let mut pipeline = Pipeline::new();
3251 let mut layout_engine = LayoutEngine::new();
3252 let scroll0 = ScrollStateMap::default();
3253 pipeline.replace_ir(ir, &Env::default());
3254 pipeline
3255 .ensure_layout(
3256 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3257 &mut layout_engine,
3258 &scroll0,
3259 )
3260 .unwrap();
3261 pipeline
3262 .prepare_current(
3263 LayoutSize {
3264 width: 320.0,
3265 height: 240.0,
3266 },
3267 LayoutSize {
3268 width: 320.0,
3269 height: 240.0,
3270 },
3271 false,
3272 &scroll0,
3273 &Default::default(),
3274 &Default::default(),
3275 &Default::default(),
3276 )
3277 .unwrap();
3278
3279 let mut scroll1 = ScrollStateMap::default();
3280 scroll1.set_offset(scroll, 180.0);
3281 pipeline
3282 .prepare_current(
3283 LayoutSize {
3284 width: 320.0,
3285 height: 240.0,
3286 },
3287 LayoutSize {
3288 width: 320.0,
3289 height: 240.0,
3290 },
3291 false,
3292 &scroll1,
3293 &Default::default(),
3294 &Default::default(),
3295 &Default::default(),
3296 )
3297 .unwrap();
3298
3299 fn find_layer_by_node(
3300 node: &fission_render::RenderNode,
3301 node_id: NodeId,
3302 ) -> Option<&fission_render::RenderLayer> {
3303 match node {
3304 fission_render::RenderNode::Paint(_) => None,
3305 fission_render::RenderNode::Layer(layer) => {
3306 if layer.node_id == Some(node_id) {
3307 return Some(layer);
3308 }
3309 for child in &layer.children {
3310 if let Some(found) = find_layer_by_node(child, node_id) {
3311 return Some(found);
3312 }
3313 }
3314 None
3315 }
3316 }
3317 }
3318
3319 let scroll_layer = pipeline
3320 .retained_scene()
3321 .and_then(|scene| {
3322 scene
3323 .roots
3324 .iter()
3325 .find_map(|node| find_layer_by_node(node, scroll))
3326 })
3327 .expect("expected a retained scroll layer");
3328 assert!(
3329 scroll_layer.style.transform.is_none(),
3330 "scrollbar chrome must not inherit the content scroll transform"
3331 );
3332 let transform = scroll_layer
3333 .children
3334 .iter()
3335 .find_map(|child| match child {
3336 fission_render::RenderNode::Layer(layer) => layer.style.transform,
3337 fission_render::RenderNode::Paint(_) => None,
3338 })
3339 .expect("scroll content layer should carry a compositor transform");
3340 assert!(
3341 (transform[13] + 180.0).abs() <= 0.01,
3342 "expected retained content transform to patch to -180, got {}",
3343 transform[13]
3344 );
3345 }
3346
3347 #[test]
3348 fn scrollbar_thumb_patches_after_scroll_offset_changes() {
3349 let mut ir = CoreIR::new();
3350 let fill = NodeId::derived(18, &[0]);
3351 let content = NodeId::derived(18, &[1]);
3352 let scroll = NodeId::derived(18, &[2]);
3353 let root = NodeId::derived(18, &[3]);
3354
3355 ir.add_node(
3356 fill,
3357 Op::Paint(PaintOp::DrawRect {
3358 fill: Some(Fill::Solid(Color {
3359 r: 120,
3360 g: 120,
3361 b: 220,
3362 a: 255,
3363 })),
3364 stroke: None,
3365 corner_radius: 0.0,
3366 shadow: None,
3367 }),
3368 vec![],
3369 );
3370 ir.add_node(
3371 content,
3372 Op::Layout(LayoutOp::Box {
3373 width: Some(320.0),
3374 height: Some(640.0),
3375 min_width: None,
3376 max_width: None,
3377 min_height: None,
3378 max_height: None,
3379 padding: [0.0, 0.0, 0.0, 0.0],
3380 flex_grow: 0.0,
3381 flex_shrink: 0.0,
3382 aspect_ratio: None,
3383 }),
3384 vec![fill],
3385 );
3386 ir.add_node(
3387 scroll,
3388 Op::Layout(LayoutOp::Scroll {
3389 direction: fission_ir::FlexDirection::Column,
3390 show_scrollbar: true,
3391 width: Some(320.0),
3392 height: Some(240.0),
3393 min_width: None,
3394 max_width: None,
3395 min_height: None,
3396 max_height: None,
3397 padding: [0.0, 0.0, 0.0, 0.0],
3398 flex_grow: 0.0,
3399 flex_shrink: 0.0,
3400 }),
3401 vec![content],
3402 );
3403 ir.add_node(
3404 root,
3405 Op::Layout(LayoutOp::Box {
3406 width: Some(320.0),
3407 height: Some(240.0),
3408 min_width: None,
3409 max_width: None,
3410 min_height: None,
3411 max_height: None,
3412 padding: [0.0, 0.0, 0.0, 0.0],
3413 flex_grow: 0.0,
3414 flex_shrink: 0.0,
3415 aspect_ratio: None,
3416 }),
3417 vec![scroll],
3418 );
3419 ir.set_root(root);
3420
3421 let mut pipeline = Pipeline::new();
3422 let mut layout_engine = LayoutEngine::new();
3423 let scroll0 = ScrollStateMap::default();
3424 pipeline.replace_ir(ir, &Env::default());
3425 pipeline
3426 .ensure_layout(
3427 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3428 &mut layout_engine,
3429 &scroll0,
3430 )
3431 .unwrap();
3432 pipeline
3433 .prepare_current(
3434 LayoutSize::new(320.0, 240.0),
3435 LayoutSize::new(320.0, 240.0),
3436 false,
3437 &scroll0,
3438 &Default::default(),
3439 &Default::default(),
3440 &Default::default(),
3441 )
3442 .unwrap();
3443 let initial_thumb_y = scrollbar_thumb_y(pipeline.retained_scene().unwrap(), scroll)
3444 .expect("initial scrollbar thumb");
3445
3446 let mut scroll1 = ScrollStateMap::default();
3447 scroll1.set_offset(scroll, 200.0);
3448 pipeline
3449 .prepare_current(
3450 LayoutSize::new(320.0, 240.0),
3451 LayoutSize::new(320.0, 240.0),
3452 false,
3453 &scroll1,
3454 &Default::default(),
3455 &Default::default(),
3456 &Default::default(),
3457 )
3458 .unwrap();
3459 let moved_thumb_y = scrollbar_thumb_y(pipeline.retained_scene().unwrap(), scroll)
3460 .expect("moved scrollbar thumb");
3461
3462 assert!(
3463 moved_thumb_y > initial_thumb_y,
3464 "body scroll must patch the retained scrollbar thumb, before={initial_thumb_y}, after={moved_thumb_y}"
3465 );
3466
3467 fn scrollbar_thumb_y(scene: &fission_render::RenderScene, scroll: NodeId) -> Option<f32> {
3468 fn find(node: &fission_render::RenderNode, scroll: NodeId) -> Option<f32> {
3469 match node {
3470 fission_render::RenderNode::Paint(list) => list.ops.iter().find_map(|op| {
3471 if let fission_render::DisplayOp::DrawRect { rect, node_id, .. } = op {
3472 if *node_id == Some(scroll)
3473 && (rect.width() - 6.0).abs() <= 0.01
3474 && rect.height() < 200.0
3475 {
3476 return Some(rect.origin.y);
3477 }
3478 }
3479 None
3480 }),
3481 fission_render::RenderNode::Layer(layer) => {
3482 layer.children.iter().find_map(|child| find(child, scroll))
3483 }
3484 }
3485 }
3486 scene.roots.iter().find_map(|root| find(root, scroll))
3487 }
3488 }
3489
3490 #[test]
3491 fn overflowing_scroll_nodes_emit_visible_scroll_rails() {
3492 let mut ir = CoreIR::new();
3493 let fill = NodeId::derived(16, &[0]);
3494 let content = NodeId::derived(16, &[1]);
3495 let scroll = NodeId::derived(16, &[2]);
3496 let root = NodeId::derived(16, &[3]);
3497
3498 ir.add_node(
3499 fill,
3500 Op::Paint(PaintOp::DrawRect {
3501 fill: Some(Fill::Solid(Color {
3502 r: 80,
3503 g: 120,
3504 b: 220,
3505 a: 255,
3506 })),
3507 stroke: None,
3508 corner_radius: 0.0,
3509 shadow: None,
3510 }),
3511 vec![],
3512 );
3513 ir.add_node(
3514 content,
3515 Op::Layout(LayoutOp::Box {
3516 width: Some(320.0),
3517 height: Some(640.0),
3518 min_width: None,
3519 max_width: None,
3520 min_height: None,
3521 max_height: None,
3522 padding: [0.0, 0.0, 0.0, 0.0],
3523 flex_grow: 0.0,
3524 flex_shrink: 0.0,
3525 aspect_ratio: None,
3526 }),
3527 vec![fill],
3528 );
3529 ir.add_node(
3530 scroll,
3531 Op::Layout(LayoutOp::Scroll {
3532 direction: fission_ir::FlexDirection::Column,
3533 show_scrollbar: true,
3534 width: Some(320.0),
3535 height: Some(240.0),
3536 min_width: None,
3537 max_width: None,
3538 min_height: None,
3539 max_height: None,
3540 padding: [0.0, 0.0, 0.0, 0.0],
3541 flex_grow: 0.0,
3542 flex_shrink: 0.0,
3543 }),
3544 vec![content],
3545 );
3546 ir.add_node(
3547 root,
3548 Op::Layout(LayoutOp::Box {
3549 width: Some(320.0),
3550 height: Some(240.0),
3551 min_width: None,
3552 max_width: None,
3553 min_height: None,
3554 max_height: None,
3555 padding: [0.0, 0.0, 0.0, 0.0],
3556 flex_grow: 0.0,
3557 flex_shrink: 0.0,
3558 aspect_ratio: None,
3559 }),
3560 vec![scroll],
3561 );
3562 ir.set_root(root);
3563
3564 let mut pipeline = Pipeline::new();
3565 let mut layout_engine = LayoutEngine::new();
3566 let scroll_map = ScrollStateMap::default();
3567 pipeline.replace_ir(ir, &Env::default());
3568 pipeline
3569 .ensure_layout(
3570 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3571 &mut layout_engine,
3572 &scroll_map,
3573 )
3574 .unwrap();
3575 pipeline
3576 .prepare_current(
3577 LayoutSize {
3578 width: 320.0,
3579 height: 240.0,
3580 },
3581 LayoutSize {
3582 width: 320.0,
3583 height: 240.0,
3584 },
3585 false,
3586 &scroll_map,
3587 &Default::default(),
3588 &Default::default(),
3589 &Default::default(),
3590 )
3591 .unwrap();
3592
3593 fn count_scroll_rails(node: &fission_render::RenderNode, scroll: NodeId) -> usize {
3594 match node {
3595 fission_render::RenderNode::Paint(list) => list
3596 .ops
3597 .iter()
3598 .filter(|op| match op {
3599 fission_render::DisplayOp::DrawRect { rect, node_id, .. } => {
3600 *node_id == Some(scroll)
3601 && (rect.width() - 6.0).abs() <= 0.01
3602 && rect.height() >= 200.0
3603 }
3604 _ => false,
3605 })
3606 .count(),
3607 fission_render::RenderNode::Layer(layer) => layer
3608 .children
3609 .iter()
3610 .map(|child| count_scroll_rails(child, scroll))
3611 .sum(),
3612 }
3613 }
3614
3615 let rail_count: usize = pipeline
3616 .retained_scene()
3617 .expect("retained scene")
3618 .roots
3619 .iter()
3620 .map(|node| count_scroll_rails(node, scroll))
3621 .sum();
3622 assert!(
3623 rail_count > 0,
3624 "expected an overflow rail for the scroll node"
3625 );
3626 }
3627}