Skip to main content

pax_gpu/
render_context.rs

1#[cfg(feature = "shaders")]
2mod effects;
3#[cfg(feature = "shaders")]
4use effects::{EffectCapture, EffectNode, EffectRecording};
5mod lighting;
6mod opacity;
7mod source_capture;
8use crate::render_backend::stencil;
9use crate::render_backend::CpuBuffers;
10use crate::render_backend::RetainedImageDraw;
11use crate::render_backend::RetainedVectorResource;
12use crate::render_backend::SharedRetainedVectorResource;
13use crate::render_backend::MAX_BATCH_COLORS;
14use crate::render_backend::MAX_BATCH_GRADIENTS;
15use crate::render_backend::MAX_BATCH_MATERIALS;
16use crate::render_backend::MAX_BATCH_PRIMITIVES;
17use crate::render_backend::MAX_BATCH_TRANSFORMS;
18use crate::render_backend::{RetainedBatchRun, RetainedDraw};
19use crate::Box2D;
20use crate::Image;
21use crate::Point2D;
22use crate::Transform2D;
23use crate::Vector2D;
24use lighting::LightingDependencies;
25use lyon::geom::{CubicBezierSegment, QuadraticBezierSegment, Segment};
26use lyon::lyon_tessellation::BuffersBuilder;
27use lyon::lyon_tessellation::FillOptions;
28use lyon::lyon_tessellation::FillRule;
29use lyon::lyon_tessellation::FillTessellator;
30use lyon::lyon_tessellation::FillVertex;
31use lyon::lyon_tessellation::VertexBuffers;
32use lyon::math::Point as LyonPoint;
33use lyon::path::EndpointId;
34use lyon::path::Path;
35use lyon::path::PathEvent;
36use lyon::tessellation::StrokeOptions;
37use lyon::tessellation::StrokeTessellator;
38use lyon::tessellation::StrokeVertex;
39use lyon::tessellation::VertexSource;
40use opacity::collect_opacity_group_keys;
41use pax_runtime_api::{OpacityScope, PathSmoothing};
42use source_capture::{SourceCapture, SourceRecording};
43
44use crate::point;
45use crate::render_backend::data::GpuColor;
46use crate::render_backend::data::GpuGradient;
47use crate::render_backend::data::GpuMaterial;
48use crate::render_backend::data::GpuPrimitive;
49use crate::render_backend::data::GpuSceneLight;
50use crate::render_backend::data::GpuSceneLighting;
51use crate::render_backend::data::GpuTransform;
52use crate::render_backend::data::GpuVertex;
53use crate::render_backend::data::MAX_SCENE_LIGHTS;
54use crate::render_backend::CachedTextureResource;
55use crate::render_backend::CapturedFrame;
56use crate::render_backend::PrimitiveBatch;
57use crate::render_backend::PrimitiveBatchSegment;
58use crate::render_backend::RenderBackend;
59use crate::render_backend::ScissorRect;
60use crate::render_backend::VectorResourceDirty;
61use std::cell::RefCell;
62use std::collections::hash_map::DefaultHasher;
63use std::collections::{HashMap, HashSet};
64use std::hash::{Hash, Hasher};
65use std::rc::Rc;
66
67const DEFAULT_TESSELLATION_TOLERANCE: f32 = 0.1;
68
69/// Vector paint used to construct a surface-local alpha mask.
70pub struct AlphaMaskPaint {
71    pub path: Path,
72    pub transform: Transform2D,
73    pub fill: Fill,
74    pub opacity: f32,
75    pub composition: Option<(u32, f32)>,
76}
77const MAX_VECTOR_GEOMETRY_CACHE_BYTES: usize = 16 * 1024 * 1024;
78const MAX_VECTOR_RESOURCE_CACHE_BYTES: usize = 128 * 1024 * 1024;
79pub const NATIVE_VECTOR_RESOURCE_CACHE_BYTES: usize = 256 * 1024 * 1024;
80
81type SharedVectorGeometryCache = Rc<RefCell<VectorGeometryCache>>;
82type SharedVectorResourceCache = Rc<RefCell<VectorResourceCache>>;
83
84/// Resource churn counters for renderer profiling.
85/// Effect counters are available with the opt-in `shaders` Cargo feature.
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
87pub struct ResourceChurnStats {
88    #[cfg(feature = "shaders")]
89    pub effect_source_renders: u64,
90    #[cfg(feature = "shaders")]
91    pub effect_source_cache_hits: u64,
92    #[cfg(feature = "shaders")]
93    pub effect_pipeline_compilations: u64,
94    #[cfg(feature = "shaders")]
95    pub effect_texture_allocations: u64,
96    #[cfg(feature = "shaders")]
97    pub effect_texture_bytes: u64,
98    #[cfg(feature = "shaders")]
99    pub effect_cache_hits: u64,
100    #[cfg(feature = "shaders")]
101    pub effect_passes: u64,
102    #[cfg(feature = "shaders")]
103    pub effect_pixels: u64,
104
105    pub mesh_texture_allocations: u64,
106    /// Bytes allocated for mesh textures during this sample (not current residency).
107    pub mesh_texture_bytes: u64,
108    pub mesh_coefficient_upload_bytes: u64,
109    pub mesh_raster_passes: u64,
110    pub mesh_cache_hits: u64,
111    pub mesh_topology_allocations: u64,
112    pub flushes: u64,
113    pub opacity_group_renders: u64,
114    pub opacity_group_cache_hits: u64,
115    pub alpha_source_renders: u64,
116    pub alpha_source_cache_hits: u64,
117    pub retained_scene_resets: u64,
118    pub vector_batch_flushes: u64,
119    pub vector_buffer_rebuilds: u64,
120    pub vector_geometry_rebuilds: u64,
121    pub vector_geometry_cache_hits: u64,
122    pub vector_geometry_cache_misses: u64,
123    pub vector_geometry_cache_evictions: u64,
124    pub vector_geometry_cache_bytes: u64,
125    pub tessellated_vertices: u64,
126    pub tessellated_indices: u64,
127    pub cached_vertices_reused: u64,
128    pub cached_indices_reused: u64,
129    pub vector_resource_creates: u64,
130    pub vector_resource_updates: u64,
131    pub vector_resource_recreates: u64,
132    pub vector_resource_create_bytes: u64,
133    pub vector_resource_update_bytes: u64,
134    pub vector_resource_cache_hits: u64,
135    pub vector_resource_cache_misses: u64,
136    pub vector_resource_cache_evictions: u64,
137    pub vector_resource_cache_bytes: u64,
138    pub texture_creates: u64,
139    pub image_draw_creates: u64,
140    pub texture_upload_bytes: u64,
141    /// Tile-local bindings created using an already resident sibling image texture.
142    pub texture_cache_hits: u64,
143    pub retained_nodes_considered: u64,
144    pub retained_nodes_visible: u64,
145    pub retained_draw_batches: u64,
146    pub retained_draws: u64,
147    pub retained_vector_draws: u64,
148    pub retained_image_draws: u64,
149}
150
151impl ResourceChurnStats {
152    pub fn merge(&mut self, other: Self) {
153        self.mesh_texture_allocations += other.mesh_texture_allocations;
154        self.mesh_texture_bytes += other.mesh_texture_bytes;
155        self.mesh_coefficient_upload_bytes += other.mesh_coefficient_upload_bytes;
156        self.mesh_raster_passes += other.mesh_raster_passes;
157        self.mesh_cache_hits += other.mesh_cache_hits;
158        self.mesh_topology_allocations += other.mesh_topology_allocations;
159        self.flushes += other.flushes;
160        #[cfg(feature = "shaders")]
161        {
162            self.effect_source_renders += other.effect_source_renders;
163            self.effect_source_cache_hits += other.effect_source_cache_hits;
164            self.effect_pipeline_compilations += other.effect_pipeline_compilations;
165            self.effect_texture_allocations += other.effect_texture_allocations;
166            self.effect_texture_bytes += other.effect_texture_bytes;
167            self.effect_cache_hits += other.effect_cache_hits;
168            self.effect_passes += other.effect_passes;
169            self.effect_pixels += other.effect_pixels;
170        }
171        self.opacity_group_renders += other.opacity_group_renders;
172        self.opacity_group_cache_hits += other.opacity_group_cache_hits;
173        self.alpha_source_renders += other.alpha_source_renders;
174        self.alpha_source_cache_hits += other.alpha_source_cache_hits;
175        self.retained_scene_resets += other.retained_scene_resets;
176        self.vector_batch_flushes += other.vector_batch_flushes;
177        self.vector_buffer_rebuilds += other.vector_buffer_rebuilds;
178        self.vector_geometry_rebuilds += other.vector_geometry_rebuilds;
179        self.vector_geometry_cache_hits += other.vector_geometry_cache_hits;
180        self.vector_geometry_cache_misses += other.vector_geometry_cache_misses;
181        self.vector_geometry_cache_evictions += other.vector_geometry_cache_evictions;
182        self.vector_geometry_cache_bytes = self
183            .vector_geometry_cache_bytes
184            .max(other.vector_geometry_cache_bytes);
185        self.tessellated_vertices += other.tessellated_vertices;
186        self.tessellated_indices += other.tessellated_indices;
187        self.cached_vertices_reused += other.cached_vertices_reused;
188        self.cached_indices_reused += other.cached_indices_reused;
189        self.vector_resource_creates += other.vector_resource_creates;
190        self.vector_resource_updates += other.vector_resource_updates;
191        self.vector_resource_recreates += other.vector_resource_recreates;
192        self.vector_resource_create_bytes += other.vector_resource_create_bytes;
193        self.vector_resource_update_bytes += other.vector_resource_update_bytes;
194        self.vector_resource_cache_hits += other.vector_resource_cache_hits;
195        self.vector_resource_cache_misses += other.vector_resource_cache_misses;
196        self.vector_resource_cache_evictions += other.vector_resource_cache_evictions;
197        self.vector_resource_cache_bytes = self
198            .vector_resource_cache_bytes
199            .max(other.vector_resource_cache_bytes);
200        self.texture_creates += other.texture_creates;
201        self.image_draw_creates += other.image_draw_creates;
202        self.texture_upload_bytes += other.texture_upload_bytes;
203        self.texture_cache_hits += other.texture_cache_hits;
204        self.retained_nodes_considered += other.retained_nodes_considered;
205        self.retained_nodes_visible += other.retained_nodes_visible;
206        self.retained_draw_batches += other.retained_draw_batches;
207        self.retained_draws += other.retained_draws;
208        self.retained_vector_draws += other.retained_vector_draws;
209        self.retained_image_draws += other.retained_image_draws;
210    }
211
212    pub fn has_resource_churn(&self) -> bool {
213        let mut stats = *self;
214        stats.flushes = 0;
215        stats.mesh_cache_hits = 0;
216        #[cfg(feature = "shaders")]
217        {
218            stats.effect_source_renders = 0;
219            stats.effect_source_cache_hits = 0;
220            stats.effect_cache_hits = 0;
221            stats.effect_passes = 0;
222            stats.effect_pixels = 0;
223        }
224        stats.opacity_group_renders = 0;
225        stats.opacity_group_cache_hits = 0;
226        stats.alpha_source_renders = 0;
227        stats.alpha_source_cache_hits = 0;
228        stats.retained_nodes_considered = 0;
229        stats.retained_nodes_visible = 0;
230        stats.retained_draw_batches = 0;
231        stats.retained_draws = 0;
232        stats.retained_vector_draws = 0;
233        stats.retained_image_draws = 0;
234        stats != Self::default()
235    }
236}
237
238/// Retained scene renderer that records Pax vector/image commands and flushes them through wgpu.
239///
240/// Transform and clip identities remain stable until their owning node releases them.
241/// GPU uniform pages grow with scene demand; the 480-entry page size is not a scene
242/// limit. Moving or fading retained geometry updates transform pages without
243/// retessellating or uploading vertex/index buffers.
244pub struct WgpuRenderer<'w> {
245    render_backend: RenderBackend<'w>,
246    scene: HashMap<u32, RetainedNode>,
247    opacity_scopes: HashMap<u32, Vec<OpacityScope>>,
248    source_nodes: HashMap<u32, u32>,
249    source_captures: HashMap<u32, SourceCapture>,
250    source_recordings: Vec<SourceRecording>,
251    #[cfg(feature = "shaders")]
252    effect_captures: HashMap<u32, EffectCapture>,
253    #[cfg(feature = "shaders")]
254    effect_nodes: HashMap<u32, u32>,
255    #[cfg(feature = "shaders")]
256    effect_recordings: Vec<EffectRecording>,
257    transform_arena: TransformArena,
258    clip_arena: ClipArena,
259    clip_owner_keys: HashMap<u32, Vec<ClipArenaKey>>,
260    sorted_nodes: Vec<(i32, u32)>,
261    order_dirty: bool,
262    scene_dirty: bool,
263    scene_lighting: GpuSceneLighting,
264    lighting_dependencies: Option<LightingDependencies>,
265    last_scene_lighting: Option<SceneLighting>,
266    cached_images: HashMap<String, CachedImageEntry>,
267    transform_stack: Vec<Transform2D>,
268    clip_stack: Vec<ClipReference>,
269    saves: Vec<SceneStateSave>,
270    current_node: Option<PendingNode>,
271    tolerance: f32,
272    vector_geometry_cache: SharedVectorGeometryCache,
273    alpha_geometry: HashMap<u32, HashMap<(u64, u32), Rc<VertexBuffers<[f32; 2], u32>>>>,
274    vector_resource_cache: SharedVectorResourceCache,
275    resource_churn_stats: ResourceChurnStats,
276}
277
278struct CachedImageEntry {
279    texture: CachedTextureResource,
280    version: u64,
281}
282
283#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
284struct VectorGeometryCacheKey {
285    geometry_signature: u64,
286    tolerance_bits: u32,
287}
288
289struct CachedVectorGeometry {
290    vertices: Vec<GpuVertex>,
291    indices: Vec<u16>,
292    bytes: usize,
293    last_used: u64,
294}
295
296#[derive(Default)]
297struct VectorGeometryCacheInsertStats {
298    evicted_entries: u64,
299    current_bytes: usize,
300}
301
302struct VectorGeometryCache {
303    entries: HashMap<VectorGeometryCacheKey, CachedVectorGeometry>,
304    current_bytes: usize,
305    max_bytes: usize,
306    clock: u64,
307}
308
309impl Default for VectorGeometryCache {
310    fn default() -> Self {
311        Self {
312            entries: HashMap::new(),
313            current_bytes: 0,
314            max_bytes: MAX_VECTOR_GEOMETRY_CACHE_BYTES,
315            clock: 0,
316        }
317    }
318}
319
320impl VectorGeometryCache {
321    fn get(&mut self, key: &VectorGeometryCacheKey) -> Option<&CachedVectorGeometry> {
322        self.clock = self.clock.wrapping_add(1);
323        let entry = self.entries.get_mut(key)?;
324        entry.last_used = self.clock;
325        Some(entry)
326    }
327
328    fn insert(
329        &mut self,
330        key: VectorGeometryCacheKey,
331        vertices: Vec<GpuVertex>,
332        indices: Vec<u16>,
333    ) -> VectorGeometryCacheInsertStats {
334        let bytes = vector_geometry_bytes(vertices.len(), indices.len());
335        if bytes > self.max_bytes {
336            return VectorGeometryCacheInsertStats {
337                current_bytes: self.current_bytes,
338                ..Default::default()
339            };
340        }
341
342        if let Some(previous) = self.entries.remove(&key) {
343            self.current_bytes = self.current_bytes.saturating_sub(previous.bytes);
344        }
345
346        self.clock = self.clock.wrapping_add(1);
347        self.current_bytes += bytes;
348        self.entries.insert(
349            key,
350            CachedVectorGeometry {
351                vertices,
352                indices,
353                bytes,
354                last_used: self.clock,
355            },
356        );
357
358        let mut stats = VectorGeometryCacheInsertStats {
359            current_bytes: self.current_bytes,
360            ..Default::default()
361        };
362        while self.current_bytes > self.max_bytes {
363            let Some(oldest_key) = self
364                .entries
365                .iter()
366                .min_by_key(|(_, entry)| entry.last_used)
367                .map(|(key, _)| *key)
368            else {
369                break;
370            };
371            if let Some(removed) = self.entries.remove(&oldest_key) {
372                self.current_bytes = self.current_bytes.saturating_sub(removed.bytes);
373                stats.evicted_entries += 1;
374            }
375        }
376        stats.current_bytes = self.current_bytes;
377        stats
378    }
379}
380
381#[derive(Clone, Debug, PartialEq, Eq, Hash)]
382struct VectorResourceKey {
383    tolerance_bits: u32,
384    geometry_signatures: Vec<u64>,
385    fill_signature: u64,
386    primitive_signature: u64,
387    transform_layout_signature: u64,
388}
389
390struct CachedVectorResource {
391    resource: Rc<SharedRetainedVectorResource>,
392    bytes: usize,
393    last_used: u64,
394}
395
396#[derive(Default)]
397struct VectorResourceCacheStats {
398    evicted_entries: u64,
399    current_bytes: usize,
400}
401
402struct VectorResourceCache {
403    entries: HashMap<VectorResourceKey, CachedVectorResource>,
404    current_bytes: usize,
405    max_bytes: usize,
406    clock: u64,
407}
408
409impl Default for VectorResourceCache {
410    fn default() -> Self {
411        Self {
412            entries: HashMap::new(),
413            current_bytes: 0,
414            max_bytes: MAX_VECTOR_RESOURCE_CACHE_BYTES,
415            clock: 0,
416        }
417    }
418}
419
420impl VectorResourceCache {
421    fn set_max_bytes(&mut self, max_bytes: usize) -> VectorResourceCacheStats {
422        self.max_bytes = max_bytes.max(1);
423        self.evict_if_needed()
424    }
425
426    fn get(&mut self, key: &VectorResourceKey) -> Option<Rc<SharedRetainedVectorResource>> {
427        self.clock = self.clock.wrapping_add(1);
428        let entry = self.entries.get_mut(key)?;
429        entry.last_used = self.clock;
430        Some(Rc::clone(&entry.resource))
431    }
432
433    fn current_bytes(&self) -> usize {
434        self.current_bytes
435    }
436
437    fn insert(
438        &mut self,
439        key: VectorResourceKey,
440        resource: Rc<SharedRetainedVectorResource>,
441        bytes: u64,
442    ) -> VectorResourceCacheStats {
443        let bytes = bytes as usize;
444        if bytes > self.max_bytes {
445            return VectorResourceCacheStats {
446                current_bytes: self.current_bytes,
447                ..Default::default()
448            };
449        }
450
451        if let Some(previous) = self.entries.remove(&key) {
452            self.current_bytes = self.current_bytes.saturating_sub(previous.bytes);
453        }
454
455        self.clock = self.clock.wrapping_add(1);
456        self.current_bytes += bytes;
457        self.entries.insert(
458            key,
459            CachedVectorResource {
460                resource,
461                bytes,
462                last_used: self.clock,
463            },
464        );
465        self.evict_if_needed()
466    }
467
468    fn remove_if_same(
469        &mut self,
470        key: &VectorResourceKey,
471        resource: &Rc<SharedRetainedVectorResource>,
472    ) -> Option<CachedVectorResource> {
473        let entry = self.entries.get(key)?;
474        if !Rc::ptr_eq(&entry.resource, resource) {
475            return None;
476        }
477        let removed = self.entries.remove(key)?;
478        self.current_bytes = self.current_bytes.saturating_sub(removed.bytes);
479        Some(removed)
480    }
481
482    fn evict_if_needed(&mut self) -> VectorResourceCacheStats {
483        let mut stats = VectorResourceCacheStats {
484            current_bytes: self.current_bytes,
485            ..Default::default()
486        };
487        while self.current_bytes > self.max_bytes {
488            let Some(oldest_key) = self
489                .entries
490                .iter()
491                .filter(|(_, entry)| Rc::strong_count(&entry.resource) == 1)
492                .min_by_key(|(_, entry)| entry.last_used)
493                .map(|(key, _)| key.clone())
494            else {
495                break;
496            };
497            if let Some(removed) = self.entries.remove(&oldest_key) {
498                self.current_bytes = self.current_bytes.saturating_sub(removed.bytes);
499                stats.evicted_entries += 1;
500            }
501        }
502        stats.current_bytes = self.current_bytes;
503        stats
504    }
505}
506
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
508struct TransformArenaKey {
509    node_id: u32,
510    op_index: u32,
511}
512
513struct TransformArena {
514    slots: Vec<GpuTransform>,
515    entries: HashMap<TransformArenaKey, u32>,
516    free_slots: Vec<u32>,
517    next_slot: u32,
518    dirty: bool,
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
522struct ClipArenaKey {
523    node_id: u32,
524    clip_index: u32,
525}
526
527#[derive(Clone, Copy, Debug, PartialEq)]
528enum ClipReference {
529    Alpha { owner: u32 },
530    Stencil { clip_id: u32 },
531    Scissor(ScissorRect),
532}
533
534struct ClipArenaEntry {
535    geometry_signature: u64,
536    geometry: VertexBuffers<stencil::Vertex, u16>,
537}
538
539struct ClipArena {
540    slots: Vec<GpuTransform>,
541    key_to_id: HashMap<ClipArenaKey, u32>,
542    entries: HashMap<u32, ClipArenaEntry>,
543    free_ids: Vec<u32>,
544    next_id: u32,
545    dirty: bool,
546}
547
548impl ClipArena {
549    fn new() -> Self {
550        Self {
551            slots: Vec::new(),
552            key_to_id: HashMap::new(),
553            entries: HashMap::new(),
554            free_ids: Vec::new(),
555            next_id: 0,
556            dirty: true,
557        }
558    }
559
560    fn sync_clip(
561        &mut self,
562        node_id: u32,
563        clip_index: u32,
564        geometry_signature: u64,
565        geometry: VertexBuffers<stencil::Vertex, u16>,
566        transform: Transform2D,
567    ) -> Option<(ClipArenaKey, u32)> {
568        let key = ClipArenaKey {
569            node_id,
570            clip_index,
571        };
572        let clip_id = if let Some(clip_id) = self.key_to_id.get(&key).copied() {
573            clip_id
574        } else {
575            let clip_id = self.free_ids.pop().unwrap_or_else(|| {
576                let clip_id = self.next_id;
577                self.next_id = self
578                    .next_id
579                    .checked_add(1)
580                    .expect("clip ID space exhausted");
581                self.slots.push(GpuTransform::default());
582                clip_id
583            });
584            self.key_to_id.insert(key, clip_id);
585            self.dirty = true;
586            clip_id
587        };
588
589        let value = GpuTransform {
590            transform: transform.to_arrays(),
591            ..GpuTransform::default()
592        };
593        if self.slots[clip_id as usize].transform != value.transform {
594            self.slots[clip_id as usize] = value;
595            self.dirty = true;
596        }
597
598        match self.entries.get_mut(&clip_id) {
599            Some(entry) => {
600                if entry.geometry_signature != geometry_signature {
601                    entry.geometry_signature = geometry_signature;
602                    entry.geometry = geometry;
603                }
604            }
605            None => {
606                self.entries.insert(
607                    clip_id,
608                    ClipArenaEntry {
609                        geometry_signature,
610                        geometry,
611                    },
612                );
613                self.dirty = true;
614            }
615        }
616
617        Some((key, clip_id))
618    }
619
620    fn release_keys(&mut self, keys: &[ClipArenaKey]) {
621        for key in keys {
622            let Some(clip_id) = self.key_to_id.remove(key) else {
623                continue;
624            };
625            self.entries.remove(&clip_id);
626            self.slots[clip_id as usize] = GpuTransform::default();
627            self.free_ids.push(clip_id);
628            self.dirty = true;
629        }
630    }
631
632    fn get(&self, clip_id: u32) -> Option<&ClipArenaEntry> {
633        self.entries.get(&clip_id)
634    }
635
636    fn flush_to_gpu<'w>(&mut self, render_backend: &mut RenderBackend<'w>) {
637        if !self.dirty {
638            return;
639        }
640        render_backend.update_scene_clip_transforms(&self.slots);
641        self.dirty = false;
642    }
643}
644
645impl TransformArena {
646    fn new() -> Self {
647        Self {
648            slots: vec![GpuTransform::default()],
649            entries: HashMap::new(),
650            free_slots: Vec::new(),
651            next_slot: 1,
652            dirty: true,
653        }
654    }
655
656    fn sync_node(
657        &mut self,
658        node_id: u32,
659        local_transforms: &[GpuTransform],
660    ) -> (Vec<TransformArenaKey>, Vec<u32>) {
661        let mut keys = Vec::with_capacity(local_transforms.len().saturating_sub(1));
662        let mut ids = Vec::with_capacity(local_transforms.len());
663        ids.push(0);
664        for (transform_index, transform) in local_transforms.iter().enumerate().skip(1) {
665            let key = TransformArenaKey {
666                node_id,
667                op_index: (transform_index - 1) as u32,
668            };
669            let slot = self.entries.get(&key).copied().unwrap_or_else(|| {
670                let slot = self.free_slots.pop().unwrap_or_else(|| {
671                    let slot = self.next_slot;
672                    self.next_slot = self
673                        .next_slot
674                        .checked_add(1)
675                        .expect("transform ID space exhausted");
676                    self.slots.push(GpuTransform::default());
677                    slot
678                });
679                self.entries.insert(key, slot);
680                self.dirty = true;
681                slot
682            });
683            if self.slots[slot as usize].transform != transform.transform
684                || (self.slots[slot as usize].opacity - transform.opacity).abs() > f32::EPSILON
685            {
686                self.slots[slot as usize] = *transform;
687                self.dirty = true;
688            }
689            keys.push(key);
690            ids.push(slot);
691        }
692        (keys, ids)
693    }
694
695    fn release_keys(&mut self, keys: &[TransformArenaKey]) {
696        for key in keys {
697            if let Some(slot) = self.entries.remove(key) {
698                if slot != 0 {
699                    self.free_slots.push(slot);
700                }
701            }
702        }
703    }
704
705    fn flush_to_gpu<'w>(&mut self, render_backend: &mut RenderBackend<'w>) {
706        if !self.dirty {
707            return;
708        }
709        render_backend.update_scene_transforms(&self.slots);
710        self.dirty = false;
711    }
712}
713
714impl<'w> WgpuRenderer<'w> {
715    /// Create a retained renderer around a low-level `RenderBackend`.
716    pub fn new(render_backend: RenderBackend<'w>) -> Self {
717        Self {
718            render_backend,
719            tolerance: DEFAULT_TESSELLATION_TOLERANCE, // TODO expose as option
720            scene: HashMap::new(),
721            opacity_scopes: HashMap::new(),
722            source_nodes: HashMap::new(),
723            source_captures: HashMap::new(),
724            source_recordings: Vec::new(),
725            #[cfg(feature = "shaders")]
726            effect_captures: HashMap::new(),
727            #[cfg(feature = "shaders")]
728            effect_nodes: HashMap::new(),
729            #[cfg(feature = "shaders")]
730            effect_recordings: Vec::new(),
731            transform_arena: TransformArena::new(),
732            clip_arena: ClipArena::new(),
733            clip_owner_keys: HashMap::new(),
734            sorted_nodes: Vec::new(),
735            order_dirty: false,
736            scene_dirty: false,
737            scene_lighting: GpuSceneLighting::default(),
738            lighting_dependencies: None,
739            last_scene_lighting: None,
740            cached_images: HashMap::new(),
741            transform_stack: vec![Transform2D::identity()],
742            clip_stack: Vec::new(),
743            saves: vec![],
744            current_node: None,
745            vector_geometry_cache: Rc::new(RefCell::new(VectorGeometryCache::default())),
746            alpha_geometry: HashMap::new(),
747            vector_resource_cache: Rc::new(RefCell::new(VectorResourceCache::default())),
748            resource_churn_stats: ResourceChurnStats::default(),
749        }
750    }
751
752    /// Share vector resource caches with another renderer for the same logical layer.
753    pub fn share_vector_caches_from(&mut self, other: &Self) {
754        self.vector_geometry_cache = Rc::clone(&other.vector_geometry_cache);
755        self.vector_resource_cache = Rc::clone(&other.vector_resource_cache);
756    }
757
758    pub fn set_vector_resource_cache_max_bytes(&mut self, max_bytes: usize) {
759        let stats = self
760            .vector_resource_cache
761            .borrow_mut()
762            .set_max_bytes(max_bytes);
763        self.merge_vector_resource_cache_stats(stats);
764    }
765
766    pub fn shared_gpu_context_id(&self) -> usize {
767        self.render_backend.shared_context_id()
768    }
769
770    /// Reuse this logical layer's GPU context when creating another physical tile.
771    /// The returned handle keeps the device and pipelines alive, not this tile's surface or scene.
772    pub fn shared_gpu_context(&self) -> crate::render_backend::SharedGpuContext {
773        self.render_backend.shared_context()
774    }
775
776    /// Return and reset accumulated resource churn counters.
777    pub fn take_resource_churn_stats(&mut self) -> ResourceChurnStats {
778        std::mem::take(&mut self.resource_churn_stats)
779    }
780
781    /// Current transform at the top of the render-state stack.
782    pub fn current_transform(&self) -> Transform2D {
783        self.transform_stack
784            .last()
785            .copied()
786            .unwrap_or_else(Transform2D::identity)
787    }
788
789    /// Set the base transform for the physical surface tile being rendered.
790    pub fn set_surface_transform(&mut self, transform: Transform2D) {
791        // A logical Pax layer may fan out to multiple physical browser canvases. Keep the tile's
792        // content-space translation as the base transform so retained node transforms remain
793        // tile-agnostic and flush/reset preserves the per-surface coordinate space.
794        if let Some(base_transform) = self.transform_stack.first_mut() {
795            *base_transform = transform;
796        } else {
797            self.transform_stack.push(transform);
798        }
799        self.scene_dirty = true;
800        self.clip_stack.clear();
801        self.saves.clear();
802    }
803
804    /// Drop retained scene state for a surface that has been rebound to a new tile origin.
805    pub fn reset_retained_scene(&mut self) {
806        self.resource_churn_stats.retained_scene_resets += 1;
807        // Reusing a physical browser surface for a different tile origin is only safe if the
808        // retained scene is truly origin-agnostic. The current retained vector path still caches
809        // per-node transforms/resources against the previous slot assignment, so drop that state
810        // and let the engine replay dirty canvas nodes into the reassigned surface.
811        self.scene.clear();
812        self.lighting_dependencies = None;
813        self.opacity_scopes.clear();
814        self.source_nodes.clear();
815        self.source_captures.clear();
816        self.source_recordings.clear();
817        #[cfg(feature = "shaders")]
818        {
819            self.effect_captures.clear();
820            self.effect_nodes.clear();
821            self.effect_recordings.clear();
822        }
823        self.render_backend.reset_captures();
824        self.render_backend.alpha_mesh_pages.clear();
825        self.transform_arena = TransformArena::new();
826        self.clip_arena = ClipArena::new();
827        self.clip_owner_keys.clear();
828        self.sorted_nodes.clear();
829        self.order_dirty = false;
830        self.scene_dirty = true;
831        self.clip_stack.clear();
832        self.saves.clear();
833        self.current_node = None;
834    }
835
836    /// Queue a stroked vector path into the current retained node.
837    pub fn stroke_path(&mut self, path: Path, stroke: Stroke) {
838        self.stroke_path_with_opacity(path, stroke, 1.0);
839    }
840
841    /// Queue a stroked vector path with an extra opacity multiplier.
842    pub fn stroke_path_with_opacity(&mut self, path: Path, stroke: Stroke, opacity: f32) {
843        self.stroke_path_with_material_and_opacity(path, stroke, Material::default(), opacity);
844    }
845
846    /// Queue a stroked vector path with material response and an extra opacity multiplier.
847    pub fn stroke_path_with_material_and_opacity(
848        &mut self,
849        path: Path,
850        stroke: Stroke,
851        material: Material,
852        opacity: f32,
853    ) {
854        self.stroke_path_with_draw_range_and_material_and_opacity(
855            path,
856            stroke,
857            material,
858            opacity,
859            DrawRange::disabled(),
860        );
861    }
862
863    /// Queue a stroked vector path with material response, opacity, and optional smoothing.
864    pub fn stroke_path_with_material_and_opacity_and_smoothing(
865        &mut self,
866        path: Path,
867        stroke: Stroke,
868        material: Material,
869        opacity: f32,
870        smoothing: PathSmoothing,
871    ) {
872        self.stroke_path_with_draw_range_and_material_and_opacity_and_smoothing(
873            path,
874            stroke,
875            material,
876            opacity,
877            DrawRange::disabled(),
878            smoothing,
879        );
880    }
881
882    /// Queue a draw-ranged stroked vector path with material response and an extra opacity multiplier.
883    pub fn stroke_path_with_draw_range_and_material_and_opacity(
884        &mut self,
885        path: Path,
886        stroke: Stroke,
887        material: Material,
888        opacity: f32,
889        draw_range: DrawRange,
890    ) {
891        self.stroke_path_with_draw_range_and_material_and_opacity_and_smoothing(
892            path,
893            stroke,
894            material,
895            opacity,
896            draw_range,
897            PathSmoothing::None,
898        );
899    }
900
901    /// Queue a draw-ranged stroked vector path with material response, opacity, and optional smoothing.
902    pub fn stroke_path_with_draw_range_and_material_and_opacity_and_smoothing(
903        &mut self,
904        path: Path,
905        stroke: Stroke,
906        material: Material,
907        opacity: f32,
908        draw_range: DrawRange,
909        smoothing: PathSmoothing,
910    ) {
911        let current_transform = self.current_transform();
912        let light_mask = self
913            .current_node
914            .as_ref()
915            .map(|node| node.light_mask)
916            .unwrap_or(0);
917        let geometry_signature = hash_vector_path(
918            &path,
919            PendingVectorOpKind::Stroke(stroke.weight, stroke.cap, stroke.join),
920            smoothing,
921        );
922        let Some(PendingNode {
923            kind: PendingNodeKind::Vector(buffers),
924            ..
925        }) = self.ensure_vector_node()
926        else {
927            return;
928        };
929        buffers.ops.push(PendingVectorOp {
930            path,
931            fill: stroke.fill,
932            material,
933            light_mask,
934            transform: current_transform,
935            opacity,
936            kind: PendingVectorOpKind::Stroke(stroke.weight, stroke.cap, stroke.join),
937            geometry_signature,
938            draw_range,
939            smoothing,
940        });
941    }
942
943    /// Queue a filled vector path into the current retained node.
944    pub fn fill_path(&mut self, path: Path, fill: Fill) {
945        self.fill_path_with_opacity(path, fill, 1.0);
946    }
947
948    /// Queue a filled vector path with an extra opacity multiplier.
949    pub fn fill_path_with_opacity(&mut self, path: Path, fill: Fill, opacity: f32) {
950        self.fill_path_with_material_and_opacity(path, fill, Material::default(), opacity);
951    }
952
953    /// Queue a filled vector path with material response and an extra opacity multiplier.
954    pub fn fill_path_with_material_and_opacity(
955        &mut self,
956        path: Path,
957        fill: Fill,
958        material: Material,
959        opacity: f32,
960    ) {
961        self.fill_path_with_material_and_opacity_and_smoothing(
962            path,
963            fill,
964            material,
965            opacity,
966            PathSmoothing::None,
967        );
968    }
969
970    /// Queue a filled vector path with material response, opacity, and optional smoothing.
971    pub fn fill_path_with_material_and_opacity_and_smoothing(
972        &mut self,
973        path: Path,
974        fill: Fill,
975        material: Material,
976        opacity: f32,
977        smoothing: PathSmoothing,
978    ) {
979        let current_transform = self.current_transform();
980        let light_mask = self
981            .current_node
982            .as_ref()
983            .map(|node| node.light_mask)
984            .unwrap_or(0);
985        let geometry_signature = hash_vector_path(&path, PendingVectorOpKind::Fill, smoothing);
986        let Some(PendingNode {
987            kind: PendingNodeKind::Vector(buffers),
988            ..
989        }) = self.ensure_vector_node()
990        else {
991            return;
992        };
993        buffers.ops.push(PendingVectorOp {
994            path,
995            fill,
996            material,
997            light_mask,
998            transform: current_transform,
999            opacity,
1000            kind: PendingVectorOpKind::Fill,
1001            geometry_signature,
1002            draw_range: DrawRange::disabled(),
1003            smoothing,
1004        });
1005    }
1006
1007    /// Upload changed surface-local lighting, returning whether retained pixels need redraw.
1008    /// Only lights used by this surface's lit vector draws invalidate its contents.
1009    /// Repeating the same value does not upload a uniform or dirty the scene.
1010    /// Each new renderer receives its own initial upload, even for default lighting.
1011    pub fn set_scene_lighting(&mut self, lighting: SceneLighting) -> bool {
1012        if self.last_scene_lighting.as_ref() == Some(&lighting) {
1013            return false;
1014        }
1015        let gpu_lighting = to_gpu_scene_lighting(&lighting);
1016        let dependencies = self.lighting_dependencies();
1017        let changed =
1018            dependencies.signature(&self.scene_lighting) != dependencies.signature(&gpu_lighting);
1019        // Keep uniforms current even without recipients. A later draw must see the
1020        // latest light values without requiring another lighting property change.
1021        self.render_backend.set_scene_lighting(gpu_lighting);
1022        self.scene_lighting = gpu_lighting;
1023        self.last_scene_lighting = Some(lighting);
1024        self.scene_dirty |= changed;
1025        changed
1026    }
1027
1028    pub fn clear(&mut self) {
1029        self.scene_dirty = true;
1030        self.render_backend.clear();
1031    }
1032
1033    /// Image identity and version must identify the same pixels across renderers sharing a GPU
1034    /// context. Change the version when replacing image content.
1035    pub fn draw_image(&mut self, image_key: &str, image_version: u64, image: &Image, rect: Box2D) {
1036        self.draw_image_with_opacity(image_key, image_version, image, rect, 1.0);
1037    }
1038
1039    /// Multiplies source pixel alpha by `opacity` without reuploading the texture.
1040    /// Sibling tiles share resident image pixels, but keep their own draw bindings.
1041    /// Identity/version follows the same contract as [`Self::draw_image`].
1042    pub fn draw_image_with_opacity(
1043        &mut self,
1044        image_key: &str,
1045        image_version: u64,
1046        image: &Image,
1047        rect: Box2D,
1048        opacity: f32,
1049    ) {
1050        let needs_binding = self
1051            .cached_images
1052            .get(image_key)
1053            .map(|entry| {
1054                entry.version != image_version
1055                    || entry.texture.width != image.pixel_width
1056                    || entry.texture.height != image.pixel_height
1057            })
1058            .unwrap_or(true);
1059        if needs_binding {
1060            let (texture, uploaded) = self.render_backend.create_cached_texture(
1061                image_key,
1062                image_version,
1063                &image.rgba,
1064                image.pixel_width,
1065                image.pixel_height,
1066            );
1067            if uploaded {
1068                self.resource_churn_stats.texture_creates += 1;
1069                self.resource_churn_stats.texture_upload_bytes += image.rgba.len() as u64;
1070            } else {
1071                self.resource_churn_stats.texture_cache_hits += 1;
1072            }
1073            self.cached_images.insert(
1074                image_key.to_owned(),
1075                CachedImageEntry {
1076                    texture,
1077                    version: image_version,
1078                },
1079            );
1080        }
1081        let transform = self.current_transform();
1082        let clip_stack = self.clip_stack.clone();
1083        let bounds = transform_box(rect, &transform);
1084        let mut signature = DefaultHasher::new();
1085        image_key.hash(&mut signature);
1086        image_version.hash(&mut signature);
1087        for value in transform
1088            .to_array()
1089            .into_iter()
1090            .chain([rect.min.x, rect.min.y, rect.max.x, rect.max.y, opacity])
1091        {
1092            value.to_bits().hash(&mut signature);
1093        }
1094        let Some(current_node) = self.current_node.as_mut() else {
1095            return;
1096        };
1097        let signature = signature.finish();
1098        let unchanged = matches!(self.scene.get(&current_node.id), Some(RetainedNode::Image(node)) if node.signature == signature);
1099        let draw = if unchanged {
1100            None
1101        } else {
1102            self.resource_churn_stats.image_draw_creates += 1;
1103            Some(self.render_backend.create_image_draw(
1104                image_key.to_owned(),
1105                transform,
1106                rect,
1107                opacity.clamp(0.0, 1.0),
1108            ))
1109        };
1110        current_node.kind = PendingNodeKind::Image(PendingImageNode {
1111            signature,
1112            draw,
1113            clip_stack,
1114            bounds,
1115        });
1116    }
1117
1118    pub fn flush(&mut self) {
1119        self.flush_internal(false);
1120    }
1121
1122    pub fn flush_deferred(&mut self) {
1123        self.flush_internal(true);
1124    }
1125
1126    fn flush_internal(&mut self, defer_submit: bool) {
1127        self.resource_churn_stats.flushes += 1;
1128        let active_alpha_masks = self.retain_reachable_sources();
1129        self.render_backend.alpha_masks.retain(&active_alpha_masks);
1130        self.render_backend
1131            .alpha_mesh_pages
1132            .retain(|id, _| active_alpha_masks.contains(id));
1133        self.alpha_geometry
1134            .retain(|owner, _| active_alpha_masks.contains(owner));
1135        if !self.scene_dirty {
1136            self.transform_stack.truncate(1);
1137            self.clip_stack.clear();
1138            self.saves.clear();
1139            self.current_node = None;
1140            return;
1141        }
1142        if self.order_dirty {
1143            self.sorted_nodes = self
1144                .scene
1145                .iter()
1146                .map(|(node_id, node)| (node.z_index(), *node_id))
1147                .collect();
1148            self.sorted_nodes.sort_unstable();
1149            self.order_dirty = false;
1150        }
1151        // Prune on both mixed and vector-only paths so removed images release their shared leases.
1152        self.cached_images.retain(|image_key, _| {
1153            self.scene.values().any(|node| {
1154                matches!(
1155                    node,
1156                    RetainedNode::Image(image_node)
1157                        if image_node.draw.resource.image_key == *image_key
1158                )
1159            })
1160        });
1161        if self.should_use_vector_scene_batch() {
1162            self.render_backend.retain_captures(&HashSet::new());
1163            self.resource_churn_stats.vector_batch_flushes += 1;
1164            self.flush_vector_scene_batch(defer_submit);
1165            self.scene_dirty = false;
1166            self.transform_stack.truncate(1);
1167            self.clip_stack.clear();
1168            self.saves.clear();
1169            self.current_node = None;
1170            return;
1171        }
1172        self.render_backend.ensure_frame_cleared();
1173        self.ensure_vector_resources_for_immediate_scene();
1174        self.transform_arena.flush_to_gpu(&mut self.render_backend);
1175        self.clip_arena.flush_to_gpu(&mut self.render_backend);
1176        self.resource_churn_stats.retained_nodes_considered += self.sorted_nodes.len() as u64;
1177        // Each retained renderer represents one physical surface tile. Cull retained nodes against
1178        // that tile-local viewport before batching so newly revealed tiles do not replay the full
1179        // layer scene.
1180        let source_capture_keys = self.render_alpha_sources(&active_alpha_masks);
1181        let mut active = source_capture_keys;
1182        #[cfg(feature = "shaders")]
1183        active.extend(self.render_effects());
1184        let plan = self.opacity_plan();
1185        collect_opacity_group_keys(&plan, &mut active);
1186        self.draw_opacity_plan(&plan);
1187        self.render_backend.retain_captures(&active);
1188        let (active_clip_signatures, active_clip_ids) =
1189            collect_active_clip_resources(&self.scene, &self.clip_arena);
1190        self.render_backend
1191            .retain_stencil_resources(&active_clip_signatures, &active_clip_ids);
1192        self.finish_backend_frame(defer_submit);
1193        self.scene_dirty = false;
1194        self.transform_stack.truncate(1);
1195        self.clip_stack.clear();
1196        self.saves.clear();
1197        self.current_node = None;
1198    }
1199
1200    fn draw_retained_nodes(&mut self, node_ids: &[u32]) {
1201        let viewport_bounds = self.viewport_bounds();
1202        let mut current_clip_stack: Vec<u32> = Vec::new();
1203        let mut current_batch: Vec<RetainedDraw<'_>> = Vec::new();
1204        let mut retained_runs: Vec<RetainedBatchRun<'_>> = Vec::new();
1205        let mut current_batch_clip_stack: Option<Vec<ClipReference>> = None;
1206        for node_id in node_ids {
1207            let Some(node) = self.scene.get(node_id) else {
1208                continue;
1209            };
1210            if !node.intersects_bounds(&viewport_bounds) {
1211                continue;
1212            }
1213            self.resource_churn_stats.retained_nodes_visible += 1;
1214            if let Some(batch_clip_stack) = current_batch_clip_stack.as_ref() {
1215                if !clip_stacks_match(batch_clip_stack, node.clip_stack()) {
1216                    sync_retained_clip_stack(
1217                        &mut self.render_backend,
1218                        &mut retained_runs,
1219                        &mut current_clip_stack,
1220                        batch_clip_stack,
1221                        &self.clip_arena,
1222                    );
1223                    let stencil_index = self.render_backend.get_clip_depth();
1224                    let scissor = scissor_for_clip_stack(batch_clip_stack);
1225                    record_retained_batch_stats(&mut self.resource_churn_stats, &current_batch);
1226                    if !current_batch.is_empty() {
1227                        retained_runs.push(RetainedBatchRun {
1228                            alpha_mask: alpha_for_clip_stack(batch_clip_stack),
1229                            stencil_index,
1230                            scissor,
1231                            draws: std::mem::take(&mut current_batch),
1232                        });
1233                    }
1234                    current_batch_clip_stack = Some(node.clip_stack().to_vec());
1235                }
1236            } else {
1237                current_batch_clip_stack = Some(node.clip_stack().to_vec());
1238            }
1239            match node {
1240                #[cfg(feature = "shaders")]
1241                RetainedNode::Effect(node) => {
1242                    if node.visible {
1243                        current_batch.push(RetainedDraw::Effect(node.id));
1244                    }
1245                }
1246                RetainedNode::Vector(node) => {
1247                    let Some(resource) = node.resource.as_ref() else {
1248                        continue;
1249                    };
1250                    if !resource.is_drawable() {
1251                        continue;
1252                    }
1253                    current_batch.push(RetainedDraw::Vector(resource));
1254                }
1255                RetainedNode::Image(node) => {
1256                    let Some(texture) = self.cached_images.get(&node.draw.resource.image_key)
1257                    else {
1258                        continue;
1259                    };
1260                    current_batch.push(RetainedDraw::Image {
1261                        texture: &texture.texture,
1262                        draw: &node.draw,
1263                    });
1264                }
1265            }
1266        }
1267        if let Some(batch_clip_stack) = current_batch_clip_stack.as_ref() {
1268            sync_retained_clip_stack(
1269                &mut self.render_backend,
1270                &mut retained_runs,
1271                &mut current_clip_stack,
1272                batch_clip_stack,
1273                &self.clip_arena,
1274            );
1275            let stencil_index = self.render_backend.get_clip_depth();
1276            let scissor = scissor_for_clip_stack(batch_clip_stack);
1277            record_retained_batch_stats(&mut self.resource_churn_stats, &current_batch);
1278            if !current_batch.is_empty() {
1279                retained_runs.push(RetainedBatchRun {
1280                    alpha_mask: alpha_for_clip_stack(batch_clip_stack),
1281                    stencil_index,
1282                    scissor,
1283                    draws: std::mem::take(&mut current_batch),
1284                });
1285            }
1286        }
1287        self.render_backend.draw_retained_batch_runs(&retained_runs);
1288    }
1289
1290    /// Updates group presentation without changing a node's paint or image resources.
1291    pub fn set_node_opacity_scopes(&mut self, node_id: u32, scopes: &[OpacityScope]) {
1292        if self.opacity_scopes.get(&node_id).map(Vec::as_slice) != Some(scopes) {
1293            self.opacity_scopes.insert(node_id, scopes.to_vec());
1294            self.scene_dirty = true;
1295        }
1296    }
1297
1298    pub fn save(&mut self) {
1299        self.saves.push(SceneStateSave {
1300            transform_depth: self.transform_stack.len(),
1301            clip_depth: self.clip_stack.len(),
1302        });
1303    }
1304
1305    pub fn restore(&mut self) {
1306        if let Some(save) = self.saves.pop() {
1307            self.transform_stack.truncate(save.transform_depth);
1308            self.clip_stack.truncate(save.clip_depth);
1309        }
1310    }
1311
1312    pub fn transform(&mut self, transform: Transform2D) {
1313        self.transform_stack
1314            .push(transform.then(&self.current_transform()));
1315    }
1316
1317    pub fn clip(&mut self, path: Path) {
1318        let transform = self.current_transform();
1319        let Some(current_node) = self.current_node.as_mut() else {
1320            log::warn!("clip called without an active node");
1321            return;
1322        };
1323        if let Some(scissor) =
1324            axis_aligned_rect_scissor(&path, &transform, self.render_backend.globals.dpr)
1325        {
1326            self.clip_stack.push(ClipReference::Scissor(scissor));
1327            return;
1328        }
1329        let geometry = tessellate_clip_geometry(&path, self.tolerance);
1330        let geometry_signature = hash_clip_geometry(&geometry);
1331        let clip_index = current_node.owned_clip_keys.len() as u32;
1332        let Some((clip_key, clip_id)) = self.clip_arena.sync_clip(
1333            current_node.id,
1334            clip_index,
1335            geometry_signature,
1336            geometry,
1337            transform,
1338        ) else {
1339            return;
1340        };
1341        current_node.owned_clip_keys.push(clip_key);
1342        self.clip_stack.push(ClipReference::Stencil { clip_id });
1343    }
1344
1345    /// Installs a painted alpha mask in the current save/restore scope.
1346    pub fn clip_alpha(&mut self, paints: Vec<AlphaMaskPaint>, feather: f32) {
1347        use crate::render_backend::alpha_mask::{Draw, Paint};
1348        let Some(node) = &self.current_node else {
1349            return;
1350        };
1351        let owner = node.id;
1352        let parent = alpha_for_clip_stack(&self.clip_stack);
1353        let mut hash = DefaultHasher::new();
1354        self.render_backend
1355            .alpha_masks
1356            .signature(parent)
1357            .hash(&mut hash);
1358        let transform = self.current_transform();
1359        let mut draws = Vec::new();
1360        let mut meshes = Vec::new();
1361        // Retain only geometry still used by this mask. Paint/order/transform
1362        // edits reuse local tessellation; removed layers release their entries.
1363        let mut previous_geometry = self.alpha_geometry.remove(&owner).unwrap_or_default();
1364        let mut next_geometry = HashMap::new();
1365        for source in paints {
1366            if let Some((id, opacity)) = source.composition {
1367                id.hash(&mut hash);
1368                opacity.to_bits().hash(&mut hash);
1369            }
1370            fn append(
1371                out: &mut Vec<Paint>,
1372                meshes: &mut Vec<crate::render_backend::mesh_paint::MeshInput>,
1373                fill: Fill,
1374                opacity: f32,
1375            ) {
1376                let mut paint = Paint::default();
1377                paint.params[1] = opacity;
1378                match fill {
1379                    Fill::Mesh {
1380                        rows,
1381                        pos,
1382                        main_axis,
1383                        off_axis,
1384                    } => {
1385                        let input = crate::render_backend::mesh_paint::MeshInput::new(
1386                            rows,
1387                            [main_axis.to_array(), off_axis.to_array()],
1388                        );
1389                        paint.params[0] = meshes.len() as f32;
1390                        paint.params[3] = 2.0;
1391                        paint.axis = [pos.x, pos.y, main_axis.x, main_axis.y];
1392                        paint.off_axis = [off_axis.x, off_axis.y, 0.0, 0.0];
1393                        paint.stops[0] = input.domain;
1394                        meshes.push(input);
1395                    }
1396                    Fill::Blend(terms) => {
1397                        for (fill, weight) in terms {
1398                            append(out, meshes, fill, opacity * weight);
1399                        }
1400                        return;
1401                    }
1402                    Fill::Solid(color) => paint.params[0] = color.rgba[3],
1403                    Fill::Gradient {
1404                        gradient_type,
1405                        pos,
1406                        main_axis,
1407                        off_axis,
1408                        stops,
1409                    } => {
1410                        paint.axis = [pos.x, pos.y, main_axis.x, main_axis.y];
1411                        paint.off_axis = [off_axis.x, off_axis.y, 0.0, 0.0];
1412                        paint.params[2] = stops.len().min(8) as f32;
1413                        if let GradientType::Radial { focal_point } = gradient_type {
1414                            paint.params[3] = 1.0;
1415                            paint.off_axis[2] = focal_point.x;
1416                            paint.off_axis[3] = focal_point.y;
1417                        }
1418                        for (slot, stop) in paint.stops.iter_mut().zip(stops.iter()) {
1419                            *slot = [stop.stop, stop.color.rgba[3], 0.0, 0.0];
1420                        }
1421                    }
1422                }
1423                out.push(paint);
1424            }
1425            let mut paints = Vec::new();
1426            hash_fill_bits(&source.fill, &mut hash);
1427            append(&mut paints, &mut meshes, source.fill, source.opacity);
1428            paints.len().hash(&mut hash);
1429            bytemuck::cast_slice::<_, u8>(&paints).hash(&mut hash);
1430            let key = (
1431                hash_vector_path(&source.path, PendingVectorOpKind::Fill, PathSmoothing::None),
1432                self.tolerance.to_bits(),
1433            );
1434            let geometry = if let Some(geometry) = next_geometry
1435                .get(&key)
1436                .cloned()
1437                .or_else(|| previous_geometry.remove(&key))
1438            {
1439                self.resource_churn_stats.vector_geometry_cache_hits += 1;
1440                geometry
1441            } else {
1442                let mut geometry: VertexBuffers<[f32; 2], u32> = VertexBuffers::new();
1443                let mut builder =
1444                    BuffersBuilder::new(&mut geometry, |v: FillVertex| v.position().to_array());
1445                if let Err(error) = FillTessellator::new().tessellate_path(
1446                    &source.path,
1447                    &FillOptions::default()
1448                        .with_tolerance(self.tolerance)
1449                        .with_fill_rule(FillRule::NonZero),
1450                    &mut builder,
1451                ) {
1452                    log::error!("alpha mask tessellation failed: {error:?}");
1453                }
1454                self.resource_churn_stats.vector_geometry_cache_misses += 1;
1455                self.resource_churn_stats.tessellated_vertices += geometry.vertices.len() as u64;
1456                self.resource_churn_stats.tessellated_indices += geometry.indices.len() as u64;
1457                Rc::new(geometry)
1458            };
1459            next_geometry.insert(key, Rc::clone(&geometry));
1460            let mapping = source.transform.then(&transform);
1461            let vertices = geometry
1462                .vertices
1463                .iter()
1464                .map(|v| mapping.transform_point(point(v[0], v[1])).to_array())
1465                .collect::<Vec<_>>();
1466            bytemuck::cast_slice::<_, u8>(&vertices).hash(&mut hash);
1467            geometry.indices.hash(&mut hash);
1468            draws.push(Draw {
1469                composition: source.composition,
1470                vertices,
1471                indices: geometry.indices.clone(),
1472                paints,
1473            });
1474        }
1475        self.alpha_geometry.insert(owner, next_geometry);
1476        feather.to_bits().hash(&mut hash);
1477        for value in self
1478            .render_backend
1479            .globals
1480            .resolution
1481            .into_iter()
1482            .chain(self.render_backend.globals.dpr)
1483        {
1484            value.to_bits().hash(&mut hash);
1485        }
1486        self.render_backend.render_alpha_mask(
1487            owner,
1488            hash.finish(),
1489            &draws,
1490            feather,
1491            parent,
1492            &meshes,
1493            &mut self.resource_churn_stats,
1494        );
1495        self.clip_stack.push(ClipReference::Alpha { owner });
1496        self.scene_dirty = true;
1497    }
1498
1499    pub fn resize(&mut self, width: f32, height: f32) {
1500        self.scene_dirty = true;
1501        self.render_backend.resize(width as u32, height as u32);
1502    }
1503
1504    pub fn resize_surface(&mut self, width: f32, height: f32) {
1505        self.scene_dirty = true;
1506        self.render_backend
1507            .resize_surface(width.round() as u32, height.round() as u32);
1508    }
1509
1510    pub fn set_viewport(&mut self, width: f32, height: f32, dpr: [f32; 2]) {
1511        self.scene_dirty = true;
1512        self.render_backend.set_viewport(width, height, dpr);
1513    }
1514
1515    pub fn max_surface_dimension(&self) -> u32 {
1516        self.render_backend.max_surface_dimension()
1517    }
1518
1519    pub fn scene_len(&self) -> usize {
1520        self.scene.len()
1521    }
1522
1523    pub fn visible_node_count(&self) -> usize {
1524        // Resource uploads are per physical surface. Skip nodes outside this tile's viewport so a
1525        // seam-crossing tile does not eagerly allocate buffers for the entire logical layer.
1526        let viewport_bounds = self.viewport_bounds();
1527        self.scene
1528            .iter()
1529            .filter(|(id, node)| {
1530                !self.source_nodes.contains_key(id) && node.intersects_bounds(&viewport_bounds)
1531            })
1532            .count()
1533    }
1534
1535    pub fn size(&self) -> (f32, f32) {
1536        let res = &self.render_backend.globals.resolution;
1537        (res[0], res[1])
1538    }
1539
1540    pub fn request_screenshot_capture(&mut self, request_id: u32) {
1541        self.scene_dirty = true;
1542        self.render_backend.request_screenshot_capture(request_id);
1543    }
1544
1545    pub fn take_screenshot_capture(&mut self, request_id: u32) -> Option<CapturedFrame> {
1546        self.render_backend.take_screenshot_capture(request_id)
1547    }
1548
1549    pub fn begin_node(&mut self, node_id: u32, mut z_index: i32, light_mask: u32) -> bool {
1550        if let Some(recording) = self.source_recordings.last_mut() {
1551            z_index = recording.nodes.len() as i32;
1552            recording.nodes.insert(node_id);
1553            self.source_nodes.insert(node_id, recording.owner);
1554        }
1555        self.current_node = Some(PendingNode {
1556            id: node_id,
1557            z_index,
1558            light_mask,
1559            owned_clip_keys: Vec::new(),
1560            kind: PendingNodeKind::Empty,
1561        });
1562        true
1563    }
1564
1565    pub fn end_node(&mut self, node_id: u32) -> bool {
1566        let Some(pending_node) = self.current_node.take() else {
1567            return true;
1568        };
1569        if pending_node.id != node_id {
1570            return false;
1571        }
1572
1573        let pending_z_index = pending_node.z_index;
1574        let pending_owned_clip_keys = pending_node.owned_clip_keys.clone();
1575        let pending_clip_count = pending_owned_clip_keys.len();
1576        let previous_owned_clip_keys = self
1577            .clip_owner_keys
1578            .get(&node_id)
1579            .cloned()
1580            .unwrap_or_default();
1581
1582        let previous = self.scene.remove(&node_id);
1583        self.lighting_dependencies = None;
1584        let prev_z = previous.as_ref().map(|node| node.z_index());
1585
1586        let updated = match pending_node.kind {
1587            PendingNodeKind::Empty => {
1588                if let Some(RetainedNode::Vector(existing)) = previous.as_ref() {
1589                    self.transform_arena.release_keys(&existing.transform_keys);
1590                }
1591                None
1592            }
1593            PendingNodeKind::Vector(buffers) => {
1594                let lighting_dependencies = LightingDependencies::from_ops(&buffers.ops);
1595                let geometry_signatures = buffers
1596                    .ops
1597                    .iter()
1598                    .map(|op| op.geometry_signature)
1599                    .collect::<Vec<_>>();
1600                let fill_signature = hash_vector_fills(&buffers.ops);
1601                let primitive_signature = hash_vector_primitives(&buffers.ops);
1602                let transform_signature = hash_vector_transforms(&buffers.ops);
1603                let local_transforms = build_local_transform_table(&buffers.ops);
1604                let (transform_keys, transform_ids) =
1605                    self.transform_arena.sync_node(node_id, &local_transforms);
1606                let transform_layout_signature = hash_transform_ids(&transform_ids);
1607                let resource_key = VectorResourceKey {
1608                    tolerance_bits: self.tolerance.to_bits(),
1609                    geometry_signatures: geometry_signatures.clone(),
1610                    fill_signature,
1611                    primitive_signature,
1612                    transform_layout_signature,
1613                };
1614
1615                match previous {
1616                    Some(RetainedNode::Vector(mut existing)) => {
1617                        if existing.transform_keys.len() > transform_keys.len() {
1618                            self.transform_arena
1619                                .release_keys(&existing.transform_keys[transform_keys.len()..]);
1620                        }
1621                        let reuse_geometry = existing.geometry_signatures == geometry_signatures;
1622                        let geometry_changed = !reuse_geometry;
1623                        let fill_changed = existing.fill_signature != fill_signature;
1624                        let primitive_changed = existing.primitive_signature != primitive_signature;
1625                        let transform_changed = existing.transform_signature != transform_signature;
1626                        let transform_layout_changed =
1627                            existing.transform_layout_signature != transform_layout_signature;
1628
1629                        if geometry_changed
1630                            || fill_changed
1631                            || primitive_changed
1632                            || transform_changed
1633                        {
1634                            rebuild_vector_buffers(
1635                                self.tolerance,
1636                                &buffers.ops,
1637                                &mut existing.buffers,
1638                                reuse_geometry,
1639                                !fill_changed,
1640                                &self.vector_geometry_cache,
1641                                &mut self.resource_churn_stats,
1642                            );
1643                        }
1644
1645                        if geometry_changed
1646                            || fill_changed
1647                            || primitive_changed
1648                            || transform_layout_changed
1649                        {
1650                            existing.retained_primitives = build_retained_primitives(
1651                                &existing.buffers.primitives,
1652                                &transform_ids,
1653                            );
1654                        }
1655
1656                        existing.transform_keys = transform_keys;
1657                        existing.lighting_dependencies = lighting_dependencies;
1658                        existing.clip_stack = buffers.clip_stack;
1659                        existing.z_index = pending_z_index;
1660                        existing.resource_dirty.geometry |= geometry_changed;
1661                        existing.resource_dirty.primitives |= geometry_changed
1662                            || fill_changed
1663                            || primitive_changed
1664                            || transform_layout_changed;
1665                        existing.resource_dirty.transforms = false;
1666                        existing.resource_dirty.fill |= fill_changed;
1667                        existing.geometry_signatures = geometry_signatures;
1668                        existing.fill_signature = fill_signature;
1669                        existing.primitive_signature = primitive_signature;
1670                        existing.transform_signature = transform_signature;
1671                        existing.transform_layout_signature = transform_layout_signature;
1672                        existing.resource_key = resource_key;
1673                        existing.bounds = compute_vector_node_bounds(&buffers.ops);
1674                        Some(RetainedNode::Vector(existing))
1675                    }
1676                    Some(_) => {
1677                        let mut buffers_out = new_cpu_buffers();
1678                        rebuild_vector_buffers(
1679                            self.tolerance,
1680                            &buffers.ops,
1681                            &mut buffers_out,
1682                            false,
1683                            false,
1684                            &self.vector_geometry_cache,
1685                            &mut self.resource_churn_stats,
1686                        );
1687                        let retained_primitives =
1688                            build_retained_primitives(&buffers_out.primitives, &transform_ids);
1689                        Some(RetainedNode::Vector(RetainedVectorNode {
1690                            lighting_dependencies,
1691                            mesh_page: None,
1692                            resource: None,
1693                            buffers: buffers_out,
1694                            retained_primitives,
1695                            transform_keys,
1696                            clip_stack: buffers.clip_stack,
1697                            z_index: pending_z_index,
1698                            bounds: compute_vector_node_bounds(&buffers.ops),
1699                            resource_dirty: VectorResourceDirty {
1700                                geometry: true,
1701                                primitives: true,
1702                                transforms: false,
1703                                fill: true,
1704                            },
1705                            geometry_signatures,
1706                            fill_signature,
1707                            primitive_signature,
1708                            transform_signature,
1709                            transform_layout_signature,
1710                            resource_key,
1711                            active_resource_key: None,
1712                        }))
1713                    }
1714                    None => {
1715                        let mut buffers_out = new_cpu_buffers();
1716                        rebuild_vector_buffers(
1717                            self.tolerance,
1718                            &buffers.ops,
1719                            &mut buffers_out,
1720                            false,
1721                            false,
1722                            &self.vector_geometry_cache,
1723                            &mut self.resource_churn_stats,
1724                        );
1725                        let retained_primitives =
1726                            build_retained_primitives(&buffers_out.primitives, &transform_ids);
1727                        Some(RetainedNode::Vector(RetainedVectorNode {
1728                            lighting_dependencies,
1729                            mesh_page: None,
1730                            resource: None,
1731                            buffers: buffers_out,
1732                            retained_primitives,
1733                            transform_keys,
1734                            clip_stack: buffers.clip_stack,
1735                            z_index: pending_z_index,
1736                            bounds: compute_vector_node_bounds(&buffers.ops),
1737                            resource_dirty: VectorResourceDirty {
1738                                geometry: true,
1739                                primitives: true,
1740                                transforms: false,
1741                                fill: true,
1742                            },
1743                            geometry_signatures,
1744                            fill_signature,
1745                            primitive_signature,
1746                            transform_signature,
1747                            transform_layout_signature,
1748                            resource_key,
1749                            active_resource_key: None,
1750                        }))
1751                    }
1752                }
1753            }
1754            PendingNodeKind::Image(image_node) => {
1755                if let Some(RetainedNode::Vector(existing)) = previous.as_ref() {
1756                    self.transform_arena.release_keys(&existing.transform_keys);
1757                }
1758                let draw = image_node.draw.unwrap_or_else(|| match previous {
1759                    Some(RetainedNode::Image(node)) => node.draw,
1760                    _ => unreachable!("unchanged image retains its draw resource"),
1761                });
1762                Some(RetainedNode::Image(RetainedImageNode {
1763                    signature: image_node.signature,
1764                    draw,
1765                    clip_stack: image_node.clip_stack,
1766                    z_index: pending_z_index,
1767                    bounds: image_node.bounds,
1768                }))
1769            }
1770        };
1771
1772        if previous_owned_clip_keys.len() > pending_clip_count {
1773            self.clip_arena
1774                .release_keys(&previous_owned_clip_keys[pending_clip_count..]);
1775        }
1776        if pending_owned_clip_keys.is_empty() {
1777            self.clip_owner_keys.remove(&node_id);
1778        } else {
1779            self.clip_owner_keys
1780                .insert(node_id, pending_owned_clip_keys.clone());
1781        }
1782
1783        match updated {
1784            Some(node) => {
1785                self.scene.insert(node_id, node);
1786                if prev_z != Some(pending_z_index) {
1787                    self.order_dirty = true;
1788                }
1789                self.scene_dirty = true;
1790            }
1791            None => {
1792                if prev_z.is_some() {
1793                    self.order_dirty = true;
1794                    self.scene_dirty = true;
1795                }
1796            }
1797        }
1798        true
1799    }
1800
1801    pub fn remove_node(&mut self, node_id: u32) -> bool {
1802        #[cfg(feature = "shaders")]
1803        {
1804            self.effect_captures.remove(&node_id);
1805            self.effect_nodes
1806                .retain(|child, owner| *child != node_id && *owner != node_id);
1807        }
1808        self.source_nodes.remove(&node_id);
1809        self.opacity_scopes.remove(&node_id);
1810        let mut removed = false;
1811        if let Some(node) = self.scene.remove(&node_id) {
1812            self.lighting_dependencies = None;
1813            if let RetainedNode::Vector(node) = &node {
1814                self.transform_arena.release_keys(&node.transform_keys);
1815            }
1816            removed = true;
1817        }
1818        if let Some(owned_clip_keys) = self.clip_owner_keys.remove(&node_id) {
1819            self.clip_arena.release_keys(&owned_clip_keys);
1820            removed = true;
1821        }
1822        if removed {
1823            self.order_dirty = true;
1824            self.scene_dirty = true;
1825        }
1826        removed
1827    }
1828
1829    fn ensure_vector_node(&mut self) -> Option<&mut PendingNode> {
1830        let current_node = self.current_node.as_mut()?;
1831        if matches!(current_node.kind, PendingNodeKind::Empty) {
1832            current_node.kind = PendingNodeKind::Vector(PendingVectorNode {
1833                ops: Vec::new(),
1834                clip_stack: self.clip_stack.clone(),
1835            });
1836        }
1837        Some(current_node)
1838    }
1839
1840    fn should_use_vector_scene_batch(&self) -> bool {
1841        // A retained mesh draw binds its own page, including every crossfade endpoint.
1842        // The concatenating fast path has one binding for the entire scene; use the
1843        // ordinary ordered retained draws whenever pages are present.
1844        self.source_captures.is_empty()
1845            && self.scene.values().all(|node| !matches!(node, RetainedNode::Vector(node) if !node.buffers.mesh_paints.is_empty()))
1846            && self.opacity_scopes.values().all(Vec::is_empty)
1847            && self.scene.len() >= 64
1848            && self
1849                .scene
1850                .values()
1851                .all(|node| matches!(node, RetainedNode::Vector(_)))
1852    }
1853
1854    fn ensure_vector_resources_for_immediate_scene(&mut self) {
1855        // Vector-scene batching has the same per-tile duplication pressure as the immediate path,
1856        // so apply the same viewport cull before appending geometry.
1857        let viewport_bounds = self.viewport_bounds();
1858        let node_ids: Vec<u32> = self
1859            .sorted_nodes
1860            .iter()
1861            .map(|(_, node_id)| *node_id)
1862            .collect();
1863        for node_id in node_ids {
1864            let Some(retained_node) = self.scene.remove(&node_id) else {
1865                continue;
1866            };
1867            let RetainedNode::Vector(mut node) = retained_node else {
1868                self.scene.insert(node_id, retained_node);
1869                continue;
1870            };
1871            if !boxes_intersect(&node.bounds, &viewport_bounds) {
1872                self.scene.insert(node_id, RetainedNode::Vector(node));
1873                continue;
1874            }
1875            self.render_backend.prepare_mesh_page(
1876                &mut node.mesh_page,
1877                &node.buffers.mesh_paints,
1878                &mut self.resource_churn_stats,
1879            );
1880            if !node.resource_dirty.any() && node.resource.is_some() {
1881                node.resource.as_mut().unwrap().mesh_bind_group =
1882                    node.mesh_page.as_ref().map(|page| page.bind_group.clone());
1883                self.scene.insert(node_id, RetainedNode::Vector(node));
1884                continue;
1885            }
1886
1887            self.ensure_vector_resource_for_node(&mut node);
1888            node.resource.as_mut().unwrap().mesh_bind_group =
1889                node.mesh_page.as_ref().map(|page| page.bind_group.clone());
1890            self.scene.insert(node_id, RetainedNode::Vector(node));
1891        }
1892    }
1893
1894    fn ensure_vector_resource_for_node(&mut self, node: &mut RetainedVectorNode) {
1895        let resource_key = node.resource_key.clone();
1896        if node.resource.is_some() && node.active_resource_key.as_ref() == Some(&resource_key) {
1897            node.resource_dirty = VectorResourceDirty::default();
1898            return;
1899        }
1900
1901        if let (Some(old_key), Some(resource)) =
1902            (node.active_resource_key.clone(), node.resource.as_mut())
1903        {
1904            let removed_cache_entry = if Rc::strong_count(resource.shared()) == 2 {
1905                self.vector_resource_cache
1906                    .borrow_mut()
1907                    .remove_if_same(&old_key, resource.shared())
1908            } else {
1909                None
1910            };
1911            if let Some(removed_cache_entry) = removed_cache_entry {
1912                self.resource_churn_stats.vector_resource_updates += 1;
1913                if let Some(upload_bytes) = self.render_backend.update_vector_resource(
1914                    resource,
1915                    &mut node.buffers,
1916                    &node.retained_primitives,
1917                    node.resource_dirty,
1918                ) {
1919                    self.resource_churn_stats.vector_resource_update_bytes += upload_bytes;
1920                    let cache_stats = self.vector_resource_cache.borrow_mut().insert(
1921                        resource_key.clone(),
1922                        Rc::clone(resource.shared()),
1923                        removed_cache_entry.bytes as u64,
1924                    );
1925                    self.merge_vector_resource_cache_stats(cache_stats);
1926                    node.active_resource_key = Some(resource_key);
1927                    node.resource_dirty = VectorResourceDirty::default();
1928                    return;
1929                }
1930            }
1931            self.resource_churn_stats.vector_resource_recreates += 1;
1932        }
1933
1934        let (cached_resource, cache_bytes) = {
1935            let mut cache = self.vector_resource_cache.borrow_mut();
1936            let cached_resource = cache.get(&resource_key);
1937            (cached_resource, cache.current_bytes())
1938        };
1939        if let Some(shared_resource) = cached_resource {
1940            self.resource_churn_stats.vector_resource_cache_hits += 1;
1941            self.resource_churn_stats.vector_resource_cache_bytes = self
1942                .resource_churn_stats
1943                .vector_resource_cache_bytes
1944                .max(cache_bytes as u64);
1945            node.resource = Some(
1946                self.render_backend
1947                    .create_vector_resource_from_shared(shared_resource),
1948            );
1949            node.active_resource_key = Some(resource_key);
1950            node.resource_dirty = VectorResourceDirty::default();
1951            return;
1952        }
1953
1954        self.resource_churn_stats.vector_resource_cache_misses += 1;
1955        self.resource_churn_stats.vector_resource_creates += 1;
1956        let (shared_resource, upload_bytes) = self
1957            .render_backend
1958            .create_shared_vector_resource(&mut node.buffers, &node.retained_primitives);
1959        self.resource_churn_stats.vector_resource_create_bytes += upload_bytes;
1960        let cache_stats = self.vector_resource_cache.borrow_mut().insert(
1961            resource_key.clone(),
1962            Rc::clone(&shared_resource),
1963            upload_bytes,
1964        );
1965        self.merge_vector_resource_cache_stats(cache_stats);
1966        node.resource = Some(
1967            self.render_backend
1968                .create_vector_resource_from_shared(shared_resource),
1969        );
1970        node.active_resource_key = Some(resource_key);
1971        node.resource_dirty = VectorResourceDirty::default();
1972    }
1973
1974    fn merge_vector_resource_cache_stats(&mut self, stats: VectorResourceCacheStats) {
1975        self.resource_churn_stats.vector_resource_cache_evictions += stats.evicted_entries;
1976        self.resource_churn_stats.vector_resource_cache_bytes = self
1977            .resource_churn_stats
1978            .vector_resource_cache_bytes
1979            .max(stats.current_bytes as u64);
1980    }
1981
1982    fn flush_vector_scene_batch(&mut self, defer_submit: bool) {
1983        // This path uploads batch-local transforms. Leave the scene arena dirty
1984        // until a later mixed/retained frame actually needs its GPU pages.
1985        self.clip_arena.flush_to_gpu(&mut self.render_backend);
1986        let viewport_bounds = self.viewport_bounds();
1987        let mut current_clip_stack: Vec<u32> = Vec::new();
1988        let mut current_batch_clip_stack: Option<Vec<ClipReference>> = None;
1989        let mut current_buffers = new_cpu_buffers();
1990        let mut current_segment_start: Option<usize> = None;
1991        let mut has_geometry = false;
1992        let mut segments = Vec::new();
1993        let mut batches = Vec::new();
1994        for (_, node_id) in &self.sorted_nodes {
1995            let Some(RetainedNode::Vector(node)) = self.scene.get(node_id) else {
1996                continue;
1997            };
1998            if !boxes_intersect(&node.bounds, &viewport_bounds) {
1999                continue;
2000            }
2001
2002            if let Some(batch_clip_stack) = current_batch_clip_stack.as_ref() {
2003                if !clip_stacks_match(batch_clip_stack, &node.clip_stack) {
2004                    if has_geometry {
2005                        push_primitive_segment(
2006                            &mut segments,
2007                            &mut current_clip_stack,
2008                            batch_clip_stack,
2009                            &self.clip_arena,
2010                            current_segment_start.take().unwrap_or(0),
2011                            current_buffers.geometry.indices.len(),
2012                        );
2013                        has_geometry = false;
2014                    }
2015                    current_batch_clip_stack = Some(node.clip_stack.clone());
2016                }
2017            } else {
2018                current_batch_clip_stack = Some(node.clip_stack.clone());
2019            }
2020
2021            // A clip change ends a segment, not the accumulated uniform batch.
2022            // Check all buffered geometry even when the current segment is empty.
2023            if !current_buffers.geometry.indices.is_empty()
2024                && would_exceed_batch_capacity(&current_buffers, &node.buffers)
2025            {
2026                if let Some(batch_clip_stack) = current_batch_clip_stack.as_ref() {
2027                    if has_geometry {
2028                        push_primitive_segment(
2029                            &mut segments,
2030                            &mut current_clip_stack,
2031                            batch_clip_stack,
2032                            &self.clip_arena,
2033                            current_segment_start.take().unwrap_or(0),
2034                            current_buffers.geometry.indices.len(),
2035                        );
2036                    }
2037                    push_primitive_batch(
2038                        &mut batches,
2039                        std::mem::replace(&mut current_buffers, new_cpu_buffers()),
2040                        std::mem::take(&mut segments),
2041                    );
2042                }
2043                current_batch_clip_stack = Some(node.clip_stack.clone());
2044            }
2045
2046            if current_segment_start.is_none() {
2047                current_segment_start = Some(current_buffers.geometry.indices.len());
2048            }
2049            append_cpu_buffers(&mut current_buffers, &node.buffers);
2050            has_geometry = true;
2051        }
2052
2053        if has_geometry {
2054            if let Some(batch_clip_stack) = current_batch_clip_stack.as_ref() {
2055                push_primitive_segment(
2056                    &mut segments,
2057                    &mut current_clip_stack,
2058                    batch_clip_stack,
2059                    &self.clip_arena,
2060                    current_segment_start.take().unwrap_or(0),
2061                    current_buffers.geometry.indices.len(),
2062                );
2063            }
2064        }
2065        push_primitive_batch(&mut batches, current_buffers, segments);
2066        if batches.is_empty() {
2067            self.render_backend.ensure_frame_cleared();
2068        } else {
2069            self.render_backend.render_primitive_batches(&mut batches);
2070        }
2071
2072        let (active_clip_signatures, active_clip_ids) =
2073            collect_active_clip_resources(&self.scene, &self.clip_arena);
2074        self.render_backend
2075            .retain_stencil_resources(&active_clip_signatures, &active_clip_ids);
2076        self.finish_backend_frame(defer_submit);
2077    }
2078
2079    fn finish_backend_frame(&mut self, defer_submit: bool) {
2080        if defer_submit {
2081            self.render_backend.finish_frame();
2082        } else {
2083            self.render_backend.present();
2084        }
2085    }
2086
2087    pub fn take_pending_command_buffers(&mut self) -> Vec<wgpu::CommandBuffer> {
2088        self.render_backend.take_pending_command_buffers()
2089    }
2090
2091    pub fn submit_command_buffers(&self, command_buffers: Vec<wgpu::CommandBuffer>) {
2092        self.render_backend.submit_command_buffers(command_buffers);
2093    }
2094
2095    pub fn complete_submitted_work(&mut self) {
2096        self.render_backend.complete_submitted_work();
2097    }
2098
2099    pub fn has_pending_submission_cleanup(&self) -> bool {
2100        self.render_backend.has_pending_submission_cleanup()
2101    }
2102
2103    pub fn present_deferred_frame(&mut self) {
2104        self.render_backend.present_queued_frame();
2105    }
2106
2107    fn viewport_bounds(&self) -> Box2D {
2108        let (width, height) = self.size();
2109        Box2D {
2110            min: point(
2111                self.render_backend.globals.origin[0],
2112                self.render_backend.globals.origin[1],
2113            ),
2114            max: point(
2115                self.render_backend.globals.origin[0] + width.max(0.0),
2116                self.render_backend.globals.origin[1] + height.max(0.0),
2117            ),
2118        }
2119    }
2120}
2121
2122struct SceneStateSave {
2123    transform_depth: usize,
2124    clip_depth: usize,
2125}
2126
2127struct PendingNode {
2128    id: u32,
2129    z_index: i32,
2130    light_mask: u32,
2131    owned_clip_keys: Vec<ClipArenaKey>,
2132    kind: PendingNodeKind,
2133}
2134
2135enum PendingNodeKind {
2136    Empty,
2137    Vector(PendingVectorNode),
2138    Image(PendingImageNode),
2139}
2140
2141struct PendingVectorNode {
2142    ops: Vec<PendingVectorOp>,
2143    clip_stack: Vec<ClipReference>,
2144}
2145
2146struct PendingVectorOp {
2147    path: Path,
2148    fill: Fill,
2149    material: Material,
2150    light_mask: u32,
2151    transform: Transform2D,
2152    opacity: f32,
2153    kind: PendingVectorOpKind,
2154    geometry_signature: u64,
2155    draw_range: DrawRange,
2156    smoothing: PathSmoothing,
2157}
2158
2159#[derive(Clone, Copy)]
2160enum PendingVectorOpKind {
2161    Fill,
2162    Stroke(f32, StrokeCap, StrokeJoin),
2163}
2164
2165struct PendingImageNode {
2166    signature: u64,
2167    draw: Option<RetainedImageDraw>,
2168    clip_stack: Vec<ClipReference>,
2169    bounds: Box2D,
2170}
2171
2172enum RetainedNode {
2173    #[cfg(feature = "shaders")]
2174    Effect(EffectNode),
2175    Vector(RetainedVectorNode),
2176    Image(RetainedImageNode),
2177}
2178
2179impl RetainedNode {
2180    fn z_index(&self) -> i32 {
2181        match self {
2182            RetainedNode::Vector(node) => node.z_index,
2183            RetainedNode::Image(node) => node.z_index,
2184            #[cfg(feature = "shaders")]
2185            RetainedNode::Effect(node) => node.z_index,
2186        }
2187    }
2188
2189    fn clip_stack(&self) -> &[ClipReference] {
2190        match self {
2191            RetainedNode::Vector(node) => &node.clip_stack,
2192            RetainedNode::Image(node) => &node.clip_stack,
2193            #[cfg(feature = "shaders")]
2194            RetainedNode::Effect(node) => &node.clip_stack,
2195        }
2196    }
2197
2198    fn intersects_bounds(&self, viewport_bounds: &Box2D) -> bool {
2199        match self {
2200            RetainedNode::Vector(node) => boxes_intersect(&node.bounds, viewport_bounds),
2201            RetainedNode::Image(node) => boxes_intersect(&node.bounds, viewport_bounds),
2202            #[cfg(feature = "shaders")]
2203            RetainedNode::Effect(node) => boxes_intersect(&node.bounds, viewport_bounds),
2204        }
2205    }
2206}
2207
2208struct RetainedVectorNode {
2209    lighting_dependencies: LightingDependencies,
2210    mesh_page: Option<Rc<crate::render_backend::mesh_paint::PaintPage>>,
2211    resource: Option<RetainedVectorResource>,
2212    buffers: CpuBuffers,
2213    retained_primitives: Vec<GpuPrimitive>,
2214    transform_keys: Vec<TransformArenaKey>,
2215    clip_stack: Vec<ClipReference>,
2216    z_index: i32,
2217    bounds: Box2D,
2218    resource_dirty: VectorResourceDirty,
2219    geometry_signatures: Vec<u64>,
2220    fill_signature: u64,
2221    primitive_signature: u64,
2222    transform_signature: u64,
2223    transform_layout_signature: u64,
2224    resource_key: VectorResourceKey,
2225    active_resource_key: Option<VectorResourceKey>,
2226}
2227
2228struct RetainedImageNode {
2229    signature: u64,
2230    draw: RetainedImageDraw,
2231    clip_stack: Vec<ClipReference>,
2232    z_index: i32,
2233    bounds: Box2D,
2234}
2235
2236fn new_cpu_buffers() -> CpuBuffers {
2237    CpuBuffers {
2238        geometry: VertexBuffers::new(),
2239        primitives: Vec::new(),
2240        colors: Vec::new(),
2241        gradients: Vec::new(),
2242        mesh_paints: Vec::new(),
2243        materials: Vec::new(),
2244        transforms: vec![GpuTransform::default()],
2245    }
2246}
2247
2248fn build_retained_primitives(
2249    local_primitives: &[GpuPrimitive],
2250    transform_ids: &[u32],
2251) -> Vec<GpuPrimitive> {
2252    local_primitives
2253        .iter()
2254        .map(|primitive| GpuPrimitive {
2255            transform_id: transform_ids
2256                .get(primitive.transform_id as usize)
2257                .copied()
2258                .unwrap_or(0),
2259            ..*primitive
2260        })
2261        .collect()
2262}
2263
2264fn record_retained_batch_stats(stats: &mut ResourceChurnStats, draws: &[RetainedDraw<'_>]) {
2265    if draws.is_empty() {
2266        return;
2267    }
2268    stats.retained_draw_batches += 1;
2269    stats.retained_draws += draws.len() as u64;
2270    for draw in draws {
2271        match draw {
2272            RetainedDraw::Vector(resource) if resource.is_drawable() => {
2273                stats.retained_vector_draws += 1
2274            }
2275            RetainedDraw::Vector(_) => {}
2276            #[cfg(feature = "shaders")]
2277            RetainedDraw::Effect(_) => {}
2278            RetainedDraw::Image { .. } => stats.retained_image_draws += 1,
2279        }
2280    }
2281}
2282
2283fn build_local_transform_table(ops: &[PendingVectorOp]) -> Vec<GpuTransform> {
2284    let mut transforms = vec![GpuTransform::default()];
2285    for op in ops {
2286        let transform = GpuTransform {
2287            transform: op.transform.to_arrays(),
2288            opacity: op.opacity,
2289            ..GpuTransform::default()
2290        };
2291        if transform.transform == GpuTransform::default().transform
2292            && (transform.opacity - GpuTransform::default().opacity).abs() <= f32::EPSILON
2293        {
2294            continue;
2295        }
2296        if transforms.iter().any(|existing| {
2297            existing.transform == transform.transform
2298                && (existing.opacity - transform.opacity).abs() <= f32::EPSILON
2299        }) {
2300            continue;
2301        }
2302        transforms.push(transform);
2303    }
2304    transforms
2305}
2306
2307fn rebuild_vector_buffers(
2308    tolerance: f32,
2309    ops: &[PendingVectorOp],
2310    buffers: &mut CpuBuffers,
2311    reuse_geometry: bool,
2312    reuse_fill: bool,
2313    geometry_cache: &SharedVectorGeometryCache,
2314    stats: &mut ResourceChurnStats,
2315) {
2316    stats.vector_buffer_rebuilds += 1;
2317    if !reuse_geometry {
2318        stats.vector_geometry_rebuilds += 1;
2319        buffers.geometry.vertices.clear();
2320        buffers.geometry.indices.clear();
2321    }
2322    buffers.primitives.clear();
2323    if !reuse_fill {
2324        buffers.colors.clear();
2325        buffers.gradients.clear();
2326        buffers.mesh_paints.clear();
2327        buffers.materials.clear();
2328    }
2329    buffers.transforms.truncate(1);
2330
2331    let mut next_color_id: u16 = 0;
2332    let mut next_gradient_id: u16 = 0;
2333    let mut next_material_id: u32 = 0;
2334
2335    for op in ops {
2336        let transform_id = push_transform_with_opacity(buffers, op.transform, op.opacity);
2337        let prim_id = if reuse_fill {
2338            push_primitive_with_existing_fill(
2339                buffers,
2340                &op.fill,
2341                transform_id,
2342                &mut next_color_id,
2343                &mut next_gradient_id,
2344                &mut next_material_id,
2345                op.draw_range,
2346                op.light_mask,
2347            )
2348        } else {
2349            push_primitive_def(
2350                buffers,
2351                op.fill.clone(),
2352                op.material,
2353                transform_id,
2354                op.draw_range,
2355                op.light_mask,
2356            )
2357        };
2358
2359        if reuse_geometry {
2360            continue;
2361        }
2362
2363        let cache_key = VectorGeometryCacheKey {
2364            geometry_signature: op.geometry_signature,
2365            tolerance_bits: tolerance.to_bits(),
2366        };
2367        let mut cache_hit = false;
2368        {
2369            let mut cache = geometry_cache.borrow_mut();
2370            if let Some(geometry) = cache.get(&cache_key) {
2371                stats.vector_geometry_cache_hits += 1;
2372                stats.cached_vertices_reused += geometry.vertices.len() as u64;
2373                stats.cached_indices_reused += geometry.indices.len() as u64;
2374                append_cached_geometry(
2375                    buffers,
2376                    &geometry.vertices,
2377                    &geometry.indices,
2378                    prim_id,
2379                    op.geometry_signature,
2380                );
2381                cache_hit = true;
2382            }
2383        }
2384        if cache_hit {
2385            continue;
2386        }
2387
2388        stats.vector_geometry_cache_misses += 1;
2389        let geometry = tessellate_vector_geometry(tolerance, op);
2390        stats.tessellated_vertices += geometry.vertices.len() as u64;
2391        stats.tessellated_indices += geometry.indices.len() as u64;
2392        append_cached_geometry(
2393            buffers,
2394            &geometry.vertices,
2395            &geometry.indices,
2396            prim_id,
2397            op.geometry_signature,
2398        );
2399        let insert_stats =
2400            geometry_cache
2401                .borrow_mut()
2402                .insert(cache_key, geometry.vertices, geometry.indices);
2403        stats.vector_geometry_cache_evictions += insert_stats.evicted_entries;
2404        stats.vector_geometry_cache_bytes = insert_stats.current_bytes as u64;
2405    }
2406}
2407
2408#[derive(Default)]
2409struct StrokeProgressMap {
2410    endpoint_corrections: Vec<f32>,
2411}
2412
2413impl StrokeProgressMap {
2414    fn correction_for_source(&self, source: VertexSource) -> f32 {
2415        let endpoint = match source {
2416            VertexSource::Endpoint { id } => id,
2417            VertexSource::Edge { from, .. } => from,
2418        };
2419        self.correction_for_endpoint(endpoint)
2420    }
2421
2422    fn correction_for_endpoint(&self, endpoint: EndpointId) -> f32 {
2423        self.endpoint_corrections
2424            .get(endpoint.0 as usize)
2425            .copied()
2426            .unwrap_or(0.0)
2427    }
2428}
2429
2430fn stroke_progress_map(path: &Path, tolerance: f32) -> StrokeProgressMap {
2431    let mut endpoint_corrections = Vec::new();
2432    let mut global_cursor = 0.0f32;
2433    let mut lyon_open_cursor = 0.0f32;
2434    let mut contour_correction = 0.0f32;
2435    let mut contour_length = 0.0f32;
2436
2437    for event in path.iter() {
2438        match event {
2439            PathEvent::Begin { at: _ } => {
2440                contour_correction = global_cursor - lyon_open_cursor;
2441                contour_length = 0.0;
2442                endpoint_corrections.push(contour_correction);
2443            }
2444            PathEvent::Line { from, to } => {
2445                contour_length += line_length(from, to);
2446                endpoint_corrections.push(contour_correction);
2447            }
2448            PathEvent::Quadratic { from, ctrl, to } => {
2449                contour_length += QuadraticBezierSegment { from, ctrl, to }
2450                    .approximate_length(tolerance)
2451                    .max(0.0);
2452                endpoint_corrections.push(contour_correction);
2453            }
2454            PathEvent::Cubic {
2455                from,
2456                ctrl1,
2457                ctrl2,
2458                to,
2459            } => {
2460                contour_length += CubicBezierSegment {
2461                    from,
2462                    ctrl1,
2463                    ctrl2,
2464                    to,
2465                }
2466                .approximate_length(tolerance)
2467                .max(0.0);
2468                endpoint_corrections.push(contour_correction);
2469            }
2470            PathEvent::End { first, last, close } => {
2471                let closed = close;
2472                if closed {
2473                    contour_length += line_length(last, first);
2474                }
2475                global_cursor += contour_length;
2476                if !closed {
2477                    lyon_open_cursor += contour_length;
2478                }
2479            }
2480        }
2481    }
2482
2483    StrokeProgressMap {
2484        endpoint_corrections,
2485    }
2486}
2487
2488#[derive(Clone, Copy)]
2489struct SmoothingParams {
2490    angle_cut_degrees: f32,
2491    passes: usize,
2492}
2493
2494fn smooth_lyon_path(path: &Path, smoothing: PathSmoothing) -> Path {
2495    let Some(params) = smoothing_params(smoothing) else {
2496        return path.clone();
2497    };
2498
2499    let mut builder = Path::builder();
2500    let mut run = Vec::new();
2501
2502    for event in path.iter() {
2503        match event {
2504            PathEvent::Begin { at } => {
2505                flush_smoothing_run(&mut builder, &mut run, params);
2506                builder.begin(at);
2507                run.clear();
2508                run.push(at);
2509            }
2510            PathEvent::Line { to, .. } => {
2511                if run.is_empty() {
2512                    builder.line_to(to);
2513                } else {
2514                    push_distinct_smoothing_point(&mut run, to);
2515                }
2516            }
2517            PathEvent::Quadratic { ctrl, to, .. } => {
2518                flush_smoothing_run(&mut builder, &mut run, params);
2519                builder.quadratic_bezier_to(ctrl, to);
2520                run.clear();
2521                run.push(to);
2522            }
2523            PathEvent::Cubic {
2524                ctrl1, ctrl2, to, ..
2525            } => {
2526                flush_smoothing_run(&mut builder, &mut run, params);
2527                builder.cubic_bezier_to(ctrl1, ctrl2, to);
2528                run.clear();
2529                run.push(to);
2530            }
2531            PathEvent::End { first, last, close } => {
2532                if close && smoothing_distance(last, first) > f32::EPSILON {
2533                    push_distinct_smoothing_point(&mut run, first);
2534                }
2535                flush_smoothing_run(&mut builder, &mut run, params);
2536                builder.end(close);
2537                run.clear();
2538            }
2539        }
2540    }
2541
2542    flush_smoothing_run(&mut builder, &mut run, params);
2543    builder.build()
2544}
2545
2546fn smoothing_params(smoothing: PathSmoothing) -> Option<SmoothingParams> {
2547    match smoothing {
2548        PathSmoothing::None => None,
2549        PathSmoothing::Light => Some(SmoothingParams {
2550            angle_cut_degrees: 130.0,
2551            passes: 1,
2552        }),
2553        PathSmoothing::Strong => Some(SmoothingParams {
2554            angle_cut_degrees: 95.0,
2555            passes: 2,
2556        }),
2557    }
2558}
2559
2560fn flush_smoothing_run(
2561    builder: &mut lyon::path::path::Builder,
2562    run: &mut Vec<LyonPoint>,
2563    params: SmoothingParams,
2564) {
2565    let clean = clean_smoothing_points(run);
2566    if clean.len() < 2 {
2567        run.clear();
2568        return;
2569    }
2570
2571    let mut cuts = vec![0usize];
2572    for index in 1..clean.len() - 1 {
2573        if smoothing_angle_degrees(clean[index - 1], clean[index], clean[index + 1])
2574            <= params.angle_cut_degrees
2575        {
2576            cuts.push(index);
2577        }
2578    }
2579    cuts.push(clean.len() - 1);
2580
2581    for pair in cuts.windows(2) {
2582        let start = pair[0];
2583        let end = pair[1];
2584        if end <= start {
2585            continue;
2586        }
2587        let subrun = smooth_smoothing_points(&clean[start..=end], params.passes);
2588        emit_smoothing_subrun(builder, &subrun);
2589    }
2590
2591    run.clear();
2592}
2593
2594fn emit_smoothing_subrun(builder: &mut lyon::path::path::Builder, points: &[LyonPoint]) {
2595    if points.len() < 2 {
2596        return;
2597    }
2598    if points.len() == 2 {
2599        builder.line_to(points[1]);
2600        return;
2601    }
2602
2603    for index in 0..points.len() - 1 {
2604        let p0 = if index == 0 {
2605            points[index]
2606        } else {
2607            points[index - 1]
2608        };
2609        let p1 = points[index];
2610        let p2 = points[index + 1];
2611        let p3 = if index + 2 < points.len() {
2612            points[index + 2]
2613        } else {
2614            points[index + 1]
2615        };
2616
2617        let control_1 = p1 + (p2 - p0) * (1.0 / 6.0);
2618        let control_2 = p2 - (p3 - p1) * (1.0 / 6.0);
2619        builder.cubic_bezier_to(control_1, control_2, p2);
2620    }
2621}
2622
2623fn smooth_smoothing_points(points: &[LyonPoint], passes: usize) -> Vec<LyonPoint> {
2624    if points.len() < 4 || passes == 0 {
2625        return points.to_vec();
2626    }
2627
2628    let mut output = points.to_vec();
2629    for _ in 0..passes {
2630        let mut next = Vec::with_capacity(output.len());
2631        next.push(output[0]);
2632        for index in 1..output.len() - 1 {
2633            next.push(LyonPoint::new(
2634                output[index - 1].x * 0.25 + output[index].x * 0.5 + output[index + 1].x * 0.25,
2635                output[index - 1].y * 0.25 + output[index].y * 0.5 + output[index + 1].y * 0.25,
2636            ));
2637        }
2638        next.push(output[output.len() - 1]);
2639        output = next;
2640    }
2641    output
2642}
2643
2644fn clean_smoothing_points(points: &[LyonPoint]) -> Vec<LyonPoint> {
2645    let mut clean = Vec::with_capacity(points.len());
2646    for point in points {
2647        push_distinct_smoothing_point(&mut clean, *point);
2648    }
2649    clean
2650}
2651
2652fn push_distinct_smoothing_point(points: &mut Vec<LyonPoint>, point: LyonPoint) {
2653    if points
2654        .last()
2655        .map(|last| smoothing_distance(*last, point) > f32::EPSILON)
2656        .unwrap_or(true)
2657    {
2658        points.push(point);
2659    }
2660}
2661
2662fn smoothing_angle_degrees(previous: LyonPoint, current: LyonPoint, next: LyonPoint) -> f32 {
2663    let vector_1 = previous - current;
2664    let vector_2 = next - current;
2665    let length_1 = vector_1.length();
2666    let length_2 = vector_2.length();
2667    if length_1 < f32::EPSILON || length_2 < f32::EPSILON {
2668        return 180.0;
2669    }
2670    let dot = ((vector_1.x * vector_2.x) + (vector_1.y * vector_2.y)) / (length_1 * length_2);
2671    dot.clamp(-1.0, 1.0).acos().to_degrees()
2672}
2673
2674fn smoothing_distance(a: LyonPoint, b: LyonPoint) -> f32 {
2675    (a - b).length()
2676}
2677
2678fn line_length(from: Point2D, to: Point2D) -> f32 {
2679    (to - from).length().max(0.0)
2680}
2681
2682fn tessellate_vector_geometry(
2683    tolerance: f32,
2684    op: &PendingVectorOp,
2685) -> VertexBuffers<GpuVertex, u16> {
2686    let mut geometry = VertexBuffers::new();
2687    let smoothed_path;
2688    let path = if op.smoothing == PathSmoothing::None {
2689        &op.path
2690    } else {
2691        smoothed_path = smooth_lyon_path(&op.path, op.smoothing);
2692        &smoothed_path
2693    };
2694    match op.kind {
2695        PendingVectorOpKind::Fill => {
2696            // Match ordinary native/SVG fills and our mask coverage. Font outlines
2697            // can overlap within a glyph; even-odd winding punches holes there.
2698            let options = FillOptions::tolerance(tolerance).with_fill_rule(FillRule::NonZero);
2699            let mut geometry_builder =
2700                BuffersBuilder::new(&mut geometry, |vertex: FillVertex| GpuVertex {
2701                    position: vertex.position().to_array(),
2702                    normal: [0.0; 2],
2703                    prim_id: 0,
2704                    path_progress: 0.0,
2705                });
2706            if let Err(err) =
2707                FillTessellator::new().tessellate_path(path, &options, &mut geometry_builder)
2708            {
2709                log::warn!("{:?}", err);
2710            }
2711        }
2712        PendingVectorOpKind::Stroke(stroke_width, stroke_cap, stroke_join) => {
2713            let options = StrokeOptions::tolerance(tolerance)
2714                .with_line_width(stroke_width)
2715                .with_line_cap(match stroke_cap {
2716                    StrokeCap::Butt => lyon::tessellation::LineCap::Butt,
2717                    StrokeCap::Round => lyon::tessellation::LineCap::Round,
2718                    StrokeCap::Square => lyon::tessellation::LineCap::Square,
2719                })
2720                .with_line_join(match stroke_join {
2721                    StrokeJoin::Miter => lyon::tessellation::LineJoin::Miter,
2722                    StrokeJoin::Round => lyon::tessellation::LineJoin::Round,
2723                    StrokeJoin::Bevel => lyon::tessellation::LineJoin::Bevel,
2724                });
2725            let progress_map = stroke_progress_map(path, tolerance);
2726            let max_advancement = std::cell::Cell::new(0.0f32);
2727            {
2728                let mut geometry_builder =
2729                    BuffersBuilder::new(&mut geometry, |vertex: StrokeVertex| {
2730                        let advancement = vertex.advancement()
2731                            + progress_map.correction_for_source(vertex.source());
2732                        let path_progress = if advancement.is_finite() {
2733                            max_advancement.set(max_advancement.get().max(advancement));
2734                            advancement.max(0.0)
2735                        } else {
2736                            0.0
2737                        };
2738                        GpuVertex {
2739                            position: vertex.position().to_array(),
2740                            normal: vertex.normal().to_array(),
2741                            prim_id: 0,
2742                            path_progress,
2743                        }
2744                    });
2745                if let Err(err) =
2746                    StrokeTessellator::new().tessellate_path(path, &options, &mut geometry_builder)
2747                {
2748                    log::warn!("{:?}", err);
2749                }
2750            }
2751            let total_advancement = max_advancement.get();
2752            if total_advancement > f32::EPSILON {
2753                for vertex in &mut geometry.vertices {
2754                    vertex.path_progress =
2755                        (vertex.path_progress / total_advancement).clamp(0.0, 1.0);
2756                }
2757            }
2758        }
2759    }
2760    geometry
2761}
2762
2763fn append_cached_geometry(
2764    buffers: &mut CpuBuffers,
2765    vertices: &[GpuVertex],
2766    indices: &[u16],
2767    prim_id: u32,
2768    geometry_signature: u64,
2769) {
2770    let vertex_offset = buffers.geometry.vertices.len();
2771    let Some(vertex_offset) = u16::try_from(vertex_offset).ok() else {
2772        log::warn!(
2773            "vector geometry cache append exceeded u16 vertex offset for signature {}",
2774            geometry_signature
2775        );
2776        return;
2777    };
2778    if indices
2779        .iter()
2780        .any(|index| (*index as usize) + vertex_offset as usize > u16::MAX as usize)
2781    {
2782        log::warn!(
2783            "vector geometry cache append exceeded u16 index capacity for signature {}",
2784            geometry_signature
2785        );
2786        return;
2787    }
2788
2789    buffers
2790        .geometry
2791        .vertices
2792        .extend(vertices.iter().map(|vertex| {
2793            let mut vertex = *vertex;
2794            vertex.prim_id = prim_id;
2795            vertex
2796        }));
2797    buffers
2798        .geometry
2799        .indices
2800        .extend(indices.iter().map(|index| *index + vertex_offset));
2801}
2802
2803fn vector_geometry_bytes(vertices: usize, indices: usize) -> usize {
2804    vertices * std::mem::size_of::<GpuVertex>() + indices * std::mem::size_of::<u16>()
2805}
2806
2807fn append_cpu_buffers(dst: &mut CpuBuffers, src: &CpuBuffers) {
2808    let vertex_offset = dst.geometry.vertices.len() as u16;
2809    let primitive_offset = dst.primitives.len() as u32;
2810    let src_uses_only_identity_transform = src.transforms.len() == 1
2811        && src.transforms[0].transform == GpuTransform::default().transform
2812        && (src.transforms[0].opacity - GpuTransform::default().opacity).abs() <= f32::EPSILON;
2813    let transform_offset = if src_uses_only_identity_transform {
2814        0
2815    } else {
2816        dst.transforms.len() as u32 - 1
2817    };
2818    let color_offset = dst.colors.len() as u16;
2819    let gradient_offset = dst.gradients.len() as u16;
2820    let material_offset = dst.materials.len() as u32;
2821
2822    dst.geometry
2823        .vertices
2824        .extend(src.geometry.vertices.iter().map(|vertex| {
2825            let mut vertex = *vertex;
2826            vertex.prim_id += primitive_offset;
2827            vertex
2828        }));
2829    dst.geometry.indices.extend(
2830        src.geometry
2831            .indices
2832            .iter()
2833            .map(|index| index.saturating_add(vertex_offset)),
2834    );
2835    dst.primitives
2836        .extend(src.primitives.iter().map(|primitive| {
2837            let mut primitive = *primitive;
2838            if primitive.transform_id != 0 {
2839                primitive.transform_id += transform_offset;
2840            }
2841            if primitive.fill_type_flag == 0 {
2842                primitive.fill_id = primitive.fill_id.saturating_add(color_offset);
2843            } else {
2844                primitive.fill_id = primitive.fill_id.saturating_add(gradient_offset);
2845            }
2846            primitive.material_id = primitive.material_id.saturating_add(material_offset);
2847            primitive
2848        }));
2849    if !src_uses_only_identity_transform {
2850        dst.transforms
2851            .extend(src.transforms.iter().skip(1).copied());
2852    }
2853    dst.colors.extend(src.colors.iter().copied());
2854    dst.gradients.extend(src.gradients.iter().copied());
2855    dst.materials.extend(src.materials.iter().copied());
2856}
2857
2858fn push_transform_with_opacity(
2859    buffers: &mut CpuBuffers,
2860    transform: Transform2D,
2861    opacity: f32,
2862) -> u32 {
2863    if transform == Transform2D::identity() && (opacity - 1.0).abs() <= f32::EPSILON {
2864        0
2865    } else {
2866        let transform_arrays = transform.to_arrays();
2867        if let Some(transform_id) = buffers.transforms.iter().position(|existing| {
2868            existing.transform == transform_arrays
2869                && (existing.opacity - opacity).abs() <= f32::EPSILON
2870        }) {
2871            return transform_id as u32;
2872        }
2873
2874        let transform_id = buffers.transforms.len() as u32;
2875        buffers.transforms.push(GpuTransform {
2876            transform: transform_arrays,
2877            opacity,
2878            ..GpuTransform::default()
2879        });
2880        transform_id
2881    }
2882}
2883
2884fn would_exceed_batch_capacity(dst: &CpuBuffers, src: &CpuBuffers) -> bool {
2885    let extra_transforms = src.transforms.len().saturating_sub(1);
2886
2887    dst.geometry.vertices.len() + src.geometry.vertices.len() > u16::MAX as usize
2888        || dst.primitives.len() + src.primitives.len() > MAX_BATCH_PRIMITIVES
2889        || dst.colors.len() + src.colors.len() > MAX_BATCH_COLORS
2890        || dst.gradients.len() + src.gradients.len() > MAX_BATCH_GRADIENTS
2891        || dst.materials.len() + src.materials.len() > MAX_BATCH_MATERIALS
2892        || dst.transforms.len() + extra_transforms > MAX_BATCH_TRANSFORMS
2893}
2894
2895fn hash_vector_fills(ops: &[PendingVectorOp]) -> u64 {
2896    let mut hasher = DefaultHasher::new();
2897    ops.len().hash(&mut hasher);
2898    for op in ops {
2899        hash_fill_bits(&op.fill, &mut hasher);
2900        hash_material_bits(&op.material, &mut hasher);
2901    }
2902    hasher.finish()
2903}
2904
2905fn hash_vector_primitives(ops: &[PendingVectorOp]) -> u64 {
2906    let mut hasher = DefaultHasher::new();
2907    ops.len().hash(&mut hasher);
2908    for op in ops {
2909        op.light_mask.hash(&mut hasher);
2910        for value in op.draw_range.as_gpu_range() {
2911            value.to_bits().hash(&mut hasher);
2912        }
2913    }
2914    hasher.finish()
2915}
2916
2917fn hash_vector_transforms(ops: &[PendingVectorOp]) -> u64 {
2918    let mut hasher = DefaultHasher::new();
2919    ops.len().hash(&mut hasher);
2920    for op in ops {
2921        hash_transform_bits(&op.transform, op.opacity, &mut hasher);
2922    }
2923    hasher.finish()
2924}
2925
2926fn hash_transform_ids(transform_ids: &[u32]) -> u64 {
2927    let mut hasher = DefaultHasher::new();
2928    transform_ids.len().hash(&mut hasher);
2929    for transform_id in transform_ids {
2930        transform_id.hash(&mut hasher);
2931    }
2932    hasher.finish()
2933}
2934
2935fn compute_vector_node_bounds(ops: &[PendingVectorOp]) -> Box2D {
2936    let mut bounds: Option<Box2D> = None;
2937    for op in ops {
2938        let Some(path_bounds) = path_control_bounds(&op.path) else {
2939            continue;
2940        };
2941        let mut op_bounds = transform_box(path_bounds, &op.transform);
2942        if let PendingVectorOpKind::Stroke(width, _, _) = op.kind {
2943            op_bounds = expand_box(op_bounds, width * 0.5);
2944        }
2945        bounds = Some(match bounds {
2946            Some(existing) => union_boxes(existing, op_bounds),
2947            None => op_bounds,
2948        });
2949    }
2950    bounds.unwrap_or_else(empty_box)
2951}
2952
2953fn path_control_bounds(path: &Path) -> Option<Box2D> {
2954    let mut min_x = f32::INFINITY;
2955    let mut min_y = f32::INFINITY;
2956    let mut max_x = f32::NEG_INFINITY;
2957    let mut max_y = f32::NEG_INFINITY;
2958
2959    let mut visit = |point: Point2D| {
2960        min_x = min_x.min(point.x);
2961        min_y = min_y.min(point.y);
2962        max_x = max_x.max(point.x);
2963        max_y = max_y.max(point.y);
2964    };
2965
2966    for event in path.iter() {
2967        match event {
2968            PathEvent::Begin { at } => visit(at),
2969            PathEvent::Line { from, to } => {
2970                visit(from);
2971                visit(to);
2972            }
2973            PathEvent::Quadratic { from, ctrl, to } => {
2974                visit(from);
2975                visit(ctrl);
2976                visit(to);
2977            }
2978            PathEvent::Cubic {
2979                from,
2980                ctrl1,
2981                ctrl2,
2982                to,
2983            } => {
2984                visit(from);
2985                visit(ctrl1);
2986                visit(ctrl2);
2987                visit(to);
2988            }
2989            PathEvent::End { first, last, .. } => {
2990                visit(first);
2991                visit(last);
2992            }
2993        }
2994    }
2995
2996    if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() {
2997        Some(Box2D {
2998            min: point(min_x, min_y),
2999            max: point(max_x, max_y),
3000        })
3001    } else {
3002        None
3003    }
3004}
3005
3006fn transform_box(bounds: Box2D, transform: &Transform2D) -> Box2D {
3007    let corners = [
3008        bounds.min,
3009        point(bounds.max.x, bounds.min.y),
3010        point(bounds.min.x, bounds.max.y),
3011        bounds.max,
3012    ];
3013    let mut min_x = f32::INFINITY;
3014    let mut min_y = f32::INFINITY;
3015    let mut max_x = f32::NEG_INFINITY;
3016    let mut max_y = f32::NEG_INFINITY;
3017    for corner in corners {
3018        let transformed = transform.transform_point(corner);
3019        min_x = min_x.min(transformed.x);
3020        min_y = min_y.min(transformed.y);
3021        max_x = max_x.max(transformed.x);
3022        max_y = max_y.max(transformed.y);
3023    }
3024    Box2D {
3025        min: point(min_x, min_y),
3026        max: point(max_x, max_y),
3027    }
3028}
3029
3030fn expand_box(bounds: Box2D, inset: f32) -> Box2D {
3031    Box2D {
3032        min: point(bounds.min.x - inset, bounds.min.y - inset),
3033        max: point(bounds.max.x + inset, bounds.max.y + inset),
3034    }
3035}
3036
3037fn union_boxes(left: Box2D, right: Box2D) -> Box2D {
3038    Box2D {
3039        min: point(left.min.x.min(right.min.x), left.min.y.min(right.min.y)),
3040        max: point(left.max.x.max(right.max.x), left.max.y.max(right.max.y)),
3041    }
3042}
3043
3044fn empty_box() -> Box2D {
3045    Box2D {
3046        min: point(0.0, 0.0),
3047        max: point(0.0, 0.0),
3048    }
3049}
3050
3051fn boxes_intersect(left: &Box2D, right: &Box2D) -> bool {
3052    left.min.x <= right.max.x
3053        && left.max.x >= right.min.x
3054        && left.min.y <= right.max.y
3055        && left.max.y >= right.min.y
3056}
3057
3058fn axis_aligned_rect_scissor(
3059    path: &Path,
3060    transform: &Transform2D,
3061    dpr: [f32; 2],
3062) -> Option<ScissorRect> {
3063    const EPSILON: f32 = 0.001;
3064    let mut points = Vec::new();
3065    for event in path.iter() {
3066        match event {
3067            PathEvent::Begin { at } => {
3068                push_unique_point(&mut points, transform.transform_point(at))
3069            }
3070            PathEvent::Line { to, .. } => {
3071                push_unique_point(&mut points, transform.transform_point(to));
3072            }
3073            PathEvent::End { close, .. } => {
3074                if !close {
3075                    return None;
3076                }
3077            }
3078            PathEvent::Quadratic { .. } | PathEvent::Cubic { .. } => return None,
3079        }
3080    }
3081
3082    if points.len() > 1 && points_close(points[0], *points.last().unwrap(), EPSILON) {
3083        points.pop();
3084    }
3085    if points.len() != 4 {
3086        return None;
3087    }
3088
3089    let mut xs = Vec::new();
3090    let mut ys = Vec::new();
3091    for point in &points {
3092        push_unique_scalar(&mut xs, point.x, EPSILON);
3093        push_unique_scalar(&mut ys, point.y, EPSILON);
3094    }
3095    if xs.len() != 2 || ys.len() != 2 {
3096        return None;
3097    }
3098    xs.sort_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal));
3099    ys.sort_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal));
3100    let rect = ScissorRect {
3101        min_x: xs[0],
3102        min_y: ys[0],
3103        max_x: xs[1],
3104        max_y: ys[1],
3105    };
3106    if rect.max_x - rect.min_x <= EPSILON || rect.max_y - rect.min_y <= EPSILON {
3107        return None;
3108    }
3109    // Scissors cover whole physical pixels. Rounding a fractional edge outward
3110    // leaks oversized ImageFit::Fill artwork beyond an adjacent gradient. Use
3111    // stencil coverage there so clips and painted edges share the same samples.
3112    if ![
3113        rect.min_x * dpr[0],
3114        rect.max_x * dpr[0],
3115        rect.min_y * dpr[1],
3116        rect.max_y * dpr[1],
3117    ]
3118    .iter()
3119    .all(|edge| edge.is_finite() && edge.fract() == 0.0)
3120    {
3121        return None;
3122    }
3123    let has_all_corners = [
3124        (rect.min_x, rect.min_y),
3125        (rect.max_x, rect.min_y),
3126        (rect.max_x, rect.max_y),
3127        (rect.min_x, rect.max_y),
3128    ]
3129    .iter()
3130    .all(|(x, y)| {
3131        points
3132            .iter()
3133            .any(|point| (point.x - *x).abs() <= EPSILON && (point.y - *y).abs() <= EPSILON)
3134    });
3135    has_all_corners.then_some(rect)
3136}
3137
3138fn push_unique_point(points: &mut Vec<Point2D>, point: Point2D) {
3139    const EPSILON: f32 = 0.001;
3140    if points
3141        .last()
3142        .is_some_and(|previous| points_close(*previous, point, EPSILON))
3143    {
3144        return;
3145    }
3146    points.push(point);
3147}
3148
3149fn points_close(left: Point2D, right: Point2D, epsilon: f32) -> bool {
3150    (left.x - right.x).abs() <= epsilon && (left.y - right.y).abs() <= epsilon
3151}
3152
3153fn push_unique_scalar(values: &mut Vec<f32>, value: f32, epsilon: f32) {
3154    if !values
3155        .iter()
3156        .any(|existing| (*existing - value).abs() <= epsilon)
3157    {
3158        values.push(value);
3159    }
3160}
3161
3162fn hash_transform_bits<H: Hasher>(transform: &Transform2D, opacity: f32, state: &mut H) {
3163    for row in transform.to_arrays() {
3164        for value in row {
3165            value.to_bits().hash(state);
3166        }
3167    }
3168    opacity.to_bits().hash(state);
3169}
3170
3171fn hash_fill_bits<H: Hasher>(fill: &Fill, state: &mut H) {
3172    match fill {
3173        Fill::Mesh {
3174            rows,
3175            pos,
3176            main_axis,
3177            off_axis,
3178        } => {
3179            3u8.hash(state);
3180            rows.len().hash(state);
3181            for row in rows {
3182                row.len().hash(state);
3183                for (position, color) in row {
3184                    position[0].to_bits().hash(state);
3185                    position[1].to_bits().hash(state);
3186                    hash_color_bits(color, state);
3187                }
3188            }
3189            for value in [
3190                pos.x,
3191                pos.y,
3192                main_axis.x,
3193                main_axis.y,
3194                off_axis.x,
3195                off_axis.y,
3196            ] {
3197                value.to_bits().hash(state);
3198            }
3199        }
3200        Fill::Blend(terms) => {
3201            2u8.hash(state);
3202            terms.len().hash(state);
3203            for (fill, weight) in terms {
3204                hash_fill_bits(fill, state);
3205                weight.to_bits().hash(state);
3206            }
3207        }
3208        Fill::Solid(color) => {
3209            0u8.hash(state);
3210            hash_color_bits(color, state);
3211        }
3212        Fill::Gradient {
3213            gradient_type,
3214            pos,
3215            main_axis,
3216            off_axis,
3217            stops,
3218        } => {
3219            1u8.hash(state);
3220            match gradient_type {
3221                GradientType::Linear => 0u8.hash(state),
3222                GradientType::Radial { focal_point } => {
3223                    1u8.hash(state);
3224                    focal_point.x.to_bits().hash(state);
3225                    focal_point.y.to_bits().hash(state);
3226                }
3227            }
3228            pos.x.to_bits().hash(state);
3229            pos.y.to_bits().hash(state);
3230            main_axis.x.to_bits().hash(state);
3231            main_axis.y.to_bits().hash(state);
3232            off_axis.x.to_bits().hash(state);
3233            off_axis.y.to_bits().hash(state);
3234            stops.len().hash(state);
3235            for stop in stops {
3236                hash_color_bits(&stop.color, state);
3237                stop.stop.to_bits().hash(state);
3238            }
3239        }
3240    }
3241}
3242
3243fn hash_material_bits<H: Hasher>(material: &Material, state: &mut H) {
3244    material.unlit.hash(state);
3245    for value in material.coefficients {
3246        value.to_bits().hash(state);
3247    }
3248    for value in material.emissive {
3249        value.to_bits().hash(state);
3250    }
3251}
3252
3253fn hash_color_bits<H: Hasher>(color: &Color, state: &mut H) {
3254    for value in color.rgba {
3255        value.to_bits().hash(state);
3256    }
3257}
3258
3259fn to_gpu_material(material: Material) -> GpuMaterial {
3260    if material.unlit {
3261        return GpuMaterial {
3262            coefficients: [-1.0, 0.0, 0.0, 0.0],
3263            emissive: [0.0; 4],
3264        };
3265    }
3266
3267    GpuMaterial {
3268        coefficients: material.coefficients,
3269        emissive: material.emissive,
3270    }
3271}
3272
3273fn to_gpu_scene_lighting(lighting: &SceneLighting) -> GpuSceneLighting {
3274    let mut out = GpuSceneLighting {
3275        ambient: [
3276            lighting.ambient_color.rgba[0],
3277            lighting.ambient_color.rgba[1],
3278            lighting.ambient_color.rgba[2],
3279            lighting.ambient_intensity.max(0.0),
3280        ],
3281        meta: [
3282            lighting.active as u32,
3283            0,
3284            lighting.ambient_is_authored as u32,
3285            0,
3286        ],
3287        ..GpuSceneLighting::default()
3288    };
3289
3290    let count = lighting.lights.len().min(MAX_SCENE_LIGHTS);
3291    out.meta[1] = count as u32;
3292    for (index, light) in lighting.lights.iter().take(count).enumerate() {
3293        out.lights[index] = GpuSceneLight {
3294            position: [light.position[0], light.position[1], light.position[2], 0.0],
3295            direction: [
3296                light.direction[0],
3297                light.direction[1],
3298                light.direction[2],
3299                0.0,
3300            ],
3301            color: [
3302                light.color.rgba[0],
3303                light.color.rgba[1],
3304                light.color.rgba[2],
3305                light.intensity.max(0.0),
3306            ],
3307            params: [
3308                match light.shape {
3309                    LightShape::Point => 0.0,
3310                    LightShape::Directional => 1.0,
3311                },
3312                light.radius.max(0.0),
3313                0.0,
3314                0.0,
3315            ],
3316        };
3317    }
3318
3319    out
3320}
3321
3322// Every mixture is sampled in one fragment invocation; drawing its leaves over
3323// each other would apply source-over repeatedly and change transparent colors.
3324fn append_gradient_paints(
3325    out: &mut Vec<GpuGradient>,
3326    meshes: &mut Vec<crate::render_backend::mesh_paint::MeshInput>,
3327    fill: Fill,
3328    weight: f32,
3329) {
3330    match fill {
3331        Fill::Mesh {
3332            rows,
3333            pos,
3334            main_axis,
3335            off_axis,
3336        } => {
3337            let index = meshes.len();
3338            let input = crate::render_backend::mesh_paint::MeshInput::new(
3339                rows,
3340                [main_axis.to_array(), off_axis.to_array()],
3341            );
3342            let mut gradient = GpuGradient {
3343                type_id: 3,
3344                weight,
3345                position: pos.to_array(),
3346                main_axis: main_axis.to_array(),
3347                off_axis: off_axis.to_array(),
3348                _padding_0: index as u32,
3349                ..Default::default()
3350            };
3351            gradient.colors[0] = input.domain;
3352            meshes.push(input);
3353            out.push(gradient);
3354        }
3355        Fill::Blend(terms) => {
3356            for (fill, inner_weight) in terms {
3357                append_gradient_paints(out, meshes, fill, weight * inner_weight);
3358            }
3359        }
3360        Fill::Solid(color) => {
3361            let mut paint = GpuGradient {
3362                type_id: 2,
3363                weight,
3364                ..Default::default()
3365            };
3366            paint.colors[0] = color.rgba;
3367            out.push(paint);
3368        }
3369        Fill::Gradient {
3370            gradient_type,
3371            pos,
3372            main_axis,
3373            off_axis,
3374            stops,
3375        } => {
3376            if stops.len() > 8 {
3377                log::warn!("can't draw gradients with more than 8 stops. truncating.");
3378            }
3379            let mut paint = GpuGradient {
3380                type_id: match gradient_type {
3381                    GradientType::Linear => 0,
3382                    GradientType::Radial { .. } => 1,
3383                },
3384                focal_point: match gradient_type {
3385                    GradientType::Radial { focal_point } => focal_point.to_array(),
3386                    GradientType::Linear => [0.0; 2],
3387                },
3388                position: pos.to_array(),
3389                main_axis: main_axis.to_array(),
3390                off_axis: off_axis.to_array(),
3391                stop_count: stops.len().min(8) as u32,
3392                weight,
3393                ..Default::default()
3394            };
3395            for (i, stop) in stops.iter().take(8).enumerate() {
3396                paint.colors[i] = stop.color.rgba;
3397                paint.stops[i] = stop.stop;
3398            }
3399            out.push(paint);
3400        }
3401    }
3402}
3403
3404fn push_primitive_def(
3405    buffers: &mut CpuBuffers,
3406    fill: Fill,
3407    material: Material,
3408    transform_id: u32,
3409    draw_range: DrawRange,
3410    light_mask: u32,
3411) -> u32 {
3412    let (fill_id, fill_type_flag, paint_count) = match fill {
3413        Fill::Solid(color) => {
3414            let fill_id = buffers.colors.len() as u16;
3415            buffers.colors.push(GpuColor { color: color.rgba });
3416            (fill_id, 0, 1)
3417        }
3418        fill => {
3419            let fill_id =
3420                u16::try_from(buffers.gradients.len()).expect("paint buffer index overflow");
3421            let is_blend = matches!(fill, Fill::Blend(_));
3422            append_gradient_paints(&mut buffers.gradients, &mut buffers.mesh_paints, fill, 1.0);
3423            let count = buffers.gradients.len() - usize::from(fill_id);
3424            (fill_id, if is_blend { 2 } else { 1 }, count as u32)
3425        }
3426    };
3427    let material_id = buffers.materials.len() as u32;
3428    buffers.materials.push(to_gpu_material(material));
3429    let primitive = GpuPrimitive {
3430        fill_id,
3431        fill_type_flag,
3432        material_id,
3433        transform_id,
3434        z_index: 0,
3435        draw_range: draw_range.as_gpu_range(),
3436        light_mask,
3437        paint_count,
3438        _padding: [0; 2],
3439    };
3440    let prim_id = buffers.primitives.len() as u32;
3441    buffers.primitives.push(primitive);
3442    prim_id
3443}
3444
3445fn push_primitive_with_existing_fill(
3446    buffers: &mut CpuBuffers,
3447    fill: &Fill,
3448    transform_id: u32,
3449    next_color_id: &mut u16,
3450    next_gradient_id: &mut u16,
3451    next_material_id: &mut u32,
3452    draw_range: DrawRange,
3453    light_mask: u32,
3454) -> u32 {
3455    let (fill_id, fill_type_flag, paint_count) = match fill {
3456        Fill::Solid(_) => {
3457            let fill_id = *next_color_id;
3458            *next_color_id = next_color_id.saturating_add(1);
3459            (fill_id, 0, 1)
3460        }
3461        Fill::Blend(_) => {
3462            let fill_id = *next_gradient_id;
3463            let count = fill.paint_count();
3464            *next_gradient_id = next_gradient_id
3465                .checked_add(u16::try_from(count).expect("paint count overflow"))
3466                .expect("paint buffer index overflow");
3467            (fill_id, 2, count as u32)
3468        }
3469        Fill::Gradient { .. } | Fill::Mesh { .. } => {
3470            let fill_id = *next_gradient_id;
3471            *next_gradient_id = next_gradient_id.saturating_add(1);
3472            (fill_id, 1, 1)
3473        }
3474    };
3475    let material_id = *next_material_id;
3476    *next_material_id = next_material_id.saturating_add(1);
3477    let prim_id = buffers.primitives.len() as u32;
3478    buffers.primitives.push(GpuPrimitive {
3479        fill_id,
3480        fill_type_flag,
3481        material_id,
3482        transform_id,
3483        z_index: 0,
3484        draw_range: draw_range.as_gpu_range(),
3485        light_mask,
3486        paint_count,
3487        _padding: [0; 2],
3488    });
3489    prim_id
3490}
3491
3492fn hash_vector_path(path: &Path, kind: PendingVectorOpKind, smoothing: PathSmoothing) -> u64 {
3493    let mut hasher = std::collections::hash_map::DefaultHasher::new();
3494    smoothing.hash(&mut hasher);
3495    match kind {
3496        PendingVectorOpKind::Fill => 0u8.hash(&mut hasher),
3497        PendingVectorOpKind::Stroke(width, cap, join) => {
3498            1u8.hash(&mut hasher);
3499            width.to_bits().hash(&mut hasher);
3500            cap.hash(&mut hasher);
3501            join.hash(&mut hasher);
3502        }
3503    }
3504    for event in path.iter() {
3505        match event {
3506            PathEvent::Begin { at } => {
3507                0u8.hash(&mut hasher);
3508                at.x.to_bits().hash(&mut hasher);
3509                at.y.to_bits().hash(&mut hasher);
3510            }
3511            PathEvent::Line { from, to } => {
3512                1u8.hash(&mut hasher);
3513                from.x.to_bits().hash(&mut hasher);
3514                from.y.to_bits().hash(&mut hasher);
3515                to.x.to_bits().hash(&mut hasher);
3516                to.y.to_bits().hash(&mut hasher);
3517            }
3518            PathEvent::Quadratic { from, ctrl, to } => {
3519                2u8.hash(&mut hasher);
3520                from.x.to_bits().hash(&mut hasher);
3521                from.y.to_bits().hash(&mut hasher);
3522                ctrl.x.to_bits().hash(&mut hasher);
3523                ctrl.y.to_bits().hash(&mut hasher);
3524                to.x.to_bits().hash(&mut hasher);
3525                to.y.to_bits().hash(&mut hasher);
3526            }
3527            PathEvent::Cubic {
3528                from,
3529                ctrl1,
3530                ctrl2,
3531                to,
3532            } => {
3533                3u8.hash(&mut hasher);
3534                from.x.to_bits().hash(&mut hasher);
3535                from.y.to_bits().hash(&mut hasher);
3536                ctrl1.x.to_bits().hash(&mut hasher);
3537                ctrl1.y.to_bits().hash(&mut hasher);
3538                ctrl2.x.to_bits().hash(&mut hasher);
3539                ctrl2.y.to_bits().hash(&mut hasher);
3540                to.x.to_bits().hash(&mut hasher);
3541                to.y.to_bits().hash(&mut hasher);
3542            }
3543            PathEvent::End { last, first, close } => {
3544                4u8.hash(&mut hasher);
3545                last.x.to_bits().hash(&mut hasher);
3546                last.y.to_bits().hash(&mut hasher);
3547                first.x.to_bits().hash(&mut hasher);
3548                first.y.to_bits().hash(&mut hasher);
3549                close.hash(&mut hasher);
3550            }
3551        }
3552    }
3553    hasher.finish()
3554}
3555
3556fn hash_clip_geometry(geometry: &VertexBuffers<stencil::Vertex, u16>) -> u64 {
3557    let mut hasher = std::collections::hash_map::DefaultHasher::new();
3558    geometry.indices.hash(&mut hasher);
3559    for vertex in &geometry.vertices {
3560        vertex.position[0].to_bits().hash(&mut hasher);
3561        vertex.position[1].to_bits().hash(&mut hasher);
3562    }
3563    hasher.finish()
3564}
3565
3566fn tessellate_clip_geometry(path: &Path, tolerance: f32) -> VertexBuffers<stencil::Vertex, u16> {
3567    // A Mask combines every coverage contour from its source subtree into one
3568    // compound clip path. Non-zero winding preserves their documented union;
3569    // even-odd fill would cancel regions covered by an even number of shapes.
3570    let options = FillOptions::tolerance(tolerance).with_fill_rule(FillRule::NonZero);
3571    let mut geometry = VertexBuffers::new();
3572    let mut geometry_builder =
3573        BuffersBuilder::new(&mut geometry, |vertex: FillVertex| stencil::Vertex {
3574            position: vertex.position().to_array(),
3575        });
3576    if let Err(err) = FillTessellator::new().tessellate_path(path, &options, &mut geometry_builder)
3577    {
3578        log::warn!("{:?}", err);
3579    }
3580    geometry
3581}
3582
3583fn clip_stack_sync<'a>(
3584    current_clip_stack: &[u32],
3585    desired_clip_stack: &[ClipReference],
3586    clip_arena: &'a ClipArena,
3587) -> (usize, Vec<stencil::ClipDraw<'a>>) {
3588    let desired_stencil_clip_ids: Vec<u32> = desired_clip_stack
3589        .iter()
3590        .filter_map(|clip| match clip {
3591            ClipReference::Stencil { clip_id } => Some(*clip_id),
3592            ClipReference::Scissor(_) | ClipReference::Alpha { .. } => None,
3593        })
3594        .collect();
3595    let shared_prefix = current_clip_stack
3596        .iter()
3597        .zip(desired_stencil_clip_ids.iter())
3598        .take_while(|(left, right)| left == right)
3599        .count();
3600    let mut clip_draws = Vec::new();
3601    for clip_id in desired_stencil_clip_ids.iter().skip(shared_prefix) {
3602        let Some(entry) = clip_arena.get(*clip_id) else {
3603            log::error!("missing clip arena entry for clip {}", clip_id);
3604            continue;
3605        };
3606        clip_draws.push(stencil::ClipDraw {
3607            clip_id: *clip_id,
3608            geometry_signature: entry.geometry_signature,
3609            geometry: &entry.geometry,
3610        });
3611    }
3612    (shared_prefix, clip_draws)
3613}
3614
3615fn update_current_clip_stack(
3616    current_clip_stack: &mut Vec<u32>,
3617    shared_prefix: usize,
3618    clip_draws: &[stencil::ClipDraw<'_>],
3619) {
3620    current_clip_stack.truncate(shared_prefix);
3621    current_clip_stack.extend(clip_draws.iter().map(|clip| clip.clip_id));
3622}
3623
3624fn push_primitive_segment<'a>(
3625    segments: &mut Vec<PrimitiveBatchSegment<'a>>,
3626    current_clip_stack: &mut Vec<u32>,
3627    desired_clip_stack: &[ClipReference],
3628    clip_arena: &'a ClipArena,
3629    index_start: usize,
3630    index_end: usize,
3631) {
3632    if index_end <= index_start {
3633        return;
3634    }
3635    let (shared_prefix, clip_draws) =
3636        clip_stack_sync(current_clip_stack, desired_clip_stack, clip_arena);
3637    update_current_clip_stack(current_clip_stack, shared_prefix, &clip_draws);
3638    segments.push(PrimitiveBatchSegment {
3639        alpha_mask: alpha_for_clip_stack(desired_clip_stack),
3640        stencil_depth: shared_prefix as u32,
3641        clips: clip_draws,
3642        scissor: scissor_for_clip_stack(desired_clip_stack),
3643        index_start: index_start as u32,
3644        index_count: (index_end - index_start) as u32,
3645    });
3646}
3647
3648fn push_primitive_batch<'a>(
3649    batches: &mut Vec<PrimitiveBatch<'a>>,
3650    buffers: CpuBuffers,
3651    segments: Vec<PrimitiveBatchSegment<'a>>,
3652) {
3653    if segments.is_empty() {
3654        return;
3655    }
3656    batches.push(PrimitiveBatch { buffers, segments });
3657}
3658
3659// Keep command ordering testable without creating a platform GPU surface.
3660trait RetainedClipBackend {
3661    fn draw_runs(&mut self, runs: &[RetainedBatchRun<'_>]);
3662    fn sync_stencil(&mut self, depth: u32, clips: &[stencil::ClipDraw<'_>]);
3663}
3664
3665impl RetainedClipBackend for RenderBackend<'_> {
3666    fn draw_runs(&mut self, runs: &[RetainedBatchRun<'_>]) {
3667        self.draw_retained_batch_runs(runs);
3668    }
3669
3670    fn sync_stencil(&mut self, depth: u32, clips: &[stencil::ClipDraw<'_>]) {
3671        self.sync_stencil_stack(depth, clips);
3672    }
3673}
3674
3675fn sync_retained_clip_stack(
3676    render_backend: &mut impl RetainedClipBackend,
3677    retained_runs: &mut Vec<RetainedBatchRun<'_>>,
3678    current_clip_stack: &mut Vec<u32>,
3679    desired_clip_stack: &[ClipReference],
3680    clip_arena: &ClipArena,
3681) {
3682    let (shared_prefix, clip_draws) =
3683        clip_stack_sync(current_clip_stack, desired_clip_stack, clip_arena);
3684    // Runs retain a stencil reference, not a snapshot of the stencil texture. Submit the
3685    // previous runs before popping/pushing clips, or sibling Frames all see the final clip.
3686    // Scissor/alpha-only changes leave the stencil intact and can still share one draw pass.
3687    if shared_prefix != current_clip_stack.len() || !clip_draws.is_empty() {
3688        render_backend.draw_runs(retained_runs);
3689        retained_runs.clear();
3690    }
3691    render_backend.sync_stencil(shared_prefix as u32, &clip_draws);
3692    update_current_clip_stack(current_clip_stack, shared_prefix, &clip_draws);
3693}
3694
3695fn scissor_for_clip_stack(clip_stack: &[ClipReference]) -> Option<ScissorRect> {
3696    let mut scissor = None;
3697    for clip in clip_stack {
3698        let ClipReference::Scissor(rect) = clip else {
3699            continue;
3700        };
3701        scissor = Some(match scissor {
3702            Some(existing) => intersect_scissor_rects(existing, *rect),
3703            None => *rect,
3704        });
3705    }
3706    scissor
3707}
3708
3709fn alpha_for_clip_stack(stack: &[ClipReference]) -> Option<u32> {
3710    stack.iter().rev().find_map(|clip| match clip {
3711        ClipReference::Alpha { owner } => Some(*owner),
3712        _ => None,
3713    })
3714}
3715
3716fn intersect_scissor_rects(left: ScissorRect, right: ScissorRect) -> ScissorRect {
3717    ScissorRect {
3718        min_x: left.min_x.max(right.min_x),
3719        min_y: left.min_y.max(right.min_y),
3720        max_x: left.max_x.min(right.max_x),
3721        max_y: left.max_y.min(right.max_y),
3722    }
3723}
3724
3725fn collect_active_clip_resources(
3726    scene: &HashMap<u32, RetainedNode>,
3727    clip_arena: &ClipArena,
3728) -> (HashSet<u64>, HashSet<u32>) {
3729    let mut signatures = HashSet::new();
3730    let mut clip_ids = HashSet::new();
3731    for node in scene.values() {
3732        for clip in node.clip_stack() {
3733            let ClipReference::Stencil { clip_id } = clip else {
3734                continue;
3735            };
3736            clip_ids.insert(*clip_id);
3737            if let Some(entry) = clip_arena.get(*clip_id) {
3738                signatures.insert(entry.geometry_signature);
3739            }
3740        }
3741    }
3742    (signatures, clip_ids)
3743}
3744
3745fn clip_stacks_match(left: &[ClipReference], right: &[ClipReference]) -> bool {
3746    left == right
3747}
3748
3749#[derive(Debug, Clone, Copy)]
3750/// One color stop in a GPU gradient fill.
3751pub struct GradientStop {
3752    pub color: Color,
3753    pub stop: f32,
3754}
3755
3756#[derive(Debug, Clone, Copy)]
3757/// Shape of a GPU gradient fill.
3758pub enum GradientType {
3759    Linear,
3760    /// Unit outer circle centered at `pos`, mapped by `main_axis`/`off_axis`.
3761    /// Stops are normalized (0..1); the focal point is in that unit circle's space.
3762    Radial {
3763        focal_point: Point2D,
3764    },
3765}
3766
3767#[derive(Debug, Clone, Copy, PartialEq)]
3768/// Linear RGBA color used by the low-level renderer.
3769pub struct Color {
3770    pub(crate) rgba: [f32; 4],
3771}
3772
3773impl Color {
3774    /// Construct a color from linear RGBA channels in the range 0.0-1.0.
3775    pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
3776        Self { rgba: [r, g, b, a] }
3777    }
3778
3779    /// Construct a color from HSV plus alpha, with hue normalized to 0.0-1.0.
3780    pub fn hsva(h: f32, s: f32, v: f32, a: f32) -> Self {
3781        let i = (h * 6.0).floor() as i32;
3782        let f = h * 6.0 - i as f32;
3783        let p = v * (1.0 - s);
3784        let q = v * (1.0 - f * s);
3785        let t = v * (1.0 - (1.0 - f) * s);
3786
3787        let (r, g, b) = match i % 6 {
3788            0 => (v, t, p),
3789            1 => (q, v, p),
3790            2 => (p, v, t),
3791            3 => (p, q, v),
3792            4 => (t, p, v),
3793            _ => (v, p, q),
3794        };
3795        Self { rgba: [r, g, b, a] }
3796    }
3797
3798    /// Construct a color from HLC/Lab-style components plus alpha.
3799    // Credit piet library: https://docs.rs/piet/latest/src/piet/color.rs.html#130-173
3800    pub fn hlca(h: f32, l: f32, c: f32, a: f32) -> Self {
3801        // The reverse transformation from Lab to XYZ, see
3802        // https://en.wikipedia.org/wiki/CIELAB_color_space
3803        fn f_inv(t: f32) -> f32 {
3804            let d = 6. / 29.;
3805            if t > d {
3806                t.powi(3)
3807            } else {
3808                3. * d * d * (t - 4. / 29.)
3809            }
3810        }
3811        let th = h * (std::f32::consts::PI / 180.);
3812        let a_2 = c * th.cos();
3813        let b = c * th.sin();
3814        let ll = (l + 16.) * (1. / 116.);
3815        // Produce raw XYZ values
3816        let x = f_inv(ll + a_2 * (1. / 500.));
3817        let y = f_inv(ll);
3818        let z = f_inv(ll - b * (1. / 200.));
3819        // This matrix is the concatenation of three sources.
3820        // First, the white point is taken to be ICC standard D50, so
3821        // the diagonal matrix of [0.9642, 1, 0.8249]. Note that there
3822        // is some controversy around this value. However, it matches
3823        // the other matrices, thus minimizing chroma error.
3824        //
3825        // Second, an adaption matrix from D50 to D65. This is the
3826        // inverse of the recommended D50 to D65 adaptation matrix
3827        // from the W3C sRGB spec:
3828        // https://www.w3.org/Graphics/Color/srgb
3829        //
3830        // Finally, the conversion from XYZ to linear sRGB values,
3831        // also taken from the W3C sRGB spec.
3832        let r_lin = 3.02172918 * x - 1.61692294 * y - 0.40480625 * z;
3833        let g_lin = -0.94339358 * x + 1.91584267 * y + 0.02755094 * z;
3834        let b_lin = 0.06945666 * x - 0.22903204 * y + 1.15957526 * z;
3835        fn gamma(u: f32) -> f32 {
3836            if u <= 0.0031308 {
3837                12.92 * u
3838            } else {
3839                1.055 * u.powf(1. / 2.4) - 0.055
3840            }
3841        }
3842        Self {
3843            rgba: [
3844                gamma(r_lin).clamp(0.0, 1.0),
3845                gamma(g_lin).clamp(0.0, 1.0),
3846                gamma(b_lin).clamp(0.0, 1.0),
3847                a,
3848            ],
3849        }
3850    }
3851}
3852
3853#[derive(Debug, Clone)]
3854/// Fill style for a tessellated vector path.
3855pub enum Fill {
3856    /// Row-connected mesh in normalized paint coordinates. The axes map that domain to scene
3857    /// coordinates; row order fixes connectivity independently of each anchor's x/y position.
3858    Mesh {
3859        rows: Vec<Vec<([f32; 2], Color)>>,
3860        pos: Point2D,
3861        main_axis: Vector2D,
3862        off_axis: Vector2D,
3863    },
3864    /// Weighted premultiplied paint mixture, sampled before compositing.
3865    Blend(Vec<(Fill, f32)>),
3866    Solid(Color),
3867    Gradient {
3868        gradient_type: GradientType,
3869        pos: Point2D,
3870        main_axis: Vector2D,
3871        off_axis: Vector2D,
3872        stops: Vec<GradientStop>,
3873    },
3874}
3875
3876impl Fill {
3877    fn paint_count(&self) -> usize {
3878        match self {
3879            Self::Blend(terms) => terms.iter().map(|(f, _)| f.paint_count()).sum(),
3880            _ => 1,
3881        }
3882    }
3883}
3884
3885#[derive(Debug, Clone, Copy, PartialEq)]
3886/// Light-reactive material coefficients for a tessellated vector path.
3887pub struct Material {
3888    pub coefficients: [f32; 4],
3889    pub emissive: [f32; 4],
3890    pub unlit: bool,
3891}
3892
3893impl Material {
3894    pub fn matte() -> Self {
3895        Self::default()
3896    }
3897
3898    pub fn custom(
3899        ambient: f32,
3900        diffuse: f32,
3901        specular: f32,
3902        roughness: f32,
3903        emissive: Color,
3904        emissive_intensity: f32,
3905    ) -> Self {
3906        Self {
3907            coefficients: [
3908                ambient.clamp(0.0, 8.0),
3909                diffuse.clamp(0.0, 8.0),
3910                specular.clamp(0.0, 8.0),
3911                roughness.clamp(0.0, 1.0),
3912            ],
3913            emissive: [
3914                emissive.rgba[0],
3915                emissive.rgba[1],
3916                emissive.rgba[2],
3917                emissive_intensity.max(0.0),
3918            ],
3919            unlit: false,
3920        }
3921    }
3922
3923    pub fn unlit() -> Self {
3924        Self {
3925            coefficients: [0.0; 4],
3926            emissive: [0.0; 4],
3927            unlit: true,
3928        }
3929    }
3930}
3931
3932impl Default for Material {
3933    fn default() -> Self {
3934        Self {
3935            coefficients: [1.0, 0.82, 0.08, 0.78],
3936            emissive: [0.0; 4],
3937            unlit: false,
3938        }
3939    }
3940}
3941
3942#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3943/// Shape of a scene light.
3944pub enum LightShape {
3945    Point,
3946    Directional,
3947}
3948
3949#[derive(Debug, Clone, Copy, PartialEq)]
3950/// A resolved scene light for the low-level renderer.
3951pub struct SceneLight {
3952    pub shape: LightShape,
3953    pub position: [f32; 3],
3954    pub direction: [f32; 3],
3955    pub color: Color,
3956    pub intensity: f32,
3957    pub radius: f32,
3958}
3959
3960#[derive(Debug, Clone, PartialEq)]
3961/// Resolved lighting state for one retained vector scene.
3962pub struct SceneLighting {
3963    pub active: bool,
3964    pub ambient_is_authored: bool,
3965    pub ambient_color: Color,
3966    pub ambient_intensity: f32,
3967    pub lights: Vec<SceneLight>,
3968}
3969
3970impl Default for SceneLighting {
3971    fn default() -> Self {
3972        Self {
3973            active: false,
3974            ambient_is_authored: false,
3975            ambient_color: Color::rgba(1.0, 1.0, 1.0, 1.0),
3976            ambient_intensity: 1.0,
3977            lights: vec![],
3978        }
3979    }
3980}
3981
3982#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3983/// Stroke end-cap style.
3984pub enum StrokeCap {
3985    Butt,
3986    Round,
3987    Square,
3988}
3989
3990#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3991/// Stroke join style.
3992pub enum StrokeJoin {
3993    Miter,
3994    Round,
3995    Bevel,
3996}
3997
3998#[derive(Debug, Clone)]
3999/// Stroke style for a tessellated vector path.
4000pub struct Stroke {
4001    pub fill: Fill,
4002    pub weight: f32,
4003    pub cap: StrokeCap,
4004    pub join: StrokeJoin,
4005}
4006
4007#[derive(Debug, Clone, Copy)]
4008/// Normalized visible range for a stroked vector path.
4009pub struct DrawRange {
4010    pub start: f32,
4011    pub end: f32,
4012    pub enabled: bool,
4013}
4014
4015impl DrawRange {
4016    pub fn disabled() -> Self {
4017        Self {
4018            start: 0.0,
4019            end: 1.0,
4020            enabled: false,
4021        }
4022    }
4023
4024    pub fn enabled(start: f32, end: f32) -> Self {
4025        Self {
4026            start,
4027            end,
4028            enabled: true,
4029        }
4030    }
4031
4032    fn as_gpu_range(&self) -> [f32; 4] {
4033        [
4034            self.start.clamp(0.0, 1.0),
4035            self.end.clamp(0.0, 1.0),
4036            if self.enabled { 1.0 } else { 0.0 },
4037            0.0,
4038        ]
4039    }
4040}
4041
4042#[cfg(test)]
4043mod tests {
4044    use super::*;
4045
4046    #[test]
4047    fn retained_transform_identity_survives_more_than_480_live_transforms() {
4048        let mut arena = TransformArena::new();
4049        let identity = GpuTransform::default();
4050        let mut slots = HashSet::new();
4051        for node_id in 0..600 {
4052            let transform = GpuTransform {
4053                transform: Transform2D::translation(node_id as f32 + 1.0, 7.0).to_arrays(),
4054                opacity: 0.5,
4055                ..identity
4056            };
4057            let (_, ids) = arena.sync_node(node_id, &[identity, transform]);
4058            slots.insert(ids[1]);
4059        }
4060        assert_eq!(arena.slots[0].transform, identity.transform);
4061        assert_eq!(arena.slots[0].opacity, 1.0);
4062        assert!(!slots.contains(&0));
4063        assert_eq!(slots.len(), 600);
4064    }
4065
4066    #[test]
4067    fn retained_transform_slots_are_stable_and_reused_without_aliases() {
4068        let mut arena = TransformArena::new();
4069        let identity = GpuTransform::default();
4070        let mut owned = Vec::new();
4071        for frame in 0..8 {
4072            let mut live = HashSet::new();
4073            for node in 0..1000 {
4074                let transform = GpuTransform {
4075                    transform: Transform2D::translation(node as f32, frame as f32).to_arrays(),
4076                    opacity: (node % 10) as f32 / 10.0,
4077                    ..identity
4078                };
4079                let (keys, ids) = arena.sync_node(node, &[identity, transform]);
4080                assert!(live.insert(ids[1]));
4081                assert_ne!(ids[1], 0);
4082                assert_eq!(arena.slots[ids[1] as usize].transform, transform.transform);
4083                assert_eq!(arena.slots[ids[1] as usize].opacity, transform.opacity);
4084                if frame == 0 {
4085                    owned.push((keys, ids[1]));
4086                } else {
4087                    assert_eq!(ids[1], owned[node as usize].1);
4088                }
4089            }
4090            assert_eq!(arena.slots.len(), 1001);
4091        }
4092        for (keys, _) in owned.iter().step_by(2) {
4093            arena.release_keys(keys);
4094        }
4095        let retained: HashSet<_> = owned
4096            .iter()
4097            .skip(1)
4098            .step_by(2)
4099            .map(|entry| entry.1)
4100            .collect();
4101        let mut reused = HashSet::new();
4102        for node in 1000..1500 {
4103            let (_, ids) = arena.sync_node(node, &[identity, identity]);
4104            assert!(reused.insert(ids[1]));
4105            assert!(!retained.contains(&ids[1]));
4106        }
4107        assert_eq!(arena.slots.len(), 1001);
4108        assert_eq!(arena.entries.len(), 1000);
4109        assert_eq!(arena.slots[0].transform, identity.transform);
4110    }
4111
4112    #[test]
4113    fn clip_slots_grow_and_reuse_only_released_owners() {
4114        let mut arena = ClipArena::new();
4115        let mut keys = Vec::new();
4116        for node in 0..1000 {
4117            let (key, id) = arena
4118                .sync_clip(
4119                    node,
4120                    0,
4121                    node as u64,
4122                    VertexBuffers::new(),
4123                    Transform2D::translation(node as f32, 0.0),
4124                )
4125                .unwrap();
4126            assert_eq!(id, node);
4127            keys.push(key);
4128        }
4129        arena.release_keys(&keys[..500]);
4130        for node in 1000..1500 {
4131            let (_, id) = arena
4132                .sync_clip(
4133                    node,
4134                    0,
4135                    node as u64,
4136                    VertexBuffers::new(),
4137                    Transform2D::translation(node as f32, 0.0),
4138                )
4139                .unwrap();
4140            assert!(id < 500);
4141            assert_eq!(arena.get(id).unwrap().geometry_signature, node as u64);
4142        }
4143        for node in 500..1000 {
4144            assert_eq!(arena.get(node).unwrap().geometry_signature, node as u64);
4145        }
4146        assert_eq!(arena.slots.len(), 1000);
4147        assert_eq!(arena.entries.len(), 1000);
4148    }
4149
4150    fn rect_path(width: f32, height: f32) -> Path {
4151        let mut builder = Path::builder();
4152        builder.begin(point(0.0, 0.0));
4153        builder.line_to(point(width, 0.0));
4154        builder.line_to(point(width, height));
4155        builder.line_to(point(0.0, height));
4156        builder.end(true);
4157        builder.build()
4158    }
4159
4160    fn two_closed_rects_path(width: f32, height: f32, gap: f32) -> Path {
4161        let mut builder = Path::builder();
4162        builder.begin(point(0.0, 0.0));
4163        builder.line_to(point(width, 0.0));
4164        builder.line_to(point(width, height));
4165        builder.line_to(point(0.0, height));
4166        builder.end(true);
4167
4168        let x = width + gap;
4169        builder.begin(point(x, 0.0));
4170        builder.line_to(point(x + width, 0.0));
4171        builder.line_to(point(x + width, height));
4172        builder.line_to(point(x, height));
4173        builder.end(true);
4174        builder.build()
4175    }
4176
4177    fn two_overlapping_rects_path(width: f32, height: f32, overlap: f32) -> Path {
4178        two_closed_rects_path(width, height, -overlap)
4179    }
4180
4181    fn tessellated_area(geometry: &VertexBuffers<stencil::Vertex, u16>) -> f32 {
4182        geometry
4183            .indices
4184            .chunks_exact(3)
4185            .map(|triangle| {
4186                let a = geometry.vertices[triangle[0] as usize].position;
4187                let b = geometry.vertices[triangle[1] as usize].position;
4188                let c = geometry.vertices[triangle[2] as usize].position;
4189                ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
4190            })
4191            .sum()
4192    }
4193
4194    #[test]
4195    fn axis_aligned_rect_clip_becomes_scissor() {
4196        let transform = Transform2D::from_array([2.0, 0.0, 0.0, 3.0, 5.0, 7.0]);
4197        let scissor =
4198            axis_aligned_rect_scissor(&rect_path(10.0, 5.0), &transform, [1.0, 1.0]).unwrap();
4199        assert_eq!(scissor.min_x, 5.0);
4200        assert_eq!(scissor.min_y, 7.0);
4201        assert_eq!(scissor.max_x, 25.0);
4202        assert_eq!(scissor.max_y, 22.0);
4203    }
4204
4205    #[test]
4206    fn sheared_rect_clip_stays_on_stencil_path() {
4207        let transform = Transform2D::from_array([1.0, 0.5, 0.0, 1.0, 0.0, 0.0]);
4208        assert!(axis_aligned_rect_scissor(&rect_path(10.0, 5.0), &transform, [1.0, 1.0]).is_none());
4209    }
4210
4211    #[test]
4212    fn fractional_rect_clip_uses_physical_pixel_alignment() {
4213        let path = rect_path(10.0, 6.0);
4214        let transform = Transform2D::translation(0.5, 0.5);
4215        assert!(axis_aligned_rect_scissor(&path, &transform, [1.0, 1.0]).is_none());
4216        assert!(axis_aligned_rect_scissor(&path, &transform, [2.0, 2.0]).is_some());
4217        assert!(axis_aligned_rect_scissor(&path, &transform, [2.0, 3.0]).is_none());
4218        let transform = Transform2D::translation(1.0, 1.0);
4219        assert!(axis_aligned_rect_scissor(&path, &transform, [1.25, 1.25]).is_none());
4220    }
4221
4222    #[derive(Default)]
4223    struct RecordingClipBackend {
4224        stencil: Vec<u32>,
4225        draws: Vec<(u32, Vec<u32>)>,
4226    }
4227
4228    impl RetainedClipBackend for RecordingClipBackend {
4229        fn draw_runs(&mut self, runs: &[RetainedBatchRun<'_>]) {
4230            for run in runs {
4231                self.draws
4232                    .push((run.alpha_mask.unwrap(), self.stencil.clone()));
4233            }
4234        }
4235
4236        fn sync_stencil(&mut self, depth: u32, clips: &[stencil::ClipDraw<'_>]) {
4237            self.stencil.truncate(depth as usize);
4238            self.stencil.extend(clips.iter().map(|clip| clip.clip_id));
4239        }
4240    }
4241
4242    fn labeled_run(label: u32) -> RetainedBatchRun<'static> {
4243        RetainedBatchRun {
4244            alpha_mask: Some(label),
4245            stencil_index: 0,
4246            scissor: None,
4247            draws: Vec::new(),
4248        }
4249    }
4250
4251    fn test_clip_arena() -> ClipArena {
4252        let mut arena = ClipArena::new();
4253        for node_id in 0..3 {
4254            let geometry = tessellate_clip_geometry(&rect_path(10.0, 10.0), 0.1);
4255            arena
4256                .sync_clip(
4257                    node_id,
4258                    0,
4259                    hash_clip_geometry(&geometry),
4260                    geometry,
4261                    Transform2D::translation(node_id as f32 * 10.0, 0.0),
4262                )
4263                .unwrap();
4264        }
4265        arena
4266    }
4267
4268    #[test]
4269    fn retained_siblings_draw_before_their_stencil_is_replaced_or_popped() {
4270        let arena = test_clip_arena();
4271        let mut backend = RecordingClipBackend::default();
4272        let mut current = Vec::new();
4273        let mut runs = Vec::new();
4274        let root = ClipReference::Stencil { clip_id: 0 };
4275        for (label, clips) in [
4276            (1, vec![root, ClipReference::Stencil { clip_id: 1 }]),
4277            (2, vec![root, ClipReference::Stencil { clip_id: 2 }]),
4278            (3, vec![root]),
4279            (4, vec![]),
4280        ] {
4281            sync_retained_clip_stack(&mut backend, &mut runs, &mut current, &clips, &arena);
4282            runs.push(labeled_run(label));
4283        }
4284        backend.draw_runs(&runs);
4285        assert_eq!(
4286            backend.draws,
4287            vec![(1, vec![0, 1]), (2, vec![0, 2]), (3, vec![0]), (4, vec![]),]
4288        );
4289    }
4290
4291    #[test]
4292    fn retained_scissor_and_alpha_changes_preserve_stencil_batching() {
4293        let arena = test_clip_arena();
4294        let mut backend = RecordingClipBackend::default();
4295        let mut current = Vec::new();
4296        let mut runs = Vec::new();
4297        let root = ClipReference::Stencil { clip_id: 0 };
4298        sync_retained_clip_stack(&mut backend, &mut runs, &mut current, &[root], &arena);
4299        runs.push(labeled_run(1));
4300        for clip in [
4301            ClipReference::Scissor(ScissorRect {
4302                min_x: 1.0,
4303                min_y: 2.0,
4304                max_x: 3.0,
4305                max_y: 4.0,
4306            }),
4307            ClipReference::Alpha { owner: 5 },
4308        ] {
4309            sync_retained_clip_stack(&mut backend, &mut runs, &mut current, &[root, clip], &arena);
4310        }
4311        assert!(backend.draws.is_empty());
4312        assert_eq!(runs.len(), 1);
4313        assert_eq!(current, vec![0]);
4314    }
4315
4316    #[test]
4317    fn overlapping_clip_contours_form_a_union() {
4318        let geometry = tessellate_clip_geometry(
4319            &two_overlapping_rects_path(10.0, 10.0, 5.0),
4320            DEFAULT_TESSELLATION_TOLERANCE,
4321        );
4322        assert!((tessellated_area(&geometry) - 150.0).abs() < 0.01);
4323    }
4324
4325    #[test]
4326    fn ordinary_fills_preserve_overlaps_and_opposite_winding_holes() {
4327        let mut hole = Path::builder();
4328        hole.begin(point(0.0, 0.0));
4329        hole.line_to(point(10.0, 0.0));
4330        hole.line_to(point(10.0, 10.0));
4331        hole.line_to(point(0.0, 10.0));
4332        hole.end(true);
4333        // The inner contour runs in the opposite direction.
4334        hole.begin(point(3.0, 3.0));
4335        hole.line_to(point(3.0, 7.0));
4336        hole.line_to(point(7.0, 7.0));
4337        hole.line_to(point(7.0, 3.0));
4338        hole.end(true);
4339
4340        for (path, area) in [
4341            (two_overlapping_rects_path(10.0, 10.0, 5.0), 150.0),
4342            (hole.build(), 84.0),
4343        ] {
4344            let op = PendingVectorOp {
4345                geometry_signature: hash_vector_path(
4346                    &path,
4347                    PendingVectorOpKind::Fill,
4348                    PathSmoothing::None,
4349                ),
4350                path,
4351                fill: Fill::Solid(Color::rgba(1.0, 1.0, 1.0, 1.0)),
4352                material: Material::default(),
4353                light_mask: 0,
4354                transform: Transform2D::identity(),
4355                opacity: 1.0,
4356                kind: PendingVectorOpKind::Fill,
4357                draw_range: DrawRange::disabled(),
4358                smoothing: PathSmoothing::None,
4359            };
4360            let geometry = tessellate_vector_geometry(DEFAULT_TESSELLATION_TOLERANCE, &op);
4361            let coverage = VertexBuffers {
4362                vertices: geometry
4363                    .vertices
4364                    .iter()
4365                    .map(|v| stencil::Vertex {
4366                        position: v.position,
4367                    })
4368                    .collect(),
4369                indices: geometry.indices,
4370            };
4371            assert!((tessellated_area(&coverage) - area).abs() < 0.01);
4372        }
4373    }
4374
4375    #[test]
4376    fn vector_geometry_cache_reuses_tessellation_and_remaps_primitive_ids() {
4377        let path = rect_path(10.0, 5.0);
4378        let op = PendingVectorOp {
4379            path: path.clone(),
4380            fill: Fill::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)),
4381            material: Material::default(),
4382            light_mask: 0,
4383            transform: Transform2D::identity(),
4384            opacity: 1.0,
4385            kind: PendingVectorOpKind::Fill,
4386            geometry_signature: hash_vector_path(
4387                &path,
4388                PendingVectorOpKind::Fill,
4389                PathSmoothing::None,
4390            ),
4391            draw_range: DrawRange::disabled(),
4392            smoothing: PathSmoothing::None,
4393        };
4394        let ops = vec![
4395            PendingVectorOp {
4396                path,
4397                fill: Fill::Solid(Color::rgba(0.0, 1.0, 0.0, 1.0)),
4398                material: Material::default(),
4399                light_mask: 0,
4400                transform: Transform2D::identity(),
4401                opacity: 1.0,
4402                kind: PendingVectorOpKind::Fill,
4403                geometry_signature: op.geometry_signature,
4404                draw_range: DrawRange::disabled(),
4405                smoothing: PathSmoothing::None,
4406            },
4407            op,
4408        ];
4409        let cache = Rc::new(RefCell::new(VectorGeometryCache::default()));
4410        let mut stats = ResourceChurnStats::default();
4411        let mut buffers = new_cpu_buffers();
4412
4413        rebuild_vector_buffers(
4414            DEFAULT_TESSELLATION_TOLERANCE,
4415            &ops,
4416            &mut buffers,
4417            false,
4418            false,
4419            &cache,
4420            &mut stats,
4421        );
4422
4423        assert_eq!(stats.vector_geometry_cache_misses, 1);
4424        assert_eq!(stats.vector_geometry_cache_hits, 1);
4425        assert!(buffers
4426            .geometry
4427            .vertices
4428            .iter()
4429            .any(|vertex| vertex.prim_id == 0));
4430        assert!(buffers
4431            .geometry
4432            .vertices
4433            .iter()
4434            .any(|vertex| vertex.prim_id == 1));
4435
4436        let mut second_stats = ResourceChurnStats::default();
4437        let mut second_buffers = new_cpu_buffers();
4438        rebuild_vector_buffers(
4439            DEFAULT_TESSELLATION_TOLERANCE,
4440            &ops,
4441            &mut second_buffers,
4442            false,
4443            false,
4444            &cache,
4445            &mut second_stats,
4446        );
4447
4448        assert_eq!(second_stats.vector_geometry_cache_misses, 0);
4449        assert_eq!(second_stats.vector_geometry_cache_hits, 2);
4450        assert_eq!(second_stats.tessellated_vertices, 0);
4451        assert_eq!(
4452            buffers.geometry.vertices.len(),
4453            second_buffers.geometry.vertices.len()
4454        );
4455        for (left, right) in buffers
4456            .geometry
4457            .vertices
4458            .iter()
4459            .zip(second_buffers.geometry.vertices.iter())
4460        {
4461            assert_eq!(left.position, right.position);
4462            assert_eq!(left.prim_id, right.prim_id);
4463        }
4464        assert_eq!(buffers.geometry.indices, second_buffers.geometry.indices);
4465    }
4466
4467    #[test]
4468    fn draw_range_changes_do_not_rebuild_stroke_geometry() {
4469        let path = rect_path(10.0, 5.0);
4470        let kind = PendingVectorOpKind::Stroke(2.0, StrokeCap::Butt, StrokeJoin::Miter);
4471        let geometry_signature = hash_vector_path(&path, kind, PathSmoothing::None);
4472        let ops = vec![
4473            PendingVectorOp {
4474                path: path.clone(),
4475                fill: Fill::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)),
4476                material: Material::default(),
4477                light_mask: 0,
4478                transform: Transform2D::identity(),
4479                opacity: 1.0,
4480                kind,
4481                geometry_signature,
4482                draw_range: DrawRange::enabled(0.0, 0.5),
4483                smoothing: PathSmoothing::None,
4484            },
4485            PendingVectorOp {
4486                path,
4487                fill: Fill::Solid(Color::rgba(0.0, 1.0, 0.0, 1.0)),
4488                material: Material::default(),
4489                light_mask: 0,
4490                transform: Transform2D::identity(),
4491                opacity: 1.0,
4492                kind,
4493                geometry_signature,
4494                draw_range: DrawRange::enabled(0.25, 0.75),
4495                smoothing: PathSmoothing::None,
4496            },
4497        ];
4498        let cache = Rc::new(RefCell::new(VectorGeometryCache::default()));
4499        let mut stats = ResourceChurnStats::default();
4500        let mut buffers = new_cpu_buffers();
4501
4502        rebuild_vector_buffers(
4503            DEFAULT_TESSELLATION_TOLERANCE,
4504            &ops,
4505            &mut buffers,
4506            false,
4507            false,
4508            &cache,
4509            &mut stats,
4510        );
4511
4512        assert_eq!(stats.vector_geometry_cache_misses, 1);
4513        assert_eq!(stats.vector_geometry_cache_hits, 1);
4514        assert_eq!(buffers.primitives[0].draw_range, [0.0, 0.5, 1.0, 0.0]);
4515        assert_eq!(buffers.primitives[1].draw_range, [0.25, 0.75, 1.0, 0.0]);
4516        assert!(buffers
4517            .geometry
4518            .vertices
4519            .iter()
4520            .any(|vertex| vertex.path_progress > 0.0 && vertex.path_progress < 1.0));
4521    }
4522
4523    #[test]
4524    fn closed_subpaths_keep_compound_path_progress() {
4525        let path = two_closed_rects_path(10.0, 5.0, 20.0);
4526        let kind = PendingVectorOpKind::Stroke(1.0, StrokeCap::Butt, StrokeJoin::Miter);
4527        let op = PendingVectorOp {
4528            path: path.clone(),
4529            fill: Fill::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)),
4530            material: Material::default(),
4531            light_mask: 0,
4532            transform: Transform2D::identity(),
4533            opacity: 1.0,
4534            kind,
4535            geometry_signature: hash_vector_path(&path, kind, PathSmoothing::None),
4536            draw_range: DrawRange::enabled(0.0, 1.0),
4537            smoothing: PathSmoothing::None,
4538        };
4539
4540        let geometry = tessellate_vector_geometry(DEFAULT_TESSELLATION_TOLERANCE, &op);
4541        let first_rect_progress = geometry
4542            .vertices
4543            .iter()
4544            .filter(|vertex| vertex.position[0] < 20.0)
4545            .map(|vertex| vertex.path_progress)
4546            .collect::<Vec<_>>();
4547        let second_rect_progress = geometry
4548            .vertices
4549            .iter()
4550            .filter(|vertex| vertex.position[0] > 20.0)
4551            .map(|vertex| vertex.path_progress)
4552            .collect::<Vec<_>>();
4553
4554        let first_max = first_rect_progress
4555            .iter()
4556            .copied()
4557            .fold(f32::NEG_INFINITY, f32::max);
4558        let second_min = second_rect_progress
4559            .iter()
4560            .copied()
4561            .fold(f32::INFINITY, f32::min);
4562        let second_max = second_rect_progress
4563            .iter()
4564            .copied()
4565            .fold(f32::NEG_INFINITY, f32::max);
4566
4567        assert!(!first_rect_progress.is_empty());
4568        assert!(!second_rect_progress.is_empty());
4569        assert!(first_max <= 0.6, "first closed contour reached {first_max}");
4570        assert!(
4571            second_min >= 0.4,
4572            "second closed contour restarted at {second_min}"
4573        );
4574        assert!(second_max > 0.9, "compound path did not reach the end");
4575    }
4576
4577    #[test]
4578    fn draw_range_changes_refresh_primitives_without_rebuilding_geometry() {
4579        let path = rect_path(10.0, 5.0);
4580        let kind = PendingVectorOpKind::Stroke(2.0, StrokeCap::Butt, StrokeJoin::Miter);
4581        let geometry_signature = hash_vector_path(&path, kind, PathSmoothing::None);
4582        let make_ops = |draw_range| {
4583            vec![PendingVectorOp {
4584                path: path.clone(),
4585                fill: Fill::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)),
4586                material: Material::default(),
4587                light_mask: 0,
4588                transform: Transform2D::identity(),
4589                opacity: 1.0,
4590                kind,
4591                geometry_signature,
4592                draw_range,
4593                smoothing: PathSmoothing::None,
4594            }]
4595        };
4596        let cache = Rc::new(RefCell::new(VectorGeometryCache::default()));
4597        let mut stats = ResourceChurnStats::default();
4598        let mut buffers = new_cpu_buffers();
4599
4600        rebuild_vector_buffers(
4601            DEFAULT_TESSELLATION_TOLERANCE,
4602            &make_ops(DrawRange::enabled(0.0, 0.5)),
4603            &mut buffers,
4604            false,
4605            false,
4606            &cache,
4607            &mut stats,
4608        );
4609        let vertices = buffers.geometry.vertices.clone();
4610        let indices = buffers.geometry.indices.clone();
4611
4612        let mut update_stats = ResourceChurnStats::default();
4613        rebuild_vector_buffers(
4614            DEFAULT_TESSELLATION_TOLERANCE,
4615            &make_ops(DrawRange::enabled(0.25, 0.75)),
4616            &mut buffers,
4617            true,
4618            true,
4619            &cache,
4620            &mut update_stats,
4621        );
4622
4623        assert_eq!(update_stats.vector_geometry_rebuilds, 0);
4624        assert_eq!(buffers.geometry.vertices.len(), vertices.len());
4625        for (left, right) in buffers.geometry.vertices.iter().zip(vertices.iter()) {
4626            assert_eq!(left.position, right.position);
4627            assert_eq!(left.normal, right.normal);
4628            assert_eq!(left.prim_id, right.prim_id);
4629            assert_eq!(left.path_progress, right.path_progress);
4630        }
4631        assert_eq!(buffers.geometry.indices, indices);
4632        assert_eq!(buffers.primitives[0].draw_range, [0.25, 0.75, 1.0, 0.0]);
4633    }
4634
4635    #[test]
4636    fn draw_range_changes_are_part_of_primitive_signature() {
4637        let path = rect_path(10.0, 5.0);
4638        let kind = PendingVectorOpKind::Stroke(2.0, StrokeCap::Butt, StrokeJoin::Miter);
4639        let geometry_signature = hash_vector_path(&path, kind, PathSmoothing::None);
4640        let make_ops = |draw_range| {
4641            vec![PendingVectorOp {
4642                path: path.clone(),
4643                fill: Fill::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)),
4644                material: Material::default(),
4645                light_mask: 0,
4646                transform: Transform2D::identity(),
4647                opacity: 1.0,
4648                kind,
4649                geometry_signature,
4650                draw_range,
4651                smoothing: PathSmoothing::None,
4652            }]
4653        };
4654
4655        assert_ne!(
4656            hash_vector_primitives(&make_ops(DrawRange::enabled(0.0, 0.5))),
4657            hash_vector_primitives(&make_ops(DrawRange::enabled(0.25, 0.75)))
4658        );
4659    }
4660
4661    #[test]
4662    fn smoothing_changes_geometry_signature_not_primitive_signature() {
4663        let path = rect_path(10.0, 5.0);
4664        let kind = PendingVectorOpKind::Stroke(2.0, StrokeCap::Butt, StrokeJoin::Round);
4665        let unsmoothed = hash_vector_path(&path, kind, PathSmoothing::None);
4666        let smoothed = hash_vector_path(&path, kind, PathSmoothing::Strong);
4667        let make_op = |smoothing, geometry_signature| PendingVectorOp {
4668            path: path.clone(),
4669            fill: Fill::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)),
4670            material: Material::default(),
4671            light_mask: 0,
4672            transform: Transform2D::identity(),
4673            opacity: 1.0,
4674            kind,
4675            geometry_signature,
4676            draw_range: DrawRange::enabled(0.0, 0.5),
4677            smoothing,
4678        };
4679
4680        assert_ne!(unsmoothed, smoothed);
4681        assert_eq!(
4682            hash_vector_primitives(&[make_op(PathSmoothing::None, unsmoothed)]),
4683            hash_vector_primitives(&[make_op(PathSmoothing::Strong, smoothed)])
4684        );
4685    }
4686}