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