Skip to main content

pax_gpu/
render_context.rs

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