1use crate::web_backend::WebSurfaceFrame;
2use anyhow::Result;
3use fission_core::diff::diff_ir;
4use fission_core::env::{AnimationStateMap, Env, VideoStateMap, WebStateMap};
5use fission_core::internal::build_layout_tree;
6use fission_core::internal::downcast_render_object;
7use fission_core::registry::AnimationPropertyId;
8use fission_core::scrollbar::scrollbar_geometry_for_node;
9use fission_core::{LayoutPoint, ScrollStateMap};
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, AnimationPropertyId)>,
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, AnimationPropertyId)],
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: &AnimationStateMap,
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: &AnimationStateMap,
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 &AnimationStateMap::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.animation_target)
602 {
603 self.compositor_animation_keys
604 .insert((target, AnimationPropertyId::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.animation_target)
612 {
613 self.compositor_animation_keys
614 .insert((target, AnimationPropertyId::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.animation_target)
622 {
623 self.compositor_animation_keys
624 .insert((target, AnimationPropertyId::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.animation_target)
632 {
633 self.compositor_animation_keys
634 .insert((target, AnimationPropertyId::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.animation_target)
642 {
643 self.compositor_animation_keys
644 .insert((target, AnimationPropertyId::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.animation_target)
680 .is_some();
681 dynamic |= node
682 .composite
683 .translate_x
684 .as_ref()
685 .and_then(|value| value.animation_target)
686 .is_some();
687 dynamic |= node
688 .composite
689 .translate_y
690 .as_ref()
691 .and_then(|value| value.animation_target)
692 .is_some();
693 dynamic |= node
694 .composite
695 .scale
696 .as_ref()
697 .and_then(|value| value.animation_target)
698 .is_some();
699 dynamic |= node
700 .composite
701 .rotation
702 .as_ref()
703 .and_then(|value| value.animation_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: &AnimationStateMap,
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, AnimationPropertyId::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 if !force {
1184 if let Some(child) = descend_through_plain_wrapper(layer) {
1185 let mut child_path = node_path.to_vec();
1186 child_path.push(0);
1187 return find_nested_texture_plan_candidate(
1188 child,
1189 &child_path,
1190 false,
1191 runtime_dynamic_nodes,
1192 scroll_nodes,
1193 runtime_dynamic_subtrees,
1194 );
1195 }
1196 }
1197
1198 let subtree_dynamic = render_node_or_subtree_is_dynamic(node, runtime_dynamic_subtrees);
1199 let own_dynamic = layer
1200 .node_id
1201 .map(|id| runtime_dynamic_nodes.contains(&id))
1202 .unwrap_or(false);
1203 let is_scroll_node = layer
1204 .node_id
1205 .map(|id| scroll_nodes.contains(&id))
1206 .unwrap_or(false);
1207 if force
1208 || layer_should_extract_as_plan(layer, subtree_dynamic, own_dynamic, is_scroll_node)
1209 {
1210 Some(TexturePlanCandidate {
1211 node,
1212 path: node_path.to_vec(),
1213 })
1214 } else {
1215 for (child_index, child) in layer.children.iter().enumerate() {
1216 let mut child_path = node_path.to_vec();
1217 child_path.push(child_index);
1218 if let Some(candidate) = find_nested_texture_plan_candidate(
1219 child,
1220 &child_path,
1221 false,
1222 runtime_dynamic_nodes,
1223 scroll_nodes,
1224 runtime_dynamic_subtrees,
1225 ) {
1226 return Some(candidate);
1227 }
1228 }
1229 None
1230 }
1231 }
1232 }
1233}
1234
1235fn descend_through_plain_wrapper<'a>(layer: &'a RenderLayer) -> Option<&'a RenderNode> {
1236 let only_child = match layer.children.as_slice() {
1237 [child] => Some(child),
1238 _ => None,
1239 }?;
1240 if layer.style.clip.is_none()
1241 && (layer.style.opacity - 1.0).abs() <= 0.001
1242 && layer.style.transform.is_none()
1243 {
1244 match only_child {
1245 RenderNode::Layer(_) => Some(only_child),
1246 RenderNode::Paint(_) => None,
1247 }
1248 } else {
1249 None
1250 }
1251}
1252
1253fn layer_should_extract_as_plan(
1254 layer: &RenderLayer,
1255 subtree_dynamic: bool,
1256 own_dynamic: bool,
1257 is_scroll_node: bool,
1258) -> bool {
1259 const MIN_PLAN_AREA: f32 = 64.0 * 64.0;
1260 if layer.children.is_empty() {
1261 return false;
1262 }
1263 if is_scroll_node {
1264 return false;
1265 }
1266 if own_dynamic {
1267 return true;
1268 }
1269 if !subtree_dynamic {
1270 return false;
1271 }
1272 let has_style = layer.style.clip.is_some()
1273 || (layer.style.opacity - 1.0).abs() > 0.001
1274 || layer.style.transform.is_some();
1275 let has_local_paint = layer
1276 .children
1277 .iter()
1278 .any(|child| matches!(child, RenderNode::Paint(_)));
1279 let has_multiple_children = layer.children.len() > 1;
1280 (has_style || has_local_paint || has_multiple_children)
1281 && layer.bounds.size.width * layer.bounds.size.height >= MIN_PLAN_AREA
1282}
1283
1284fn localized_scene_for_compositor_children(
1285 children: Vec<RenderNode>,
1286 bounds: LayoutRect,
1287) -> RenderScene {
1288 let local_bounds = LayoutRect::new(0.0, 0.0, bounds.size.width, bounds.size.height);
1289 let mut root = RenderLayer::new(local_bounds);
1290 root.style.transform = Some(translation_matrix(-bounds.origin.x, -bounds.origin.y));
1291 root.children.extend(children);
1292
1293 let mut scene = RenderScene::new(local_bounds);
1294 scene.roots.push(RenderNode::Layer(root));
1295 scene
1296}
1297
1298fn render_node_or_subtree_is_dynamic(
1299 node: &RenderNode,
1300 runtime_dynamic_subtrees: &HashMap<WidgetId, bool>,
1301) -> bool {
1302 match node {
1303 RenderNode::Paint(_) => false,
1304 RenderNode::Layer(layer) => {
1305 layer
1306 .node_id
1307 .and_then(|id| runtime_dynamic_subtrees.get(&id).copied())
1308 .unwrap_or(false)
1309 || layer
1310 .children
1311 .iter()
1312 .any(|child| render_node_or_subtree_is_dynamic(child, runtime_dynamic_subtrees))
1313 }
1314 }
1315}
1316
1317fn texture_plan_key_for_layer(layer: &RenderLayer) -> u64 {
1318 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1319 layer.node_id.hash(&mut hasher);
1320 layer.bounds.size.width.to_bits().hash(&mut hasher);
1321 layer.bounds.size.height.to_bits().hash(&mut hasher);
1322 hash_serde_value(&layer.style.clip, &mut hasher);
1323 hasher.finish()
1324}
1325
1326fn texture_plan_key_for_paint(list: &DisplayList) -> u64 {
1327 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1328 list.bounds.size.width.to_bits().hash(&mut hasher);
1329 list.bounds.size.height.to_bits().hash(&mut hasher);
1330 hash_serde_value(list, &mut hasher);
1331 hasher.finish()
1332}
1333
1334fn scene_cache_key(scene: &RenderScene) -> u64 {
1335 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1336 hash_serde_value(scene, &mut hasher);
1337 hasher.finish()
1338}
1339
1340fn plan_content_key(scene_cache_key: Option<u64>, children: &[CompositorTexturePlan]) -> u64 {
1341 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1342 scene_cache_key.hash(&mut hasher);
1343 for child in children {
1344 child.key.hash(&mut hasher);
1345 child.content_key.hash(&mut hasher);
1346 child.bounds.origin.x.to_bits().hash(&mut hasher);
1347 child.bounds.origin.y.to_bits().hash(&mut hasher);
1348 child.bounds.size.width.to_bits().hash(&mut hasher);
1349 child.bounds.size.height.to_bits().hash(&mut hasher);
1350 child.opacity.to_bits().hash(&mut hasher);
1351 hash_serde_value(&child.transform, &mut hasher);
1352 hash_serde_value(&child.clip, &mut hasher);
1353 }
1354 hasher.finish()
1355}
1356
1357fn patch_texture_compositor_plans(plans: &mut [CompositorTexturePlan], scene: &RenderScene) {
1358 for plan in plans {
1359 patch_texture_compositor_plan(plan, scene);
1360 }
1361}
1362
1363fn patch_texture_compositor_plan(plan: &mut CompositorTexturePlan, scene: &RenderScene) {
1364 for child in &mut plan.children {
1365 patch_texture_compositor_plan(child, scene);
1366 }
1367
1368 if let Some(path) = plan.source_layer_path.as_deref() {
1369 if let Some(layer) = layer_ref_at_path(scene, path) {
1370 plan.bounds = layer.bounds;
1371 plan.opacity = layer.style.opacity;
1372 plan.transform = layer.style.transform;
1373 plan.transform_clip = layer.style.transform_clip;
1374 plan.clip = layer.style.clip.clone();
1375 }
1376 }
1377
1378 plan.content_key = plan_content_key(plan.scene_cache_key, &plan.children);
1379}
1380
1381fn hash_serde_value<T: Serialize, H: Hasher>(value: &T, hasher: &mut H) {
1382 if let Ok(bytes) = bincode::serialize(value) {
1383 bytes.hash(hasher);
1384 }
1385}
1386
1387fn presentation_transform_matrix(
1388 render_viewport_size: LayoutSize,
1389 layout_viewport_size: LayoutSize,
1390 resize_preview: bool,
1391) -> Option<[f32; 16]> {
1392 if !resize_preview
1393 || render_viewport_size.width <= 0.0
1394 || render_viewport_size.height <= 0.0
1395 || layout_viewport_size.width <= 0.0
1396 || layout_viewport_size.height <= 0.0
1397 {
1398 return None;
1399 }
1400
1401 None
1405}
1406
1407fn compose_dynamic_layer_transform(
1408 binding: &TransformBinding,
1409 scroll_map: &ScrollStateMap,
1410 animation_map: &AnimationStateMap,
1411) -> Option<[f32; 16]> {
1412 let mut matrix: Option<[f32; 16]> = None;
1413
1414 if let Some(scroll) = &binding.scroll {
1415 let offset = scroll_map.get_offset(scroll.node_id);
1416 let scroll_matrix = match scroll.direction {
1417 FlexDirection::Row => translation_matrix(-offset, 0.0),
1418 FlexDirection::Column => translation_matrix(0.0, -offset),
1419 };
1420 matrix = append_transform(matrix, scroll_matrix);
1421 }
1422
1423 if let Some(layout_transform) = binding.layout_transform {
1424 matrix = append_transform(matrix, layout_transform);
1425 }
1426
1427 let translate_x = binding
1428 .translate_x
1429 .as_ref()
1430 .map(|scalar| resolve_scalar_value(scalar, animation_map, AnimationPropertyId::TranslateX))
1431 .unwrap_or(0.0);
1432 let translate_y = binding
1433 .translate_y
1434 .as_ref()
1435 .map(|scalar| resolve_scalar_value(scalar, animation_map, AnimationPropertyId::TranslateY))
1436 .unwrap_or(0.0);
1437 let scale = binding
1438 .scale
1439 .as_ref()
1440 .map(|scalar| resolve_scalar_value(scalar, animation_map, AnimationPropertyId::Scale))
1441 .unwrap_or(1.0);
1442 let rotation = binding
1443 .rotation
1444 .as_ref()
1445 .map(|scalar| resolve_scalar_value(scalar, animation_map, AnimationPropertyId::Rotation))
1446 .unwrap_or(0.0);
1447
1448 let has_composite_transform = translate_x.abs() > 0.001
1449 || translate_y.abs() > 0.001
1450 || (scale - 1.0).abs() > 0.001
1451 || rotation.abs() > 0.001;
1452 if has_composite_transform {
1453 matrix = append_transform(
1454 matrix,
1455 composite_transform_matrix(binding.rect, translate_x, translate_y, scale, rotation),
1456 );
1457 }
1458
1459 matrix.filter(|value| !is_identity_matrix(value))
1460}
1461
1462fn append_transform(current: Option<[f32; 16]>, next: [f32; 16]) -> Option<[f32; 16]> {
1463 Some(match current {
1464 Some(existing) => multiply_matrix(existing, next),
1465 None => next,
1466 })
1467}
1468
1469fn generate_render_layer_recursive(
1470 node_id: WidgetId,
1471 ir: &CoreIR,
1472 snapshot: &LayoutSnapshot,
1473 scroll_map: &ScrollStateMap,
1474 animation_map: &AnimationStateMap,
1475 paint_cache: &mut HashMap<WidgetId, (u64, DisplayList)>,
1476 boundary_cache: &mut HashMap<WidgetId, BoundaryCacheEntry>,
1477 runtime_dynamic_subtrees: &HashMap<WidgetId, bool>,
1478 miss_count: &mut usize,
1479 hit_count: &mut usize,
1480 scene_cache_allowed: bool,
1481 visited: &mut HashSet<WidgetId>,
1482 bindings: &mut RetainedDynamicOps,
1483 layer_path: Vec<usize>,
1484) -> Option<RenderLayer> {
1485 if !visited.insert(node_id) {
1486 return None;
1487 }
1488
1489 let (Some(node), Some(geom)) = (ir.nodes.get(&node_id), snapshot.nodes.get(&node_id)) else {
1490 return None;
1491 };
1492
1493 let rect = geom.rect;
1494 let can_use_boundary_cache = !runtime_dynamic_subtrees
1495 .get(&node_id)
1496 .copied()
1497 .unwrap_or(false);
1498
1499 let scene_cache_key = boundary_hash(node, rect);
1500 let can_cache_scene = scene_cache_allowed && can_use_boundary_cache && node.parent.is_some();
1501 if can_cache_scene {
1502 if let Some(entry) = boundary_cache.get(&node_id) {
1503 if entry.hash == scene_cache_key {
1504 *hit_count += 1;
1505 return Some(entry.layer.clone());
1506 }
1507 }
1508 } else if can_use_boundary_cache {
1509 if let Some(entry) = boundary_cache.get(&node_id) {
1510 if entry.hash == scene_cache_key {
1511 *hit_count += 1;
1512 return Some(entry.layer.clone());
1513 }
1514 }
1515 }
1516
1517 let composite_opacity = resolve_composite_scalar(
1518 node.composite.opacity.as_ref(),
1519 animation_map,
1520 AnimationPropertyId::Opacity,
1521 );
1522 let composite_tx = resolve_composite_scalar(
1523 node.composite.translate_x.as_ref(),
1524 animation_map,
1525 AnimationPropertyId::TranslateX,
1526 );
1527 let composite_ty = resolve_composite_scalar(
1528 node.composite.translate_y.as_ref(),
1529 animation_map,
1530 AnimationPropertyId::TranslateY,
1531 );
1532 let composite_scale = resolve_composite_scalar(
1533 node.composite.scale.as_ref(),
1534 animation_map,
1535 AnimationPropertyId::Scale,
1536 )
1537 .unwrap_or(1.0);
1538 let composite_rotation = resolve_composite_scalar(
1539 node.composite.rotation.as_ref(),
1540 animation_map,
1541 AnimationPropertyId::Rotation,
1542 )
1543 .unwrap_or(0.0);
1544
1545 let _has_composite_transform = composite_tx.unwrap_or(0.0).abs() > 0.001
1546 || composite_ty.unwrap_or(0.0).abs() > 0.001
1547 || (composite_scale - 1.0).abs() > 0.001
1548 || composite_rotation.abs() > 0.001;
1549 let has_opacity_layer = composite_opacity
1550 .map(|value| (value - 1.0).abs() > 0.001)
1551 .unwrap_or(false);
1552 let needs_dynamic_opacity = node
1553 .composite
1554 .opacity
1555 .as_ref()
1556 .and_then(|value| value.animation_target)
1557 .is_some();
1558 let needs_dynamic_transform = node
1559 .composite
1560 .translate_x
1561 .as_ref()
1562 .and_then(|value| value.animation_target)
1563 .is_some()
1564 || node
1565 .composite
1566 .translate_y
1567 .as_ref()
1568 .and_then(|value| value.animation_target)
1569 .is_some()
1570 || node
1571 .composite
1572 .scale
1573 .as_ref()
1574 .and_then(|value| value.animation_target)
1575 .is_some()
1576 || node
1577 .composite
1578 .rotation
1579 .as_ref()
1580 .and_then(|value| value.animation_target)
1581 .is_some();
1582 let emit_opacity_layer = has_opacity_layer || needs_dynamic_opacity;
1583 let has_runtime_clip = node.composite.clip_to_bounds;
1584 let scroll = match &node.op {
1585 Op::Layout(LayoutOp::Scroll { direction, .. }) => Some(ScrollTransform {
1586 node_id,
1587 direction: *direction,
1588 }),
1589 _ => None,
1590 };
1591 let layout_transform = match &node.op {
1592 Op::Layout(LayoutOp::Transform { transform }) => Some(*transform),
1593 _ => None,
1594 };
1595 let has_own_transform = needs_dynamic_transform || layout_transform.is_some();
1596 let has_dynamic_transform = has_own_transform || scroll.is_some();
1597 let has_dynamic_style = emit_opacity_layer || has_dynamic_transform || has_runtime_clip;
1598 let has_dynamic_children = node.children.iter().any(|child| {
1599 runtime_dynamic_subtrees
1600 .get(child)
1601 .copied()
1602 .unwrap_or(false)
1603 });
1604 let mut layer = RenderLayer::new(rect);
1605 layer.node_id = Some(node_id);
1606 if can_cache_scene {
1607 layer.style.cache_key = Some(scene_cache_key);
1608 } else if has_dynamic_style && !has_dynamic_children {
1609 layer.style.content_cache_key = Some(scene_cache_key ^ 0x9E37_79B9_7F4A_7C15);
1610 }
1611
1612 layer.style.clip = match &node.op {
1613 Op::Layout(LayoutOp::Scroll { .. }) | Op::Layout(LayoutOp::Clip { .. }) => {
1614 Some(LayerClip::Rect(rect))
1615 }
1616 _ if has_runtime_clip => Some(LayerClip::Rect(rect)),
1617 _ => None,
1618 };
1619 if emit_opacity_layer {
1620 layer.style.opacity = composite_opacity.unwrap_or(1.0);
1621 }
1622
1623 if let Some(transform) = compose_dynamic_layer_transform(
1624 &TransformBinding {
1625 layer_path: layer_path.clone(),
1626 rect,
1627 layout_transform,
1628 scroll: None,
1629 translate_x: node.composite.translate_x.clone(),
1630 translate_y: node.composite.translate_y.clone(),
1631 scale: node.composite.scale.clone(),
1632 rotation: node.composite.rotation.clone(),
1633 },
1634 scroll_map,
1635 animation_map,
1636 ) {
1637 layer.style.transform = Some(transform);
1638 }
1639
1640 let local_hash = local_paint_hash(node);
1641 let local_paint = if let Some((cached_hash, cached_ops)) = paint_cache.get(&node_id) {
1642 if *cached_hash == local_hash {
1643 *hit_count += 1;
1644 Some(cached_ops.clone())
1645 } else {
1646 *miss_count += 1;
1647 let ops = build_local_paint_list(ir, node_id, node, rect);
1648 if let Some(ops) = ops.clone() {
1649 paint_cache.insert(node_id, (local_hash, ops));
1650 } else {
1651 paint_cache.remove(&node_id);
1652 }
1653 ops
1654 }
1655 } else {
1656 *miss_count += 1;
1657 let ops = build_local_paint_list(ir, node_id, node, rect);
1658 if let Some(ops) = ops.clone() {
1659 paint_cache.insert(node_id, (local_hash, ops));
1660 }
1661 ops
1662 };
1663
1664 if let Some(local_paint) = local_paint {
1665 layer.children.push(RenderNode::Paint(local_paint));
1666 }
1667
1668 if needs_dynamic_opacity {
1669 if let Some(scalar) = node.composite.opacity.as_ref() {
1670 bindings.opacity.push(OpacityBinding {
1671 layer_path: layer_path.clone(),
1672 scalar: scalar.clone(),
1673 });
1674 }
1675 }
1676 if has_own_transform {
1677 bindings.transform.push(TransformBinding {
1678 layer_path: layer_path.clone(),
1679 rect,
1680 layout_transform,
1681 scroll: None,
1682 translate_x: node.composite.translate_x.clone(),
1683 translate_y: node.composite.translate_y.clone(),
1684 scale: node.composite.scale.clone(),
1685 rotation: node.composite.rotation.clone(),
1686 });
1687 }
1688
1689 if let Some(scroll) = scroll {
1690 let content_index = layer.children.len();
1691 let mut content_path = layer_path.clone();
1692 content_path.push(content_index);
1693 let mut content_layer = RenderLayer::new(rect);
1694 content_layer.style.transform = compose_dynamic_layer_transform(
1695 &TransformBinding {
1696 layer_path: content_path.clone(),
1697 rect,
1698 layout_transform: None,
1699 scroll: Some(scroll.clone()),
1700 translate_x: None,
1701 translate_y: None,
1702 scale: None,
1703 rotation: None,
1704 },
1705 scroll_map,
1706 animation_map,
1707 );
1708 content_layer.style.transform_clip = false;
1709 bindings.transform.push(TransformBinding {
1710 layer_path: content_path.clone(),
1711 rect,
1712 layout_transform: None,
1713 scroll: Some(scroll),
1714 translate_x: None,
1715 translate_y: None,
1716 scale: None,
1717 rotation: None,
1718 });
1719
1720 for child in &node.children {
1721 let child_index = content_layer.children.len();
1722 let mut child_path = content_path.clone();
1723 child_path.push(child_index);
1724 if let Some(child_layer) = generate_render_layer_recursive(
1725 *child,
1726 ir,
1727 snapshot,
1728 scroll_map,
1729 animation_map,
1730 paint_cache,
1731 boundary_cache,
1732 runtime_dynamic_subtrees,
1733 miss_count,
1734 hit_count,
1735 scene_cache_allowed,
1736 visited,
1737 bindings,
1738 child_path,
1739 ) {
1740 content_layer.children.push(RenderNode::Layer(child_layer));
1741 }
1742 }
1743
1744 if !content_layer.children.is_empty() {
1745 layer.children.push(RenderNode::Layer(content_layer));
1746 }
1747 } else {
1748 for child in &node.children {
1749 let child_index = layer.children.len();
1750 let mut child_path = layer_path.clone();
1751 child_path.push(child_index);
1752 if let Some(child_layer) = generate_render_layer_recursive(
1753 *child,
1754 ir,
1755 snapshot,
1756 scroll_map,
1757 animation_map,
1758 paint_cache,
1759 boundary_cache,
1760 runtime_dynamic_subtrees,
1761 miss_count,
1762 hit_count,
1763 scene_cache_allowed,
1764 visited,
1765 bindings,
1766 child_path,
1767 ) {
1768 layer.children.push(RenderNode::Layer(child_layer));
1769 }
1770 }
1771 }
1772
1773 if let Some(scrollbar) = build_scrollbar_paint(ir, node_id, snapshot, scroll_map) {
1774 let mut scrollbar_path = layer_path.clone();
1775 scrollbar_path.push(layer.children.len());
1776 layer.children.push(RenderNode::Paint(scrollbar));
1777 bindings.scrollbar.push(ScrollbarBinding {
1778 node_path: scrollbar_path,
1779 node_id,
1780 });
1781 }
1782
1783 if can_use_boundary_cache {
1784 boundary_cache.insert(
1785 node_id,
1786 BoundaryCacheEntry {
1787 hash: scene_cache_key,
1788 layer: layer.clone(),
1789 },
1790 );
1791 }
1792
1793 Some(layer)
1794}
1795
1796fn push_video_surface(
1797 video_surfaces: &mut Vec<VideoSurfaceFrame>,
1798 widget_id: WidgetId,
1799 rect: LayoutRect,
1800 video_map: &VideoStateMap,
1801) {
1802 if let Some(state) = video_map.states.get(&widget_id) {
1803 let surface_id = state.surface_id.unwrap_or(0);
1804 video_surfaces.push(VideoSurfaceFrame {
1805 widget_id,
1806 surface_id,
1807 rect,
1808 });
1809 }
1810}
1811
1812fn push_web_surface(
1813 web_surfaces: &mut Vec<WebSurfaceFrame>,
1814 widget_id: WidgetId,
1815 rect: LayoutRect,
1816 web_map: &WebStateMap,
1817) {
1818 if let Some(state) = web_map.states.get(&widget_id) {
1819 if !state.url.trim().is_empty() {
1820 web_surfaces.push(WebSurfaceFrame {
1821 widget_id,
1822 url: state.url.clone(),
1823 user_agent: state.user_agent.clone(),
1824 rect,
1825 });
1826 }
1827 }
1828}
1829
1830fn collect_video_surfaces(
1831 node_id: WidgetId,
1832 ir: &CoreIR,
1833 snapshot: &LayoutSnapshot,
1834 video_map: &VideoStateMap,
1835 web_map: &WebStateMap,
1836 scroll_map: &ScrollStateMap,
1837 accumulated_offset: LayoutPoint,
1838 video_surfaces: &mut Vec<VideoSurfaceFrame>,
1839 web_surfaces: &mut Vec<WebSurfaceFrame>,
1840 scene_3d_surfaces: &mut Vec<(WidgetId, LayoutRect, Vec<u8>)>,
1841) {
1842 let mut visited = HashSet::new();
1843 collect_video_surfaces_with_visited(
1844 node_id,
1845 ir,
1846 snapshot,
1847 video_map,
1848 web_map,
1849 scroll_map,
1850 accumulated_offset,
1851 video_surfaces,
1852 web_surfaces,
1853 scene_3d_surfaces,
1854 &mut visited,
1855 );
1856}
1857
1858fn collect_video_surfaces_with_visited(
1859 node_id: WidgetId,
1860 ir: &CoreIR,
1861 snapshot: &LayoutSnapshot,
1862 video_map: &VideoStateMap,
1863 web_map: &WebStateMap,
1864 scroll_map: &ScrollStateMap,
1865 accumulated_offset: LayoutPoint,
1866 video_surfaces: &mut Vec<VideoSurfaceFrame>,
1867 web_surfaces: &mut Vec<WebSurfaceFrame>,
1868 scene_3d_surfaces: &mut Vec<(WidgetId, LayoutRect, Vec<u8>)>,
1869 visited: &mut HashSet<WidgetId>,
1870) {
1871 if !visited.insert(node_id) {
1872 return;
1873 }
1874 if let (Some(node), Some(geom)) = (ir.nodes.get(&node_id), snapshot.nodes.get(&node_id)) {
1875 let mut child_offset = accumulated_offset;
1876 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1877 let offset = scroll_map.get_offset(node_id);
1878 child_offset = match direction {
1879 fission_ir::FlexDirection::Row => {
1880 LayoutPoint::new(accumulated_offset.x - offset, accumulated_offset.y)
1881 }
1882 fission_ir::FlexDirection::Column => {
1883 LayoutPoint::new(accumulated_offset.x, accumulated_offset.y - offset)
1884 }
1885 };
1886 }
1887
1888 if let Op::Layout(LayoutOp::Embed {
1889 kind: EmbedKind::Video,
1890 widget_id,
1891 ..
1892 }) = &node.op
1893 {
1894 let translated_rect = translate_rect(geom.rect, accumulated_offset);
1895 push_video_surface(video_surfaces, *widget_id, translated_rect, video_map);
1896 } else if let Op::Layout(LayoutOp::Embed {
1897 kind: EmbedKind::Web,
1898 widget_id,
1899 ..
1900 }) = &node.op
1901 {
1902 let translated_rect = translate_rect(geom.rect, accumulated_offset);
1903 push_web_surface(web_surfaces, *widget_id, translated_rect, web_map);
1904 } else if let Op::Layout(LayoutOp::Embed {
1905 kind: EmbedKind::Custom(payload),
1906 widget_id,
1907 ..
1908 }) = &node.op
1909 {
1910 let translated_rect = translate_rect(geom.rect, accumulated_offset);
1911 scene_3d_surfaces.push((*widget_id, translated_rect, payload.clone()));
1912 }
1913
1914 for child in &node.children {
1915 collect_video_surfaces_with_visited(
1916 *child,
1917 ir,
1918 snapshot,
1919 video_map,
1920 web_map,
1921 scroll_map,
1922 child_offset,
1923 video_surfaces,
1924 web_surfaces,
1925 scene_3d_surfaces,
1926 visited,
1927 );
1928 }
1929 }
1930}
1931
1932fn local_paint_hash(node: &fission_ir::CoreNode) -> u64 {
1933 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1934 node.op.hash(&mut hasher);
1935 hasher.finish()
1936}
1937
1938fn boundary_hash(node: &fission_ir::CoreNode, rect: LayoutRect) -> u64 {
1939 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1940 node.hash.hash(&mut hasher);
1941 rect.origin.x.to_bits().hash(&mut hasher);
1942 rect.origin.y.to_bits().hash(&mut hasher);
1943 rect.size.width.to_bits().hash(&mut hasher);
1944 rect.size.height.to_bits().hash(&mut hasher);
1945 hasher.finish()
1946}
1947
1948fn build_local_paint_list(
1949 ir: &CoreIR,
1950 node_id: WidgetId,
1951 node: &fission_ir::CoreNode,
1952 rect: LayoutRect,
1953) -> Option<DisplayList> {
1954 let mut list = DisplayList::new(rect);
1955 match &node.op {
1956 Op::Paint(fission_ir::PaintOp::DrawRect {
1957 fill,
1958 stroke,
1959 corner_radius,
1960 shadow,
1961 }) => {
1962 list.push(DisplayOp::DrawRect {
1963 rect,
1964 fill: fill.as_ref().map(map_fill),
1965 stroke: stroke.as_ref().map(map_stroke),
1966 corner_radius: *corner_radius,
1967 shadow: shadow.as_ref().map(|s| BoxShadow {
1968 color: RenderColor {
1969 r: s.color.r,
1970 g: s.color.g,
1971 b: s.color.b,
1972 a: s.color.a,
1973 },
1974 blur_radius: s.blur_radius,
1975 offset: s.offset,
1976 }),
1977 bounds: rect,
1978 node_id: Some(node_id),
1979 });
1980 }
1981 Op::Paint(fission_ir::PaintOp::DrawText {
1982 text,
1983 size,
1984 color,
1985 underline,
1986 wrap,
1987 caret_index,
1988 caret_color,
1989 caret_width,
1990 caret_height,
1991 caret_radius,
1992 paragraph_style,
1993 }) => {
1994 list.push(DisplayOp::DrawText {
1995 text: text.clone(),
1996 position: rect.origin,
1997 size: *size,
1998 color: RenderColor {
1999 r: color.r,
2000 g: color.g,
2001 b: color.b,
2002 a: color.a,
2003 },
2004 bounds: rect,
2005 node_id: Some(node_id),
2006 underline: *underline,
2007 wrap: *wrap,
2008 caret_index: *caret_index,
2009 caret_color: caret_color.map(|color| RenderColor {
2010 r: color.r,
2011 g: color.g,
2012 b: color.b,
2013 a: color.a,
2014 }),
2015 caret_width: *caret_width,
2016 caret_height: *caret_height,
2017 caret_radius: *caret_radius,
2018 paragraph_style: *paragraph_style,
2019 });
2020 }
2021 Op::Paint(fission_ir::PaintOp::DrawRichText {
2022 runs,
2023 wrap,
2024 caret_index,
2025 caret_color,
2026 caret_width,
2027 caret_height,
2028 caret_radius,
2029 paragraph_style,
2030 }) => {
2031 let annotations = ir
2032 .custom_render_objects
2033 .get(&node_id)
2034 .and_then(|sidecar| {
2035 sidecar.downcast_ref::<Vec<fission_ir::op::RichTextAnnotation>>()
2036 })
2037 .cloned()
2038 .unwrap_or_default();
2039 let render_runs = runs
2040 .iter()
2041 .map(|r| fission_render::TextRun {
2042 text: r.text.clone(),
2043 style: fission_render::TextStyle {
2044 font_size: r.style.font_size,
2045 color: RenderColor {
2046 r: r.style.color.r,
2047 g: r.style.color.g,
2048 b: r.style.color.b,
2049 a: r.style.color.a,
2050 },
2051 underline: r.style.underline,
2052 font_family: r.style.font_family.clone(),
2053 locale: r.style.locale.clone(),
2054 font_weight: r.style.font_weight,
2055 font_style: r.style.font_style,
2056 line_height: r.style.line_height,
2057 letter_spacing: r.style.letter_spacing,
2058 background_color: r.style.background_color.map(|c| RenderColor {
2059 r: c.r,
2060 g: c.g,
2061 b: c.b,
2062 a: c.a,
2063 }),
2064 },
2065 })
2066 .collect();
2067
2068 list.push(DisplayOp::DrawRichText {
2069 runs: render_runs,
2070 position: rect.origin,
2071 bounds: rect,
2072 node_id: Some(node_id),
2073 wrap: *wrap,
2074 caret_index: *caret_index,
2075 caret_color: caret_color.map(|color| RenderColor {
2076 r: color.r,
2077 g: color.g,
2078 b: color.b,
2079 a: color.a,
2080 }),
2081 caret_width: *caret_width,
2082 caret_height: *caret_height,
2083 caret_radius: *caret_radius,
2084 paragraph_style: *paragraph_style,
2085 annotations,
2086 });
2087 }
2088 Op::Paint(fission_ir::PaintOp::DrawImage {
2089 request,
2090 fit,
2091 alignment,
2092 }) => {
2093 list.push(DisplayOp::DrawImage {
2094 rect,
2095 request: request.clone(),
2096 fit: match fit {
2097 fission_ir::op::ImageFit::Contain => fission_render::ImageFit::Contain,
2098 fission_ir::op::ImageFit::Cover => fission_render::ImageFit::Cover,
2099 fission_ir::op::ImageFit::Fill => fission_render::ImageFit::Fill,
2100 fission_ir::op::ImageFit::None => fission_render::ImageFit::None,
2101 },
2102 alignment: *alignment,
2103 bounds: rect,
2104 node_id: Some(node_id),
2105 });
2106 }
2107 Op::Paint(fission_ir::PaintOp::DrawPath { path, fill, stroke }) => {
2108 list.push(DisplayOp::DrawPath {
2109 path: path.clone(),
2110 fill: fill.as_ref().map(map_fill),
2111 stroke: stroke.as_ref().map(map_stroke),
2112 bounds: rect,
2113 node_id: Some(node_id),
2114 });
2115 }
2116 Op::Paint(fission_ir::PaintOp::DrawSvg {
2117 content,
2118 fill,
2119 stroke,
2120 }) => {
2121 list.push(DisplayOp::DrawSvg {
2122 content: content.clone(),
2123 fill: fill.as_ref().map(map_fill),
2124 stroke: stroke.as_ref().map(map_stroke),
2125 bounds: rect,
2126 node_id: Some(node_id),
2127 });
2128 }
2129 Op::Layout(LayoutOp::Embed {
2130 kind, widget_id, ..
2131 }) => {
2132 list.push(DisplayOp::DrawSurface {
2133 rect,
2134 surface_id: embed_surface_id(kind, *widget_id),
2135 position: 0,
2136 bounds: rect,
2137 node_id: Some(node_id),
2138 });
2139 }
2140 _ => {}
2141 }
2142 if list.ops.is_empty() {
2143 None
2144 } else {
2145 Some(list)
2146 }
2147}
2148
2149fn build_scrollbar_paint(
2150 ir: &CoreIR,
2151 node_id: WidgetId,
2152 snapshot: &LayoutSnapshot,
2153 scroll_map: &ScrollStateMap,
2154) -> Option<DisplayList> {
2155 let geometry = scrollbar_geometry_for_node(ir, snapshot, scroll_map, node_id)?;
2156 let rail_fill = Some(Fill::Solid(RenderColor {
2157 r: 160,
2158 g: 168,
2159 b: 180,
2160 a: 80,
2161 }));
2162 let thumb_fill = Some(Fill::Solid(RenderColor {
2163 r: 82,
2164 g: 91,
2165 b: 108,
2166 a: 190,
2167 }));
2168 let mut list = DisplayList::new(geometry.rail_rect);
2169 let corner_radius = fission_core::scrollbar::SCROLLBAR_THICKNESS / 2.0;
2170
2171 list.push(DisplayOp::DrawRect {
2172 rect: geometry.rail_rect,
2173 fill: rail_fill,
2174 stroke: None,
2175 corner_radius,
2176 shadow: None,
2177 bounds: geometry.rail_rect,
2178 node_id: Some(node_id),
2179 });
2180 list.push(DisplayOp::DrawRect {
2181 rect: geometry.thumb_rect,
2182 fill: thumb_fill,
2183 stroke: None,
2184 corner_radius,
2185 shadow: None,
2186 bounds: geometry.thumb_rect,
2187 node_id: Some(node_id),
2188 });
2189
2190 Some(list)
2191}
2192
2193fn resolve_composite_scalar(
2194 scalar: Option<&fission_ir::CompositeScalar>,
2195 animation_map: &AnimationStateMap,
2196 property: AnimationPropertyId,
2197) -> Option<f32> {
2198 let scalar = scalar?;
2199 Some(resolve_scalar_value(scalar, animation_map, property))
2200}
2201
2202fn resolve_scalar_value(
2203 scalar: &fission_ir::CompositeScalar,
2204 animation_map: &AnimationStateMap,
2205 property: AnimationPropertyId,
2206) -> f32 {
2207 scalar
2208 .animation_target
2209 .and_then(|target| animation_map.values.get(&(target, property)).copied())
2210 .unwrap_or(scalar.base)
2211}
2212
2213fn composite_transform_matrix(
2214 rect: LayoutRect,
2215 translate_x: f32,
2216 translate_y: f32,
2217 scale: f32,
2218 rotation: f32,
2219) -> [f32; 16] {
2220 let center_x = rect.origin.x + rect.size.width * 0.5;
2221 let center_y = rect.origin.y + rect.size.height * 0.5;
2222
2223 let to_center = translation_matrix(center_x, center_y);
2224 let from_center = translation_matrix(-center_x, -center_y);
2225 let scale_matrix = scale_matrix(scale);
2226 let rotation_matrix = rotation_z_matrix(rotation);
2227 let animated_translate = translation_matrix(translate_x, translate_y);
2228
2229 multiply_matrix(
2230 animated_translate,
2231 multiply_matrix(
2232 to_center,
2233 multiply_matrix(rotation_matrix, multiply_matrix(scale_matrix, from_center)),
2234 ),
2235 )
2236}
2237
2238fn translation_matrix(tx: f32, ty: f32) -> [f32; 16] {
2239 [
2240 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,
2241 ]
2242}
2243
2244fn scale_matrix(scale: f32) -> [f32; 16] {
2245 [
2246 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,
2247 ]
2248}
2249
2250fn rotation_z_matrix(radians: f32) -> [f32; 16] {
2251 let sin = radians.sin();
2252 let cos = radians.cos();
2253 [
2254 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,
2255 ]
2256}
2257
2258fn multiply_matrix(a: [f32; 16], b: [f32; 16]) -> [f32; 16] {
2259 let mut out = [0.0; 16];
2260 for row in 0..4 {
2261 for col in 0..4 {
2262 let mut sum = 0.0;
2263 for k in 0..4 {
2264 sum += a[row * 4 + k] * b[k * 4 + col];
2265 }
2266 out[row * 4 + col] = sum;
2267 }
2268 }
2269 out
2270}
2271
2272fn is_identity_matrix(matrix: &[f32; 16]) -> bool {
2273 const IDENTITY: [f32; 16] = [
2274 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,
2275 ];
2276 matrix
2277 .iter()
2278 .zip(IDENTITY.iter())
2279 .all(|(lhs, rhs)| (*lhs - *rhs).abs() <= 0.000_1)
2280}
2281
2282#[cfg(test)]
2283fn scroll_offsets_changed(prev: &HashMap<WidgetId, u32>, scroll_map: &ScrollStateMap) -> bool {
2284 if prev.len() != scroll_map.offsets.len() {
2285 return true;
2286 }
2287
2288 scroll_map
2289 .offsets
2290 .iter()
2291 .any(|(id, offset)| prev.get(id).copied() != Some(offset.to_bits()))
2292}
2293
2294impl SnapshotProvider for Pipeline {
2295 fn snapshot(&self, kind: SnapshotKind) -> Option<SnapshotBlob> {
2296 match kind {
2297 SnapshotKind::Layout => self.last_snapshot.as_ref().and_then(|snap| {
2298 serde_json::to_string_pretty(snap)
2299 .ok()
2300 .map(|json| SnapshotBlob { kind, json })
2301 }),
2302 }
2303 }
2304}
2305
2306fn map_fill(f: &fission_ir::op::Fill) -> Fill {
2307 match f {
2308 fission_ir::op::Fill::Solid(c) => Fill::Solid(RenderColor {
2309 r: c.r,
2310 g: c.g,
2311 b: c.b,
2312 a: c.a,
2313 }),
2314 fission_ir::op::Fill::LinearGradient { start, end, stops } => Fill::LinearGradient {
2315 start: *start,
2316 end: *end,
2317 stops: stops
2318 .iter()
2319 .map(|(o, c)| {
2320 (
2321 *o,
2322 RenderColor {
2323 r: c.r,
2324 g: c.g,
2325 b: c.b,
2326 a: c.a,
2327 },
2328 )
2329 })
2330 .collect(),
2331 },
2332 fission_ir::op::Fill::RadialGradient {
2333 center,
2334 radius,
2335 stops,
2336 } => Fill::RadialGradient {
2337 center: *center,
2338 radius: *radius,
2339 stops: stops
2340 .iter()
2341 .map(|(o, c)| {
2342 (
2343 *o,
2344 RenderColor {
2345 r: c.r,
2346 g: c.g,
2347 b: c.b,
2348 a: c.a,
2349 },
2350 )
2351 })
2352 .collect(),
2353 },
2354 }
2355}
2356
2357fn map_stroke(s: &fission_ir::op::Stroke) -> Stroke {
2358 Stroke {
2359 fill: map_fill(&s.fill),
2360 width: s.width,
2361 dash_array: s.dash_array.clone(),
2362 line_cap: match s.line_cap {
2363 fission_ir::op::LineCap::Butt => fission_render::LineCap::Butt,
2364 fission_ir::op::LineCap::Round => fission_render::LineCap::Round,
2365 fission_ir::op::LineCap::Square => fission_render::LineCap::Square,
2366 },
2367 line_join: match s.line_join {
2368 fission_ir::op::LineJoin::Miter => fission_render::LineJoin::Miter,
2369 fission_ir::op::LineJoin::Round => fission_render::LineJoin::Round,
2370 fission_ir::op::LineJoin::Bevel => fission_render::LineJoin::Bevel,
2371 },
2372 }
2373}
2374
2375fn translate_rect(rect: LayoutRect, offset: LayoutPoint) -> LayoutRect {
2376 LayoutRect {
2377 origin: LayoutPoint::new(rect.origin.x + offset.x, rect.origin.y + offset.y),
2378 size: rect.size,
2379 }
2380}
2381
2382#[cfg(test)]
2383mod tests {
2384 use super::{build_local_paint_list, scroll_offsets_changed, InvalidationSet, Pipeline};
2385 use fission_core::env::Env;
2386 use fission_core::registry::AnimationPropertyId;
2387 use fission_core::ScrollStateMap;
2388 use fission_ir::op::{
2389 Color, Fill, ImageAlignment, ImageFit, ImageRequest, ImageSource, RichTextAnnotation,
2390 TextRun, TextStyle,
2391 };
2392 use fission_ir::semantics::ActionTrigger;
2393 use fission_ir::{
2394 ActionEntry, CompositeScalar, CompositeStyle, CoreIR, EmbedKind, LayoutOp, Op, PaintOp,
2395 WidgetId,
2396 };
2397 use fission_layout::{LayoutEngine, LayoutRect, LayoutSize};
2398 use fission_render::{DisplayOp, RenderScene, Renderer};
2399 use std::collections::HashMap;
2400 use std::sync::Arc;
2401
2402 struct NullRenderer;
2403
2404 impl Renderer for NullRenderer {
2405 fn render_scene(&mut self, _scene: &RenderScene) -> anyhow::Result<()> {
2406 Ok(())
2407 }
2408 }
2409
2410 fn two_child_layout_ir(second_width: f32) -> CoreIR {
2411 let root = WidgetId::derived(50, &[0]);
2412 let first = WidgetId::derived(50, &[1]);
2413 let second = WidgetId::derived(50, &[2]);
2414 let mut ir = CoreIR::new();
2415 ir.add_node(
2416 first,
2417 Op::Layout(LayoutOp::Box {
2418 width: Some(40.0),
2419 height: Some(20.0),
2420 min_width: None,
2421 max_width: None,
2422 min_height: None,
2423 max_height: None,
2424 padding: [0.0; 4],
2425 flex_grow: 0.0,
2426 flex_shrink: 1.0,
2427 aspect_ratio: None,
2428 }),
2429 vec![],
2430 );
2431 ir.add_node(
2432 second,
2433 Op::Layout(LayoutOp::Box {
2434 width: Some(second_width),
2435 height: Some(20.0),
2436 min_width: None,
2437 max_width: None,
2438 min_height: None,
2439 max_height: None,
2440 padding: [0.0; 4],
2441 flex_grow: 0.0,
2442 flex_shrink: 1.0,
2443 aspect_ratio: None,
2444 }),
2445 vec![],
2446 );
2447 ir.add_node(
2448 root,
2449 Op::Layout(LayoutOp::Flex {
2450 direction: fission_ir::FlexDirection::Column,
2451 wrap: fission_ir::op::FlexWrap::NoWrap,
2452 flex_grow: 0.0,
2453 flex_shrink: 1.0,
2454 padding: [0.0; 4],
2455 gap: Some(4.0),
2456 align_items: fission_ir::op::AlignItems::Start,
2457 justify_content: fission_ir::op::JustifyContent::Start,
2458 }),
2459 vec![first, second],
2460 );
2461 ir.set_root(root);
2462 ir
2463 }
2464
2465 #[test]
2466 fn unchanged_scroll_offsets_do_not_invalidate_cache() {
2467 let id = WidgetId::derived(1, &[0]);
2468 let mut prev = HashMap::new();
2469 prev.insert(id, 12.5f32.to_bits());
2470 let mut scroll = ScrollStateMap::default();
2471 scroll.set_offset(id, 12.5);
2472 assert!(!scroll_offsets_changed(&prev, &scroll));
2473 }
2474
2475 #[test]
2476 fn changed_scroll_offsets_invalidate_cache() {
2477 let id = WidgetId::derived(2, &[0]);
2478 let mut prev = HashMap::new();
2479 prev.insert(id, 0.0f32.to_bits());
2480 let mut scroll = ScrollStateMap::default();
2481 scroll.set_offset(id, 4.0);
2482 assert!(scroll_offsets_changed(&prev, &scroll));
2483 }
2484
2485 #[test]
2486 fn incremental_layout_keeps_rebuild_telemetry_honest() {
2487 let mut pipeline = Pipeline::new();
2488 let mut layout_engine = LayoutEngine::new();
2489 let scroll = ScrollStateMap::default();
2490
2491 pipeline.replace_ir(two_child_layout_ir(60.0), &Env::default());
2492 let first_pass = pipeline
2493 .ensure_layout(
2494 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2495 &mut layout_engine,
2496 &scroll,
2497 )
2498 .expect("initial layout");
2499 assert_eq!(first_pass, pipeline.layout_input_nodes.len());
2500 assert_eq!(pipeline.layout_full_rebuild_count, 1);
2501
2502 pipeline.replace_ir(two_child_layout_ir(90.0), &Env::default());
2503 let second_pass = pipeline
2504 .ensure_layout(
2505 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2506 &mut layout_engine,
2507 &scroll,
2508 )
2509 .expect("incremental layout");
2510
2511 assert_eq!(second_pass, 1);
2512 assert_eq!(pipeline.layout_full_rebuild_count, 1);
2513 assert!(pipeline.pending_layout_dirty_nodes.is_empty());
2514 }
2515
2516 #[test]
2517 fn rich_text_annotations_flow_into_display_ops() {
2518 let node_id = WidgetId::derived(9, &[0]);
2519 let mut ir = CoreIR::new();
2520 ir.add_node(
2521 node_id,
2522 Op::Paint(PaintOp::DrawRichText {
2523 runs: vec![TextRun {
2524 text: "docs".into(),
2525 style: TextStyle {
2526 font_size: 14.0,
2527 color: Color::BLACK,
2528 underline: false,
2529 font_family: None,
2530 locale: None,
2531 font_weight: 400,
2532 font_style: fission_ir::op::FontStyle::Normal,
2533 line_height: None,
2534 letter_spacing: 0.0,
2535 background_color: None,
2536 },
2537 }],
2538 wrap: true,
2539 caret_index: None,
2540 caret_color: None,
2541 caret_width: None,
2542 caret_height: None,
2543 caret_radius: None,
2544 paragraph_style: None,
2545 }),
2546 vec![],
2547 );
2548 ir.custom_render_objects.insert(
2549 node_id,
2550 Arc::new(vec![RichTextAnnotation {
2551 range: 0..4,
2552 semantics_label: Some("Documentation".into()),
2553 semantics_identifier: Some("docs-link".into()),
2554 spell_out: Some(true),
2555 mouse_cursor: Some(fission_ir::op::MouseCursor::Pointer),
2556 actions: vec![ActionEntry {
2557 trigger: ActionTrigger::Default,
2558 action_id: 7,
2559 payload_data: Some(vec![1, 2, 3]),
2560 }],
2561 }]),
2562 );
2563
2564 let node = ir.nodes.get(&node_id).expect("paint node");
2565 let list =
2566 build_local_paint_list(&ir, node_id, node, LayoutRect::new(0.0, 0.0, 160.0, 40.0))
2567 .expect("display list");
2568 match list.ops.first() {
2569 Some(DisplayOp::DrawRichText { annotations, .. }) => {
2570 assert_eq!(annotations.len(), 1);
2571 assert_eq!(annotations[0].range, 0..4);
2572 assert_eq!(
2573 annotations[0].semantics_identifier.as_deref(),
2574 Some("docs-link")
2575 );
2576 }
2577 other => panic!("expected rich text op, got {other:?}"),
2578 }
2579 }
2580
2581 #[test]
2582 fn draw_image_paint_ops_flow_into_display_ops() {
2583 let node_id = WidgetId::derived(12, &[0]);
2584 let request = ImageRequest {
2585 source: ImageSource::Network {
2586 url: "https://example.com/product.webp".into(),
2587 headers: Vec::new(),
2588 cache_policy: Default::default(),
2589 },
2590 cache_width: Some(220),
2591 cache_height: Some(160),
2592 semantic_label: Some("Product thumbnail".into()),
2593 ..Default::default()
2594 };
2595 let mut ir = CoreIR::new();
2596 ir.add_node(
2597 node_id,
2598 Op::Paint(PaintOp::DrawImage {
2599 request: request.clone(),
2600 fit: ImageFit::Cover,
2601 alignment: ImageAlignment::Center,
2602 }),
2603 vec![],
2604 );
2605
2606 let node = ir.nodes.get(&node_id).expect("image node");
2607 let rect = LayoutRect::new(24.0, 32.0, 220.0, 160.0);
2608 let list = build_local_paint_list(&ir, node_id, node, rect).expect("display list");
2609
2610 match list.ops.first() {
2611 Some(DisplayOp::DrawImage {
2612 rect: image_rect,
2613 request: image_request,
2614 fit,
2615 alignment,
2616 bounds,
2617 node_id: Some(image_node_id),
2618 }) => {
2619 assert_eq!(*image_rect, rect);
2620 assert_eq!(image_request, &request);
2621 assert_eq!(*fit, fission_render::ImageFit::Cover);
2622 assert_eq!(*alignment, ImageAlignment::Center);
2623 assert_eq!(*bounds, rect);
2624 assert_eq!(*image_node_id, node_id);
2625 }
2626 other => panic!("expected image display op, got {other:?}"),
2627 }
2628 }
2629
2630 #[test]
2631 fn retained_pipeline_scene_keeps_draw_image_ops() {
2632 let image_id = WidgetId::derived(13, &[0]);
2633 let root_id = WidgetId::derived(13, &[1]);
2634 let request = ImageRequest {
2635 source: ImageSource::Network {
2636 url: "https://example.com/catalog/thumbnail.webp".into(),
2637 headers: Vec::new(),
2638 cache_policy: Default::default(),
2639 },
2640 semantic_label: Some("Catalog thumbnail".into()),
2641 ..Default::default()
2642 };
2643 let mut ir = CoreIR::new();
2644 ir.add_node(
2645 image_id,
2646 Op::Paint(PaintOp::DrawImage {
2647 request: request.clone(),
2648 fit: ImageFit::Cover,
2649 alignment: ImageAlignment::Center,
2650 }),
2651 vec![],
2652 );
2653 ir.add_node(
2654 root_id,
2655 Op::Layout(LayoutOp::Box {
2656 width: Some(220.0),
2657 height: Some(160.0),
2658 min_width: None,
2659 max_width: None,
2660 min_height: None,
2661 max_height: None,
2662 padding: [0.0, 0.0, 0.0, 0.0],
2663 flex_grow: 0.0,
2664 flex_shrink: 0.0,
2665 aspect_ratio: None,
2666 }),
2667 vec![image_id],
2668 );
2669 ir.set_root(root_id);
2670
2671 let mut pipeline = Pipeline::new();
2672 let mut layout_engine = LayoutEngine::new();
2673 let scroll = ScrollStateMap::default();
2674 pipeline.replace_ir(ir, &Env::default());
2675 pipeline
2676 .ensure_layout(
2677 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2678 &mut layout_engine,
2679 &scroll,
2680 )
2681 .unwrap();
2682 pipeline
2683 .prepare_current(
2684 LayoutSize {
2685 width: 320.0,
2686 height: 240.0,
2687 },
2688 LayoutSize {
2689 width: 320.0,
2690 height: 240.0,
2691 },
2692 false,
2693 &scroll,
2694 &Default::default(),
2695 &Default::default(),
2696 &Default::default(),
2697 )
2698 .unwrap();
2699
2700 let display_list = pipeline.retained_scene().expect("retained scene").flatten();
2701 let image_op = display_list.ops.iter().find_map(|op| match op {
2702 DisplayOp::DrawImage {
2703 rect,
2704 request: image_request,
2705 fit,
2706 alignment,
2707 ..
2708 } => Some((rect, image_request, fit, alignment)),
2709 _ => None,
2710 });
2711
2712 let Some((rect, image_request, fit, alignment)) = image_op else {
2713 panic!("retained scene dropped DrawImage op");
2714 };
2715 assert_eq!(image_request, &request);
2716 assert_eq!(*fit, fission_render::ImageFit::Cover);
2717 assert_eq!(*alignment, ImageAlignment::Center);
2718 assert_eq!(rect.size.width, 220.0);
2719 assert_eq!(rect.size.height, 160.0);
2720 }
2721
2722 #[test]
2723 fn embed_layout_ops_flow_into_surface_display_ops() {
2724 let node_id = WidgetId::derived(14, &[0]);
2725 let widget_id = WidgetId::explicit("embed.surface");
2726 let mut ir = CoreIR::new();
2727 ir.add_node(
2728 node_id,
2729 Op::Layout(LayoutOp::Embed {
2730 kind: EmbedKind::Web,
2731 widget_id,
2732 width: Some(320.0),
2733 height: Some(180.0),
2734 }),
2735 vec![],
2736 );
2737
2738 let node = ir.nodes.get(&node_id).expect("embed node");
2739 let rect = LayoutRect::new(12.0, 24.0, 320.0, 180.0);
2740 let list = build_local_paint_list(&ir, node_id, node, rect).expect("display list");
2741
2742 match list.ops.first() {
2743 Some(DisplayOp::DrawSurface {
2744 rect: surface_rect,
2745 bounds,
2746 node_id: Some(surface_node_id),
2747 ..
2748 }) => {
2749 assert_eq!(*surface_rect, rect);
2750 assert_eq!(*bounds, rect);
2751 assert_eq!(*surface_node_id, node_id);
2752 }
2753 other => panic!("expected surface display op, got {other:?}"),
2754 }
2755 }
2756
2757 #[test]
2758 fn compositor_bound_opacity_animation_is_composite_only() {
2759 let mut ir = CoreIR::new();
2760 let child = WidgetId::derived(10, &[1]);
2761 let root = WidgetId::derived(10, &[0]);
2762 ir.add_node(child, Op::Layout(LayoutOp::AbsoluteFill), vec![]);
2763 ir.add_node_with_composite(
2764 root,
2765 Op::Structural(fission_ir::StructuralOp::Group { stable_hash: 1 }),
2766 CompositeStyle {
2767 opacity: Some(CompositeScalar::new(0.0).animated(WidgetId::explicit("fade"))),
2768 ..Default::default()
2769 },
2770 vec![child],
2771 );
2772 ir.set_root(root);
2773
2774 let mut pipeline = Pipeline::new();
2775 pipeline.replace_ir(ir, &Env::default());
2776 let invalidation = pipeline.classify_animation_updates(&[(
2777 WidgetId::explicit("fade"),
2778 AnimationPropertyId::Opacity,
2779 )]);
2780 assert_eq!(
2781 invalidation,
2782 InvalidationSet {
2783 build: false,
2784 layout: false,
2785 paint: false,
2786 composite: true,
2787 }
2788 );
2789 }
2790
2791 #[test]
2792 fn unbound_custom_animation_requires_build() {
2793 let pipeline = Pipeline::new();
2794 let invalidation = pipeline.classify_animation_updates(&[(
2795 WidgetId::explicit("custom"),
2796 AnimationPropertyId::custom("phase"),
2797 )]);
2798 assert!(invalidation.build);
2799 assert!(invalidation.layout);
2800 }
2801
2802 #[test]
2803 fn compositor_bound_translate_animation_is_composite_only() {
2804 let mut ir = CoreIR::new();
2805 let child = WidgetId::derived(11, &[1]);
2806 let root = WidgetId::derived(11, &[0]);
2807 ir.add_node(
2808 child,
2809 Op::Paint(PaintOp::DrawRect {
2810 fill: Some(Fill::Solid(Color {
2811 r: 0,
2812 g: 0,
2813 b: 0,
2814 a: 255,
2815 })),
2816 stroke: None,
2817 corner_radius: 0.0,
2818 shadow: None,
2819 }),
2820 vec![],
2821 );
2822 ir.add_node_with_composite(
2823 root,
2824 Op::Layout(LayoutOp::Box {
2825 width: Some(120.0),
2826 height: Some(64.0),
2827 min_width: None,
2828 max_width: None,
2829 min_height: None,
2830 max_height: None,
2831 padding: [0.0, 0.0, 0.0, 0.0],
2832 flex_grow: 0.0,
2833 flex_shrink: 0.0,
2834 aspect_ratio: None,
2835 }),
2836 CompositeStyle {
2837 translate_x: Some(CompositeScalar::new(12.0).animated(WidgetId::explicit("slide"))),
2838 ..Default::default()
2839 },
2840 vec![child],
2841 );
2842 ir.set_root(root);
2843
2844 let mut pipeline = Pipeline::new();
2845 pipeline.replace_ir(ir, &Env::default());
2846 let invalidation = pipeline.classify_animation_updates(&[(
2847 WidgetId::explicit("slide"),
2848 AnimationPropertyId::TranslateX,
2849 )]);
2850 assert_eq!(
2851 invalidation,
2852 InvalidationSet {
2853 build: false,
2854 layout: false,
2855 paint: false,
2856 composite: true,
2857 }
2858 );
2859 }
2860
2861 #[test]
2862 fn dynamic_layer_with_static_contents_gets_content_cache_key() {
2863 let mut ir = CoreIR::new();
2864 let child = WidgetId::derived(12, &[1]);
2865 let root = WidgetId::derived(12, &[0]);
2866 ir.add_node(
2867 child,
2868 Op::Paint(PaintOp::DrawRect {
2869 fill: Some(Fill::Solid(Color {
2870 r: 20,
2871 g: 40,
2872 b: 60,
2873 a: 255,
2874 })),
2875 stroke: None,
2876 corner_radius: 8.0,
2877 shadow: None,
2878 }),
2879 vec![],
2880 );
2881 ir.add_node_with_composite(
2882 root,
2883 Op::Layout(LayoutOp::Box {
2884 width: Some(160.0),
2885 height: Some(72.0),
2886 min_width: None,
2887 max_width: None,
2888 min_height: None,
2889 max_height: None,
2890 padding: [0.0, 0.0, 0.0, 0.0],
2891 flex_grow: 0.0,
2892 flex_shrink: 0.0,
2893 aspect_ratio: None,
2894 }),
2895 CompositeStyle {
2896 opacity: Some(CompositeScalar::new(0.4).animated(WidgetId::explicit("fade-cache"))),
2897 ..Default::default()
2898 },
2899 vec![child],
2900 );
2901 ir.set_root(root);
2902
2903 let mut pipeline = Pipeline::new();
2904 let mut layout_engine = LayoutEngine::new();
2905 let mut renderer = NullRenderer;
2906 let scroll = ScrollStateMap::default();
2907 pipeline.replace_ir(ir, &Env::default());
2908 pipeline
2909 .ensure_layout(
2910 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
2911 &mut layout_engine,
2912 &scroll,
2913 )
2914 .unwrap();
2915 pipeline
2916 .render_current(
2917 LayoutSize {
2918 width: 320.0,
2919 height: 240.0,
2920 },
2921 LayoutSize {
2922 width: 320.0,
2923 height: 240.0,
2924 },
2925 false,
2926 &mut renderer,
2927 &scroll,
2928 &Default::default(),
2929 &Default::default(),
2930 &Default::default(),
2931 )
2932 .unwrap();
2933
2934 let scene = pipeline
2935 .retained_scene
2936 .as_ref()
2937 .expect("retained scene missing");
2938 let presentation_root = match scene.roots.first() {
2939 Some(fission_render::RenderNode::Layer(layer)) => layer,
2940 _ => panic!("missing presentation layer"),
2941 };
2942 let animated_layer = match presentation_root.children.first() {
2943 Some(fission_render::RenderNode::Layer(layer)) => layer,
2944 _ => panic!("missing animated layer"),
2945 };
2946
2947 assert!(animated_layer.style.cache_key.is_none());
2948 assert!(animated_layer.style.content_cache_key.is_some());
2949 }
2950
2951 #[test]
2952 fn nested_dynamic_descendant_becomes_child_texture_plan() {
2953 let mut ir = CoreIR::new();
2954 let left_paint = WidgetId::derived(13, &[0]);
2955 let animated_paint = WidgetId::derived(13, &[1]);
2956 let animated_wrapper = WidgetId::derived(13, &[2]);
2957 let outer_static = WidgetId::derived(13, &[3]);
2958 let outer_group = WidgetId::derived(13, &[4]);
2959 let root = WidgetId::derived(13, &[5]);
2960
2961 ir.add_node(
2962 left_paint,
2963 Op::Paint(PaintOp::DrawRect {
2964 fill: Some(Fill::Solid(Color {
2965 r: 10,
2966 g: 10,
2967 b: 10,
2968 a: 255,
2969 })),
2970 stroke: None,
2971 corner_radius: 0.0,
2972 shadow: None,
2973 }),
2974 vec![],
2975 );
2976 ir.add_node(
2977 animated_paint,
2978 Op::Paint(PaintOp::DrawRect {
2979 fill: Some(Fill::Solid(Color {
2980 r: 200,
2981 g: 40,
2982 b: 40,
2983 a: 255,
2984 })),
2985 stroke: None,
2986 corner_radius: 0.0,
2987 shadow: None,
2988 }),
2989 vec![],
2990 );
2991 ir.add_node_with_composite(
2992 animated_wrapper,
2993 Op::Layout(LayoutOp::Box {
2994 width: Some(96.0),
2995 height: Some(96.0),
2996 min_width: None,
2997 max_width: None,
2998 min_height: None,
2999 max_height: None,
3000 padding: [0.0, 0.0, 0.0, 0.0],
3001 flex_grow: 0.0,
3002 flex_shrink: 0.0,
3003 aspect_ratio: None,
3004 }),
3005 CompositeStyle {
3006 opacity: Some(
3007 CompositeScalar::new(0.4).animated(WidgetId::explicit("nested-fade")),
3008 ),
3009 ..Default::default()
3010 },
3011 vec![animated_paint],
3012 );
3013 ir.add_node(
3014 outer_static,
3015 Op::Paint(PaintOp::DrawRect {
3016 fill: Some(Fill::Solid(Color {
3017 r: 20,
3018 g: 100,
3019 b: 180,
3020 a: 255,
3021 })),
3022 stroke: None,
3023 corner_radius: 8.0,
3024 shadow: None,
3025 }),
3026 vec![],
3027 );
3028 ir.add_node(
3029 outer_group,
3030 Op::Layout(LayoutOp::Box {
3031 width: Some(160.0),
3032 height: Some(120.0),
3033 min_width: None,
3034 max_width: None,
3035 min_height: None,
3036 max_height: None,
3037 padding: [0.0, 0.0, 0.0, 0.0],
3038 flex_grow: 0.0,
3039 flex_shrink: 0.0,
3040 aspect_ratio: None,
3041 }),
3042 vec![outer_static, animated_wrapper],
3043 );
3044 ir.add_node(
3045 root,
3046 Op::Layout(LayoutOp::Box {
3047 width: Some(320.0),
3048 height: Some(240.0),
3049 min_width: None,
3050 max_width: None,
3051 min_height: None,
3052 max_height: None,
3053 padding: [0.0, 0.0, 0.0, 0.0],
3054 flex_grow: 0.0,
3055 flex_shrink: 0.0,
3056 aspect_ratio: None,
3057 }),
3058 vec![left_paint, outer_group],
3059 );
3060 ir.set_root(root);
3061
3062 let mut pipeline = Pipeline::new();
3063 let mut layout_engine = LayoutEngine::new();
3064 let scroll = ScrollStateMap::default();
3065 pipeline.replace_ir(ir, &Env::default());
3066 pipeline
3067 .ensure_layout(
3068 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3069 &mut layout_engine,
3070 &scroll,
3071 )
3072 .unwrap();
3073 pipeline
3074 .prepare_current(
3075 LayoutSize {
3076 width: 320.0,
3077 height: 240.0,
3078 },
3079 LayoutSize {
3080 width: 320.0,
3081 height: 240.0,
3082 },
3083 false,
3084 &scroll,
3085 &Default::default(),
3086 &Default::default(),
3087 &Default::default(),
3088 )
3089 .unwrap();
3090
3091 let plans = pipeline.texture_compositor_plans();
3092 assert!(!plans.is_empty());
3093 assert!(
3094 plans.iter().any(|plan| !plan.children.is_empty()),
3095 "expected at least one retained texture plan to extract nested dynamic descendants"
3096 );
3097 }
3098
3099 #[test]
3100 fn resize_preview_keeps_texture_compositor_root_transform() {
3101 let mut ir = CoreIR::new();
3102 let left = WidgetId::derived(14, &[0]);
3103 let right = WidgetId::derived(14, &[1]);
3104 let root = WidgetId::derived(14, &[2]);
3105
3106 ir.add_node(
3107 left,
3108 Op::Paint(PaintOp::DrawRect {
3109 fill: Some(Fill::Solid(Color {
3110 r: 80,
3111 g: 80,
3112 b: 80,
3113 a: 255,
3114 })),
3115 stroke: None,
3116 corner_radius: 0.0,
3117 shadow: None,
3118 }),
3119 vec![],
3120 );
3121 ir.add_node(
3122 right,
3123 Op::Paint(PaintOp::DrawRect {
3124 fill: Some(Fill::Solid(Color {
3125 r: 180,
3126 g: 180,
3127 b: 180,
3128 a: 255,
3129 })),
3130 stroke: None,
3131 corner_radius: 0.0,
3132 shadow: None,
3133 }),
3134 vec![],
3135 );
3136 ir.add_node(
3137 root,
3138 Op::Layout(LayoutOp::Box {
3139 width: Some(300.0),
3140 height: Some(200.0),
3141 min_width: None,
3142 max_width: None,
3143 min_height: None,
3144 max_height: None,
3145 padding: [0.0, 0.0, 0.0, 0.0],
3146 flex_grow: 0.0,
3147 flex_shrink: 0.0,
3148 aspect_ratio: None,
3149 }),
3150 vec![left, right],
3151 );
3152 ir.set_root(root);
3153
3154 let mut pipeline = Pipeline::new();
3155 let mut layout_engine = LayoutEngine::new();
3156 let scroll = ScrollStateMap::default();
3157 pipeline.replace_ir(ir, &Env::default());
3158 pipeline
3159 .ensure_layout(
3160 LayoutRect::new(0.0, 0.0, 300.0, 200.0),
3161 &mut layout_engine,
3162 &scroll,
3163 )
3164 .unwrap();
3165 pipeline
3166 .prepare_current(
3167 LayoutSize {
3168 width: 540.0,
3169 height: 360.0,
3170 },
3171 LayoutSize {
3172 width: 300.0,
3173 height: 200.0,
3174 },
3175 true,
3176 &scroll,
3177 &Default::default(),
3178 &Default::default(),
3179 &Default::default(),
3180 )
3181 .unwrap();
3182
3183 assert!(pipeline.texture_compositor_root_transform().is_none());
3184 assert!(!pipeline.texture_compositor_plans().is_empty());
3185 }
3186
3187 #[test]
3188 fn scroll_only_layers_patch_retained_transforms_after_offset_changes() {
3189 let mut ir = CoreIR::new();
3190 let content = WidgetId::derived(15, &[0]);
3191 let scroll = WidgetId::derived(15, &[1]);
3192 let root = WidgetId::derived(15, &[2]);
3193
3194 ir.add_node(
3195 content,
3196 Op::Paint(PaintOp::DrawRect {
3197 fill: Some(Fill::Solid(Color {
3198 r: 120,
3199 g: 120,
3200 b: 220,
3201 a: 255,
3202 })),
3203 stroke: None,
3204 corner_radius: 0.0,
3205 shadow: None,
3206 }),
3207 vec![],
3208 );
3209 ir.add_node(
3210 scroll,
3211 Op::Layout(LayoutOp::Scroll {
3212 direction: fission_ir::FlexDirection::Column,
3213 show_scrollbar: true,
3214 width: Some(320.0),
3215 height: Some(240.0),
3216 min_width: None,
3217 max_width: None,
3218 min_height: None,
3219 max_height: None,
3220 padding: [0.0, 0.0, 0.0, 0.0],
3221 flex_grow: 0.0,
3222 flex_shrink: 0.0,
3223 }),
3224 vec![content],
3225 );
3226 ir.add_node(
3227 root,
3228 Op::Layout(LayoutOp::Box {
3229 width: Some(320.0),
3230 height: Some(240.0),
3231 min_width: None,
3232 max_width: None,
3233 min_height: None,
3234 max_height: None,
3235 padding: [0.0, 0.0, 0.0, 0.0],
3236 flex_grow: 0.0,
3237 flex_shrink: 0.0,
3238 aspect_ratio: None,
3239 }),
3240 vec![scroll],
3241 );
3242 ir.set_root(root);
3243
3244 let mut pipeline = Pipeline::new();
3245 let mut layout_engine = LayoutEngine::new();
3246 let scroll0 = ScrollStateMap::default();
3247 pipeline.replace_ir(ir, &Env::default());
3248 pipeline
3249 .ensure_layout(
3250 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3251 &mut layout_engine,
3252 &scroll0,
3253 )
3254 .unwrap();
3255 pipeline
3256 .prepare_current(
3257 LayoutSize {
3258 width: 320.0,
3259 height: 240.0,
3260 },
3261 LayoutSize {
3262 width: 320.0,
3263 height: 240.0,
3264 },
3265 false,
3266 &scroll0,
3267 &Default::default(),
3268 &Default::default(),
3269 &Default::default(),
3270 )
3271 .unwrap();
3272
3273 let mut scroll1 = ScrollStateMap::default();
3274 scroll1.set_offset(scroll, 180.0);
3275 pipeline
3276 .prepare_current(
3277 LayoutSize {
3278 width: 320.0,
3279 height: 240.0,
3280 },
3281 LayoutSize {
3282 width: 320.0,
3283 height: 240.0,
3284 },
3285 false,
3286 &scroll1,
3287 &Default::default(),
3288 &Default::default(),
3289 &Default::default(),
3290 )
3291 .unwrap();
3292
3293 fn find_layer_by_node(
3294 node: &fission_render::RenderNode,
3295 node_id: WidgetId,
3296 ) -> Option<&fission_render::RenderLayer> {
3297 match node {
3298 fission_render::RenderNode::Paint(_) => None,
3299 fission_render::RenderNode::Layer(layer) => {
3300 if layer.node_id == Some(node_id) {
3301 return Some(layer);
3302 }
3303 for child in &layer.children {
3304 if let Some(found) = find_layer_by_node(child, node_id) {
3305 return Some(found);
3306 }
3307 }
3308 None
3309 }
3310 }
3311 }
3312
3313 let scroll_layer = pipeline
3314 .retained_scene()
3315 .and_then(|scene| {
3316 scene
3317 .roots
3318 .iter()
3319 .find_map(|node| find_layer_by_node(node, scroll))
3320 })
3321 .expect("expected a retained scroll layer");
3322 assert!(
3323 scroll_layer.style.transform.is_none(),
3324 "scrollbar chrome must not inherit the content scroll transform"
3325 );
3326 let transform = scroll_layer
3327 .children
3328 .iter()
3329 .find_map(|child| match child {
3330 fission_render::RenderNode::Layer(layer) => layer.style.transform,
3331 fission_render::RenderNode::Paint(_) => None,
3332 })
3333 .expect("scroll content layer should carry a compositor transform");
3334 assert!(
3335 (transform[13] + 180.0).abs() <= 0.01,
3336 "expected retained content transform to patch to -180, got {}",
3337 transform[13]
3338 );
3339 }
3340
3341 #[test]
3342 fn scrollbar_thumb_patches_after_scroll_offset_changes() {
3343 let mut ir = CoreIR::new();
3344 let fill = WidgetId::derived(18, &[0]);
3345 let content = WidgetId::derived(18, &[1]);
3346 let scroll = WidgetId::derived(18, &[2]);
3347 let root = WidgetId::derived(18, &[3]);
3348
3349 ir.add_node(
3350 fill,
3351 Op::Paint(PaintOp::DrawRect {
3352 fill: Some(Fill::Solid(Color {
3353 r: 120,
3354 g: 120,
3355 b: 220,
3356 a: 255,
3357 })),
3358 stroke: None,
3359 corner_radius: 0.0,
3360 shadow: None,
3361 }),
3362 vec![],
3363 );
3364 ir.add_node(
3365 content,
3366 Op::Layout(LayoutOp::Box {
3367 width: Some(320.0),
3368 height: Some(640.0),
3369 min_width: None,
3370 max_width: None,
3371 min_height: None,
3372 max_height: None,
3373 padding: [0.0, 0.0, 0.0, 0.0],
3374 flex_grow: 0.0,
3375 flex_shrink: 0.0,
3376 aspect_ratio: None,
3377 }),
3378 vec![fill],
3379 );
3380 ir.add_node(
3381 scroll,
3382 Op::Layout(LayoutOp::Scroll {
3383 direction: fission_ir::FlexDirection::Column,
3384 show_scrollbar: true,
3385 width: Some(320.0),
3386 height: Some(240.0),
3387 min_width: None,
3388 max_width: None,
3389 min_height: None,
3390 max_height: None,
3391 padding: [0.0, 0.0, 0.0, 0.0],
3392 flex_grow: 0.0,
3393 flex_shrink: 0.0,
3394 }),
3395 vec![content],
3396 );
3397 ir.add_node(
3398 root,
3399 Op::Layout(LayoutOp::Box {
3400 width: Some(320.0),
3401 height: Some(240.0),
3402 min_width: None,
3403 max_width: None,
3404 min_height: None,
3405 max_height: None,
3406 padding: [0.0, 0.0, 0.0, 0.0],
3407 flex_grow: 0.0,
3408 flex_shrink: 0.0,
3409 aspect_ratio: None,
3410 }),
3411 vec![scroll],
3412 );
3413 ir.set_root(root);
3414
3415 let mut pipeline = Pipeline::new();
3416 let mut layout_engine = LayoutEngine::new();
3417 let scroll0 = ScrollStateMap::default();
3418 pipeline.replace_ir(ir, &Env::default());
3419 pipeline
3420 .ensure_layout(
3421 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3422 &mut layout_engine,
3423 &scroll0,
3424 )
3425 .unwrap();
3426 pipeline
3427 .prepare_current(
3428 LayoutSize::new(320.0, 240.0),
3429 LayoutSize::new(320.0, 240.0),
3430 false,
3431 &scroll0,
3432 &Default::default(),
3433 &Default::default(),
3434 &Default::default(),
3435 )
3436 .unwrap();
3437 let initial_thumb_y = scrollbar_thumb_y(pipeline.retained_scene().unwrap(), scroll)
3438 .expect("initial scrollbar thumb");
3439
3440 let mut scroll1 = ScrollStateMap::default();
3441 scroll1.set_offset(scroll, 200.0);
3442 pipeline
3443 .prepare_current(
3444 LayoutSize::new(320.0, 240.0),
3445 LayoutSize::new(320.0, 240.0),
3446 false,
3447 &scroll1,
3448 &Default::default(),
3449 &Default::default(),
3450 &Default::default(),
3451 )
3452 .unwrap();
3453 let moved_thumb_y = scrollbar_thumb_y(pipeline.retained_scene().unwrap(), scroll)
3454 .expect("moved scrollbar thumb");
3455
3456 assert!(
3457 moved_thumb_y > initial_thumb_y,
3458 "body scroll must patch the retained scrollbar thumb, before={initial_thumb_y}, after={moved_thumb_y}"
3459 );
3460
3461 fn scrollbar_thumb_y(scene: &fission_render::RenderScene, scroll: WidgetId) -> Option<f32> {
3462 fn find(node: &fission_render::RenderNode, scroll: WidgetId) -> Option<f32> {
3463 match node {
3464 fission_render::RenderNode::Paint(list) => list.ops.iter().find_map(|op| {
3465 if let fission_render::DisplayOp::DrawRect { rect, node_id, .. } = op {
3466 if *node_id == Some(scroll)
3467 && (rect.width() - 6.0).abs() <= 0.01
3468 && rect.height() < 200.0
3469 {
3470 return Some(rect.origin.y);
3471 }
3472 }
3473 None
3474 }),
3475 fission_render::RenderNode::Layer(layer) => {
3476 layer.children.iter().find_map(|child| find(child, scroll))
3477 }
3478 }
3479 }
3480 scene.roots.iter().find_map(|root| find(root, scroll))
3481 }
3482 }
3483
3484 #[test]
3485 fn overflowing_scroll_nodes_emit_visible_scroll_rails() {
3486 let mut ir = CoreIR::new();
3487 let fill = WidgetId::derived(16, &[0]);
3488 let content = WidgetId::derived(16, &[1]);
3489 let scroll = WidgetId::derived(16, &[2]);
3490 let root = WidgetId::derived(16, &[3]);
3491
3492 ir.add_node(
3493 fill,
3494 Op::Paint(PaintOp::DrawRect {
3495 fill: Some(Fill::Solid(Color {
3496 r: 80,
3497 g: 120,
3498 b: 220,
3499 a: 255,
3500 })),
3501 stroke: None,
3502 corner_radius: 0.0,
3503 shadow: None,
3504 }),
3505 vec![],
3506 );
3507 ir.add_node(
3508 content,
3509 Op::Layout(LayoutOp::Box {
3510 width: Some(320.0),
3511 height: Some(640.0),
3512 min_width: None,
3513 max_width: None,
3514 min_height: None,
3515 max_height: None,
3516 padding: [0.0, 0.0, 0.0, 0.0],
3517 flex_grow: 0.0,
3518 flex_shrink: 0.0,
3519 aspect_ratio: None,
3520 }),
3521 vec![fill],
3522 );
3523 ir.add_node(
3524 scroll,
3525 Op::Layout(LayoutOp::Scroll {
3526 direction: fission_ir::FlexDirection::Column,
3527 show_scrollbar: true,
3528 width: Some(320.0),
3529 height: Some(240.0),
3530 min_width: None,
3531 max_width: None,
3532 min_height: None,
3533 max_height: None,
3534 padding: [0.0, 0.0, 0.0, 0.0],
3535 flex_grow: 0.0,
3536 flex_shrink: 0.0,
3537 }),
3538 vec![content],
3539 );
3540 ir.add_node(
3541 root,
3542 Op::Layout(LayoutOp::Box {
3543 width: Some(320.0),
3544 height: Some(240.0),
3545 min_width: None,
3546 max_width: None,
3547 min_height: None,
3548 max_height: None,
3549 padding: [0.0, 0.0, 0.0, 0.0],
3550 flex_grow: 0.0,
3551 flex_shrink: 0.0,
3552 aspect_ratio: None,
3553 }),
3554 vec![scroll],
3555 );
3556 ir.set_root(root);
3557
3558 let mut pipeline = Pipeline::new();
3559 let mut layout_engine = LayoutEngine::new();
3560 let scroll_map = ScrollStateMap::default();
3561 pipeline.replace_ir(ir, &Env::default());
3562 pipeline
3563 .ensure_layout(
3564 LayoutRect::new(0.0, 0.0, 320.0, 240.0),
3565 &mut layout_engine,
3566 &scroll_map,
3567 )
3568 .unwrap();
3569 pipeline
3570 .prepare_current(
3571 LayoutSize {
3572 width: 320.0,
3573 height: 240.0,
3574 },
3575 LayoutSize {
3576 width: 320.0,
3577 height: 240.0,
3578 },
3579 false,
3580 &scroll_map,
3581 &Default::default(),
3582 &Default::default(),
3583 &Default::default(),
3584 )
3585 .unwrap();
3586
3587 fn count_scroll_rails(node: &fission_render::RenderNode, scroll: WidgetId) -> usize {
3588 match node {
3589 fission_render::RenderNode::Paint(list) => list
3590 .ops
3591 .iter()
3592 .filter(|op| match op {
3593 fission_render::DisplayOp::DrawRect { rect, node_id, .. } => {
3594 *node_id == Some(scroll)
3595 && (rect.width() - 6.0).abs() <= 0.01
3596 && rect.height() >= 200.0
3597 }
3598 _ => false,
3599 })
3600 .count(),
3601 fission_render::RenderNode::Layer(layer) => layer
3602 .children
3603 .iter()
3604 .map(|child| count_scroll_rails(child, scroll))
3605 .sum(),
3606 }
3607 }
3608
3609 let rail_count: usize = pipeline
3610 .retained_scene()
3611 .expect("retained scene")
3612 .roots
3613 .iter()
3614 .map(|node| count_scroll_rails(node, scroll))
3615 .sum();
3616 assert!(
3617 rail_count > 0,
3618 "expected an overflow rail for the scroll node"
3619 );
3620 }
3621}