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 crate::render_instrumentation::{count, Counter};
8use kurbo::{BezPath, PathEl, Rect, Shape};
9use pax_gpu::render_backend::SharedGpuContext;
10use pax_gpu::{
11    point, Box2D, DrawRange as PixelDrawRange, Image, LightShape as PixelLightShape,
12    Material as PixelMaterial, Path, ResourceChurnStats, SceneLight as PixelSceneLight,
13    SceneLighting as PixelSceneLighting, Stroke as PixelStroke, StrokeCap as PixelStrokeCap,
14    StrokeJoin as PixelStrokeJoin, Transform2D, WgpuRenderer,
15};
16use pax_runtime_api::{
17    Axis, LayerSurfaceScreenshotData, PathSmoothing, RenderContext, ReplayCanvasLayerUpdate,
18    ResolvedMaterial, ResolvedStroke, SceneLighting, ScreenshotData, StrokeCap, StrokeJoin,
19};
20use std::{
21    cell::{Cell, RefCell},
22    collections::{HashMap, HashSet, VecDeque},
23    future::Future,
24    pin::Pin,
25    rc::Rc,
26};
27
28#[cfg(not(target_arch = "wasm32"))]
29use pollster;
30
31/// Retained renderer bound to one physical surface tile for a logical layer.
32pub struct LayerRenderer {
33    key: String,
34    host_signature: String,
35    renderer: WgpuRenderer<'static>,
36    origin_x: f32,
37    origin_y: f32,
38    logical_width: f32,
39    logical_height: f32,
40    surface_width: u32,
41    surface_height: u32,
42    dpr: [f32; 2],
43    needs_initial_replay: bool,
44}
45
46/// Current renderer set for one logical layer.
47pub struct LayerTarget {
48    renderers: Vec<LayerRenderer>,
49    active: bool,
50    needs_replay: bool,
51    failed_layout: Option<LayerSurfaceLayout>,
52}
53
54#[cfg(debug_assertions)]
55#[derive(Clone, Copy, Default)]
56struct TileCullStats {
57    nodes_considered: u64,
58    selected_surfaces: u64,
59    skipped_surfaces: u64,
60    stale_surface_removal_attempts: u64,
61    origin_only_retargets: u64,
62    targeted_replay_batches: u64,
63    targeted_replay_surfaces: u64,
64    targeted_replay_visible_batches: u64,
65    targeted_replay_warm_batches: u64,
66    targeted_replay_visible_surfaces: u64,
67    targeted_replay_warm_surfaces: u64,
68    full_layer_replays: u64,
69    resize_resets: u64,
70}
71
72#[cfg(debug_assertions)]
73fn replay_priority_stats(
74    entries: &[ReplayPriorityEntry],
75    batches: &[Vec<usize>],
76) -> (usize, usize, usize, usize) {
77    let visible_surfaces = entries.iter().filter(|entry| entry.priority <= 0).count();
78    let warm_surfaces = entries.len().saturating_sub(visible_surfaces);
79    let priority_by_index: HashMap<usize, i32> = entries
80        .iter()
81        .map(|entry| (entry.index, entry.priority))
82        .collect();
83    let visible_batches = batches
84        .iter()
85        .filter(|batch| {
86            batch.iter().any(|index| {
87                priority_by_index
88                    .get(index)
89                    .map_or(false, |priority| *priority <= 0)
90            })
91        })
92        .count();
93    let warm_batches = batches.len().saturating_sub(visible_batches);
94
95    (
96        visible_batches,
97        warm_batches,
98        visible_surfaces,
99        warm_surfaces,
100    )
101}
102
103#[cfg(debug_assertions)]
104fn log_visible_surface_escape(layer: usize, escape: VisibleSurfaceEscape) {
105    log::warn!(
106        "[pax-tile-window-escape] backend=wgpu layer={} visible_surfaces={} escaped_visible_surfaces={} max_gap_x={:.1} max_gap_y={:.1} previous_bounds={} visible_bounds={}",
107        layer,
108        escape.visible_surfaces,
109        escape.escaped_visible_surfaces,
110        escape.max_gap_x,
111        escape.max_gap_y,
112        format_rect(escape.previous_bounds),
113        format_rect(escape.visible_bounds),
114    );
115}
116
117#[cfg(debug_assertions)]
118fn format_rect(bounds: Rect) -> String {
119    format!(
120        "{:.1},{:.1}-{:.1},{:.1}",
121        bounds.x0, bounds.y0, bounds.x1, bounds.y1
122    )
123}
124
125#[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
126fn should_print_gpu_profile_logs() -> bool {
127    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
128    *ENABLED.get_or_init(|| {
129        std::env::var("PAX_GPU_PROFILE")
130            .map(|value| {
131                let value = value.trim().to_ascii_lowercase();
132                !(value.is_empty() || value == "0" || value == "false" || value == "off")
133            })
134            .unwrap_or(false)
135    })
136}
137
138type LayerDef = (LayerTarget, Pin<Box<dyn Fn() -> LayerSurfaceLayout>>);
139type LayerFactory =
140    dyn Fn(usize, LayerCreationRequest) -> Pin<Box<dyn Future<Output = Option<LayerDef>>>>;
141
142/// Snapshot of surviving tiles supplied to a chassis factory. Return only missing surfaces;
143/// the runtime keeps survivors ready and validates returned tiles against the current layout.
144pub struct LayerCreationRequest {
145    retained: Vec<(String, String)>,
146    /// Context shared by this logical layer's surviving tiles, if any.
147    pub shared_context: Option<SharedGpuContext>,
148    /// Adapter limit already established by the surviving tiles.
149    pub max_surface_dimension: u32,
150}
151
152impl Default for LayerCreationRequest {
153    fn default() -> Self {
154        Self {
155            retained: Vec::new(),
156            shared_context: None,
157            max_surface_dimension: u32::MAX,
158        }
159    }
160}
161
162impl LayerCreationRequest {
163    /// Whether the factory must create this key and backing-surface generation.
164    pub fn needs_surface(&self, key: &str, host_signature: &str) -> bool {
165        !self
166            .retained
167            .iter()
168            .any(|(k, h)| k == key && h == host_signature)
169    }
170
171    fn for_target(target: &LayerTarget) -> Self {
172        Self {
173            retained: target
174                .renderers
175                .iter()
176                .map(|r| (r.key.clone(), r.host_signature.clone()))
177                .collect(),
178            shared_context: target
179                .renderers
180                .first()
181                .map(|r| r.renderer.shared_gpu_context()),
182            max_surface_dimension: target
183                .renderers
184                .first()
185                .map_or(u32::MAX, |r| r.renderer.max_surface_dimension()),
186        }
187    }
188}
189
190impl LayerRenderer {
191    /// Create a renderer wrapper with its current tile geometry.
192    pub fn new(
193        key: String,
194        host_signature: String,
195        renderer: WgpuRenderer<'static>,
196        origin_x: f32,
197        origin_y: f32,
198        logical_width: f32,
199        logical_height: f32,
200        surface_width: u32,
201        surface_height: u32,
202        dpr: [f32; 2],
203    ) -> Self {
204        count(Counter::TileCreated, 1);
205        Self {
206            key,
207            host_signature,
208            renderer,
209            origin_x,
210            origin_y,
211            logical_width,
212            logical_height,
213            surface_width,
214            surface_height,
215            dpr,
216            needs_initial_replay: true,
217        }
218    }
219
220    /// Access the underlying retained `pax-gpu` renderer.
221    pub fn renderer_mut(&mut self) -> &mut WgpuRenderer<'static> {
222        &mut self.renderer
223    }
224
225    /// Whether this renderer belongs to the entry's current physical surface generation.
226    pub fn matches_surface(&self, surface: &LayerSurfaceEntry) -> bool {
227        self.key == surface.key && self.host_signature == surface.host_signature
228    }
229
230    fn intersects_coverage_bounds(&self, bounds: &Rect) -> bool {
231        surface_intersects_coverage_bounds(
232            bounds,
233            self.origin_x as f64,
234            self.origin_y as f64,
235            self.logical_width as f64,
236            self.logical_height as f64,
237        )
238    }
239
240    fn coverage_bounds(&self) -> Rect {
241        Rect::new(
242            self.origin_x as f64,
243            self.origin_y as f64,
244            self.origin_x as f64 + self.logical_width as f64,
245            self.origin_y as f64 + self.logical_height as f64,
246        )
247    }
248
249    fn update_layout(&mut self, surface: &LayerSurfaceEntry) -> LayoutChangeKind {
250        let origin_changed = (self.origin_x - surface.origin_x).abs() > f32::EPSILON
251            || (self.origin_y - surface.origin_y).abs() > f32::EPSILON;
252        let size_changed = self.logical_width != surface.surface.logical_width
253            || self.logical_height != surface.surface.logical_height
254            || self.surface_width != surface.surface.surface_width
255            || self.surface_height != surface.surface.surface_height
256            || self.dpr != surface.surface.dpr;
257
258        self.origin_x = surface.origin_x;
259        self.origin_y = surface.origin_y;
260        self.logical_width = surface.surface.logical_width;
261        self.logical_height = surface.surface.logical_height;
262        self.surface_width = surface.surface.surface_width;
263        self.surface_height = surface.surface.surface_height;
264        self.dpr = surface.surface.dpr;
265
266        if size_changed {
267            LayoutChangeKind::Resized
268        } else if origin_changed {
269            LayoutChangeKind::OriginOnly
270        } else {
271            LayoutChangeKind::Unchanged
272        }
273    }
274
275    /// Update the retained layout metadata after the owner has already applied matching backend
276    /// surface/view transforms.
277    pub fn sync_layout_metadata(&mut self, surface: &LayerSurfaceEntry) {
278        self.update_layout(surface);
279    }
280}
281
282impl LayerTarget {
283    /// Create a layer target from physical surface renderers.
284    pub fn new(mut renderers: Vec<LayerRenderer>, active: bool) -> Self {
285        // Draw bindings remain surface-local. Share CPU geometry/resource caches as well as the
286        // image textures already indexed by the logical layer's shared GPU context.
287        if let Some((first, rest)) = renderers.split_first_mut() {
288            for renderer in rest {
289                renderer.renderer.share_vector_caches_from(&first.renderer);
290            }
291        }
292        Self {
293            renderers,
294            active,
295            needs_replay: false,
296            failed_layout: None,
297        }
298    }
299
300    /// Mutable access to each physical renderer backing this logical layer.
301    pub fn renderers_mut(&mut self) -> &mut [LayerRenderer] {
302        &mut self.renderers
303    }
304
305    fn activate(&mut self) {
306        if !self.active {
307            self.active = true;
308            self.needs_replay = true;
309        }
310    }
311
312    fn deactivate(&mut self) {
313        self.active = false;
314    }
315
316    fn prepare_for_render(&mut self) {
317        if !self.needs_replay {
318            return;
319        }
320        for renderer in &mut self.renderers {
321            count(Counter::SceneReset, 1);
322            renderer.renderer.reset_retained_scene();
323        }
324        self.needs_replay = false;
325    }
326}
327
328fn layer_layout_matches_target(target: &LayerTarget, layout: &LayerSurfaceLayout) -> bool {
329    // `host_signature` identifies the DOM surface host that currently owns a layer tile. It is
330    // intentionally separate from node identity: the same expanded node can be rebound to a new
331    // browser surface host when scroller islands are recreated.
332    layout.surfaces.len() == target.renderers.len()
333        && layout
334            .surfaces
335            .iter()
336            .zip(target.renderers.iter())
337            .all(|(surface, renderer)| {
338                surface.key == renderer.key && surface.host_signature == renderer.host_signature
339            })
340}
341
342fn layer_layout_matches_bootstrapped_target(
343    target: &LayerTarget,
344    layout: &LayerSurfaceLayout,
345) -> bool {
346    // Async layer creation can overlap DOM scroller setup. Only publish a freshly-created renderer
347    // if it still matches the current host geometry; once published, refresh_layer_layouts handles
348    // ordinary resize/origin changes in place.
349    target.active == layout.active
350        && layer_layout_matches_target(target, layout)
351        && layout
352            .surfaces
353            .iter()
354            .zip(target.renderers.iter())
355            .all(|(surface, renderer)| {
356                (surface.origin_x - renderer.origin_x).abs() <= f32::EPSILON
357                    && (surface.origin_y - renderer.origin_y).abs() <= f32::EPSILON
358                    && (surface.surface.logical_width - renderer.logical_width).abs()
359                        <= f32::EPSILON
360                    && (surface.surface.logical_height - renderer.logical_height).abs()
361                        <= f32::EPSILON
362                    && surface.surface.surface_width == renderer.surface_width
363                    && surface.surface.surface_height == renderer.surface_height
364                    && (surface.surface.dpr[0] - renderer.dpr[0]).abs() <= f32::EPSILON
365                    && (surface.surface.dpr[1] - renderer.dpr[1]).abs() <= f32::EPSILON
366            })
367}
368
369// Match survivors in desired order; unmatched entries are created by the chassis factory.
370// Reject duplicate identities instead of moving the same owned renderer twice.
371fn retained_surface_order(
372    existing: &[(&str, &str)],
373    desired: &[(&str, &str)],
374) -> Option<Vec<usize>> {
375    let mut seen = HashSet::new();
376    let mut order = Vec::new();
377    for identity in desired {
378        if !seen.insert(*identity) {
379            return None;
380        }
381        if let Some(index) = existing.iter().position(|candidate| candidate == identity) {
382            order.push(index);
383        }
384    }
385    Some(order)
386}
387
388fn retain_surfaces<T>(surfaces: &mut Vec<T>, order: Vec<usize>) {
389    let mut previous: Vec<_> = std::mem::take(surfaces).into_iter().map(Some).collect();
390    *surfaces = order
391        .into_iter()
392        .map(|index| previous[index].take().unwrap())
393        .collect();
394}
395
396const MAX_CONCURRENT_LAYER_INITIALIZATIONS: usize = 4;
397
398// A token belongs to one lifetime of a layer index. Removed layers invalidate it,
399// so an async completion cannot overwrite a replacement at the same index.
400struct LayerInitialization {
401    index: usize,
402    cancelled: Cell<bool>,
403    scheduled: Cell<bool>,
404}
405
406impl LayerInitialization {
407    fn new(index: usize) -> Self {
408        Self {
409            index,
410            cancelled: Cell::new(false),
411            scheduled: Cell::new(false),
412        }
413    }
414
415    fn complete(
416        &self,
417        backend: Option<LayerDef>,
418        requested_layout: Option<LayerSurfaceLayout>,
419        backends: &RefCell<Vec<RenderLayerState>>,
420        ready_layers: &RefCell<Vec<usize>>,
421        changed_layers: &RefCell<Vec<usize>>,
422    ) -> bool {
423        if self.cancelled.get() {
424            return false;
425        }
426        let mut states = backends.borrow_mut();
427        let Some(state) = states.get_mut(self.index) else {
428            return false;
429        };
430        match backend {
431            Some((mut additions, provider)) => {
432                let layout = provider();
433                if let RenderLayerState::Ready((target, old_provider)) = state {
434                    // A surface can be rebound during await. Keep only additions
435                    // whose physical host still belongs to this layer; geometry
436                    // changes are applied by refresh before targeted replay.
437                    additions
438                        .renderers
439                        .retain(|r| layout.surfaces.iter().any(|s| r.matches_surface(s)));
440                    for mut addition in additions.renderers {
441                        if target.renderers.iter().any(|r| {
442                            r.key == addition.key && r.host_signature == addition.host_signature
443                        }) {
444                            continue;
445                        }
446                        if let Some(first) = target.renderers.first() {
447                            addition.renderer.share_vector_caches_from(&first.renderer);
448                        }
449                        target.renderers.push(addition);
450                    }
451                    target.failed_layout = None;
452                    *old_provider = provider;
453                    changed_layers.borrow_mut().push(self.index);
454                    false
455                } else if layer_layout_matches_bootstrapped_target(&additions, &layout) {
456                    *state = RenderLayerState::Ready((additions, provider));
457                    ready_layers.borrow_mut().push(self.index);
458                    false
459                } else {
460                    // Fresh layers have no survivor to keep ready. Retry a stale
461                    // bootstrap against current hosts before publishing it.
462                    *state = RenderLayerState::Pending;
463                    true
464                }
465            }
466            None => {
467                // Failed additions must not discard a surviving scene. Retry
468                // when its layout changes instead of spinning on allocation.
469                if let RenderLayerState::Ready((target, _)) = state {
470                    target.failed_layout = requested_layout;
471                } else {
472                    *state = RenderLayerState::Failed;
473                }
474                log::warn!(
475                    "failed to initialize render surfaces for layer {}",
476                    self.index
477                );
478                false
479            }
480        }
481    }
482}
483
484fn pop_next_layer_initialization(
485    queue: &RefCell<VecDeque<Rc<LayerInitialization>>>,
486    priority: &dyn Fn(usize) -> i32,
487) -> Option<Rc<LayerInitialization>> {
488    let mut queue = queue.borrow_mut();
489    queue.retain(|entry| !entry.cancelled.get());
490    // Re-evaluate when a slot opens: native scrolling can make queued warm work
491    // visible while another layer is awaiting GPU initialization. Ties stay FIFO.
492    let index = queue
493        .iter()
494        .enumerate()
495        .min_by_key(|(index, entry)| (priority(entry.index), *index))
496        .map(|(index, _)| index)?;
497    queue.remove(index)
498}
499
500fn pump_layer_initialization_queue(
501    factory: Rc<LayerFactory>,
502    backends: Rc<RefCell<Vec<RenderLayerState>>>,
503    queue: Rc<RefCell<VecDeque<Rc<LayerInitialization>>>>,
504    in_flight: Rc<Cell<usize>>,
505    ready_layers: Rc<RefCell<Vec<usize>>>,
506    changed_layers: Rc<RefCell<Vec<usize>>>,
507    priority: Rc<dyn Fn(usize) -> i32>,
508) {
509    while in_flight.get() < MAX_CONCURRENT_LAYER_INITIALIZATIONS {
510        let next_layer = pop_next_layer_initialization(&queue, priority.as_ref());
511        let Some(initialization) = next_layer else {
512            break;
513        };
514        if initialization.cancelled.get() {
515            continue;
516        }
517
518        let (request, requested_layout) = match backends.borrow().get(initialization.index) {
519            Some(RenderLayerState::Ready((target, provider))) => {
520                (LayerCreationRequest::for_target(target), Some(provider()))
521            }
522            _ => (LayerCreationRequest::default(), None),
523        };
524        in_flight.set(in_flight.get() + 1);
525        let factory = Rc::clone(&factory);
526        let backends = Rc::clone(&backends);
527        let queue = Rc::clone(&queue);
528        let in_flight_count = Rc::clone(&in_flight);
529        let ready_layers = Rc::clone(&ready_layers);
530        let changed_layers = Rc::clone(&changed_layers);
531        let priority = Rc::clone(&priority);
532
533        let task = async move {
534            let should_requeue = if initialization.cancelled.get() {
535                false
536            } else {
537                let _init_timing = crate::render_instrumentation::Span::new(
538                    crate::render_instrumentation::Phase::LayerInit,
539                );
540                let backend = (factory)(initialization.index, request).await;
541                initialization.complete(
542                    backend,
543                    requested_layout,
544                    &backends,
545                    &ready_layers,
546                    &changed_layers,
547                )
548            };
549            initialization.scheduled.set(false);
550            if should_requeue {
551                initialization.scheduled.set(true);
552                queue.borrow_mut().push_back(initialization);
553            }
554            in_flight_count.set(in_flight_count.get().saturating_sub(1));
555            pump_layer_initialization_queue(
556                factory,
557                backends,
558                queue,
559                in_flight_count,
560                ready_layers,
561                changed_layers,
562                priority,
563            );
564        };
565        #[cfg(target_arch = "wasm32")]
566        wasm_bindgen_futures::spawn_local(task);
567        #[cfg(not(target_arch = "wasm32"))]
568        pollster::block_on(task);
569    }
570}
571/// Runtime `RenderContext` implementation backed by `pax-gpu`/wgpu.
572pub struct PaxGpuRenderer {
573    backends: Rc<RefCell<Vec<RenderLayerState>>>,
574    #[cfg(test)]
575    flushed_surfaces: RefCell<Vec<(usize, usize, ResourceChurnStats)>>,
576    layer_factory: Rc<LayerFactory>,
577    layer_initialization_priority: Rc<dyn Fn(usize) -> i32>,
578    changed_layers: Rc<RefCell<Vec<usize>>>,
579    image_map: HashMap<String, Image>,
580    image_versions: HashMap<String, u64>,
581    failed_context_gets: RefCell<Vec<bool>>,
582    ready_layers: Rc<RefCell<Vec<usize>>>,
583    replay_layers: Rc<RefCell<Vec<usize>>>,
584    pending_layer_initializations: Rc<RefCell<VecDeque<Rc<LayerInitialization>>>>,
585    layer_initializations: Vec<Rc<LayerInitialization>>,
586    layer_initializations_in_flight: Rc<Cell<usize>>,
587    active_render_scopes: RefCell<Vec<Vec<Vec<usize>>>>,
588    dirty_render_surfaces: RefCell<Vec<HashSet<usize>>>,
589    last_scene_lighting: RefCell<Vec<Option<PixelSceneLighting>>>,
590    surface_replay: RefCell<SurfaceReplayCoordinator>,
591    clean_skipped_canvas_nodes: RefCell<HashSet<(usize, u32)>>,
592    alpha_source_scopes: Vec<(usize, usize)>,
593    #[cfg(feature = "shaders")]
594    shader_effect_scopes: Vec<(usize, usize)>,
595    #[cfg(debug_assertions)]
596    tile_cull_stats: RefCell<Vec<TileCullStats>>,
597}
598
599/// Lifecycle state for a lazily-created render layer.
600pub enum RenderLayerState {
601    Pending,
602    Failed,
603    Ready(LayerDef),
604}
605
606impl PaxGpuRenderer {
607    fn is_capturing_source(&self) -> bool {
608        #[cfg(feature = "shaders")]
609        if !self.shader_effect_scopes.is_empty() {
610            return true;
611        }
612        !self.alpha_source_scopes.is_empty()
613    }
614
615    /// Create a renderer that lazily asks the chassis for layer backends.
616    pub fn new(
617        layer_factory: impl Fn(usize, LayerCreationRequest) -> Pin<Box<dyn Future<Output = Option<LayerDef>>>>
618            + 'static,
619    ) -> Self {
620        Self {
621            backends: Default::default(),
622            #[cfg(test)]
623            flushed_surfaces: Default::default(),
624            layer_factory: Rc::new(layer_factory),
625            layer_initialization_priority: Rc::new(|_| 0),
626            changed_layers: Default::default(),
627            image_map: Default::default(),
628            image_versions: Default::default(),
629            failed_context_gets: RefCell::new(vec![]),
630            ready_layers: Default::default(),
631            replay_layers: Default::default(),
632            pending_layer_initializations: Rc::new(RefCell::new(VecDeque::new())),
633            layer_initializations: Vec::new(),
634            layer_initializations_in_flight: Rc::new(Cell::new(0)),
635            active_render_scopes: Default::default(),
636            dirty_render_surfaces: Default::default(),
637            last_scene_lighting: Default::default(),
638            surface_replay: Default::default(),
639            clean_skipped_canvas_nodes: Default::default(),
640            alpha_source_scopes: Vec::new(),
641            #[cfg(feature = "shaders")]
642            shader_effect_scopes: Vec::new(),
643            #[cfg(debug_assertions)]
644            tile_cull_stats: Default::default(),
645        }
646    }
647}
648
649impl PaxGpuRenderer {
650    /// Prioritize queued layer creation using current chassis presentation state.
651    /// Lower values start first; equal values retain FIFO order. This does not
652    /// preempt in-flight work or increase the initialization concurrency limit.
653    pub fn with_layer_initialization_priority(
654        mut self,
655        priority: impl Fn(usize) -> i32 + 'static,
656    ) -> Self {
657        self.layer_initialization_priority = Rc::new(priority);
658        self
659    }
660
661    fn queue_layer_initialization(&self, layer_index: usize) {
662        let initialization = &self.layer_initializations[layer_index];
663        if !initialization.scheduled.replace(true) {
664            self.pending_layer_initializations
665                .borrow_mut()
666                .push_back(Rc::clone(initialization));
667        }
668    }
669
670    fn enqueue_layer_initialization(&self, layer_index: usize) {
671        self.queue_layer_initialization(layer_index);
672        self.schedule_layer_initialization();
673    }
674
675    fn with_layer_context(&self, layer: usize, mut f: impl FnMut(&mut WgpuRenderer)) {
676        let mut backends = self.backends.borrow_mut();
677        match backends.get_mut(layer) {
678            Some(layer_state) => match layer_state {
679                RenderLayerState::Pending => {
680                    let mut failed_context_gets = self.failed_context_gets.borrow_mut();
681                    if failed_context_gets.len() <= layer {
682                        failed_context_gets.resize(layer + 1, false);
683                    }
684                    failed_context_gets[layer] = true;
685                }
686                RenderLayerState::Failed => {}
687                RenderLayerState::Ready((target, _)) => {
688                    if !target.active {
689                        return;
690                    }
691                    target.prepare_for_render();
692                    let scoped_indices = self.active_render_scope(layer).or_else(|| {
693                        // Targeted replay is active outside a node scope during clear(), and then
694                        // nested into per-node scopes for actual draw operations.
695                        self.targeted_replay_scope(layer)
696                    });
697                    if let Some(indices) = scoped_indices {
698                        for index in indices {
699                            if let Some(renderer) = target.renderers.get_mut(index) {
700                                f(&mut renderer.renderer);
701                            }
702                        }
703                    } else {
704                        for renderer in &mut target.renderers {
705                            f(&mut renderer.renderer);
706                        }
707                    }
708                }
709            },
710            None => log::warn!(
711                "tried to retrieve layer {} context for non-existent layer",
712                layer
713            ),
714        }
715    }
716
717    fn active_render_scope(&self, layer: usize) -> Option<Vec<usize>> {
718        self.active_render_scopes
719            .borrow()
720            .get(layer)
721            .and_then(|scopes| scopes.last().cloned())
722    }
723
724    fn push_render_scope(&self, layer: usize, renderer_indices: Vec<usize>) {
725        let mut scopes = self.active_render_scopes.borrow_mut();
726        if scopes.len() <= layer {
727            scopes.resize_with(layer + 1, Vec::new);
728        }
729        scopes[layer].push(renderer_indices);
730    }
731
732    fn pop_render_scope(&self, layer: usize) {
733        if let Some(scopes) = self.active_render_scopes.borrow_mut().get_mut(layer) {
734            scopes.pop();
735        }
736    }
737
738    fn mark_render_surfaces_dirty(
739        &self,
740        layer: usize,
741        renderer_indices: impl IntoIterator<Item = usize>,
742    ) {
743        let mut dirty_surfaces = self.dirty_render_surfaces.borrow_mut();
744        if dirty_surfaces.len() <= layer {
745            dirty_surfaces.resize_with(layer + 1, HashSet::new);
746        }
747        dirty_surfaces[layer].extend(renderer_indices);
748    }
749
750    fn dirty_render_surface_scope(&self, layer: usize) -> Option<Vec<usize>> {
751        let mut indices: Vec<_> = self
752            .dirty_render_surfaces
753            .borrow()
754            .get(layer)?
755            .iter()
756            .copied()
757            .collect();
758        if indices.is_empty() {
759            return None;
760        }
761        indices.sort_unstable();
762        Some(indices)
763    }
764
765    fn clear_dirty_render_surfaces(&self, layer: usize, renderer_indices: &[usize]) {
766        if let Some(dirty_surfaces) = self.dirty_render_surfaces.borrow_mut().get_mut(layer) {
767            for index in renderer_indices {
768                dirty_surfaces.remove(index);
769            }
770        }
771    }
772
773    fn update_scene_lighting_cache(&self, layer: usize, lighting: &PixelSceneLighting) -> bool {
774        let mut last_scene_lighting = self.last_scene_lighting.borrow_mut();
775        if last_scene_lighting.len() <= layer {
776            last_scene_lighting.resize_with(layer + 1, || None);
777        }
778        let changed = last_scene_lighting[layer].as_ref() != Some(lighting);
779        if changed {
780            last_scene_lighting[layer] = Some(lighting.clone());
781        }
782        changed
783    }
784
785    fn targeted_replay_scope(&self, layer: usize) -> Option<Vec<usize>> {
786        self.surface_replay.borrow().targeted_replay_scope(layer)
787    }
788
789    fn set_targeted_replay_batches(
790        &self,
791        layer: usize,
792        batches: Vec<Vec<usize>>,
793        bounds_by_surface: HashMap<usize, Vec<Rect>>,
794    ) {
795        self.surface_replay
796            .borrow_mut()
797            .set_targeted_replay_batches(layer, batches, bounds_by_surface);
798    }
799
800    fn advance_targeted_replay_queue(&self, layer: usize) -> bool {
801        self.surface_replay
802            .borrow_mut()
803            .advance_targeted_replay_queue(layer)
804    }
805
806    fn flush_targeted_or_dirty(&self, layer: usize, target: &mut LayerTarget) -> bool {
807        #[cfg(test)]
808        self.flushed_surfaces
809            .borrow_mut()
810            .retain(|(old_layer, _, _)| *old_layer != layer);
811        use crate::render_instrumentation::{Phase, Span};
812        let targeted_indices = self.targeted_replay_scope(layer);
813        let dirty_indices = self.dirty_render_surface_scope(layer);
814        let used_targeted_replay = targeted_indices.is_some();
815        #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
816        let targeted_surface_count = targeted_indices.as_ref().map(Vec::len).unwrap_or(0);
817        #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
818        let dirty_surface_count = dirty_indices.as_ref().map(Vec::len).unwrap_or(0);
819        let mut indices = targeted_indices.unwrap_or_default();
820        if let Some(dirty_indices) = dirty_indices {
821            indices.extend(dirty_indices);
822        }
823        indices.sort_unstable();
824        indices.dedup();
825
826        if !indices.is_empty() {
827            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
828            let flush_start = std::time::Instant::now();
829            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
830            let mut encode_us = 0u128;
831            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
832            let mut submit_us = 0u128;
833            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
834            let mut cleanup_us = 0u128;
835            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
836            let mut present_us = 0u128;
837            let mut resource_stats = ResourceChurnStats::default();
838            let mut command_buffers = Vec::new();
839            let mut submitter_index = None;
840            let mut cleanup_indices = Vec::new();
841            let mut present_indices = Vec::new();
842            for index in &indices {
843                if let Some(renderer) = target.renderers.get_mut(*index) {
844                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
845                    let encode_start = std::time::Instant::now();
846                    let encode_timing = Span::new(Phase::Encode);
847                    renderer.renderer.flush_deferred();
848                    drop(encode_timing);
849                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
850                    {
851                        encode_us += encode_start.elapsed().as_micros();
852                    }
853                    let mut renderer_command_buffers =
854                        renderer.renderer.take_pending_command_buffers();
855                    if !renderer_command_buffers.is_empty() {
856                        if submitter_index.is_none() {
857                            submitter_index = Some(*index);
858                        }
859                        command_buffers.append(&mut renderer_command_buffers);
860                    }
861                    if renderer.renderer.has_pending_submission_cleanup() {
862                        cleanup_indices.push(*index);
863                    }
864                    present_indices.push(*index);
865                    let stats = renderer.renderer.take_resource_churn_stats();
866                    #[cfg(test)]
867                    self.flushed_surfaces
868                        .borrow_mut()
869                        .push((layer, *index, stats));
870                    count(
871                        Counter::ImageTextureUploaded,
872                        stats.texture_creates as usize,
873                    );
874                    count(
875                        Counter::ImageTextureUploadBytes,
876                        stats.texture_upload_bytes as usize,
877                    );
878                    count(
879                        Counter::ImageTextureReused,
880                        stats.texture_cache_hits as usize,
881                    );
882                    resource_stats.merge(stats);
883                }
884            }
885            let command_buffer_count = command_buffers.len();
886            if command_buffer_count != 0 {
887                if let Some(index) = submitter_index {
888                    if let Some(renderer) = target.renderers.get(index) {
889                        #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
890                        let submit_start = std::time::Instant::now();
891                        let submit_timing = Span::new(Phase::Submit);
892                        renderer.renderer.submit_command_buffers(command_buffers);
893                        drop(submit_timing);
894                        #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
895                        {
896                            submit_us += submit_start.elapsed().as_micros();
897                        }
898                        log::trace!(
899                            "[pax-gpu-submit] layer={} surfaces={} command_buffers={} shared_submit=1",
900                            layer,
901                            indices.len(),
902                            command_buffer_count
903                        );
904                    }
905                }
906            }
907            for index in &cleanup_indices {
908                if let Some(renderer) = target.renderers.get_mut(*index) {
909                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
910                    let cleanup_start = std::time::Instant::now();
911                    let cleanup_timing = Span::new(Phase::Cleanup);
912                    renderer.renderer.complete_submitted_work();
913                    drop(cleanup_timing);
914                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
915                    {
916                        cleanup_us += cleanup_start.elapsed().as_micros();
917                    }
918                }
919            }
920            for index in &present_indices {
921                if let Some(renderer) = target.renderers.get_mut(*index) {
922                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
923                    let present_start = std::time::Instant::now();
924                    let present_timing = Span::new(Phase::Present);
925                    renderer.renderer.present_deferred_frame();
926                    drop(present_timing);
927                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
928                    {
929                        present_us += present_start.elapsed().as_micros();
930                    }
931                }
932            }
933            #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
934            {
935                let total_us = flush_start.elapsed().as_micros();
936                let line = format!(
937                    "[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={} texture_cache_hits={}",
938                    layer,
939                    indices.len(),
940                    used_targeted_replay,
941                    targeted_surface_count,
942                    dirty_surface_count,
943                    command_buffer_count,
944                    cleanup_indices.len(),
945                    present_indices.len(),
946                    total_us,
947                    encode_us,
948                    submit_us,
949                    cleanup_us,
950                    present_us,
951                    resource_stats.flushes,
952                    resource_stats.retained_nodes_visible,
953                    resource_stats.retained_nodes_considered,
954                    resource_stats.retained_draw_batches,
955                    resource_stats.retained_draws,
956                    resource_stats.retained_vector_draws,
957                    resource_stats.retained_image_draws,
958                    resource_stats.vector_batch_flushes,
959                    resource_stats.vector_geometry_rebuilds,
960                    resource_stats.vector_geometry_cache_hits,
961                    resource_stats.vector_geometry_cache_misses,
962                    resource_stats.vector_geometry_cache_evictions,
963                    resource_stats.vector_geometry_cache_bytes,
964                    resource_stats.tessellated_vertices,
965                    resource_stats.tessellated_indices,
966                    resource_stats.cached_vertices_reused,
967                    resource_stats.cached_indices_reused,
968                    resource_stats.vector_resource_creates,
969                    resource_stats.vector_resource_updates,
970                    resource_stats.vector_resource_recreates,
971                    resource_stats.vector_resource_create_bytes,
972                    resource_stats.vector_resource_update_bytes,
973                    resource_stats.vector_resource_cache_hits,
974                    resource_stats.vector_resource_cache_misses,
975                    resource_stats.vector_resource_cache_evictions,
976                    resource_stats.vector_resource_cache_bytes,
977                    resource_stats.texture_creates,
978                    resource_stats.texture_upload_bytes,
979                    resource_stats.texture_cache_hits,
980                );
981                log::trace!("{}", line);
982                #[cfg(not(target_arch = "wasm32"))]
983                if should_print_gpu_profile_logs()
984                    && (used_targeted_replay
985                        || total_us >= 8_000
986                        || resource_stats.has_resource_churn())
987                {
988                    println!("{}", line);
989                }
990            }
991            if resource_stats.has_resource_churn() {
992                log::trace!(
993                    "[pax-gpu-resources] layer={} surfaces={} targeted={} {:?}",
994                    layer,
995                    indices.len(),
996                    used_targeted_replay,
997                    resource_stats
998                );
999            }
1000            self.clear_dirty_render_surfaces(layer, &indices);
1001        }
1002
1003        used_targeted_replay
1004    }
1005
1006    fn clear_targeted_replay_scope(&self, layer: usize) {
1007        self.surface_replay
1008            .borrow_mut()
1009            .clear_targeted_replay_scope(layer);
1010    }
1011
1012    fn targeted_or_all_indices(&self, layer: usize, renderer_count: usize) -> Vec<usize> {
1013        self.surface_replay
1014            .borrow()
1015            .targeted_or_all_indices(layer, renderer_count)
1016    }
1017
1018    fn mark_clean_skipped_node(&self, layer: usize, node_id: u32) {
1019        self.clean_skipped_canvas_nodes
1020            .borrow_mut()
1021            .insert((layer, node_id));
1022    }
1023
1024    fn targeted_replay_surface_bounds(
1025        &self,
1026        layer: usize,
1027        target: &LayerTarget,
1028    ) -> Option<Vec<Rect>> {
1029        self.surface_replay
1030            .borrow()
1031            .targeted_replay_surface_bounds(layer, |index| {
1032                let renderer = target.renderers.get(index)?;
1033                Some(Rect::new(
1034                    renderer.origin_x as f64,
1035                    renderer.origin_y as f64,
1036                    renderer.origin_x as f64 + renderer.logical_width as f64,
1037                    renderer.origin_y as f64 + renderer.logical_height as f64,
1038                ))
1039            })
1040    }
1041
1042    fn replay_region_bounds(&self, layer: usize) -> Option<Vec<kurbo::Rect>> {
1043        let backends = self.backends.borrow();
1044        let Some(RenderLayerState::Ready((target, _))) = backends.get(layer) else {
1045            return None;
1046        };
1047        self.targeted_replay_surface_bounds(layer, target)
1048    }
1049
1050    fn take_replay_layer_ids(&mut self) -> Vec<usize> {
1051        let mut replay_layers = self.replay_layers.borrow_mut();
1052        let mut replay = std::mem::take(&mut *replay_layers);
1053        replay.sort_unstable();
1054        replay.dedup();
1055        replay
1056    }
1057
1058    #[cfg(debug_assertions)]
1059    fn update_tile_cull_stats(&self, layer: usize, update: impl FnOnce(&mut TileCullStats)) {
1060        let mut stats = self.tile_cull_stats.borrow_mut();
1061        if stats.len() <= layer {
1062            stats.resize_with(layer + 1, TileCullStats::default);
1063        }
1064        update(&mut stats[layer]);
1065    }
1066
1067    #[cfg(debug_assertions)]
1068    fn emit_tile_cull_stats(&self, layer: usize) {
1069        let stats = self
1070            .tile_cull_stats
1071            .borrow_mut()
1072            .get_mut(layer)
1073            .map(std::mem::take);
1074        if let Some(stats) = stats {
1075            if stats.origin_only_retargets > 0
1076                || stats.targeted_replay_batches > 0
1077                || stats.full_layer_replays > 0
1078                || stats.resize_resets > 0
1079            {
1080                let line = format!(
1081                    "[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={}",
1082                    layer,
1083                    stats.nodes_considered,
1084                    stats.selected_surfaces,
1085                    stats.skipped_surfaces,
1086                    stats.stale_surface_removal_attempts,
1087                    stats.origin_only_retargets,
1088                    stats.targeted_replay_batches,
1089                    stats.targeted_replay_surfaces,
1090                    stats.targeted_replay_visible_batches,
1091                    stats.targeted_replay_warm_batches,
1092                    stats.targeted_replay_visible_surfaces,
1093                    stats.targeted_replay_warm_surfaces,
1094                    stats.full_layer_replays,
1095                    stats.resize_resets
1096                );
1097                log::trace!("{}", line);
1098                #[cfg(not(target_arch = "wasm32"))]
1099                if should_print_gpu_profile_logs() {
1100                    println!("{}", line);
1101                }
1102            }
1103        }
1104    }
1105
1106    fn schedule_layer_initialization(&self) {
1107        pump_layer_initialization_queue(
1108            Rc::clone(&self.layer_factory),
1109            Rc::clone(&self.backends),
1110            Rc::clone(&self.pending_layer_initializations),
1111            Rc::clone(&self.layer_initializations_in_flight),
1112            Rc::clone(&self.ready_layers),
1113            Rc::clone(&self.changed_layers),
1114            Rc::clone(&self.layer_initialization_priority),
1115        );
1116    }
1117
1118    fn refresh_layer_layouts<I>(&mut self, layer_indices: I)
1119    where
1120        I: IntoIterator<Item = usize>,
1121    {
1122        let mut needs_reinitialization = false;
1123        let mut backends = self.backends.borrow_mut();
1124        for layer_index in layer_indices {
1125            let Some(backend) = backends.get_mut(layer_index) else {
1126                continue;
1127            };
1128            match backend {
1129                RenderLayerState::Pending => {}
1130                RenderLayerState::Failed => {}
1131                RenderLayerState::Ready((target, layout_provider)) => {
1132                    let layout = (layout_provider)();
1133                    let layout_matches = layer_layout_matches_target(target, &layout);
1134
1135                    if layout.active {
1136                        target.activate();
1137                    } else {
1138                        target.deactivate();
1139                        self.clear_targeted_replay_scope(layer_index);
1140                    }
1141
1142                    if !layout_matches {
1143                        let existing: Vec<_> = target
1144                            .renderers
1145                            .iter()
1146                            .map(|r| (r.key.as_str(), r.host_signature.as_str()))
1147                            .collect();
1148                        let desired: Vec<_> = layout
1149                            .surfaces
1150                            .iter()
1151                            .map(|s| (s.key.as_str(), s.host_signature.as_str()))
1152                            .collect();
1153                        let Some(order) = retained_surface_order(&existing, &desired) else {
1154                            log::error!("duplicate surface identity in layer {}", layer_index);
1155                            continue;
1156                        };
1157                        let reordered = order.len() != target.renderers.len()
1158                            || order.iter().enumerate().any(|(new, old)| new != *old);
1159                        if reordered {
1160                            count(Counter::TileSetChanged, 1);
1161                            count(Counter::TileReused, order.len());
1162                            count(
1163                                Counter::MatchingTileDiscarded,
1164                                existing
1165                                    .iter()
1166                                    .enumerate()
1167                                    .filter(|(index, identity)| {
1168                                        desired.contains(identity) && !order.contains(index)
1169                                    })
1170                                    .count(),
1171                            );
1172                            self.surface_replay
1173                                .borrow_mut()
1174                                .remap_surfaces(layer_index, &order);
1175                            if let Some(dirty) =
1176                                self.dirty_render_surfaces.borrow_mut().get_mut(layer_index)
1177                            {
1178                                *dirty = order
1179                                    .iter()
1180                                    .enumerate()
1181                                    .filter_map(|(new, old)| dirty.contains(old).then_some(new))
1182                                    .collect();
1183                            }
1184                            retain_surfaces(&mut target.renderers, order);
1185                        }
1186                        if target.renderers.len() < layout.surfaces.len()
1187                            && target.failed_layout.as_ref() != Some(&layout)
1188                        {
1189                            self.queue_layer_initialization(layer_index);
1190                            needs_reinitialization = true;
1191                        }
1192                    }
1193
1194                    let mut targeted_replay_entries = Vec::new();
1195                    let mut targeted_replay_bounds = HashMap::new();
1196                    // Warm work can become visible without an origin change. Reclassify it
1197                    // using this layout, retaining coverage until the tile has actually replayed.
1198                    for (index, bounds) in self
1199                        .surface_replay
1200                        .borrow()
1201                        .pending_replay_surfaces(layer_index)
1202                    {
1203                        let Some(renderer) = target.renderers.get(index) else {
1204                            continue;
1205                        };
1206                        let Some(surface) = layout
1207                            .surfaces
1208                            .iter()
1209                            .find(|surface| renderer.matches_surface(surface))
1210                        else {
1211                            continue;
1212                        };
1213                        let current = renderer.coverage_bounds();
1214                        targeted_replay_entries.push(ReplayPriorityEntry::new(
1215                            index,
1216                            surface.replay_priority,
1217                            current,
1218                            current,
1219                        ));
1220                        targeted_replay_bounds.insert(index, bounds);
1221                    }
1222                    let needs_full_layer_replay = target.needs_replay;
1223                    #[cfg(debug_assertions)]
1224                    {
1225                        let previous_surface_bounds: Vec<_> = target
1226                            .renderers
1227                            .iter()
1228                            .map(LayerRenderer::coverage_bounds)
1229                            .collect();
1230                        if let Some(escape) =
1231                            visible_surface_escape(&previous_surface_bounds, &layout.surfaces)
1232                        {
1233                            log_visible_surface_escape(layer_index, escape);
1234                        }
1235                    }
1236                    for (index, renderer) in target.renderers.iter_mut().enumerate() {
1237                        let Some(surface) = layout.surfaces.iter().find(|surface| {
1238                            surface.key == renderer.key
1239                                && surface.host_signature == renderer.host_signature
1240                        }) else {
1241                            continue;
1242                        };
1243                        if std::mem::take(&mut renderer.needs_initial_replay) {
1244                            let bounds = Rect::new(
1245                                surface.origin_x as f64,
1246                                surface.origin_y as f64,
1247                                (surface.origin_x + surface.surface.logical_width) as f64,
1248                                (surface.origin_y + surface.surface.logical_height) as f64,
1249                            );
1250                            targeted_replay_entries.push(ReplayPriorityEntry::new(
1251                                index,
1252                                surface.replay_priority,
1253                                bounds,
1254                                bounds,
1255                            ));
1256                            targeted_replay_bounds.insert(index, vec![bounds]);
1257                            // Lighting may not have changed since the surviving tiles were drawn.
1258                            if let Some(Some(lighting)) =
1259                                self.last_scene_lighting.borrow().get(layer_index)
1260                            {
1261                                renderer.renderer.set_scene_lighting(
1262                                    translate_scene_lighting_for_surface(
1263                                        lighting,
1264                                        surface.origin_x,
1265                                        surface.origin_y,
1266                                    ),
1267                                );
1268                            }
1269                        }
1270                        let previous_bounds = renderer.coverage_bounds();
1271                        let layout_change = renderer.update_layout(surface);
1272                        let current_bounds = renderer.coverage_bounds();
1273                        match layout_change {
1274                            LayoutChangeKind::Unchanged => {}
1275                            LayoutChangeKind::OriginOnly => {
1276                                count(Counter::OriginRetargeted, 1);
1277                                count(Counter::SceneReset, 1);
1278                                targeted_replay_entries.push(ReplayPriorityEntry::new(
1279                                    index,
1280                                    surface.replay_priority,
1281                                    previous_bounds,
1282                                    current_bounds,
1283                                ));
1284                                // Retained nodes carry transforms/resources built against the
1285                                // previous surface origin, so an origin retarget must drop the
1286                                // old scene before replaying nodes into the new tile.
1287                                targeted_replay_bounds
1288                                    .insert(index, vec![previous_bounds, current_bounds]);
1289                                renderer.renderer.reset_retained_scene();
1290                                renderer
1291                                    .renderer
1292                                    .set_surface_transform(Transform2D::from_array([
1293                                        1.0,
1294                                        0.0,
1295                                        0.0,
1296                                        1.0,
1297                                        -surface.origin_x,
1298                                        -surface.origin_y,
1299                                    ]));
1300                                self.replay_layers.borrow_mut().push(layer_index);
1301                            }
1302                            LayoutChangeKind::Resized => {
1303                                count(Counter::TileResized, 1);
1304                                count(Counter::SceneReset, 1);
1305                                #[cfg(debug_assertions)]
1306                                self.update_tile_cull_stats(layer_index, |stats| {
1307                                    stats.resize_resets += 1;
1308                                });
1309                                renderer.renderer.reset_retained_scene();
1310                                renderer
1311                                    .renderer
1312                                    .set_surface_transform(Transform2D::from_array([
1313                                        1.0,
1314                                        0.0,
1315                                        0.0,
1316                                        1.0,
1317                                        -surface.origin_x,
1318                                        -surface.origin_y,
1319                                    ]));
1320                                renderer.renderer.resize_surface(
1321                                    surface.surface.surface_width as f32,
1322                                    surface.surface.surface_height as f32,
1323                                );
1324                                renderer.renderer.set_viewport(
1325                                    surface.surface.logical_width,
1326                                    surface.surface.logical_height,
1327                                    surface.surface.dpr,
1328                                );
1329                                targeted_replay_entries.push(ReplayPriorityEntry::new(
1330                                    index,
1331                                    surface.replay_priority,
1332                                    previous_bounds,
1333                                    current_bounds,
1334                                ));
1335                                targeted_replay_bounds
1336                                    .insert(index, vec![previous_bounds, current_bounds]);
1337                            }
1338                        }
1339                    }
1340                    if needs_full_layer_replay {
1341                        self.clear_targeted_replay_scope(layer_index);
1342                        #[cfg(debug_assertions)]
1343                        self.update_tile_cull_stats(layer_index, |stats| {
1344                            stats.full_layer_replays += 1;
1345                        });
1346                        self.mark_render_surfaces_dirty(layer_index, 0..target.renderers.len());
1347                        self.replay_layers.borrow_mut().push(layer_index);
1348                    } else if !targeted_replay_entries.is_empty() {
1349                        targeted_replay_entries.sort_by_key(|entry| entry.index);
1350                        targeted_replay_entries.dedup_by_key(|entry| entry.index);
1351                        let replay_batches =
1352                            replay_batches_by_directional_priority(&targeted_replay_entries);
1353                        #[cfg(debug_assertions)]
1354                        let replay_priority_stats =
1355                            replay_priority_stats(&targeted_replay_entries, &replay_batches);
1356                        #[cfg(debug_assertions)]
1357                        self.update_tile_cull_stats(layer_index, |stats| {
1358                            stats.origin_only_retargets +=
1359                                replay_batches.iter().map(Vec::len).sum::<usize>() as u64;
1360                            stats.targeted_replay_batches += replay_batches.len() as u64;
1361                            stats.targeted_replay_surfaces +=
1362                                replay_batches.iter().map(Vec::len).sum::<usize>() as u64;
1363                            stats.targeted_replay_visible_batches += replay_priority_stats.0 as u64;
1364                            stats.targeted_replay_warm_batches += replay_priority_stats.1 as u64;
1365                            stats.targeted_replay_visible_surfaces +=
1366                                replay_priority_stats.2 as u64;
1367                            stats.targeted_replay_warm_surfaces += replay_priority_stats.3 as u64;
1368                        });
1369                        self.set_targeted_replay_batches(
1370                            layer_index,
1371                            replay_batches,
1372                            targeted_replay_bounds,
1373                        );
1374                        self.replay_layers.borrow_mut().push(layer_index);
1375                    }
1376                }
1377            }
1378        }
1379        drop(backends);
1380        if needs_reinitialization {
1381            self.schedule_layer_initialization();
1382        }
1383    }
1384}
1385
1386impl RenderContext for PaxGpuRenderer {
1387    fn fill_with_opacity(
1388        &mut self,
1389        layer: usize,
1390        path: kurbo::BezPath,
1391        fill: &pax_runtime_api::Paint,
1392        opacity: f64,
1393    ) {
1394        self.with_layer_context(layer, |context| {
1395            let bounds = path.bounding_box();
1396            let path = convert_kurbo_to_lyon_path(&path);
1397            let fill = to_pax_gpu_fill(fill, bounds, context.current_transform());
1398            context.fill_path_with_opacity(path, fill, opacity as f32);
1399        });
1400    }
1401
1402    fn fill_with_material_and_opacity(
1403        &mut self,
1404        layer: usize,
1405        path: kurbo::BezPath,
1406        fill: &pax_runtime_api::Paint,
1407        material: &ResolvedMaterial,
1408        opacity: f64,
1409    ) {
1410        self.with_layer_context(layer, |context| {
1411            let bounds = path.bounding_box();
1412            let path = convert_kurbo_to_lyon_path(&path);
1413            let fill = to_pax_gpu_fill(fill, bounds, context.current_transform());
1414            let material = to_pax_gpu_material(material);
1415            context.fill_path_with_material_and_opacity(path, fill, material, opacity as f32);
1416        });
1417    }
1418
1419    fn fill_with_material_and_opacity_and_smoothing(
1420        &mut self,
1421        layer: usize,
1422        path: kurbo::BezPath,
1423        fill: &pax_runtime_api::Paint,
1424        material: &ResolvedMaterial,
1425        opacity: f64,
1426        smoothing: PathSmoothing,
1427    ) {
1428        self.with_layer_context(layer, |context| {
1429            let bounds =
1430                pax_runtime_api::drawing::path_smoothing::smooth_bez_path(&path, smoothing)
1431                    .bounding_box();
1432            let path = convert_kurbo_to_lyon_path(&path);
1433            let fill = to_pax_gpu_fill(fill, bounds, context.current_transform());
1434            let material = to_pax_gpu_material(material);
1435            context.fill_path_with_material_and_opacity_and_smoothing(
1436                path,
1437                fill,
1438                material,
1439                opacity as f32,
1440                smoothing,
1441            );
1442        });
1443    }
1444
1445    fn stroke_with_opacity(
1446        &mut self,
1447        layer: usize,
1448        path: kurbo::BezPath,
1449        stroke: &ResolvedStroke,
1450        opacity: f64,
1451    ) {
1452        self.with_layer_context(layer, |context| {
1453            let bounds = path.bounding_box();
1454            context.stroke_path_with_opacity(
1455                convert_kurbo_to_lyon_path(&path),
1456                PixelStroke {
1457                    fill: to_pax_gpu_fill(&stroke.paint, bounds, context.current_transform()),
1458                    weight: stroke.width_pixels() as f32,
1459                    cap: match stroke.cap {
1460                        StrokeCap::Butt => PixelStrokeCap::Butt,
1461                        StrokeCap::Round => PixelStrokeCap::Round,
1462                        StrokeCap::Square => PixelStrokeCap::Square,
1463                    },
1464                    join: match stroke.join {
1465                        StrokeJoin::Miter => PixelStrokeJoin::Miter,
1466                        StrokeJoin::Round => PixelStrokeJoin::Round,
1467                        StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1468                    },
1469                },
1470                opacity as f32,
1471            );
1472        });
1473    }
1474
1475    fn stroke_with_material_and_opacity(
1476        &mut self,
1477        layer: usize,
1478        path: kurbo::BezPath,
1479        stroke: &ResolvedStroke,
1480        material: &ResolvedMaterial,
1481        opacity: f64,
1482    ) {
1483        self.with_layer_context(layer, |context| {
1484            let bounds = path.bounding_box();
1485            context.stroke_path_with_material_and_opacity(
1486                convert_kurbo_to_lyon_path(&path),
1487                PixelStroke {
1488                    fill: to_pax_gpu_fill(&stroke.paint, bounds, context.current_transform()),
1489                    weight: stroke.width_pixels() as f32,
1490                    cap: match stroke.cap {
1491                        StrokeCap::Butt => PixelStrokeCap::Butt,
1492                        StrokeCap::Round => PixelStrokeCap::Round,
1493                        StrokeCap::Square => PixelStrokeCap::Square,
1494                    },
1495                    join: match stroke.join {
1496                        StrokeJoin::Miter => PixelStrokeJoin::Miter,
1497                        StrokeJoin::Round => PixelStrokeJoin::Round,
1498                        StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1499                    },
1500                },
1501                to_pax_gpu_material(material),
1502                opacity as f32,
1503            );
1504        });
1505    }
1506
1507    fn stroke_with_material_and_opacity_and_smoothing(
1508        &mut self,
1509        layer: usize,
1510        path: kurbo::BezPath,
1511        stroke: &ResolvedStroke,
1512        material: &ResolvedMaterial,
1513        opacity: f64,
1514        smoothing: PathSmoothing,
1515    ) {
1516        self.with_layer_context(layer, |context| {
1517            let bounds =
1518                pax_runtime_api::drawing::path_smoothing::smooth_bez_path(&path, smoothing)
1519                    .bounding_box();
1520            context.stroke_path_with_material_and_opacity_and_smoothing(
1521                convert_kurbo_to_lyon_path(&path),
1522                PixelStroke {
1523                    fill: to_pax_gpu_fill(&stroke.paint, bounds, context.current_transform()),
1524                    weight: stroke.width_pixels() as f32,
1525                    cap: match stroke.cap {
1526                        StrokeCap::Butt => PixelStrokeCap::Butt,
1527                        StrokeCap::Round => PixelStrokeCap::Round,
1528                        StrokeCap::Square => PixelStrokeCap::Square,
1529                    },
1530                    join: match stroke.join {
1531                        StrokeJoin::Miter => PixelStrokeJoin::Miter,
1532                        StrokeJoin::Round => PixelStrokeJoin::Round,
1533                        StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1534                    },
1535                },
1536                to_pax_gpu_material(material),
1537                opacity as f32,
1538                smoothing,
1539            );
1540        });
1541    }
1542
1543    fn stroke_with_draw_range_and_material_and_opacity(
1544        &mut self,
1545        layer: usize,
1546        path: kurbo::BezPath,
1547        stroke: &ResolvedStroke,
1548        material: &ResolvedMaterial,
1549        opacity: f64,
1550        draw_start: f64,
1551        draw_end: f64,
1552    ) {
1553        self.with_layer_context(layer, |context| {
1554            let bounds = path.bounding_box();
1555            context.stroke_path_with_draw_range_and_material_and_opacity(
1556                convert_kurbo_to_lyon_path(&path),
1557                PixelStroke {
1558                    fill: to_pax_gpu_fill(&stroke.paint, bounds, context.current_transform()),
1559                    weight: stroke.width_pixels() as f32,
1560                    cap: match stroke.cap {
1561                        StrokeCap::Butt => PixelStrokeCap::Butt,
1562                        StrokeCap::Round => PixelStrokeCap::Round,
1563                        StrokeCap::Square => PixelStrokeCap::Square,
1564                    },
1565                    join: match stroke.join {
1566                        StrokeJoin::Miter => PixelStrokeJoin::Miter,
1567                        StrokeJoin::Round => PixelStrokeJoin::Round,
1568                        StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1569                    },
1570                },
1571                to_pax_gpu_material(material),
1572                opacity as f32,
1573                PixelDrawRange::enabled(draw_start as f32, draw_end as f32),
1574            );
1575        });
1576    }
1577
1578    fn stroke_with_draw_range_and_material_and_opacity_and_smoothing(
1579        &mut self,
1580        layer: usize,
1581        path: kurbo::BezPath,
1582        stroke: &ResolvedStroke,
1583        material: &ResolvedMaterial,
1584        opacity: f64,
1585        draw_start: f64,
1586        draw_end: f64,
1587        smoothing: PathSmoothing,
1588    ) {
1589        self.with_layer_context(layer, |context| {
1590            let bounds =
1591                pax_runtime_api::drawing::path_smoothing::smooth_bez_path(&path, smoothing)
1592                    .bounding_box();
1593            context.stroke_path_with_draw_range_and_material_and_opacity_and_smoothing(
1594                convert_kurbo_to_lyon_path(&path),
1595                PixelStroke {
1596                    fill: to_pax_gpu_fill(&stroke.paint, bounds, context.current_transform()),
1597                    weight: stroke.width_pixels() as f32,
1598                    cap: match stroke.cap {
1599                        StrokeCap::Butt => PixelStrokeCap::Butt,
1600                        StrokeCap::Round => PixelStrokeCap::Round,
1601                        StrokeCap::Square => PixelStrokeCap::Square,
1602                    },
1603                    join: match stroke.join {
1604                        StrokeJoin::Miter => PixelStrokeJoin::Miter,
1605                        StrokeJoin::Round => PixelStrokeJoin::Round,
1606                        StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1607                    },
1608                },
1609                to_pax_gpu_material(material),
1610                opacity as f32,
1611                PixelDrawRange::enabled(draw_start as f32, draw_end as f32),
1612                smoothing,
1613            );
1614        });
1615    }
1616
1617    fn save(&mut self, layer: usize) {
1618        self.with_layer_context(layer, |context| {
1619            context.save();
1620        });
1621    }
1622    fn restore(&mut self, layer: usize) {
1623        self.with_layer_context(layer, |context| {
1624            context.restore();
1625        });
1626    }
1627    fn clip(&mut self, layer: usize, path: kurbo::BezPath) {
1628        self.with_layer_context(layer, |context| {
1629            let path = convert_kurbo_to_lyon_path(&path);
1630            context.clip(path);
1631        });
1632    }
1633    fn clip_alpha(
1634        &mut self,
1635        layer: usize,
1636        paints: &[pax_runtime_api::AlphaMaskPaint],
1637        feather: f64,
1638    ) {
1639        self.with_layer_context(layer, |context| {
1640            let paints = paints
1641                .iter()
1642                .map(|paint| {
1643                    let transform =
1644                        Transform2D::from_array(paint.transform.as_coeffs().map(|v| v as f32));
1645                    pax_gpu::AlphaMaskPaint {
1646                        path: convert_kurbo_to_lyon_path(&paint.path),
1647                        fill: to_pax_gpu_alpha_fill(
1648                            &paint.fill,
1649                            paint.paint_bounds,
1650                            transform.then(&context.current_transform()),
1651                        ),
1652                        transform,
1653                        opacity: paint.opacity.clamp(0.0, 1.0) as f32,
1654                        composition: paint.composition,
1655                    }
1656                })
1657                .collect();
1658            context.clip_alpha(paints, feather.max(0.0) as f32);
1659        });
1660    }
1661
1662    fn transform(&mut self, layer: usize, affine: kurbo::Affine) {
1663        self.with_layer_context(layer, |context| {
1664            context.transform(Transform2D::from_array(
1665                affine.as_coeffs().map(|v| v as f32),
1666            ))
1667        });
1668    }
1669
1670    fn load_image(&mut self, identifier: &str, image: &[u8], width: usize, height: usize) {
1671        self.image_map.insert(
1672            identifier.to_string(),
1673            Image {
1674                rgba: image.into(),
1675                pixel_width: width as u32,
1676                pixel_height: height as u32,
1677            },
1678        );
1679        *self
1680            .image_versions
1681            .entry(identifier.to_string())
1682            .or_insert(0) += 1;
1683    }
1684
1685    fn draw_image_with_opacity(
1686        &mut self,
1687        layer: usize,
1688        image_path: &str,
1689        rect: kurbo::Rect,
1690        opacity: f64,
1691    ) {
1692        self.with_layer_context(layer, |context| {
1693            if let Some(image) = self.image_map.get(image_path) {
1694                let version = *self.image_versions.get(image_path).unwrap_or(&0);
1695                context.draw_image_with_opacity(
1696                    image_path,
1697                    version,
1698                    image,
1699                    Box2D {
1700                        min: point(rect.x0 as f32, rect.y0 as f32),
1701                        max: point(rect.x1 as f32, rect.y1 as f32),
1702                    },
1703                    opacity as f32,
1704                );
1705            }
1706        });
1707    }
1708
1709    fn get_image_size(&mut self, image_path: &str) -> Option<(usize, usize)> {
1710        self.image_map
1711            .get(image_path)
1712            .map(|img| (img.pixel_width as usize, img.pixel_height as usize))
1713    }
1714
1715    fn image_loaded(&self, image_path: &str) -> bool {
1716        self.image_map.contains_key(image_path)
1717    }
1718
1719    fn layers(&self) -> usize {
1720        self.backends.borrow().len()
1721    }
1722
1723    fn resize_layers_to(&mut self, layer_count: usize, _dirty_canvases: Rc<RefCell<Vec<bool>>>) {
1724        let current_len = self.backends.borrow().len();
1725        match layer_count.cmp(&current_len) {
1726            std::cmp::Ordering::Less => {
1727                for initialization in self.layer_initializations.drain(layer_count..) {
1728                    initialization.cancelled.set(true);
1729                }
1730                self.pending_layer_initializations
1731                    .borrow_mut()
1732                    .retain(|initialization| !initialization.cancelled.get());
1733                self.backends.borrow_mut().truncate(layer_count);
1734                self.ready_layers
1735                    .borrow_mut()
1736                    .retain(|layer| *layer < layer_count);
1737                self.changed_layers
1738                    .borrow_mut()
1739                    .retain(|layer| *layer < layer_count);
1740                self.replay_layers
1741                    .borrow_mut()
1742                    .retain(|layer| *layer < layer_count);
1743                self.failed_context_gets.borrow_mut().truncate(layer_count);
1744                self.active_render_scopes.borrow_mut().truncate(layer_count);
1745                self.dirty_render_surfaces
1746                    .borrow_mut()
1747                    .truncate(layer_count);
1748                self.last_scene_lighting.borrow_mut().truncate(layer_count);
1749                self.surface_replay
1750                    .borrow_mut()
1751                    .truncate_layers(layer_count);
1752                self.clean_skipped_canvas_nodes
1753                    .borrow_mut()
1754                    .retain(|(layer, _)| *layer < layer_count);
1755                #[cfg(debug_assertions)]
1756                self.tile_cull_stats.borrow_mut().truncate(layer_count);
1757            }
1758            std::cmp::Ordering::Equal => return,
1759            std::cmp::Ordering::Greater => {
1760                for i in current_len..layer_count {
1761                    self.backends.borrow_mut().push(RenderLayerState::Pending);
1762                    self.layer_initializations
1763                        .push(Rc::new(LayerInitialization::new(i)));
1764                    self.enqueue_layer_initialization(i);
1765                }
1766            }
1767        }
1768    }
1769
1770    fn clear(&mut self, layer: usize) {
1771        let mut backends = self.backends.borrow_mut();
1772        match backends.get_mut(layer) {
1773            Some(RenderLayerState::Pending) => {
1774                let mut failed_context_gets = self.failed_context_gets.borrow_mut();
1775                if failed_context_gets.len() <= layer {
1776                    failed_context_gets.resize(layer + 1, false);
1777                }
1778                failed_context_gets[layer] = true;
1779            }
1780            Some(RenderLayerState::Failed) => {}
1781            Some(RenderLayerState::Ready((target, _))) => {
1782                if !target.active {
1783                    return;
1784                }
1785                target.prepare_for_render();
1786                let indices = self.targeted_or_all_indices(layer, target.renderers.len());
1787                for index in &indices {
1788                    if let Some(renderer) = target.renderers.get_mut(*index) {
1789                        renderer.renderer.clear();
1790                    }
1791                }
1792                self.mark_render_surfaces_dirty(layer, indices);
1793            }
1794            None => log::warn!(
1795                "tried to clear layer {} context for non-existent layer",
1796                layer
1797            ),
1798        }
1799    }
1800
1801    fn flush(&mut self, layer: usize, dirty_canvases: Rc<RefCell<Vec<bool>>>) {
1802        #[cfg(debug_assertions)]
1803        self.emit_tile_cull_stats(layer);
1804
1805        let mut flushed_targeted_batch = false;
1806        let mut backends = self.backends.borrow_mut();
1807        match backends.get_mut(layer) {
1808            Some(RenderLayerState::Pending) => {}
1809            Some(RenderLayerState::Failed) => {}
1810            Some(RenderLayerState::Ready((target, _))) => {
1811                if !target.active {
1812                    return;
1813                }
1814                if let Some(failed) = self.failed_context_gets.borrow_mut().get_mut(layer) {
1815                    if *failed {
1816                        if let Some(dirty_bit) = dirty_canvases.borrow_mut().get_mut(layer) {
1817                            // If we failed to draw to this layer because the context wasn't
1818                            // available yet, retry once the backend is ready.
1819                            *dirty_bit = true;
1820                        }
1821                        *failed = false;
1822                    }
1823                }
1824                flushed_targeted_batch = self.flush_targeted_or_dirty(layer, target);
1825            }
1826            None => log::warn!(
1827                "tried to flush layer {} context for non-existent layer",
1828                layer
1829            ),
1830        }
1831        drop(backends);
1832
1833        if flushed_targeted_batch && self.advance_targeted_replay_queue(layer) {
1834            self.replay_layers.borrow_mut().push(layer);
1835        }
1836    }
1837
1838    fn resize(&mut self, _width: usize, _height: usize) {
1839        let layer_count = self.backends.borrow().len();
1840        self.refresh_layer_layouts(0..layer_count);
1841    }
1842
1843    fn refresh_layers(&mut self, layers: &[usize]) {
1844        self.refresh_layer_layouts(layers.iter().copied());
1845    }
1846
1847    fn set_scene_lighting(&mut self, layer: usize, lighting: &SceneLighting) {
1848        let lighting = to_pax_gpu_scene_lighting(lighting);
1849        self.update_scene_lighting_cache(layer, &lighting);
1850        let mut backends = self.backends.borrow_mut();
1851        match backends.get_mut(layer) {
1852            Some(RenderLayerState::Pending) => {}
1853            Some(RenderLayerState::Failed) => {}
1854            Some(RenderLayerState::Ready((target, _))) => {
1855                if !target.active {
1856                    return;
1857                }
1858                for (index, renderer) in target.renderers.iter_mut().enumerate() {
1859                    if renderer
1860                        .renderer
1861                        .set_scene_lighting(translate_scene_lighting_for_surface(
1862                            &lighting,
1863                            renderer.origin_x,
1864                            renderer.origin_y,
1865                        ))
1866                    {
1867                        self.mark_render_surfaces_dirty(layer, [index]);
1868                    }
1869                }
1870            }
1871            None => log::warn!(
1872                "tried to set lighting for layer {} context for non-existent layer",
1873                layer
1874            ),
1875        }
1876    }
1877
1878    fn clear_targeted_replay(&mut self, layer: usize) {
1879        self.clear_targeted_replay_scope(layer);
1880    }
1881
1882    fn take_ready_canvas_layers(&mut self) -> Vec<usize> {
1883        let mut ready = Vec::new();
1884        // Native creation completes synchronously. Drain its additions before drawing; a stale
1885        // completion can trigger another creation, so bound retries to avoid monopolizing a frame.
1886        for _ in 0..MAX_CONCURRENT_LAYER_INITIALIZATIONS {
1887            let newly_ready = std::mem::take(&mut *self.ready_layers.borrow_mut());
1888            let mut changed = std::mem::take(&mut *self.changed_layers.borrow_mut());
1889            changed.extend(newly_ready.iter().copied());
1890            ready.extend(newly_ready);
1891            changed.sort_unstable();
1892            changed.dedup();
1893            if changed.is_empty() {
1894                break;
1895            }
1896            self.refresh_layer_layouts(changed);
1897        }
1898        ready.sort_unstable();
1899        ready.dedup();
1900        for layer in &ready {
1901            self.clear_targeted_replay_scope(*layer);
1902        }
1903        ready
1904    }
1905
1906    fn take_replay_canvas_layers(&mut self) -> Vec<usize> {
1907        self.take_replay_layer_ids()
1908    }
1909
1910    fn take_replay_canvas_layer_updates(&mut self) -> Vec<ReplayCanvasLayerUpdate> {
1911        self.take_replay_layer_ids()
1912            .into_iter()
1913            .map(|layer| ReplayCanvasLayerUpdate {
1914                layer,
1915                regions: self.replay_region_bounds(layer),
1916            })
1917            .collect()
1918    }
1919
1920    fn request_layer_screenshot(&mut self, layer: usize, request_id: u32) {
1921        self.with_layer_context(layer, |context| {
1922            context.request_screenshot_capture(request_id);
1923        });
1924        self.replay_layers.borrow_mut().push(layer);
1925    }
1926
1927    fn take_layer_screenshot(&mut self, layer: usize, request_id: u32) -> Option<ScreenshotData> {
1928        let mut screenshot = None;
1929        self.with_layer_context(layer, |context| {
1930            screenshot =
1931                context
1932                    .take_screenshot_capture(request_id)
1933                    .map(|capture| ScreenshotData {
1934                        id: request_id,
1935                        data: capture.rgba,
1936                        width: capture.width as usize,
1937                        height: capture.height as usize,
1938                    });
1939        });
1940        screenshot
1941    }
1942
1943    fn take_layer_surface_screenshots(
1944        &mut self,
1945        layer: usize,
1946        request_id: u32,
1947    ) -> Vec<LayerSurfaceScreenshotData> {
1948        let mut screenshots = Vec::new();
1949        let mut backends = self.backends.borrow_mut();
1950        let Some(RenderLayerState::Ready((target, _))) = backends.get_mut(layer) else {
1951            return screenshots;
1952        };
1953        if !target.active {
1954            return screenshots;
1955        }
1956
1957        for renderer in target.renderers_mut() {
1958            let Some(capture) = renderer.renderer_mut().take_screenshot_capture(request_id) else {
1959                continue;
1960            };
1961            screenshots.push(LayerSurfaceScreenshotData {
1962                id: request_id,
1963                key: renderer.key.clone(),
1964                data: capture.rgba,
1965                width: capture.width as usize,
1966                height: capture.height as usize,
1967                origin_x: renderer.origin_x,
1968                origin_y: renderer.origin_y,
1969                logical_width: renderer.logical_width,
1970                logical_height: renderer.logical_height,
1971            });
1972        }
1973        screenshots
1974    }
1975
1976    #[cfg(feature = "shaders")]
1977    fn begin_shader_effect(
1978        &mut self,
1979        layer: usize,
1980        id: u32,
1981        z: i32,
1982        effect: &pax_runtime_api::ShaderEffect,
1983        transform: kurbo::Affine,
1984        bounds: (f64, f64),
1985        scopes: &[pax_runtime_api::OpacityScope],
1986        children: &[u32],
1987    ) -> bool {
1988        let inherited = self
1989            .shader_effect_scopes
1990            .last()
1991            .map_or(0, |s| s.1)
1992            .min(scopes.len());
1993        let mut replay = false;
1994        let mut selected = Vec::new();
1995        {
1996            let mut backends = self.backends.borrow_mut();
1997            if let Some(RenderLayerState::Ready((target, _))) = backends.get_mut(layer) {
1998                if target.active {
1999                    target.prepare_for_render();
2000                    selected = self.active_render_scope(layer).unwrap_or_else(|| {
2001                        self.targeted_or_all_indices(layer, target.renderers.len())
2002                    });
2003                    for index in &selected {
2004                        replay |= target.renderers[*index].renderer.begin_shader_effect(
2005                            id,
2006                            z,
2007                            effect,
2008                            Transform2D::from_array(transform.as_coeffs().map(|v| v as f32)),
2009                            bounds,
2010                            &scopes[inherited..],
2011                            children,
2012                        );
2013                    }
2014                }
2015            }
2016        }
2017        self.mark_render_surfaces_dirty(layer, selected.iter().copied());
2018        self.push_render_scope(layer, selected);
2019        self.shader_effect_scopes.push((layer, scopes.len()));
2020        replay
2021    }
2022
2023    #[cfg(feature = "shaders")]
2024    fn end_shader_effect(&mut self, layer: usize, _id: u32) {
2025        self.with_layer_context(layer, |context| context.end_shader_effect());
2026        assert_eq!(self.shader_effect_scopes.pop().map(|s| s.0), Some(layer));
2027        self.pop_render_scope(layer);
2028    }
2029
2030    fn supports_alpha_source_capture(&self) -> bool {
2031        true
2032    }
2033
2034    fn alpha_source_layer(&self) -> Option<usize> {
2035        self.alpha_source_scopes.last().map(|&(layer, _)| layer)
2036    }
2037
2038    fn begin_alpha_source(
2039        &mut self,
2040        layer: usize,
2041        mapping: kurbo::Affine,
2042        inherited_scopes: usize,
2043        feather: f64,
2044    ) {
2045        self.with_layer_context(layer, |context| {
2046            context.begin_alpha_source(
2047                Transform2D::from_array(mapping.as_coeffs().map(|v| v as f32)),
2048                feather as f32,
2049            )
2050        });
2051        self.alpha_source_scopes.push((layer, inherited_scopes));
2052    }
2053
2054    fn end_alpha_source(&mut self, layer: usize) {
2055        self.with_layer_context(layer, |context| context.end_alpha_source());
2056        assert_eq!(self.alpha_source_scopes.pop().map(|s| s.0), Some(layer));
2057    }
2058
2059    fn supports_subtree_opacity(&self) -> bool {
2060        true
2061    }
2062
2063    fn set_node_opacity_scopes(
2064        &mut self,
2065        layer: usize,
2066        node_id: u32,
2067        scopes: &[pax_runtime_api::OpacityScope],
2068    ) {
2069        let inherited = self.alpha_source_scopes.last();
2070        #[cfg(feature = "shaders")]
2071        let inherited = inherited.or_else(|| self.shader_effect_scopes.last());
2072        let scopes = &scopes[inherited.map_or(0, |s| s.1).min(scopes.len())..];
2073        self.with_layer_context(layer, |context| {
2074            context.set_node_opacity_scopes(node_id, scopes)
2075        });
2076    }
2077
2078    fn begin_node(&mut self, layer: usize, node_id: u32, z_index: i32, light_mask: u32) -> bool {
2079        if self.is_capturing_source() {
2080            let mut began = false;
2081            self.with_layer_context(layer, |context| {
2082                began |= context.begin_node(node_id, z_index, light_mask);
2083            });
2084            if began {
2085                let indices = self
2086                    .active_render_scope(layer)
2087                    .expect("source has an owning mask scope");
2088                self.push_render_scope(layer, indices);
2089            }
2090            return began;
2091        }
2092        let mut backends = self.backends.borrow_mut();
2093        match backends.get_mut(layer) {
2094            Some(RenderLayerState::Pending) => {
2095                let mut failed_context_gets = self.failed_context_gets.borrow_mut();
2096                if failed_context_gets.len() <= layer {
2097                    failed_context_gets.resize(layer + 1, false);
2098                }
2099                failed_context_gets[layer] = true;
2100                false
2101            }
2102            Some(RenderLayerState::Failed) => false,
2103            Some(RenderLayerState::Ready((target, _))) => {
2104                if !target.active {
2105                    return false;
2106                }
2107                target.prepare_for_render();
2108                let candidate_indices = self.targeted_or_all_indices(layer, target.renderers.len());
2109                let mut selected = Vec::new();
2110                for index in candidate_indices {
2111                    let Some(renderer) = target.renderers.get_mut(index) else {
2112                        continue;
2113                    };
2114                    if renderer.renderer.begin_node(node_id, z_index, light_mask) {
2115                        selected.push(index);
2116                    }
2117                }
2118                let began = !selected.is_empty();
2119                if began {
2120                    self.mark_render_surfaces_dirty(layer, selected.iter().copied());
2121                    self.push_render_scope(layer, selected);
2122                }
2123                began
2124            }
2125            None => {
2126                log::warn!(
2127                    "tried to retrieve layer {} context for non-existent layer",
2128                    layer
2129                );
2130                false
2131            }
2132        }
2133    }
2134
2135    fn begin_node_with_bounds(
2136        &mut self,
2137        layer: usize,
2138        node_id: u32,
2139        z_index: i32,
2140        coverage_bounds: Rect,
2141        light_mask: u32,
2142    ) -> bool {
2143        if self.is_capturing_source() {
2144            // Feather can read beyond a tile's visible viewport; capture culling owns that margin.
2145            return self.begin_node(layer, node_id, z_index, light_mask);
2146        }
2147        let mut backends = self.backends.borrow_mut();
2148        match backends.get_mut(layer) {
2149            Some(RenderLayerState::Pending) => {
2150                let mut failed_context_gets = self.failed_context_gets.borrow_mut();
2151                if failed_context_gets.len() <= layer {
2152                    failed_context_gets.resize(layer + 1, false);
2153                }
2154                failed_context_gets[layer] = true;
2155                false
2156            }
2157            Some(RenderLayerState::Failed) => false,
2158            Some(RenderLayerState::Ready((target, _))) => {
2159                if !target.active {
2160                    return false;
2161                }
2162                target.prepare_for_render();
2163                let renderer_count = target.renderers.len();
2164                let candidate_indices = self.targeted_or_all_indices(layer, renderer_count);
2165                let candidate_indices: HashSet<_> = candidate_indices.into_iter().collect();
2166                #[cfg(debug_assertions)]
2167                self.update_tile_cull_stats(layer, |stats| {
2168                    stats.nodes_considered += 1;
2169                });
2170                let mut selected = Vec::new();
2171                let mut removed = Vec::new();
2172                #[cfg(debug_assertions)]
2173                let mut skipped_surfaces = 0u64;
2174                #[cfg(debug_assertions)]
2175                let mut stale_surface_removal_attempts = 0u64;
2176                for (index, renderer) in target.renderers.iter_mut().enumerate() {
2177                    let intersects = renderer.intersects_coverage_bounds(&coverage_bounds);
2178                    if !intersects {
2179                        #[cfg(debug_assertions)]
2180                        {
2181                            skipped_surfaces += 1;
2182                            stale_surface_removal_attempts += 1;
2183                        }
2184                        // If a dirty node moved out of this tile, skipping begin_node is not
2185                        // enough: the renderer may still retain that node from an earlier frame.
2186                        if renderer.renderer.remove_node(node_id) {
2187                            removed.push(index);
2188                        }
2189                        continue;
2190                    }
2191                    if !candidate_indices.contains(&index) {
2192                        #[cfg(debug_assertions)]
2193                        {
2194                            skipped_surfaces += 1;
2195                        }
2196                        continue;
2197                    }
2198                    if renderer.renderer.begin_node(node_id, z_index, light_mask) {
2199                        selected.push(index);
2200                    }
2201                }
2202                let began = !selected.is_empty();
2203                self.mark_render_surfaces_dirty(
2204                    layer,
2205                    selected.iter().chain(removed.iter()).copied(),
2206                );
2207                #[cfg(debug_assertions)]
2208                self.update_tile_cull_stats(layer, |stats| {
2209                    let selected_count = selected.len() as u64;
2210                    stats.selected_surfaces += selected_count;
2211                    stats.skipped_surfaces += skipped_surfaces;
2212                    stats.stale_surface_removal_attempts += stale_surface_removal_attempts;
2213                });
2214                if began {
2215                    self.push_render_scope(layer, selected);
2216                } else {
2217                    self.mark_clean_skipped_node(layer, node_id);
2218                }
2219                began
2220            }
2221            None => {
2222                log::warn!(
2223                    "tried to retrieve layer {} context for non-existent layer",
2224                    layer
2225                );
2226                false
2227            }
2228        }
2229    }
2230
2231    fn take_clean_skipped_node(&mut self, layer: usize, node_id: u32) -> bool {
2232        self.clean_skipped_canvas_nodes
2233            .borrow_mut()
2234            .remove(&(layer, node_id))
2235    }
2236
2237    fn end_node(&mut self, layer: usize, node_id: u32) -> bool {
2238        let mut ended = false;
2239        self.with_layer_context(layer, |context| {
2240            ended = context.end_node(node_id);
2241        });
2242        self.pop_render_scope(layer);
2243        ended
2244    }
2245
2246    fn remove_node(&mut self, layer: usize, node_id: u32) -> bool {
2247        let mut backends = self.backends.borrow_mut();
2248        match backends.get_mut(layer) {
2249            Some(RenderLayerState::Pending) => {
2250                let mut failed_context_gets = self.failed_context_gets.borrow_mut();
2251                if failed_context_gets.len() <= layer {
2252                    failed_context_gets.resize(layer + 1, false);
2253                }
2254                failed_context_gets[layer] = true;
2255                false
2256            }
2257            Some(RenderLayerState::Failed) => false,
2258            Some(RenderLayerState::Ready((target, _))) => {
2259                if !target.active {
2260                    return false;
2261                }
2262                target.prepare_for_render();
2263                let candidate_indices = 0..target.renderers.len();
2264                let mut removed_indices = Vec::new();
2265                for index in candidate_indices {
2266                    let Some(renderer) = target.renderers.get_mut(index) else {
2267                        continue;
2268                    };
2269                    if renderer.renderer.remove_node(node_id) {
2270                        removed_indices.push(index);
2271                    }
2272                }
2273                let removed = !removed_indices.is_empty();
2274                if removed {
2275                    self.mark_render_surfaces_dirty(layer, removed_indices);
2276                }
2277                true
2278            }
2279            None => {
2280                log::warn!(
2281                    "tried to remove node from layer {} context for non-existent layer",
2282                    layer
2283                );
2284                false
2285            }
2286        }
2287    }
2288}
2289
2290fn to_pax_gpu_fill(
2291    fill: &pax_runtime_api::Paint,
2292    rect: kurbo::Rect,
2293    transform: pax_gpu::Transform2D,
2294) -> pax_gpu::Fill {
2295    let Some(rect) = pax_runtime_api::drawing::paint_bounds(rect) else {
2296        return pax_gpu::Fill::Solid(pax_gpu::Color::rgba(0.0, 0.0, 0.0, 0.0));
2297    };
2298    let bounds = (rect.width(), rect.height());
2299    let orig = rect.origin();
2300    match fill {
2301        pax_runtime_api::Paint::MeshGradient(mesh) => {
2302            if let Err(error) = mesh.validate() {
2303                thread_local! { static REPORTED: std::cell::RefCell<std::collections::HashSet<String>> = std::cell::RefCell::new(std::collections::HashSet::new()); }
2304                REPORTED.with(|reported| {
2305                    if reported.borrow_mut().insert(error.clone()) {
2306                        log::warn!("{error}; mesh paints no field");
2307                    }
2308                });
2309                return pax_gpu::Fill::Solid(pax_gpu::Color::rgba(0.0, 0.0, 0.0, 0.0));
2310            }
2311            let pos = transform.transform_point(point(orig.x as f32, orig.y as f32));
2312            let main_axis =
2313                transform.transform_vector(pax_gpu::Vector2D::new(bounds.0 as f32, 0.0));
2314            let off_axis = transform.transform_vector(pax_gpu::Vector2D::new(0.0, bounds.1 as f32));
2315            let determinant = main_axis.cross(off_axis);
2316            if ![
2317                pos.x,
2318                pos.y,
2319                main_axis.x,
2320                main_axis.y,
2321                off_axis.x,
2322                off_axis.y,
2323                determinant,
2324            ]
2325            .iter()
2326            .all(|v| v.is_finite())
2327                || determinant.abs() < 1e-6
2328            {
2329                return pax_gpu::Fill::Solid(pax_gpu::Color::rgba(0.0, 0.0, 0.0, 0.0));
2330            }
2331            pax_gpu::Fill::Mesh {
2332                rows: mesh
2333                    .rows
2334                    .iter()
2335                    .map(|row| {
2336                        row.iter()
2337                            .map(|p| {
2338                                (
2339                                    [
2340                                        (p.position.0.evaluate(bounds, Axis::X) / bounds.0) as f32,
2341                                        (p.position.1.evaluate(bounds, Axis::Y) / bounds.1) as f32,
2342                                    ],
2343                                    to_pax_gpu_color(&p.color),
2344                                )
2345                            })
2346                            .collect()
2347                    })
2348                    .collect(),
2349                pos,
2350                main_axis,
2351                off_axis,
2352            }
2353        }
2354        pax_runtime_api::Paint::Blend(terms) => pax_gpu::Fill::Blend(
2355            terms
2356                .iter()
2357                .map(|(fill, weight)| (to_pax_gpu_fill(fill, rect, transform), *weight as f32))
2358                .collect(),
2359        ),
2360        pax_runtime_api::Paint::Solid(color) => pax_gpu::Fill::Solid(to_pax_gpu_color(color)),
2361        pax_runtime_api::Paint::LinearGradient(gradient) => {
2362            let start_x = gradient.start.0.evaluate(bounds, Axis::X);
2363            let start_y = gradient.start.1.evaluate(bounds, Axis::Y);
2364            let end_x = gradient.end.0.evaluate(bounds, Axis::X);
2365            let end_y = gradient.end.1.evaluate(bounds, Axis::Y);
2366            let local_pos =
2367                pax_gpu::Point2D::new((orig.x + start_x) as f32, (orig.y + start_y) as f32);
2368            let local_end = pax_gpu::Point2D::new((orig.x + end_x) as f32, (orig.y + end_y) as f32);
2369            let world_pos = transform.transform_point(local_pos);
2370            let world_end = transform.transform_point(local_end);
2371            let main_axis = world_end - world_pos;
2372            let local_axis = local_end - local_pos;
2373            let local_length = local_axis.length();
2374            let off_axis =
2375                transform.transform_vector(pax_gpu::Vector2D::new(-local_axis.y, local_axis.x));
2376            if local_length <= f32::EPSILON
2377                || !local_length.is_finite()
2378                || !main_axis.length().is_finite()
2379            {
2380                return pax_gpu::Fill::Solid(pax_gpu::Color::rgba(0.0, 0.0, 0.0, 0.0));
2381            }
2382            pax_gpu::Fill::Gradient {
2383                stops: gradient
2384                    .stops
2385                    .iter()
2386                    .map(|g| pax_gpu::GradientStop {
2387                        color: to_pax_gpu_color(&g.color),
2388                        stop: g.position.evaluate((local_length as f64, 0.0), Axis::X) as f32
2389                            / local_length
2390                            * main_axis.length(),
2391                    })
2392                    .collect(),
2393                gradient_type: pax_gpu::GradientType::Linear,
2394                pos: world_pos,
2395                main_axis,
2396                off_axis,
2397            }
2398        }
2399        pax_runtime_api::Paint::RadialGradient(gradient) => {
2400            let Some(g) = gradient.resolve_geometry(rect) else {
2401                return pax_gpu::Fill::Solid(pax_gpu::Color::rgba(0.0, 0.0, 0.0, 0.0));
2402            };
2403            let center = point(g.center.x as f32, g.center.y as f32);
2404            let pos = transform.transform_point(center);
2405            let main_axis =
2406                transform.transform_vector(pax_gpu::Vector2D::new(g.radius as f32, 0.0));
2407            let off_axis = transform.transform_vector(pax_gpu::Vector2D::new(0.0, g.radius as f32));
2408            let focal_point = point(
2409                ((g.focal_point.x - g.center.x) / g.radius) as f32,
2410                ((g.focal_point.y - g.center.y) / g.radius) as f32,
2411            );
2412            // Check after f64 -> f32 conversion too; GPU paint must remain finite.
2413            if ![
2414                pos.x,
2415                pos.y,
2416                main_axis.x,
2417                main_axis.y,
2418                off_axis.x,
2419                off_axis.y,
2420                focal_point.x,
2421                focal_point.y,
2422            ]
2423            .into_iter()
2424            .all(f32::is_finite)
2425            {
2426                return pax_gpu::Fill::Solid(pax_gpu::Color::rgba(0.0, 0.0, 0.0, 0.0));
2427            }
2428            pax_gpu::Fill::Gradient {
2429                gradient_type: pax_gpu::GradientType::Radial { focal_point },
2430                pos,
2431                main_axis,
2432                off_axis,
2433                stops: gradient
2434                    .stops
2435                    .iter()
2436                    .map(|s| pax_gpu::GradientStop {
2437                        color: to_pax_gpu_color(&s.color),
2438                        stop: (s.position.evaluate((g.radius, 0.0), Axis::X) / g.radius) as f32,
2439                    })
2440                    .collect(),
2441            }
2442        }
2443    }
2444}
2445
2446fn to_pax_gpu_alpha_fill(
2447    fill: &pax_runtime_api::Paint,
2448    rect: Rect,
2449    transform: Transform2D,
2450) -> pax_gpu::Fill {
2451    to_pax_gpu_fill(fill, rect, transform)
2452}
2453
2454fn to_pax_gpu_material(material: &pax_runtime_api::ResolvedMaterial) -> PixelMaterial {
2455    match material {
2456        pax_runtime_api::ResolvedMaterial::Unlit => PixelMaterial::unlit(),
2457        pax_runtime_api::ResolvedMaterial::Lit(params) => {
2458            let metallic = params.metallic.clamp(0.0, 1.0) as f32;
2459            let diffuse = params.diffuse.max(0.0) as f32 * (1.0 - metallic * 0.55);
2460            let specular = params.specular.max(0.0) as f32 + metallic * 0.45;
2461            let roughness =
2462                (params.roughness.clamp(0.0, 1.0) as f32 * (1.0 - metallic * 0.35)).clamp(0.0, 1.0);
2463            PixelMaterial::custom(
2464                params.ambient.max(0.0) as f32,
2465                diffuse,
2466                specular,
2467                roughness,
2468                to_pax_gpu_color(&params.emissive.clone()),
2469                params.emissive_intensity.max(0.0) as f32,
2470            )
2471        }
2472    }
2473}
2474
2475fn to_pax_gpu_scene_lighting(lighting: &SceneLighting) -> PixelSceneLighting {
2476    PixelSceneLighting {
2477        active: lighting.active,
2478        ambient_is_authored: lighting.ambient_is_authored,
2479        ambient_color: to_pax_gpu_color(&lighting.ambient.color),
2480        ambient_intensity: lighting.ambient.intensity.max(0.0) as f32,
2481        lights: lighting
2482            .lights
2483            .iter()
2484            .map(|light| PixelSceneLight {
2485                shape: match light.shape {
2486                    pax_runtime_api::LightShape::Point => PixelLightShape::Point,
2487                    pax_runtime_api::LightShape::Directional => PixelLightShape::Directional,
2488                },
2489                position: [
2490                    light.position.x as f32,
2491                    light.position.y as f32,
2492                    light.position.z as f32,
2493                ],
2494                direction: [
2495                    light.direction.x as f32,
2496                    light.direction.y as f32,
2497                    light.direction.z as f32,
2498                ],
2499                color: to_pax_gpu_color(&light.color),
2500                intensity: light.intensity.max(0.0) as f32,
2501                radius: light.radius.max(0.0) as f32,
2502            })
2503            .collect(),
2504    }
2505}
2506
2507fn translate_scene_lighting_for_surface(
2508    lighting: &PixelSceneLighting,
2509    origin_x: f32,
2510    origin_y: f32,
2511) -> PixelSceneLighting {
2512    let mut lighting = lighting.clone();
2513    for light in &mut lighting.lights {
2514        if matches!(light.shape, PixelLightShape::Point) {
2515            light.position[0] -= origin_x;
2516            light.position[1] -= origin_y;
2517        }
2518    }
2519    lighting
2520}
2521
2522/// Convert a runtime API color into the `pax-gpu` render-context color.
2523pub fn to_pax_gpu_color(color: &pax_runtime_api::Color) -> pax_gpu::Color {
2524    let [r, g, b, a] = color.to_rgba_0_1();
2525    pax_gpu::Color::rgba(r as f32, g as f32, b as f32, a as f32)
2526}
2527
2528/// Convert a kurbo path emitted by primitives into a lyon path consumed by `pax-gpu`.
2529pub fn convert_kurbo_to_lyon_path(kurbo_path: &BezPath) -> Path {
2530    let mut builder = Path::builder();
2531    let mut has_open_subpath = false;
2532    for el in kurbo_path.elements() {
2533        match el {
2534            PathEl::MoveTo(p) => {
2535                if has_open_subpath {
2536                    builder.end(false);
2537                }
2538                builder.begin(point(p.x as f32, p.y as f32));
2539                has_open_subpath = true;
2540            }
2541            PathEl::LineTo(p) => {
2542                if has_open_subpath {
2543                    builder.line_to(point(p.x as f32, p.y as f32));
2544                }
2545            }
2546            PathEl::QuadTo(p1, p2) => {
2547                if has_open_subpath {
2548                    builder.quadratic_bezier_to(
2549                        point(p1.x as f32, p1.y as f32),
2550                        point(p2.x as f32, p2.y as f32),
2551                    );
2552                }
2553            }
2554            PathEl::CurveTo(p1, p2, p3) => {
2555                if has_open_subpath {
2556                    builder.cubic_bezier_to(
2557                        point(p1.x as f32, p1.y as f32),
2558                        point(p2.x as f32, p2.y as f32),
2559                        point(p3.x as f32, p3.y as f32),
2560                    );
2561                }
2562            }
2563            PathEl::ClosePath => {
2564                if has_open_subpath {
2565                    builder.end(true);
2566                    has_open_subpath = false;
2567                }
2568            }
2569        }
2570    }
2571    if has_open_subpath {
2572        builder.end(false);
2573    }
2574
2575    builder.build()
2576}
2577
2578#[cfg(test)]
2579mod tests {
2580    use super::*;
2581
2582    #[test]
2583    fn surface_eviction_keeps_survivors_in_host_order() {
2584        let existing = [("a", "row"), ("b", "row"), ("c", "row")];
2585        assert_eq!(
2586            retained_surface_order(&existing, &[("c", "row"), ("a", "row")]),
2587            Some(vec![2, 0])
2588        );
2589        assert_eq!(retained_surface_order(&existing, &[]), Some(vec![]));
2590        assert_eq!(
2591            retained_surface_order(&existing, &existing),
2592            Some(vec![0, 1, 2])
2593        );
2594    }
2595
2596    #[test]
2597    fn new_or_rebound_surface_cannot_reuse_an_old_backend() {
2598        let existing = [("a", "row"), ("b", "row")];
2599        assert_eq!(
2600            retained_surface_order(&existing, &[("a", "row"), ("c", "row")]),
2601            Some(vec![0])
2602        );
2603        assert_eq!(
2604            retained_surface_order(&existing, &[("a", "new-host")]),
2605            Some(vec![])
2606        );
2607        assert_eq!(
2608            retained_surface_order(&existing, &[("a", "row"), ("a", "row")]),
2609            None
2610        );
2611    }
2612
2613    #[test]
2614    fn surface_eviction_releases_only_removed_resources() {
2615        struct Resource(Rc<Cell<usize>>);
2616        impl Drop for Resource {
2617            fn drop(&mut self) {
2618                self.0.set(self.0.get() + 1);
2619            }
2620        }
2621        let drops = Rc::new(Cell::new(0));
2622        let mut surfaces: Vec<_> = (0..3)
2623            .map(|_| Box::new(Resource(Rc::clone(&drops))))
2624            .collect();
2625        let first = surfaces[0].as_ref() as *const Resource;
2626        let last = surfaces[2].as_ref() as *const Resource;
2627        let order = retained_surface_order(
2628            &[("a", "row"), ("b", "row"), ("c", "row")],
2629            &[("c", "row"), ("a", "row")],
2630        )
2631        .unwrap();
2632        retain_surfaces(&mut surfaces, order);
2633        assert_eq!(drops.get(), 1);
2634        assert_eq!(surfaces[0].as_ref() as *const Resource, last);
2635        assert_eq!(surfaces[1].as_ref() as *const Resource, first);
2636        retain_surfaces(&mut surfaces, vec![]);
2637        assert_eq!(drops.get(), 3);
2638        assert!(surfaces.is_empty());
2639    }
2640
2641    #[test]
2642    fn stale_initialization_cannot_publish_into_reused_layer_slot() {
2643        let states = Rc::new(RefCell::new(vec![RenderLayerState::Pending]));
2644        let first = Rc::new(LayerInitialization::new(0));
2645        let replacement = Rc::new(LayerInitialization::new(0));
2646        first.scheduled.set(true);
2647        replacement.scheduled.set(true);
2648        let queue = Rc::new(RefCell::new(VecDeque::from([Rc::clone(&first)])));
2649        let ready = Rc::new(RefCell::new(Vec::new()));
2650        let changed = Rc::new(RefCell::new(Vec::new()));
2651        let calls = Rc::new(Cell::new(0));
2652        let factory: Rc<LayerFactory> = Rc::new({
2653            let first = Rc::clone(&first);
2654            let replacement = Rc::clone(&replacement);
2655            let queue = Rc::clone(&queue);
2656            let calls = Rc::clone(&calls);
2657            let states = Rc::clone(&states);
2658            let ready = Rc::clone(&ready);
2659            move |_, _| {
2660                let first = Rc::clone(&first);
2661                let replacement = Rc::clone(&replacement);
2662                let queue = Rc::clone(&queue);
2663                let calls = Rc::clone(&calls);
2664                let states = Rc::clone(&states);
2665                let ready = Rc::clone(&ready);
2666                Box::pin(async move {
2667                    calls.set(calls.get() + 1);
2668                    if calls.get() == 1 {
2669                        // Removal/recreation during await cancels the old token
2670                        // and queues a distinct request for the same numeric slot.
2671                        first.cancelled.set(true);
2672                        queue.borrow_mut().push_back(replacement);
2673                    } else {
2674                        assert!(matches!(states.borrow()[0], RenderLayerState::Pending));
2675                        assert!(ready.borrow().is_empty(), "stale result was published");
2676                    }
2677                    Some(empty_layer(true, true))
2678                })
2679            }
2680        });
2681        let in_flight = Rc::new(Cell::new(0));
2682        pump_layer_initialization_queue(
2683            factory,
2684            Rc::clone(&states),
2685            Rc::clone(&queue),
2686            Rc::clone(&in_flight),
2687            Rc::clone(&ready),
2688            changed,
2689            Rc::new(|_| 0),
2690        );
2691        assert_eq!(calls.get(), 2);
2692        assert_eq!(*ready.borrow(), vec![0]);
2693        assert!(matches!(&states.borrow()[0], RenderLayerState::Ready(_)));
2694        assert!(!first.scheduled.get());
2695        assert!(!replacement.scheduled.get());
2696        assert!(queue.borrow().is_empty());
2697        assert_eq!(in_flight.get(), 0);
2698    }
2699
2700    #[test]
2701    fn initialization_priority_tracks_visibility_and_keeps_fifo_ties() {
2702        let queue = RefCell::new(VecDeque::from_iter(
2703            (0..5).map(|index| Rc::new(LayerInitialization::new(index))),
2704        ));
2705        queue.borrow()[2].cancelled.set(true);
2706        let visible = Cell::new(3);
2707        let priority = |layer| if layer == visible.get() { 0 } else { 1 };
2708        assert_eq!(
2709            pop_next_layer_initialization(&queue, &priority)
2710                .unwrap()
2711                .index,
2712            3
2713        );
2714        visible.set(4);
2715        assert_eq!(
2716            pop_next_layer_initialization(&queue, &priority)
2717                .unwrap()
2718                .index,
2719            4
2720        );
2721        assert_eq!(
2722            pop_next_layer_initialization(&queue, &priority)
2723                .unwrap()
2724                .index,
2725            0
2726        );
2727        assert_eq!(
2728            pop_next_layer_initialization(&queue, &priority)
2729                .unwrap()
2730                .index,
2731            1
2732        );
2733        assert!(pop_next_layer_initialization(&queue, &priority).is_none());
2734    }
2735
2736    #[test]
2737    fn failed_addition_preserves_ready_layer_and_records_retry_layout() {
2738        let mut target = LayerTarget::new(vec![], true);
2739        target.needs_replay = true;
2740        let layout = LayerSurfaceLayout {
2741            surfaces: vec![],
2742            active: true,
2743        };
2744        let states = Rc::new(RefCell::new(vec![RenderLayerState::Ready((
2745            target,
2746            Box::pin(|| LayerSurfaceLayout {
2747                surfaces: vec![],
2748                active: true,
2749            }),
2750        ))]));
2751        let ready = Rc::new(RefCell::new(Vec::new()));
2752        pump_layer_initialization_queue(
2753            Rc::new(|_, _| Box::pin(async { None })),
2754            Rc::clone(&states),
2755            Rc::new(RefCell::new(VecDeque::from([Rc::new(
2756                LayerInitialization::new(0),
2757            )]))),
2758            Rc::new(Cell::new(0)),
2759            Rc::clone(&ready),
2760            Rc::new(RefCell::new(Vec::new())),
2761            Rc::new(|_| 0),
2762        );
2763        let states = states.borrow();
2764        let RenderLayerState::Ready((target, _)) = &states[0] else {
2765            panic!("survivor lost");
2766        };
2767        assert!(target.needs_replay);
2768        assert_eq!(target.failed_layout, Some(layout));
2769        assert!(ready.borrow().is_empty());
2770    }
2771
2772    #[test]
2773    fn mesh_paint_resolves_full_bounds_origin_combined_units_and_affine_axes() {
2774        use pax_runtime_api::{Color, MeshGradient, MeshPoint, Paint, Size};
2775        let paint = Paint::MeshGradient(MeshGradient {
2776            rows: vec![
2777                vec![
2778                    MeshPoint {
2779                        position: (
2780                            Size::Combined(5.into(), 50.into()),
2781                            Size::Percent(100.into())
2782                        ),
2783                        color: Color::CYAN,
2784                    };
2785                    2
2786                ];
2787                2
2788            ],
2789        });
2790        let transform = pax_gpu::Transform2D::new(2.0, 0.5, 1.0, 3.0, 4.0, 7.0);
2791        let rect = kurbo::Rect::new(10.0, 20.0, 110.0, 70.0);
2792        let pax_gpu::Fill::Mesh {
2793            rows,
2794            pos,
2795            main_axis,
2796            off_axis,
2797        } = to_pax_gpu_fill(&paint, rect, transform)
2798        else {
2799            panic!("mesh")
2800        };
2801        assert_eq!(pos, point(44.0, 72.0));
2802        assert_eq!(main_axis, pax_gpu::Vector2D::new(200.0, 50.0));
2803        assert_eq!(off_axis, pax_gpu::Vector2D::new(50.0, 150.0));
2804        assert_eq!(rows[0][0].0, [0.55, 1.0]);
2805        let pax_gpu::Fill::Mesh {
2806            rows, main_axis, ..
2807        } = to_pax_gpu_fill(
2808            &paint,
2809            kurbo::Rect::new(10.0, 20.0, 10.0, 70.0),
2810            pax_gpu::Transform2D::identity(),
2811        )
2812        else {
2813            panic!("canonical zero axis")
2814        };
2815        assert_eq!(main_axis.x, 1.0);
2816        assert_eq!(rows[0][0].0[0], 5.5);
2817        assert!(matches!(
2818            to_pax_gpu_fill(&paint, rect, pax_gpu::Transform2D::scale(0.0, 1.0)),
2819            pax_gpu::Fill::Solid(_)
2820        ));
2821    }
2822
2823    fn empty_layer(active: bool, current_active: bool) -> LayerDef {
2824        (
2825            LayerTarget::new(Vec::new(), active),
2826            Box::pin(move || LayerSurfaceLayout {
2827                active: current_active,
2828                surfaces: Vec::new(),
2829            }),
2830        )
2831    }
2832
2833    #[test]
2834    fn removed_layer_initializations_cannot_publish_into_reused_indices() {
2835        let mut renderer = PaxGpuRenderer::new(|_, _| panic!("initialization stays queued"));
2836        renderer
2837            .layer_initializations_in_flight
2838            .set(MAX_CONCURRENT_LAYER_INITIALIZATIONS);
2839        let dirty = Rc::new(RefCell::new(Vec::new()));
2840        renderer.resize_layers_to(2, Rc::clone(&dirty));
2841        let removed = Rc::clone(&renderer.layer_initializations[1]);
2842        renderer.ready_layers.borrow_mut().push(1);
2843        renderer.changed_layers.borrow_mut().push(1);
2844        renderer.replay_layers.borrow_mut().push(1);
2845        renderer.resize_layers_to(1, Rc::clone(&dirty));
2846        assert!(removed.cancelled.get());
2847        assert_eq!(renderer.pending_layer_initializations.borrow().len(), 1);
2848        assert!(renderer.ready_layers.borrow().is_empty());
2849        assert!(renderer.replay_layers.borrow().is_empty());
2850        assert!(renderer.changed_layers.borrow().is_empty());
2851        assert!(!removed.complete(
2852            Some(empty_layer(true, true)),
2853            None,
2854            &renderer.backends,
2855            &renderer.ready_layers,
2856            &renderer.changed_layers
2857        ));
2858
2859        renderer.resize_layers_to(2, dirty);
2860        let replacement = &renderer.layer_initializations[1];
2861        assert!(!Rc::ptr_eq(&removed, replacement));
2862        for stale_result in [Some(empty_layer(true, true)), None] {
2863            assert!(!removed.complete(
2864                stale_result,
2865                None,
2866                &renderer.backends,
2867                &renderer.ready_layers,
2868                &renderer.changed_layers
2869            ));
2870            assert!(matches!(
2871                renderer.backends.borrow()[1],
2872                RenderLayerState::Pending
2873            ));
2874            assert!(replacement.scheduled.get());
2875            assert!(renderer.ready_layers.borrow().is_empty());
2876        }
2877        assert!(!replacement.complete(
2878            Some(empty_layer(true, true)),
2879            None,
2880            &renderer.backends,
2881            &renderer.ready_layers,
2882            &renderer.changed_layers
2883        ));
2884        assert!(matches!(
2885            renderer.backends.borrow()[1],
2886            RenderLayerState::Ready(_)
2887        ));
2888        assert_eq!(*renderer.ready_layers.borrow(), vec![1]);
2889    }
2890
2891    #[test]
2892    fn live_layer_initialization_retries_changed_hosts_and_reports_failure() {
2893        let initialization = LayerInitialization::new(0);
2894        let backends = RefCell::new(vec![RenderLayerState::Pending]);
2895        let ready = RefCell::new(Vec::new());
2896        assert!(initialization.complete(
2897            Some(empty_layer(true, false)),
2898            None,
2899            &backends,
2900            &ready,
2901            &RefCell::new(Vec::new())
2902        ));
2903        assert!(matches!(backends.borrow()[0], RenderLayerState::Pending));
2904        assert!(ready.borrow().is_empty());
2905        assert!(!initialization.complete(None, None, &backends, &ready, &RefCell::new(Vec::new())));
2906        assert!(matches!(backends.borrow()[0], RenderLayerState::Failed));
2907    }
2908
2909    #[test]
2910    fn alpha_radial_gradient_retains_radius_with_coincident_endpoints() {
2911        use pax_runtime_api::{Paint, RadialGradient, Size};
2912        let fill = Paint::RadialGradient(RadialGradient {
2913            start: (Size::Percent(50.into()), Size::Percent(50.into())),
2914            end: (Size::Percent(50.into()), Size::Percent(50.into())),
2915            radius: 25.0,
2916            stops: vec![],
2917        });
2918        let pax_gpu::Fill::Gradient {
2919            pos,
2920            main_axis,
2921            off_axis,
2922            ..
2923        } = to_pax_gpu_alpha_fill(
2924            &fill,
2925            Rect::new(0.0, 0.0, 100.0, 100.0),
2926            Transform2D::new(2.0, 0.0, 1.0, 3.0, 0.0, 0.0),
2927        )
2928        else {
2929            panic!("expected a radial gradient")
2930        };
2931        assert_eq!(pos, point(150.0, 150.0));
2932        assert_eq!(main_axis, pax_gpu::Vector2D::new(50.0, 0.0));
2933        assert_eq!(off_axis, pax_gpu::Vector2D::new(25.0, 75.0));
2934    }
2935
2936    #[test]
2937    fn radial_color_and_mask_share_focal_geometry_and_local_pixel_stops() {
2938        use pax_runtime_api::{Color, GradientStop, Paint, RadialGradient, Size};
2939        let fill = Paint::RadialGradient(RadialGradient {
2940            start: (Size::Percent(75.into()), Size::Percent(25.into())),
2941            end: (Size::Percent(50.into()), Size::Percent(50.into())),
2942            radius: 50.0,
2943            stops: vec![
2944                GradientStop::get(Color::BLACK, Size::Pixels(25.into())),
2945                GradientStop::get(Color::WHITE, Size::Percent(100.into())),
2946            ],
2947        });
2948        let rect = Rect::new(10.0, 20.0, 210.0, 120.0);
2949        let transform = Transform2D::new(-2.0, 0.0, 1.0, 3.0, 5.0, 7.0);
2950        for paint in [
2951            to_pax_gpu_fill(&fill, rect, transform),
2952            to_pax_gpu_alpha_fill(&fill, rect, transform),
2953        ] {
2954            let pax_gpu::Fill::Gradient {
2955                gradient_type,
2956                pos,
2957                main_axis,
2958                off_axis,
2959                stops,
2960            } = paint
2961            else {
2962                panic!("expected radial paint")
2963            };
2964            let pax_gpu::GradientType::Radial { focal_point } = gradient_type else {
2965                panic!("radial")
2966            };
2967            assert_eq!(focal_point, point(1.0, -0.5));
2968            assert_eq!(pos, point(-145.0, 217.0));
2969            assert_eq!(main_axis, pax_gpu::Vector2D::new(-100.0, 0.0));
2970            assert_eq!(off_axis, pax_gpu::Vector2D::new(50.0, 150.0));
2971            assert_eq!(
2972                stops.iter().map(|s| s.stop).collect::<Vec<_>>(),
2973                vec![0.5, 1.0]
2974            );
2975        }
2976    }
2977
2978    #[test]
2979    fn alpha_linear_gradient_retains_sheared_basis() {
2980        use pax_runtime_api::{LinearGradient, Paint, Size};
2981        let fill = Paint::LinearGradient(LinearGradient {
2982            start: (Size::Percent(0.into()), Size::Percent(0.into())),
2983            end: (Size::Percent(100.into()), Size::Percent(0.into())),
2984            stops: vec![],
2985        });
2986        let pax_gpu::Fill::Gradient {
2987            main_axis,
2988            off_axis,
2989            ..
2990        } = to_pax_gpu_alpha_fill(
2991            &fill,
2992            Rect::new(0.0, 0.0, 100.0, 100.0),
2993            Transform2D::new(1.0, 0.0, 0.5, 1.0, 0.0, 0.0),
2994        )
2995        else {
2996            panic!("expected a linear gradient")
2997        };
2998        assert_eq!(main_axis, pax_gpu::Vector2D::new(100.0, 0.0));
2999        assert_eq!(off_axis, pax_gpu::Vector2D::new(50.0, 100.0));
3000    }
3001
3002    #[test]
3003    fn surface_intersection_includes_edges() {
3004        let bounds = Rect::new(100.0, 100.0, 200.0, 200.0);
3005
3006        assert!(surface_intersects_coverage_bounds(
3007            &bounds, 200.0, 100.0, 100.0, 100.0
3008        ));
3009        assert!(surface_intersects_coverage_bounds(
3010            &bounds, 0.0, 0.0, 100.0, 100.0
3011        ));
3012    }
3013
3014    #[test]
3015    fn surface_intersection_rejects_disjoint_surfaces() {
3016        let bounds = Rect::new(100.0, 100.0, 200.0, 200.0);
3017
3018        assert!(!surface_intersects_coverage_bounds(
3019            &bounds, 201.0, 100.0, 100.0, 100.0
3020        ));
3021        assert!(!surface_intersects_coverage_bounds(
3022            &bounds, 100.0, 201.0, 100.0, 100.0
3023        ));
3024    }
3025
3026    #[test]
3027    fn surface_intersection_keeps_non_finite_bounds_conservative() {
3028        let bounds = Rect::new(f64::NAN, 0.0, 100.0, 100.0);
3029
3030        assert!(surface_intersects_coverage_bounds(
3031            &bounds, 500.0, 500.0, 100.0, 100.0
3032        ));
3033    }
3034
3035    #[test]
3036    fn converts_multiple_kurbo_subpaths_to_lyon_path() {
3037        let mut path = BezPath::new();
3038        path.move_to((0.0, 0.0));
3039        path.line_to((10.0, 0.0));
3040        path.move_to((20.0, 0.0));
3041        path.line_to((30.0, 0.0));
3042        path.close_path();
3043
3044        let lyon_path = convert_kurbo_to_lyon_path(&path);
3045
3046        assert_eq!(lyon_path.iter().count(), 6);
3047    }
3048}
3049
3050#[cfg(all(test, target_os = "macos"))]
3051#[path = "pax_gpu_tile_tests.rs"]
3052mod tile_tests;