Skip to main content

pax_runtime/engine/
pax_gpu_render_context.rs

1use super::layer_surface::{
2    replay_batches_by_directional_priority, surface_intersects_coverage_bounds, LayerSurfaceEntry,
3    LayerSurfaceLayout, LayoutChangeKind, ReplayPriorityEntry, SurfaceReplayCoordinator,
4};
5#[cfg(debug_assertions)]
6use super::layer_surface::{visible_surface_escape, VisibleSurfaceEscape};
7use kurbo::{BezPath, PathEl, Rect, Shape};
8use pax_gpu::{
9    point, Box2D, DrawRange as PixelDrawRange, Image, LightShape as PixelLightShape,
10    Material as PixelMaterial, Path, ResourceChurnStats, SceneLight as PixelSceneLight,
11    SceneLighting as PixelSceneLighting, Stroke as PixelStroke, StrokeCap as PixelStrokeCap,
12    StrokeJoin as PixelStrokeJoin, Transform2D, WgpuRenderer,
13};
14use pax_runtime_api::{
15    Axis, LayerSurfaceScreenshotData, Material, PathSmoothing, RenderContext,
16    ReplayCanvasLayerUpdate, SceneLighting, ScreenshotData, Stroke, StrokeCap, StrokeJoin,
17};
18use std::{
19    cell::{Cell, RefCell},
20    collections::{HashMap, HashSet, VecDeque},
21    future::Future,
22    pin::Pin,
23    rc::Rc,
24};
25
26#[cfg(not(target_arch = "wasm32"))]
27use pollster;
28
29/// Retained renderer bound to one physical surface tile for a logical layer.
30pub struct LayerRenderer {
31    key: String,
32    host_signature: String,
33    renderer: WgpuRenderer<'static>,
34    origin_x: f32,
35    origin_y: f32,
36    logical_width: f32,
37    logical_height: f32,
38    surface_width: u32,
39    surface_height: u32,
40    dpr: [f32; 2],
41}
42
43/// Current renderer set for one logical layer.
44pub struct LayerTarget {
45    renderers: Vec<LayerRenderer>,
46    active: bool,
47    needs_replay: bool,
48}
49
50#[cfg(debug_assertions)]
51#[derive(Clone, Copy, Default)]
52struct TileCullStats {
53    nodes_considered: u64,
54    selected_surfaces: u64,
55    skipped_surfaces: u64,
56    stale_surface_removal_attempts: u64,
57    origin_only_retargets: u64,
58    targeted_replay_batches: u64,
59    targeted_replay_surfaces: u64,
60    targeted_replay_visible_batches: u64,
61    targeted_replay_warm_batches: u64,
62    targeted_replay_visible_surfaces: u64,
63    targeted_replay_warm_surfaces: u64,
64    full_layer_replays: u64,
65    resize_resets: u64,
66}
67
68#[cfg(debug_assertions)]
69fn replay_priority_stats(
70    entries: &[ReplayPriorityEntry],
71    batches: &[Vec<usize>],
72) -> (usize, usize, usize, usize) {
73    let visible_surfaces = entries.iter().filter(|entry| entry.priority <= 0).count();
74    let warm_surfaces = entries.len().saturating_sub(visible_surfaces);
75    let priority_by_index: HashMap<usize, i32> = entries
76        .iter()
77        .map(|entry| (entry.index, entry.priority))
78        .collect();
79    let visible_batches = batches
80        .iter()
81        .filter(|batch| {
82            batch.iter().any(|index| {
83                priority_by_index
84                    .get(index)
85                    .map_or(false, |priority| *priority <= 0)
86            })
87        })
88        .count();
89    let warm_batches = batches.len().saturating_sub(visible_batches);
90
91    (
92        visible_batches,
93        warm_batches,
94        visible_surfaces,
95        warm_surfaces,
96    )
97}
98
99#[cfg(debug_assertions)]
100fn log_visible_surface_escape(layer: usize, escape: VisibleSurfaceEscape) {
101    log::warn!(
102        "[pax-tile-window-escape] backend=wgpu layer={} visible_surfaces={} escaped_visible_surfaces={} max_gap_x={:.1} max_gap_y={:.1} previous_bounds={} visible_bounds={}",
103        layer,
104        escape.visible_surfaces,
105        escape.escaped_visible_surfaces,
106        escape.max_gap_x,
107        escape.max_gap_y,
108        format_rect(escape.previous_bounds),
109        format_rect(escape.visible_bounds),
110    );
111}
112
113#[cfg(debug_assertions)]
114fn format_rect(bounds: Rect) -> String {
115    format!(
116        "{:.1},{:.1}-{:.1},{:.1}",
117        bounds.x0, bounds.y0, bounds.x1, bounds.y1
118    )
119}
120
121#[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
122fn should_print_gpu_profile_logs() -> bool {
123    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
124    *ENABLED.get_or_init(|| {
125        std::env::var("PAX_GPU_PROFILE")
126            .map(|value| {
127                let value = value.trim().to_ascii_lowercase();
128                !(value.is_empty() || value == "0" || value == "false" || value == "off")
129            })
130            .unwrap_or(false)
131    })
132}
133
134type LayerDef = (LayerTarget, Pin<Box<dyn Fn() -> LayerSurfaceLayout>>);
135
136impl LayerRenderer {
137    /// Create a renderer wrapper with its current tile geometry.
138    pub fn new(
139        key: String,
140        host_signature: String,
141        renderer: WgpuRenderer<'static>,
142        origin_x: f32,
143        origin_y: f32,
144        logical_width: f32,
145        logical_height: f32,
146        surface_width: u32,
147        surface_height: u32,
148        dpr: [f32; 2],
149    ) -> Self {
150        Self {
151            key,
152            host_signature,
153            renderer,
154            origin_x,
155            origin_y,
156            logical_width,
157            logical_height,
158            surface_width,
159            surface_height,
160            dpr,
161        }
162    }
163
164    /// Access the underlying retained `pax-gpu` renderer.
165    pub fn renderer_mut(&mut self) -> &mut WgpuRenderer<'static> {
166        &mut self.renderer
167    }
168
169    fn intersects_coverage_bounds(&self, bounds: &Rect) -> bool {
170        surface_intersects_coverage_bounds(
171            bounds,
172            self.origin_x as f64,
173            self.origin_y as f64,
174            self.logical_width as f64,
175            self.logical_height as f64,
176        )
177    }
178
179    fn coverage_bounds(&self) -> Rect {
180        Rect::new(
181            self.origin_x as f64,
182            self.origin_y as f64,
183            self.origin_x as f64 + self.logical_width as f64,
184            self.origin_y as f64 + self.logical_height as f64,
185        )
186    }
187
188    fn update_layout(&mut self, surface: &LayerSurfaceEntry) -> LayoutChangeKind {
189        let origin_changed = (self.origin_x - surface.origin_x).abs() > f32::EPSILON
190            || (self.origin_y - surface.origin_y).abs() > f32::EPSILON;
191        let size_changed = self.logical_width != surface.surface.logical_width
192            || self.logical_height != surface.surface.logical_height
193            || self.surface_width != surface.surface.surface_width
194            || self.surface_height != surface.surface.surface_height
195            || self.dpr != surface.surface.dpr;
196
197        self.origin_x = surface.origin_x;
198        self.origin_y = surface.origin_y;
199        self.logical_width = surface.surface.logical_width;
200        self.logical_height = surface.surface.logical_height;
201        self.surface_width = surface.surface.surface_width;
202        self.surface_height = surface.surface.surface_height;
203        self.dpr = surface.surface.dpr;
204
205        if size_changed {
206            LayoutChangeKind::Resized
207        } else if origin_changed {
208            LayoutChangeKind::OriginOnly
209        } else {
210            LayoutChangeKind::Unchanged
211        }
212    }
213
214    /// Update the retained layout metadata after the owner has already applied matching backend
215    /// surface/view transforms.
216    pub fn sync_layout_metadata(&mut self, surface: &LayerSurfaceEntry) {
217        self.update_layout(surface);
218    }
219}
220
221impl LayerTarget {
222    /// Create a layer target from physical surface renderers.
223    pub fn new(mut renderers: Vec<LayerRenderer>, active: bool) -> Self {
224        // GPU resources remain surface-local, but CPU tessellation is pure geometry work. Share that
225        // cache across the physical surfaces for one logical layer so tile replays can reuse meshes.
226        if let Some((first, rest)) = renderers.split_first_mut() {
227            for renderer in rest {
228                renderer.renderer.share_vector_caches_from(&first.renderer);
229            }
230        }
231        Self {
232            renderers,
233            active,
234            needs_replay: false,
235        }
236    }
237
238    /// Mutable access to each physical renderer backing this logical layer.
239    pub fn renderers_mut(&mut self) -> &mut [LayerRenderer] {
240        &mut self.renderers
241    }
242
243    fn activate(&mut self) {
244        if !self.active {
245            self.active = true;
246            self.needs_replay = true;
247        }
248    }
249
250    fn deactivate(&mut self) {
251        self.active = false;
252    }
253
254    fn prepare_for_render(&mut self) {
255        if !self.needs_replay {
256            return;
257        }
258        for renderer in &mut self.renderers {
259            renderer.renderer.reset_retained_scene();
260        }
261        self.needs_replay = false;
262    }
263}
264
265fn layer_layout_matches_target(target: &LayerTarget, layout: &LayerSurfaceLayout) -> bool {
266    // `host_signature` identifies the DOM surface host that currently owns a layer tile. It is
267    // intentionally separate from node identity: the same expanded node can be rebound to a new
268    // browser surface host when scroller islands are recreated.
269    layout.surfaces.len() == target.renderers.len()
270        && layout
271            .surfaces
272            .iter()
273            .zip(target.renderers.iter())
274            .all(|(surface, renderer)| {
275                surface.key == renderer.key && surface.host_signature == renderer.host_signature
276            })
277}
278
279fn layer_layout_matches_bootstrapped_target(
280    target: &LayerTarget,
281    layout: &LayerSurfaceLayout,
282) -> bool {
283    // Async layer creation can overlap DOM scroller setup. Only publish a freshly-created renderer
284    // if it still matches the current host geometry; once published, refresh_layer_layouts handles
285    // ordinary resize/origin changes in place.
286    target.active == layout.active
287        && layer_layout_matches_target(target, layout)
288        && layout
289            .surfaces
290            .iter()
291            .zip(target.renderers.iter())
292            .all(|(surface, renderer)| {
293                (surface.origin_x - renderer.origin_x).abs() <= f32::EPSILON
294                    && (surface.origin_y - renderer.origin_y).abs() <= f32::EPSILON
295                    && (surface.surface.logical_width - renderer.logical_width).abs()
296                        <= f32::EPSILON
297                    && (surface.surface.logical_height - renderer.logical_height).abs()
298                        <= f32::EPSILON
299                    && surface.surface.surface_width == renderer.surface_width
300                    && surface.surface.surface_height == renderer.surface_height
301                    && (surface.surface.dpr[0] - renderer.dpr[0]).abs() <= f32::EPSILON
302                    && (surface.surface.dpr[1] - renderer.dpr[1]).abs() <= f32::EPSILON
303            })
304}
305
306const MAX_CONCURRENT_LAYER_INITIALIZATIONS: usize = 4;
307
308fn pump_layer_initialization_queue(
309    factory: Rc<dyn Fn(usize) -> Pin<Box<dyn Future<Output = Option<LayerDef>>>>>,
310    backends: Rc<RefCell<Vec<RenderLayerState>>>,
311    queue: Rc<RefCell<VecDeque<usize>>>,
312    scheduled: Rc<RefCell<HashSet<usize>>>,
313    in_flight: Rc<Cell<usize>>,
314    ready_layers: Rc<RefCell<Vec<usize>>>,
315) {
316    while in_flight.get() < MAX_CONCURRENT_LAYER_INITIALIZATIONS {
317        let next_layer = queue.borrow_mut().pop_front();
318        let Some(layer_index) = next_layer else {
319            break;
320        };
321
322        in_flight.set(in_flight.get() + 1);
323        let factory = Rc::clone(&factory);
324        let backends = Rc::clone(&backends);
325        let queue = Rc::clone(&queue);
326        let scheduled = Rc::clone(&scheduled);
327        let in_flight_count = Rc::clone(&in_flight);
328        let ready_layers = Rc::clone(&ready_layers);
329
330        let task = async move {
331            let backend = (factory)(layer_index).await;
332            let mut should_requeue = false;
333            match backend {
334                Some(layer_def) => {
335                    let current_layout = layer_def.1();
336                    let layout_matches =
337                        layer_layout_matches_bootstrapped_target(&layer_def.0, &current_layout);
338                    let mut backend_states = backends.borrow_mut();
339                    match backend_states.get_mut(layer_index) {
340                        Some(change) if layout_matches => {
341                            *change = RenderLayerState::Ready(layer_def);
342                            ready_layers.borrow_mut().push(layer_index);
343                        }
344                        Some(change) => {
345                            // Layer initialization is async relative to host ownership on the
346                            // web. If the DOM host changes while this backend is bootstrapping,
347                            // discard it and retry against the latest layout instead of
348                            // publishing a stale renderer.
349                            *change = RenderLayerState::Pending;
350                            should_requeue = true;
351                        }
352                        None => {
353                            log::warn!(
354                                "failed to set poll state to ready: layer {} doesn't exist anymore",
355                                layer_index
356                            );
357                        }
358                    }
359                }
360                None => match backends.borrow_mut().get_mut(layer_index) {
361                    Some(change) => {
362                        *change = RenderLayerState::Failed;
363                        log::warn!(
364                            "failed to initialize render backend for layer {}",
365                            layer_index
366                        );
367                    }
368                    None => log::warn!(
369                        "failed to set poll state to ready: layer {} doesn't exist and backend failed to initialize",
370                        layer_index
371                    ),
372                },
373            }
374
375            scheduled.borrow_mut().remove(&layer_index);
376            if should_requeue && scheduled.borrow_mut().insert(layer_index) {
377                queue.borrow_mut().push_back(layer_index);
378            }
379
380            in_flight_count.set(in_flight_count.get().saturating_sub(1));
381            pump_layer_initialization_queue(
382                factory,
383                backends,
384                queue,
385                scheduled,
386                in_flight_count,
387                ready_layers,
388            );
389        };
390
391        #[cfg(target_arch = "wasm32")]
392        wasm_bindgen_futures::spawn_local(task);
393        #[cfg(not(target_arch = "wasm32"))]
394        {
395            pollster::block_on(task);
396        }
397    }
398}
399/// Runtime `RenderContext` implementation backed by `pax-gpu`/wgpu.
400pub struct PaxGpuRenderer {
401    backends: Rc<RefCell<Vec<RenderLayerState>>>,
402    layer_factory: Rc<dyn Fn(usize) -> Pin<Box<dyn Future<Output = Option<LayerDef>>>>>,
403    image_map: HashMap<String, Image>,
404    image_versions: HashMap<String, u64>,
405    failed_context_gets: RefCell<Vec<bool>>,
406    ready_layers: Rc<RefCell<Vec<usize>>>,
407    replay_layers: Rc<RefCell<Vec<usize>>>,
408    pending_layer_initializations: Rc<RefCell<VecDeque<usize>>>,
409    scheduled_layer_initializations: Rc<RefCell<HashSet<usize>>>,
410    layer_initializations_in_flight: Rc<Cell<usize>>,
411    active_render_scopes: RefCell<Vec<Vec<Vec<usize>>>>,
412    dirty_render_surfaces: RefCell<Vec<HashSet<usize>>>,
413    last_scene_lighting: RefCell<Vec<Option<PixelSceneLighting>>>,
414    surface_replay: RefCell<SurfaceReplayCoordinator>,
415    clean_skipped_canvas_nodes: RefCell<HashSet<(usize, u32)>>,
416    #[cfg(debug_assertions)]
417    tile_cull_stats: RefCell<Vec<TileCullStats>>,
418}
419
420/// Lifecycle state for a lazily-created render layer.
421pub enum RenderLayerState {
422    Pending,
423    Failed,
424    Ready(LayerDef),
425}
426
427impl PaxGpuRenderer {
428    /// Create a renderer that lazily asks the chassis for layer backends.
429    pub fn new(
430        layer_factory: impl Fn(usize) -> Pin<Box<dyn Future<Output = Option<LayerDef>>>> + 'static,
431    ) -> Self {
432        Self {
433            backends: Default::default(),
434            layer_factory: Rc::new(layer_factory),
435            image_map: Default::default(),
436            image_versions: Default::default(),
437            failed_context_gets: RefCell::new(vec![]),
438            ready_layers: Default::default(),
439            replay_layers: Default::default(),
440            pending_layer_initializations: Rc::new(RefCell::new(VecDeque::new())),
441            scheduled_layer_initializations: Rc::new(RefCell::new(HashSet::new())),
442            layer_initializations_in_flight: Rc::new(Cell::new(0)),
443            active_render_scopes: Default::default(),
444            dirty_render_surfaces: Default::default(),
445            last_scene_lighting: Default::default(),
446            surface_replay: Default::default(),
447            clean_skipped_canvas_nodes: Default::default(),
448            #[cfg(debug_assertions)]
449            tile_cull_stats: Default::default(),
450        }
451    }
452}
453
454impl PaxGpuRenderer {
455    fn queue_layer_initialization(&self, layer_index: usize) {
456        if self
457            .scheduled_layer_initializations
458            .borrow_mut()
459            .insert(layer_index)
460        {
461            self.pending_layer_initializations
462                .borrow_mut()
463                .push_back(layer_index);
464        }
465    }
466
467    fn enqueue_layer_initialization(&self, layer_index: usize) {
468        self.queue_layer_initialization(layer_index);
469        self.schedule_layer_initialization();
470    }
471
472    fn with_layer_context(&self, layer: usize, mut f: impl FnMut(&mut WgpuRenderer)) {
473        let mut backends = self.backends.borrow_mut();
474        match backends.get_mut(layer) {
475            Some(layer_state) => match layer_state {
476                RenderLayerState::Pending => {
477                    let mut failed_context_gets = self.failed_context_gets.borrow_mut();
478                    if failed_context_gets.len() <= layer {
479                        failed_context_gets.resize(layer + 1, false);
480                    }
481                    failed_context_gets[layer] = true;
482                }
483                RenderLayerState::Failed => {}
484                RenderLayerState::Ready((target, _)) => {
485                    if !target.active {
486                        return;
487                    }
488                    target.prepare_for_render();
489                    let scoped_indices = self.active_render_scope(layer).or_else(|| {
490                        // Targeted replay is active outside a node scope during clear(), and then
491                        // nested into per-node scopes for actual draw operations.
492                        self.targeted_replay_scope(layer)
493                    });
494                    if let Some(indices) = scoped_indices {
495                        for index in indices {
496                            if let Some(renderer) = target.renderers.get_mut(index) {
497                                f(&mut renderer.renderer);
498                            }
499                        }
500                    } else {
501                        for renderer in &mut target.renderers {
502                            f(&mut renderer.renderer);
503                        }
504                    }
505                }
506            },
507            None => log::warn!(
508                "tried to retrieve layer {} context for non-existent layer",
509                layer
510            ),
511        }
512    }
513
514    fn active_render_scope(&self, layer: usize) -> Option<Vec<usize>> {
515        self.active_render_scopes
516            .borrow()
517            .get(layer)
518            .and_then(|scopes| scopes.last().cloned())
519    }
520
521    fn push_render_scope(&self, layer: usize, renderer_indices: Vec<usize>) {
522        let mut scopes = self.active_render_scopes.borrow_mut();
523        if scopes.len() <= layer {
524            scopes.resize_with(layer + 1, Vec::new);
525        }
526        scopes[layer].push(renderer_indices);
527    }
528
529    fn pop_render_scope(&self, layer: usize) {
530        if let Some(scopes) = self.active_render_scopes.borrow_mut().get_mut(layer) {
531            scopes.pop();
532        }
533    }
534
535    fn mark_render_surfaces_dirty(
536        &self,
537        layer: usize,
538        renderer_indices: impl IntoIterator<Item = usize>,
539    ) {
540        let mut dirty_surfaces = self.dirty_render_surfaces.borrow_mut();
541        if dirty_surfaces.len() <= layer {
542            dirty_surfaces.resize_with(layer + 1, HashSet::new);
543        }
544        dirty_surfaces[layer].extend(renderer_indices);
545    }
546
547    fn dirty_render_surface_scope(&self, layer: usize) -> Option<Vec<usize>> {
548        let mut indices: Vec<_> = self
549            .dirty_render_surfaces
550            .borrow()
551            .get(layer)?
552            .iter()
553            .copied()
554            .collect();
555        if indices.is_empty() {
556            return None;
557        }
558        indices.sort_unstable();
559        Some(indices)
560    }
561
562    fn clear_dirty_render_surfaces(&self, layer: usize, renderer_indices: &[usize]) {
563        if let Some(dirty_surfaces) = self.dirty_render_surfaces.borrow_mut().get_mut(layer) {
564            for index in renderer_indices {
565                dirty_surfaces.remove(index);
566            }
567        }
568    }
569
570    fn update_scene_lighting_cache(&self, layer: usize, lighting: &PixelSceneLighting) -> bool {
571        let mut last_scene_lighting = self.last_scene_lighting.borrow_mut();
572        if last_scene_lighting.len() <= layer {
573            last_scene_lighting.resize_with(layer + 1, || None);
574        }
575        let changed = last_scene_lighting[layer].as_ref() != Some(lighting);
576        if changed {
577            last_scene_lighting[layer] = Some(lighting.clone());
578        }
579        changed
580    }
581
582    fn targeted_replay_scope(&self, layer: usize) -> Option<Vec<usize>> {
583        self.surface_replay.borrow().targeted_replay_scope(layer)
584    }
585
586    fn set_targeted_replay_batches(
587        &self,
588        layer: usize,
589        batches: Vec<Vec<usize>>,
590        bounds_by_surface: HashMap<usize, Vec<Rect>>,
591    ) {
592        self.surface_replay
593            .borrow_mut()
594            .set_targeted_replay_batches(layer, batches, bounds_by_surface);
595    }
596
597    fn advance_targeted_replay_queue(&self, layer: usize) -> bool {
598        self.surface_replay
599            .borrow_mut()
600            .advance_targeted_replay_queue(layer)
601    }
602
603    fn flush_targeted_or_dirty(&self, layer: usize, target: &mut LayerTarget) -> bool {
604        let targeted_indices = self.targeted_replay_scope(layer);
605        let dirty_indices = self.dirty_render_surface_scope(layer);
606        let used_targeted_replay = targeted_indices.is_some();
607        #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
608        let targeted_surface_count = targeted_indices.as_ref().map(Vec::len).unwrap_or(0);
609        #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
610        let dirty_surface_count = dirty_indices.as_ref().map(Vec::len).unwrap_or(0);
611        let mut indices = targeted_indices.unwrap_or_default();
612        if let Some(dirty_indices) = dirty_indices {
613            indices.extend(dirty_indices);
614        }
615        indices.sort_unstable();
616        indices.dedup();
617
618        if !indices.is_empty() {
619            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
620            let flush_start = std::time::Instant::now();
621            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
622            let mut encode_us = 0u128;
623            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
624            let mut submit_us = 0u128;
625            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
626            let mut cleanup_us = 0u128;
627            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
628            let mut present_us = 0u128;
629            let mut resource_stats = ResourceChurnStats::default();
630            let mut command_buffers = Vec::new();
631            let mut submitter_index = None;
632            let mut cleanup_indices = Vec::new();
633            let mut present_indices = Vec::new();
634            for index in &indices {
635                if let Some(renderer) = target.renderers.get_mut(*index) {
636                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
637                    let encode_start = std::time::Instant::now();
638                    renderer.renderer.flush_deferred();
639                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
640                    {
641                        encode_us += encode_start.elapsed().as_micros();
642                    }
643                    let mut renderer_command_buffers =
644                        renderer.renderer.take_pending_command_buffers();
645                    if !renderer_command_buffers.is_empty() {
646                        if submitter_index.is_none() {
647                            submitter_index = Some(*index);
648                        }
649                        command_buffers.append(&mut renderer_command_buffers);
650                    }
651                    if renderer.renderer.has_pending_submission_cleanup() {
652                        cleanup_indices.push(*index);
653                    }
654                    present_indices.push(*index);
655                    resource_stats.merge(renderer.renderer.take_resource_churn_stats());
656                }
657            }
658            let command_buffer_count = command_buffers.len();
659            if command_buffer_count != 0 {
660                if let Some(index) = submitter_index {
661                    if let Some(renderer) = target.renderers.get(index) {
662                        #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
663                        let submit_start = std::time::Instant::now();
664                        renderer.renderer.submit_command_buffers(command_buffers);
665                        #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
666                        {
667                            submit_us += submit_start.elapsed().as_micros();
668                        }
669                        log::trace!(
670                            "[pax-gpu-submit] layer={} surfaces={} command_buffers={} shared_submit=1",
671                            layer,
672                            indices.len(),
673                            command_buffer_count
674                        );
675                    }
676                }
677            }
678            for index in &cleanup_indices {
679                if let Some(renderer) = target.renderers.get_mut(*index) {
680                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
681                    let cleanup_start = std::time::Instant::now();
682                    renderer.renderer.complete_submitted_work();
683                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
684                    {
685                        cleanup_us += cleanup_start.elapsed().as_micros();
686                    }
687                }
688            }
689            for index in &present_indices {
690                if let Some(renderer) = target.renderers.get_mut(*index) {
691                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
692                    let present_start = std::time::Instant::now();
693                    renderer.renderer.present_deferred_frame();
694                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
695                    {
696                        present_us += present_start.elapsed().as_micros();
697                    }
698                }
699            }
700            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
701            {
702                let total_us = flush_start.elapsed().as_micros();
703                let line = format!(
704                    "[pax-gpu-flush] layer={} surfaces={} targeted={} targeted_surfaces={} dirty_surfaces={} command_buffers={} cleanup_surfaces={} present_surfaces={} total_us={} encode_us={} submit_us={} cleanup_us={} present_us={} flushes={} retained_nodes={}/{} retained_batches={} retained_draws={} vector_draws={} image_draws={} batch_flushes={} geometry_rebuilds={} geometry_cache_hits={} geometry_cache_misses={} geometry_cache_evictions={} geometry_cache_bytes={} tessellated_vertices={} tessellated_indices={} cached_vertices_reused={} cached_indices_reused={} resource_creates={} resource_updates={} resource_recreates={} create_bytes={} update_bytes={} cache_hits={} cache_misses={} cache_evictions={} cache_bytes={} texture_creates={} texture_bytes={}",
705                    layer,
706                    indices.len(),
707                    used_targeted_replay,
708                    targeted_surface_count,
709                    dirty_surface_count,
710                    command_buffer_count,
711                    cleanup_indices.len(),
712                    present_indices.len(),
713                    total_us,
714                    encode_us,
715                    submit_us,
716                    cleanup_us,
717                    present_us,
718                    resource_stats.flushes,
719                    resource_stats.retained_nodes_visible,
720                    resource_stats.retained_nodes_considered,
721                    resource_stats.retained_draw_batches,
722                    resource_stats.retained_draws,
723                    resource_stats.retained_vector_draws,
724                    resource_stats.retained_image_draws,
725                    resource_stats.vector_batch_flushes,
726                    resource_stats.vector_geometry_rebuilds,
727                    resource_stats.vector_geometry_cache_hits,
728                    resource_stats.vector_geometry_cache_misses,
729                    resource_stats.vector_geometry_cache_evictions,
730                    resource_stats.vector_geometry_cache_bytes,
731                    resource_stats.tessellated_vertices,
732                    resource_stats.tessellated_indices,
733                    resource_stats.cached_vertices_reused,
734                    resource_stats.cached_indices_reused,
735                    resource_stats.vector_resource_creates,
736                    resource_stats.vector_resource_updates,
737                    resource_stats.vector_resource_recreates,
738                    resource_stats.vector_resource_create_bytes,
739                    resource_stats.vector_resource_update_bytes,
740                    resource_stats.vector_resource_cache_hits,
741                    resource_stats.vector_resource_cache_misses,
742                    resource_stats.vector_resource_cache_evictions,
743                    resource_stats.vector_resource_cache_bytes,
744                    resource_stats.texture_creates,
745                    resource_stats.texture_upload_bytes,
746                );
747                log::trace!("{}", line);
748                #[cfg(not(target_arch = "wasm32"))]
749                if should_print_gpu_profile_logs()
750                    && (used_targeted_replay
751                        || total_us >= 8_000
752                        || resource_stats.has_resource_churn())
753                {
754                    println!("{}", line);
755                }
756            }
757            if resource_stats.has_resource_churn() {
758                log::trace!(
759                    "[pax-gpu-resources] layer={} surfaces={} targeted={} {:?}",
760                    layer,
761                    indices.len(),
762                    used_targeted_replay,
763                    resource_stats
764                );
765            }
766            self.clear_dirty_render_surfaces(layer, &indices);
767        }
768
769        used_targeted_replay
770    }
771
772    fn clear_targeted_replay_scope(&self, layer: usize) {
773        self.surface_replay
774            .borrow_mut()
775            .clear_targeted_replay_scope(layer);
776    }
777
778    fn targeted_or_all_indices(&self, layer: usize, renderer_count: usize) -> Vec<usize> {
779        self.surface_replay
780            .borrow()
781            .targeted_or_all_indices(layer, renderer_count)
782    }
783
784    fn remember_canvas_node_coverage(&self, layer: usize, node_id: u32, coverage_bounds: Rect) {
785        self.surface_replay
786            .borrow_mut()
787            .remember_canvas_node_coverage(layer, node_id, coverage_bounds);
788    }
789
790    fn forget_canvas_node_coverage(&self, layer: usize, node_id: u32) {
791        self.surface_replay
792            .borrow_mut()
793            .forget_canvas_node_coverage(layer, node_id);
794    }
795
796    fn mark_clean_skipped_node(&self, layer: usize, node_id: u32) {
797        self.clean_skipped_canvas_nodes
798            .borrow_mut()
799            .insert((layer, node_id));
800    }
801
802    fn targeted_replay_surface_bounds(
803        &self,
804        layer: usize,
805        target: &LayerTarget,
806    ) -> Option<Vec<Rect>> {
807        self.surface_replay
808            .borrow()
809            .targeted_replay_surface_bounds(layer, |index| {
810                let renderer = target.renderers.get(index)?;
811                Some(Rect::new(
812                    renderer.origin_x as f64,
813                    renderer.origin_y as f64,
814                    renderer.origin_x as f64 + renderer.logical_width as f64,
815                    renderer.origin_y as f64 + renderer.logical_height as f64,
816                ))
817            })
818    }
819
820    fn spatial_replay_node_ids(&self, layer: usize) -> Option<Vec<u32>> {
821        let backends = self.backends.borrow();
822        let Some(RenderLayerState::Ready((target, _))) = backends.get(layer) else {
823            return None;
824        };
825        let surface_bounds = self.targeted_replay_surface_bounds(layer, target)?;
826        self.surface_replay
827            .borrow()
828            .spatial_replay_node_ids_for_surface_bounds(layer, &surface_bounds)
829    }
830
831    fn take_replay_layer_ids(&mut self) -> Vec<usize> {
832        let mut replay_layers = self.replay_layers.borrow_mut();
833        let mut replay = std::mem::take(&mut *replay_layers);
834        replay.sort_unstable();
835        replay.dedup();
836        replay
837    }
838
839    #[cfg(debug_assertions)]
840    fn update_tile_cull_stats(&self, layer: usize, update: impl FnOnce(&mut TileCullStats)) {
841        let mut stats = self.tile_cull_stats.borrow_mut();
842        if stats.len() <= layer {
843            stats.resize_with(layer + 1, TileCullStats::default);
844        }
845        update(&mut stats[layer]);
846    }
847
848    #[cfg(debug_assertions)]
849    fn emit_tile_cull_stats(&self, layer: usize) {
850        let stats = self
851            .tile_cull_stats
852            .borrow_mut()
853            .get_mut(layer)
854            .map(std::mem::take);
855        if let Some(stats) = stats {
856            if stats.origin_only_retargets > 0
857                || stats.targeted_replay_batches > 0
858                || stats.full_layer_replays > 0
859                || stats.resize_resets > 0
860            {
861                let line = format!(
862                    "[pax-tile-cull] layer={} nodes={} selected_surfaces={} skipped_surfaces={} stale_removal_attempts={} origin_only_retargets={} targeted_replay_batches={} targeted_replay_surfaces={} visible_batches={} warm_batches={} visible_surfaces={} warm_surfaces={} full_layer_replays={} resize_resets={}",
863                    layer,
864                    stats.nodes_considered,
865                    stats.selected_surfaces,
866                    stats.skipped_surfaces,
867                    stats.stale_surface_removal_attempts,
868                    stats.origin_only_retargets,
869                    stats.targeted_replay_batches,
870                    stats.targeted_replay_surfaces,
871                    stats.targeted_replay_visible_batches,
872                    stats.targeted_replay_warm_batches,
873                    stats.targeted_replay_visible_surfaces,
874                    stats.targeted_replay_warm_surfaces,
875                    stats.full_layer_replays,
876                    stats.resize_resets
877                );
878                log::trace!("{}", line);
879                #[cfg(not(target_arch = "wasm32"))]
880                if should_print_gpu_profile_logs() {
881                    println!("{}", line);
882                }
883            }
884        }
885    }
886
887    fn schedule_layer_initialization(&self) {
888        pump_layer_initialization_queue(
889            Rc::clone(&self.layer_factory),
890            Rc::clone(&self.backends),
891            Rc::clone(&self.pending_layer_initializations),
892            Rc::clone(&self.scheduled_layer_initializations),
893            Rc::clone(&self.layer_initializations_in_flight),
894            Rc::clone(&self.ready_layers),
895        );
896    }
897
898    fn refresh_layer_layouts<I>(&mut self, layer_indices: I)
899    where
900        I: IntoIterator<Item = usize>,
901    {
902        let mut needs_reinitialization = false;
903        let mut backends = self.backends.borrow_mut();
904        for layer_index in layer_indices {
905            let Some(backend) = backends.get_mut(layer_index) else {
906                continue;
907            };
908            match backend {
909                RenderLayerState::Pending => {}
910                RenderLayerState::Failed => {}
911                RenderLayerState::Ready((target, layout_provider)) => {
912                    let layout = (layout_provider)();
913                    let layout_matches = layer_layout_matches_target(target, &layout);
914
915                    if layout.active {
916                        target.activate();
917                    } else {
918                        target.deactivate();
919                        self.clear_targeted_replay_scope(layer_index);
920                    }
921
922                    if !layout_matches {
923                        // A different keyed surface set means the DOM host really changed shape
924                        // underneath us. Stable slot keys let ordinary scroll slide tile origins in
925                        // place; reserve full reinitialization for real additions/removals.
926                        self.clear_targeted_replay_scope(layer_index);
927                        *backend = RenderLayerState::Pending;
928                        self.queue_layer_initialization(layer_index);
929                        needs_reinitialization = true;
930                        continue;
931                    }
932
933                    let mut targeted_replay_entries = Vec::new();
934                    let mut targeted_replay_bounds = HashMap::new();
935                    let mut needs_full_layer_replay = false;
936                    #[cfg(debug_assertions)]
937                    {
938                        let previous_surface_bounds: Vec<_> = target
939                            .renderers
940                            .iter()
941                            .map(LayerRenderer::coverage_bounds)
942                            .collect();
943                        if let Some(escape) =
944                            visible_surface_escape(&previous_surface_bounds, &layout.surfaces)
945                        {
946                            log_visible_surface_escape(layer_index, escape);
947                        }
948                    }
949                    for (index, (surface, renderer)) in layout
950                        .surfaces
951                        .iter()
952                        .zip(target.renderers.iter_mut())
953                        .enumerate()
954                    {
955                        let previous_bounds = renderer.coverage_bounds();
956                        let layout_change = renderer.update_layout(surface);
957                        let current_bounds = renderer.coverage_bounds();
958                        match layout_change {
959                            LayoutChangeKind::Unchanged => {}
960                            LayoutChangeKind::OriginOnly => {
961                                targeted_replay_entries.push(ReplayPriorityEntry::new(
962                                    index,
963                                    surface.replay_priority,
964                                    previous_bounds,
965                                    current_bounds,
966                                ));
967                                // Retained nodes carry transforms/resources built against the
968                                // previous surface origin, so an origin retarget must drop the
969                                // old scene before replaying nodes into the new tile.
970                                targeted_replay_bounds
971                                    .insert(index, vec![previous_bounds, current_bounds]);
972                                renderer.renderer.reset_retained_scene();
973                                renderer
974                                    .renderer
975                                    .set_surface_transform(Transform2D::from_array([
976                                        1.0,
977                                        0.0,
978                                        0.0,
979                                        1.0,
980                                        -surface.origin_x,
981                                        -surface.origin_y,
982                                    ]));
983                                self.replay_layers.borrow_mut().push(layer_index);
984                            }
985                            LayoutChangeKind::Resized => {
986                                #[cfg(debug_assertions)]
987                                self.update_tile_cull_stats(layer_index, |stats| {
988                                    stats.resize_resets += 1;
989                                });
990                                renderer.renderer.reset_retained_scene();
991                                renderer
992                                    .renderer
993                                    .set_surface_transform(Transform2D::from_array([
994                                        1.0,
995                                        0.0,
996                                        0.0,
997                                        1.0,
998                                        -surface.origin_x,
999                                        -surface.origin_y,
1000                                    ]));
1001                                renderer.renderer.resize_surface(
1002                                    surface.surface.surface_width as f32,
1003                                    surface.surface.surface_height as f32,
1004                                );
1005                                renderer.renderer.set_viewport(
1006                                    surface.surface.logical_width,
1007                                    surface.surface.logical_height,
1008                                    surface.surface.dpr,
1009                                );
1010                                needs_full_layer_replay = true;
1011                            }
1012                        }
1013                    }
1014                    if needs_full_layer_replay {
1015                        self.clear_targeted_replay_scope(layer_index);
1016                        #[cfg(debug_assertions)]
1017                        self.update_tile_cull_stats(layer_index, |stats| {
1018                            stats.full_layer_replays += 1;
1019                        });
1020                        self.mark_render_surfaces_dirty(layer_index, 0..target.renderers.len());
1021                        self.replay_layers.borrow_mut().push(layer_index);
1022                    } else if !targeted_replay_entries.is_empty() {
1023                        let replay_batches =
1024                            replay_batches_by_directional_priority(&targeted_replay_entries);
1025                        #[cfg(debug_assertions)]
1026                        let replay_priority_stats =
1027                            replay_priority_stats(&targeted_replay_entries, &replay_batches);
1028                        #[cfg(debug_assertions)]
1029                        self.update_tile_cull_stats(layer_index, |stats| {
1030                            stats.origin_only_retargets +=
1031                                replay_batches.iter().map(Vec::len).sum::<usize>() as u64;
1032                            stats.targeted_replay_batches += replay_batches.len() as u64;
1033                            stats.targeted_replay_surfaces +=
1034                                replay_batches.iter().map(Vec::len).sum::<usize>() as u64;
1035                            stats.targeted_replay_visible_batches += replay_priority_stats.0 as u64;
1036                            stats.targeted_replay_warm_batches += replay_priority_stats.1 as u64;
1037                            stats.targeted_replay_visible_surfaces +=
1038                                replay_priority_stats.2 as u64;
1039                            stats.targeted_replay_warm_surfaces += replay_priority_stats.3 as u64;
1040                        });
1041                        self.set_targeted_replay_batches(
1042                            layer_index,
1043                            replay_batches,
1044                            targeted_replay_bounds,
1045                        );
1046                        self.replay_layers.borrow_mut().push(layer_index);
1047                    }
1048                }
1049            }
1050        }
1051        drop(backends);
1052        if needs_reinitialization {
1053            self.schedule_layer_initialization();
1054        }
1055    }
1056}
1057
1058impl RenderContext for PaxGpuRenderer {
1059    fn fill_with_opacity(
1060        &mut self,
1061        layer: usize,
1062        path: kurbo::BezPath,
1063        fill: &pax_runtime_api::Fill,
1064        opacity: f64,
1065    ) {
1066        self.with_layer_context(layer, |context| {
1067            let bounds = path.bounding_box();
1068            let path = convert_kurbo_to_lyon_path(&path);
1069            let fill = to_pax_gpu_fill(fill, bounds, context.current_transform());
1070            context.fill_path_with_opacity(path, fill, opacity as f32);
1071        });
1072    }
1073
1074    fn fill_with_material_and_opacity(
1075        &mut self,
1076        layer: usize,
1077        path: kurbo::BezPath,
1078        fill: &pax_runtime_api::Fill,
1079        material: &Material,
1080        opacity: f64,
1081    ) {
1082        self.with_layer_context(layer, |context| {
1083            let bounds = path.bounding_box();
1084            let path = convert_kurbo_to_lyon_path(&path);
1085            let fill = to_pax_gpu_fill(fill, bounds, context.current_transform());
1086            let material = to_pax_gpu_material(material);
1087            context.fill_path_with_material_and_opacity(path, fill, material, opacity as f32);
1088        });
1089    }
1090
1091    fn fill_with_material_and_opacity_and_smoothing(
1092        &mut self,
1093        layer: usize,
1094        path: kurbo::BezPath,
1095        fill: &pax_runtime_api::Fill,
1096        material: &Material,
1097        opacity: f64,
1098        smoothing: PathSmoothing,
1099    ) {
1100        self.with_layer_context(layer, |context| {
1101            let bounds = path.bounding_box();
1102            let path = convert_kurbo_to_lyon_path(&path);
1103            let fill = to_pax_gpu_fill(fill, bounds, context.current_transform());
1104            let material = to_pax_gpu_material(material);
1105            context.fill_path_with_material_and_opacity_and_smoothing(
1106                path,
1107                fill,
1108                material,
1109                opacity as f32,
1110                smoothing,
1111            );
1112        });
1113    }
1114
1115    fn stroke_with_opacity(
1116        &mut self,
1117        layer: usize,
1118        path: kurbo::BezPath,
1119        stroke: &Stroke,
1120        opacity: f64,
1121    ) {
1122        self.with_layer_context(layer, |context| {
1123            let bounds = path.bounding_box();
1124            context.stroke_path_with_opacity(
1125                convert_kurbo_to_lyon_path(&path),
1126                PixelStroke {
1127                    fill: to_pax_gpu_fill(
1128                        &pax_runtime_api::Fill::Solid(stroke.color.get()),
1129                        bounds,
1130                        context.current_transform(),
1131                    ),
1132                    weight: stroke.width.get().expect_pixels().to_float() as f32,
1133                    cap: match stroke.cap.get() {
1134                        StrokeCap::Butt => PixelStrokeCap::Butt,
1135                        StrokeCap::Round => PixelStrokeCap::Round,
1136                        StrokeCap::Square => PixelStrokeCap::Square,
1137                    },
1138                    join: match stroke.join.get() {
1139                        StrokeJoin::Miter => PixelStrokeJoin::Miter,
1140                        StrokeJoin::Round => PixelStrokeJoin::Round,
1141                        StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1142                    },
1143                },
1144                opacity as f32,
1145            );
1146        });
1147    }
1148
1149    fn stroke_with_material_and_opacity(
1150        &mut self,
1151        layer: usize,
1152        path: kurbo::BezPath,
1153        stroke: &Stroke,
1154        material: &Material,
1155        opacity: f64,
1156    ) {
1157        self.with_layer_context(layer, |context| {
1158            let bounds = path.bounding_box();
1159            context.stroke_path_with_material_and_opacity(
1160                convert_kurbo_to_lyon_path(&path),
1161                PixelStroke {
1162                    fill: to_pax_gpu_fill(
1163                        &pax_runtime_api::Fill::Solid(stroke.color.get()),
1164                        bounds,
1165                        context.current_transform(),
1166                    ),
1167                    weight: stroke.width.get().expect_pixels().to_float() as f32,
1168                    cap: match stroke.cap.get() {
1169                        StrokeCap::Butt => PixelStrokeCap::Butt,
1170                        StrokeCap::Round => PixelStrokeCap::Round,
1171                        StrokeCap::Square => PixelStrokeCap::Square,
1172                    },
1173                    join: match stroke.join.get() {
1174                        StrokeJoin::Miter => PixelStrokeJoin::Miter,
1175                        StrokeJoin::Round => PixelStrokeJoin::Round,
1176                        StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1177                    },
1178                },
1179                to_pax_gpu_material(material),
1180                opacity as f32,
1181            );
1182        });
1183    }
1184
1185    fn stroke_with_material_and_opacity_and_smoothing(
1186        &mut self,
1187        layer: usize,
1188        path: kurbo::BezPath,
1189        stroke: &Stroke,
1190        material: &Material,
1191        opacity: f64,
1192        smoothing: PathSmoothing,
1193    ) {
1194        self.with_layer_context(layer, |context| {
1195            let bounds = path.bounding_box();
1196            context.stroke_path_with_material_and_opacity_and_smoothing(
1197                convert_kurbo_to_lyon_path(&path),
1198                PixelStroke {
1199                    fill: to_pax_gpu_fill(
1200                        &pax_runtime_api::Fill::Solid(stroke.color.get()),
1201                        bounds,
1202                        context.current_transform(),
1203                    ),
1204                    weight: stroke.width.get().expect_pixels().to_float() as f32,
1205                    cap: match stroke.cap.get() {
1206                        StrokeCap::Butt => PixelStrokeCap::Butt,
1207                        StrokeCap::Round => PixelStrokeCap::Round,
1208                        StrokeCap::Square => PixelStrokeCap::Square,
1209                    },
1210                    join: match stroke.join.get() {
1211                        StrokeJoin::Miter => PixelStrokeJoin::Miter,
1212                        StrokeJoin::Round => PixelStrokeJoin::Round,
1213                        StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1214                    },
1215                },
1216                to_pax_gpu_material(material),
1217                opacity as f32,
1218                smoothing,
1219            );
1220        });
1221    }
1222
1223    fn stroke_with_draw_range_and_material_and_opacity(
1224        &mut self,
1225        layer: usize,
1226        path: kurbo::BezPath,
1227        stroke: &Stroke,
1228        material: &Material,
1229        opacity: f64,
1230        draw_start: f64,
1231        draw_end: f64,
1232    ) {
1233        self.with_layer_context(layer, |context| {
1234            let bounds = path.bounding_box();
1235            context.stroke_path_with_draw_range_and_material_and_opacity(
1236                convert_kurbo_to_lyon_path(&path),
1237                PixelStroke {
1238                    fill: to_pax_gpu_fill(
1239                        &pax_runtime_api::Fill::Solid(stroke.color.get()),
1240                        bounds,
1241                        context.current_transform(),
1242                    ),
1243                    weight: stroke.width.get().expect_pixels().to_float() as f32,
1244                    cap: match stroke.cap.get() {
1245                        StrokeCap::Butt => PixelStrokeCap::Butt,
1246                        StrokeCap::Round => PixelStrokeCap::Round,
1247                        StrokeCap::Square => PixelStrokeCap::Square,
1248                    },
1249                    join: match stroke.join.get() {
1250                        StrokeJoin::Miter => PixelStrokeJoin::Miter,
1251                        StrokeJoin::Round => PixelStrokeJoin::Round,
1252                        StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1253                    },
1254                },
1255                to_pax_gpu_material(material),
1256                opacity as f32,
1257                PixelDrawRange::enabled(draw_start as f32, draw_end as f32),
1258            );
1259        });
1260    }
1261
1262    fn stroke_with_draw_range_and_material_and_opacity_and_smoothing(
1263        &mut self,
1264        layer: usize,
1265        path: kurbo::BezPath,
1266        stroke: &Stroke,
1267        material: &Material,
1268        opacity: f64,
1269        draw_start: f64,
1270        draw_end: f64,
1271        smoothing: PathSmoothing,
1272    ) {
1273        self.with_layer_context(layer, |context| {
1274            let bounds = path.bounding_box();
1275            context.stroke_path_with_draw_range_and_material_and_opacity_and_smoothing(
1276                convert_kurbo_to_lyon_path(&path),
1277                PixelStroke {
1278                    fill: to_pax_gpu_fill(
1279                        &pax_runtime_api::Fill::Solid(stroke.color.get()),
1280                        bounds,
1281                        context.current_transform(),
1282                    ),
1283                    weight: stroke.width.get().expect_pixels().to_float() as f32,
1284                    cap: match stroke.cap.get() {
1285                        StrokeCap::Butt => PixelStrokeCap::Butt,
1286                        StrokeCap::Round => PixelStrokeCap::Round,
1287                        StrokeCap::Square => PixelStrokeCap::Square,
1288                    },
1289                    join: match stroke.join.get() {
1290                        StrokeJoin::Miter => PixelStrokeJoin::Miter,
1291                        StrokeJoin::Round => PixelStrokeJoin::Round,
1292                        StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1293                    },
1294                },
1295                to_pax_gpu_material(material),
1296                opacity as f32,
1297                PixelDrawRange::enabled(draw_start as f32, draw_end as f32),
1298                smoothing,
1299            );
1300        });
1301    }
1302
1303    fn save(&mut self, layer: usize) {
1304        self.with_layer_context(layer, |context| {
1305            context.save();
1306        });
1307    }
1308    fn restore(&mut self, layer: usize) {
1309        self.with_layer_context(layer, |context| {
1310            context.restore();
1311        });
1312    }
1313    fn clip(&mut self, layer: usize, path: kurbo::BezPath) {
1314        self.with_layer_context(layer, |context| {
1315            let path = convert_kurbo_to_lyon_path(&path);
1316            context.clip(path);
1317        });
1318    }
1319    fn clip_alpha(
1320        &mut self,
1321        layer: usize,
1322        paints: &[pax_runtime_api::AlphaMaskPaint],
1323        feather: f64,
1324    ) {
1325        self.with_layer_context(layer, |context| {
1326            let paints = paints
1327                .iter()
1328                .map(|paint| {
1329                    let transform =
1330                        Transform2D::from_array(paint.transform.as_coeffs().map(|v| v as f32));
1331                    pax_gpu::AlphaMaskPaint {
1332                        path: convert_kurbo_to_lyon_path(&paint.path),
1333                        fill: to_pax_gpu_alpha_fill(
1334                            &paint.fill,
1335                            paint.path.bounding_box(),
1336                            transform.then(&context.current_transform()),
1337                        ),
1338                        transform,
1339                        opacity: paint.opacity.clamp(0.0, 1.0) as f32,
1340                    }
1341                })
1342                .collect();
1343            context.clip_alpha(paints, feather.max(0.0) as f32);
1344        });
1345    }
1346
1347    fn transform(&mut self, layer: usize, affine: kurbo::Affine) {
1348        self.with_layer_context(layer, |context| {
1349            context.transform(Transform2D::from_array(
1350                affine.as_coeffs().map(|v| v as f32),
1351            ))
1352        });
1353    }
1354
1355    fn load_image(&mut self, identifier: &str, image: &[u8], width: usize, height: usize) {
1356        self.image_map.insert(
1357            identifier.to_string(),
1358            Image {
1359                rgba: image.into(),
1360                pixel_width: width as u32,
1361                pixel_height: height as u32,
1362            },
1363        );
1364        *self
1365            .image_versions
1366            .entry(identifier.to_string())
1367            .or_insert(0) += 1;
1368    }
1369
1370    fn draw_image(&mut self, layer: usize, image_path: &str, rect: kurbo::Rect) {
1371        self.with_layer_context(layer, |context| {
1372            if let Some(image) = self.image_map.get(image_path) {
1373                let version = *self.image_versions.get(image_path).unwrap_or(&0);
1374                context.draw_image(
1375                    image_path,
1376                    version,
1377                    image,
1378                    Box2D {
1379                        min: point(rect.x0 as f32, rect.y0 as f32),
1380                        max: point(rect.x1 as f32, rect.y1 as f32),
1381                    },
1382                );
1383            }
1384        });
1385    }
1386
1387    fn get_image_size(&mut self, image_path: &str) -> Option<(usize, usize)> {
1388        self.image_map
1389            .get(image_path)
1390            .map(|img| (img.pixel_width as usize, img.pixel_height as usize))
1391    }
1392
1393    fn image_loaded(&self, image_path: &str) -> bool {
1394        self.image_map.contains_key(image_path)
1395    }
1396
1397    fn layers(&self) -> usize {
1398        self.backends.borrow().len()
1399    }
1400
1401    fn resize_layers_to(&mut self, layer_count: usize, _dirty_canvases: Rc<RefCell<Vec<bool>>>) {
1402        let current_len = self.backends.borrow().len();
1403        match layer_count.cmp(&current_len) {
1404            std::cmp::Ordering::Less => {
1405                self.backends.borrow_mut().truncate(layer_count);
1406            }
1407            std::cmp::Ordering::Equal => return,
1408            std::cmp::Ordering::Greater => {
1409                for i in current_len..layer_count {
1410                    self.backends.borrow_mut().push(RenderLayerState::Pending);
1411                    self.enqueue_layer_initialization(i);
1412                }
1413            }
1414        }
1415    }
1416
1417    fn clear(&mut self, layer: usize) {
1418        let mut backends = self.backends.borrow_mut();
1419        match backends.get_mut(layer) {
1420            Some(RenderLayerState::Pending) => {
1421                let mut failed_context_gets = self.failed_context_gets.borrow_mut();
1422                if failed_context_gets.len() <= layer {
1423                    failed_context_gets.resize(layer + 1, false);
1424                }
1425                failed_context_gets[layer] = true;
1426            }
1427            Some(RenderLayerState::Failed) => {}
1428            Some(RenderLayerState::Ready((target, _))) => {
1429                if !target.active {
1430                    return;
1431                }
1432                target.prepare_for_render();
1433                let indices = self.targeted_or_all_indices(layer, target.renderers.len());
1434                for index in &indices {
1435                    if let Some(renderer) = target.renderers.get_mut(*index) {
1436                        renderer.renderer.clear();
1437                    }
1438                }
1439                self.mark_render_surfaces_dirty(layer, indices);
1440            }
1441            None => log::warn!(
1442                "tried to clear layer {} context for non-existent layer",
1443                layer
1444            ),
1445        }
1446    }
1447
1448    fn flush(&mut self, layer: usize, dirty_canvases: Rc<RefCell<Vec<bool>>>) {
1449        #[cfg(debug_assertions)]
1450        self.emit_tile_cull_stats(layer);
1451
1452        let mut flushed_targeted_batch = false;
1453        let mut backends = self.backends.borrow_mut();
1454        match backends.get_mut(layer) {
1455            Some(RenderLayerState::Pending) => {}
1456            Some(RenderLayerState::Failed) => {}
1457            Some(RenderLayerState::Ready((target, _))) => {
1458                if !target.active {
1459                    return;
1460                }
1461                if let Some(failed) = self.failed_context_gets.borrow_mut().get_mut(layer) {
1462                    if *failed {
1463                        if let Some(dirty_bit) = dirty_canvases.borrow_mut().get_mut(layer) {
1464                            // If we failed to draw to this layer because the context wasn't
1465                            // available yet, retry once the backend is ready.
1466                            *dirty_bit = true;
1467                        }
1468                        *failed = false;
1469                    }
1470                }
1471                flushed_targeted_batch = self.flush_targeted_or_dirty(layer, target);
1472            }
1473            None => log::warn!(
1474                "tried to flush layer {} context for non-existent layer",
1475                layer
1476            ),
1477        }
1478        drop(backends);
1479
1480        if flushed_targeted_batch && self.advance_targeted_replay_queue(layer) {
1481            self.replay_layers.borrow_mut().push(layer);
1482        }
1483    }
1484
1485    fn resize(&mut self, _width: usize, _height: usize) {
1486        let layer_count = self.backends.borrow().len();
1487        self.refresh_layer_layouts(0..layer_count);
1488    }
1489
1490    fn refresh_layers(&mut self, layers: &[usize]) {
1491        self.refresh_layer_layouts(layers.iter().copied());
1492    }
1493
1494    fn set_scene_lighting(&mut self, layer: usize, lighting: &SceneLighting) {
1495        let lighting = to_pax_gpu_scene_lighting(lighting);
1496        let lighting_changed = self.update_scene_lighting_cache(layer, &lighting);
1497        let mut backends = self.backends.borrow_mut();
1498        match backends.get_mut(layer) {
1499            Some(RenderLayerState::Pending) => {}
1500            Some(RenderLayerState::Failed) => {}
1501            Some(RenderLayerState::Ready((target, _))) => {
1502                if !target.active {
1503                    return;
1504                }
1505                if lighting_changed {
1506                    self.mark_render_surfaces_dirty(layer, 0..target.renderers.len());
1507                }
1508                for renderer in &mut target.renderers {
1509                    renderer
1510                        .renderer
1511                        .set_scene_lighting(translate_scene_lighting_for_surface(
1512                            &lighting,
1513                            renderer.origin_x,
1514                            renderer.origin_y,
1515                        ));
1516                }
1517            }
1518            None => log::warn!(
1519                "tried to set lighting for layer {} context for non-existent layer",
1520                layer
1521            ),
1522        }
1523    }
1524
1525    fn clear_targeted_replay(&mut self, layer: usize) {
1526        self.clear_targeted_replay_scope(layer);
1527    }
1528
1529    fn take_ready_canvas_layers(&mut self) -> Vec<usize> {
1530        let mut ready_layers = self.ready_layers.borrow_mut();
1531        let mut ready = std::mem::take(&mut *ready_layers);
1532        drop(ready_layers);
1533        ready.sort_unstable();
1534        ready.dedup();
1535        if !ready.is_empty() {
1536            // A DOM surface update may have been routed through the engine while this layer was
1537            // still bootstrapping. Reconcile the just-published renderer against the current
1538            // layout before the chassis asks the runtime to replay retained canvas nodes into it.
1539            self.refresh_layer_layouts(ready.iter().copied());
1540        }
1541        ready
1542    }
1543
1544    fn take_replay_canvas_layers(&mut self) -> Vec<usize> {
1545        self.take_replay_layer_ids()
1546    }
1547
1548    fn take_replay_canvas_layer_updates(&mut self) -> Vec<ReplayCanvasLayerUpdate> {
1549        self.take_replay_layer_ids()
1550            .into_iter()
1551            .map(|layer| ReplayCanvasLayerUpdate {
1552                layer,
1553                node_ids: self.spatial_replay_node_ids(layer),
1554            })
1555            .collect()
1556    }
1557
1558    fn request_layer_screenshot(&mut self, layer: usize, request_id: u32) {
1559        self.with_layer_context(layer, |context| {
1560            context.request_screenshot_capture(request_id);
1561        });
1562        self.replay_layers.borrow_mut().push(layer);
1563    }
1564
1565    fn take_layer_screenshot(&mut self, layer: usize, request_id: u32) -> Option<ScreenshotData> {
1566        let mut screenshot = None;
1567        self.with_layer_context(layer, |context| {
1568            screenshot =
1569                context
1570                    .take_screenshot_capture(request_id)
1571                    .map(|capture| ScreenshotData {
1572                        id: request_id,
1573                        data: capture.rgba,
1574                        width: capture.width as usize,
1575                        height: capture.height as usize,
1576                    });
1577        });
1578        screenshot
1579    }
1580
1581    fn take_layer_surface_screenshots(
1582        &mut self,
1583        layer: usize,
1584        request_id: u32,
1585    ) -> Vec<LayerSurfaceScreenshotData> {
1586        let mut screenshots = Vec::new();
1587        let mut backends = self.backends.borrow_mut();
1588        let Some(RenderLayerState::Ready((target, _))) = backends.get_mut(layer) else {
1589            return screenshots;
1590        };
1591        if !target.active {
1592            return screenshots;
1593        }
1594
1595        for renderer in target.renderers_mut() {
1596            let Some(capture) = renderer.renderer_mut().take_screenshot_capture(request_id) else {
1597                continue;
1598            };
1599            screenshots.push(LayerSurfaceScreenshotData {
1600                id: request_id,
1601                key: renderer.key.clone(),
1602                data: capture.rgba,
1603                width: capture.width as usize,
1604                height: capture.height as usize,
1605                origin_x: renderer.origin_x,
1606                origin_y: renderer.origin_y,
1607                logical_width: renderer.logical_width,
1608                logical_height: renderer.logical_height,
1609            });
1610        }
1611        screenshots
1612    }
1613
1614    fn begin_node(&mut self, layer: usize, node_id: u32, z_index: i32, light_mask: u32) -> bool {
1615        let mut backends = self.backends.borrow_mut();
1616        match backends.get_mut(layer) {
1617            Some(RenderLayerState::Pending) => {
1618                let mut failed_context_gets = self.failed_context_gets.borrow_mut();
1619                if failed_context_gets.len() <= layer {
1620                    failed_context_gets.resize(layer + 1, false);
1621                }
1622                failed_context_gets[layer] = true;
1623                false
1624            }
1625            Some(RenderLayerState::Failed) => false,
1626            Some(RenderLayerState::Ready((target, _))) => {
1627                if !target.active {
1628                    return false;
1629                }
1630                self.remember_canvas_node_coverage(
1631                    layer,
1632                    node_id,
1633                    Rect::new(
1634                        f64::NEG_INFINITY,
1635                        f64::NEG_INFINITY,
1636                        f64::INFINITY,
1637                        f64::INFINITY,
1638                    ),
1639                );
1640                target.prepare_for_render();
1641                let candidate_indices = self.targeted_or_all_indices(layer, target.renderers.len());
1642                let mut selected = Vec::new();
1643                for index in candidate_indices {
1644                    let Some(renderer) = target.renderers.get_mut(index) else {
1645                        continue;
1646                    };
1647                    if renderer.renderer.begin_node(node_id, z_index, light_mask) {
1648                        selected.push(index);
1649                    }
1650                }
1651                let began = !selected.is_empty();
1652                if began {
1653                    self.mark_render_surfaces_dirty(layer, selected.iter().copied());
1654                    self.push_render_scope(layer, selected);
1655                }
1656                began
1657            }
1658            None => {
1659                log::warn!(
1660                    "tried to retrieve layer {} context for non-existent layer",
1661                    layer
1662                );
1663                false
1664            }
1665        }
1666    }
1667
1668    fn begin_node_with_bounds(
1669        &mut self,
1670        layer: usize,
1671        node_id: u32,
1672        z_index: i32,
1673        coverage_bounds: Rect,
1674        light_mask: u32,
1675    ) -> bool {
1676        let mut backends = self.backends.borrow_mut();
1677        match backends.get_mut(layer) {
1678            Some(RenderLayerState::Pending) => {
1679                let mut failed_context_gets = self.failed_context_gets.borrow_mut();
1680                if failed_context_gets.len() <= layer {
1681                    failed_context_gets.resize(layer + 1, false);
1682                }
1683                failed_context_gets[layer] = true;
1684                false
1685            }
1686            Some(RenderLayerState::Failed) => false,
1687            Some(RenderLayerState::Ready((target, _))) => {
1688                if !target.active {
1689                    return false;
1690                }
1691                self.remember_canvas_node_coverage(layer, node_id, coverage_bounds);
1692                target.prepare_for_render();
1693                let renderer_count = target.renderers.len();
1694                let candidate_indices = self.targeted_or_all_indices(layer, renderer_count);
1695                let candidate_indices: HashSet<_> = candidate_indices.into_iter().collect();
1696                #[cfg(debug_assertions)]
1697                self.update_tile_cull_stats(layer, |stats| {
1698                    stats.nodes_considered += 1;
1699                });
1700                let mut selected = Vec::new();
1701                let mut removed = Vec::new();
1702                #[cfg(debug_assertions)]
1703                let mut skipped_surfaces = 0u64;
1704                #[cfg(debug_assertions)]
1705                let mut stale_surface_removal_attempts = 0u64;
1706                for (index, renderer) in target.renderers.iter_mut().enumerate() {
1707                    let intersects = renderer.intersects_coverage_bounds(&coverage_bounds);
1708                    if !intersects {
1709                        #[cfg(debug_assertions)]
1710                        {
1711                            skipped_surfaces += 1;
1712                            stale_surface_removal_attempts += 1;
1713                        }
1714                        // If a dirty node moved out of this tile, skipping begin_node is not
1715                        // enough: the renderer may still retain that node from an earlier frame.
1716                        if renderer.renderer.remove_node(node_id) {
1717                            removed.push(index);
1718                        }
1719                        continue;
1720                    }
1721                    if !candidate_indices.contains(&index) {
1722                        #[cfg(debug_assertions)]
1723                        {
1724                            skipped_surfaces += 1;
1725                        }
1726                        continue;
1727                    }
1728                    if renderer.renderer.begin_node(node_id, z_index, light_mask) {
1729                        selected.push(index);
1730                    }
1731                }
1732                let began = !selected.is_empty();
1733                self.mark_render_surfaces_dirty(
1734                    layer,
1735                    selected.iter().chain(removed.iter()).copied(),
1736                );
1737                #[cfg(debug_assertions)]
1738                self.update_tile_cull_stats(layer, |stats| {
1739                    let selected_count = selected.len() as u64;
1740                    stats.selected_surfaces += selected_count;
1741                    stats.skipped_surfaces += skipped_surfaces;
1742                    stats.stale_surface_removal_attempts += stale_surface_removal_attempts;
1743                });
1744                if began {
1745                    self.push_render_scope(layer, selected);
1746                } else {
1747                    self.mark_clean_skipped_node(layer, node_id);
1748                }
1749                began
1750            }
1751            None => {
1752                log::warn!(
1753                    "tried to retrieve layer {} context for non-existent layer",
1754                    layer
1755                );
1756                false
1757            }
1758        }
1759    }
1760
1761    fn take_clean_skipped_node(&mut self, layer: usize, node_id: u32) -> bool {
1762        self.clean_skipped_canvas_nodes
1763            .borrow_mut()
1764            .remove(&(layer, node_id))
1765    }
1766
1767    fn end_node(&mut self, layer: usize, node_id: u32) -> bool {
1768        let mut ended = false;
1769        self.with_layer_context(layer, |context| {
1770            ended = context.end_node(node_id);
1771        });
1772        self.pop_render_scope(layer);
1773        ended
1774    }
1775
1776    fn remove_node(&mut self, layer: usize, node_id: u32) -> bool {
1777        let mut backends = self.backends.borrow_mut();
1778        match backends.get_mut(layer) {
1779            Some(RenderLayerState::Pending) => {
1780                let mut failed_context_gets = self.failed_context_gets.borrow_mut();
1781                if failed_context_gets.len() <= layer {
1782                    failed_context_gets.resize(layer + 1, false);
1783                }
1784                failed_context_gets[layer] = true;
1785                false
1786            }
1787            Some(RenderLayerState::Failed) => false,
1788            Some(RenderLayerState::Ready((target, _))) => {
1789                if !target.active {
1790                    return false;
1791                }
1792                self.forget_canvas_node_coverage(layer, node_id);
1793                target.prepare_for_render();
1794                let candidate_indices = 0..target.renderers.len();
1795                let mut removed_indices = Vec::new();
1796                for index in candidate_indices {
1797                    let Some(renderer) = target.renderers.get_mut(index) else {
1798                        continue;
1799                    };
1800                    if renderer.renderer.remove_node(node_id) {
1801                        removed_indices.push(index);
1802                    }
1803                }
1804                let removed = !removed_indices.is_empty();
1805                if removed {
1806                    self.mark_render_surfaces_dirty(layer, removed_indices);
1807                }
1808                true
1809            }
1810            None => {
1811                log::warn!(
1812                    "tried to remove node from layer {} context for non-existent layer",
1813                    layer
1814                );
1815                false
1816            }
1817        }
1818    }
1819}
1820
1821fn to_pax_gpu_fill(
1822    fill: &pax_runtime_api::Fill,
1823    rect: kurbo::Rect,
1824    transform: pax_gpu::Transform2D,
1825) -> pax_gpu::Fill {
1826    let bounds = (rect.width(), rect.height());
1827    let orig = rect.origin();
1828    match fill {
1829        pax_runtime_api::Fill::Solid(color) => pax_gpu::Fill::Solid(to_pax_gpu_color(color)),
1830        pax_runtime_api::Fill::LinearGradient(gradient) => {
1831            let start_x = gradient.start.0.evaluate(bounds, Axis::X);
1832            let start_y = gradient.start.1.evaluate(bounds, Axis::Y);
1833            let end_x = gradient.end.0.evaluate(bounds, Axis::X);
1834            let end_y = gradient.end.1.evaluate(bounds, Axis::Y);
1835            let local_pos =
1836                pax_gpu::Point2D::new((orig.x + start_x) as f32, (orig.y + start_y) as f32);
1837            let local_end = pax_gpu::Point2D::new((orig.x + end_x) as f32, (orig.y + end_y) as f32);
1838            let world_pos = transform.transform_point(local_pos);
1839            let world_end = transform.transform_point(local_end);
1840            let main_axis = world_end - world_pos;
1841            pax_gpu::Fill::Gradient {
1842                stops: gradient
1843                    .stops
1844                    .iter()
1845                    .map(|g| pax_gpu::GradientStop {
1846                        color: to_pax_gpu_color(&g.color),
1847                        stop: g
1848                            .position
1849                            .evaluate((main_axis.length() as f64, 0.0), Axis::X)
1850                            as f32,
1851                    })
1852                    .collect(),
1853                gradient_type: pax_gpu::GradientType::Linear,
1854                pos: world_pos,
1855                main_axis,
1856                off_axis: pax_gpu::Vector2D::zero(), //not used for linear
1857            }
1858        }
1859        pax_runtime_api::Fill::RadialGradient(gradient) => {
1860            let start_x = gradient.start.0.evaluate(bounds, Axis::X);
1861            let start_y = gradient.start.1.evaluate(bounds, Axis::Y);
1862            let end_x = gradient.end.0.evaluate(bounds, Axis::X);
1863            let end_y = gradient.end.1.evaluate(bounds, Axis::Y);
1864            let r = gradient.radius as f32;
1865            let local_pos =
1866                pax_gpu::Point2D::new((orig.x + start_x) as f32, (orig.y + start_y) as f32);
1867            let local_main_axis =
1868                pax_gpu::Vector2D::new(r * (end_x - start_x) as f32, r * (end_y - start_y) as f32);
1869            let local_off_axis = pax_gpu::Vector2D::new(-local_main_axis.y, local_main_axis.x);
1870            let world_pos = transform.transform_point(local_pos);
1871            let world_main_axis = transform.transform_point(pax_gpu::Point2D::new(
1872                local_pos.x + local_main_axis.x,
1873                local_pos.y + local_main_axis.y,
1874            )) - world_pos;
1875            let world_off_axis = transform.transform_point(pax_gpu::Point2D::new(
1876                local_pos.x + local_off_axis.x,
1877                local_pos.y + local_off_axis.y,
1878            )) - world_pos;
1879            pax_gpu::Fill::Gradient {
1880                gradient_type: pax_gpu::GradientType::Radial,
1881                pos: world_pos,
1882                main_axis: world_main_axis,
1883                off_axis: world_off_axis,
1884                stops: gradient
1885                    .stops
1886                    .iter()
1887                    .map(|g| pax_gpu::GradientStop {
1888                        color: to_pax_gpu_color(&g.color),
1889                        stop: g
1890                            .position
1891                            .evaluate((world_main_axis.length() as f64, 0.0), Axis::X)
1892                            as f32,
1893                    })
1894                    .collect(),
1895            }
1896        }
1897    }
1898}
1899
1900fn to_pax_gpu_alpha_fill(
1901    fill: &pax_runtime_api::Fill,
1902    rect: Rect,
1903    transform: Transform2D,
1904) -> pax_gpu::Fill {
1905    // Preserve a complete gradient basis so alpha stays attached to a sheared
1906    // or nonuniformly scaled mask source. A concentric radial gradient has no
1907    // direction vector; its authored radius still defines a valid circle.
1908    let bounds = (rect.width(), rect.height());
1909    match fill {
1910        pax_runtime_api::Fill::RadialGradient(g) => {
1911            let center = point(
1912                (rect.x0 + g.start.0.evaluate(bounds, Axis::X)) as f32,
1913                (rect.y0 + g.start.1.evaluate(bounds, Axis::Y)) as f32,
1914            );
1915            let direction = pax_gpu::Vector2D::new(
1916                (g.end.0.evaluate(bounds, Axis::X) - g.start.0.evaluate(bounds, Axis::X)) as f32,
1917                (g.end.1.evaluate(bounds, Axis::Y) - g.start.1.evaluate(bounds, Axis::Y)) as f32,
1918            );
1919            let unit = if direction.length() > 0.0001 {
1920                direction / direction.length()
1921            } else {
1922                pax_gpu::Vector2D::new(1.0, 0.0)
1923            };
1924            let axis = unit * (g.radius as f32).max(0.0001);
1925            let off = pax_gpu::Vector2D::new(-axis.y, axis.x);
1926            let pos = transform.transform_point(center);
1927            let main_axis = transform.transform_point(center + axis) - pos;
1928            let off_axis = transform.transform_point(center + off) - pos;
1929            pax_gpu::Fill::Gradient {
1930                gradient_type: pax_gpu::GradientType::Radial,
1931                pos,
1932                main_axis,
1933                off_axis,
1934                stops: g
1935                    .stops
1936                    .iter()
1937                    .map(|s| pax_gpu::GradientStop {
1938                        color: to_pax_gpu_color(&s.color),
1939                        stop: s
1940                            .position
1941                            .evaluate((main_axis.length() as f64, 0.0), Axis::X)
1942                            as f32,
1943                    })
1944                    .collect(),
1945            }
1946        }
1947        pax_runtime_api::Fill::LinearGradient(g) => {
1948            let mut resolved = to_pax_gpu_fill(fill, rect, transform);
1949            if let pax_gpu::Fill::Gradient { off_axis, .. } = &mut resolved {
1950                let dx = (g.end.0.evaluate(bounds, Axis::X) - g.start.0.evaluate(bounds, Axis::X))
1951                    as f32;
1952                let dy = (g.end.1.evaluate(bounds, Axis::Y) - g.start.1.evaluate(bounds, Axis::Y))
1953                    as f32;
1954                *off_axis = transform.transform_vector(pax_gpu::Vector2D::new(-dy, dx));
1955            }
1956            resolved
1957        }
1958        _ => to_pax_gpu_fill(fill, rect, transform),
1959    }
1960}
1961
1962fn to_pax_gpu_material(material: &pax_runtime_api::Material) -> PixelMaterial {
1963    match material {
1964        pax_runtime_api::Material::Unlit => PixelMaterial::unlit(),
1965        pax_runtime_api::Material::Lit(params) => {
1966            let metallic = params.metallic.get().clamp(0.0, 1.0) as f32;
1967            let diffuse = params.diffuse.get().max(0.0) as f32 * (1.0 - metallic * 0.55);
1968            let specular = params.specular.get().max(0.0) as f32 + metallic * 0.45;
1969            let roughness = (params.roughness.get().clamp(0.0, 1.0) as f32
1970                * (1.0 - metallic * 0.35))
1971                .clamp(0.0, 1.0);
1972            PixelMaterial::custom(
1973                params.ambient.get().max(0.0) as f32,
1974                diffuse,
1975                specular,
1976                roughness,
1977                to_pax_gpu_color(&params.emissive.get()),
1978                params.emissive_intensity.get().max(0.0) as f32,
1979            )
1980        }
1981    }
1982}
1983
1984fn to_pax_gpu_scene_lighting(lighting: &SceneLighting) -> PixelSceneLighting {
1985    PixelSceneLighting {
1986        active: lighting.active,
1987        ambient_is_authored: lighting.ambient_is_authored,
1988        ambient_color: to_pax_gpu_color(&lighting.ambient.color),
1989        ambient_intensity: lighting.ambient.intensity.max(0.0) as f32,
1990        lights: lighting
1991            .lights
1992            .iter()
1993            .map(|light| PixelSceneLight {
1994                shape: match light.shape {
1995                    pax_runtime_api::LightShape::Point => PixelLightShape::Point,
1996                    pax_runtime_api::LightShape::Directional => PixelLightShape::Directional,
1997                },
1998                position: [
1999                    light.position.x as f32,
2000                    light.position.y as f32,
2001                    light.position.z as f32,
2002                ],
2003                direction: [
2004                    light.direction.x as f32,
2005                    light.direction.y as f32,
2006                    light.direction.z as f32,
2007                ],
2008                color: to_pax_gpu_color(&light.color),
2009                intensity: light.intensity.max(0.0) as f32,
2010                radius: light.radius.max(0.0) as f32,
2011            })
2012            .collect(),
2013    }
2014}
2015
2016fn translate_scene_lighting_for_surface(
2017    lighting: &PixelSceneLighting,
2018    origin_x: f32,
2019    origin_y: f32,
2020) -> PixelSceneLighting {
2021    let mut lighting = lighting.clone();
2022    for light in &mut lighting.lights {
2023        if matches!(light.shape, PixelLightShape::Point) {
2024            light.position[0] -= origin_x;
2025            light.position[1] -= origin_y;
2026        }
2027    }
2028    lighting
2029}
2030
2031/// Convert a runtime API color into the `pax-gpu` render-context color.
2032pub fn to_pax_gpu_color(color: &pax_runtime_api::Color) -> pax_gpu::Color {
2033    let [r, g, b, a] = color.to_rgba_0_1();
2034    pax_gpu::Color::rgba(r as f32, g as f32, b as f32, a as f32)
2035}
2036
2037/// Convert a kurbo path emitted by primitives into a lyon path consumed by `pax-gpu`.
2038pub fn convert_kurbo_to_lyon_path(kurbo_path: &BezPath) -> Path {
2039    let mut builder = Path::builder();
2040    let mut has_open_subpath = false;
2041    for el in kurbo_path.elements() {
2042        match el {
2043            PathEl::MoveTo(p) => {
2044                if has_open_subpath {
2045                    builder.end(false);
2046                }
2047                builder.begin(point(p.x as f32, p.y as f32));
2048                has_open_subpath = true;
2049            }
2050            PathEl::LineTo(p) => {
2051                if has_open_subpath {
2052                    builder.line_to(point(p.x as f32, p.y as f32));
2053                }
2054            }
2055            PathEl::QuadTo(p1, p2) => {
2056                if has_open_subpath {
2057                    builder.quadratic_bezier_to(
2058                        point(p1.x as f32, p1.y as f32),
2059                        point(p2.x as f32, p2.y as f32),
2060                    );
2061                }
2062            }
2063            PathEl::CurveTo(p1, p2, p3) => {
2064                if has_open_subpath {
2065                    builder.cubic_bezier_to(
2066                        point(p1.x as f32, p1.y as f32),
2067                        point(p2.x as f32, p2.y as f32),
2068                        point(p3.x as f32, p3.y as f32),
2069                    );
2070                }
2071            }
2072            PathEl::ClosePath => {
2073                if has_open_subpath {
2074                    builder.end(true);
2075                    has_open_subpath = false;
2076                }
2077            }
2078        }
2079    }
2080    if has_open_subpath {
2081        builder.end(false);
2082    }
2083
2084    builder.build()
2085}
2086
2087#[cfg(test)]
2088mod tests {
2089    use super::*;
2090
2091    #[test]
2092    fn alpha_radial_gradient_retains_radius_with_coincident_endpoints() {
2093        use pax_runtime_api::{Fill, RadialGradient, Size};
2094        let fill = Fill::RadialGradient(RadialGradient {
2095            start: (Size::Percent(50.into()), Size::Percent(50.into())),
2096            end: (Size::Percent(50.into()), Size::Percent(50.into())),
2097            radius: 25.0,
2098            stops: vec![],
2099        });
2100        let pax_gpu::Fill::Gradient {
2101            pos,
2102            main_axis,
2103            off_axis,
2104            ..
2105        } = to_pax_gpu_alpha_fill(
2106            &fill,
2107            Rect::new(0.0, 0.0, 100.0, 100.0),
2108            Transform2D::new(2.0, 0.0, 1.0, 3.0, 0.0, 0.0),
2109        )
2110        else {
2111            panic!("expected a radial gradient")
2112        };
2113        assert_eq!(pos, point(150.0, 150.0));
2114        assert_eq!(main_axis, pax_gpu::Vector2D::new(50.0, 0.0));
2115        assert_eq!(off_axis, pax_gpu::Vector2D::new(25.0, 75.0));
2116    }
2117
2118    #[test]
2119    fn alpha_linear_gradient_retains_sheared_basis() {
2120        use pax_runtime_api::{Fill, LinearGradient, Size};
2121        let fill = Fill::LinearGradient(LinearGradient {
2122            start: (Size::Percent(0.into()), Size::Percent(0.into())),
2123            end: (Size::Percent(100.into()), Size::Percent(0.into())),
2124            stops: vec![],
2125        });
2126        let pax_gpu::Fill::Gradient {
2127            main_axis,
2128            off_axis,
2129            ..
2130        } = to_pax_gpu_alpha_fill(
2131            &fill,
2132            Rect::new(0.0, 0.0, 100.0, 100.0),
2133            Transform2D::new(1.0, 0.0, 0.5, 1.0, 0.0, 0.0),
2134        )
2135        else {
2136            panic!("expected a linear gradient")
2137        };
2138        assert_eq!(main_axis, pax_gpu::Vector2D::new(100.0, 0.0));
2139        assert_eq!(off_axis, pax_gpu::Vector2D::new(50.0, 100.0));
2140    }
2141
2142    #[test]
2143    fn surface_intersection_includes_edges() {
2144        let bounds = Rect::new(100.0, 100.0, 200.0, 200.0);
2145
2146        assert!(surface_intersects_coverage_bounds(
2147            &bounds, 200.0, 100.0, 100.0, 100.0
2148        ));
2149        assert!(surface_intersects_coverage_bounds(
2150            &bounds, 0.0, 0.0, 100.0, 100.0
2151        ));
2152    }
2153
2154    #[test]
2155    fn surface_intersection_rejects_disjoint_surfaces() {
2156        let bounds = Rect::new(100.0, 100.0, 200.0, 200.0);
2157
2158        assert!(!surface_intersects_coverage_bounds(
2159            &bounds, 201.0, 100.0, 100.0, 100.0
2160        ));
2161        assert!(!surface_intersects_coverage_bounds(
2162            &bounds, 100.0, 201.0, 100.0, 100.0
2163        ));
2164    }
2165
2166    #[test]
2167    fn surface_intersection_keeps_non_finite_bounds_conservative() {
2168        let bounds = Rect::new(f64::NAN, 0.0, 100.0, 100.0);
2169
2170        assert!(surface_intersects_coverage_bounds(
2171            &bounds, 500.0, 500.0, 100.0, 100.0
2172        ));
2173    }
2174
2175    #[test]
2176    fn converts_multiple_kurbo_subpaths_to_lyon_path() {
2177        let mut path = BezPath::new();
2178        path.move_to((0.0, 0.0));
2179        path.line_to((10.0, 0.0));
2180        path.move_to((20.0, 0.0));
2181        path.line_to((30.0, 0.0));
2182        path.close_path();
2183
2184        let lyon_path = convert_kurbo_to_lyon_path(&path);
2185
2186        assert_eq!(lyon_path.iter().count(), 6);
2187    }
2188}