Skip to main content

pax_runtime/engine/
piet_render_context.rs

1use pax_runtime_api::{OpacityScope, Paint, ResolvedStroke, StrokeCap, StrokeJoin};
2use piet::{
3    kurbo::{self, Affine, Shape},
4    FixedRadialGradient, LineCap, LineJoin, RenderContext as _, StrokeStyle,
5};
6use std::{
7    cell::RefCell,
8    collections::{HashMap, HashSet},
9    rc::Rc,
10};
11
12use super::layer_surface::{
13    replay_batches_by_directional_priority, surface_intersects_coverage_bounds, LayerSurfaceEntry,
14    LayerSurfaceLayout, LayoutChangeKind, ReplayPriorityEntry, SurfaceReplayCoordinator,
15};
16#[cfg(debug_assertions)]
17use super::layer_surface::{visible_surface_escape, VisibleSurfaceEscape};
18use crate::api;
19
20#[cfg(test)]
21#[path = "piet_compositing_tests.rs"]
22mod compositing_tests;
23
24#[cfg(debug_assertions)]
25fn log_visible_surface_escape(layer: usize, escape: VisibleSurfaceEscape) {
26    log::warn!(
27        "[pax-tile-window-escape] backend=piet layer={} visible_surfaces={} escaped_visible_surfaces={} max_gap_x={:.1} max_gap_y={:.1} previous_bounds={} visible_bounds={}",
28        layer,
29        escape.visible_surfaces,
30        escape.escaped_visible_surfaces,
31        escape.max_gap_x,
32        escape.max_gap_y,
33        format_rect(escape.previous_bounds),
34        format_rect(escape.visible_bounds),
35    );
36}
37
38#[cfg(debug_assertions)]
39fn format_rect(bounds: kurbo::Rect) -> String {
40    format!(
41        "{:.1},{:.1}-{:.1},{:.1}",
42        bounds.x0, bounds.y0, bounds.x1, bounds.y1
43    )
44}
45
46struct ImgData<R: piet::RenderContext> {
47    img: R::Image,
48    size: (usize, usize),
49}
50
51/// Chassis operations missing from Piet's portable API: reusable transparent
52/// surfaces and source-over composition with one opacity for the complete image.
53pub trait PietSurface: Sized {
54    type Context: piet::RenderContext;
55
56    fn context(&mut self) -> &mut Self::Context;
57    fn clear(&mut self);
58    fn configure(&mut self, origin: (f32, f32), size: (u32, u32), dpr: [f32; 2]);
59    /// Create a transparent surface with the same dimensions and coordinate space.
60    fn create_group(&self) -> Self;
61    /// Composite physical pixels without applying the current primitive transform
62    /// or clip again. Each recorded draw already carries its complete clip state.
63    fn composite(&mut self, source: &Self, opacity: f64);
64    /// Accumulate a paint mixture before applying its shape coverage and opacity.
65    fn draw_blend(
66        &mut self,
67        path: &kurbo::BezPath,
68        terms: &[(Paint, f64)],
69        opacity: f64,
70        paint_bounds: kurbo::Rect,
71    );
72    fn draw_image(
73        &mut self,
74        image: &<Self::Context as piet::RenderContext>::Image,
75        rect: kurbo::Rect,
76        opacity: f64,
77    );
78}
79
80type LayerDef<S> = (PietLayerTarget<S>, Box<dyn Fn() -> LayerSurfaceLayout>);
81
82#[derive(Clone, Default)]
83struct DrawState {
84    transform: Affine,
85    // Paths are captured in surface coordinates when clip() is called, not when
86    // a later draw changes its transform. Rc keeps per-draw snapshots inexpensive.
87    clips: Vec<Rc<kurbo::BezPath>>,
88}
89
90enum PietPaint<I> {
91    Fill(kurbo::BezPath, piet::PaintBrush),
92    Blend(kurbo::BezPath, Vec<(Paint, f64)>, f64, kurbo::Rect),
93    ResolvedStroke(kurbo::BezPath, piet::PaintBrush, f64, StrokeStyle),
94    Image(I, kurbo::Rect, f64),
95}
96
97struct PietDraw<I> {
98    state: DrawState,
99    scopes: Vec<OpacityScope>,
100    paint: PietPaint<I>,
101}
102
103// One reusable surface per simultaneously active nesting level, shared by sibling
104// runs. Unlike WGPU, Piet replays dirty content; no cross-frame content cache here.
105struct GroupSurface<S> {
106    surface: S,
107    child: Option<Box<GroupSurface<S>>>,
108}
109
110fn paint_draws<S: PietSurface>(
111    surface: &mut S,
112    scratch: &mut Option<Box<GroupSurface<S>>>,
113    draws: &[PietDraw<<S::Context as piet::RenderContext>::Image>],
114    depth: usize,
115) {
116    let mut index = 0;
117    while index < draws.len() {
118        if let Some(scope) = draws[index].scopes.get(depth) {
119            let end = index
120                + draws[index..]
121                    .iter()
122                    .take_while(|draw| {
123                        draw.scopes
124                            .get(depth)
125                            .is_some_and(|next| next.node_id == scope.node_id)
126                    })
127                    .count();
128            let opacity = scope.opacity.clamp(0.0, 1.0) as f64;
129            if opacity == 1.0 {
130                paint_draws(surface, scratch, &draws[index..end], depth + 1);
131            } else if opacity > 0.0 {
132                let group = scratch.get_or_insert_with(|| {
133                    Box::new(GroupSurface {
134                        surface: surface.create_group(),
135                        child: None,
136                    })
137                });
138                group.surface.clear();
139                paint_draws(
140                    &mut group.surface,
141                    &mut group.child,
142                    &draws[index..end],
143                    depth + 1,
144                );
145                surface.composite(&group.surface, opacity);
146            }
147            index = end;
148            continue;
149        }
150        let draw = &draws[index];
151        let context = surface.context();
152        let _ = context.save();
153        for clip in &draw.state.clips {
154            context.clip(clip.as_ref());
155        }
156        context.transform(draw.state.transform);
157        match &draw.paint {
158            PietPaint::Fill(path, brush) => context.fill(path, brush),
159            PietPaint::Blend(path, terms, opacity, bounds) => {
160                surface.draw_blend(path, terms, *opacity, *bounds)
161            }
162            PietPaint::ResolvedStroke(path, brush, width, style) => {
163                context.stroke_styled(path, brush, *width, style)
164            }
165            PietPaint::Image(image, rect, opacity) => surface.draw_image(image, *rect, *opacity),
166        }
167        let _ = surface.context().restore();
168        index += 1;
169    }
170}
171
172/// Retained metadata for one piet-backed browser canvas surface.
173pub struct PietLayerRenderer<S: PietSurface> {
174    key: String,
175    host_signature: String,
176    surface: S,
177    scratch: Option<Box<GroupSurface<S>>>,
178    state: DrawState,
179    saves: Vec<DrawState>,
180    scopes: Vec<OpacityScope>,
181    draws: Vec<PietDraw<<S::Context as piet::RenderContext>::Image>>,
182    origin_x: f32,
183    origin_y: f32,
184    logical_width: f32,
185    logical_height: f32,
186    surface_width: u32,
187    surface_height: u32,
188    dpr: [f32; 2],
189}
190
191impl<S: PietSurface> PietLayerRenderer<S> {
192    pub fn new(
193        key: String,
194        host_signature: String,
195        canvas: S,
196        surface: &LayerSurfaceEntry,
197    ) -> Self {
198        Self {
199            key,
200            host_signature,
201            surface: canvas,
202            scratch: None,
203            state: DrawState::default(),
204            saves: Vec::new(),
205            scopes: Vec::new(),
206            draws: Vec::new(),
207            origin_x: surface.origin_x,
208            origin_y: surface.origin_y,
209            logical_width: surface.surface.logical_width,
210            logical_height: surface.surface.logical_height,
211            surface_width: surface.surface.surface_width,
212            surface_height: surface.surface.surface_height,
213            dpr: surface.surface.dpr,
214        }
215    }
216
217    fn context_mut(&mut self) -> &mut S::Context {
218        self.surface.context()
219    }
220
221    fn clear(&mut self) {
222        self.surface.clear();
223        self.draws.clear();
224        self.state = DrawState::default();
225        self.saves.clear();
226        self.scopes.clear();
227    }
228
229    fn record(&mut self, paint: PietPaint<<S::Context as piet::RenderContext>::Image>) {
230        self.draws.push(PietDraw {
231            state: self.state.clone(),
232            scopes: self.scopes.clone(),
233            paint,
234        });
235    }
236
237    fn flush(&mut self) {
238        paint_draws(&mut self.surface, &mut self.scratch, &self.draws, 0);
239        self.draws.clear();
240    }
241
242    fn intersects_coverage_bounds(&self, bounds: &kurbo::Rect) -> bool {
243        surface_intersects_coverage_bounds(
244            bounds,
245            self.origin_x as f64,
246            self.origin_y as f64,
247            self.logical_width as f64,
248            self.logical_height as f64,
249        )
250    }
251
252    fn coverage_bounds(&self) -> kurbo::Rect {
253        kurbo::Rect::new(
254            self.origin_x as f64,
255            self.origin_y as f64,
256            self.origin_x as f64 + self.logical_width as f64,
257            self.origin_y as f64 + self.logical_height as f64,
258        )
259    }
260
261    fn update_layout(&mut self, surface: &LayerSurfaceEntry) -> LayoutChangeKind {
262        let origin_changed = (self.origin_x - surface.origin_x).abs() > f32::EPSILON
263            || (self.origin_y - surface.origin_y).abs() > f32::EPSILON;
264        let size_changed = self.logical_width != surface.surface.logical_width
265            || self.logical_height != surface.surface.logical_height
266            || self.surface_width != surface.surface.surface_width
267            || self.surface_height != surface.surface.surface_height
268            || self.dpr != surface.surface.dpr;
269
270        self.origin_x = surface.origin_x;
271        self.origin_y = surface.origin_y;
272        self.logical_width = surface.surface.logical_width;
273        self.logical_height = surface.surface.logical_height;
274        self.surface_width = surface.surface.surface_width;
275        self.surface_height = surface.surface.surface_height;
276        self.dpr = surface.surface.dpr;
277
278        if size_changed || origin_changed {
279            self.surface.configure(
280                (self.origin_x, self.origin_y),
281                (self.surface_width, self.surface_height),
282                self.dpr,
283            );
284            self.scratch = None;
285        }
286
287        if size_changed {
288            self.clear();
289        }
290
291        if size_changed {
292            LayoutChangeKind::Resized
293        } else if origin_changed {
294            LayoutChangeKind::OriginOnly
295        } else {
296            LayoutChangeKind::Unchanged
297        }
298    }
299}
300
301/// Current piet surface set for one logical layer.
302pub struct PietLayerTarget<S: PietSurface> {
303    renderers: Vec<PietLayerRenderer<S>>,
304    active: bool,
305}
306
307impl<S: PietSurface> PietLayerTarget<S> {
308    pub fn new(renderers: Vec<PietLayerRenderer<S>>, active: bool) -> Self {
309        Self { renderers, active }
310    }
311
312    fn activate(&mut self) -> bool {
313        let changed = !self.active;
314        self.active = true;
315        changed
316    }
317
318    fn deactivate(&mut self) {
319        self.active = false;
320    }
321}
322
323/// `RenderContext` implementation backed by piet.
324pub struct PietRenderer<S: PietSurface> {
325    layers: Vec<LayerDef<S>>,
326    image_map: HashMap<String, ImgData<S::Context>>,
327    layer_factory: Box<dyn Fn(usize) -> LayerDef<S>>,
328    ready_layers: Vec<usize>,
329    replay_layers: Vec<usize>,
330    surface_replay: SurfaceReplayCoordinator,
331    active_render_scopes: Vec<Vec<Vec<usize>>>,
332    clean_skipped_canvas_nodes: HashSet<(usize, u32)>,
333}
334
335impl<S: PietSurface> PietRenderer<S> {
336    /// Create a piet renderer with a chassis-provided logical layer factory.
337    pub fn new(layer_factory: impl Fn(usize) -> LayerDef<S> + 'static) -> Self {
338        Self {
339            layer_factory: Box::new(layer_factory),
340            layers: Vec::new(),
341            image_map: HashMap::new(),
342            ready_layers: Vec::new(),
343            replay_layers: Vec::new(),
344            surface_replay: SurfaceReplayCoordinator::default(),
345            active_render_scopes: Vec::new(),
346            clean_skipped_canvas_nodes: HashSet::new(),
347        }
348    }
349
350    fn first_context_mut(&mut self) -> Option<&mut S::Context> {
351        self.layers
352            .iter_mut()
353            .find_map(|(target, _)| target.renderers.first_mut())
354            .map(PietLayerRenderer::context_mut)
355    }
356
357    fn with_layer_renderer(&mut self, layer: usize, mut f: impl FnMut(&mut PietLayerRenderer<S>)) {
358        let scoped_indices = self
359            .active_render_scopes
360            .get(layer)
361            .and_then(|scopes| scopes.last())
362            .cloned();
363
364        let Some((target, _)) = self.layers.get_mut(layer) else {
365            return;
366        };
367        if !target.active {
368            return;
369        }
370
371        if let Some(indices) = scoped_indices {
372            for index in indices {
373                if let Some(renderer) = target.renderers.get_mut(index) {
374                    f(renderer);
375                }
376            }
377        } else {
378            for renderer in &mut target.renderers {
379                f(renderer);
380            }
381        }
382    }
383
384    fn push_render_scope(&mut self, layer: usize, renderer_indices: Vec<usize>) {
385        if self.active_render_scopes.len() <= layer {
386            self.active_render_scopes.resize_with(layer + 1, Vec::new);
387        }
388        self.active_render_scopes[layer].push(renderer_indices);
389    }
390
391    fn pop_render_scope(&mut self, layer: usize) {
392        if let Some(scopes) = self.active_render_scopes.get_mut(layer) {
393            scopes.pop();
394        }
395    }
396
397    fn take_replay_layer_ids(&mut self) -> Vec<usize> {
398        let mut replay = std::mem::take(&mut self.replay_layers);
399        replay.sort_unstable();
400        replay.dedup();
401        replay
402    }
403
404    fn targeted_or_all_indices(&self, layer: usize, renderer_count: usize) -> Vec<usize> {
405        self.surface_replay
406            .targeted_or_all_indices(layer, renderer_count)
407    }
408
409    fn clear_targeted_replay_scope(&mut self, layer: usize) {
410        self.surface_replay.clear_targeted_replay_scope(layer);
411    }
412
413    fn set_targeted_replay_batches(
414        &mut self,
415        layer: usize,
416        batches: Vec<Vec<usize>>,
417        bounds_by_surface: HashMap<usize, Vec<kurbo::Rect>>,
418    ) {
419        self.surface_replay
420            .set_targeted_replay_batches(layer, batches, bounds_by_surface);
421    }
422
423    fn advance_targeted_replay_queue(&mut self, layer: usize) -> bool {
424        self.surface_replay.advance_targeted_replay_queue(layer)
425    }
426
427    fn targeted_replay_surface_bounds(
428        &self,
429        layer: usize,
430        target: &PietLayerTarget<S>,
431    ) -> Option<Vec<kurbo::Rect>> {
432        self.surface_replay
433            .targeted_replay_surface_bounds(layer, |index| {
434                target
435                    .renderers
436                    .get(index)
437                    .map(PietLayerRenderer::coverage_bounds)
438            })
439    }
440
441    fn replay_region_bounds(&self, layer: usize) -> Option<Vec<kurbo::Rect>> {
442        let Some((target, _)) = self.layers.get(layer) else {
443            return None;
444        };
445        self.targeted_replay_surface_bounds(layer, target)
446    }
447
448    fn refresh_layer_layouts<I>(&mut self, layer_indices: I)
449    where
450        I: IntoIterator<Item = usize>,
451    {
452        for layer_index in layer_indices {
453            let Some((target, layout_provider)) = self.layers.get_mut(layer_index) else {
454                continue;
455            };
456            let layout = (layout_provider)();
457            let layout_matches = layer_layout_matches_target(target, &layout);
458
459            if !layout.active {
460                target.deactivate();
461                self.clear_targeted_replay_scope(layer_index);
462                continue;
463            }
464
465            if !layout_matches {
466                self.clear_targeted_replay_scope(layer_index);
467                let (new_target, new_provider) = (self.layer_factory)(layer_index);
468                let has_surfaces = !new_target.renderers.is_empty();
469                self.layers[layer_index] = (new_target, new_provider);
470                if has_surfaces {
471                    self.ready_layers.push(layer_index);
472                    self.replay_layers.push(layer_index);
473                }
474                continue;
475            }
476
477            let activated_replay = target.activate() && !target.renderers.is_empty();
478
479            let mut targeted_replay_entries = Vec::new();
480            let mut targeted_replay_bounds = HashMap::new();
481            let mut needs_full_layer_replay = false;
482            #[cfg(debug_assertions)]
483            {
484                let previous_surface_bounds: Vec<_> = target
485                    .renderers
486                    .iter()
487                    .map(PietLayerRenderer::coverage_bounds)
488                    .collect();
489                if let Some(escape) =
490                    visible_surface_escape(&previous_surface_bounds, &layout.surfaces)
491                {
492                    log_visible_surface_escape(layer_index, escape);
493                }
494            }
495            for (index, (surface, renderer)) in layout
496                .surfaces
497                .iter()
498                .zip(target.renderers.iter_mut())
499                .enumerate()
500            {
501                let previous_bounds = renderer.coverage_bounds();
502                let layout_change = renderer.update_layout(surface);
503                let current_bounds = renderer.coverage_bounds();
504                match layout_change {
505                    LayoutChangeKind::Unchanged => {}
506                    LayoutChangeKind::OriginOnly => {
507                        targeted_replay_entries.push(ReplayPriorityEntry::new(
508                            index,
509                            surface.replay_priority,
510                            previous_bounds,
511                            current_bounds,
512                        ));
513                        targeted_replay_bounds.insert(index, vec![previous_bounds, current_bounds]);
514                    }
515                    LayoutChangeKind::Resized => {
516                        needs_full_layer_replay = true;
517                    }
518                }
519            }
520            if activated_replay || needs_full_layer_replay {
521                self.clear_targeted_replay_scope(layer_index);
522                self.replay_layers.push(layer_index);
523            } else if !targeted_replay_entries.is_empty() {
524                self.set_targeted_replay_batches(
525                    layer_index,
526                    replay_batches_by_directional_priority(&targeted_replay_entries),
527                    targeted_replay_bounds,
528                );
529                self.replay_layers.push(layer_index);
530            }
531        }
532    }
533}
534
535fn layer_layout_matches_target<S: PietSurface>(
536    target: &PietLayerTarget<S>,
537    layout: &LayerSurfaceLayout,
538) -> bool {
539    layout.surfaces.len() == target.renderers.len()
540        && layout
541            .surfaces
542            .iter()
543            .zip(target.renderers.iter())
544            .all(|(surface, renderer)| {
545                surface.key == renderer.key && surface.host_signature == renderer.host_signature
546            })
547}
548
549#[cfg(test)]
550mod tests {
551    use super::replay_batches_by_directional_priority;
552    use crate::engine::layer_surface::ReplayPriorityEntry;
553    use kurbo::Rect;
554
555    #[test]
556    fn piet_replay_batches_coalesce_visible_surfaces_first() {
557        let batches = replay_batches_by_directional_priority(&[
558            replay_entry(0, 0, 0.0, 100.0),
559            replay_entry(1, 0, 100.0, 200.0),
560            replay_entry(2, 1, 200.0, 300.0),
561            replay_entry(3, 1, -100.0, 0.0),
562        ]);
563
564        assert_eq!(batches[0], vec![0, 1]);
565        assert_eq!(batches[1], vec![2]);
566        assert_eq!(batches[2], vec![3]);
567    }
568
569    #[test]
570    fn piet_replay_batches_keep_warm_only_directional_batches() {
571        let batches = replay_batches_by_directional_priority(&[
572            replay_entry(0, 1, 0.0, 100.0),
573            replay_entry(1, 1, 100.0, 200.0),
574            replay_entry(2, 1, -100.0, 0.0),
575        ]);
576
577        assert_eq!(batches, vec![vec![1], vec![0], vec![2]]);
578    }
579
580    fn replay_entry(
581        index: usize,
582        priority: i32,
583        previous_y: f64,
584        current_y: f64,
585    ) -> ReplayPriorityEntry {
586        ReplayPriorityEntry::new(
587            index,
588            priority,
589            Rect::new(0.0, previous_y, 100.0, previous_y + 100.0),
590            Rect::new(0.0, current_y, 100.0, current_y + 100.0),
591        )
592    }
593}
594
595impl<S: PietSurface> api::RenderContext for PietRenderer<S> {
596    fn fill_with_opacity(
597        &mut self,
598        layer: usize,
599        path: kurbo::BezPath,
600        fill: &Paint,
601        opacity: f64,
602    ) {
603        if let Paint::Blend(terms) = fill {
604            self.with_layer_renderer(layer, |renderer| {
605                renderer.record(PietPaint::Blend(
606                    path.clone(),
607                    terms.clone(),
608                    opacity,
609                    path.bounding_box(),
610                ))
611            });
612        } else if let Some(brush) =
613            fill_to_piet_brush(&fill.with_alpha_factor(opacity), path.bounding_box())
614        {
615            self.with_layer_renderer(layer, |renderer| {
616                renderer.record(PietPaint::Fill(path.clone(), brush.clone()))
617            });
618        }
619    }
620
621    fn stroke_with_opacity(
622        &mut self,
623        layer: usize,
624        path: kurbo::BezPath,
625        stroke: &ResolvedStroke,
626        opacity: f64,
627    ) {
628        self.stroke_with_draw_range_and_material_and_opacity(
629            layer,
630            path,
631            stroke,
632            &stroke.material,
633            opacity,
634            0.0,
635            1.0,
636        );
637    }
638
639    fn stroke_with_draw_range_and_material_and_opacity(
640        &mut self,
641        layer: usize,
642        path: kurbo::BezPath,
643        stroke: &ResolvedStroke,
644        _material: &api::ResolvedMaterial,
645        opacity: f64,
646        draw_start: f64,
647        draw_end: f64,
648    ) {
649        if draw_start >= draw_end || stroke.width_pixels() <= f64::EPSILON {
650            return;
651        }
652        let bounds = path.bounding_box();
653        let path = api::drawing::path_trim::trim_bez_path(&path, draw_start, draw_end);
654        let paint = stroke.paint.clone();
655        if let Paint::Blend(terms) = paint {
656            if let Some(outline) = api::drawing::stroke_utils::stroked_outline_path(&path, stroke) {
657                self.with_layer_renderer(layer, |renderer| {
658                    renderer.record(PietPaint::Blend(
659                        outline.clone(),
660                        terms.clone(),
661                        opacity,
662                        bounds,
663                    ))
664                });
665            }
666        } else if let Some(brush) = fill_to_piet_brush(&paint.with_alpha_factor(opacity), bounds) {
667            self.with_layer_renderer(layer, |renderer| {
668                renderer.record(PietPaint::ResolvedStroke(
669                    path.clone(),
670                    brush.clone(),
671                    stroke.width_pixels(),
672                    stroke_to_piet_style(stroke),
673                ))
674            });
675        }
676    }
677
678    fn save(&mut self, layer: usize) {
679        self.with_layer_renderer(layer, |renderer| {
680            renderer.saves.push(renderer.state.clone())
681        });
682    }
683
684    fn transform(&mut self, layer: usize, affine: Affine) {
685        self.with_layer_renderer(layer, |renderer| renderer.state.transform *= affine);
686    }
687
688    fn clip(&mut self, layer: usize, path: kurbo::BezPath) {
689        self.with_layer_renderer(layer, |renderer| {
690            renderer
691                .state
692                .clips
693                .push(Rc::new(renderer.state.transform * &path))
694        });
695    }
696
697    fn restore(&mut self, layer: usize) {
698        self.with_layer_renderer(layer, |renderer| {
699            if let Some(state) = renderer.saves.pop() {
700                renderer.state = state;
701            }
702        });
703    }
704
705    fn load_image(&mut self, path: &str, buf: &[u8], width: usize, height: usize) {
706        let Some(render_context) = self.first_context_mut() else {
707            return;
708        };
709        let img = render_context
710            .make_image(width, height, buf, piet::ImageFormat::RgbaSeparate)
711            .expect("image creation successful");
712        self.image_map.insert(
713            path.to_owned(),
714            ImgData {
715                img,
716                size: (width, height),
717            },
718        );
719    }
720
721    fn get_image_size(&mut self, image_path: &str) -> Option<(usize, usize)> {
722        self.image_map.get(image_path).map(|img| img.size)
723    }
724
725    fn draw_image_with_opacity(
726        &mut self,
727        layer: usize,
728        image_path: &str,
729        rect: kurbo::Rect,
730        opacity: f64,
731    ) {
732        let Some(data) = self.image_map.get(image_path) else {
733            return;
734        };
735        let image = data.img.clone();
736        self.with_layer_renderer(layer, |renderer| {
737            renderer.record(PietPaint::Image(
738                image.clone(),
739                rect,
740                opacity.clamp(0.0, 1.0),
741            ));
742        });
743    }
744
745    fn layers(&self) -> usize {
746        self.layers.len()
747    }
748
749    fn resize_layers_to(&mut self, layer_count: usize, dirty_canvases: Rc<RefCell<Vec<bool>>>) {
750        let current_len = self.layers.len();
751        match layer_count.cmp(&current_len) {
752            std::cmp::Ordering::Less => {
753                self.layers.truncate(layer_count);
754                self.active_render_scopes.truncate(layer_count);
755                self.surface_replay.truncate_layers(layer_count);
756            }
757            std::cmp::Ordering::Equal => return,
758            std::cmp::Ordering::Greater => {
759                for i in current_len..layer_count {
760                    let (target, provider) = (self.layer_factory)(i);
761                    let has_surfaces = target.active && !target.renderers.is_empty();
762                    self.layers.push((target, provider));
763                    if let Some(dirty_bit) = dirty_canvases.borrow_mut().get_mut(i) {
764                        *dirty_bit = true;
765                    }
766                    if has_surfaces {
767                        self.ready_layers.push(i);
768                    }
769                }
770            }
771        }
772    }
773
774    fn image_loaded(&self, path: &str) -> bool {
775        self.image_map.contains_key(path)
776    }
777
778    fn clear(&mut self, layer: usize) {
779        let scoped_indices = self
780            .active_render_scopes
781            .get(layer)
782            .and_then(|scopes| scopes.last())
783            .cloned()
784            .or_else(|| {
785                self.layers
786                    .get(layer)
787                    .map(|(target, _)| self.targeted_or_all_indices(layer, target.renderers.len()))
788            });
789        let Some((target, _)) = self.layers.get_mut(layer) else {
790            return;
791        };
792        if !target.active {
793            return;
794        }
795        if let Some(indices) = scoped_indices {
796            for index in indices {
797                if let Some(renderer) = target.renderers.get_mut(index) {
798                    renderer.clear();
799                }
800            }
801        } else {
802            for renderer in &mut target.renderers {
803                renderer.clear();
804            }
805        }
806    }
807
808    fn supports_subtree_opacity(&self) -> bool {
809        true
810    }
811
812    fn set_node_opacity_scopes(&mut self, layer: usize, _node_id: u32, scopes: &[OpacityScope]) {
813        self.with_layer_renderer(layer, |renderer| renderer.scopes = scopes.to_vec());
814    }
815
816    fn flush(&mut self, layer: usize, _dirty_canvases: Rc<RefCell<Vec<bool>>>) {
817        self.with_layer_renderer(layer, |renderer| renderer.flush());
818        if self.advance_targeted_replay_queue(layer) {
819            self.replay_layers.push(layer);
820        }
821    }
822
823    fn resize(&mut self, _width: usize, _height: usize) {
824        let layer_count = self.layers.len();
825        self.refresh_layer_layouts(0..layer_count);
826    }
827
828    fn refresh_layers(&mut self, layers: &[usize]) {
829        self.refresh_layer_layouts(layers.iter().copied());
830    }
831
832    fn clear_targeted_replay(&mut self, layer: usize) {
833        self.clear_targeted_replay_scope(layer);
834    }
835
836    fn take_ready_canvas_layers(&mut self) -> Vec<usize> {
837        let mut ready = std::mem::take(&mut self.ready_layers);
838        ready.sort_unstable();
839        ready.dedup();
840        ready
841    }
842
843    fn take_replay_canvas_layer_updates(
844        &mut self,
845    ) -> Vec<pax_runtime_api::ReplayCanvasLayerUpdate> {
846        let replay_layers = self.take_replay_layer_ids();
847        replay_layers
848            .into_iter()
849            .map(|layer| pax_runtime_api::ReplayCanvasLayerUpdate {
850                layer,
851                regions: self.replay_region_bounds(layer),
852            })
853            .collect()
854    }
855
856    fn retains_canvas_nodes(&self) -> bool {
857        false
858    }
859
860    fn begin_node(&mut self, layer: usize, _node_id: u32, _z_index: i32, _light_mask: u32) -> bool {
861        let renderer_count = {
862            let Some((target, _)) = self.layers.get(layer) else {
863                return false;
864            };
865            if !target.active {
866                return false;
867            }
868            target.renderers.len()
869        };
870        let selected = self.targeted_or_all_indices(layer, renderer_count);
871        let began = !selected.is_empty();
872        if began {
873            self.push_render_scope(layer, selected);
874        }
875        began
876    }
877
878    fn begin_node_with_bounds(
879        &mut self,
880        layer: usize,
881        node_id: u32,
882        _z_index: i32,
883        coverage_bounds: kurbo::Rect,
884        _light_mask: u32,
885    ) -> bool {
886        let renderer_count = {
887            let Some((target, _)) = self.layers.get(layer) else {
888                return false;
889            };
890            if !target.active {
891                return false;
892            }
893            target.renderers.len()
894        };
895        let candidate_indices = self.targeted_or_all_indices(layer, renderer_count);
896        let Some((target, _)) = self.layers.get(layer) else {
897            return false;
898        };
899        let selected: Vec<_> = target
900            .renderers
901            .iter()
902            .enumerate()
903            .filter(|(index, _)| candidate_indices.contains(index))
904            .filter_map(|(index, renderer)| {
905                renderer
906                    .intersects_coverage_bounds(&coverage_bounds)
907                    .then_some(index)
908            })
909            .collect();
910        let began = !selected.is_empty();
911        if began {
912            self.push_render_scope(layer, selected);
913        } else {
914            self.clean_skipped_canvas_nodes.insert((layer, node_id));
915        }
916        began
917    }
918
919    fn take_clean_skipped_node(&mut self, layer: usize, node_id: u32) -> bool {
920        self.clean_skipped_canvas_nodes.remove(&(layer, node_id))
921    }
922
923    fn end_node(&mut self, layer: usize, _node_id: u32) -> bool {
924        self.pop_render_scope(layer);
925        true
926    }
927
928    fn remove_node(&mut self, _layer: usize, _node_id: u32) -> bool {
929        true
930    }
931}
932
933/// Resolves a single paint to a Piet brush. Mixtures require the chassis paint
934/// accumulator, since generic Piet has no additive compositing operation.
935pub fn fill_to_piet_brush(fill: &Paint, rect: kurbo::Rect) -> Option<piet::PaintBrush> {
936    let rect = api::drawing::paint_bounds(rect)?;
937    Some(match fill {
938        Paint::MeshGradient(_) => {
939            static WARNING: std::sync::Once = std::sync::Once::new();
940            WARNING.call_once(|| {
941                log::warn!("Mesh-gradient paint requires WGPU; Piet paints no mesh field")
942            });
943            return None;
944        }
945        Paint::Blend(_) => return None,
946        Paint::Solid(color) => color.to_piet_color().into(),
947        Paint::LinearGradient(linear) => {
948            let bounds = (rect.width(), rect.height());
949            let point = |value: (api::Size, api::Size)| {
950                kurbo::Point::new(
951                    rect.x0 + value.0.evaluate(bounds, api::Axis::X),
952                    rect.y0 + value.1.evaluate(bounds, api::Axis::Y),
953                )
954            };
955            let start = point(linear.start);
956            let end = point(linear.end);
957            let length = start.distance(end);
958            if !length.is_finite() || length <= f64::EPSILON {
959                return Some(piet::Color::TRANSPARENT.into());
960            }
961            piet::FixedLinearGradient {
962                start,
963                end,
964                stops: linear
965                    .stops
966                    .iter()
967                    .map(|stop| piet::GradientStop {
968                        pos: (stop.position.evaluate((length, 0.0), api::Axis::X) / length) as f32,
969                        color: stop.color.to_piet_color(),
970                    })
971                    .collect(),
972            }
973            .into()
974        }
975        Paint::RadialGradient(radial) => {
976            let Some(g) = radial.resolve_geometry(rect) else {
977                return Some(piet::Color::TRANSPARENT.into());
978            };
979            if radial.stops.is_empty() {
980                return Some(piet::Color::TRANSPARENT.into());
981            }
982            let stops = radial
983                .stops
984                .iter()
985                .map(|s| piet::GradientStop {
986                    pos: (s
987                        .position
988                        .evaluate((g.radius, 0.0), pax_runtime_api::Axis::X)
989                        / g.radius) as f32,
990                    color: s.color.to_piet_color(),
991                })
992                .collect();
993            FixedRadialGradient {
994                center: g.center,
995                origin_offset: g.focal_point - g.center,
996                radius: g.radius,
997                stops,
998            }
999            .into()
1000        }
1001    })
1002}
1003
1004fn stroke_to_piet_style(stroke: &ResolvedStroke) -> StrokeStyle {
1005    let mut style = StrokeStyle::new();
1006    style.set_line_cap(match stroke.cap {
1007        StrokeCap::Butt => LineCap::Butt,
1008        StrokeCap::Round => LineCap::Round,
1009        StrokeCap::Square => LineCap::Square,
1010    });
1011    style.set_line_join(match stroke.join {
1012        StrokeJoin::Miter => LineJoin::Miter { limit: 4.0 },
1013        StrokeJoin::Round => LineJoin::Round,
1014        StrokeJoin::Bevel => LineJoin::Bevel,
1015    });
1016    style
1017}
1018
1019#[cfg(test)]
1020mod radial_tests {
1021    use super::*;
1022    use pax_runtime_api::{Color, GradientStop, RadialGradient, Size};
1023
1024    #[test]
1025    fn linear_brush_keeps_origin_and_pixel_stops_on_collapsed_axes() {
1026        let paint = Paint::linearGradient(
1027            (Size::Percent(0.into()), Size::Percent(50.into())),
1028            (Size::Percent(100.into()), Size::Percent(50.into())),
1029            vec![
1030                GradientStop::get(Color::RED, Size::Pixels(25.into())),
1031                GradientStop::get(Color::BLUE, Size::Percent(100.into())),
1032            ],
1033        );
1034        let Some(piet::PaintBrush::Fixed(piet::FixedGradient::Linear(g))) =
1035            fill_to_piet_brush(&paint, kurbo::Rect::new(40.0, 60.0, 140.0, 60.0))
1036        else {
1037            panic!("linear brush")
1038        };
1039        assert_eq!(g.start, kurbo::Point::new(40.0, 60.0));
1040        assert_eq!(g.end, kurbo::Point::new(140.0, 60.0));
1041        assert_eq!(g.stops[0].pos, 0.25);
1042    }
1043
1044    #[test]
1045    fn radial_brush_uses_local_bounds_radius_and_focal_point() {
1046        let mut radial = RadialGradient {
1047            start: (Size::Percent(75.into()), Size::Percent(25.into())),
1048            end: (Size::Percent(50.into()), Size::Percent(50.into())),
1049            radius: 50.0,
1050            stops: vec![
1051                GradientStop::get(Color::BLACK, Size::Pixels(25.into())),
1052                GradientStop::get(Color::WHITE, Size::Percent(100.into())),
1053            ],
1054        };
1055        let rect = kurbo::Rect::new(10.0, 20.0, 210.0, 120.0);
1056        let Some(piet::PaintBrush::Fixed(piet::FixedGradient::Radial(g))) =
1057            fill_to_piet_brush(&Paint::RadialGradient(radial.clone()), rect)
1058        else {
1059            panic!("radial brush")
1060        };
1061        assert_eq!(g.center, kurbo::Point::new(110.0, 70.0));
1062        assert_eq!(g.origin_offset, kurbo::Vec2::new(50.0, -25.0));
1063        assert_eq!(g.radius, 50.0);
1064        assert_eq!(
1065            g.stops.iter().map(|s| s.pos).collect::<Vec<_>>(),
1066            vec![0.5, 1.0]
1067        );
1068        for radius in [0.0, -1.0, f64::NAN, f64::INFINITY] {
1069            radial.radius = radius;
1070            assert!(radial.resolve_geometry(rect).is_none());
1071            assert!(
1072                matches!(fill_to_piet_brush(&Paint::RadialGradient(radial.clone()), rect),
1073                Some(piet::PaintBrush::Color(c)) if c == piet::Color::TRANSPARENT)
1074            );
1075        }
1076    }
1077}