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::DrawPath { path, fill, stroke }) => {
2091 list.push(DisplayOp::DrawPath {
2092 path: path.clone(),
2093 fill: fill.as_ref().map(map_fill),
2094 stroke: stroke.as_ref().map(map_stroke),
2095 bounds: rect,
2096 node_id: Some(node_id),
2097 });
2098 }
2099 Op::Paint(fission_ir::PaintOp::DrawSvg {
2100 content,
2101 fill,
2102 stroke,
2103 }) => {
2104 list.push(DisplayOp::DrawSvg {
2105 content: content.clone(),
2106 fill: fill.as_ref().map(map_fill),
2107 stroke: stroke.as_ref().map(map_stroke),
2108 bounds: rect,
2109 node_id: Some(node_id),
2110 });
2111 }
2112 Op::Layout(LayoutOp::Embed {
2113 kind, widget_id, ..
2114 }) => {
2115 list.push(DisplayOp::DrawSurface {
2116 rect,
2117 surface_id: embed_surface_id(kind, *widget_id),
2118 position: 0,
2119 bounds: rect,
2120 node_id: Some(node_id),
2121 });
2122 }
2123 _ => {}
2124 }
2125 if list.ops.is_empty() {
2126 None
2127 } else {
2128 Some(list)
2129 }
2130}
2131
2132fn build_scrollbar_paint(
2133 ir: &CoreIR,
2134 node_id: NodeId,
2135 snapshot: &LayoutSnapshot,
2136 scroll_map: &ScrollStateMap,
2137) -> Option<DisplayList> {
2138 let geometry = scrollbar_geometry_for_node(ir, snapshot, scroll_map, node_id)?;
2139 let rail_fill = Some(Fill::Solid(RenderColor {
2140 r: 160,
2141 g: 168,
2142 b: 180,
2143 a: 80,
2144 }));
2145 let thumb_fill = Some(Fill::Solid(RenderColor {
2146 r: 82,
2147 g: 91,
2148 b: 108,
2149 a: 190,
2150 }));
2151 let mut list = DisplayList::new(geometry.rail_rect);
2152 let corner_radius = fission_core::scrollbar::SCROLLBAR_THICKNESS / 2.0;
2153
2154 list.push(DisplayOp::DrawRect {
2155 rect: geometry.rail_rect,
2156 fill: rail_fill,
2157 stroke: None,
2158 corner_radius,
2159 shadow: None,
2160 bounds: geometry.rail_rect,
2161 node_id: Some(node_id),
2162 });
2163 list.push(DisplayOp::DrawRect {
2164 rect: geometry.thumb_rect,
2165 fill: thumb_fill,
2166 stroke: None,
2167 corner_radius,
2168 shadow: None,
2169 bounds: geometry.thumb_rect,
2170 node_id: Some(node_id),
2171 });
2172
2173 Some(list)
2174}
2175
2176fn resolve_composite_scalar(
2177 scalar: Option<&fission_ir::CompositeScalar>,
2178 animation_map: &AnimationStateMap,
2179 property: AnimationPropertyId,
2180) -> Option<f32> {
2181 let scalar = scalar?;
2182 Some(resolve_scalar_value(scalar, animation_map, property))
2183}
2184
2185fn resolve_scalar_value(
2186 scalar: &fission_ir::CompositeScalar,
2187 animation_map: &AnimationStateMap,
2188 property: AnimationPropertyId,
2189) -> f32 {
2190 scalar
2191 .animation_target
2192 .and_then(|target| animation_map.values.get(&(target, property)).copied())
2193 .unwrap_or(scalar.base)
2194}
2195
2196fn composite_transform_matrix(
2197 rect: LayoutRect,
2198 translate_x: f32,
2199 translate_y: f32,
2200 scale: f32,
2201 rotation: f32,
2202) -> [f32; 16] {
2203 let center_x = rect.origin.x + rect.size.width * 0.5;
2204 let center_y = rect.origin.y + rect.size.height * 0.5;
2205
2206 let to_center = translation_matrix(center_x, center_y);
2207 let from_center = translation_matrix(-center_x, -center_y);
2208 let scale_matrix = scale_matrix(scale);
2209 let rotation_matrix = rotation_z_matrix(rotation);
2210 let animated_translate = translation_matrix(translate_x, translate_y);
2211
2212 multiply_matrix(
2213 animated_translate,
2214 multiply_matrix(
2215 to_center,
2216 multiply_matrix(rotation_matrix, multiply_matrix(scale_matrix, from_center)),
2217 ),
2218 )
2219}
2220
2221fn translation_matrix(tx: f32, ty: f32) -> [f32; 16] {
2222 [
2223 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,
2224 ]
2225}
2226
2227fn scale_matrix(scale: f32) -> [f32; 16] {
2228 [
2229 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,
2230 ]
2231}
2232
2233fn rotation_z_matrix(radians: f32) -> [f32; 16] {
2234 let sin = radians.sin();
2235 let cos = radians.cos();
2236 [
2237 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,
2238 ]
2239}
2240
2241fn multiply_matrix(a: [f32; 16], b: [f32; 16]) -> [f32; 16] {
2242 let mut out = [0.0; 16];
2243 for row in 0..4 {
2244 for col in 0..4 {
2245 let mut sum = 0.0;
2246 for k in 0..4 {
2247 sum += a[row * 4 + k] * b[k * 4 + col];
2248 }
2249 out[row * 4 + col] = sum;
2250 }
2251 }
2252 out
2253}
2254
2255fn is_identity_matrix(matrix: &[f32; 16]) -> bool {
2256 const IDENTITY: [f32; 16] = [
2257 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,
2258 ];
2259 matrix
2260 .iter()
2261 .zip(IDENTITY.iter())
2262 .all(|(lhs, rhs)| (*lhs - *rhs).abs() <= 0.000_1)
2263}
2264
2265#[cfg(test)]
2266fn scroll_offsets_changed(prev: &HashMap<NodeId, u32>, scroll_map: &ScrollStateMap) -> bool {
2267 if prev.len() != scroll_map.offsets.len() {
2268 return true;
2269 }
2270
2271 scroll_map
2272 .offsets
2273 .iter()
2274 .any(|(id, offset)| prev.get(id).copied() != Some(offset.to_bits()))
2275}
2276
2277impl SnapshotProvider for Pipeline {
2278 fn snapshot(&self, kind: SnapshotKind) -> Option<SnapshotBlob> {
2279 match kind {
2280 SnapshotKind::Layout => self.last_snapshot.as_ref().and_then(|snap| {
2281 serde_json::to_string_pretty(snap)
2282 .ok()
2283 .map(|json| SnapshotBlob { kind, json })
2284 }),
2285 }
2286 }
2287}
2288
2289fn map_fill(f: &fission_ir::op::Fill) -> Fill {
2290 match f {
2291 fission_ir::op::Fill::Solid(c) => Fill::Solid(RenderColor {
2292 r: c.r,
2293 g: c.g,
2294 b: c.b,
2295 a: c.a,
2296 }),
2297 fission_ir::op::Fill::LinearGradient { start, end, stops } => Fill::LinearGradient {
2298 start: *start,
2299 end: *end,
2300 stops: stops
2301 .iter()
2302 .map(|(o, c)| {
2303 (
2304 *o,
2305 RenderColor {
2306 r: c.r,
2307 g: c.g,
2308 b: c.b,
2309 a: c.a,
2310 },
2311 )
2312 })
2313 .collect(),
2314 },
2315 fission_ir::op::Fill::RadialGradient {
2316 center,
2317 radius,
2318 stops,
2319 } => Fill::RadialGradient {
2320 center: *center,
2321 radius: *radius,
2322 stops: stops
2323 .iter()
2324 .map(|(o, c)| {
2325 (
2326 *o,
2327 RenderColor {
2328 r: c.r,
2329 g: c.g,
2330 b: c.b,
2331 a: c.a,
2332 },
2333 )
2334 })
2335 .collect(),
2336 },
2337 }
2338}
2339
2340fn map_stroke(s: &fission_ir::op::Stroke) -> Stroke {
2341 Stroke {
2342 fill: map_fill(&s.fill),
2343 width: s.width,
2344 dash_array: s.dash_array.clone(),
2345 line_cap: match s.line_cap {
2346 fission_ir::op::LineCap::Butt => fission_render::LineCap::Butt,
2347 fission_ir::op::LineCap::Round => fission_render::LineCap::Round,
2348 fission_ir::op::LineCap::Square => fission_render::LineCap::Square,
2349 },
2350 line_join: match s.line_join {
2351 fission_ir::op::LineJoin::Miter => fission_render::LineJoin::Miter,
2352 fission_ir::op::LineJoin::Round => fission_render::LineJoin::Round,
2353 fission_ir::op::LineJoin::Bevel => fission_render::LineJoin::Bevel,
2354 },
2355 }
2356}
2357
2358fn translate_rect(rect: LayoutRect, offset: LayoutPoint) -> LayoutRect {
2359 LayoutRect {
2360 origin: LayoutPoint::new(rect.origin.x + offset.x, rect.origin.y + offset.y),
2361 size: rect.size,
2362 }
2363}
2364
2365#[cfg(test)]
2366mod tests {
2367 use super::{build_local_paint_list, scroll_offsets_changed, InvalidationSet, Pipeline};
2368 use fission_core::env::Env;
2369 use fission_core::registry::AnimationPropertyId;
2370 use fission_core::ScrollStateMap;
2371 use fission_ir::op::{Color, Fill, RichTextAnnotation, TextRun, TextStyle};
2372 use fission_ir::semantics::ActionTrigger;
2373 use fission_ir::{
2374 ActionEntry, CompositeScalar, CompositeStyle, CoreIR, EmbedKind, LayoutOp, NodeId, Op,
2375 PaintOp, WidgetNodeId,
2376 };
2377 use fission_layout::{LayoutEngine, LayoutRect, LayoutSize};
2378 use fission_render::{DisplayOp, RenderScene, Renderer};
2379 use std::collections::HashMap;
2380 use std::sync::Arc;
2381
2382 struct NullRenderer;
2383
2384 impl Renderer for NullRenderer {
2385 fn render_scene(&mut self, _scene: &RenderScene) -> anyhow::Result<()> {
2386 Ok(())
2387 }
2388 }
2389
2390 fn two_child_layout_ir(second_width: f32) -> CoreIR {
2391 let root = NodeId::derived(50, &[0]);
2392 let first = NodeId::derived(50, &[1]);
2393 let second = NodeId::derived(50, &[2]);
2394 let mut ir = CoreIR::new();
2395 ir.add_node(
2396 first,
2397 Op::Layout(LayoutOp::Box {
2398 width: Some(40.0),
2399 height: Some(20.0),
2400 min_width: None,
2401 max_width: None,
2402 min_height: None,
2403 max_height: None,
2404 padding: [0.0; 4],
2405 flex_grow: 0.0,
2406 flex_shrink: 1.0,
2407 aspect_ratio: None,
2408 }),
2409 vec![],
2410 );
2411 ir.add_node(
2412 second,
2413 Op::Layout(LayoutOp::Box {
2414 width: Some(second_width),
2415 height: Some(20.0),
2416 min_width: None,
2417 max_width: None,
2418 min_height: None,
2419 max_height: None,
2420 padding: [0.0; 4],
2421 flex_grow: 0.0,
2422 flex_shrink: 1.0,
2423 aspect_ratio: None,
2424 }),
2425 vec![],
2426 );
2427 ir.add_node(
2428 root,
2429 Op::Layout(LayoutOp::Flex {
2430 direction: fission_ir::FlexDirection::Column,
2431 wrap: fission_ir::op::FlexWrap::NoWrap,
2432 flex_grow: 0.0,
2433 flex_shrink: 1.0,
2434 padding: [0.0; 4],
2435 gap: Some(4.0),
2436 align_items: fission_ir::op::AlignItems::Start,
2437 justify_content: fission_ir::op::JustifyContent::Start,
2438 }),
2439 vec![first, second],
2440 );
2441 ir.set_root(root);
2442 ir
2443 }
2444
2445 #[test]
2446 fn unchanged_scroll_offsets_do_not_invalidate_cache() {
2447 let id = NodeId::derived(1, &[0]);
2448 let mut prev = HashMap::new();
2449 prev.insert(id, 12.5f32.to_bits());
2450 let mut scroll = ScrollStateMap::default();
2451 scroll.set_offset(id, 12.5);
2452 assert!(!scroll_offsets_changed(&prev, &scroll));
2453 }
2454
2455 #[test]
2456 fn changed_scroll_offsets_invalidate_cache() {
2457 let id = NodeId::derived(2, &[0]);
2458 let mut prev = HashMap::new();
2459 prev.insert(id, 0.0f32.to_bits());
2460 let mut scroll = ScrollStateMap::default();
2461 scroll.set_offset(id, 4.0);
2462 assert!(scroll_offsets_changed(&prev, &scroll));
2463 }
2464
2465 #[test]
2466 fn incremental_layout_keeps_rebuild_telemetry_honest() {
2467 let mut pipeline = Pipeline::new();
2468 let mut layout_engine = LayoutEngine::new();
2469 let scroll = ScrollStateMap::default();
2470
2471 pipeline.replace_ir(two_child_layout_ir(60.0), &Env::default());
2472 let first_pass = pipeline
2473 .ensure_layout(
2474 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2475 &mut layout_engine,
2476 &scroll,
2477 )
2478 .expect("initial layout");
2479 assert_eq!(first_pass, pipeline.layout_input_nodes.len());
2480 assert_eq!(pipeline.layout_full_rebuild_count, 1);
2481
2482 pipeline.replace_ir(two_child_layout_ir(90.0), &Env::default());
2483 let second_pass = pipeline
2484 .ensure_layout(
2485 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2486 &mut layout_engine,
2487 &scroll,
2488 )
2489 .expect("incremental layout");
2490
2491 assert_eq!(second_pass, 1);
2492 assert_eq!(pipeline.layout_full_rebuild_count, 1);
2493 assert!(pipeline.pending_layout_dirty_nodes.is_empty());
2494 }
2495
2496 #[test]
2497 fn rich_text_annotations_flow_into_display_ops() {
2498 let node_id = NodeId::derived(9, &[0]);
2499 let mut ir = CoreIR::new();
2500 ir.add_node(
2501 node_id,
2502 Op::Paint(PaintOp::DrawRichText {
2503 runs: vec![TextRun {
2504 text: "docs".into(),
2505 style: TextStyle {
2506 font_size: 14.0,
2507 color: Color::BLACK,
2508 underline: false,
2509 font_family: None,
2510 locale: None,
2511 font_weight: 400,
2512 font_style: fission_ir::op::FontStyle::Normal,
2513 line_height: None,
2514 letter_spacing: 0.0,
2515 background_color: None,
2516 },
2517 }],
2518 wrap: true,
2519 caret_index: None,
2520 caret_color: None,
2521 caret_width: None,
2522 caret_height: None,
2523 caret_radius: None,
2524 paragraph_style: None,
2525 }),
2526 vec![],
2527 );
2528 ir.custom_render_objects.insert(
2529 node_id,
2530 Arc::new(vec![RichTextAnnotation {
2531 range: 0..4,
2532 semantics_label: Some("Documentation".into()),
2533 semantics_identifier: Some("docs-link".into()),
2534 spell_out: Some(true),
2535 mouse_cursor: Some(fission_ir::op::MouseCursor::Pointer),
2536 actions: vec![ActionEntry {
2537 trigger: ActionTrigger::Default,
2538 action_id: 7,
2539 payload_data: Some(vec![1, 2, 3]),
2540 }],
2541 }]),
2542 );
2543
2544 let node = ir.nodes.get(&node_id).expect("paint node");
2545 let list =
2546 build_local_paint_list(&ir, node_id, node, LayoutRect::new(0.0, 0.0, 160.0, 40.0))
2547 .expect("display list");
2548 match list.ops.first() {
2549 Some(DisplayOp::DrawRichText { annotations, .. }) => {
2550 assert_eq!(annotations.len(), 1);
2551 assert_eq!(annotations[0].range, 0..4);
2552 assert_eq!(
2553 annotations[0].semantics_identifier.as_deref(),
2554 Some("docs-link")
2555 );
2556 }
2557 other => panic!("expected rich text op, got {other:?}"),
2558 }
2559 }
2560
2561 #[test]
2562 fn embed_layout_ops_flow_into_surface_display_ops() {
2563 let node_id = NodeId::derived(14, &[0]);
2564 let widget_id = WidgetNodeId::explicit("embed.surface");
2565 let mut ir = CoreIR::new();
2566 ir.add_node(
2567 node_id,
2568 Op::Layout(LayoutOp::Embed {
2569 kind: EmbedKind::Web,
2570 widget_id,
2571 width: Some(320.0),
2572 height: Some(180.0),
2573 }),
2574 vec![],
2575 );
2576
2577 let node = ir.nodes.get(&node_id).expect("embed node");
2578 let rect = LayoutRect::new(12.0, 24.0, 320.0, 180.0);
2579 let list = build_local_paint_list(&ir, node_id, node, rect).expect("display list");
2580
2581 match list.ops.first() {
2582 Some(DisplayOp::DrawSurface {
2583 rect: surface_rect,
2584 bounds,
2585 node_id: Some(surface_node_id),
2586 ..
2587 }) => {
2588 assert_eq!(*surface_rect, rect);
2589 assert_eq!(*bounds, rect);
2590 assert_eq!(*surface_node_id, node_id);
2591 }
2592 other => panic!("expected surface display op, got {other:?}"),
2593 }
2594 }
2595
2596 #[test]
2597 fn compositor_bound_opacity_animation_is_composite_only() {
2598 let mut ir = CoreIR::new();
2599 let child = NodeId::derived(10, &[1]);
2600 let root = NodeId::derived(10, &[0]);
2601 ir.add_node(child, Op::Layout(LayoutOp::AbsoluteFill), vec![]);
2602 ir.add_node_with_composite(
2603 root,
2604 Op::Structural(fission_ir::StructuralOp::Group { stable_hash: 1 }),
2605 CompositeStyle {
2606 opacity: Some(CompositeScalar::new(0.0).animated(WidgetNodeId::explicit("fade"))),
2607 ..Default::default()
2608 },
2609 vec![child],
2610 );
2611 ir.set_root(root);
2612
2613 let mut pipeline = Pipeline::new();
2614 pipeline.replace_ir(ir, &Env::default());
2615 let invalidation = pipeline.classify_animation_updates(&[(
2616 WidgetNodeId::explicit("fade"),
2617 AnimationPropertyId::Opacity,
2618 )]);
2619 assert_eq!(
2620 invalidation,
2621 InvalidationSet {
2622 build: false,
2623 layout: false,
2624 paint: false,
2625 composite: true,
2626 }
2627 );
2628 }
2629
2630 #[test]
2631 fn unbound_custom_animation_requires_build() {
2632 let pipeline = Pipeline::new();
2633 let invalidation = pipeline.classify_animation_updates(&[(
2634 WidgetNodeId::explicit("custom"),
2635 AnimationPropertyId::custom("phase"),
2636 )]);
2637 assert!(invalidation.build);
2638 assert!(invalidation.layout);
2639 }
2640
2641 #[test]
2642 fn compositor_bound_translate_animation_is_composite_only() {
2643 let mut ir = CoreIR::new();
2644 let child = NodeId::derived(11, &[1]);
2645 let root = NodeId::derived(11, &[0]);
2646 ir.add_node(
2647 child,
2648 Op::Paint(PaintOp::DrawRect {
2649 fill: Some(Fill::Solid(Color {
2650 r: 0,
2651 g: 0,
2652 b: 0,
2653 a: 255,
2654 })),
2655 stroke: None,
2656 corner_radius: 0.0,
2657 shadow: None,
2658 }),
2659 vec![],
2660 );
2661 ir.add_node_with_composite(
2662 root,
2663 Op::Layout(LayoutOp::Box {
2664 width: Some(120.0),
2665 height: Some(64.0),
2666 min_width: None,
2667 max_width: None,
2668 min_height: None,
2669 max_height: None,
2670 padding: [0.0, 0.0, 0.0, 0.0],
2671 flex_grow: 0.0,
2672 flex_shrink: 0.0,
2673 aspect_ratio: None,
2674 }),
2675 CompositeStyle {
2676 translate_x: Some(
2677 CompositeScalar::new(12.0).animated(WidgetNodeId::explicit("slide")),
2678 ),
2679 ..Default::default()
2680 },
2681 vec![child],
2682 );
2683 ir.set_root(root);
2684
2685 let mut pipeline = Pipeline::new();
2686 pipeline.replace_ir(ir, &Env::default());
2687 let invalidation = pipeline.classify_animation_updates(&[(
2688 WidgetNodeId::explicit("slide"),
2689 AnimationPropertyId::TranslateX,
2690 )]);
2691 assert_eq!(
2692 invalidation,
2693 InvalidationSet {
2694 build: false,
2695 layout: false,
2696 paint: false,
2697 composite: true,
2698 }
2699 );
2700 }
2701
2702 #[test]
2703 fn dynamic_layer_with_static_contents_gets_content_cache_key() {
2704 let mut ir = CoreIR::new();
2705 let child = NodeId::derived(12, &[1]);
2706 let root = NodeId::derived(12, &[0]);
2707 ir.add_node(
2708 child,
2709 Op::Paint(PaintOp::DrawRect {
2710 fill: Some(Fill::Solid(Color {
2711 r: 20,
2712 g: 40,
2713 b: 60,
2714 a: 255,
2715 })),
2716 stroke: None,
2717 corner_radius: 8.0,
2718 shadow: None,
2719 }),
2720 vec![],
2721 );
2722 ir.add_node_with_composite(
2723 root,
2724 Op::Layout(LayoutOp::Box {
2725 width: Some(160.0),
2726 height: Some(72.0),
2727 min_width: None,
2728 max_width: None,
2729 min_height: None,
2730 max_height: None,
2731 padding: [0.0, 0.0, 0.0, 0.0],
2732 flex_grow: 0.0,
2733 flex_shrink: 0.0,
2734 aspect_ratio: None,
2735 }),
2736 CompositeStyle {
2737 opacity: Some(
2738 CompositeScalar::new(0.4).animated(WidgetNodeId::explicit("fade-cache")),
2739 ),
2740 ..Default::default()
2741 },
2742 vec![child],
2743 );
2744 ir.set_root(root);
2745
2746 let mut pipeline = Pipeline::new();
2747 let mut layout_engine = LayoutEngine::new();
2748 let mut renderer = NullRenderer;
2749 let scroll = ScrollStateMap::default();
2750 pipeline.replace_ir(ir, &Env::default());
2751 pipeline
2752 .ensure_layout(
2753 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2754 &mut layout_engine,
2755 &scroll,
2756 )
2757 .unwrap();
2758 pipeline
2759 .render_current(
2760 LayoutSize {
2761 width: 320.0,
2762 height: 240.0,
2763 },
2764 LayoutSize {
2765 width: 320.0,
2766 height: 240.0,
2767 },
2768 false,
2769 &mut renderer,
2770 &scroll,
2771 &Default::default(),
2772 &Default::default(),
2773 &Default::default(),
2774 )
2775 .unwrap();
2776
2777 let scene = pipeline
2778 .retained_scene
2779 .as_ref()
2780 .expect("retained scene missing");
2781 let presentation_root = match scene.roots.first() {
2782 Some(fission_render::RenderNode::Layer(layer)) => layer,
2783 _ => panic!("missing presentation layer"),
2784 };
2785 let animated_layer = match presentation_root.children.first() {
2786 Some(fission_render::RenderNode::Layer(layer)) => layer,
2787 _ => panic!("missing animated layer"),
2788 };
2789
2790 assert!(animated_layer.style.cache_key.is_none());
2791 assert!(animated_layer.style.content_cache_key.is_some());
2792 }
2793
2794 #[test]
2795 fn nested_dynamic_descendant_becomes_child_texture_plan() {
2796 let mut ir = CoreIR::new();
2797 let left_paint = NodeId::derived(13, &[0]);
2798 let animated_paint = NodeId::derived(13, &[1]);
2799 let animated_wrapper = NodeId::derived(13, &[2]);
2800 let outer_static = NodeId::derived(13, &[3]);
2801 let outer_group = NodeId::derived(13, &[4]);
2802 let root = NodeId::derived(13, &[5]);
2803
2804 ir.add_node(
2805 left_paint,
2806 Op::Paint(PaintOp::DrawRect {
2807 fill: Some(Fill::Solid(Color {
2808 r: 10,
2809 g: 10,
2810 b: 10,
2811 a: 255,
2812 })),
2813 stroke: None,
2814 corner_radius: 0.0,
2815 shadow: None,
2816 }),
2817 vec![],
2818 );
2819 ir.add_node(
2820 animated_paint,
2821 Op::Paint(PaintOp::DrawRect {
2822 fill: Some(Fill::Solid(Color {
2823 r: 200,
2824 g: 40,
2825 b: 40,
2826 a: 255,
2827 })),
2828 stroke: None,
2829 corner_radius: 0.0,
2830 shadow: None,
2831 }),
2832 vec![],
2833 );
2834 ir.add_node_with_composite(
2835 animated_wrapper,
2836 Op::Layout(LayoutOp::Box {
2837 width: Some(96.0),
2838 height: Some(96.0),
2839 min_width: None,
2840 max_width: None,
2841 min_height: None,
2842 max_height: None,
2843 padding: [0.0, 0.0, 0.0, 0.0],
2844 flex_grow: 0.0,
2845 flex_shrink: 0.0,
2846 aspect_ratio: None,
2847 }),
2848 CompositeStyle {
2849 opacity: Some(
2850 CompositeScalar::new(0.4).animated(WidgetNodeId::explicit("nested-fade")),
2851 ),
2852 ..Default::default()
2853 },
2854 vec![animated_paint],
2855 );
2856 ir.add_node(
2857 outer_static,
2858 Op::Paint(PaintOp::DrawRect {
2859 fill: Some(Fill::Solid(Color {
2860 r: 20,
2861 g: 100,
2862 b: 180,
2863 a: 255,
2864 })),
2865 stroke: None,
2866 corner_radius: 8.0,
2867 shadow: None,
2868 }),
2869 vec![],
2870 );
2871 ir.add_node(
2872 outer_group,
2873 Op::Layout(LayoutOp::Box {
2874 width: Some(160.0),
2875 height: Some(120.0),
2876 min_width: None,
2877 max_width: None,
2878 min_height: None,
2879 max_height: None,
2880 padding: [0.0, 0.0, 0.0, 0.0],
2881 flex_grow: 0.0,
2882 flex_shrink: 0.0,
2883 aspect_ratio: None,
2884 }),
2885 vec![outer_static, animated_wrapper],
2886 );
2887 ir.add_node(
2888 root,
2889 Op::Layout(LayoutOp::Box {
2890 width: Some(320.0),
2891 height: Some(240.0),
2892 min_width: None,
2893 max_width: None,
2894 min_height: None,
2895 max_height: None,
2896 padding: [0.0, 0.0, 0.0, 0.0],
2897 flex_grow: 0.0,
2898 flex_shrink: 0.0,
2899 aspect_ratio: None,
2900 }),
2901 vec![left_paint, outer_group],
2902 );
2903 ir.set_root(root);
2904
2905 let mut pipeline = Pipeline::new();
2906 let mut layout_engine = LayoutEngine::new();
2907 let scroll = ScrollStateMap::default();
2908 pipeline.replace_ir(ir, &Env::default());
2909 pipeline
2910 .ensure_layout(
2911 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2912 &mut layout_engine,
2913 &scroll,
2914 )
2915 .unwrap();
2916 pipeline
2917 .prepare_current(
2918 LayoutSize {
2919 width: 320.0,
2920 height: 240.0,
2921 },
2922 LayoutSize {
2923 width: 320.0,
2924 height: 240.0,
2925 },
2926 false,
2927 &scroll,
2928 &Default::default(),
2929 &Default::default(),
2930 &Default::default(),
2931 )
2932 .unwrap();
2933
2934 let plans = pipeline.texture_compositor_plans();
2935 assert!(!plans.is_empty());
2936 assert!(
2937 plans.iter().any(|plan| !plan.children.is_empty()),
2938 "expected at least one retained texture plan to extract nested dynamic descendants"
2939 );
2940 }
2941
2942 #[test]
2943 fn resize_preview_keeps_texture_compositor_root_transform() {
2944 let mut ir = CoreIR::new();
2945 let left = NodeId::derived(14, &[0]);
2946 let right = NodeId::derived(14, &[1]);
2947 let root = NodeId::derived(14, &[2]);
2948
2949 ir.add_node(
2950 left,
2951 Op::Paint(PaintOp::DrawRect {
2952 fill: Some(Fill::Solid(Color {
2953 r: 80,
2954 g: 80,
2955 b: 80,
2956 a: 255,
2957 })),
2958 stroke: None,
2959 corner_radius: 0.0,
2960 shadow: None,
2961 }),
2962 vec![],
2963 );
2964 ir.add_node(
2965 right,
2966 Op::Paint(PaintOp::DrawRect {
2967 fill: Some(Fill::Solid(Color {
2968 r: 180,
2969 g: 180,
2970 b: 180,
2971 a: 255,
2972 })),
2973 stroke: None,
2974 corner_radius: 0.0,
2975 shadow: None,
2976 }),
2977 vec![],
2978 );
2979 ir.add_node(
2980 root,
2981 Op::Layout(LayoutOp::Box {
2982 width: Some(300.0),
2983 height: Some(200.0),
2984 min_width: None,
2985 max_width: None,
2986 min_height: None,
2987 max_height: None,
2988 padding: [0.0, 0.0, 0.0, 0.0],
2989 flex_grow: 0.0,
2990 flex_shrink: 0.0,
2991 aspect_ratio: None,
2992 }),
2993 vec![left, right],
2994 );
2995 ir.set_root(root);
2996
2997 let mut pipeline = Pipeline::new();
2998 let mut layout_engine = LayoutEngine::new();
2999 let scroll = ScrollStateMap::default();
3000 pipeline.replace_ir(ir, &Env::default());
3001 pipeline
3002 .ensure_layout(
3003 LayoutRect::new(0.0, 0.0, 300.0, 200.0),
3004 &mut layout_engine,
3005 &scroll,
3006 )
3007 .unwrap();
3008 pipeline
3009 .prepare_current(
3010 LayoutSize {
3011 width: 540.0,
3012 height: 360.0,
3013 },
3014 LayoutSize {
3015 width: 300.0,
3016 height: 200.0,
3017 },
3018 true,
3019 &scroll,
3020 &Default::default(),
3021 &Default::default(),
3022 &Default::default(),
3023 )
3024 .unwrap();
3025
3026 assert!(pipeline.texture_compositor_root_transform().is_none());
3027 assert!(!pipeline.texture_compositor_plans().is_empty());
3028 }
3029
3030 #[test]
3031 fn scroll_only_layers_patch_retained_transforms_after_offset_changes() {
3032 let mut ir = CoreIR::new();
3033 let content = NodeId::derived(15, &[0]);
3034 let scroll = NodeId::derived(15, &[1]);
3035 let root = NodeId::derived(15, &[2]);
3036
3037 ir.add_node(
3038 content,
3039 Op::Paint(PaintOp::DrawRect {
3040 fill: Some(Fill::Solid(Color {
3041 r: 120,
3042 g: 120,
3043 b: 220,
3044 a: 255,
3045 })),
3046 stroke: None,
3047 corner_radius: 0.0,
3048 shadow: None,
3049 }),
3050 vec![],
3051 );
3052 ir.add_node(
3053 scroll,
3054 Op::Layout(LayoutOp::Scroll {
3055 direction: fission_ir::FlexDirection::Column,
3056 show_scrollbar: true,
3057 width: Some(320.0),
3058 height: Some(240.0),
3059 min_width: None,
3060 max_width: None,
3061 min_height: None,
3062 max_height: None,
3063 padding: [0.0, 0.0, 0.0, 0.0],
3064 flex_grow: 0.0,
3065 flex_shrink: 0.0,
3066 }),
3067 vec![content],
3068 );
3069 ir.add_node(
3070 root,
3071 Op::Layout(LayoutOp::Box {
3072 width: Some(320.0),
3073 height: Some(240.0),
3074 min_width: None,
3075 max_width: None,
3076 min_height: None,
3077 max_height: None,
3078 padding: [0.0, 0.0, 0.0, 0.0],
3079 flex_grow: 0.0,
3080 flex_shrink: 0.0,
3081 aspect_ratio: None,
3082 }),
3083 vec![scroll],
3084 );
3085 ir.set_root(root);
3086
3087 let mut pipeline = Pipeline::new();
3088 let mut layout_engine = LayoutEngine::new();
3089 let scroll0 = ScrollStateMap::default();
3090 pipeline.replace_ir(ir, &Env::default());
3091 pipeline
3092 .ensure_layout(
3093 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3094 &mut layout_engine,
3095 &scroll0,
3096 )
3097 .unwrap();
3098 pipeline
3099 .prepare_current(
3100 LayoutSize {
3101 width: 320.0,
3102 height: 240.0,
3103 },
3104 LayoutSize {
3105 width: 320.0,
3106 height: 240.0,
3107 },
3108 false,
3109 &scroll0,
3110 &Default::default(),
3111 &Default::default(),
3112 &Default::default(),
3113 )
3114 .unwrap();
3115
3116 let mut scroll1 = ScrollStateMap::default();
3117 scroll1.set_offset(scroll, 180.0);
3118 pipeline
3119 .prepare_current(
3120 LayoutSize {
3121 width: 320.0,
3122 height: 240.0,
3123 },
3124 LayoutSize {
3125 width: 320.0,
3126 height: 240.0,
3127 },
3128 false,
3129 &scroll1,
3130 &Default::default(),
3131 &Default::default(),
3132 &Default::default(),
3133 )
3134 .unwrap();
3135
3136 fn find_layer_by_node(
3137 node: &fission_render::RenderNode,
3138 node_id: NodeId,
3139 ) -> Option<&fission_render::RenderLayer> {
3140 match node {
3141 fission_render::RenderNode::Paint(_) => None,
3142 fission_render::RenderNode::Layer(layer) => {
3143 if layer.node_id == Some(node_id) {
3144 return Some(layer);
3145 }
3146 for child in &layer.children {
3147 if let Some(found) = find_layer_by_node(child, node_id) {
3148 return Some(found);
3149 }
3150 }
3151 None
3152 }
3153 }
3154 }
3155
3156 let scroll_layer = pipeline
3157 .retained_scene()
3158 .and_then(|scene| {
3159 scene
3160 .roots
3161 .iter()
3162 .find_map(|node| find_layer_by_node(node, scroll))
3163 })
3164 .expect("expected a retained scroll layer");
3165 assert!(
3166 scroll_layer.style.transform.is_none(),
3167 "scrollbar chrome must not inherit the content scroll transform"
3168 );
3169 let transform = scroll_layer
3170 .children
3171 .iter()
3172 .find_map(|child| match child {
3173 fission_render::RenderNode::Layer(layer) => layer.style.transform,
3174 fission_render::RenderNode::Paint(_) => None,
3175 })
3176 .expect("scroll content layer should carry a compositor transform");
3177 assert!(
3178 (transform[13] + 180.0).abs() <= 0.01,
3179 "expected retained content transform to patch to -180, got {}",
3180 transform[13]
3181 );
3182 }
3183
3184 #[test]
3185 fn scrollbar_thumb_patches_after_scroll_offset_changes() {
3186 let mut ir = CoreIR::new();
3187 let fill = NodeId::derived(18, &[0]);
3188 let content = NodeId::derived(18, &[1]);
3189 let scroll = NodeId::derived(18, &[2]);
3190 let root = NodeId::derived(18, &[3]);
3191
3192 ir.add_node(
3193 fill,
3194 Op::Paint(PaintOp::DrawRect {
3195 fill: Some(Fill::Solid(Color {
3196 r: 120,
3197 g: 120,
3198 b: 220,
3199 a: 255,
3200 })),
3201 stroke: None,
3202 corner_radius: 0.0,
3203 shadow: None,
3204 }),
3205 vec![],
3206 );
3207 ir.add_node(
3208 content,
3209 Op::Layout(LayoutOp::Box {
3210 width: Some(320.0),
3211 height: Some(640.0),
3212 min_width: None,
3213 max_width: None,
3214 min_height: None,
3215 max_height: None,
3216 padding: [0.0, 0.0, 0.0, 0.0],
3217 flex_grow: 0.0,
3218 flex_shrink: 0.0,
3219 aspect_ratio: None,
3220 }),
3221 vec![fill],
3222 );
3223 ir.add_node(
3224 scroll,
3225 Op::Layout(LayoutOp::Scroll {
3226 direction: fission_ir::FlexDirection::Column,
3227 show_scrollbar: true,
3228 width: Some(320.0),
3229 height: Some(240.0),
3230 min_width: None,
3231 max_width: None,
3232 min_height: None,
3233 max_height: None,
3234 padding: [0.0, 0.0, 0.0, 0.0],
3235 flex_grow: 0.0,
3236 flex_shrink: 0.0,
3237 }),
3238 vec![content],
3239 );
3240 ir.add_node(
3241 root,
3242 Op::Layout(LayoutOp::Box {
3243 width: Some(320.0),
3244 height: Some(240.0),
3245 min_width: None,
3246 max_width: None,
3247 min_height: None,
3248 max_height: None,
3249 padding: [0.0, 0.0, 0.0, 0.0],
3250 flex_grow: 0.0,
3251 flex_shrink: 0.0,
3252 aspect_ratio: None,
3253 }),
3254 vec![scroll],
3255 );
3256 ir.set_root(root);
3257
3258 let mut pipeline = Pipeline::new();
3259 let mut layout_engine = LayoutEngine::new();
3260 let scroll0 = ScrollStateMap::default();
3261 pipeline.replace_ir(ir, &Env::default());
3262 pipeline
3263 .ensure_layout(
3264 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3265 &mut layout_engine,
3266 &scroll0,
3267 )
3268 .unwrap();
3269 pipeline
3270 .prepare_current(
3271 LayoutSize::new(320.0, 240.0),
3272 LayoutSize::new(320.0, 240.0),
3273 false,
3274 &scroll0,
3275 &Default::default(),
3276 &Default::default(),
3277 &Default::default(),
3278 )
3279 .unwrap();
3280 let initial_thumb_y = scrollbar_thumb_y(pipeline.retained_scene().unwrap(), scroll)
3281 .expect("initial scrollbar thumb");
3282
3283 let mut scroll1 = ScrollStateMap::default();
3284 scroll1.set_offset(scroll, 200.0);
3285 pipeline
3286 .prepare_current(
3287 LayoutSize::new(320.0, 240.0),
3288 LayoutSize::new(320.0, 240.0),
3289 false,
3290 &scroll1,
3291 &Default::default(),
3292 &Default::default(),
3293 &Default::default(),
3294 )
3295 .unwrap();
3296 let moved_thumb_y = scrollbar_thumb_y(pipeline.retained_scene().unwrap(), scroll)
3297 .expect("moved scrollbar thumb");
3298
3299 assert!(
3300 moved_thumb_y > initial_thumb_y,
3301 "body scroll must patch the retained scrollbar thumb, before={initial_thumb_y}, after={moved_thumb_y}"
3302 );
3303
3304 fn scrollbar_thumb_y(scene: &fission_render::RenderScene, scroll: NodeId) -> Option<f32> {
3305 fn find(node: &fission_render::RenderNode, scroll: NodeId) -> Option<f32> {
3306 match node {
3307 fission_render::RenderNode::Paint(list) => list.ops.iter().find_map(|op| {
3308 if let fission_render::DisplayOp::DrawRect { rect, node_id, .. } = op {
3309 if *node_id == Some(scroll)
3310 && (rect.width() - 6.0).abs() <= 0.01
3311 && rect.height() < 200.0
3312 {
3313 return Some(rect.origin.y);
3314 }
3315 }
3316 None
3317 }),
3318 fission_render::RenderNode::Layer(layer) => {
3319 layer.children.iter().find_map(|child| find(child, scroll))
3320 }
3321 }
3322 }
3323 scene.roots.iter().find_map(|root| find(root, scroll))
3324 }
3325 }
3326
3327 #[test]
3328 fn overflowing_scroll_nodes_emit_visible_scroll_rails() {
3329 let mut ir = CoreIR::new();
3330 let fill = NodeId::derived(16, &[0]);
3331 let content = NodeId::derived(16, &[1]);
3332 let scroll = NodeId::derived(16, &[2]);
3333 let root = NodeId::derived(16, &[3]);
3334
3335 ir.add_node(
3336 fill,
3337 Op::Paint(PaintOp::DrawRect {
3338 fill: Some(Fill::Solid(Color {
3339 r: 80,
3340 g: 120,
3341 b: 220,
3342 a: 255,
3343 })),
3344 stroke: None,
3345 corner_radius: 0.0,
3346 shadow: None,
3347 }),
3348 vec![],
3349 );
3350 ir.add_node(
3351 content,
3352 Op::Layout(LayoutOp::Box {
3353 width: Some(320.0),
3354 height: Some(640.0),
3355 min_width: None,
3356 max_width: None,
3357 min_height: None,
3358 max_height: None,
3359 padding: [0.0, 0.0, 0.0, 0.0],
3360 flex_grow: 0.0,
3361 flex_shrink: 0.0,
3362 aspect_ratio: None,
3363 }),
3364 vec![fill],
3365 );
3366 ir.add_node(
3367 scroll,
3368 Op::Layout(LayoutOp::Scroll {
3369 direction: fission_ir::FlexDirection::Column,
3370 show_scrollbar: true,
3371 width: Some(320.0),
3372 height: Some(240.0),
3373 min_width: None,
3374 max_width: None,
3375 min_height: None,
3376 max_height: None,
3377 padding: [0.0, 0.0, 0.0, 0.0],
3378 flex_grow: 0.0,
3379 flex_shrink: 0.0,
3380 }),
3381 vec![content],
3382 );
3383 ir.add_node(
3384 root,
3385 Op::Layout(LayoutOp::Box {
3386 width: Some(320.0),
3387 height: Some(240.0),
3388 min_width: None,
3389 max_width: None,
3390 min_height: None,
3391 max_height: None,
3392 padding: [0.0, 0.0, 0.0, 0.0],
3393 flex_grow: 0.0,
3394 flex_shrink: 0.0,
3395 aspect_ratio: None,
3396 }),
3397 vec![scroll],
3398 );
3399 ir.set_root(root);
3400
3401 let mut pipeline = Pipeline::new();
3402 let mut layout_engine = LayoutEngine::new();
3403 let scroll_map = ScrollStateMap::default();
3404 pipeline.replace_ir(ir, &Env::default());
3405 pipeline
3406 .ensure_layout(
3407 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3408 &mut layout_engine,
3409 &scroll_map,
3410 )
3411 .unwrap();
3412 pipeline
3413 .prepare_current(
3414 LayoutSize {
3415 width: 320.0,
3416 height: 240.0,
3417 },
3418 LayoutSize {
3419 width: 320.0,
3420 height: 240.0,
3421 },
3422 false,
3423 &scroll_map,
3424 &Default::default(),
3425 &Default::default(),
3426 &Default::default(),
3427 )
3428 .unwrap();
3429
3430 fn count_scroll_rails(node: &fission_render::RenderNode, scroll: NodeId) -> usize {
3431 match node {
3432 fission_render::RenderNode::Paint(list) => list
3433 .ops
3434 .iter()
3435 .filter(|op| match op {
3436 fission_render::DisplayOp::DrawRect { rect, node_id, .. } => {
3437 *node_id == Some(scroll)
3438 && (rect.width() - 6.0).abs() <= 0.01
3439 && rect.height() >= 200.0
3440 }
3441 _ => false,
3442 })
3443 .count(),
3444 fission_render::RenderNode::Layer(layer) => layer
3445 .children
3446 .iter()
3447 .map(|child| count_scroll_rails(child, scroll))
3448 .sum(),
3449 }
3450 }
3451
3452 let rail_count: usize = pipeline
3453 .retained_scene()
3454 .expect("retained scene")
3455 .roots
3456 .iter()
3457 .map(|node| count_scroll_rails(node, scroll))
3458 .sum();
3459 assert!(
3460 rail_count > 0,
3461 "expected an overflow rail for the scroll node"
3462 );
3463 }
3464}