Skip to main content

repose_render_wgpu/
lib.rs

1use std::any::TypeId;
2use std::borrow::Cow;
3use std::collections::{HashMap, HashSet};
4use std::hash::{Hash, Hasher};
5use std::num::NonZeroU64;
6#[cfg(feature = "winit-surface")]
7use std::panic::{AssertUnwindSafe, catch_unwind};
8use std::sync::{Arc, Weak};
9
10use repose_core::color::{ChromaSiting, ColorInfo, PixelFormat};
11use repose_core::{
12    Brush, FontStyle, ImageAlignment, ImageFilter, ImageFit, ImageSourceRect, Scene, SceneNode,
13    StrokeCap, Transform, Vec2,
14};
15#[cfg(feature = "winit-surface")]
16use repose_core::{GlyphRasterConfig, PresentModePref, RenderBackend, request_frame};
17#[cfg(feature = "winit-surface")]
18use wgpu::Instance;
19
20mod slug;
21
22fn align_up(value: u64, alignment: u64) -> anyhow::Result<u64> {
23    if !alignment.is_power_of_two() {
24        anyhow::bail!("alignment must be a power of two");
25    }
26    value
27        .checked_add(alignment - 1)
28        .map(|v| v & !(alignment - 1))
29        .ok_or_else(|| anyhow::anyhow!("alignment overflow"))
30}
31
32mod commands;
33pub use commands::apply_render_commands;
34
35pub mod offscreen;
36
37mod callback;
38pub use callback::{
39    Callback, CallbackRenderPass, CallbackResources, ScreenDescriptor, WgpuCallback,
40};
41
42mod depth_composite;
43pub use depth_composite::{BlitRenderPass, DepthComposite};
44
45#[derive(Clone)]
46struct UploadRing {
47    buf: wgpu::Buffer,
48    cap: u64,
49    head: u64,
50    usage: wgpu::BufferUsages,
51}
52
53impl UploadRing {
54    fn new(device: &wgpu::Device, label: &str, cap: u64, usage: wgpu::BufferUsages) -> Self {
55        let max = device.limits().max_buffer_size;
56        let cap = cap.max(4).min(max).min(MAX_UPLOAD_RING_BYTES).max(4);
57        let buf = device.create_buffer(&wgpu::BufferDescriptor {
58            label: Some(label),
59            size: cap,
60            usage,
61            mapped_at_creation: false,
62        });
63        Self {
64            buf,
65            cap,
66            head: 0,
67            usage,
68        }
69    }
70
71    fn reset(&mut self) {
72        self.head = 0;
73    }
74
75    fn grow_to_fit(
76        &mut self,
77        device: &wgpu::Device,
78        encoder: &mut wgpu::CommandEncoder,
79        needed: u64,
80    ) -> anyhow::Result<()> {
81        if needed == 0 {
82            return Ok(());
83        }
84        let start = align_up(self.head, 4)?;
85        let end = start
86            .checked_add(needed)
87            .ok_or_else(|| anyhow::anyhow!("upload ring offset overflow"))?;
88        if end <= self.cap {
89            return Ok(());
90        }
91        let required = align_up(end, 4)?;
92        let doubled = self.cap.checked_mul(2).unwrap_or(0);
93        let new_cap = required.max(doubled).max(256);
94        let new_cap = align_up(new_cap, 4)?;
95        if new_cap > device.limits().max_buffer_size || new_cap > MAX_UPLOAD_RING_BYTES {
96            anyhow::bail!("upload ring exceeds resource budget");
97        }
98        if !self.usage.contains(wgpu::BufferUsages::COPY_SRC) {
99            anyhow::bail!("upload ring growth requires COPY_SRC");
100        }
101        let old = self.buf.clone();
102        self.buf = device.create_buffer(&wgpu::BufferDescriptor {
103            label: Some("upload ring (grown)"),
104            size: new_cap,
105            usage: self.usage,
106            mapped_at_creation: false,
107        });
108        self.cap = new_cap;
109        if self.head > 0 {
110            let copy_size = align_up(self.head, 4)?;
111            if copy_size > old.size() {
112                anyhow::bail!("upload ring old data exceeds old buffer");
113            }
114            encoder.copy_buffer_to_buffer(&old, 0, &self.buf, 0, copy_size);
115        }
116        Ok(())
117    }
118
119    fn alloc_write(&mut self, queue: &wgpu::Queue, bytes: &[u8]) -> anyhow::Result<u64> {
120        let len = u64::try_from(bytes.len())
121            .map_err(|_| anyhow::anyhow!("upload byte length exceeds u64"))?;
122        let start = align_up(self.head, 4)?;
123        let end = start
124            .checked_add(len)
125            .ok_or_else(|| anyhow::anyhow!("upload ring offset overflow"))?;
126        if end > self.cap {
127            anyhow::bail!(
128                "upload ring overflow: start={start} len={len} cap={}",
129                self.cap
130            );
131        }
132        queue.write_buffer(&self.buf, start, bytes);
133        self.head = end;
134        Ok(start)
135    }
136}
137
138struct InstancedPipe<I: bytemuck::Pod> {
139    ring: UploadRing,
140    _marker: std::marker::PhantomData<I>,
141}
142
143impl<I: bytemuck::Pod> InstancedPipe<I> {
144    fn new(ring: UploadRing) -> Self {
145        Self {
146            ring,
147            _marker: std::marker::PhantomData,
148        }
149    }
150
151    fn upload(
152        &mut self,
153        device: &wgpu::Device,
154        queue: &wgpu::Queue,
155        encoder: &mut wgpu::CommandEncoder,
156        data: &[I],
157    ) -> Option<(u64, u32)> {
158        if data.is_empty() {
159            return None;
160        }
161        let count = u32::try_from(data.len()).ok()?;
162        let bytes = bytemuck::cast_slice(data);
163        let byte_len = u64::try_from(bytes.len()).ok()?;
164        if byte_len == 0 || byte_len > device.limits().max_buffer_size {
165            return None;
166        }
167        self.ring
168            .grow_to_fit(device, encoder, byte_len)
169            .map_err(|error| log::error!("{error:#}"))
170            .ok()?;
171        let off = self
172            .ring
173            .alloc_write(queue, bytes)
174            .map_err(|error| log::error!("{error:#}"))
175            .ok()?;
176        Some((off, count))
177    }
178
179    fn reset(&mut self) {
180        self.ring.reset();
181    }
182}
183
184#[repr(C)]
185#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
186struct Globals {
187    ndc_to_px: [f32; 2],
188    _pad: [f32; 2],
189}
190
191fn make_globals(target_w: f32, target_h: f32) -> Globals {
192    Globals {
193        ndc_to_px: [target_w * 0.5, target_h * 0.5],
194        _pad: [0.0, 0.0],
195    }
196}
197
198pub struct WgpuSceneRenderer {
199    pub device: wgpu::Device,
200    pub queue: wgpu::Queue,
201    pub output_format: wgpu::TextureFormat,
202    pub output_width: u32,
203    pub output_height: u32,
204    /// Pixels per point (DPI scale) for `ScreenDescriptor` / `PaintCallbackInfo`.
205    pub pixels_per_point: f32,
206
207    // Render pipelines. Two sets: one for the MSAA surface pass, one for
208    // graphics-layer render-to-texture passes (sample_count = 1).
209    surface_pipes: Pipelines,
210    working_space_pipes: Pipelines,
211    layer_pipes: Pipelines,
212    working_space_layer_pipes: Pipelines,
213
214    // Instanced draw rings
215    rects: InstancedPipe<RectInstance>,
216    borders: InstancedPipe<BorderInstance>,
217    ellipses: InstancedPipe<EllipseInstance>,
218    ellipse_borders: InstancedPipe<EllipseBorderInstance>,
219    arcs: InstancedPipe<ArcInstance>,
220    glyph_mask: InstancedPipe<GlyphInstance>,
221    glyph_color: InstancedPipe<GlyphInstance>,
222
223    // Image bind layouts and shared sampler
224    image_bind_layout_rgba: wgpu::BindGroupLayout,
225    image_bind_layout_nv12: wgpu::BindGroupLayout,
226    image_sampler: wgpu::Sampler,
227    image_sampler_nearest: wgpu::Sampler,
228    image_sampler_repeat: wgpu::Sampler,
229    image_sampler_repeat_nearest: wgpu::Sampler,
230    layer_sampler: wgpu::Sampler,
231    layer_sampler_linear: wgpu::Sampler,
232
233    // Blur composite ring (for graphics-layer drop shadows)
234    blur_ring: UploadRing,
235
236    text_bind_layout: wgpu::BindGroupLayout,
237
238    // Stencil clip ring
239    clip_ring: UploadRing,
240
241    // Projective layer-composite ring (one ProjectiveInstance per flattened
242    // perspective subtree)
243    projective_ring: UploadRing,
244
245    // Backdrop-blend composite ring (one BlendInstance per isolated blend)
246    blend_ring: UploadRing,
247
248    // Tessellated vector glyph pipeline (always enabled)
249    slug_enabled: bool,
250    slug_ring: UploadRing,
251    slug_cache: slug::GlyphSlugCache,
252    slug_draw_cache: HashMap<SlugDrawKey, SlugDrawCacheEntry>,
253    slug_draw_cache_bytes_total: u64,
254
255    // Instanced NV12 ring
256    nv12: InstancedPipe<Nv12Instance>,
257
258    mesh_cache: HashMap<MeshCacheKey, MeshCacheEntry>,
259    mesh_cache_bytes_total: u64,
260    mesh_uniform_buf: wgpu::Buffer,
261    mesh_bind: wgpu::BindGroup,
262    mesh_uniform_head: u64,
263    mesh_uniform_alignment: u64,
264    mesh_uniform_cap: u64,
265
266    /// Translator-owned flatten layer ids used by the previous frame;
267    /// drained from the layer pool at the start of each translation (they
268    /// are single-frame by construction).
269    flatten_layer_ids: Vec<u32>,
270
271    /// Backdrop snapshots keyed by isolated-blend layer id. Filled during
272    /// translation (texture allocated) and populated by a texture copy at
273    /// execution time, before the blend composite draws.
274    blend_snapshots: std::collections::HashMap<u32, BlendSnapshot>,
275    blend_snapshot_pool: HashMap<BlendSnapshotKey, BlendSnapshot>,
276    /// (blend layer id, parent target) copies to run before the pass that
277    /// composites the blend. Executed between passes: copies the current
278    /// target region into the snapshot texture.
279    blend_copies: Vec<BlendCopy>,
280
281    msaa_samples: u32,
282    working_space_msaa_samples: u32,
283
284    // Depth-stencil target
285    depth_stencil_tex: wgpu::Texture,
286    depth_stencil_view: wgpu::TextureView,
287
288    // Optional MSAA color target
289    msaa_tex: Option<wgpu::Texture>,
290    msaa_view: Option<wgpu::TextureView>,
291    ws_msaa_tex: Option<wgpu::Texture>,
292    ws_msaa_view: Option<wgpu::TextureView>,
293    surface_resolve_tex: Option<wgpu::Texture>,
294    surface_resolve_view: Option<wgpu::TextureView>,
295    surface_resolve_bytes: u64,
296    msaa_bytes: u64,
297    ws_msaa_bytes: u64,
298    depth_stencil_bytes: u64,
299    working_space_bytes: u64,
300    gpu_budget_bytes: u64,
301
302    globals_buf: wgpu::Buffer,
303    globals_bind: wgpu::BindGroup,
304    globals_staging: wgpu::Buffer,
305    globals_staging_cap: u64,
306
307    // Glyph atlas
308    atlas_mask: AtlasA8,
309    atlas_color: AtlasRGBA,
310    atlas_mask_bind: wgpu::BindGroup,
311    atlas_color_bind: wgpu::BindGroup,
312    atlas_generation: u64,
313    atlas_mask_pending: AtlasPending,
314    atlas_color_pending: AtlasPending,
315    atlas_last_compact_frame: u64,
316    atlas_mask_failures: HashSet<repose_text::CacheKey>,
317    atlas_color_failures: HashSet<repose_text::CacheKey>,
318    /// Glyphs that rasterized fine but had no room. Skipped until the next
319    /// repack so a full atlas does not re-rasterize every frame.
320    atlas_mask_deferred: HashSet<repose_text::CacheKey>,
321    atlas_color_deferred: HashSet<repose_text::CacheKey>,
322
323    glyph_outline_cache: HashMap<repose_text::CacheKey, GlyphOutlineEntry>,
324    legacy_shaped_text_cache: HashMap<TextShapeKey, TextShapeCacheEntry>,
325
326    // Image management
327    next_image_handle: u64,
328    images: HashMap<u64, ImageTex>,
329    retained: HashMap<u64, RetainedImage>,
330    retained_bytes_total: u64,
331
332    // A8 coverage-tile management (host-rasterized masks composited tinted;
333    // no retained CPU copies — tiles are immutable and re-registered).
334    next_coverage_handle: u64,
335    coverages: HashMap<u64, CoverageTex>,
336
337    // Eviction stats
338    frame_index: u64,
339    cache_touch_clock: u64,
340    image_bytes_total: u64,
341    image_evict_after_frames: u64,
342    image_budget_bytes: u64,
343
344    // Graphics layer pool. Maps `SceneNode::BeginLayer::layer_id` to a
345    // cached offscreen render target.
346    layer_pool: HashMap<u32, LayerTarget>,
347    producer_layer_ids: HashSet<u32>,
348    layer_bytes_total: u64,
349    transient_layer_bytes_total: u64,
350
351    // Linear working-space mode (default off -> fast playback path).
352    // When enabled, the scene is rendered into an Rgba16Float intermediate
353    // texture, then a final full-screen pass applies the display OETF.
354    working_space: bool,
355    ws_tex: Option<wgpu::Texture>,
356    ws_view: Option<wgpu::TextureView>,
357    ws_bind: Option<wgpu::BindGroup>,
358    display_pipeline: Option<wgpu::RenderPipeline>,
359    display_layout: Option<wgpu::BindGroupLayout>,
360
361    pub callback_resources: CallbackResources,
362    callback_scoped_resources: HashMap<CallbackScopeKey, CallbackResources>,
363    callback_scope_uses: HashMap<CallbackScopeKey, CallbackScopeUse>,
364    callback_scope_payloads: HashMap<CallbackScopeKey, Weak<Callback>>,
365    callback_scope_clock: u64,
366    blend_snapshot_bytes_total: u64,
367    frame_active: bool,
368    last_render_error: Option<String>,
369}
370
371pub struct WgpuSurfaceBackend {
372    #[cfg(feature = "winit-surface")]
373    instance: Option<wgpu::Instance>,
374    #[cfg(feature = "winit-surface")]
375    window: Option<Arc<winit::window::Window>>,
376    pub surface: Option<wgpu::Surface<'static>>,
377    pub surface_config: Option<wgpu::SurfaceConfiguration>,
378    #[cfg_attr(not(feature = "winit-surface"), allow(dead_code))]
379    pending_reconfigure: bool,
380    pub renderer: WgpuSceneRenderer,
381}
382
383impl std::ops::Deref for WgpuSurfaceBackend {
384    type Target = WgpuSceneRenderer;
385    fn deref(&self) -> &Self::Target {
386        &self.renderer
387    }
388}
389impl std::ops::DerefMut for WgpuSurfaceBackend {
390    fn deref_mut(&mut self) -> &mut Self::Target {
391        &mut self.renderer
392    }
393}
394
395#[cfg(feature = "winit-surface")]
396pub type WgpuBackend = WgpuSurfaceBackend;
397
398impl Drop for WgpuSceneRenderer {
399    fn drop(&mut self) {
400        let _ = self.device.poll(wgpu::PollType::Poll);
401        #[cfg(not(target_arch = "wasm32"))]
402        {
403            let _ = self.device.poll(wgpu::PollType::Wait {
404                submission_index: None,
405                timeout: Some(std::time::Duration::from_millis(100)),
406            });
407        }
408    }
409}
410
411#[derive(Clone)]
412struct LayerTarget {
413    texture: wgpu::Texture,
414    view: wgpu::TextureView,
415    bind: wgpu::BindGroup,
416    bind_linear: wgpu::BindGroup,
417    depth_stencil_view: wgpu::TextureView,
418    width: u32,
419    height: u32,
420    format: wgpu::TextureFormat,
421    bytes: u64,
422    rect_px: (f32, f32, f32, f32),
423    transient: bool,
424}
425
426/// Backdrop snapshot for one isolated blend: a copy of the current target
427/// region taken before the source layer is composited, sampled as the
428/// "backdrop" input of the blend shader.
429#[derive(Clone, Copy, PartialEq, Eq, Hash)]
430struct BlendSnapshotKey {
431    format: wgpu::TextureFormat,
432    width: u32,
433    height: u32,
434}
435
436#[derive(Clone)]
437struct BlendSnapshot {
438    key: BlendSnapshotKey,
439    texture: wgpu::Texture,
440    bind: wgpu::BindGroup,
441    bytes: u64,
442}
443
444struct BlendCopy {
445    pass_id: u64,
446    blend_id: u32,
447    target: PassTarget,
448    region: repose_core::Rect,
449}
450
451#[derive(Clone)]
452enum ActiveClip {
453    Rect {
454        off: u64,
455        cnt: u32,
456        rect: repose_core::Rect,
457        radii: [f32; 4],
458        /// Scissor the matching push drew its stencil pass with. The pop must
459        /// reuse it: a different (wider) bound decrements stencil the push
460        /// never incremented, leaving those pixels a level below the base so
461        /// every later `Equal` draw drops them.
462        scissor: (u32, u32, u32, u32),
463        difference: bool,
464        applied: bool,
465        blocked: bool,
466    },
467    Vector {
468        mesh: Option<MeshGpu>,
469        source: Arc<repose_core::VectorMeshData>,
470        uoff: u64,
471        affine: [f32; 6],
472        scissor: (u32, u32, u32, u32),
473        difference: bool,
474        applied: bool,
475        blocked: bool,
476    },
477}
478
479impl ActiveClip {
480    fn difference(&self) -> bool {
481        match self {
482            Self::Rect { difference, .. } | Self::Vector { difference, .. } => *difference,
483        }
484    }
485
486    fn blocked(&self) -> bool {
487        match self {
488            Self::Rect { blocked, .. } | Self::Vector { blocked, .. } => *blocked,
489        }
490    }
491}
492
493/// Identifies which render target a `Pass` draws into.
494#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
495enum PassTarget {
496    Surface,
497    Layer(u32),
498}
499
500#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
501struct CallbackIdentity {
502    type_id: TypeId,
503    value: u64,
504    reusable: bool,
505}
506
507#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
508struct CallbackScopeKey {
509    callback: CallbackIdentity,
510    target: PassTarget,
511    width: u32,
512    height: u32,
513    target_format: wgpu::TextureFormat,
514    sample_count: u32,
515    pixels_per_point_bits: u32,
516}
517
518#[derive(Clone, Copy)]
519struct CallbackScopeUse {
520    tick: u64,
521    frame: u64,
522}
523
524fn callback_identity(callback: &Callback, fallback: usize) -> CallbackIdentity {
525    callback
526        .0
527        .resource_key()
528        .map(|key| {
529            let mut hasher = std::collections::hash_map::DefaultHasher::new();
530            key.hash(&mut hasher);
531            CallbackIdentity {
532                type_id: callback.0.resource_type_id(),
533                value: hasher.finish(),
534                reusable: true,
535            }
536        })
537        .unwrap_or(CallbackIdentity {
538            type_id: TypeId::of::<Callback>(),
539            value: fallback as u64,
540            reusable: false,
541        })
542}
543
544fn callback_scope_key(
545    callback: &Callback,
546    fallback: usize,
547    target: PassTarget,
548    descriptor: &ScreenDescriptor,
549) -> CallbackScopeKey {
550    CallbackScopeKey {
551        callback: callback_identity(callback, fallback),
552        target,
553        width: descriptor.size_in_pixels[0],
554        height: descriptor.size_in_pixels[1],
555        target_format: descriptor.target_format,
556        sample_count: descriptor.sample_count,
557        pixels_per_point_bits: descriptor.pixels_per_point.to_bits(),
558    }
559}
560
561/// A bundle of render pipelines for a single sample-count target. Created
562/// twice: once with `sample_count = msaa_samples` for the surface pass, and
563/// once with `sample_count = 1` for graphics-layer render-to-texture passes
564/// (where MSAA is wasted).
565#[derive(Clone)]
566struct Pipelines {
567    rects: wgpu::RenderPipeline,
568    borders: wgpu::RenderPipeline,
569    ellipses: wgpu::RenderPipeline,
570    ellipse_borders: wgpu::RenderPipeline,
571    arcs: wgpu::RenderPipeline,
572    text_mask: wgpu::RenderPipeline,
573    text_color: wgpu::RenderPipeline,
574    image_rgba: wgpu::RenderPipeline,
575    /// Tinted A8 coverage composite (`coverage.wgsl`): same vertex
576    /// attributes and bind groups as the text/color path, sampling a
577    /// single-channel tile registered with `register_coverage_a8`.
578    coverage: wgpu::RenderPipeline,
579    image_nv12: wgpu::RenderPipeline,
580    blur: wgpu::RenderPipeline,
581    blur_content: wgpu::RenderPipeline,
582    clip_bin: wgpu::RenderPipeline,
583    clip_dec: wgpu::RenderPipeline,
584    slug: Option<wgpu::RenderPipeline>,
585    /// Tessellated vector mesh (fill/stroke). Uses an `Equal` stencil compare
586    /// so world content is correctly masked to the active `PushVectorClip`
587    /// shape (outside any clip the stencil is 0 == ref 0, so it draws).
588    mesh: wgpu::RenderPipeline,
589    /// Screen-space overlay meshes: `LessEqual` compare so they always draw
590    /// regardless of any active vector clip.
591    mesh_overlay: wgpu::RenderPipeline,
592    /// Fixed-function blend variants of the mesh pipeline, selected by
593    /// `BlendMode`. Backdrop-dependent modes (overlay, color-dodge/burn,
594    /// hard/soft-light, exclusion, hue/saturation/color/luminosity) use the
595    /// `blend_layer` shader instead.
596    mesh_add: wgpu::RenderPipeline,
597    mesh_multiply: wgpu::RenderPipeline,
598    mesh_screen: wgpu::RenderPipeline,
599    mesh_darken: wgpu::RenderPipeline,
600    mesh_lighten: wgpu::RenderPipeline,
601    /// Backdrop-blend composite: samples an isolated source layer and the
602    /// current target, applies the CSS blend formula selected by a uniform,
603    /// and composites premultiplied source-over.
604    blend_layer: wgpu::RenderPipeline,
605    /// Stencil increment for vector clips.
606    mesh_clip_inc: wgpu::RenderPipeline,
607    /// Stencil decrement for vector clips.
608    mesh_clip_dec: wgpu::RenderPipeline,
609    /// Projective layer composite (perspective flattening): samples a
610    /// graphics-layer texture through a 2D projective map. Drawn with a
611    /// `ProjectiveInstance` from `projective_ring`.
612    projective_layer: wgpu::RenderPipeline,
613}
614
615impl Pipelines {
616    fn create(
617        device: &wgpu::Device,
618        format: wgpu::TextureFormat,
619        sample_count: u32,
620        globals_layout: &wgpu::BindGroupLayout,
621        text_bind_layout: &wgpu::BindGroupLayout,
622        image_bind_layout_nv12: &wgpu::BindGroupLayout,
623        clip_pipeline_layout: &wgpu::PipelineLayout,
624        stencil_for_content: &wgpu::DepthStencilState,
625        stencil_for_clip_inc: &wgpu::DepthStencilState,
626        stencil_for_clip_dec: &wgpu::DepthStencilState,
627        clip_vertex_layout: &wgpu::VertexBufferLayout,
628        mesh_bind_layout: &wgpu::BindGroupLayout,
629    ) -> Self {
630        let msaa_state = wgpu::MultisampleState {
631            count: sample_count,
632            mask: !0,
633            alpha_to_coverage_enabled: false,
634        };
635
636        macro_rules! make_content_pipeline {
637            ($name:ident, $shader:literal, $inst_type:ty, $attrs:expr) => {
638                let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
639                    label: Some(concat!($shader, ".wgsl")),
640                    source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(concat!(
641                        "shaders/", $shader, ".wgsl"
642                    )))),
643                });
644                let pipeline_layout =
645                    device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
646                        label: Some(concat!($shader, " pipeline layout")),
647                        bind_group_layouts: &[Some(globals_layout)],
648                        immediate_size: 0,
649                    });
650                let $name = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
651                    label: Some(concat!($shader, " pipeline")),
652                    layout: Some(&pipeline_layout),
653                    vertex: wgpu::VertexState {
654                        module: &shader_module,
655                        entry_point: Some("vs_main"),
656                        buffers: &[Some(wgpu::VertexBufferLayout {
657                            array_stride: std::mem::size_of::<$inst_type>() as u64,
658                            step_mode: wgpu::VertexStepMode::Instance,
659                            attributes: $attrs,
660                        })],
661                        compilation_options: wgpu::PipelineCompilationOptions::default(),
662                    },
663                    fragment: Some(wgpu::FragmentState {
664                        module: &shader_module,
665                        entry_point: Some("fs_main"),
666                        targets: &[Some(wgpu::ColorTargetState {
667                            format,
668                            blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
669                            write_mask: wgpu::ColorWrites::ALL,
670                        })],
671                        compilation_options: wgpu::PipelineCompilationOptions::default(),
672                    }),
673                    primitive: wgpu::PrimitiveState::default(),
674                    depth_stencil: Some(stencil_for_content.clone()),
675                    multisample: msaa_state,
676                    multiview_mask: None,
677                    cache: None,
678                });
679            };
680        }
681
682        let rect_attrs: &[wgpu::VertexAttribute] = &[
683            wgpu::VertexAttribute {
684                shader_location: 0,
685                offset: 0,
686                format: wgpu::VertexFormat::Float32x4,
687            },
688            wgpu::VertexAttribute {
689                shader_location: 1,
690                offset: 16,
691                format: wgpu::VertexFormat::Float32x4,
692            },
693            wgpu::VertexAttribute {
694                shader_location: 2,
695                offset: 32,
696                format: wgpu::VertexFormat::Uint32,
697            },
698            wgpu::VertexAttribute {
699                shader_location: 3,
700                offset: 36,
701                format: wgpu::VertexFormat::Uint32,
702            },
703            wgpu::VertexAttribute {
704                shader_location: 4,
705                offset: 48,
706                format: wgpu::VertexFormat::Float32x4,
707            },
708            wgpu::VertexAttribute {
709                shader_location: 5,
710                offset: 64,
711                format: wgpu::VertexFormat::Float32x4,
712            },
713            wgpu::VertexAttribute {
714                shader_location: 6,
715                offset: 80,
716                format: wgpu::VertexFormat::Float32x2,
717            },
718            wgpu::VertexAttribute {
719                shader_location: 7,
720                offset: 88,
721                format: wgpu::VertexFormat::Float32x2,
722            },
723            wgpu::VertexAttribute {
724                shader_location: 8,
725                offset: 96,
726                format: wgpu::VertexFormat::Uint32,
727            },
728            wgpu::VertexAttribute {
729                shader_location: 9,
730                offset: 112,
731                format: wgpu::VertexFormat::Float32x4,
732            },
733        ];
734        let border_attrs: &[wgpu::VertexAttribute] = &[
735            wgpu::VertexAttribute {
736                shader_location: 0,
737                offset: 0,
738                format: wgpu::VertexFormat::Float32x4,
739            },
740            wgpu::VertexAttribute {
741                shader_location: 1,
742                offset: 16,
743                format: wgpu::VertexFormat::Float32x4,
744            },
745            wgpu::VertexAttribute {
746                shader_location: 2,
747                offset: 32,
748                format: wgpu::VertexFormat::Float32,
749            },
750            wgpu::VertexAttribute {
751                shader_location: 3,
752                offset: 36,
753                format: wgpu::VertexFormat::Uint32,
754            },
755            wgpu::VertexAttribute {
756                shader_location: 4,
757                offset: 48,
758                format: wgpu::VertexFormat::Uint32,
759            },
760            wgpu::VertexAttribute {
761                shader_location: 5,
762                offset: 52,
763                format: wgpu::VertexFormat::Float32x4,
764            },
765            wgpu::VertexAttribute {
766                shader_location: 6,
767                offset: 68,
768                format: wgpu::VertexFormat::Float32x4,
769            },
770            wgpu::VertexAttribute {
771                shader_location: 7,
772                offset: 84,
773                format: wgpu::VertexFormat::Float32x2,
774            },
775            wgpu::VertexAttribute {
776                shader_location: 8,
777                offset: 92,
778                format: wgpu::VertexFormat::Float32x2,
779            },
780            wgpu::VertexAttribute {
781                shader_location: 9,
782                offset: 100,
783                format: wgpu::VertexFormat::Uint32,
784            },
785            wgpu::VertexAttribute {
786                shader_location: 10,
787                offset: 116,
788                format: wgpu::VertexFormat::Float32x4,
789            },
790        ];
791        let ellipse_attrs: &[wgpu::VertexAttribute] = &[
792            wgpu::VertexAttribute {
793                shader_location: 0,
794                offset: 0,
795                format: wgpu::VertexFormat::Float32x4,
796            },
797            wgpu::VertexAttribute {
798                shader_location: 1,
799                offset: 16,
800                format: wgpu::VertexFormat::Uint32,
801            },
802            wgpu::VertexAttribute {
803                shader_location: 2,
804                offset: 20,
805                format: wgpu::VertexFormat::Uint32,
806            },
807            wgpu::VertexAttribute {
808                shader_location: 3,
809                offset: 32,
810                format: wgpu::VertexFormat::Float32x4,
811            },
812            wgpu::VertexAttribute {
813                shader_location: 4,
814                offset: 48,
815                format: wgpu::VertexFormat::Float32x4,
816            },
817            wgpu::VertexAttribute {
818                shader_location: 5,
819                offset: 64,
820                format: wgpu::VertexFormat::Float32x2,
821            },
822            wgpu::VertexAttribute {
823                shader_location: 6,
824                offset: 72,
825                format: wgpu::VertexFormat::Float32x2,
826            },
827            wgpu::VertexAttribute {
828                shader_location: 7,
829                offset: 80,
830                format: wgpu::VertexFormat::Uint32,
831            },
832            wgpu::VertexAttribute {
833                shader_location: 8,
834                offset: 96,
835                format: wgpu::VertexFormat::Float32x4,
836            },
837        ];
838        let ellipse_border_attrs: &[wgpu::VertexAttribute] = &[
839            wgpu::VertexAttribute {
840                shader_location: 0,
841                offset: 0,
842                format: wgpu::VertexFormat::Float32x4,
843            },
844            wgpu::VertexAttribute {
845                shader_location: 1,
846                offset: 16,
847                format: wgpu::VertexFormat::Float32,
848            },
849            wgpu::VertexAttribute {
850                shader_location: 2,
851                offset: 20,
852                format: wgpu::VertexFormat::Float32,
853            },
854            wgpu::VertexAttribute {
855                shader_location: 3,
856                offset: 24,
857                format: wgpu::VertexFormat::Uint32,
858            },
859            wgpu::VertexAttribute {
860                shader_location: 4,
861                offset: 28,
862                format: wgpu::VertexFormat::Uint32,
863            },
864            wgpu::VertexAttribute {
865                shader_location: 5,
866                offset: 32,
867                format: wgpu::VertexFormat::Float32x4,
868            },
869            wgpu::VertexAttribute {
870                shader_location: 6,
871                offset: 48,
872                format: wgpu::VertexFormat::Float32x4,
873            },
874            wgpu::VertexAttribute {
875                shader_location: 7,
876                offset: 64,
877                format: wgpu::VertexFormat::Float32x2,
878            },
879            wgpu::VertexAttribute {
880                shader_location: 8,
881                offset: 72,
882                format: wgpu::VertexFormat::Float32x2,
883            },
884            wgpu::VertexAttribute {
885                shader_location: 9,
886                offset: 80,
887                format: wgpu::VertexFormat::Uint32,
888            },
889            wgpu::VertexAttribute {
890                shader_location: 10,
891                offset: 96,
892                format: wgpu::VertexFormat::Float32x4,
893            },
894        ];
895
896        make_content_pipeline!(rects, "rect", RectInstance, rect_attrs);
897        make_content_pipeline!(borders, "border", BorderInstance, border_attrs);
898        make_content_pipeline!(ellipses, "ellipse", EllipseInstance, ellipse_attrs);
899        make_content_pipeline!(
900            ellipse_borders,
901            "ellipse_border",
902            EllipseBorderInstance,
903            ellipse_border_attrs
904        );
905
906        let arc_attrs: &[wgpu::VertexAttribute] = &[
907            wgpu::VertexAttribute {
908                shader_location: 0,
909                offset: 0,
910                format: wgpu::VertexFormat::Float32x4,
911            },
912            wgpu::VertexAttribute {
913                shader_location: 1,
914                offset: 16,
915                format: wgpu::VertexFormat::Float32,
916            },
917            wgpu::VertexAttribute {
918                shader_location: 2,
919                offset: 20,
920                format: wgpu::VertexFormat::Float32,
921            },
922            wgpu::VertexAttribute {
923                shader_location: 3,
924                offset: 24,
925                format: wgpu::VertexFormat::Float32,
926            },
927            wgpu::VertexAttribute {
928                shader_location: 4,
929                offset: 28,
930                format: wgpu::VertexFormat::Float32,
931            },
932            wgpu::VertexAttribute {
933                shader_location: 5,
934                offset: 32,
935                format: wgpu::VertexFormat::Uint32,
936            },
937            wgpu::VertexAttribute {
938                shader_location: 6,
939                offset: 36,
940                format: wgpu::VertexFormat::Uint32,
941            },
942            wgpu::VertexAttribute {
943                shader_location: 7,
944                offset: 48,
945                format: wgpu::VertexFormat::Float32x4,
946            },
947            wgpu::VertexAttribute {
948                shader_location: 8,
949                offset: 64,
950                format: wgpu::VertexFormat::Float32x4,
951            },
952            wgpu::VertexAttribute {
953                shader_location: 9,
954                offset: 80,
955                format: wgpu::VertexFormat::Float32x2,
956            },
957            wgpu::VertexAttribute {
958                shader_location: 10,
959                offset: 88,
960                format: wgpu::VertexFormat::Float32x2,
961            },
962            wgpu::VertexAttribute {
963                shader_location: 11,
964                offset: 96,
965                format: wgpu::VertexFormat::Uint32,
966            },
967            wgpu::VertexAttribute {
968                shader_location: 12,
969                offset: 100,
970                format: wgpu::VertexFormat::Float32,
971            },
972            wgpu::VertexAttribute {
973                shader_location: 13,
974                offset: 112,
975                format: wgpu::VertexFormat::Float32x4,
976            },
977        ];
978
979        make_content_pipeline!(arcs, "arc", ArcInstance, arc_attrs);
980
981        // Text (mask)
982        let text_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
983            label: Some("text.wgsl"),
984            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/text.wgsl"))),
985        });
986        // Text (color)
987        let text_color_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
988            label: Some("text_color.wgsl"),
989            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
990                "shaders/text_color.wgsl"
991            ))),
992        });
993        let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
994            label: Some("text pipeline layout"),
995            bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
996            immediate_size: 0,
997        });
998        let glyph_vertex = wgpu::VertexBufferLayout {
999            array_stride: std::mem::size_of::<GlyphInstance>() as u64,
1000            step_mode: wgpu::VertexStepMode::Instance,
1001            attributes: &[
1002                wgpu::VertexAttribute {
1003                    shader_location: 0,
1004                    offset: 0,
1005                    format: wgpu::VertexFormat::Float32x4,
1006                },
1007                wgpu::VertexAttribute {
1008                    shader_location: 1,
1009                    offset: 16,
1010                    format: wgpu::VertexFormat::Float32x4,
1011                },
1012                wgpu::VertexAttribute {
1013                    shader_location: 2,
1014                    offset: 32,
1015                    format: wgpu::VertexFormat::Float32x4,
1016                },
1017                wgpu::VertexAttribute {
1018                    shader_location: 3,
1019                    offset: 48,
1020                    format: wgpu::VertexFormat::Float32x4,
1021                },
1022            ],
1023        };
1024        let text_mask = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1025            label: Some("text pipeline (mask)"),
1026            layout: Some(&text_pipeline_layout),
1027            vertex: wgpu::VertexState {
1028                module: &text_mask_shader,
1029                entry_point: Some("vs_main"),
1030                buffers: &[Some(glyph_vertex.clone())],
1031                compilation_options: wgpu::PipelineCompilationOptions::default(),
1032            },
1033            fragment: Some(wgpu::FragmentState {
1034                module: &text_mask_shader,
1035                entry_point: Some("fs_main"),
1036                targets: &[Some(wgpu::ColorTargetState {
1037                    format,
1038                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1039                    write_mask: wgpu::ColorWrites::ALL,
1040                })],
1041                compilation_options: wgpu::PipelineCompilationOptions::default(),
1042            }),
1043            primitive: wgpu::PrimitiveState::default(),
1044            depth_stencil: Some(stencil_for_content.clone()),
1045            multisample: msaa_state,
1046            multiview_mask: None,
1047            cache: None,
1048        });
1049        let text_color = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1050            label: Some("text pipeline (color)"),
1051            layout: Some(&text_pipeline_layout),
1052            vertex: wgpu::VertexState {
1053                module: &text_color_shader,
1054                entry_point: Some("vs_main"),
1055                buffers: &[Some(glyph_vertex.clone())],
1056                compilation_options: wgpu::PipelineCompilationOptions::default(),
1057            },
1058            fragment: Some(wgpu::FragmentState {
1059                module: &text_color_shader,
1060                entry_point: Some("fs_main"),
1061                targets: &[Some(wgpu::ColorTargetState {
1062                    format,
1063                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1064                    write_mask: wgpu::ColorWrites::ALL,
1065                })],
1066                compilation_options: wgpu::PipelineCompilationOptions::default(),
1067            }),
1068            primitive: wgpu::PrimitiveState::default(),
1069            depth_stencil: Some(stencil_for_content.clone()),
1070            multisample: msaa_state,
1071            multiview_mask: None,
1072            cache: None,
1073        });
1074        // image_rgba reuses the text color pipeline (same vertex/bindings).
1075        let image_rgba = text_color.clone();
1076
1077        // Tinted A8 coverage composite. Same vertex attributes (GlyphInstance)
1078        // and bind groups (globals + texture/sampler) as the text color path,
1079        // sampling R8 tiles uploaded via `register_coverage_a8`.
1080        let coverage_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1081            label: Some("coverage.wgsl"),
1082            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/coverage.wgsl"))),
1083        });
1084        let coverage = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1085            label: Some("coverage pipeline (tinted a8)"),
1086            layout: Some(&text_pipeline_layout),
1087            vertex: wgpu::VertexState {
1088                module: &coverage_shader,
1089                entry_point: Some("vs_main"),
1090                buffers: &[Some(glyph_vertex.clone())],
1091                compilation_options: wgpu::PipelineCompilationOptions::default(),
1092            },
1093            fragment: Some(wgpu::FragmentState {
1094                module: &coverage_shader,
1095                entry_point: Some("fs_main"),
1096                targets: &[Some(wgpu::ColorTargetState {
1097                    format,
1098                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1099                    write_mask: wgpu::ColorWrites::ALL,
1100                })],
1101                compilation_options: wgpu::PipelineCompilationOptions::default(),
1102            }),
1103            primitive: wgpu::PrimitiveState::default(),
1104            depth_stencil: Some(stencil_for_content.clone()),
1105            multisample: msaa_state,
1106            multiview_mask: None,
1107            cache: None,
1108        });
1109
1110        // Blur composite pipeline (graphics-layer drop shadow)
1111        let blur_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1112            label: Some("blur_shadow.wgsl"),
1113            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1114                "shaders/blur_shadow.wgsl"
1115            ))),
1116        });
1117        let blur_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1118            label: Some("blur pipeline layout"),
1119            bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
1120            immediate_size: 0,
1121        });
1122        let blur = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1123            label: Some("blur pipeline"),
1124            layout: Some(&blur_pipeline_layout),
1125            vertex: wgpu::VertexState {
1126                module: &blur_shader,
1127                entry_point: Some("vs_main"),
1128                buffers: &[Some(wgpu::VertexBufferLayout {
1129                    array_stride: std::mem::size_of::<BlurInstance>() as u64,
1130                    step_mode: wgpu::VertexStepMode::Instance,
1131                    attributes: &[
1132                        wgpu::VertexAttribute {
1133                            shader_location: 0,
1134                            offset: 0,
1135                            format: wgpu::VertexFormat::Float32x4,
1136                        },
1137                        wgpu::VertexAttribute {
1138                            shader_location: 1,
1139                            offset: 16,
1140                            format: wgpu::VertexFormat::Float32x4,
1141                        },
1142                        wgpu::VertexAttribute {
1143                            shader_location: 2,
1144                            offset: 32,
1145                            format: wgpu::VertexFormat::Float32x4,
1146                        },
1147                        wgpu::VertexAttribute {
1148                            shader_location: 3,
1149                            offset: 48,
1150                            format: wgpu::VertexFormat::Float32x2,
1151                        },
1152                        wgpu::VertexAttribute {
1153                            shader_location: 4,
1154                            offset: 56,
1155                            format: wgpu::VertexFormat::Float32x4,
1156                        },
1157                        wgpu::VertexAttribute {
1158                            shader_location: 5,
1159                            offset: 72,
1160                            format: wgpu::VertexFormat::Uint32,
1161                        },
1162                    ],
1163                })],
1164                compilation_options: wgpu::PipelineCompilationOptions::default(),
1165            },
1166            fragment: Some(wgpu::FragmentState {
1167                module: &blur_shader,
1168                entry_point: Some("fs_main"),
1169                targets: &[Some(wgpu::ColorTargetState {
1170                    format,
1171                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1172                    write_mask: wgpu::ColorWrites::ALL,
1173                })],
1174                compilation_options: wgpu::PipelineCompilationOptions::default(),
1175            }),
1176            primitive: wgpu::PrimitiveState::default(),
1177            depth_stencil: Some(stencil_for_content.clone()),
1178            multisample: msaa_state,
1179            multiview_mask: None,
1180            cache: None,
1181        });
1182
1183        // Content blur pipeline (full RGBA gaussian blur)
1184        let blur_content_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1185            label: Some("blur_content.wgsl"),
1186            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1187                "shaders/blur_content.wgsl"
1188            ))),
1189        });
1190        let blur_content = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1191            label: Some("blur content pipeline"),
1192            layout: Some(&blur_pipeline_layout),
1193            vertex: wgpu::VertexState {
1194                module: &blur_content_shader,
1195                entry_point: Some("vs_main"),
1196                buffers: &[Some(wgpu::VertexBufferLayout {
1197                    array_stride: std::mem::size_of::<BlurInstance>() as u64,
1198                    step_mode: wgpu::VertexStepMode::Instance,
1199                    attributes: &[
1200                        wgpu::VertexAttribute {
1201                            shader_location: 0,
1202                            offset: 0,
1203                            format: wgpu::VertexFormat::Float32x4,
1204                        },
1205                        wgpu::VertexAttribute {
1206                            shader_location: 1,
1207                            offset: 16,
1208                            format: wgpu::VertexFormat::Float32x4,
1209                        },
1210                        wgpu::VertexAttribute {
1211                            shader_location: 2,
1212                            offset: 32,
1213                            format: wgpu::VertexFormat::Float32x4,
1214                        },
1215                        wgpu::VertexAttribute {
1216                            shader_location: 3,
1217                            offset: 48,
1218                            format: wgpu::VertexFormat::Float32x2,
1219                        },
1220                        wgpu::VertexAttribute {
1221                            shader_location: 4,
1222                            offset: 56,
1223                            format: wgpu::VertexFormat::Float32x4,
1224                        },
1225                        wgpu::VertexAttribute {
1226                            shader_location: 5,
1227                            offset: 72,
1228                            format: wgpu::VertexFormat::Uint32,
1229                        },
1230                    ],
1231                })],
1232                compilation_options: wgpu::PipelineCompilationOptions::default(),
1233            },
1234            fragment: Some(wgpu::FragmentState {
1235                module: &blur_content_shader,
1236                entry_point: Some("fs_main"),
1237                targets: &[Some(wgpu::ColorTargetState {
1238                    format,
1239                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1240                    write_mask: wgpu::ColorWrites::ALL,
1241                })],
1242                compilation_options: wgpu::PipelineCompilationOptions::default(),
1243            }),
1244            primitive: wgpu::PrimitiveState::default(),
1245            depth_stencil: Some(stencil_for_content.clone()),
1246            multisample: msaa_state,
1247            multiview_mask: None,
1248            cache: None,
1249        });
1250
1251        // NV12 Image Pipeline
1252        let image_nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1253            label: Some("image_nv12.wgsl"),
1254            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1255                "shaders/image_nv12.wgsl"
1256            ))),
1257        });
1258        let image_nv12_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1259            label: Some("image nv12 pipeline layout"),
1260            bind_group_layouts: &[Some(globals_layout), Some(image_bind_layout_nv12)],
1261            immediate_size: 0,
1262        });
1263        let image_nv12 = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1264            label: Some("image nv12 pipeline"),
1265            layout: Some(&image_nv12_layout),
1266            vertex: wgpu::VertexState {
1267                module: &image_nv12_shader,
1268                entry_point: Some("vs_main"),
1269                buffers: &[Some(wgpu::VertexBufferLayout {
1270                    array_stride: std::mem::size_of::<Nv12Instance>() as u64,
1271                    step_mode: wgpu::VertexStepMode::Instance,
1272                    attributes: &[
1273                        wgpu::VertexAttribute {
1274                            shader_location: 0,
1275                            offset: 0,
1276                            format: wgpu::VertexFormat::Float32x4,
1277                        },
1278                        wgpu::VertexAttribute {
1279                            shader_location: 1,
1280                            offset: 16,
1281                            format: wgpu::VertexFormat::Float32x4,
1282                        },
1283                        wgpu::VertexAttribute {
1284                            shader_location: 2,
1285                            offset: 32,
1286                            format: wgpu::VertexFormat::Float32x4,
1287                        },
1288                        wgpu::VertexAttribute {
1289                            shader_location: 3,
1290                            offset: 48,
1291                            format: wgpu::VertexFormat::Float32,
1292                        },
1293                        wgpu::VertexAttribute {
1294                            shader_location: 4,
1295                            offset: 52,
1296                            format: wgpu::VertexFormat::Float32,
1297                        },
1298                        wgpu::VertexAttribute {
1299                            shader_location: 5,
1300                            offset: 56,
1301                            format: wgpu::VertexFormat::Float32x4,
1302                        },
1303                    ],
1304                })],
1305                compilation_options: wgpu::PipelineCompilationOptions::default(),
1306            },
1307            fragment: Some(wgpu::FragmentState {
1308                module: &image_nv12_shader,
1309                entry_point: Some("fs_main"),
1310                targets: &[Some(wgpu::ColorTargetState {
1311                    format,
1312                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1313                    write_mask: wgpu::ColorWrites::ALL,
1314                })],
1315                compilation_options: wgpu::PipelineCompilationOptions::default(),
1316            }),
1317            primitive: wgpu::PrimitiveState::default(),
1318            depth_stencil: Some(stencil_for_content.clone()),
1319            multisample: msaa_state,
1320            multiview_mask: None,
1321            cache: None,
1322        });
1323
1324        let clip_color_target = wgpu::ColorTargetState {
1325            format,
1326            blend: None,
1327            write_mask: wgpu::ColorWrites::empty(),
1328        };
1329
1330        // Clipping
1331        let clip_shader_bin = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1332            label: Some("clip_round_rect_bin.wgsl"),
1333            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1334                "shaders/clip_round_rect_bin.wgsl"
1335            ))),
1336        });
1337        let clip_bin = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1338            label: Some("clip pipeline (bin)"),
1339            layout: Some(clip_pipeline_layout),
1340            vertex: wgpu::VertexState {
1341                module: &clip_shader_bin,
1342                entry_point: Some("vs_main"),
1343                buffers: &[Some(clip_vertex_layout.clone())],
1344                compilation_options: wgpu::PipelineCompilationOptions::default(),
1345            },
1346            fragment: Some(wgpu::FragmentState {
1347                module: &clip_shader_bin,
1348                entry_point: Some("fs_main"),
1349                targets: &[Some(clip_color_target.clone())],
1350                compilation_options: wgpu::PipelineCompilationOptions::default(),
1351            }),
1352            primitive: wgpu::PrimitiveState::default(),
1353            depth_stencil: Some(stencil_for_clip_inc.clone()),
1354            multisample: wgpu::MultisampleState {
1355                count: sample_count,
1356                mask: !0,
1357                alpha_to_coverage_enabled: false,
1358            },
1359            multiview_mask: None,
1360            cache: None,
1361        });
1362        let clip_dec = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1363            label: Some("clip pipeline (dec)"),
1364            layout: Some(clip_pipeline_layout),
1365            vertex: wgpu::VertexState {
1366                module: &clip_shader_bin,
1367                entry_point: Some("vs_main"),
1368                buffers: &[Some(clip_vertex_layout.clone())],
1369                compilation_options: wgpu::PipelineCompilationOptions::default(),
1370            },
1371            fragment: Some(wgpu::FragmentState {
1372                module: &clip_shader_bin,
1373                entry_point: Some("fs_main"),
1374                targets: &[Some(clip_color_target.clone())],
1375                compilation_options: wgpu::PipelineCompilationOptions::default(),
1376            }),
1377            primitive: wgpu::PrimitiveState::default(),
1378            depth_stencil: Some(stencil_for_clip_dec.clone()),
1379            multisample: wgpu::MultisampleState {
1380                count: sample_count,
1381                mask: !0,
1382                alpha_to_coverage_enabled: false,
1383            },
1384            multiview_mask: None,
1385            cache: None,
1386        });
1387
1388        let slug = Some(slug::create_pipeline(
1389            device,
1390            format,
1391            sample_count,
1392            stencil_for_content,
1393        ));
1394
1395        // Tessellated vector mesh pipeline (host-supplied vertex/index data).
1396        let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1397            label: Some("mesh.wgsl"),
1398            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/mesh.wgsl"))),
1399        });
1400        let mesh_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1401            label: Some("mesh pipeline layout"),
1402            bind_group_layouts: &[Some(globals_layout), Some(mesh_bind_layout)],
1403            immediate_size: 0,
1404        });
1405        let mesh_vertex_layout = wgpu::VertexBufferLayout {
1406            array_stride: std::mem::size_of::<MeshVertex>() as u64,
1407            step_mode: wgpu::VertexStepMode::Vertex,
1408            attributes: &[
1409                wgpu::VertexAttribute {
1410                    shader_location: 0,
1411                    offset: 0,
1412                    format: wgpu::VertexFormat::Float32x2,
1413                },
1414                wgpu::VertexAttribute {
1415                    shader_location: 1,
1416                    offset: 8,
1417                    format: wgpu::VertexFormat::Float32x4,
1418                },
1419                wgpu::VertexAttribute {
1420                    shader_location: 2,
1421                    offset: 24,
1422                    format: wgpu::VertexFormat::Float32x2,
1423                },
1424            ],
1425        };
1426        let make_mesh_pipeline =
1427            |label: &str, depth: &wgpu::DepthStencilState, color: &wgpu::ColorTargetState| {
1428                device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1429                    label: Some(label),
1430                    layout: Some(&mesh_pipeline_layout),
1431                    vertex: wgpu::VertexState {
1432                        module: &mesh_shader,
1433                        entry_point: Some("vs_main"),
1434                        buffers: &[Some(mesh_vertex_layout.clone())],
1435                        compilation_options: wgpu::PipelineCompilationOptions::default(),
1436                    },
1437                    fragment: Some(wgpu::FragmentState {
1438                        module: &mesh_shader,
1439                        entry_point: Some("fs_main"),
1440                        targets: &[Some(color.clone())],
1441                        compilation_options: wgpu::PipelineCompilationOptions::default(),
1442                    }),
1443                    primitive: wgpu::PrimitiveState {
1444                        topology: wgpu::PrimitiveTopology::TriangleList,
1445                        ..Default::default()
1446                    },
1447                    depth_stencil: Some(depth.clone()),
1448                    multisample: msaa_state,
1449                    multiview_mask: None,
1450                    cache: None,
1451                })
1452            };
1453        let mesh_color_target = wgpu::ColorTargetState {
1454            format,
1455            blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1456            write_mask: wgpu::ColorWrites::ALL,
1457        };
1458        let mut stencil_for_mesh = stencil_for_content.clone();
1459        stencil_for_mesh.stencil.front.compare = wgpu::CompareFunction::Equal;
1460        stencil_for_mesh.stencil.back.compare = wgpu::CompareFunction::Equal;
1461        let mut stencil_for_overlay = stencil_for_content.clone();
1462        stencil_for_overlay.stencil.front.compare = wgpu::CompareFunction::LessEqual;
1463        stencil_for_overlay.stencil.back.compare = wgpu::CompareFunction::LessEqual;
1464        let mesh = make_mesh_pipeline("mesh pipeline", &stencil_for_mesh, &mesh_color_target);
1465        let mesh_overlay = make_mesh_pipeline(
1466            "mesh overlay pipeline",
1467            &stencil_for_overlay,
1468            &mesh_color_target,
1469        );
1470        let mesh_clip_inc = make_mesh_pipeline(
1471            "mesh clip (inc) pipeline",
1472            stencil_for_clip_inc,
1473            &clip_color_target,
1474        );
1475        let mesh_clip_dec = make_mesh_pipeline(
1476            "mesh clip (dec) pipeline",
1477            stencil_for_clip_dec,
1478            &clip_color_target,
1479        );
1480        let mesh_blend_target = |blend: wgpu::BlendState| wgpu::ColorTargetState {
1481            format,
1482            blend: Some(blend),
1483            write_mask: wgpu::ColorWrites::ALL,
1484        };
1485        let premult_alpha = wgpu::BlendComponent::OVER;
1486        let mesh_add = make_mesh_pipeline(
1487            "mesh pipeline (add)",
1488            &stencil_for_mesh,
1489            &mesh_blend_target(wgpu::BlendState {
1490                color: wgpu::BlendComponent {
1491                    src_factor: wgpu::BlendFactor::One,
1492                    dst_factor: wgpu::BlendFactor::One,
1493                    operation: wgpu::BlendOperation::Add,
1494                },
1495                alpha: wgpu::BlendComponent {
1496                    src_factor: wgpu::BlendFactor::One,
1497                    dst_factor: wgpu::BlendFactor::One,
1498                    operation: wgpu::BlendOperation::Add,
1499                },
1500            }),
1501        );
1502        let mesh_multiply = make_mesh_pipeline(
1503            "mesh pipeline (multiply)",
1504            &stencil_for_mesh,
1505            &mesh_blend_target(wgpu::BlendState {
1506                color: wgpu::BlendComponent {
1507                    src_factor: wgpu::BlendFactor::Dst,
1508                    dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1509                    operation: wgpu::BlendOperation::Add,
1510                },
1511                alpha: premult_alpha,
1512            }),
1513        );
1514        let mesh_screen = make_mesh_pipeline(
1515            "mesh pipeline (screen)",
1516            &stencil_for_mesh,
1517            &mesh_blend_target(wgpu::BlendState {
1518                color: wgpu::BlendComponent {
1519                    src_factor: wgpu::BlendFactor::One,
1520                    dst_factor: wgpu::BlendFactor::OneMinusSrc,
1521                    operation: wgpu::BlendOperation::Add,
1522                },
1523                alpha: premult_alpha,
1524            }),
1525        );
1526        let mesh_darken = make_mesh_pipeline(
1527            "mesh pipeline (darken)",
1528            &stencil_for_mesh,
1529            &mesh_blend_target(wgpu::BlendState {
1530                color: wgpu::BlendComponent {
1531                    src_factor: wgpu::BlendFactor::One,
1532                    dst_factor: wgpu::BlendFactor::One,
1533                    operation: wgpu::BlendOperation::Min,
1534                },
1535                alpha: premult_alpha,
1536            }),
1537        );
1538        let mesh_lighten = make_mesh_pipeline(
1539            "mesh pipeline (lighten)",
1540            &stencil_for_mesh,
1541            &mesh_blend_target(wgpu::BlendState {
1542                color: wgpu::BlendComponent {
1543                    src_factor: wgpu::BlendFactor::One,
1544                    dst_factor: wgpu::BlendFactor::One,
1545                    operation: wgpu::BlendOperation::Max,
1546                },
1547                alpha: premult_alpha,
1548            }),
1549        );
1550        // Projective layer composite (perspective flattening). Same
1551        // bind groups as the text/image path (globals + layer texture), with
1552        // per-instance projected corners. Like `image_rgba` it draws into the
1553        // parent target, so it shares the content stencil state.
1554        let projective_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1555            label: Some("projective_layer.wgsl"),
1556            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1557                "shaders/projective_layer.wgsl"
1558            ))),
1559        });
1560        let projective_pipeline_layout =
1561            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1562                label: Some("projective layer pipeline layout"),
1563                bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
1564                immediate_size: 0,
1565            });
1566        let projective_vertex_layout = wgpu::VertexBufferLayout {
1567            array_stride: std::mem::size_of::<ProjectiveInstance>() as u64,
1568            step_mode: wgpu::VertexStepMode::Instance,
1569            attributes: &[
1570                wgpu::VertexAttribute {
1571                    shader_location: 0,
1572                    offset: 0,
1573                    format: wgpu::VertexFormat::Float32x2,
1574                },
1575                wgpu::VertexAttribute {
1576                    shader_location: 1,
1577                    offset: 8,
1578                    format: wgpu::VertexFormat::Float32x2,
1579                },
1580                wgpu::VertexAttribute {
1581                    shader_location: 2,
1582                    offset: 16,
1583                    format: wgpu::VertexFormat::Float32x2,
1584                },
1585                wgpu::VertexAttribute {
1586                    shader_location: 3,
1587                    offset: 24,
1588                    format: wgpu::VertexFormat::Float32x2,
1589                },
1590                wgpu::VertexAttribute {
1591                    shader_location: 4,
1592                    offset: 32,
1593                    format: wgpu::VertexFormat::Float32x4,
1594                },
1595                wgpu::VertexAttribute {
1596                    shader_location: 5,
1597                    offset: 48,
1598                    format: wgpu::VertexFormat::Float32x4,
1599                },
1600                wgpu::VertexAttribute {
1601                    shader_location: 6,
1602                    offset: 64,
1603                    format: wgpu::VertexFormat::Float32,
1604                },
1605            ],
1606        };
1607        let projective_layer = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1608            label: Some("projective layer composite pipeline"),
1609            layout: Some(&projective_pipeline_layout),
1610            vertex: wgpu::VertexState {
1611                module: &projective_shader,
1612                entry_point: Some("vs_main"),
1613                buffers: &[Some(projective_vertex_layout)],
1614                compilation_options: wgpu::PipelineCompilationOptions::default(),
1615            },
1616            fragment: Some(wgpu::FragmentState {
1617                module: &projective_shader,
1618                entry_point: Some("fs_main"),
1619                targets: &[Some(wgpu::ColorTargetState {
1620                    format,
1621                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1622                    write_mask: wgpu::ColorWrites::ALL,
1623                })],
1624                compilation_options: wgpu::PipelineCompilationOptions::default(),
1625            }),
1626            primitive: wgpu::PrimitiveState::default(),
1627            depth_stencil: Some(stencil_for_content.clone()),
1628            multisample: msaa_state,
1629            multiview_mask: None,
1630            cache: None,
1631        });
1632
1633        let blend_layer_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1634            label: Some("blend_layer.wgsl"),
1635            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1636                "shaders/blend_layer.wgsl"
1637            ))),
1638        });
1639        let blend_layer_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1640            label: Some("blend layer pipeline layout"),
1641            bind_group_layouts: &[
1642                Some(globals_layout),
1643                Some(text_bind_layout),
1644                Some(text_bind_layout),
1645            ],
1646            immediate_size: 0,
1647        });
1648        let blend_vertex_layout = wgpu::VertexBufferLayout {
1649            array_stride: std::mem::size_of::<BlendInstance>() as u64,
1650            step_mode: wgpu::VertexStepMode::Instance,
1651            attributes: &[
1652                wgpu::VertexAttribute {
1653                    shader_location: 0,
1654                    offset: 0,
1655                    format: wgpu::VertexFormat::Float32x4,
1656                },
1657                wgpu::VertexAttribute {
1658                    shader_location: 1,
1659                    offset: 16,
1660                    format: wgpu::VertexFormat::Float32x4,
1661                },
1662                wgpu::VertexAttribute {
1663                    shader_location: 2,
1664                    offset: 32,
1665                    format: wgpu::VertexFormat::Float32x4,
1666                },
1667                wgpu::VertexAttribute {
1668                    shader_location: 3,
1669                    offset: 48,
1670                    format: wgpu::VertexFormat::Float32x4,
1671                },
1672                wgpu::VertexAttribute {
1673                    shader_location: 4,
1674                    offset: 64,
1675                    format: wgpu::VertexFormat::Uint32,
1676                },
1677            ],
1678        };
1679        let blend_layer = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1680            label: Some("blend layer pipeline"),
1681            layout: Some(&blend_layer_layout),
1682            vertex: wgpu::VertexState {
1683                module: &blend_layer_shader,
1684                entry_point: Some("vs_main"),
1685                buffers: &[Some(blend_vertex_layout)],
1686                compilation_options: wgpu::PipelineCompilationOptions::default(),
1687            },
1688            fragment: Some(wgpu::FragmentState {
1689                module: &blend_layer_shader,
1690                entry_point: Some("fs_main"),
1691                targets: &[Some(wgpu::ColorTargetState {
1692                    format,
1693                    blend: None,
1694                    write_mask: wgpu::ColorWrites::ALL,
1695                })],
1696                compilation_options: wgpu::PipelineCompilationOptions::default(),
1697            }),
1698            primitive: wgpu::PrimitiveState::default(),
1699            depth_stencil: Some(stencil_for_content.clone()),
1700            multisample: msaa_state,
1701            multiview_mask: None,
1702            cache: None,
1703        });
1704
1705        Self {
1706            rects,
1707            borders,
1708            ellipses,
1709            ellipse_borders,
1710            arcs,
1711            text_mask,
1712            text_color,
1713            image_rgba,
1714            image_nv12,
1715            coverage,
1716            blur,
1717            blur_content,
1718            clip_bin,
1719            clip_dec,
1720            slug,
1721            mesh,
1722            mesh_add,
1723            mesh_multiply,
1724            mesh_screen,
1725            mesh_darken,
1726            mesh_lighten,
1727            blend_layer,
1728            mesh_overlay,
1729            mesh_clip_inc,
1730            mesh_clip_dec,
1731            projective_layer,
1732        }
1733    }
1734}
1735
1736/// A segment of the frame that draws into a single render target.
1737struct Pass {
1738    id: u64,
1739    target: PassTarget,
1740    /// The scissor to apply when the pass is opened.
1741    initial_scissor: (u32, u32, u32, u32),
1742    /// The currently active scissor in the translated pass. `None` means an
1743    /// empty intersection and all draws must be suppressed.
1744    active_scissor: Option<(u32, u32, u32, u32)>,
1745    /// `None` means `LoadOp::Load` (resume existing content);
1746    /// `Some(c)` means `LoadOp::Clear(c)`.
1747    clear_color: Option<[f32; 4]>,
1748    cmds: Vec<Cmd>,
1749}
1750
1751struct LayerState {
1752    layer_id: u32,
1753    parent_target: PassTarget,
1754    parent_scissors: Vec<repose_core::Rect>,
1755    parent_root_clip: repose_core::Rect,
1756    parent_size: (f32, f32),
1757    parent_transform: Transform,
1758    parent_scissor: Option<(u32, u32, u32, u32)>,
1759    parent_clips: Vec<ActiveClip>,
1760    alpha: f32,
1761    blur: (f32, f32),
1762    rectangle_edge: bool,
1763}
1764
1765/// One translator-flattened perspective layer (see
1766/// `push_perspective_layer`): the projective map and everything needed to
1767/// restore the parent target on the matching pop.
1768struct FlattenRecord {
1769    /// `transform_stack.len()` before this flatten pushed its two entries
1770    /// (stripped affine + layer-local shift).
1771    stack_len: usize,
1772    layer_id: u32,
1773    /// Full projective map (row-major 3x3): affine ancestors over the
1774    /// perspective node, in parent-target coordinates.
1775    map: [f32; 9],
1776    /// Layer rect in the parent target's coordinates.
1777    layer_rect: repose_core::Rect,
1778    saved_scissor_stack: Vec<repose_core::Rect>,
1779    saved_root: repose_core::Rect,
1780    saved_size: (f32, f32),
1781    saved_active_scissor: Option<(u32, u32, u32, u32)>,
1782    saved_clips: Vec<ActiveClip>,
1783}
1784
1785/// First translator-owned flatten layer id. Producer ids start at 1 per
1786/// scene, so this range never collides; ids are drained from the layer pool
1787/// after each frame, so reuse across frames is safe.
1788const FLATTEN_ID_BASE: u32 = 0xF000_0000;
1789const MAX_GRAPHICS_LAYERS: usize = 128;
1790const MAX_GRAPHICS_LAYER_BYTES: u64 = 256 * 1024 * 1024;
1791const MAX_TRANSIENT_GRAPHICS_LAYERS: usize = 64;
1792const MAX_TRANSIENT_GRAPHICS_LAYER_BYTES: u64 = 128 * 1024 * 1024;
1793const STENCIL_BASE: u32 = 128;
1794const STENCIL_MAX_DEPTH: u32 = 127;
1795
1796#[allow(non_snake_case)]
1797enum Cmd {
1798    ClipPush {
1799        off: u64,
1800        cnt: u32,
1801        scissor: (u32, u32, u32, u32),
1802        difference: bool,
1803        applied: bool,
1804    },
1805    ClipPop {
1806        off: u64,
1807        cnt: u32,
1808        /// Scissor restored for the draws that follow the pop (the parent clip).
1809        scissor: (u32, u32, u32, u32),
1810        /// Scissor the matching push drew its stencil pass with; the decrement
1811        /// must use it so both passes touch the same pixels.
1812        stencil_scissor: (u32, u32, u32, u32),
1813        difference: bool,
1814        applied: bool,
1815    },
1816    Rect {
1817        off: u64,
1818        cnt: u32,
1819    },
1820    Border {
1821        off: u64,
1822        cnt: u32,
1823    },
1824    Ellipse {
1825        off: u64,
1826        cnt: u32,
1827    },
1828    EllipseBorder {
1829        off: u64,
1830        cnt: u32,
1831    },
1832    Arc {
1833        off: u64,
1834        cnt: u32,
1835    },
1836    GlyphsMask {
1837        off: u64,
1838        cnt: u32,
1839    },
1840    GlyphsColor {
1841        off: u64,
1842        cnt: u32,
1843    },
1844    GlyphsVector {
1845        off: u64,
1846        cnt: u32,
1847    },
1848    ImageRgba {
1849        off: u64,
1850        cnt: u32,
1851        handle: u64,
1852        filter: ImageFilter,
1853        /// Bind to the repeating samplers so `ImageFit::Tile` UVs wrap.
1854        tile: bool,
1855    },
1856    /// Composite a tinted A8 coverage tile (`SceneNode::Coverage`). The
1857    /// instance lives in `self.glyph_color.ring` (a `GlyphInstance`); the
1858    /// bind comes from the coverage registry.
1859    Coverage {
1860        off: u64,
1861        cnt: u32,
1862        handle: u64,
1863    },
1864    ImageNv12 {
1865        off: u64,
1866        cnt: u32,
1867        handle: u64,
1868        filter: ImageFilter,
1869    },
1870    /// Composite a previously-rendered graphics layer back into the
1871    /// current target as a textured quad. The quad's vertex buffer
1872    /// lives in `self.glyph_color.ring` (a `GlyphInstance`).
1873    CompositeLayer {
1874        off: u64,
1875        cnt: u32,
1876        layer_id: u32,
1877    },
1878    /// Composite a blurred drop shadow of a previously-rendered graphics
1879    /// layer. The quad's vertex buffer lives in `self.blur_ring` (a
1880    /// `BlurInstance`).
1881    CompositeShadow {
1882        off: u64,
1883        cnt: u32,
1884        layer_id: u32,
1885    },
1886    /// Apply gaussian blur to a layer and composite the blurred result.
1887    /// Uses the `blur_content` pipeline (full RGBA blur).
1888    CompositeBlur {
1889        off: u64,
1890        cnt: u32,
1891        layer_id: u32,
1892    },
1893    /// Composite a flattened perspective layer through its projective map.
1894    /// The instance lives in `self.projective_ring` (a `ProjectiveInstance`
1895    /// with CPU-projected NDC corners); sampled from the layer's texture
1896    /// with perspective-correct UVs by the `projective_layer` pipeline.
1897    CompositeProjective {
1898        off: u64,
1899        cnt: u32,
1900        layer_id: u32,
1901    },
1902    /// Draw a tessellated vector mesh (solid or gradient paint).
1903    VectorMesh {
1904        mesh: MeshGpu,
1905        uoff: u64,
1906        blend: repose_core::BlendMode,
1907    },
1908    /// Composite an isolated source layer over the current target with a
1909    /// backdrop-dependent CSS blend mode. The shader samples both textures
1910    /// and composites source-over by hand, so the pass runs REPLACE.
1911    /// `parent` records which target the composite draws into (surface or
1912    /// layer); the instance NDC is mapped against it, so any parent origin
1913    /// works.
1914    BlendLayer {
1915        off: u64,
1916        cnt: u32,
1917        src_layer: u32,
1918        dst_layer: Option<u32>,
1919        parent: PassTarget,
1920        scissor: (u32, u32, u32, u32),
1921    },
1922    /// Draw a screen-space overlay mesh (identity transform, device pixels).
1923    VectorOverlay {
1924        mesh: MeshGpu,
1925        uoff: u64,
1926    },
1927    /// Increment the stencil buffer with a tessellated vector mask.
1928    /// `difference` marks an inverse (`\iclip`-style) mask: content draws
1929    /// *outside* it. The counting still balances (push increments, pop
1930    /// decrements); only the depth bookkeeping differs (see executor).
1931    VectorClipPush {
1932        mesh: MeshGpu,
1933        uoff: u64,
1934        scissor: (u32, u32, u32, u32),
1935        difference: bool,
1936        applied: bool,
1937    },
1938    /// Decrement the stencil buffer with the matching vector mask.
1939    VectorClipPop {
1940        mesh: MeshGpu,
1941        uoff: u64,
1942        scissor: (u32, u32, u32, u32),
1943        stencil_scissor: (u32, u32, u32, u32),
1944        difference: bool,
1945        applied: bool,
1946    },
1947    Callback {
1948        rect: repose_core::Rect,
1949        clip_rect: repose_core::Rect,
1950        scissor: (u32, u32, u32, u32),
1951        restore_scissor: (u32, u32, u32, u32),
1952        callback_id: usize,
1953        payload: repose_core::PaintCallbackPayload,
1954    },
1955}
1956
1957/// A registered A8 coverage tile: single-channel mask sampled as coverage
1958/// by `SceneNode::Coverage`. Tiles are immutable; producers re-register on
1959/// geometry change and `remove_coverage` stale handles (unused tiles also
1960/// age out via the image eviction policy).
1961struct CoverageTex {
1962    // Held to keep the GPU texture alive (freed on remove/evict).
1963    #[allow(dead_code)]
1964    tex: wgpu::Texture,
1965    bind: wgpu::BindGroup,
1966    w: u32,
1967    h: u32,
1968    last_used_frame: u64,
1969    bytes: u64,
1970}
1971
1972struct ImageBinds {
1973    linear: wgpu::BindGroup,
1974    nearest: wgpu::BindGroup,
1975    /// `ImageFit::Tile` variants, bound to repeating samplers.
1976    tile_linear: wgpu::BindGroup,
1977    tile_nearest: wgpu::BindGroup,
1978}
1979
1980impl ImageBinds {
1981    fn get(&self, filter: ImageFilter, tile: bool) -> &wgpu::BindGroup {
1982        match (filter, tile) {
1983            (ImageFilter::Linear, false) => &self.linear,
1984            (ImageFilter::Nearest, false) => &self.nearest,
1985            (ImageFilter::Linear, true) => &self.tile_linear,
1986            (ImageFilter::Nearest, true) => &self.tile_nearest,
1987        }
1988    }
1989}
1990
1991enum ImageTex {
1992    Rgba {
1993        tex: wgpu::Texture,
1994        binds: ImageBinds,
1995        w: u32,
1996        h: u32,
1997        format: wgpu::TextureFormat,
1998        last_used_frame: u64,
1999        bytes: u64,
2000    },
2001    /// For a user-provided texture view.
2002    User {
2003        binds: ImageBinds,
2004        w: u32,
2005        h: u32,
2006        last_used_frame: u64,
2007        bytes: u64,
2008    },
2009    Nv12 {
2010        tex_y: wgpu::Texture,
2011        tex_uv: wgpu::Texture,
2012        binds: ImageBinds,
2013        yuv_buf: wgpu::Buffer,
2014        w: u32,
2015        h: u32,
2016        pixel_format: PixelFormat,
2017        fourcc: Option<u32>,
2018        color_info: ColorInfo,
2019        external: bool,
2020        last_used_frame: u64,
2021        bytes: u64,
2022    },
2023}
2024
2025impl ImageTex {
2026    fn evictable(&self) -> bool {
2027        match self {
2028            Self::Rgba { .. } => true,
2029            Self::User { .. } => false,
2030            Self::Nv12 { external, .. } => !*external,
2031        }
2032    }
2033}
2034
2035#[derive(Clone)]
2036struct RetainedImage {
2037    w: u32,
2038    h: u32,
2039    format: wgpu::TextureFormat,
2040    rgba: Vec<u8>,
2041    last_used_frame: u64,
2042}
2043
2044struct ResolvedImageSource {
2045    width: f32,
2046    height: f32,
2047    uv_min: [f32; 2],
2048    uv_max: [f32; 2],
2049}
2050
2051fn resolve_image_source(
2052    source_rect: Option<ImageSourceRect>,
2053    image_width: u32,
2054    image_height: u32,
2055) -> Option<ResolvedImageSource> {
2056    if image_width == 0 || image_height == 0 {
2057        return None;
2058    }
2059    let Some(source) = source_rect else {
2060        return Some(ResolvedImageSource {
2061            width: image_width as f32,
2062            height: image_height as f32,
2063            uv_min: [0.0, 0.0],
2064            uv_max: [1.0, 1.0],
2065        });
2066    };
2067    if source.is_empty() {
2068        return None;
2069    }
2070    let right = source.x.checked_add(source.width)?;
2071    let bottom = source.y.checked_add(source.height)?;
2072    if right > image_width || bottom > image_height {
2073        return None;
2074    }
2075    Some(ResolvedImageSource {
2076        width: source.width as f32,
2077        height: source.height as f32,
2078        uv_min: [
2079            source.x as f32 / image_width as f32,
2080            source.y as f32 / image_height as f32,
2081        ],
2082        uv_max: [
2083            right as f32 / image_width as f32,
2084            bottom as f32 / image_height as f32,
2085        ],
2086    })
2087}
2088
2089/// Draw rect and UV range for `fit` inside `rect`.
2090///
2091/// Mirrors Compose's `PainterNode.draw`: scale the source by
2092/// `ContentScale.computeScaleFactor`, align the result within the bounds, then
2093/// clip to those bounds. Clipping remaps the UVs rather than pushing a clip
2094/// rect, matching what Compose's `clipToBounds` and Godot's `clip_contents`
2095/// produce visually without the extra pass. Modes that scale past an edge
2096/// (`Cover`, `FitWidth`, `FitHeight`, `None`) rely on it to stay in bounds.
2097fn image_fit_geometry(
2098    rect: repose_core::Rect,
2099    fit: ImageFit,
2100    alignment: ImageAlignment,
2101    source: &ResolvedImageSource,
2102) -> Option<(repose_core::Rect, [f32; 4])> {
2103    let src_w = source.width;
2104    let src_h = source.height;
2105    let dst_w = rect.w.max(0.0);
2106    let dst_h = rect.h.max(0.0);
2107    if src_w <= 0.0 || src_h <= 0.0 || dst_w <= 0.0 || dst_h <= 0.0 {
2108        return None;
2109    }
2110
2111    // Slack placement inside the destination rect: `alignment` picks which
2112    // corner the fitted image hugs (CSS `object-position`).
2113    let slack = |content: f32, extent: f32| match alignment {
2114        ImageAlignment::Center => (extent - content) * 0.5,
2115        ImageAlignment::Begin => 0.0,
2116        ImageAlignment::End => extent - content,
2117    };
2118
2119    // A tiled draw covers the bounds at native resolution, so its UVs run past
2120    // 1.0 and the sampler wraps (see address-mode selection at draw time).
2121    let tiled = matches!(fit, ImageFit::Tile);
2122    let (scale_x, scale_y) = fit.scale_factor((src_w, src_h), (dst_w, dst_h));
2123    let drawn = if tiled {
2124        repose_core::Rect {
2125            x: rect.x,
2126            y: rect.y,
2127            w: dst_w,
2128            h: dst_h,
2129        }
2130    } else {
2131        let (w, h) = (src_w * scale_x, src_h * scale_y);
2132        repose_core::Rect {
2133            x: rect.x + slack(w, dst_w),
2134            y: rect.y + slack(h, dst_h),
2135            w,
2136            h,
2137        }
2138    };
2139
2140    let uv_min = source.uv_min;
2141    let uv_max = source.uv_max;
2142    let repeats = if tiled {
2143        [dst_w / src_w, dst_h / src_h]
2144    } else {
2145        [1.0, 1.0]
2146    };
2147    let mut uv = [
2148        uv_min[0],
2149        uv_max[1],
2150        uv_min[0] + (uv_max[0] - uv_min[0]) * repeats[0],
2151        uv_max[1] - (uv_max[1] - uv_min[1]) * repeats[1],
2152    ];
2153
2154    let clipped = intersect(drawn, rect);
2155    if clipped.w <= 0.0 || clipped.h <= 0.0 {
2156        return None;
2157    }
2158    let du = uv[2] - uv[0];
2159    let dv = uv[3] - uv[1];
2160    uv = [
2161        uv[0] + du * (clipped.x - drawn.x) / drawn.w,
2162        uv[1] + dv * (clipped.y - drawn.y) / drawn.h,
2163        uv[0] + du * (clipped.x + clipped.w - drawn.x) / drawn.w,
2164        uv[1] + dv * (clipped.y + clipped.h - drawn.y) / drawn.h,
2165    ];
2166    Some((clipped, uv))
2167}
2168
2169struct AtlasA8 {
2170    tex: wgpu::Texture,
2171    view: wgpu::TextureView,
2172    sampler: wgpu::Sampler,
2173    size: u32,
2174    next_x: u32,
2175    next_y: u32,
2176    row_h: u32,
2177    map: HashMap<repose_text::CacheKey, GlyphInfo>,
2178    /// Summed `w * h` of every live entry. The pixel budget is what actually
2179    /// binds a shelf packer, so eviction trims on area, not entry count.
2180    used_area: u64,
2181}
2182
2183struct AtlasRGBA {
2184    tex: wgpu::Texture,
2185    view: wgpu::TextureView,
2186    sampler: wgpu::Sampler,
2187    size: u32,
2188    next_x: u32,
2189    next_y: u32,
2190    row_h: u32,
2191    map: HashMap<repose_text::CacheKey, GlyphInfo>,
2192    used_area: u64,
2193}
2194
2195/// Work an atlas could not do yet, applied at the next frame boundary. Both
2196/// growth and compaction re-pack the atlas, which would invalidate UVs already
2197/// batched into the frame in flight, so neither may run mid-frame.
2198#[derive(Default, Clone, Copy)]
2199struct AtlasPending {
2200    /// A cell did not fit and the atlas is still below its size cap.
2201    grow: bool,
2202    /// The atlas is at its cap, so the only remedy left is dropping cold
2203    /// entries.
2204    compact: bool,
2205}
2206
2207#[derive(Clone, Copy)]
2208struct GlyphInfo {
2209    u0: f32,
2210    v0: f32,
2211    u1: f32,
2212    v1: f32,
2213    w: f32,
2214    h: f32,
2215    /// `cache_touch_clock` at the last request, and the frame this glyph was
2216    /// last batched into. Entries in flight are never evicted: the instance
2217    /// buffer already holds their UVs, and a repack would move them.
2218    last_touch: u64,
2219    last_used_frame: u64,
2220}
2221
2222#[derive(Clone, Hash, PartialEq, Eq)]
2223struct TextShapeKey {
2224    text: Arc<str>,
2225    px_bits: u32,
2226    line_height_bits: u32,
2227    font_family: Option<&'static str>,
2228    font_weight: u16,
2229    font_style: u8,
2230    letter_spacing_bits: u32,
2231    variation: Option<Arc<str>>,
2232    font_generation: u64,
2233}
2234
2235struct TextShapeCacheEntry {
2236    glyphs: Arc<[repose_text::ShapedGlyph]>,
2237    last_used_frame: u64,
2238}
2239
2240#[repr(C)]
2241#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2242struct RectInstance {
2243    xywh: [f32; 4],
2244    radii: [f32; 4],
2245    brush_type: u32,
2246    grad_kind: u32,
2247    _pad: [f32; 2],
2248    color0: [f32; 4],
2249    color1: [f32; 4],
2250    grad_p0: [f32; 2],
2251    grad_p1: [f32; 2],
2252    tile_mode: u32,
2253    _pad2: [f32; 3],
2254    fwd_mat: [f32; 4],
2255}
2256
2257#[repr(C)]
2258#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2259struct BorderInstance {
2260    xywh: [f32; 4],
2261    radii: [f32; 4],
2262    stroke: f32,
2263    brush_type: u32,
2264    _pad: [f32; 2],
2265    grad_kind: u32,
2266    color0: [f32; 4],
2267    color1: [f32; 4],
2268    grad_p0: [f32; 2],
2269    grad_p1: [f32; 2],
2270    tile_mode: u32,
2271    _pad2: [f32; 3],
2272    fwd_mat: [f32; 4],
2273}
2274
2275#[repr(C)]
2276#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2277struct EllipseInstance {
2278    xywh: [f32; 4],
2279    brush_type: u32,
2280    grad_kind: u32,
2281    _pad: [f32; 2],
2282    color0: [f32; 4],
2283    color1: [f32; 4],
2284    grad_p0: [f32; 2],
2285    grad_p1: [f32; 2],
2286    tile_mode: u32,
2287    _pad2: [f32; 3],
2288    fwd_mat: [f32; 4],
2289}
2290
2291#[repr(C)]
2292#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2293struct EllipseBorderInstance {
2294    xywh: [f32; 4],
2295    stroke: f32,
2296    pad: f32,
2297    brush_type: u32,
2298    grad_kind: u32,
2299    color0: [f32; 4],
2300    color1: [f32; 4],
2301    grad_p0: [f32; 2],
2302    grad_p1: [f32; 2],
2303    tile_mode: u32,
2304    _pad2: [f32; 3],
2305    fwd_mat: [f32; 4],
2306}
2307
2308#[repr(C)]
2309#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2310struct ArcInstance {
2311    xywh: [f32; 4],
2312    start_angle: f32,
2313    sweep_angle: f32,
2314    stroke: f32,
2315    pad: f32,
2316    brush_type: u32,
2317    grad_kind: u32,
2318    _pad0: [f32; 2],
2319    color0: [f32; 4],
2320    color1: [f32; 4],
2321    grad_p0: [f32; 2],
2322    grad_p1: [f32; 2],
2323    tile_mode: u32,
2324    cap: f32, // 0=Butt, 1=Round, 2=Square
2325    _pad1: [f32; 2],
2326    fwd_mat: [f32; 4],
2327}
2328
2329#[repr(C)]
2330#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2331struct GlyphInstance {
2332    xywh: [f32; 4],
2333    uv: [f32; 4],
2334    color: [f32; 4],
2335    fwd_mat: [f32; 4],
2336}
2337
2338#[repr(C)]
2339#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2340struct BlurInstance {
2341    xywh: [f32; 4],
2342    uv: [f32; 4],
2343    color: [f32; 4],
2344    blur_uv: [f32; 2],
2345    fwd_mat: [f32; 4],
2346    edge_mode: u32,
2347    _pad: [f32; 3],
2348}
2349
2350/// Projective layer-composite instance: the four layer-rect corners projected
2351/// to NDC (`c0..c3`, counter-clockwise from top-left) with their homogeneous
2352/// `w`, the layer-texture uv bounds, and a group alpha. Matches
2353/// `projective_layer.wgsl` (offsets: c0@0 c1@8 c2@16 c3@24 uv@32 w@48
2354/// alpha@64; stride 80).
2355#[repr(C)]
2356#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2357struct ProjectiveInstance {
2358    c0: [f32; 2],
2359    c1: [f32; 2],
2360    c2: [f32; 2],
2361    c3: [f32; 2],
2362    uv: [f32; 4],
2363    w: [f32; 4],
2364    alpha: f32,
2365    _pad: [f32; 3],
2366}
2367
2368/// CPU-computed Y′CbCr -> R′G′B′ transform uploaded as a uniform buffer.
2369/// Layout matches the WGSL `YuvTransform` struct (5 × vec4<f32>).
2370#[repr(C)]
2371#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2372struct YuvTransformRaw {
2373    row0: [f32; 4],
2374    row1: [f32; 4],
2375    row2: [f32; 4],
2376    b: [f32; 4],
2377    transfer: [f32; 4],
2378}
2379
2380fn make_yuv_transform_raw(color_info: ColorInfo, pixel_format: PixelFormat) -> YuvTransformRaw {
2381    let yuv = color_info.to_yuv_transform();
2382    let sample_scale = match pixel_format {
2383        PixelFormat::P010 => 65535.0 / (64.0 * 1020.0),
2384        _ => 1.0,
2385    };
2386    let transfer_mode = match color_info.transfer {
2387        repose_core::color::Transfer::Srgb => 0.0,
2388        repose_core::color::Transfer::Bt709 => 1.0,
2389        repose_core::color::Transfer::Linear => 2.0,
2390        repose_core::color::Transfer::Pq => 3.0,
2391        repose_core::color::Transfer::Hlg => 4.0,
2392    };
2393    YuvTransformRaw {
2394        row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
2395        row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
2396        row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
2397        b: [yuv.b[0], yuv.b[1], yuv.b[2], sample_scale],
2398        transfer: [transfer_mode, 0.0, 0.0, 0.0],
2399    }
2400}
2401
2402#[repr(C)]
2403#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2404struct Nv12Instance {
2405    xywh: [f32; 4],
2406    uv: [f32; 4],
2407    color: [f32; 4],
2408    uv_x_offset: f32,
2409    uv_y_offset: f32,
2410    fwd_mat: [f32; 4],
2411}
2412
2413#[repr(C)]
2414#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2415struct ClipInstance {
2416    xywh: [f32; 4],
2417    radii: [f32; 4],
2418    fwd_mat: [f32; 4],
2419}
2420
2421/// Backdrop-blend composite instance: a `GlyphInstance`-shaped quad plus
2422/// the `BlendMode` discriminant consumed by `blend_layer.wgsl`.
2423#[repr(C)]
2424#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2425struct BlendInstance {
2426    xywh: [f32; 4],
2427    uv: [f32; 4],
2428    color: [f32; 4],
2429    fwd_mat: [f32; 4],
2430    mode: u32,
2431    _pad: [f32; 3],
2432}
2433
2434#[repr(C)]
2435#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2436struct MeshVertex {
2437    pos: [f32; 2],
2438    color: [f32; 4],
2439    uv: [f32; 2],
2440}
2441
2442#[derive(Clone)]
2443struct MeshGpu {
2444    vertices: wgpu::Buffer,
2445    indices: wgpu::Buffer,
2446    vertex_count: u32,
2447    index_count: u32,
2448    bytes: u64,
2449}
2450
2451#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2452struct MeshCacheKey {
2453    source: usize,
2454    index: usize,
2455    kind: u8,
2456    vertex_len: usize,
2457    index_len: usize,
2458}
2459
2460#[derive(Clone)]
2461enum MeshOwner {
2462    Object(Arc<repose_core::VectorMeshData>),
2463    Overlay(Arc<[repose_core::VectorMeshData]>),
2464}
2465
2466struct MeshCacheEntry {
2467    owner: MeshOwner,
2468    gpu: MeshGpu,
2469    last_used_frame: u64,
2470    last_touch_frame: u64,
2471}
2472
2473#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2474struct SlugDrawKey {
2475    cache_key: repose_text::CacheKey,
2476    fill: bool,
2477    stroke_hash: u64,
2478    transform: [u32; 11],
2479    origin: [u32; 2],
2480    px_bits: u32,
2481    target: [u32; 2],
2482    color: [u32; 4],
2483}
2484
2485struct SlugDrawCacheEntry {
2486    vertices: Arc<[slug::TessVertex]>,
2487    bytes: u64,
2488    last_used_frame: u64,
2489    last_touch_frame: u64,
2490}
2491
2492/// Vector outline for one glyph, cached so repeated text at the same size does
2493/// not re-run outline extraction. `commands` is `None` for glyphs the extractor
2494/// rejects; that negative result is cached too, so a bad glyph is not retried.
2495struct GlyphOutlineEntry {
2496    commands: Option<Arc<[repose_text::Command]>>,
2497    last_touch: u64,
2498}
2499
2500/// Drop least-recently-used atlas entries until the live set fits `target_area`
2501/// pixels, skipping glyphs batched into the frame in flight. Never empties the
2502/// atlas, so a repack always has something left to pack around. Returns false
2503/// when nothing is evictable.
2504fn evict_atlas_lru(
2505    map: &mut HashMap<repose_text::CacheKey, GlyphInfo>,
2506    used_area: &mut u64,
2507    frame_index: u64,
2508    target_area: u64,
2509) -> bool {
2510    let mut victims: Vec<(u64, repose_text::CacheKey)> = map
2511        .iter()
2512        .filter(|(_, info)| info.last_used_frame != frame_index)
2513        .map(|(key, info)| (info.last_touch, *key))
2514        .collect();
2515    if victims.is_empty() {
2516        return false;
2517    }
2518    // Ties are equally cold, so their relative order does not matter.
2519    victims.sort_unstable_by_key(|victim| victim.0);
2520    let mut evicted = false;
2521    for (_, key) in victims {
2522        if *used_area <= target_area || map.len() <= 1 {
2523            break;
2524        }
2525        if let Some(info) = map.remove(&key) {
2526            *used_area = used_area.saturating_sub(glyph_cell_area(&info));
2527            evicted = true;
2528        }
2529    }
2530    evicted
2531}
2532
2533/// Pixels a packed cell occupies. `w`/`h` are exact small integers by the time
2534/// they reach `GlyphInfo`, so the round trip through `f32` is lossless.
2535fn glyph_cell_area(info: &GlyphInfo) -> u64 {
2536    (info.w as u64).saturating_mul(info.h as u64)
2537}
2538
2539/// Remember that a glyph had no room, so it is skipped instead of re-rasterized
2540/// every frame until the next repack. Capped like the failure sets; dropping
2541/// the oldest entries only costs an earlier retry.
2542fn defer_atlas_glyph(deferred: &mut HashSet<repose_text::CacheKey>, key: repose_text::CacheKey) {
2543    if deferred.len() >= MAX_GLYPH_ATLAS_ENTRIES {
2544        deferred.clear();
2545    }
2546    deferred.insert(key);
2547}
2548
2549/// What one slug draw cache entry really costs: the vertex array plus the key,
2550/// the entry record and the map slot holding it. Charging only the array makes
2551/// the byte budget silently optimistic.
2552fn slug_draw_cache_entry_bytes(vertices: usize) -> u64 {
2553    let per_entry = (std::mem::size_of::<SlugDrawKey>()
2554        + std::mem::size_of::<SlugDrawCacheEntry>()
2555        + std::mem::size_of::<(SlugDrawKey, SlugDrawCacheEntry)>()) as u64;
2556    u64::try_from(vertices)
2557        .ok()
2558        .and_then(|count| count.checked_mul(std::mem::size_of::<slug::TessVertex>() as u64))
2559        .and_then(|payload| payload.checked_add(per_entry))
2560        .unwrap_or(u64::MAX)
2561}
2562
2563#[repr(C)]
2564#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2565struct MeshUniform {
2566    m0: [f32; 4],
2567    m1: [f32; 4],
2568    paint: [u32; 4],
2569    color0: [f32; 4],
2570    color1: [f32; 4],
2571    grad_start: [f32; 2],
2572    _p3: [f32; 2],
2573    grad_end: [f32; 2],
2574    _p4: [f32; 2],
2575}
2576
2577const MESH_UNIFORM_CAP: u64 = 4 * 1024 * 1024;
2578const MAX_RETAINED_IMAGE_BYTES: u64 = 256 * 1024 * 1024;
2579const MAX_RETAINED_IMAGES: usize = 512;
2580const MAX_BLEND_SNAPSHOT_BYTES: u64 = 64 * 1024 * 1024;
2581const MAX_BLEND_SNAPSHOT_POOL_ENTRIES: usize = 64;
2582const MAX_GLYPH_ATLAS_ENTRIES: usize = 32768;
2583/// Hard ceiling on atlas edge length. Past this the atlas is compacted instead
2584/// of grown, dropping least-recently-used glyphs.
2585const MAX_GLYPH_ATLAS_SIZE: u32 = 4096;
2586/// Fraction of the live set a compaction keeps. Trimming to a fraction of the
2587/// current area (rather than an absolute budget) guarantees a repack always
2588/// reclaims shelf space, even when the atlas is only a few large glyphs deep.
2589const ATLAS_COMPACT_KEEP_NUM: u64 = 3;
2590const ATLAS_COMPACT_KEEP_DEN: u64 = 4;
2591/// Minimum frames between compactions. A working set that genuinely exceeds
2592/// atlas capacity then costs a few missing glyphs per interval instead of
2593/// re-uploading the whole atlas every frame.
2594const ATLAS_COMPACT_MIN_FRAME_INTERVAL: u64 = 30;
2595const MAX_GPU_RESOURCE_BYTES: u64 = 1024 * 1024 * 1024;
2596const MAX_UPLOAD_RING_BYTES: u64 = 64 * 1024 * 1024;
2597const MAX_MESH_CACHE_BYTES: u64 = 256 * 1024 * 1024;
2598const MAX_MESH_CACHE_ENTRIES: usize = 512;
2599const MAX_SLUG_DRAW_CACHE_ENTRIES: usize = 2048;
2600const MAX_SLUG_DRAW_CACHE_BYTES: u64 = 32 * 1024 * 1024;
2601const MAX_LEGACY_SHAPED_TEXT_CACHE_ENTRIES: usize = 256;
2602const MAX_GLYPH_OUTLINE_CACHE_ENTRIES: usize = 4096;
2603const MAX_CALLBACK_SCOPES: usize = 256;
2604const NV12_FOURCC: u32 = 0x3231_564e;
2605const P010_FOURCC: u32 = 0x3031_3050;
2606
2607fn canonical_fourcc(format: PixelFormat) -> u32 {
2608    match format {
2609        PixelFormat::P010 => P010_FOURCC,
2610        _ => NV12_FOURCC,
2611    }
2612}
2613
2614impl MeshUniform {
2615    fn identity() -> Self {
2616        Self {
2617            m0: [1.0, 0.0, 0.0, 0.0],
2618            m1: [0.0, 1.0, 0.0, 0.0],
2619            paint: [0; 4],
2620            color0: [0.0; 4],
2621            color1: [0.0; 4],
2622            grad_start: [0.0; 2],
2623            _p3: [0.0; 2],
2624            grad_end: [0.0; 2],
2625            _p4: [0.0; 2],
2626        }
2627    }
2628}
2629
2630fn mesh_uniform_from_paint(affine: [f32; 6], paint: &repose_core::PaintDesc) -> MeshUniform {
2631    let (paint_type, paint_kind, color0, color1, grad_start, grad_end) = match paint {
2632        repose_core::PaintDesc::Solid => (0u32, 0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
2633        repose_core::PaintDesc::Linear {
2634            start,
2635            end,
2636            start_color,
2637            end_color,
2638        } => (
2639            1u32,
2640            0u32,
2641            start_color.to_linear(),
2642            end_color.to_linear(),
2643            [start.x, start.y],
2644            [end.x, end.y],
2645        ),
2646        repose_core::PaintDesc::Radial {
2647            center,
2648            radius,
2649            start_color,
2650            end_color,
2651        } => (
2652            1u32,
2653            1u32,
2654            start_color.to_linear(),
2655            end_color.to_linear(),
2656            [center.x, center.y],
2657            [radius.max(0.0), 0.0],
2658        ),
2659        repose_core::PaintDesc::Sweep {
2660            center,
2661            start_color,
2662            end_color,
2663        } => (
2664            1u32,
2665            2u32,
2666            start_color.to_linear(),
2667            end_color.to_linear(),
2668            [center.x, center.y],
2669            [0.0, 0.0],
2670        ),
2671        _ => (0u32, 0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
2672    };
2673    MeshUniform {
2674        m0: [affine[0], affine[1], affine[2], 0.0],
2675        m1: [affine[3], affine[4], affine[5], 0.0],
2676        paint: [paint_type, paint_kind, 0, 0],
2677        color0,
2678        color1,
2679        grad_start,
2680        _p3: [0.0; 2],
2681        grad_end,
2682        _p4: [0.0; 2],
2683    }
2684}
2685
2686fn combine_mesh_affine(current: &Transform, mesh: [f32; 6]) -> [f32; 6] {
2687    let cm = current.linear();
2688    let (cm00, cm01, cm10, cm11) = (cm[0], cm[1], cm[2], cm[3]);
2689    let mm00 = mesh[0];
2690    let mm01 = mesh[1];
2691    let mm10 = mesh[2];
2692    let mm11 = mesh[3];
2693    let mtx = mesh[4];
2694    let mty = mesh[5];
2695    let r00 = cm00 * mm00 + cm01 * mm10;
2696    let r01 = cm00 * mm01 + cm01 * mm11;
2697    let r10 = cm10 * mm00 + cm11 * mm10;
2698    let r11 = cm10 * mm01 + cm11 * mm11;
2699    let tx = cm00 * mtx + cm01 * mty + current.translate_x;
2700    let ty = cm10 * mtx + cm11 * mty + current.translate_y;
2701    // Canonical slot order consumed by `MeshUniform`/shader and `mesh_aabb`:
2702    // [A, B, tx, C, D, ty] where world = [[A,B],[C,D]] * local + (tx, ty).
2703    [r00, r01, tx, r10, r11, ty]
2704}
2705
2706fn mesh_aabb(mesh: &repose_core::VectorMeshData, affine: [f32; 6]) -> repose_core::Rect {
2707    let mut min_x = f32::INFINITY;
2708    let mut min_y = f32::INFINITY;
2709    let mut max_x = f32::NEG_INFINITY;
2710    let mut max_y = f32::NEG_INFINITY;
2711    for v in mesh.vertices.iter() {
2712        let x = affine[0] * v.pos[0] + affine[1] * v.pos[1] + affine[2];
2713        let y = affine[3] * v.pos[0] + affine[4] * v.pos[1] + affine[5];
2714        min_x = min_x.min(x);
2715        min_y = min_y.min(y);
2716        max_x = max_x.max(x);
2717        max_y = max_y.max(y);
2718    }
2719    let w = (max_x - min_x).max(0.0);
2720    let h = (max_y - min_y).max(0.0);
2721    if !min_x.is_finite() || !min_y.is_finite() {
2722        return repose_core::Rect {
2723            x: 0.0,
2724            y: 0.0,
2725            w: 0.0,
2726            h: 0.0,
2727        };
2728    }
2729    repose_core::Rect {
2730        x: min_x,
2731        y: min_y,
2732        w,
2733        h,
2734    }
2735}
2736
2737fn quantize_glyph_px(px: f32) -> f32 {
2738    if !px.is_finite() || px <= 0.0 {
2739        return 1.0;
2740    }
2741    ((px * 4.0).round() / 4.0).max(0.25)
2742}
2743
2744fn swash_to_a8_coverage(content: repose_text::SwashContent, data: &[u8]) -> Option<Vec<u8>> {
2745    match content {
2746        repose_text::SwashContent::Mask => Some(data.to_vec()),
2747        repose_text::SwashContent::SubpixelMask => {
2748            let mut out = Vec::with_capacity(data.len() / 4);
2749            for px in data.as_chunks::<4>().0 {
2750                let r = px[0];
2751                let g = px[1];
2752                let b = px[2];
2753                out.push(r.max(g).max(b));
2754            }
2755            Some(out)
2756        }
2757        repose_text::SwashContent::Color => None,
2758    }
2759}
2760
2761impl WgpuSceneRenderer {
2762    pub fn from_device(
2763        device: wgpu::Device,
2764        queue: wgpu::Queue,
2765        output_format: wgpu::TextureFormat,
2766        msaa_samples: u32,
2767    ) -> Self {
2768        Self::from_device_with_working_space_msaa(
2769            device,
2770            queue,
2771            output_format,
2772            msaa_samples,
2773            msaa_samples,
2774        )
2775    }
2776
2777    pub fn from_device_with_working_space_msaa(
2778        device: wgpu::Device,
2779        queue: wgpu::Queue,
2780        output_format: wgpu::TextureFormat,
2781        msaa_samples: u32,
2782        working_space_msaa_samples: u32,
2783    ) -> Self {
2784        let msaa_samples = msaa_samples.max(1);
2785        let working_space_msaa_samples = working_space_msaa_samples.max(1);
2786        let mesh_uniform_size = std::mem::size_of::<MeshUniform>() as u64;
2787        let base_mesh_alignment =
2788            u64::from(device.limits().min_uniform_buffer_offset_alignment).max(4);
2789        let mesh_uniform_alignment =
2790            align_up(mesh_uniform_size, base_mesh_alignment).unwrap_or(base_mesh_alignment);
2791        let mesh_uniform_cap = align_up(
2792            MESH_UNIFORM_CAP
2793                .min(device.limits().max_buffer_size)
2794                .max(mesh_uniform_alignment),
2795            mesh_uniform_alignment,
2796        )
2797        .unwrap_or(mesh_uniform_alignment);
2798        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2799            label: Some("globals layout"),
2800            entries: &[wgpu::BindGroupLayoutEntry {
2801                binding: 0,
2802                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
2803                ty: wgpu::BindingType::Buffer {
2804                    ty: wgpu::BufferBindingType::Uniform,
2805                    has_dynamic_offset: false,
2806                    min_binding_size: None,
2807                },
2808                count: None,
2809            }],
2810        });
2811
2812        let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
2813            label: Some("globals buf"),
2814            size: std::mem::size_of::<Globals>() as u64,
2815            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2816            mapped_at_creation: false,
2817        });
2818
2819        let globals_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
2820            label: Some("globals bind"),
2821            layout: &globals_layout,
2822            entries: &[wgpu::BindGroupEntry {
2823                binding: 0,
2824                resource: globals_buf.as_entire_binding(),
2825            }],
2826        });
2827        let globals_staging_cap =
2828            (64 * std::mem::size_of::<Globals>() as u64).max(std::mem::size_of::<Globals>() as u64);
2829        let globals_staging = device.create_buffer(&wgpu::BufferDescriptor {
2830            label: Some("globals staging"),
2831            size: globals_staging_cap,
2832            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
2833            mapped_at_creation: false,
2834        });
2835
2836        let ds_format = wgpu::TextureFormat::Depth24PlusStencil8;
2837
2838        let stencil_for_content = wgpu::DepthStencilState {
2839            format: ds_format,
2840            depth_write_enabled: Some(false),
2841            depth_compare: Some(wgpu::CompareFunction::Always),
2842            stencil: wgpu::StencilState {
2843                front: wgpu::StencilFaceState {
2844                    // Equal (not LessEqual): inverse (`Difference`) vector
2845                    // masks work by keeping the depth while incrementing the
2846                    // masked pixels, so content must test exact equality.
2847                    // Outcomes match LessEqual everywhere except pre-existing
2848                    // stencil leaks, which now fail visibly instead of
2849                    // drawing through (unbalanced clips already warn).
2850                    compare: wgpu::CompareFunction::Equal,
2851                    fail_op: wgpu::StencilOperation::Keep,
2852                    depth_fail_op: wgpu::StencilOperation::Keep,
2853                    pass_op: wgpu::StencilOperation::Keep,
2854                },
2855                back: wgpu::StencilFaceState {
2856                    compare: wgpu::CompareFunction::Equal,
2857                    fail_op: wgpu::StencilOperation::Keep,
2858                    depth_fail_op: wgpu::StencilOperation::Keep,
2859                    pass_op: wgpu::StencilOperation::Keep,
2860                },
2861                read_mask: 0xFF,
2862                write_mask: 0x00,
2863            },
2864            bias: wgpu::DepthBiasState::default(),
2865        };
2866
2867        let stencil_for_clip_inc = wgpu::DepthStencilState {
2868            format: ds_format,
2869            depth_write_enabled: Some(false),
2870            depth_compare: Some(wgpu::CompareFunction::Always),
2871            stencil: wgpu::StencilState {
2872                front: wgpu::StencilFaceState {
2873                    compare: wgpu::CompareFunction::Always,
2874                    fail_op: wgpu::StencilOperation::Keep,
2875                    depth_fail_op: wgpu::StencilOperation::Keep,
2876                    pass_op: wgpu::StencilOperation::IncrementClamp,
2877                },
2878                back: wgpu::StencilFaceState {
2879                    compare: wgpu::CompareFunction::Always,
2880                    fail_op: wgpu::StencilOperation::Keep,
2881                    depth_fail_op: wgpu::StencilOperation::Keep,
2882                    pass_op: wgpu::StencilOperation::IncrementClamp,
2883                },
2884                read_mask: 0xFF,
2885                write_mask: 0xFF,
2886            },
2887            bias: wgpu::DepthBiasState::default(),
2888        };
2889
2890        let stencil_for_clip_dec = wgpu::DepthStencilState {
2891            format: ds_format,
2892            depth_write_enabled: Some(false),
2893            depth_compare: Some(wgpu::CompareFunction::Always),
2894            stencil: wgpu::StencilState {
2895                front: wgpu::StencilFaceState {
2896                    compare: wgpu::CompareFunction::Always,
2897                    fail_op: wgpu::StencilOperation::Keep,
2898                    depth_fail_op: wgpu::StencilOperation::Keep,
2899                    pass_op: wgpu::StencilOperation::DecrementClamp,
2900                },
2901                back: wgpu::StencilFaceState {
2902                    compare: wgpu::CompareFunction::Always,
2903                    fail_op: wgpu::StencilOperation::Keep,
2904                    depth_fail_op: wgpu::StencilOperation::Keep,
2905                    pass_op: wgpu::StencilOperation::DecrementClamp,
2906                },
2907                read_mask: 0xFF,
2908                write_mask: 0xFF,
2909            },
2910            bias: wgpu::DepthBiasState::default(),
2911        };
2912
2913        let _multisample_state = wgpu::MultisampleState {
2914            count: msaa_samples,
2915            mask: !0,
2916            alpha_to_coverage_enabled: false,
2917        };
2918
2919        // PIPELINES
2920
2921        // Single shared sampler for images/text
2922        let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2923            label: Some("image/text sampler"),
2924            address_mode_u: wgpu::AddressMode::ClampToEdge,
2925            address_mode_v: wgpu::AddressMode::ClampToEdge,
2926            mag_filter: wgpu::FilterMode::Linear,
2927            min_filter: wgpu::FilterMode::Linear,
2928            mipmap_filter: wgpu::MipmapFilterMode::Linear,
2929            ..Default::default()
2930        });
2931        let image_sampler_nearest = device.create_sampler(&wgpu::SamplerDescriptor {
2932            label: Some("image nearest sampler"),
2933            address_mode_u: wgpu::AddressMode::ClampToEdge,
2934            address_mode_v: wgpu::AddressMode::ClampToEdge,
2935            mag_filter: wgpu::FilterMode::Nearest,
2936            min_filter: wgpu::FilterMode::Nearest,
2937            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
2938            ..Default::default()
2939        });
2940
2941        // `ImageFit::Tile` emits UVs past 1.0 and relies on wrapping, so it
2942        // needs its own samplers rather than the clamped pair above.
2943        let image_sampler_repeat = device.create_sampler(&wgpu::SamplerDescriptor {
2944            label: Some("image repeat sampler"),
2945            address_mode_u: wgpu::AddressMode::Repeat,
2946            address_mode_v: wgpu::AddressMode::Repeat,
2947            mag_filter: wgpu::FilterMode::Linear,
2948            min_filter: wgpu::FilterMode::Linear,
2949            mipmap_filter: wgpu::MipmapFilterMode::Linear,
2950            ..Default::default()
2951        });
2952        let image_sampler_repeat_nearest = device.create_sampler(&wgpu::SamplerDescriptor {
2953            label: Some("image repeat nearest sampler"),
2954            address_mode_u: wgpu::AddressMode::Repeat,
2955            address_mode_v: wgpu::AddressMode::Repeat,
2956            mag_filter: wgpu::FilterMode::Nearest,
2957            min_filter: wgpu::FilterMode::Nearest,
2958            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
2959            ..Default::default()
2960        });
2961
2962        // linear filtering only blurs them; nearest keeps the blit crisp.
2963        let layer_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2964            label: Some("layer nearest sampler"),
2965            address_mode_u: wgpu::AddressMode::ClampToEdge,
2966            address_mode_v: wgpu::AddressMode::ClampToEdge,
2967            mag_filter: wgpu::FilterMode::Nearest,
2968            min_filter: wgpu::FilterMode::Nearest,
2969            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
2970            ..Default::default()
2971        });
2972
2973        // Linear taps for Gaussian blur/shadow passes; nearest is kept for
2974        // the sharp 1:1 layer composite.
2975        let layer_sampler_linear = device.create_sampler(&wgpu::SamplerDescriptor {
2976            label: Some("layer linear sampler"),
2977            address_mode_u: wgpu::AddressMode::ClampToEdge,
2978            address_mode_v: wgpu::AddressMode::ClampToEdge,
2979            mag_filter: wgpu::FilterMode::Linear,
2980            min_filter: wgpu::FilterMode::Linear,
2981            mipmap_filter: wgpu::MipmapFilterMode::Linear,
2982            ..Default::default()
2983        });
2984
2985        // Layout for Text / RGBA Images (Texture + Sampler)
2986        let text_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2987            label: Some("text/rgba bind layout"),
2988            entries: &[
2989                wgpu::BindGroupLayoutEntry {
2990                    binding: 0,
2991                    visibility: wgpu::ShaderStages::FRAGMENT,
2992                    ty: wgpu::BindingType::Texture {
2993                        multisampled: false,
2994                        view_dimension: wgpu::TextureViewDimension::D2,
2995                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
2996                    },
2997                    count: None,
2998                },
2999                wgpu::BindGroupLayoutEntry {
3000                    binding: 1,
3001                    visibility: wgpu::ShaderStages::FRAGMENT,
3002                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
3003                    count: None,
3004                },
3005            ],
3006        });
3007        // We reuse this for RGBA images for simplicity, or create a distinct one
3008        let image_bind_layout_rgba = text_bind_layout.clone();
3009
3010        // Layout for NV12 Images (TextureY + TextureUV + Sampler + YuvTransform uniform)
3011        let image_bind_layout_nv12 =
3012            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3013                label: Some("image bind layout nv12"),
3014                entries: &[
3015                    // Y plane
3016                    wgpu::BindGroupLayoutEntry {
3017                        binding: 0,
3018                        visibility: wgpu::ShaderStages::FRAGMENT,
3019                        ty: wgpu::BindingType::Texture {
3020                            multisampled: false,
3021                            view_dimension: wgpu::TextureViewDimension::D2,
3022                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
3023                        },
3024                        count: None,
3025                    },
3026                    // UV plane
3027                    wgpu::BindGroupLayoutEntry {
3028                        binding: 1,
3029                        visibility: wgpu::ShaderStages::FRAGMENT,
3030                        ty: wgpu::BindingType::Texture {
3031                            multisampled: false,
3032                            view_dimension: wgpu::TextureViewDimension::D2,
3033                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
3034                        },
3035                        count: None,
3036                    },
3037                    // Sampler
3038                    wgpu::BindGroupLayoutEntry {
3039                        binding: 2,
3040                        visibility: wgpu::ShaderStages::FRAGMENT,
3041                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
3042                        count: None,
3043                    },
3044                    // YUV transform uniform buffer
3045                    wgpu::BindGroupLayoutEntry {
3046                        binding: 3,
3047                        visibility: wgpu::ShaderStages::FRAGMENT,
3048                        ty: wgpu::BindingType::Buffer {
3049                            ty: wgpu::BufferBindingType::Uniform,
3050                            has_dynamic_offset: false,
3051                            min_binding_size: None,
3052                        },
3053                        count: None,
3054                    },
3055                ],
3056            });
3057
3058        // Clipping layout
3059        let clip_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3060            label: Some("clip pipeline layout"),
3061            bind_group_layouts: &[Some(&globals_layout)],
3062            immediate_size: 0,
3063        });
3064        let clip_vertex_layout = wgpu::VertexBufferLayout {
3065            array_stride: std::mem::size_of::<ClipInstance>() as u64,
3066            step_mode: wgpu::VertexStepMode::Instance,
3067            attributes: &[
3068                wgpu::VertexAttribute {
3069                    shader_location: 0,
3070                    offset: 0,
3071                    format: wgpu::VertexFormat::Float32x4,
3072                },
3073                wgpu::VertexAttribute {
3074                    shader_location: 1,
3075                    offset: 16,
3076                    format: wgpu::VertexFormat::Float32x4,
3077                },
3078                wgpu::VertexAttribute {
3079                    shader_location: 2,
3080                    offset: 32,
3081                    format: wgpu::VertexFormat::Float32x4,
3082                },
3083            ],
3084        };
3085        // Bind layout for per-draw vector mesh uniforms (dynamic offset).
3086        let mesh_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3087            label: Some("mesh uniform layout"),
3088            entries: &[wgpu::BindGroupLayoutEntry {
3089                binding: 0,
3090                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
3091                ty: wgpu::BindingType::Buffer {
3092                    ty: wgpu::BufferBindingType::Uniform,
3093                    has_dynamic_offset: true,
3094                    min_binding_size: NonZeroU64::new(mesh_uniform_size),
3095                },
3096                count: None,
3097            }],
3098        });
3099        let mesh_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
3100            label: Some("mesh uniform buffer"),
3101            size: mesh_uniform_cap,
3102            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3103            mapped_at_creation: false,
3104        });
3105        let mesh_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
3106            label: Some("mesh uniform bind"),
3107            layout: &mesh_bind_layout,
3108            entries: &[wgpu::BindGroupEntry {
3109                binding: 0,
3110                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3111                    buffer: &mesh_uniform_buf,
3112                    offset: 0,
3113                    size: NonZeroU64::new(mesh_uniform_size),
3114                }),
3115            }],
3116        });
3117
3118        // Two sets of pipelines: one for the MSAA surface pass, one for layer
3119        // render-to-texture passes (sample_count = 1).
3120        let surface_pipes = Pipelines::create(
3121            &device,
3122            output_format,
3123            msaa_samples,
3124            &globals_layout,
3125            &text_bind_layout,
3126            &image_bind_layout_nv12,
3127            &clip_pipeline_layout,
3128            &stencil_for_content,
3129            &stencil_for_clip_inc,
3130            &stencil_for_clip_dec,
3131            &clip_vertex_layout,
3132            &mesh_bind_layout,
3133        );
3134        let working_space_pipes = Pipelines::create(
3135            &device,
3136            wgpu::TextureFormat::Rgba16Float,
3137            working_space_msaa_samples,
3138            &globals_layout,
3139            &text_bind_layout,
3140            &image_bind_layout_nv12,
3141            &clip_pipeline_layout,
3142            &stencil_for_content,
3143            &stencil_for_clip_inc,
3144            &stencil_for_clip_dec,
3145            &clip_vertex_layout,
3146            &mesh_bind_layout,
3147        );
3148        let layer_pipes = if msaa_samples == 1 {
3149            surface_pipes.clone()
3150        } else {
3151            Pipelines::create(
3152                &device,
3153                output_format,
3154                1,
3155                &globals_layout,
3156                &text_bind_layout,
3157                &image_bind_layout_nv12,
3158                &clip_pipeline_layout,
3159                &stencil_for_content,
3160                &stencil_for_clip_inc,
3161                &stencil_for_clip_dec,
3162                &clip_vertex_layout,
3163                &mesh_bind_layout,
3164            )
3165        };
3166        let working_space_layer_pipes = if working_space_msaa_samples == 1 {
3167            working_space_pipes.clone()
3168        } else {
3169            Pipelines::create(
3170                &device,
3171                wgpu::TextureFormat::Rgba16Float,
3172                1,
3173                &globals_layout,
3174                &text_bind_layout,
3175                &image_bind_layout_nv12,
3176                &clip_pipeline_layout,
3177                &stencil_for_content,
3178                &stencil_for_clip_inc,
3179                &stencil_for_clip_dec,
3180                &clip_vertex_layout,
3181                &mesh_bind_layout,
3182            )
3183        };
3184
3185        // Vector glyph rendering always available with tessellation+MSAA approach.
3186        let slug_enabled = true;
3187
3188        // Blur composite ring (for graphics-layer drop shadows)
3189        let blur_ring = UploadRing::new(
3190            &device,
3191            "blur ring",
3192            1024 * 1024,
3193            wgpu::BufferUsages::VERTEX
3194                | wgpu::BufferUsages::COPY_DST
3195                | wgpu::BufferUsages::COPY_SRC,
3196        );
3197
3198        // Atlases
3199        let atlas_mask = init_atlas_mask(&device);
3200        let atlas_color = init_atlas_color(&device);
3201        let atlas_mask_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
3202            label: Some("atlas mask bind"),
3203            layout: &text_bind_layout,
3204            entries: &[
3205                wgpu::BindGroupEntry {
3206                    binding: 0,
3207                    resource: wgpu::BindingResource::TextureView(&atlas_mask.view),
3208                },
3209                wgpu::BindGroupEntry {
3210                    binding: 1,
3211                    resource: wgpu::BindingResource::Sampler(&atlas_mask.sampler),
3212                },
3213            ],
3214        });
3215        let atlas_color_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
3216            label: Some("atlas color bind"),
3217            layout: &text_bind_layout,
3218            entries: &[
3219                wgpu::BindGroupEntry {
3220                    binding: 0,
3221                    resource: wgpu::BindingResource::TextureView(&atlas_color.view),
3222                },
3223                wgpu::BindGroupEntry {
3224                    binding: 1,
3225                    resource: wgpu::BindingResource::Sampler(&atlas_color.sampler),
3226                },
3227            ],
3228        });
3229
3230        // Upload rings
3231        let ring_rect = UploadRing::new(
3232            &device,
3233            "ring rect",
3234            1 << 20,
3235            wgpu::BufferUsages::VERTEX
3236                | wgpu::BufferUsages::COPY_DST
3237                | wgpu::BufferUsages::COPY_SRC,
3238        );
3239        let ring_border = UploadRing::new(
3240            &device,
3241            "ring border",
3242            1 << 20,
3243            wgpu::BufferUsages::VERTEX
3244                | wgpu::BufferUsages::COPY_DST
3245                | wgpu::BufferUsages::COPY_SRC,
3246        );
3247        let ring_ellipse = UploadRing::new(
3248            &device,
3249            "ring ellipse",
3250            1 << 20,
3251            wgpu::BufferUsages::VERTEX
3252                | wgpu::BufferUsages::COPY_DST
3253                | wgpu::BufferUsages::COPY_SRC,
3254        );
3255        let ring_ellipse_border = UploadRing::new(
3256            &device,
3257            "ring ellipse border",
3258            1 << 20,
3259            wgpu::BufferUsages::VERTEX
3260                | wgpu::BufferUsages::COPY_DST
3261                | wgpu::BufferUsages::COPY_SRC,
3262        );
3263        let ring_arc = UploadRing::new(
3264            &device,
3265            "ring arc",
3266            1 << 20,
3267            wgpu::BufferUsages::VERTEX
3268                | wgpu::BufferUsages::COPY_DST
3269                | wgpu::BufferUsages::COPY_SRC,
3270        );
3271        let ring_glyph_mask = UploadRing::new(
3272            &device,
3273            "ring glyph mask",
3274            1 << 20,
3275            wgpu::BufferUsages::VERTEX
3276                | wgpu::BufferUsages::COPY_DST
3277                | wgpu::BufferUsages::COPY_SRC,
3278        );
3279        let ring_glyph_color = UploadRing::new(
3280            &device,
3281            "ring glyph color",
3282            1 << 20,
3283            wgpu::BufferUsages::VERTEX
3284                | wgpu::BufferUsages::COPY_DST
3285                | wgpu::BufferUsages::COPY_SRC,
3286        );
3287        let ring_slug = UploadRing::new(
3288            &device,
3289            "ring slug",
3290            1 << 22,
3291            wgpu::BufferUsages::VERTEX
3292                | wgpu::BufferUsages::COPY_DST
3293                | wgpu::BufferUsages::COPY_SRC,
3294        );
3295        let ring_clip = UploadRing::new(
3296            &device,
3297            "ring clip",
3298            1 << 16,
3299            wgpu::BufferUsages::VERTEX
3300                | wgpu::BufferUsages::COPY_DST
3301                | wgpu::BufferUsages::COPY_SRC,
3302        );
3303        let blend_ring = UploadRing::new(
3304            &device,
3305            "ring blend",
3306            1 << 16,
3307            wgpu::BufferUsages::VERTEX
3308                | wgpu::BufferUsages::COPY_DST
3309                | wgpu::BufferUsages::COPY_SRC,
3310        );
3311        let ring_projective = UploadRing::new(
3312            &device,
3313            "ring projective",
3314            1 << 16,
3315            wgpu::BufferUsages::VERTEX
3316                | wgpu::BufferUsages::COPY_DST
3317                | wgpu::BufferUsages::COPY_SRC,
3318        );
3319        let ring_nv12 = UploadRing::new(
3320            &device,
3321            "ring nv12",
3322            1 << 20,
3323            wgpu::BufferUsages::VERTEX
3324                | wgpu::BufferUsages::COPY_DST
3325                | wgpu::BufferUsages::COPY_SRC,
3326        );
3327        // Placeholder textures
3328        let depth_stencil_tex = device.create_texture(&wgpu::TextureDescriptor {
3329            label: Some("temp ds"),
3330            size: wgpu::Extent3d {
3331                width: 1,
3332                height: 1,
3333                depth_or_array_layers: 1,
3334            },
3335            mip_level_count: 1,
3336            sample_count: 1,
3337            dimension: wgpu::TextureDimension::D2,
3338            format: wgpu::TextureFormat::Depth24PlusStencil8,
3339            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
3340            view_formats: &[],
3341        });
3342        let depth_stencil_view =
3343            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
3344
3345        let mut renderer = WgpuSceneRenderer {
3346            device,
3347            queue,
3348            output_format,
3349            output_width: 0,
3350            output_height: 0,
3351            pixels_per_point: 1.0,
3352
3353            surface_pipes,
3354            working_space_pipes,
3355            layer_pipes,
3356            working_space_layer_pipes,
3357
3358            rects: InstancedPipe::new(ring_rect),
3359            borders: InstancedPipe::new(ring_border),
3360            ellipses: InstancedPipe::new(ring_ellipse),
3361            ellipse_borders: InstancedPipe::new(ring_ellipse_border),
3362            arcs: InstancedPipe::new(ring_arc),
3363            glyph_mask: InstancedPipe::new(ring_glyph_mask),
3364            glyph_color: InstancedPipe::new(ring_glyph_color),
3365
3366            text_bind_layout,
3367
3368            image_bind_layout_rgba,
3369            image_bind_layout_nv12,
3370            image_sampler,
3371            image_sampler_nearest,
3372            image_sampler_repeat,
3373            image_sampler_repeat_nearest,
3374            layer_sampler,
3375            layer_sampler_linear,
3376
3377            blur_ring,
3378
3379            slug_enabled,
3380            slug_ring: ring_slug,
3381            slug_cache: slug::GlyphSlugCache::new(),
3382            slug_draw_cache: HashMap::new(),
3383            slug_draw_cache_bytes_total: 0,
3384
3385            clip_ring: ring_clip,
3386
3387            nv12: InstancedPipe::new(ring_nv12),
3388
3389            mesh_cache: HashMap::new(),
3390            mesh_cache_bytes_total: 0,
3391            mesh_uniform_buf,
3392            mesh_bind,
3393            mesh_uniform_head: 0,
3394            mesh_uniform_alignment,
3395            mesh_uniform_cap,
3396
3397            projective_ring: ring_projective,
3398            blend_ring,
3399            flatten_layer_ids: Vec::new(),
3400            blend_snapshots: std::collections::HashMap::new(),
3401            blend_snapshot_pool: HashMap::new(),
3402            blend_copies: Vec::new(),
3403
3404            msaa_samples,
3405            working_space_msaa_samples,
3406            depth_stencil_tex,
3407            depth_stencil_view,
3408            msaa_tex: None,
3409            msaa_view: None,
3410            ws_msaa_tex: None,
3411            ws_msaa_view: None,
3412            surface_resolve_tex: None,
3413            surface_resolve_view: None,
3414            surface_resolve_bytes: 0,
3415            msaa_bytes: 0,
3416            ws_msaa_bytes: 0,
3417            depth_stencil_bytes: 0,
3418            working_space_bytes: 0,
3419            gpu_budget_bytes: MAX_GPU_RESOURCE_BYTES,
3420            globals_bind,
3421            globals_buf,
3422            globals_staging,
3423            globals_staging_cap,
3424
3425            atlas_mask,
3426            atlas_color,
3427            atlas_mask_bind,
3428            atlas_color_bind,
3429            atlas_generation: repose_text::font_generation(),
3430            atlas_mask_pending: AtlasPending::default(),
3431            atlas_color_pending: AtlasPending::default(),
3432            atlas_last_compact_frame: 0,
3433            atlas_mask_failures: HashSet::new(),
3434            atlas_color_failures: HashSet::new(),
3435            atlas_mask_deferred: HashSet::new(),
3436            atlas_color_deferred: HashSet::new(),
3437            glyph_outline_cache: HashMap::new(),
3438            legacy_shaped_text_cache: HashMap::new(),
3439
3440            next_image_handle: 1,
3441            images: HashMap::new(),
3442            retained: HashMap::new(),
3443            retained_bytes_total: 0,
3444
3445            next_coverage_handle: 1,
3446            coverages: HashMap::new(),
3447
3448            frame_index: 0,
3449            cache_touch_clock: 0,
3450            image_bytes_total: 0,
3451            image_evict_after_frames: 600,         // ~10s @ 60fps
3452            image_budget_bytes: 512 * 1024 * 1024, // 512 MB
3453            layer_pool: HashMap::new(),
3454            producer_layer_ids: HashSet::new(),
3455            layer_bytes_total: 0,
3456            transient_layer_bytes_total: 0,
3457
3458            working_space: false,
3459            ws_tex: None,
3460            ws_view: None,
3461            ws_bind: None,
3462            display_pipeline: None,
3463            display_layout: None,
3464
3465            callback_resources: CallbackResources::default(),
3466            callback_scoped_resources: HashMap::new(),
3467            callback_scope_uses: HashMap::new(),
3468            callback_scope_payloads: HashMap::new(),
3469            callback_scope_clock: 0,
3470            blend_snapshot_bytes_total: 0,
3471            frame_active: false,
3472            last_render_error: None,
3473        };
3474
3475        renderer.recreate_msaa_and_depth_stencil();
3476        renderer
3477    }
3478}
3479
3480impl WgpuSurfaceBackend {
3481    #[cfg(feature = "winit-surface")]
3482    pub async fn new_async(
3483        window: Arc<winit::window::Window>,
3484    ) -> anyhow::Result<WgpuSurfaceBackend> {
3485        Self::new_async_with_options(window, 4, PresentModePref::Auto).await
3486    }
3487
3488    /// Create a windowed surface backend, honoring the requested MSAA sample
3489    /// count (falling back to the largest supported count <= `msaa_samples`).
3490    #[cfg(feature = "winit-surface")]
3491    pub async fn new_async_with_msaa(
3492        window: Arc<winit::window::Window>,
3493        msaa_samples: u32,
3494    ) -> anyhow::Result<WgpuSurfaceBackend> {
3495        Self::new_async_with_options(window, msaa_samples, PresentModePref::Auto).await
3496    }
3497
3498    /// Create a windowed surface backend, honoring the requested MSAA sample
3499    /// count and present-mode preference.
3500    #[cfg(feature = "winit-surface")]
3501    pub async fn new_async_with_options(
3502        window: Arc<winit::window::Window>,
3503        msaa_samples: u32,
3504        present_mode: PresentModePref,
3505    ) -> anyhow::Result<WgpuSurfaceBackend> {
3506        let instance: Instance = if cfg!(target_arch = "wasm32") {
3507            let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
3508            desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
3509            wgpu::util::new_instance_with_webgpu_detection(desc).await
3510        } else {
3511            wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle())
3512        };
3513
3514        let surface = instance.create_surface(window.clone())?;
3515
3516        let adapter = instance
3517            .request_adapter(&wgpu::RequestAdapterOptions {
3518                power_preference: wgpu::PowerPreference::HighPerformance,
3519                compatible_surface: Some(&surface),
3520                force_fallback_adapter: false,
3521                apply_limit_buckets: false,
3522            })
3523            .await
3524            .map_err(|e| anyhow::anyhow!("No suitable adapter: {e:?}"))?;
3525
3526        let limits = adapter.limits();
3527
3528        let features = {
3529            let available = adapter.features();
3530            let mut features = wgpu::Features::empty();
3531            if available.contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM) {
3532                features |= wgpu::Features::TEXTURE_FORMAT_16BIT_NORM;
3533            }
3534            #[cfg(target_os = "linux")]
3535            {
3536                if available.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_FD) {
3537                    features |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_FD;
3538                }
3539                if available.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF) {
3540                    features |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF;
3541                }
3542            }
3543            features
3544        };
3545
3546        let (device, queue) = adapter
3547            .request_device(&wgpu::DeviceDescriptor {
3548                label: Some("repose-rs device"),
3549                required_features: features,
3550                required_limits: limits,
3551                experimental_features: wgpu::ExperimentalFeatures::disabled(),
3552                memory_hints: wgpu::MemoryHints::default(),
3553                trace: wgpu::Trace::Off,
3554            })
3555            .await
3556            .map_err(|e| anyhow::anyhow!("request_device failed: {e:?}"))?;
3557
3558        let size = window.inner_size();
3559
3560        let caps = surface.get_capabilities(&adapter);
3561
3562        let (format, view_format) = if cfg!(target_arch = "wasm32")
3563            && adapter
3564                .get_downlevel_capabilities()
3565                .flags
3566                .contains(wgpu::DownlevelFlags::SURFACE_VIEW_FORMATS)
3567        {
3568            let non_srgb = caps
3569                .formats
3570                .iter()
3571                .copied()
3572                .find(|f| !f.is_srgb())
3573                .unwrap_or(caps.formats[0]);
3574            (non_srgb, Some(non_srgb.add_srgb_suffix()))
3575        } else if cfg!(target_arch = "wasm32") {
3576            let fmt = caps
3577                .formats
3578                .iter()
3579                .copied()
3580                .find(|f| f.is_srgb())
3581                .unwrap_or(caps.formats[0]);
3582            (fmt, None)
3583        } else {
3584            let fmt = caps
3585                .formats
3586                .iter()
3587                .copied()
3588                .find(|f| f.is_srgb())
3589                .unwrap_or(caps.formats[0]);
3590            (fmt, None)
3591        };
3592
3593        let present_mode = pick_present_mode(&caps, present_mode);
3594        let alpha_mode = caps.alpha_modes[0];
3595
3596        let render_format = view_format.unwrap_or(format);
3597        let msaa_samples = pick_surface_msaa(&adapter, render_format, msaa_samples);
3598        let working_space_msaa_samples = pick_surface_msaa_for_mode(
3599            &adapter,
3600            wgpu::TextureFormat::Rgba16Float,
3601            msaa_samples,
3602            true,
3603        );
3604        let mut renderer = WgpuSceneRenderer::from_device_with_working_space_msaa(
3605            device,
3606            queue,
3607            render_format,
3608            msaa_samples,
3609            working_space_msaa_samples,
3610        );
3611        renderer.resize(size.width, size.height);
3612
3613        let view_formats = view_format.into_iter().collect::<Vec<_>>();
3614
3615        let surface_usage = if cfg!(target_arch = "wasm32") {
3616            wgpu::TextureUsages::RENDER_ATTACHMENT
3617        } else {
3618            wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC
3619        };
3620        let config = wgpu::SurfaceConfiguration {
3621            usage: surface_usage,
3622            format,
3623            width: size.width.max(1),
3624            height: size.height.max(1),
3625            present_mode,
3626            alpha_mode,
3627            color_space: wgpu::SurfaceColorSpace::Auto,
3628            view_formats,
3629            desired_maximum_frame_latency: 1,
3630        };
3631        surface.configure(&renderer.device, &config);
3632
3633        Ok(WgpuSurfaceBackend {
3634            #[cfg(feature = "winit-surface")]
3635            instance: Some(instance),
3636            #[cfg(feature = "winit-surface")]
3637            window: Some(window),
3638            surface: Some(surface),
3639            surface_config: Some(config),
3640            pending_reconfigure: false,
3641            renderer,
3642        })
3643    }
3644
3645    #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
3646    pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<WgpuSurfaceBackend> {
3647        pollster::block_on(Self::new_async(window))
3648    }
3649
3650    #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
3651    pub fn new_with_msaa(
3652        window: Arc<winit::window::Window>,
3653        msaa_samples: u32,
3654    ) -> anyhow::Result<WgpuSurfaceBackend> {
3655        pollster::block_on(Self::new_async_with_msaa(window, msaa_samples))
3656    }
3657
3658    #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
3659    pub fn new_with_options(
3660        window: Arc<winit::window::Window>,
3661        msaa_samples: u32,
3662        present_mode: PresentModePref,
3663    ) -> anyhow::Result<WgpuSurfaceBackend> {
3664        pollster::block_on(Self::new_async_with_options(
3665            window,
3666            msaa_samples,
3667            present_mode,
3668        ))
3669    }
3670
3671    #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
3672    pub fn new(_window: Arc<winit::window::Window>) -> anyhow::Result<WgpuSurfaceBackend> {
3673        anyhow::bail!("Use WgpuSurfaceBackend::new_async(window).await on wasm32")
3674    }
3675
3676    #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
3677    pub fn new_with_msaa(
3678        _window: Arc<winit::window::Window>,
3679        _msaa_samples: u32,
3680    ) -> anyhow::Result<WgpuSurfaceBackend> {
3681        anyhow::bail!("Use WgpuSurfaceBackend::new_async_with_msaa(window, msaa).await on wasm32")
3682    }
3683
3684    #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
3685    pub fn new_with_options(
3686        _window: Arc<winit::window::Window>,
3687        _msaa_samples: u32,
3688        _present_mode: PresentModePref,
3689    ) -> anyhow::Result<WgpuSurfaceBackend> {
3690        anyhow::bail!(
3691            "Use WgpuSurfaceBackend::new_async_with_options(window, msaa, mode).await on wasm32"
3692        )
3693    }
3694}
3695
3696/// Pick the swapchain present mode honoring `pref`, falling back to an "auto"
3697/// Fifo-first selection when the preferred mode is unavailable.
3698#[cfg(feature = "winit-surface")]
3699fn pick_present_mode(caps: &wgpu::SurfaceCapabilities, pref: PresentModePref) -> wgpu::PresentMode {
3700    let auto = || {
3701        caps.present_modes
3702            .iter()
3703            .copied()
3704            .find(|m| *m == wgpu::PresentMode::Fifo)
3705            .or_else(|| {
3706                caps.present_modes
3707                    .iter()
3708                    .copied()
3709                    .find(|m| *m == wgpu::PresentMode::Mailbox)
3710            })
3711            .unwrap_or(wgpu::PresentMode::Immediate)
3712    };
3713    match pref {
3714        PresentModePref::Auto => auto(),
3715        PresentModePref::Fifo if caps.present_modes.contains(&wgpu::PresentMode::Fifo) => {
3716            wgpu::PresentMode::Fifo
3717        }
3718        PresentModePref::Mailbox if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) => {
3719            wgpu::PresentMode::Mailbox
3720        }
3721        PresentModePref::Immediate
3722            if caps.present_modes.contains(&wgpu::PresentMode::Immediate) =>
3723        {
3724            wgpu::PresentMode::Immediate
3725        }
3726        _ => auto(),
3727    }
3728}
3729
3730/// Pick the MSAA sample count for the surface pass, honoring `requested` and
3731/// falling back to the largest supported count <= it.
3732pub fn pick_surface_msaa(
3733    adapter: &wgpu::Adapter,
3734    format: wgpu::TextureFormat,
3735    requested: u32,
3736) -> u32 {
3737    pick_surface_msaa_for_mode(adapter, format, requested, false)
3738}
3739
3740pub fn pick_surface_msaa_for_mode(
3741    adapter: &wgpu::Adapter,
3742    format: wgpu::TextureFormat,
3743    requested: u32,
3744    working_space: bool,
3745) -> u32 {
3746    let requested = requested.max(1);
3747    let color_feat = adapter.get_texture_format_features(format);
3748    let working_space_feat = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba16Float);
3749    let depth_feat = adapter.get_texture_format_features(wgpu::TextureFormat::Depth24PlusStencil8);
3750    let supported = |n: u32| {
3751        color_feat
3752            .allowed_usages
3753            .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
3754            && color_feat.flags.sample_count_supported(n)
3755            && (!working_space
3756                || (working_space_feat
3757                    .allowed_usages
3758                    .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
3759                    && working_space_feat.flags.sample_count_supported(n)
3760                    && (n == 1
3761                        || working_space_feat
3762                            .flags
3763                            .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE))))
3764            && (n == 1
3765                || color_feat
3766                    .flags
3767                    .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE))
3768            && depth_feat
3769                .allowed_usages
3770                .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
3771            && depth_feat.flags.sample_count_supported(n)
3772    };
3773    let mut candidates = vec![requested];
3774    for n in [8, 4, 2, 1] {
3775        if n < requested {
3776            candidates.push(n);
3777        }
3778    }
3779    let chosen = candidates.into_iter().find(|&n| supported(n)).unwrap_or(1);
3780    if chosen != requested {
3781        log::info!("requested MSAA x{requested}, using x{chosen}");
3782    }
3783    chosen
3784}
3785
3786impl WgpuSceneRenderer {
3787    // Image API
3788
3789    pub fn set_image_from_bytes(
3790        &mut self,
3791        handle: u64,
3792        data: &[u8],
3793        srgb: bool,
3794    ) -> anyhow::Result<()> {
3795        let img = image::load_from_memory(data)?;
3796        let rgba = img.to_rgba8();
3797        let (w, h) = rgba.dimensions();
3798        // Encoded uploads have no size at the call site, so publish it here for
3799        // layout once the decode lands.
3800        repose_core::set_image_intrinsic_size(handle, w, h);
3801        self.set_image_rgba8(handle, w, h, &rgba, srgb)
3802    }
3803
3804    pub fn set_image_rgba8(
3805        &mut self,
3806        handle: u64,
3807        w: u32,
3808        h: u32,
3809        rgba: &[u8],
3810        srgb: bool,
3811    ) -> anyhow::Result<()> {
3812        validate_image_handle(handle)?;
3813        validate_texture_dimensions(&self.device, w, h)?;
3814        let expected = checked_image_bytes_usize(w, h, 4)?;
3815        if rgba.len() < expected {
3816            return Err(anyhow::anyhow!(
3817                "RGBA buffer too small: {} < {}",
3818                rgba.len(),
3819                expected
3820            ));
3821        }
3822
3823        let format = if srgb {
3824            wgpu::TextureFormat::Rgba8UnormSrgb
3825        } else {
3826            wgpu::TextureFormat::Rgba8Unorm
3827        };
3828
3829        let needs_recreate = match self.images.get(&handle) {
3830            Some(ImageTex::Rgba {
3831                w: cw,
3832                h: ch,
3833                format: cf,
3834                ..
3835            }) => *cw != w || *ch != h || *cf != format,
3836            _ => true,
3837        };
3838
3839        if needs_recreate {
3840            self.remove_image(handle);
3841
3842            let (tex, binds) = self.create_rgba_tex(w, h, format);
3843            let bytes = checked_image_bytes(w, h, 4)?;
3844            self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
3845
3846            self.images.insert(
3847                handle,
3848                ImageTex::Rgba {
3849                    tex,
3850                    binds,
3851                    w,
3852                    h,
3853                    format,
3854                    last_used_frame: self.frame_index,
3855                    bytes,
3856                },
3857            );
3858        }
3859
3860        if let Some(old) = self.retained.remove(&handle) {
3861            self.retained_bytes_total = self
3862                .retained_bytes_total
3863                .saturating_sub(old.rgba.len() as u64);
3864        }
3865        self.retained_bytes_total = self.retained_bytes_total.saturating_add(expected as u64);
3866        self.retained.insert(
3867            handle,
3868            RetainedImage {
3869                w,
3870                h,
3871                format,
3872                rgba: rgba[..expected].to_vec(),
3873                last_used_frame: self.frame_index,
3874            },
3875        );
3876        self.evict_retained_excess();
3877
3878        let tex = match self.images.get(&handle) {
3879            Some(ImageTex::Rgba { tex, .. }) => tex,
3880            _ => unreachable!(),
3881        };
3882
3883        self.queue.write_texture(
3884            wgpu::TexelCopyTextureInfo {
3885                texture: tex,
3886                mip_level: 0,
3887                origin: wgpu::Origin3d::ZERO,
3888                aspect: wgpu::TextureAspect::All,
3889            },
3890            &rgba[..expected],
3891            wgpu::TexelCopyBufferLayout {
3892                offset: 0,
3893                bytes_per_row: Some(4 * w),
3894                rows_per_image: Some(h),
3895            },
3896            wgpu::Extent3d {
3897                width: w,
3898                height: h,
3899                depth_or_array_layers: 1,
3900            },
3901        );
3902
3903        // Ensure budget limits
3904        self.evict_budget_excess();
3905
3906        Ok(())
3907    }
3908
3909    /// Create (but do not populate) the GPU texture, view and bind group for an
3910    /// RGBA image. Pixels are written separately via `write_texture`.
3911    fn create_image_binds(
3912        &self,
3913        view: &wgpu::TextureView,
3914        label: &str,
3915        sampler: Option<&wgpu::Sampler>,
3916    ) -> ImageBinds {
3917        let make = |name: &str, sampler: &wgpu::Sampler| {
3918            self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3919                label: Some(name),
3920                layout: &self.image_bind_layout_rgba,
3921                entries: &[
3922                    wgpu::BindGroupEntry {
3923                        binding: 0,
3924                        resource: wgpu::BindingResource::TextureView(view),
3925                    },
3926                    wgpu::BindGroupEntry {
3927                        binding: 1,
3928                        resource: wgpu::BindingResource::Sampler(sampler),
3929                    },
3930                ],
3931            })
3932        };
3933        match sampler {
3934            Some(sampler) => {
3935                let bind = make(label, sampler);
3936                ImageBinds {
3937                    linear: make(label, sampler),
3938                    nearest: bind.clone(),
3939                    tile_linear: bind.clone(),
3940                    tile_nearest: bind,
3941                }
3942            }
3943            None => ImageBinds {
3944                linear: make(label, &self.image_sampler),
3945                nearest: make(label, &self.image_sampler_nearest),
3946                tile_linear: make(label, &self.image_sampler_repeat),
3947                tile_nearest: make(label, &self.image_sampler_repeat_nearest),
3948            },
3949        }
3950    }
3951
3952    fn create_nv12_binds(
3953        &self,
3954        view_y: &wgpu::TextureView,
3955        view_uv: &wgpu::TextureView,
3956        yuv_buf: &wgpu::Buffer,
3957        label: &str,
3958    ) -> ImageBinds {
3959        let make = |name: &str, sampler: &wgpu::Sampler| {
3960            self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3961                label: Some(name),
3962                layout: &self.image_bind_layout_nv12,
3963                entries: &[
3964                    wgpu::BindGroupEntry {
3965                        binding: 0,
3966                        resource: wgpu::BindingResource::TextureView(view_y),
3967                    },
3968                    wgpu::BindGroupEntry {
3969                        binding: 1,
3970                        resource: wgpu::BindingResource::TextureView(view_uv),
3971                    },
3972                    wgpu::BindGroupEntry {
3973                        binding: 2,
3974                        resource: wgpu::BindingResource::Sampler(sampler),
3975                    },
3976                    wgpu::BindGroupEntry {
3977                        binding: 3,
3978                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3979                            buffer: yuv_buf,
3980                            offset: 0,
3981                            size: None,
3982                        }),
3983                    },
3984                ],
3985            })
3986        };
3987        ImageBinds {
3988            linear: make(label, &self.image_sampler),
3989            nearest: make(label, &self.image_sampler_nearest),
3990            tile_linear: make(label, &self.image_sampler_repeat),
3991            tile_nearest: make(label, &self.image_sampler_repeat_nearest),
3992        }
3993    }
3994
3995    fn create_rgba_tex(
3996        &self,
3997        w: u32,
3998        h: u32,
3999        format: wgpu::TextureFormat,
4000    ) -> (wgpu::Texture, ImageBinds) {
4001        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
4002            label: Some("user image rgba"),
4003            size: wgpu::Extent3d {
4004                width: w,
4005                height: h,
4006                depth_or_array_layers: 1,
4007            },
4008            mip_level_count: 1,
4009            sample_count: 1,
4010            dimension: wgpu::TextureDimension::D2,
4011            format,
4012            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4013            view_formats: &[],
4014        });
4015        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
4016        let binds = self.create_image_binds(&view, "image bind rgba", None);
4017        (tex, binds)
4018    }
4019
4020    /// Register an externally-created `wgpu::TextureView` as an image (zero-copy).
4021    fn validate_native_texture_view(
4022        &self,
4023        view: &wgpu::TextureView,
4024        width: u32,
4025        height: u32,
4026    ) -> anyhow::Result<()> {
4027        if width == 0 || height == 0 {
4028            anyhow::bail!("native texture dimensions must be non-zero");
4029        }
4030        let texture = view.texture();
4031        if texture.sample_count() != 1
4032            || texture.dimension() != wgpu::TextureDimension::D2
4033            || texture.depth_or_array_layers() != 1
4034            || texture.mip_level_count() == 0
4035        {
4036            anyhow::bail!("native texture must be a single-sample 2D texture with mip levels");
4037        }
4038        if width > self.device.limits().max_texture_dimension_2d
4039            || height > self.device.limits().max_texture_dimension_2d
4040        {
4041            anyhow::bail!("native texture dimensions exceed the device limit");
4042        }
4043        if !texture
4044            .usage()
4045            .contains(wgpu::TextureUsages::TEXTURE_BINDING)
4046        {
4047            anyhow::bail!("native texture lacks TEXTURE_BINDING usage");
4048        }
4049        if !native_format_supported(texture.format(), self.device.features()) {
4050            anyhow::bail!(
4051                "native texture format {:?} is not a filterable alpha color format",
4052                texture.format()
4053            );
4054        }
4055        if texture.width() < width || texture.height() < height {
4056            anyhow::bail!(
4057                "native texture is {}x{}, smaller than requested {width}x{height}",
4058                texture.width(),
4059                texture.height()
4060            );
4061        }
4062        Ok(())
4063    }
4064
4065    pub fn register_native_texture(
4066        &mut self,
4067        view: &wgpu::TextureView,
4068        width: u32,
4069        height: u32,
4070    ) -> u64 {
4071        if let Err(error) = self.validate_native_texture_view(view, width, height) {
4072            log::warn!("register_native_texture: {error:#}");
4073            return 0;
4074        }
4075        let handle = self.next_image_handle;
4076        self.next_image_handle += 1;
4077        let binds = self.create_image_binds(view, "user native image", None);
4078        self.images.insert(
4079            handle,
4080            ImageTex::User {
4081                binds,
4082                w: width,
4083                h: height,
4084                last_used_frame: self.frame_index,
4085                bytes: 0,
4086            },
4087        );
4088        handle
4089    }
4090
4091    /// Like `register_native_texture` but with custom sampler descriptor.
4092    pub fn register_native_texture_with_sampler(
4093        &mut self,
4094        view: &wgpu::TextureView,
4095        sampler_desc: wgpu::SamplerDescriptor<'_>,
4096        width: u32,
4097        height: u32,
4098    ) -> u64 {
4099        if let Err(error) = self.validate_native_texture_view(view, width, height) {
4100            log::warn!("register_native_texture: {error:#}");
4101            return 0;
4102        }
4103        if let Err(error) = validate_sampler_descriptor(&self.device, &sampler_desc) {
4104            log::warn!("register_native_texture: {error:#}");
4105            return 0;
4106        }
4107        let handle = self.next_image_handle;
4108        self.next_image_handle += 1;
4109        let sampler = self.device.create_sampler(&sampler_desc);
4110        let binds = self.create_image_binds(view, "user native image sampleropts", Some(&sampler));
4111        self.images.insert(
4112            handle,
4113            ImageTex::User {
4114                binds,
4115                w: width,
4116                h: height,
4117                last_used_frame: self.frame_index,
4118                bytes: 0,
4119            },
4120        );
4121        handle
4122    }
4123
4124    /// Update an existing native texture handle with a new view (reuse handle).
4125    pub fn update_native_texture(&mut self, handle: u64, view: &wgpu::TextureView) {
4126        if handle == 0 {
4127            log::warn!("update_native_texture: reserved handle");
4128            return;
4129        }
4130        let Some((w, h)) = self.images.get(&handle).and_then(|entry| match entry {
4131            ImageTex::User { w, h, .. } => Some((*w, *h)),
4132            _ => None,
4133        }) else {
4134            log::warn!("update_native_texture: handle {handle} is not a native image");
4135            return;
4136        };
4137        if let Err(error) = self.validate_native_texture_view(view, w, h) {
4138            log::warn!("update_native_texture: {error:#}");
4139            return;
4140        }
4141        let binds = self.create_image_binds(view, "user native image update", None);
4142        if let Some(entry) = self.images.get_mut(&handle) {
4143            *entry = ImageTex::User {
4144                binds,
4145                w,
4146                h,
4147                last_used_frame: self.frame_index,
4148                bytes: 0,
4149            };
4150        }
4151    }
4152
4153    pub fn set_image_nv12(
4154        &mut self,
4155        handle: u64,
4156        w: u32,
4157        h: u32,
4158        y: &[u8],
4159        uv: &[u8],
4160        color_info: ColorInfo,
4161    ) -> anyhow::Result<()> {
4162        validate_image_handle(handle)?;
4163        validate_texture_dimensions(&self.device, w, h)?;
4164        if self
4165            .images
4166            .get(&handle)
4167            .is_some_and(|image| matches!(image, ImageTex::Nv12 { external: true, .. }))
4168        {
4169            anyhow::bail!("cannot overwrite an external DMA-BUF image");
4170        }
4171        let y_expected = checked_image_bytes_usize(w, h, 1)?;
4172        let uv_w = w.div_ceil(2);
4173        let uv_h = h.div_ceil(2);
4174        let uv_expected = checked_image_bytes_usize(uv_w, uv_h, 2)?;
4175
4176        if y.len() < y_expected {
4177            return Err(anyhow::anyhow!("Y plane too small"));
4178        }
4179        if uv.len() < uv_expected {
4180            return Err(anyhow::anyhow!("UV plane too small"));
4181        }
4182
4183        let needs_recreate = match self.images.get(&handle) {
4184            Some(ImageTex::Nv12 {
4185                w: ww,
4186                h: hh,
4187                pixel_format,
4188                ..
4189            }) => *ww != w || *hh != h || *pixel_format != PixelFormat::Nv12,
4190            _ => true,
4191        };
4192
4193        // Compute the YUV->RGB transform on the CPU.
4194        let yuv_raw = make_yuv_transform_raw(color_info, PixelFormat::Nv12);
4195
4196        if needs_recreate {
4197            self.remove_image(handle);
4198
4199            let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
4200                label: Some("nv12 Y"),
4201                size: wgpu::Extent3d {
4202                    width: w,
4203                    height: h,
4204                    depth_or_array_layers: 1,
4205                },
4206                mip_level_count: 1,
4207                sample_count: 1,
4208                dimension: wgpu::TextureDimension::D2,
4209                format: wgpu::TextureFormat::R8Unorm,
4210                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4211                view_formats: &[],
4212            });
4213            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
4214
4215            let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
4216                label: Some("nv12 UV"),
4217                size: wgpu::Extent3d {
4218                    width: uv_w,
4219                    height: uv_h,
4220                    depth_or_array_layers: 1,
4221                },
4222                mip_level_count: 1,
4223                sample_count: 1,
4224                dimension: wgpu::TextureDimension::D2,
4225                format: wgpu::TextureFormat::Rg8Unorm,
4226                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4227                view_formats: &[],
4228            });
4229            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
4230
4231            // Create a uniform buffer for the YUV transform (per-image).
4232            let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
4233                label: Some("nv12 yuv transform"),
4234                size: std::mem::size_of::<YuvTransformRaw>() as u64,
4235                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4236                mapped_at_creation: false,
4237            });
4238
4239            // Write initial transform.
4240            self.queue
4241                .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
4242
4243            let binds = self.create_nv12_binds(&view_y, &view_uv, &yuv_buf, "nv12 bind");
4244
4245            let bytes = checked_image_bytes(w, h, 1)?
4246                .checked_add(checked_image_bytes(uv_w, uv_h, 2)?)
4247                .and_then(|bytes| bytes.checked_add(std::mem::size_of::<YuvTransformRaw>() as u64))
4248                .ok_or_else(|| anyhow::anyhow!("NV12 image byte size overflow"))?;
4249            self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
4250
4251            self.images.insert(
4252                handle,
4253                ImageTex::Nv12 {
4254                    tex_y,
4255                    tex_uv,
4256                    binds,
4257                    yuv_buf,
4258                    w,
4259                    h,
4260                    pixel_format: PixelFormat::Nv12,
4261                    fourcc: Some(NV12_FOURCC),
4262                    color_info,
4263                    external: false,
4264                    last_used_frame: self.frame_index,
4265                    bytes,
4266                },
4267            );
4268        } else {
4269            // Re-use existing textures; just update the YUV transform if needed.
4270            if let Some(ImageTex::Nv12 {
4271                yuv_buf,
4272                color_info: stored_color,
4273                last_used_frame,
4274                ..
4275            }) = self.images.get_mut(&handle)
4276            {
4277                *stored_color = color_info;
4278                *last_used_frame = self.frame_index;
4279                self.queue
4280                    .write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
4281            }
4282        }
4283
4284        let (tex_y, tex_uv, _binds) = match self.images.get(&handle) {
4285            Some(ImageTex::Nv12 {
4286                tex_y,
4287                tex_uv,
4288                binds,
4289                ..
4290            }) => (tex_y, tex_uv, binds),
4291            _ => return Err(anyhow::anyhow!("Handle is not NV12")),
4292        };
4293
4294        self.queue.write_texture(
4295            wgpu::TexelCopyTextureInfo {
4296                texture: tex_y,
4297                mip_level: 0,
4298                origin: wgpu::Origin3d::ZERO,
4299                aspect: wgpu::TextureAspect::All,
4300            },
4301            &y[..y_expected],
4302            wgpu::TexelCopyBufferLayout {
4303                offset: 0,
4304                bytes_per_row: Some(w),
4305                rows_per_image: Some(h),
4306            },
4307            wgpu::Extent3d {
4308                width: w,
4309                height: h,
4310                depth_or_array_layers: 1,
4311            },
4312        );
4313
4314        self.queue.write_texture(
4315            wgpu::TexelCopyTextureInfo {
4316                texture: tex_uv,
4317                mip_level: 0,
4318                origin: wgpu::Origin3d::ZERO,
4319                aspect: wgpu::TextureAspect::All,
4320            },
4321            &uv[..uv_expected],
4322            wgpu::TexelCopyBufferLayout {
4323                offset: 0,
4324                bytes_per_row: Some(2 * uv_w),
4325                rows_per_image: Some(uv_h),
4326            },
4327            wgpu::Extent3d {
4328                width: uv_w,
4329                height: uv_h,
4330                depth_or_array_layers: 1,
4331            },
4332        );
4333
4334        self.evict_budget_excess();
4335        Ok(())
4336    }
4337
4338    pub fn set_image_planes(
4339        &mut self,
4340        handle: u64,
4341        w: u32,
4342        h: u32,
4343        pixel_format: PixelFormat,
4344        planes: &[&[u8]],
4345        color_info: ColorInfo,
4346    ) -> anyhow::Result<()> {
4347        validate_image_handle(handle)?;
4348        if planes.len() != pixel_format.num_planes() as usize {
4349            anyhow::bail!(
4350                "{} requires {} planes, got {}",
4351                match pixel_format {
4352                    PixelFormat::Nv12 => "NV12",
4353                    PixelFormat::P010 => "P010",
4354                    PixelFormat::I420 => "I420",
4355                    PixelFormat::I444 => "I444",
4356                    PixelFormat::Rgba => "RGBA",
4357                },
4358                pixel_format.num_planes(),
4359                planes.len()
4360            );
4361        }
4362        match pixel_format {
4363            PixelFormat::Nv12 => {
4364                let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
4365                let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
4366                self.set_image_nv12(handle, w, h, y, uv, color_info)
4367            }
4368            PixelFormat::P010 => {
4369                let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
4370                let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
4371                self.set_image_p010(handle, w, h, y, uv, color_info)
4372            }
4373            PixelFormat::I420 | PixelFormat::I444 => Err(anyhow::anyhow!(
4374                "I420/I444 not implemented and unlikely -> cheap to convert to NV12 (better for the GPU too)"
4375            )),
4376            PixelFormat::Rgba => {
4377                let rgba = planes
4378                    .first()
4379                    .ok_or(anyhow::anyhow!("missing RGBA plane"))?;
4380                self.set_image_rgba8(handle, w, h, rgba, false)
4381            }
4382        }
4383    }
4384
4385    fn set_image_p010(
4386        &mut self,
4387        handle: u64,
4388        w: u32,
4389        h: u32,
4390        y: &[u8],
4391        uv: &[u8],
4392        color_info: ColorInfo,
4393    ) -> anyhow::Result<()> {
4394        validate_image_handle(handle)?;
4395        validate_texture_dimensions(&self.device, w, h)?;
4396        if self
4397            .images
4398            .get(&handle)
4399            .is_some_and(|image| matches!(image, ImageTex::Nv12 { external: true, .. }))
4400        {
4401            anyhow::bail!("cannot overwrite an external DMA-BUF image");
4402        }
4403        if !self
4404            .device
4405            .features()
4406            .contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
4407        {
4408            anyhow::bail!("P010 requires TEXTURE_FORMAT_16BIT_NORM");
4409        }
4410        let uv_w = w.div_ceil(2);
4411        let uv_h = h.div_ceil(2);
4412
4413        let y_expected = checked_image_bytes_usize(w, h, 2)?;
4414        let uv_expected = checked_image_bytes_usize(uv_w, uv_h, 4)?;
4415
4416        if y.len() < y_expected {
4417            return Err(anyhow::anyhow!("P010 Y plane too small"));
4418        }
4419        if uv.len() < uv_expected {
4420            return Err(anyhow::anyhow!("P010 UV plane too small"));
4421        }
4422
4423        // P010 reuses the NV12 pipeline (same bind group layout -> wgpu
4424        // abstracts the storage format so R16Unorm/Rg16Unorm are
4425        // filterable float textures just like R8Unorm/Rg8Unorm).
4426        let needs_recreate = match self.images.get(&handle) {
4427            Some(ImageTex::Nv12 {
4428                w: ww,
4429                h: hh,
4430                pixel_format,
4431                ..
4432            }) => *ww != w || *hh != h || *pixel_format != PixelFormat::P010,
4433            _ => true,
4434        };
4435
4436        let yuv_raw = make_yuv_transform_raw(color_info, PixelFormat::P010);
4437
4438        if needs_recreate {
4439            self.remove_image(handle);
4440
4441            let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
4442                label: Some("p010 Y"),
4443                size: wgpu::Extent3d {
4444                    width: w,
4445                    height: h,
4446                    depth_or_array_layers: 1,
4447                },
4448                mip_level_count: 1,
4449                sample_count: 1,
4450                dimension: wgpu::TextureDimension::D2,
4451                format: wgpu::TextureFormat::R16Unorm,
4452                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4453                view_formats: &[],
4454            });
4455            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
4456
4457            let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
4458                label: Some("p010 UV"),
4459                size: wgpu::Extent3d {
4460                    width: uv_w,
4461                    height: uv_h,
4462                    depth_or_array_layers: 1,
4463                },
4464                mip_level_count: 1,
4465                sample_count: 1,
4466                dimension: wgpu::TextureDimension::D2,
4467                format: wgpu::TextureFormat::Rg16Unorm,
4468                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4469                view_formats: &[],
4470            });
4471            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
4472
4473            let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
4474                label: Some("p010 yuv transform"),
4475                size: std::mem::size_of::<YuvTransformRaw>() as u64,
4476                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4477                mapped_at_creation: false,
4478            });
4479            self.queue
4480                .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
4481
4482            let binds = self.create_nv12_binds(&view_y, &view_uv, &yuv_buf, "p010 bind");
4483
4484            let bytes = checked_image_bytes(w, h, 2)?
4485                .checked_add(checked_image_bytes(uv_w, uv_h, 4)?)
4486                .and_then(|bytes| bytes.checked_add(std::mem::size_of::<YuvTransformRaw>() as u64))
4487                .ok_or_else(|| anyhow::anyhow!("P010 image byte size overflow"))?;
4488            self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
4489
4490            self.images.insert(
4491                handle,
4492                ImageTex::Nv12 {
4493                    tex_y,
4494                    tex_uv,
4495                    binds,
4496                    yuv_buf,
4497                    w,
4498                    h,
4499                    pixel_format: PixelFormat::P010,
4500                    fourcc: Some(P010_FOURCC),
4501                    color_info,
4502                    external: false,
4503                    last_used_frame: self.frame_index,
4504                    bytes,
4505                },
4506            );
4507        } else {
4508            if let Some(ImageTex::Nv12 {
4509                yuv_buf,
4510                color_info: stored_color,
4511                last_used_frame,
4512                ..
4513            }) = self.images.get_mut(&handle)
4514            {
4515                *stored_color = color_info;
4516                *last_used_frame = self.frame_index;
4517                self.queue
4518                    .write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
4519            }
4520        }
4521
4522        let (tex_y, tex_uv, _binds) = match self.images.get(&handle) {
4523            Some(ImageTex::Nv12 {
4524                tex_y,
4525                tex_uv,
4526                binds,
4527                ..
4528            }) => (tex_y, tex_uv, binds),
4529            _ => return Err(anyhow::anyhow!("Handle is not P010/NV12")),
4530        };
4531
4532        self.queue.write_texture(
4533            wgpu::TexelCopyTextureInfo {
4534                texture: tex_y,
4535                mip_level: 0,
4536                origin: wgpu::Origin3d::ZERO,
4537                aspect: wgpu::TextureAspect::All,
4538            },
4539            &y[..y_expected],
4540            wgpu::TexelCopyBufferLayout {
4541                offset: 0,
4542                bytes_per_row: Some(
4543                    w.checked_mul(2)
4544                        .ok_or_else(|| anyhow::anyhow!("P010 row size overflow"))?,
4545                ),
4546                rows_per_image: Some(h),
4547            },
4548            wgpu::Extent3d {
4549                width: w,
4550                height: h,
4551                depth_or_array_layers: 1,
4552            },
4553        );
4554        self.queue.write_texture(
4555            wgpu::TexelCopyTextureInfo {
4556                texture: tex_uv,
4557                mip_level: 0,
4558                origin: wgpu::Origin3d::ZERO,
4559                aspect: wgpu::TextureAspect::All,
4560            },
4561            &uv[..uv_expected],
4562            wgpu::TexelCopyBufferLayout {
4563                offset: 0,
4564                bytes_per_row: Some(
4565                    uv_w.checked_mul(4)
4566                        .ok_or_else(|| anyhow::anyhow!("P010 UV row size overflow"))?,
4567                ),
4568                rows_per_image: Some(uv_h),
4569            },
4570            wgpu::Extent3d {
4571                width: uv_w,
4572                height: uv_h,
4573                depth_or_array_layers: 1,
4574            },
4575        );
4576
4577        self.evict_budget_excess();
4578        Ok(())
4579    }
4580
4581    #[cfg(target_os = "linux")]
4582    pub fn set_image_dmabuf(
4583        &mut self,
4584        handle: u64,
4585        w: u32,
4586        h: u32,
4587        fds: Vec<std::os::unix::io::OwnedFd>,
4588        modifier: u64,
4589        strides: Vec<u32>,
4590        offsets: Vec<u64>,
4591        color_info: ColorInfo,
4592    ) -> anyhow::Result<()> {
4593        self.set_image_dmabuf_fourcc(
4594            handle,
4595            w,
4596            h,
4597            fds,
4598            NV12_FOURCC,
4599            modifier,
4600            strides,
4601            offsets,
4602            color_info,
4603        )
4604    }
4605
4606    #[cfg(target_os = "linux")]
4607    pub fn set_image_dmabuf_fourcc(
4608        &mut self,
4609        handle: u64,
4610        w: u32,
4611        h: u32,
4612        fds: Vec<std::os::unix::io::OwnedFd>,
4613        fourcc: u32,
4614        modifier: u64,
4615        strides: Vec<u32>,
4616        offsets: Vec<u64>,
4617        color_info: ColorInfo,
4618    ) -> anyhow::Result<()> {
4619        validate_image_handle(handle)?;
4620        checked_image_bytes(w, h, 1)?;
4621        if w == 0
4622            || h == 0
4623            || w > self.device.limits().max_texture_dimension_2d
4624            || h > self.device.limits().max_texture_dimension_2d
4625        {
4626            anyhow::bail!("DMA-BUF dimensions are outside the device texture limit");
4627        }
4628        if modifier == u64::MAX {
4629            anyhow::bail!("DMA-BUF modifier is invalid");
4630        }
4631        if fds.len() != 2 || strides.len() != 2 || offsets.len() != 2 {
4632            anyhow::bail!(
4633                "DMA-BUF import requires two fds, strides, and offsets (got {}, {}, {})",
4634                fds.len(),
4635                strides.len(),
4636                offsets.len()
4637            );
4638        }
4639        let pixel_format = match fourcc {
4640            0x3231_564e | 0x4e56_3132 => PixelFormat::Nv12,
4641            0x3031_3050 | 0x5030_3130 => PixelFormat::P010,
4642            _ => anyhow::bail!("unsupported DMA-BUF fourcc 0x{fourcc:08x}"),
4643        };
4644        if !self
4645            .device
4646            .features()
4647            .contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF)
4648        {
4649            anyhow::bail!("DMA-BUF import requires VULKAN_EXTERNAL_MEMORY_DMA_BUF");
4650        }
4651        if pixel_format == PixelFormat::P010
4652            && !self
4653                .device
4654                .features()
4655                .contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
4656        {
4657            anyhow::bail!("P010 DMA-BUF requires TEXTURE_FORMAT_16BIT_NORM");
4658        }
4659        let bytes_per_pixel = if pixel_format == PixelFormat::P010 {
4660            2
4661        } else {
4662            1
4663        };
4664        let uv_w = w.div_ceil(2);
4665        let uv_h = h.div_ceil(2);
4666        let y_file_size =
4667            validate_dmabuf_plane(&fds[0], offsets[0], strides[0], w, h, bytes_per_pixel)?;
4668        let uv_file_size = validate_dmabuf_plane(
4669            &fds[1],
4670            offsets[1],
4671            strides[1],
4672            uv_w,
4673            uv_h,
4674            bytes_per_pixel * 2,
4675        )?;
4676        let yuv_raw = make_yuv_transform_raw(color_info, pixel_format);
4677        let (y_format, uv_format) = if pixel_format == PixelFormat::P010 {
4678            (
4679                wgpu::TextureFormat::R16Unorm,
4680                wgpu::TextureFormat::Rg16Unorm,
4681            )
4682        } else {
4683            (wgpu::TextureFormat::R8Unorm, wgpu::TextureFormat::Rg8Unorm)
4684        };
4685        let hal_y_desc = wgpu::hal::TextureDescriptor {
4686            label: Some("dmabuf y"),
4687            size: wgpu::Extent3d {
4688                width: w,
4689                height: h,
4690                depth_or_array_layers: 1,
4691            },
4692            mip_level_count: 1,
4693            sample_count: 1,
4694            dimension: wgpu::TextureDimension::D2,
4695            format: y_format,
4696            usage: wgpu::wgt::TextureUses::RESOURCE,
4697            memory_flags: wgpu::hal::MemoryFlags::empty(),
4698            view_formats: vec![],
4699        };
4700        let hal_uv_desc = wgpu::hal::TextureDescriptor {
4701            label: Some("dmabuf uv"),
4702            size: wgpu::Extent3d {
4703                width: uv_w,
4704                height: uv_h,
4705                depth_or_array_layers: 1,
4706            },
4707            mip_level_count: 1,
4708            sample_count: 1,
4709            dimension: wgpu::TextureDimension::D2,
4710            format: uv_format,
4711            usage: wgpu::wgt::TextureUses::RESOURCE,
4712            memory_flags: wgpu::hal::MemoryFlags::empty(),
4713            view_formats: vec![],
4714        };
4715        let wgpu_y_desc = wgpu::TextureDescriptor {
4716            label: Some("dmabuf y"),
4717            size: hal_y_desc.size,
4718            mip_level_count: 1,
4719            sample_count: 1,
4720            dimension: wgpu::TextureDimension::D2,
4721            format: y_format,
4722            usage: wgpu::TextureUsages::TEXTURE_BINDING,
4723            view_formats: &[],
4724        };
4725        let wgpu_uv_desc = wgpu::TextureDescriptor {
4726            label: Some("dmabuf uv"),
4727            size: hal_uv_desc.size,
4728            mip_level_count: 1,
4729            sample_count: 1,
4730            dimension: wgpu::TextureDimension::D2,
4731            format: uv_format,
4732            usage: wgpu::TextureUsages::TEXTURE_BINDING,
4733            view_formats: &[],
4734        };
4735
4736        let (tex_y, view_y, tex_uv, view_uv) = unsafe {
4737            let hal_guard = self
4738                .device
4739                .as_hal::<wgpu::hal::vulkan::Api>()
4740                .ok_or_else(|| anyhow::anyhow!("device is not Vulkan"))?;
4741            let mut fds = fds.into_iter();
4742            let y_fd = fds.next().expect("validated fd count");
4743            let uv_fd = fds.next().expect("validated fd count");
4744            let y_texture = hal_guard.texture_from_dmabuf_fd(
4745                y_fd,
4746                &hal_y_desc,
4747                modifier,
4748                strides[0] as u64,
4749                offsets[0],
4750            );
4751            let y_texture =
4752                y_texture.map_err(|error| anyhow::anyhow!("import Y DMA-BUF: {error:?}"))?;
4753            let uv_texture = hal_guard.texture_from_dmabuf_fd(
4754                uv_fd,
4755                &hal_uv_desc,
4756                modifier,
4757                strides[1] as u64,
4758                offsets[1],
4759            );
4760            let uv_texture =
4761                uv_texture.map_err(|error| anyhow::anyhow!("import UV DMA-BUF: {error:?}"))?;
4762            drop(hal_guard);
4763            let tex_y = self
4764                .device
4765                .create_texture_from_hal::<wgpu::hal::vulkan::Api>(
4766                    y_texture,
4767                    &wgpu_y_desc,
4768                    wgpu::wgt::TextureUses::UNINITIALIZED,
4769                );
4770            let tex_uv = self
4771                .device
4772                .create_texture_from_hal::<wgpu::hal::vulkan::Api>(
4773                    uv_texture,
4774                    &wgpu_uv_desc,
4775                    wgpu::wgt::TextureUses::UNINITIALIZED,
4776                );
4777            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
4778            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
4779            (tex_y, view_y, tex_uv, view_uv)
4780        };
4781
4782        let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
4783            label: Some("dmabuf yuv transform"),
4784            size: std::mem::size_of::<YuvTransformRaw>() as u64,
4785            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4786            mapped_at_creation: false,
4787        });
4788        self.queue
4789            .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
4790        let binds = self.create_nv12_binds(&view_y, &view_uv, &yuv_buf, "dmabuf yuv bind");
4791        let bytes = y_file_size
4792            .checked_add(uv_file_size)
4793            .and_then(|value| value.checked_add(std::mem::size_of::<YuvTransformRaw>() as u64))
4794            .ok_or_else(|| anyhow::anyhow!("DMA-BUF byte size overflow"))?;
4795        self.evict_budget_excess();
4796        let replaced_bytes = self
4797            .images
4798            .get(&handle)
4799            .map(|image| match image {
4800                ImageTex::Rgba { bytes, .. }
4801                | ImageTex::Nv12 { bytes, .. }
4802                | ImageTex::User { bytes, .. } => *bytes,
4803            })
4804            .unwrap_or(0);
4805        let projected = self
4806            .image_bytes_total
4807            .saturating_sub(replaced_bytes)
4808            .checked_add(bytes)
4809            .ok_or_else(|| anyhow::anyhow!("DMA-BUF image budget overflow"))?;
4810        if projected > self.image_budget_bytes {
4811            anyhow::bail!("DMA-BUF image budget exceeded");
4812        }
4813        self.remove_image(handle);
4814        self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
4815        self.images.insert(
4816            handle,
4817            ImageTex::Nv12 {
4818                tex_y,
4819                tex_uv,
4820                binds,
4821                yuv_buf,
4822                w,
4823                h,
4824                pixel_format,
4825                fourcc: Some(canonical_fourcc(pixel_format)),
4826                color_info,
4827                external: true,
4828                last_used_frame: self.frame_index,
4829                bytes,
4830            },
4831        );
4832        self.evict_budget_excess();
4833        Ok(())
4834    }
4835
4836    pub fn image_fourcc(&self, handle: u64) -> Option<u32> {
4837        match self.images.get(&handle) {
4838            Some(ImageTex::Nv12 { fourcc, .. }) => *fourcc,
4839            _ => None,
4840        }
4841    }
4842
4843    pub fn remove_image(&mut self, handle: u64) {
4844        if let Some(img) = self.images.remove(&handle) {
4845            let b = match &img {
4846                ImageTex::Rgba { bytes, .. } => *bytes,
4847                ImageTex::Nv12 { bytes, .. } => *bytes,
4848                ImageTex::User { bytes, .. } => *bytes,
4849            };
4850            self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
4851        }
4852        if let Some(retained) = self.retained.remove(&handle) {
4853            self.retained_bytes_total = self
4854                .retained_bytes_total
4855                .saturating_sub(retained.rgba.len() as u64);
4856        }
4857    }
4858
4859    fn evict_image_gpu(&mut self, handle: u64) -> u64 {
4860        let Some(img) = self.images.remove(&handle) else {
4861            return 0;
4862        };
4863        let b = match &img {
4864            ImageTex::Rgba { bytes, .. } => *bytes,
4865            ImageTex::Nv12 { bytes, .. } => *bytes,
4866            ImageTex::User { bytes, .. } => *bytes,
4867        };
4868        self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
4869        b
4870    }
4871
4872    fn revive_retained_image(&mut self, handle: u64) -> anyhow::Result<bool> {
4873        if self.images.contains_key(&handle) {
4874            return Ok(true);
4875        }
4876        let Some(r) = self.retained.get_mut(&handle) else {
4877            return Ok(false);
4878        };
4879        r.last_used_frame = self.frame_index;
4880        let r = r.clone();
4881        let (tex, binds) = self.create_rgba_tex(r.w, r.h, r.format);
4882
4883        self.queue.write_texture(
4884            wgpu::TexelCopyTextureInfo {
4885                texture: &tex,
4886                mip_level: 0,
4887                origin: wgpu::Origin3d::ZERO,
4888                aspect: wgpu::TextureAspect::All,
4889            },
4890            &r.rgba,
4891            wgpu::TexelCopyBufferLayout {
4892                offset: 0,
4893                bytes_per_row: Some(4 * r.w),
4894                rows_per_image: Some(r.h),
4895            },
4896            wgpu::Extent3d {
4897                width: r.w,
4898                height: r.h,
4899                depth_or_array_layers: 1,
4900            },
4901        );
4902
4903        let bytes = checked_image_bytes(r.w, r.h, 4)?;
4904        self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
4905        self.images.insert(
4906            handle,
4907            ImageTex::Rgba {
4908                tex,
4909                binds,
4910                w: r.w,
4911                h: r.h,
4912                format: r.format,
4913                last_used_frame: self.frame_index,
4914                bytes,
4915            },
4916        );
4917        Ok(true)
4918    }
4919
4920    fn resolve_image_for_draw(&mut self, handle: u64) -> Option<(u32, u32, bool)> {
4921        if let Some(retained) = self.retained.get_mut(&handle) {
4922            retained.last_used_frame = self.frame_index;
4923        }
4924        if let Some(t) = self.images.get_mut(&handle) {
4925            return match t {
4926                ImageTex::Rgba {
4927                    w,
4928                    h,
4929                    last_used_frame,
4930                    ..
4931                } => {
4932                    *last_used_frame = self.frame_index;
4933                    Some((*w, *h, false))
4934                }
4935                ImageTex::User {
4936                    w,
4937                    h,
4938                    last_used_frame,
4939                    ..
4940                } => {
4941                    *last_used_frame = self.frame_index;
4942                    Some((*w, *h, false))
4943                }
4944                ImageTex::Nv12 {
4945                    w,
4946                    h,
4947                    last_used_frame,
4948                    ..
4949                } => {
4950                    *last_used_frame = self.frame_index;
4951                    Some((*w, *h, true))
4952                }
4953            };
4954        }
4955        if self.revive_retained_image(handle).unwrap_or(false)
4956            && let Some(ImageTex::Rgba {
4957                w,
4958                h,
4959                last_used_frame,
4960                ..
4961            }) = self.images.get_mut(&handle)
4962        {
4963            *last_used_frame = self.frame_index;
4964            return Some((*w, *h, false));
4965        }
4966        None
4967    }
4968
4969    // Legacy support from Step 1 instructions (temporary until platform render logic is fully swapped)
4970    pub fn register_image_from_bytes(&mut self, data: &[u8], srgb: bool) -> u64 {
4971        let handle = self.next_image_handle;
4972        self.next_image_handle += 1;
4973        if let Err(e) = self.set_image_from_bytes(handle, data, srgb) {
4974            log::error!("Failed to register image: {e}");
4975        }
4976        handle
4977    }
4978
4979    /// Register raw RGBA8 pixels (`w * h * 4` bytes) as an image, returning
4980    /// its handle for `SceneNode::Image`. Used by CPU-rasterized overlays
4981    /// (e.g. subtitle bitmap layers) that have no encoded image bytes.
4982    /// Pass `srgb = true` for sRGB overlays composited over video.
4983    pub fn register_image_rgba8(&mut self, w: u32, h: u32, rgba: &[u8], srgb: bool) -> u64 {
4984        let handle = self.next_image_handle;
4985        self.next_image_handle += 1;
4986        if let Err(e) = self.set_image_rgba8(handle, w, h, rgba, srgb) {
4987            log::error!("Failed to register image: {e}");
4988        }
4989        handle
4990    }
4991
4992    /// Register an 8-bit coverage tile (`w * h` bytes, 0 = empty, 255 =
4993    /// fully covered) for `SceneNode::Coverage`, returning its handle.
4994    /// Coverage tiles are immutable: re-register on geometry change and
4995    /// `remove_coverage` handles you no longer emit (stale tiles also age
4996    /// out under the image eviction policy).
4997    pub fn register_coverage_a8(&mut self, w: u32, h: u32, coverage: &[u8]) -> u64 {
4998        if validate_texture_dimensions(&self.device, w, h).is_err() {
4999            log::error!("Coverage dimensions are outside the device limit");
5000            return 0;
5001        }
5002        let Ok(expected_bytes) = checked_image_bytes(w, h, 1) else {
5003            log::error!("Coverage dimensions overflow");
5004            return 0;
5005        };
5006        let Ok(expected) = usize::try_from(expected_bytes) else {
5007            log::error!("Coverage dimensions exceed addressable memory");
5008            return 0;
5009        };
5010        if coverage.len() < expected {
5011            log::error!("Coverage buffer too small: {} < {expected}", coverage.len());
5012            return 0;
5013        }
5014        let handle = self.next_coverage_handle;
5015        self.next_coverage_handle += 1;
5016
5017        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5018            label: Some("coverage tile a8"),
5019            size: wgpu::Extent3d {
5020                width: w,
5021                height: h,
5022                depth_or_array_layers: 1,
5023            },
5024            mip_level_count: 1,
5025            sample_count: 1,
5026            dimension: wgpu::TextureDimension::D2,
5027            format: wgpu::TextureFormat::R8Unorm,
5028            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
5029            view_formats: &[],
5030        });
5031        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5032        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5033            label: Some("coverage bind a8"),
5034            layout: &self.image_bind_layout_rgba,
5035            entries: &[
5036                wgpu::BindGroupEntry {
5037                    binding: 0,
5038                    resource: wgpu::BindingResource::TextureView(&view),
5039                },
5040                wgpu::BindGroupEntry {
5041                    binding: 1,
5042                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
5043                },
5044            ],
5045        });
5046        self.queue.write_texture(
5047            wgpu::TexelCopyTextureInfo {
5048                texture: &tex,
5049                mip_level: 0,
5050                origin: wgpu::Origin3d::ZERO,
5051                aspect: wgpu::TextureAspect::All,
5052            },
5053            &coverage[..expected],
5054            wgpu::TexelCopyBufferLayout {
5055                offset: 0,
5056                bytes_per_row: Some(w),
5057                rows_per_image: Some(h),
5058            },
5059            wgpu::Extent3d {
5060                width: w,
5061                height: h,
5062                depth_or_array_layers: 1,
5063            },
5064        );
5065        let bytes = checked_image_bytes(w, h, 1).unwrap_or(u64::MAX);
5066        self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
5067        self.coverages.insert(
5068            handle,
5069            CoverageTex {
5070                tex,
5071                bind,
5072                w,
5073                h,
5074                last_used_frame: self.frame_index,
5075                bytes,
5076            },
5077        );
5078        self.evict_budget_excess();
5079        handle
5080    }
5081
5082    /// Remove a coverage tile registered with [`register_coverage_a8`](Self::register_coverage_a8).
5083    pub fn remove_coverage(&mut self, handle: u64) {
5084        if let Some(tile) = self.coverages.remove(&handle) {
5085            self.image_bytes_total = self.image_bytes_total.saturating_sub(tile.bytes);
5086        }
5087    }
5088
5089    /// Tile dimensions, marking the handle used (keeps it alive under the
5090    /// eviction policy). Returns `None` for unknown handles.
5091    pub fn coverage_dimensions(&mut self, handle: u64) -> Option<(u32, u32)> {
5092        if let Some(tile) = self.coverages.get_mut(&handle) {
5093            tile.last_used_frame = self.frame_index;
5094            return Some((tile.w, tile.h));
5095        }
5096        None
5097    }
5098
5099    fn evict_retained_excess(&mut self) {
5100        let mut candidates: Vec<(u64, u64, u64)> = self
5101            .retained
5102            .iter()
5103            .map(|(handle, image)| (*handle, image.last_used_frame, image.rgba.len() as u64))
5104            .collect();
5105        candidates.sort_by_key(|candidate| candidate.1);
5106        for (handle, _last_used, bytes) in candidates {
5107            if self.retained.len() <= MAX_RETAINED_IMAGES
5108                && self.retained_bytes_total <= MAX_RETAINED_IMAGE_BYTES
5109            {
5110                break;
5111            }
5112            if let Some(image) = self.retained.remove(&handle) {
5113                self.retained_bytes_total = self
5114                    .retained_bytes_total
5115                    .saturating_sub(bytes.min(image.rgba.len() as u64));
5116            }
5117        }
5118    }
5119
5120    fn evict_unused_images(&mut self) {
5121        let now = self.frame_index;
5122        let evict_after = self.image_evict_after_frames;
5123
5124        // Time based eviction. Eviction only frees GPU memory: retained RGBA
5125        // sources stay so the image can be lazily re-uploaded when drawn again.
5126        let mut to_evict = Vec::new();
5127        for (h, t) in self.images.iter() {
5128            if !t.evictable() {
5129                continue;
5130            }
5131            let last = match t {
5132                ImageTex::Rgba {
5133                    last_used_frame, ..
5134                } => *last_used_frame,
5135                ImageTex::User {
5136                    last_used_frame, ..
5137                } => *last_used_frame,
5138                ImageTex::Nv12 {
5139                    last_used_frame, ..
5140                } => *last_used_frame,
5141            };
5142            if now.saturating_sub(last) > evict_after {
5143                to_evict.push(*h);
5144            }
5145        }
5146        for h in to_evict {
5147            if self.retained.contains_key(&h) {
5148                self.evict_image_gpu(h);
5149            } else {
5150                self.remove_image(h);
5151            }
5152        }
5153
5154        // Coverage tiles have no retained CPU copies: age-out removes them.
5155        let mut stale = Vec::new();
5156        for (h, t) in self.coverages.iter() {
5157            if now.saturating_sub(t.last_used_frame) > evict_after {
5158                stale.push(*h);
5159            }
5160        }
5161        for h in stale {
5162            self.remove_coverage(h);
5163        }
5164
5165        self.evict_budget_excess();
5166    }
5167
5168    fn next_cache_touch(&mut self) -> u64 {
5169        let touch = self.cache_touch_clock;
5170        self.cache_touch_clock = self.cache_touch_clock.wrapping_add(1);
5171        touch
5172    }
5173
5174    fn gpu_bytes_total(&self) -> u64 {
5175        self.image_bytes_total
5176            .saturating_add(self.layer_bytes_total)
5177            .saturating_add(self.blend_snapshot_bytes_total)
5178            .saturating_add(self.mesh_cache_bytes_total)
5179            .saturating_add(self.working_space_bytes)
5180            .saturating_add(self.surface_resolve_bytes)
5181            .saturating_add(self.msaa_bytes)
5182            .saturating_add(self.ws_msaa_bytes)
5183            .saturating_add(self.depth_stencil_bytes)
5184    }
5185
5186    fn budget_exceeded(&self) -> bool {
5187        self.image_bytes_total > self.image_budget_bytes
5188            || self.gpu_bytes_total() > self.gpu_budget_bytes
5189    }
5190
5191    fn evict_budget_excess(&mut self) {
5192        let image_over_budget = self.image_bytes_total > self.image_budget_bytes;
5193        let gpu_over_budget = self.gpu_bytes_total() > self.gpu_budget_bytes;
5194        if !image_over_budget && !gpu_over_budget {
5195            return;
5196        }
5197        let mut coverage_candidates: Vec<(u64, u64)> = self
5198            .coverages
5199            .iter()
5200            .map(|(handle, tile)| (*handle, tile.last_used_frame))
5201            .collect();
5202        coverage_candidates.sort_by_key(|candidate| candidate.1);
5203        let now = self.frame_index;
5204        for (handle, last_used) in coverage_candidates {
5205            if !self.budget_exceeded() {
5206                break;
5207            }
5208            if last_used == now {
5209                continue;
5210            }
5211            self.remove_coverage(handle);
5212        }
5213        if !self.budget_exceeded() {
5214            return;
5215        }
5216        // Collect (handle, last_used, bytes)
5217        let mut candidates: Vec<(u64, u64, u64)> = self
5218            .images
5219            .iter()
5220            .filter_map(|(h, t)| {
5221                if !t.evictable() {
5222                    return None;
5223                }
5224                let (last, bytes) = match t {
5225                    ImageTex::Rgba {
5226                        last_used_frame,
5227                        bytes,
5228                        ..
5229                    } => (*last_used_frame, *bytes),
5230                    ImageTex::User {
5231                        last_used_frame,
5232                        bytes,
5233                        ..
5234                    } => (*last_used_frame, *bytes),
5235                    ImageTex::Nv12 {
5236                        last_used_frame,
5237                        bytes,
5238                        ..
5239                    } => (*last_used_frame, *bytes),
5240                };
5241                Some((*h, last, bytes))
5242            })
5243            .collect();
5244
5245        // Sort by last_used ascending (LRU first)
5246        candidates.sort_by_key(|k| k.1);
5247
5248        for (h, last, _bytes) in candidates {
5249            if !self.budget_exceeded() {
5250                break;
5251            }
5252            if last == now {
5253                continue;
5254            }
5255            if self.retained.contains_key(&h) {
5256                self.evict_image_gpu(h);
5257            } else {
5258                self.remove_image(h);
5259            }
5260        }
5261        while self.gpu_bytes_total() > self.gpu_budget_bytes {
5262            if !self.evict_mesh_cache() {
5263                break;
5264            }
5265        }
5266        if self.gpu_bytes_total() > self.gpu_budget_bytes {
5267            log::warn!(
5268                "renderer GPU resource budget exceeded: {} > {}",
5269                self.gpu_bytes_total(),
5270                self.gpu_budget_bytes
5271            );
5272        }
5273    }
5274
5275    /// Set pixels per point (DPI scale) for callback `ScreenDescriptor` / `PaintCallbackInfo`.
5276    pub fn set_pixels_per_point(&mut self, ppp: f32) {
5277        if ppp.is_finite() {
5278            self.pixels_per_point = ppp.clamp(0.5, 8.0);
5279        }
5280    }
5281
5282    /// Enable or disable linear working-space rendering.
5283    /// When enabled, the scene is rendered into an Rgba16Float intermediate
5284    /// and a final full-screen pass applies the display OETF.
5285    pub fn set_working_space(&mut self, enabled: bool) {
5286        if enabled == self.working_space {
5287            return;
5288        }
5289        self.working_space = enabled;
5290        if enabled {
5291            self.ensure_display_pipeline();
5292            self.recreate_msaa_and_depth_stencil();
5293            self.recreate_working_space_texture();
5294        } else {
5295            self.ws_tex = None;
5296            self.ws_view = None;
5297            self.ws_bind = None;
5298            self.ws_msaa_tex = None;
5299            self.ws_msaa_view = None;
5300            self.surface_resolve_tex = None;
5301            self.surface_resolve_view = None;
5302            self.surface_resolve_bytes = 0;
5303            self.working_space_bytes = 0;
5304            self.recreate_msaa_and_depth_stencil();
5305        }
5306        self.evict_budget_excess();
5307    }
5308
5309    fn ensure_display_pipeline(&mut self) {
5310        if self.display_pipeline.is_some() {
5311            return;
5312        }
5313
5314        let layout = self
5315            .device
5316            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
5317                label: Some("display transform layout"),
5318                entries: &[
5319                    wgpu::BindGroupLayoutEntry {
5320                        binding: 0,
5321                        visibility: wgpu::ShaderStages::FRAGMENT,
5322                        ty: wgpu::BindingType::Texture {
5323                            multisampled: false,
5324                            view_dimension: wgpu::TextureViewDimension::D2,
5325                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
5326                        },
5327                        count: None,
5328                    },
5329                    wgpu::BindGroupLayoutEntry {
5330                        binding: 1,
5331                        visibility: wgpu::ShaderStages::FRAGMENT,
5332                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
5333                        count: None,
5334                    },
5335                ],
5336            });
5337        self.display_layout = Some(layout);
5338
5339        let display_source: Cow<'static, str> = if self.output_format.is_srgb() {
5340            Cow::Borrowed(include_str!("shaders/display_transform_passthrough.wgsl"))
5341        } else {
5342            Cow::Borrowed(include_str!("shaders/display_transform.wgsl"))
5343        };
5344        let shader = self
5345            .device
5346            .create_shader_module(wgpu::ShaderModuleDescriptor {
5347                label: Some("display transform"),
5348                source: wgpu::ShaderSource::Wgsl(display_source),
5349            });
5350
5351        let pipeline_layout = self
5352            .device
5353            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
5354                label: Some("display transform pipeline layout"),
5355                bind_group_layouts: &[None, self.display_layout.as_ref()],
5356                immediate_size: 0,
5357            });
5358
5359        let pipeline = self
5360            .device
5361            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
5362                label: Some("display transform pipeline"),
5363                layout: Some(&pipeline_layout),
5364                vertex: wgpu::VertexState {
5365                    module: &shader,
5366                    entry_point: Some("vs_main"),
5367                    buffers: &[],
5368                    compilation_options: wgpu::PipelineCompilationOptions::default(),
5369                },
5370                fragment: Some(wgpu::FragmentState {
5371                    module: &shader,
5372                    entry_point: Some("fs_main"),
5373                    targets: &[Some(wgpu::ColorTargetState {
5374                        format: self.output_format,
5375                        blend: None,
5376                        write_mask: wgpu::ColorWrites::ALL,
5377                    })],
5378                    compilation_options: wgpu::PipelineCompilationOptions::default(),
5379                }),
5380                primitive: wgpu::PrimitiveState::default(),
5381                depth_stencil: None,
5382                multisample: wgpu::MultisampleState::default(),
5383                multiview_mask: None,
5384                cache: None,
5385            });
5386        self.display_pipeline = Some(pipeline);
5387    }
5388
5389    /// Resize the render target dimensions.
5390    ///
5391    /// Recreates MSAA, depth-stencil, and working-space textures to match the
5392    /// new size..
5393    pub fn resize(&mut self, width: u32, height: u32) {
5394        let max = self.device.limits().max_texture_dimension_2d;
5395        let width = if width == 0 { 0 } else { width.min(max) };
5396        let height = if height == 0 { 0 } else { height.min(max) };
5397        if self.output_width == width && self.output_height == height {
5398            return;
5399        }
5400        self.output_width = width;
5401        self.output_height = height;
5402        if width == 0 || height == 0 {
5403            self.msaa_tex = None;
5404            self.msaa_view = None;
5405            self.ws_msaa_tex = None;
5406            self.ws_msaa_view = None;
5407            self.surface_resolve_tex = None;
5408            self.surface_resolve_view = None;
5409            self.surface_resolve_bytes = 0;
5410            self.msaa_bytes = 0;
5411            self.ws_msaa_bytes = 0;
5412            self.depth_stencil_bytes = 0;
5413            self.working_space_bytes = 0;
5414            self.ws_tex = None;
5415            self.ws_view = None;
5416            self.ws_bind = None;
5417            self.evict_budget_excess();
5418            return;
5419        }
5420        self.recreate_msaa_and_depth_stencil();
5421        self.recreate_working_space_texture();
5422        self.evict_budget_excess();
5423    }
5424
5425    fn recreate_working_space_texture(&mut self) {
5426        if !self.working_space || self.output_width == 0 || self.output_height == 0 {
5427            return;
5428        }
5429        self.working_space_bytes = 0;
5430        let w = self
5431            .output_width
5432            .clamp(1, self.device.limits().max_texture_dimension_2d);
5433        let h = self
5434            .output_height
5435            .clamp(1, self.device.limits().max_texture_dimension_2d);
5436
5437        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5438            label: Some("working space"),
5439            size: wgpu::Extent3d {
5440                width: w,
5441                height: h,
5442                depth_or_array_layers: 1,
5443            },
5444            mip_level_count: 1,
5445            sample_count: 1,
5446            dimension: wgpu::TextureDimension::D2,
5447            format: wgpu::TextureFormat::Rgba16Float,
5448            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
5449                | wgpu::TextureUsages::TEXTURE_BINDING
5450                | wgpu::TextureUsages::COPY_SRC
5451                | wgpu::TextureUsages::COPY_DST,
5452            view_formats: &[],
5453        });
5454        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5455
5456        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5457            label: Some("working space bind"),
5458            layout: self.display_layout.as_ref().unwrap(),
5459            entries: &[
5460                wgpu::BindGroupEntry {
5461                    binding: 0,
5462                    resource: wgpu::BindingResource::TextureView(&view),
5463                },
5464                wgpu::BindGroupEntry {
5465                    binding: 1,
5466                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
5467                },
5468            ],
5469        });
5470
5471        self.working_space_bytes =
5472            texture_storage_bytes(wgpu::TextureFormat::Rgba16Float, w, h).unwrap_or(0);
5473        self.ws_tex = Some(tex);
5474        self.ws_view = Some(view);
5475        self.ws_bind = Some(bind);
5476    }
5477
5478    fn recreate_msaa_and_depth_stencil(&mut self) {
5479        if self.output_width == 0 || self.output_height == 0 {
5480            self.msaa_tex = None;
5481            self.msaa_view = None;
5482            self.ws_msaa_tex = None;
5483            self.ws_msaa_view = None;
5484            self.surface_resolve_tex = None;
5485            self.surface_resolve_view = None;
5486            self.surface_resolve_bytes = 0;
5487            self.msaa_bytes = 0;
5488            self.ws_msaa_bytes = 0;
5489            self.depth_stencil_bytes = 0;
5490            return;
5491        }
5492        self.msaa_bytes = 0;
5493        self.ws_msaa_bytes = 0;
5494        self.depth_stencil_bytes = 0;
5495        if self.msaa_samples > 1 && !self.working_space {
5496            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5497                label: Some("msaa color"),
5498                size: wgpu::Extent3d {
5499                    width: self.output_width.max(1),
5500                    height: self.output_height.max(1),
5501                    depth_or_array_layers: 1,
5502                },
5503                mip_level_count: 1,
5504                sample_count: self.msaa_samples,
5505                dimension: wgpu::TextureDimension::D2,
5506                format: self.output_format,
5507                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
5508                view_formats: &[],
5509            });
5510            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5511            self.msaa_bytes = texture_storage_bytes_with_samples(
5512                self.output_format,
5513                self.output_width.max(1),
5514                self.output_height.max(1),
5515                self.msaa_samples,
5516            )
5517            .unwrap_or(0);
5518            self.msaa_tex = Some(tex);
5519            self.msaa_view = Some(view);
5520        } else {
5521            self.msaa_tex = None;
5522            self.msaa_view = None;
5523        }
5524        if self.working_space && self.working_space_msaa_samples > 1 {
5525            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5526                label: Some("working space msaa color"),
5527                size: wgpu::Extent3d {
5528                    width: self.output_width.max(1),
5529                    height: self.output_height.max(1),
5530                    depth_or_array_layers: 1,
5531                },
5532                mip_level_count: 1,
5533                sample_count: self.working_space_msaa_samples,
5534                dimension: wgpu::TextureDimension::D2,
5535                format: wgpu::TextureFormat::Rgba16Float,
5536                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
5537                view_formats: &[],
5538            });
5539            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5540            self.ws_msaa_bytes = texture_storage_bytes_with_samples(
5541                wgpu::TextureFormat::Rgba16Float,
5542                self.output_width.max(1),
5543                self.output_height.max(1),
5544                self.working_space_msaa_samples,
5545            )
5546            .unwrap_or(0);
5547            self.ws_msaa_tex = Some(tex);
5548            self.ws_msaa_view = Some(view);
5549        } else {
5550            self.ws_msaa_tex = None;
5551            self.ws_msaa_view = None;
5552        }
5553        self.surface_resolve_tex = None;
5554        self.surface_resolve_view = None;
5555        self.surface_resolve_bytes = 0;
5556        if self.msaa_samples > 1 && !self.working_space {
5557            let width = self.output_width.max(1);
5558            let height = self.output_height.max(1);
5559            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5560                label: Some("surface resolve"),
5561                size: wgpu::Extent3d {
5562                    width,
5563                    height,
5564                    depth_or_array_layers: 1,
5565                },
5566                mip_level_count: 1,
5567                sample_count: 1,
5568                dimension: wgpu::TextureDimension::D2,
5569                format: self.output_format,
5570                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
5571                    | wgpu::TextureUsages::TEXTURE_BINDING
5572                    | wgpu::TextureUsages::COPY_SRC,
5573                view_formats: &[],
5574            });
5575            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5576            self.surface_resolve_bytes =
5577                texture_storage_bytes(self.output_format, width, height).unwrap_or(0);
5578            self.surface_resolve_tex = Some(tex);
5579            self.surface_resolve_view = Some(view);
5580        }
5581
5582        self.depth_stencil_bytes = texture_storage_bytes_with_samples(
5583            wgpu::TextureFormat::Depth24PlusStencil8,
5584            self.output_width.max(1),
5585            self.output_height.max(1),
5586            self.active_surface_msaa_samples(),
5587        )
5588        .unwrap_or(0);
5589        self.depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
5590            label: Some("depth-stencil (stencil clips)"),
5591            size: wgpu::Extent3d {
5592                width: self.output_width.max(1),
5593                height: self.output_height.max(1),
5594                depth_or_array_layers: 1,
5595            },
5596            mip_level_count: 1,
5597            sample_count: self.active_surface_msaa_samples(),
5598            dimension: wgpu::TextureDimension::D2,
5599            format: wgpu::TextureFormat::Depth24PlusStencil8,
5600            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
5601            view_formats: &[],
5602        });
5603        self.depth_stencil_view = self
5604            .depth_stencil_tex
5605            .create_view(&wgpu::TextureViewDescriptor::default());
5606    }
5607
5608    fn active_surface_msaa_samples(&self) -> u32 {
5609        if self.working_space {
5610            self.working_space_msaa_samples
5611        } else {
5612            self.msaa_samples
5613        }
5614    }
5615
5616    fn layer_target_format(&self) -> wgpu::TextureFormat {
5617        if self.working_space {
5618            wgpu::TextureFormat::Rgba16Float
5619        } else {
5620            self.output_format
5621        }
5622    }
5623
5624    fn get_or_create_layer(
5625        &mut self,
5626        layer_id: u32,
5627        width: u32,
5628        height: u32,
5629        rect: repose_core::Rect,
5630        transient: bool,
5631    ) -> bool {
5632        if width == 0
5633            || height == 0
5634            || width > self.device.limits().max_texture_dimension_2d
5635            || height > self.device.limits().max_texture_dimension_2d
5636        {
5637            return false;
5638        }
5639        let needs_alloc = match self.layer_pool.get(&layer_id) {
5640            Some(lt) => {
5641                lt.width != width || lt.height != height || lt.format != self.layer_target_format()
5642            }
5643            None => true,
5644        };
5645        if !needs_alloc {
5646            if let Some(lt) = self.layer_pool.get_mut(&layer_id) {
5647                lt.rect_px = (rect.x, rect.y, rect.w, rect.h);
5648            }
5649            return true;
5650        }
5651        let color_bytes =
5652            texture_storage_bytes(self.layer_target_format(), width.max(1), height.max(1))
5653                .unwrap_or(MAX_GRAPHICS_LAYER_BYTES + 1);
5654        let depth_bytes = checked_image_bytes(width.max(1), height.max(1), 4)
5655            .unwrap_or(MAX_GRAPHICS_LAYER_BYTES + 1);
5656        let bytes = color_bytes.saturating_add(depth_bytes);
5657        let old = self.layer_pool.get(&layer_id);
5658        let old_bytes = old.map_or(0, |layer| layer.bytes);
5659        let old_transient_bytes =
5660            old.map_or(0, |layer| if layer.transient { layer.bytes } else { 0 });
5661        let transient_count = self
5662            .layer_pool
5663            .values()
5664            .filter(|layer| layer.transient)
5665            .count();
5666        let transient_count = transient_count
5667            .saturating_sub(usize::from(old.is_some_and(|layer| layer.transient)))
5668            .saturating_add(usize::from(transient));
5669        let count = self.layer_pool.len() + usize::from(!self.layer_pool.contains_key(&layer_id));
5670        let total = self
5671            .layer_bytes_total
5672            .saturating_sub(old_bytes)
5673            .saturating_add(bytes);
5674        let projected_gpu = self
5675            .gpu_bytes_total()
5676            .saturating_sub(old_bytes)
5677            .checked_add(bytes)
5678            .unwrap_or(u64::MAX);
5679        let projected_transient = self
5680            .transient_layer_bytes_total
5681            .saturating_sub(old_transient_bytes)
5682            .saturating_add(if transient { bytes } else { 0 });
5683        if count > MAX_GRAPHICS_LAYERS
5684            || (transient
5685                && (transient_count > MAX_TRANSIENT_GRAPHICS_LAYERS
5686                    || projected_transient > MAX_TRANSIENT_GRAPHICS_LAYER_BYTES))
5687            || bytes > MAX_GRAPHICS_LAYER_BYTES
5688            || total > MAX_GRAPHICS_LAYER_BYTES
5689            || projected_gpu > self.gpu_budget_bytes
5690        {
5691            log::warn!("graphics layer budget exhausted; layer {layer_id} skipped");
5692            if let Some(old) = self.layer_pool.remove(&layer_id) {
5693                self.layer_bytes_total = self.layer_bytes_total.saturating_sub(old.bytes);
5694                if old.transient {
5695                    self.transient_layer_bytes_total =
5696                        self.transient_layer_bytes_total.saturating_sub(old.bytes);
5697                }
5698            }
5699            return false;
5700        }
5701        if let Some(old) = self.layer_pool.remove(&layer_id) {
5702            self.layer_bytes_total = self.layer_bytes_total.saturating_sub(old.bytes);
5703            if old.transient {
5704                self.transient_layer_bytes_total =
5705                    self.transient_layer_bytes_total.saturating_sub(old.bytes);
5706            }
5707        }
5708        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5709            label: Some("graphics layer"),
5710            size: wgpu::Extent3d {
5711                width: width.max(1),
5712                height: height.max(1),
5713                depth_or_array_layers: 1,
5714            },
5715            mip_level_count: 1,
5716            sample_count: 1,
5717            dimension: wgpu::TextureDimension::D2,
5718            format: self.layer_target_format(),
5719            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
5720                | wgpu::TextureUsages::TEXTURE_BINDING
5721                | wgpu::TextureUsages::COPY_SRC,
5722            view_formats: &[],
5723        });
5724        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5725        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5726            label: Some("layer bind"),
5727            layout: &self.image_bind_layout_rgba,
5728            entries: &[
5729                wgpu::BindGroupEntry {
5730                    binding: 0,
5731                    resource: wgpu::BindingResource::TextureView(&view),
5732                },
5733                wgpu::BindGroupEntry {
5734                    binding: 1,
5735                    resource: wgpu::BindingResource::Sampler(&self.layer_sampler),
5736                },
5737            ],
5738        });
5739        let bind_linear = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5740            label: Some("layer bind linear"),
5741            layout: &self.image_bind_layout_rgba,
5742            entries: &[
5743                wgpu::BindGroupEntry {
5744                    binding: 0,
5745                    resource: wgpu::BindingResource::TextureView(&view),
5746                },
5747                wgpu::BindGroupEntry {
5748                    binding: 1,
5749                    resource: wgpu::BindingResource::Sampler(&self.layer_sampler_linear),
5750                },
5751            ],
5752        });
5753        let depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
5754            label: Some("graphics layer depth-stencil"),
5755            size: wgpu::Extent3d {
5756                width: width.max(1),
5757                height: height.max(1),
5758                depth_or_array_layers: 1,
5759            },
5760            mip_level_count: 1,
5761            sample_count: 1,
5762            dimension: wgpu::TextureDimension::D2,
5763            format: wgpu::TextureFormat::Depth24PlusStencil8,
5764            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
5765            view_formats: &[],
5766        });
5767        let depth_stencil_view =
5768            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
5769        self.layer_pool.insert(
5770            layer_id,
5771            LayerTarget {
5772                texture: tex,
5773                view,
5774                bind,
5775                bind_linear,
5776                depth_stencil_view,
5777                width,
5778                height,
5779                format: self.layer_target_format(),
5780                bytes,
5781                rect_px: (rect.x, rect.y, rect.w, rect.h),
5782                transient,
5783            },
5784        );
5785        self.layer_bytes_total = self.layer_bytes_total.saturating_add(bytes);
5786        if transient {
5787            self.transient_layer_bytes_total =
5788                self.transient_layer_bytes_total.saturating_add(bytes);
5789        }
5790        true
5791    }
5792
5793    fn ensure_globals_staging(&mut self, pass_count: usize) -> bool {
5794        let size = std::mem::size_of::<Globals>() as u64;
5795        let needed = match (pass_count.max(1) as u64).checked_mul(size) {
5796            Some(needed) => needed,
5797            None => return false,
5798        };
5799        if needed <= self.globals_staging_cap {
5800            return true;
5801        }
5802        let cap = needed
5803            .next_power_of_two()
5804            .min(self.device.limits().max_buffer_size);
5805        if cap < needed {
5806            return false;
5807        }
5808        self.globals_staging = self.device.create_buffer(&wgpu::BufferDescriptor {
5809            label: Some("globals staging"),
5810            size: cap,
5811            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
5812            mapped_at_creation: false,
5813        });
5814        self.globals_staging_cap = cap;
5815        true
5816    }
5817
5818    fn reset_atlas_storage(&mut self) {
5819        self.atlas_mask = init_atlas_mask(&self.device);
5820        self.atlas_color = init_atlas_color(&self.device);
5821        self.atlas_mask_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5822            label: Some("atlas mask bind"),
5823            layout: &self.text_bind_layout,
5824            entries: &[
5825                wgpu::BindGroupEntry {
5826                    binding: 0,
5827                    resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
5828                },
5829                wgpu::BindGroupEntry {
5830                    binding: 1,
5831                    resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
5832                },
5833            ],
5834        });
5835        self.atlas_color_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5836            label: Some("atlas color bind"),
5837            layout: &self.text_bind_layout,
5838            entries: &[
5839                wgpu::BindGroupEntry {
5840                    binding: 0,
5841                    resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
5842                },
5843                wgpu::BindGroupEntry {
5844                    binding: 1,
5845                    resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
5846                },
5847            ],
5848        });
5849        self.atlas_mask_pending = AtlasPending::default();
5850        self.atlas_color_pending = AtlasPending::default();
5851        self.atlas_mask_deferred.clear();
5852        self.atlas_color_deferred.clear();
5853    }
5854
5855    /// Apply atlas growth and compaction scheduled during the previous frame.
5856    /// Called from `begin_frame`, before any glyph is batched: both operations
5857    /// re-pack the atlas, so running them mid-frame would leave already-batched
5858    /// instances pointing at the old packing.
5859    fn apply_atlas_pending(&mut self) {
5860        if self.atlas_mask_pending.grow {
5861            self.atlas_mask_pending.grow = false;
5862            let size = (self.atlas_mask.size * 2).min(MAX_GLYPH_ATLAS_SIZE);
5863            self.repack_mask(size);
5864        }
5865        if self.atlas_color_pending.grow {
5866            self.atlas_color_pending.grow = false;
5867            let size = (self.atlas_color.size * 2).min(MAX_GLYPH_ATLAS_SIZE);
5868            self.repack_color(size);
5869        }
5870        if self.frame_index.wrapping_sub(self.atlas_last_compact_frame)
5871            < ATLAS_COMPACT_MIN_FRAME_INTERVAL
5872        {
5873            return;
5874        }
5875        // Stay armed when a compaction finds nothing evictable, so the deferred
5876        // glyphs it would rescue get another chance next frame.
5877        if self.atlas_mask_pending.compact && self.compact_atlas_mask() {
5878            self.atlas_mask_pending.compact = false;
5879        }
5880        if self.atlas_color_pending.compact && self.compact_atlas_color() {
5881            self.atlas_color_pending.compact = false;
5882        }
5883    }
5884
5885    /// Drop the least recently used mask glyphs and re-pack at the same size.
5886    /// A shelf packer cannot hand back an individual cell, so reclaiming space
5887    /// means evicting cold entries and rebuilding around the warm ones. Returns
5888    /// false when there was nothing to evict.
5889    fn compact_atlas_mask(&mut self) -> bool {
5890        let target = self.atlas_mask.used_area / ATLAS_COMPACT_KEEP_DEN * ATLAS_COMPACT_KEEP_NUM;
5891        if !evict_atlas_lru(
5892            &mut self.atlas_mask.map,
5893            &mut self.atlas_mask.used_area,
5894            self.frame_index,
5895            target,
5896        ) {
5897            return false;
5898        }
5899        let size = self.atlas_mask.size;
5900        self.repack_mask(size);
5901        self.atlas_last_compact_frame = self.frame_index;
5902        true
5903    }
5904
5905    fn compact_atlas_color(&mut self) -> bool {
5906        let target = self.atlas_color.used_area / ATLAS_COMPACT_KEEP_DEN * ATLAS_COMPACT_KEEP_NUM;
5907        if !evict_atlas_lru(
5908            &mut self.atlas_color.map,
5909            &mut self.atlas_color.used_area,
5910            self.frame_index,
5911            target,
5912        ) {
5913            return false;
5914        }
5915        let size = self.atlas_color.size;
5916        self.repack_color(size);
5917        self.atlas_last_compact_frame = self.frame_index;
5918        true
5919    }
5920
5921    /// Re-pack the mask atlas into a `new_size` texture and re-upload every
5922    /// surviving entry, preserving its recency so a repack does not make warm
5923    /// glyphs look cold.
5924    fn repack_mask(&mut self, new_size: u32) {
5925        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5926            label: Some("glyph atlas A8 (repacked)"),
5927            size: wgpu::Extent3d {
5928                width: new_size,
5929                height: new_size,
5930                depth_or_array_layers: 1,
5931            },
5932            mip_level_count: 1,
5933            sample_count: 1,
5934            dimension: wgpu::TextureDimension::D2,
5935            format: wgpu::TextureFormat::R8Unorm,
5936            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
5937            view_formats: &[],
5938        });
5939        self.atlas_mask.tex = tex;
5940        self.atlas_mask.view = self
5941            .atlas_mask
5942            .tex
5943            .create_view(&wgpu::TextureViewDescriptor::default());
5944        self.atlas_mask.size = new_size;
5945        self.atlas_mask.next_x = 1;
5946        self.atlas_mask.next_y = 1;
5947        self.atlas_mask.row_h = 0;
5948        self.atlas_mask.used_area = 0;
5949        self.atlas_mask_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5950            label: Some("atlas mask bind"),
5951            layout: &self.text_bind_layout,
5952            entries: &[
5953                wgpu::BindGroupEntry {
5954                    binding: 0,
5955                    resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
5956                },
5957                wgpu::BindGroupEntry {
5958                    binding: 1,
5959                    resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
5960                },
5961            ],
5962        });
5963        let survivors: Vec<_> = self.atlas_mask.map.drain().collect();
5964        self.atlas_mask_deferred.clear();
5965        for (key, previous) in survivors {
5966            let _ = self.upload_glyph_mask(key, f32::from_bits(key.font_size_bits));
5967            if let Some(info) = self.atlas_mask.map.get_mut(&key) {
5968                info.last_touch = previous.last_touch;
5969                info.last_used_frame = previous.last_used_frame;
5970            }
5971        }
5972    }
5973
5974    /// Re-pack the color atlas into a `new_size` texture. See [`Self::repack_mask`].
5975    fn repack_color(&mut self, new_size: u32) {
5976        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5977            label: Some("glyph atlas RGBA (repacked)"),
5978            size: wgpu::Extent3d {
5979                width: new_size,
5980                height: new_size,
5981                depth_or_array_layers: 1,
5982            },
5983            mip_level_count: 1,
5984            sample_count: 1,
5985            dimension: wgpu::TextureDimension::D2,
5986            format: wgpu::TextureFormat::Rgba8UnormSrgb,
5987            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
5988            view_formats: &[],
5989        });
5990        self.atlas_color.tex = tex;
5991        self.atlas_color.view = self
5992            .atlas_color
5993            .tex
5994            .create_view(&wgpu::TextureViewDescriptor::default());
5995        self.atlas_color.size = new_size;
5996        self.atlas_color.next_x = 1;
5997        self.atlas_color.next_y = 1;
5998        self.atlas_color.row_h = 0;
5999        self.atlas_color.used_area = 0;
6000        self.atlas_color_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
6001            label: Some("atlas color bind"),
6002            layout: &self.text_bind_layout,
6003            entries: &[
6004                wgpu::BindGroupEntry {
6005                    binding: 0,
6006                    resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
6007                },
6008                wgpu::BindGroupEntry {
6009                    binding: 1,
6010                    resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
6011                },
6012            ],
6013        });
6014        let survivors: Vec<_> = self.atlas_color.map.drain().collect();
6015        self.atlas_color_deferred.clear();
6016        for (key, previous) in survivors {
6017            let _ = self.upload_glyph_color(key, f32::from_bits(key.font_size_bits));
6018            if let Some(info) = self.atlas_color.map.get_mut(&key) {
6019                info.last_touch = previous.last_touch;
6020                info.last_used_frame = previous.last_used_frame;
6021            }
6022        }
6023    }
6024
6025    fn sync_text_caches(&mut self) {
6026        let generation = repose_text::font_generation();
6027        if generation == self.atlas_generation {
6028            return;
6029        }
6030        self.atlas_generation = generation;
6031        self.reset_atlas_storage();
6032        self.atlas_mask_failures.clear();
6033        self.atlas_color_failures.clear();
6034        self.glyph_outline_cache.clear();
6035        self.legacy_shaped_text_cache.clear();
6036        self.slug_draw_cache.clear();
6037        self.slug_draw_cache_bytes_total = 0;
6038        self.slug_cache = slug::GlyphSlugCache::new();
6039    }
6040
6041    fn shape_text_cached(
6042        &mut self,
6043        text: &Arc<str>,
6044        px: f32,
6045        line_height_ratio: f32,
6046        font_family: Option<&'static str>,
6047        font_weight: u16,
6048        font_style: u8,
6049        letter_spacing: f32,
6050        variation: Option<&Arc<str>>,
6051    ) -> Arc<repose_text::SharedShapedText> {
6052        repose_text::shape_line_cached(
6053            text.as_ref(),
6054            px,
6055            line_height_ratio,
6056            font_family,
6057            font_weight,
6058            font_style,
6059            letter_spacing,
6060            variation.map(AsRef::as_ref),
6061        )
6062    }
6063
6064    fn legacy_shape_text_cached(
6065        &mut self,
6066        text: &Arc<str>,
6067        px: f32,
6068        line_height_ratio: f32,
6069        font_family: Option<&'static str>,
6070        font_weight: u16,
6071        font_style: u8,
6072        letter_spacing: f32,
6073        variation: Option<&Arc<str>>,
6074    ) -> Arc<[repose_text::ShapedGlyph]> {
6075        let key = TextShapeKey {
6076            text: text.clone(),
6077            px_bits: px.to_bits(),
6078            line_height_bits: line_height_ratio.to_bits(),
6079            font_family,
6080            font_weight,
6081            font_style,
6082            letter_spacing_bits: letter_spacing.to_bits(),
6083            variation: variation.cloned(),
6084            font_generation: self.atlas_generation,
6085        };
6086        if let Some(entry) = self.legacy_shaped_text_cache.get_mut(&key) {
6087            entry.last_used_frame = self.frame_index;
6088            return entry.glyphs.clone();
6089        }
6090        let glyphs: Arc<[repose_text::ShapedGlyph]> = Arc::from(repose_text::shape_line(
6091            text.as_ref(),
6092            px,
6093            line_height_ratio,
6094            font_family,
6095            font_weight,
6096            font_style,
6097            letter_spacing,
6098            variation.map(AsRef::as_ref),
6099        ));
6100        self.legacy_shaped_text_cache.insert(
6101            key,
6102            TextShapeCacheEntry {
6103                glyphs: glyphs.clone(),
6104                last_used_frame: self.frame_index,
6105            },
6106        );
6107        while self.legacy_shaped_text_cache.len() > MAX_LEGACY_SHAPED_TEXT_CACHE_ENTRIES {
6108            let Some(victim) = self
6109                .legacy_shaped_text_cache
6110                .iter()
6111                .min_by_key(|(_, entry)| entry.last_used_frame)
6112                .map(|(key, _)| key.clone())
6113            else {
6114                break;
6115            };
6116            self.legacy_shaped_text_cache.remove(&victim);
6117        }
6118        glyphs
6119    }
6120
6121    fn slug_draw_key(
6122        cache_key: repose_text::CacheKey,
6123        fill: bool,
6124        stroke_key: Option<&slug::StrokeTessKey>,
6125        transform: &Transform,
6126        origin: (f32, f32),
6127        px: f32,
6128        target: (f32, f32),
6129        color: [f32; 4],
6130    ) -> SlugDrawKey {
6131        let linear = transform.linear();
6132        let mut hasher = std::collections::hash_map::DefaultHasher::new();
6133        stroke_key.hash(&mut hasher);
6134        SlugDrawKey {
6135            cache_key,
6136            fill,
6137            stroke_hash: hasher.finish(),
6138            transform: [
6139                linear[0].to_bits(),
6140                linear[1].to_bits(),
6141                linear[2].to_bits(),
6142                linear[3].to_bits(),
6143                transform.scale_x.to_bits(),
6144                transform.scale_y.to_bits(),
6145                transform.translate_x.to_bits(),
6146                transform.translate_y.to_bits(),
6147                transform.rotate.to_bits(),
6148                transform.shear_x.to_bits(),
6149                transform.shear_y.to_bits(),
6150            ],
6151            origin: [origin.0.to_bits(), origin.1.to_bits()],
6152            px_bits: px.to_bits(),
6153            target: [target.0.to_bits(), target.1.to_bits()],
6154            color: color.map(f32::to_bits),
6155        }
6156    }
6157
6158    fn evict_slug_draw_cache(&mut self) -> bool {
6159        let victim = self
6160            .slug_draw_cache
6161            .iter()
6162            .filter(|(_, entry)| entry.last_used_frame != self.frame_index)
6163            .min_by_key(|(_, entry)| entry.last_touch_frame)
6164            .map(|(key, _)| *key);
6165        let Some(victim) = victim else {
6166            return false;
6167        };
6168        let Some(entry) = self.slug_draw_cache.remove(&victim) else {
6169            return false;
6170        };
6171        self.slug_draw_cache_bytes_total =
6172            self.slug_draw_cache_bytes_total.saturating_sub(entry.bytes);
6173        true
6174    }
6175
6176    fn reserve_slug_draw_cache(&mut self, bytes: u64) -> bool {
6177        while self.slug_draw_cache.len() >= MAX_SLUG_DRAW_CACHE_ENTRIES
6178            || self.slug_draw_cache_bytes_total.saturating_add(bytes) > MAX_SLUG_DRAW_CACHE_BYTES
6179        {
6180            if !self.evict_slug_draw_cache() {
6181                return false;
6182            }
6183        }
6184        true
6185    }
6186
6187    fn get_cached_slug_vertices(&mut self, key: &SlugDrawKey) -> Option<Arc<[slug::TessVertex]>> {
6188        let touch = self.next_cache_touch();
6189        self.slug_draw_cache.get_mut(key).map(|entry| {
6190            entry.last_used_frame = self.frame_index;
6191            entry.last_touch_frame = touch;
6192            entry.vertices.clone()
6193        })
6194    }
6195
6196    fn cached_slug_vertices(
6197        &mut self,
6198        key: SlugDrawKey,
6199        vertices: Vec<slug::TessVertex>,
6200    ) -> Arc<[slug::TessVertex]> {
6201        let touch = self.next_cache_touch();
6202        if let Some(entry) = self.slug_draw_cache.get_mut(&key) {
6203            entry.last_used_frame = self.frame_index;
6204            entry.last_touch_frame = touch;
6205            return entry.vertices.clone();
6206        }
6207        let bytes = slug_draw_cache_entry_bytes(vertices.len());
6208        if bytes > MAX_SLUG_DRAW_CACHE_BYTES || !self.reserve_slug_draw_cache(bytes) {
6209            return Arc::from(vertices);
6210        }
6211        let touch = self.next_cache_touch();
6212        let vertices: Arc<[slug::TessVertex]> = Arc::from(vertices);
6213        self.slug_draw_cache.insert(
6214            key,
6215            SlugDrawCacheEntry {
6216                vertices: vertices.clone(),
6217                bytes,
6218                last_used_frame: self.frame_index,
6219                last_touch_frame: touch,
6220            },
6221        );
6222        self.slug_draw_cache_bytes_total = self.slug_draw_cache_bytes_total.saturating_add(bytes);
6223        vertices
6224    }
6225
6226    fn cached_glyph_outline(
6227        &mut self,
6228        cache_key: repose_text::CacheKey,
6229    ) -> Option<Arc<[repose_text::Command]>> {
6230        let touch = self.next_cache_touch();
6231        if let Some(entry) = self.glyph_outline_cache.get_mut(&cache_key) {
6232            entry.last_touch = touch;
6233            return entry.commands.clone();
6234        }
6235        let commands = repose_text::extract_outline_commands(cache_key).map(Arc::from);
6236        if self.glyph_outline_cache.len() >= MAX_GLYPH_OUTLINE_CACHE_ENTRIES {
6237            self.trim_glyph_outline_cache();
6238        }
6239        self.glyph_outline_cache.insert(
6240            cache_key,
6241            GlyphOutlineEntry {
6242                commands: commands.clone(),
6243                last_touch: touch,
6244            },
6245        );
6246        commands
6247    }
6248
6249    /// Drop the coldest outlines so a working set larger than the cap degrades
6250    /// into steady churn instead of a full rebuild on every miss.
6251    fn trim_glyph_outline_cache(&mut self) {
6252        let target = (MAX_GLYPH_OUTLINE_CACHE_ENTRIES / ATLAS_COMPACT_KEEP_DEN as usize)
6253            * ATLAS_COMPACT_KEEP_NUM as usize;
6254        let mut victims: Vec<(u64, repose_text::CacheKey)> = self
6255            .glyph_outline_cache
6256            .iter()
6257            .map(|(key, entry)| (entry.last_touch, *key))
6258            .collect();
6259        victims.sort_unstable_by_key(|victim| victim.0);
6260        for (_, key) in victims {
6261            if self.glyph_outline_cache.len() <= target {
6262                break;
6263            }
6264            self.glyph_outline_cache.remove(&key);
6265        }
6266    }
6267
6268    fn atlas_bind_group_mask(&self) -> wgpu::BindGroup {
6269        self.atlas_mask_bind.clone()
6270    }
6271
6272    fn atlas_bind_group_color(&self) -> wgpu::BindGroup {
6273        self.atlas_color_bind.clone()
6274    }
6275
6276    fn mark_atlas_mask_failure(&mut self, key: repose_text::CacheKey) {
6277        if self.atlas_mask_failures.len() >= MAX_GLYPH_ATLAS_ENTRIES {
6278            self.atlas_mask_failures.clear();
6279        }
6280        self.atlas_mask_failures.insert(key);
6281    }
6282
6283    fn mark_atlas_color_failure(&mut self, key: repose_text::CacheKey) {
6284        if self.atlas_color_failures.len() >= MAX_GLYPH_ATLAS_ENTRIES {
6285            self.atlas_color_failures.clear();
6286        }
6287        self.atlas_color_failures.insert(key);
6288    }
6289
6290    fn upload_glyph_mask(
6291        &mut self,
6292        cache_key: repose_text::CacheKey,
6293        px: f32,
6294    ) -> Option<GlyphInfo> {
6295        let px = quantize_glyph_px(px);
6296        let keyp = repose_text::CacheKey {
6297            font_size_bits: px.to_bits(),
6298            ..cache_key
6299        };
6300        let touch = self.next_cache_touch();
6301        if let Some(info) = self.atlas_mask.map.get_mut(&keyp) {
6302            info.last_touch = touch;
6303            info.last_used_frame = self.frame_index;
6304            return Some(*info);
6305        }
6306        if self.atlas_mask_failures.contains(&keyp) || self.atlas_mask_deferred.contains(&keyp) {
6307            return None;
6308        }
6309
6310        let Some(gb) = repose_text::rasterize_cache_key(keyp) else {
6311            self.mark_atlas_mask_failure(keyp);
6312            return None;
6313        };
6314        if gb.w == 0 || gb.h == 0 || gb.data.is_empty() {
6315            self.mark_atlas_mask_failure(keyp);
6316            return None;
6317        }
6318        let Some(coverage) = swash_to_a8_coverage(gb.content, &gb.data) else {
6319            self.mark_atlas_mask_failure(keyp);
6320            return None;
6321        };
6322
6323        let w = gb.w.max(1);
6324        let h = gb.h.max(1);
6325        let expected = checked_image_bytes_usize(w, h, 1).ok()?;
6326        if coverage.len() < expected {
6327            self.mark_atlas_mask_failure(keyp);
6328            return None;
6329        }
6330        if w >= MAX_GLYPH_ATLAS_SIZE || h >= MAX_GLYPH_ATLAS_SIZE {
6331            self.mark_atlas_mask_failure(keyp);
6332            return None;
6333        }
6334        if !self.alloc_or_schedule_mask(w, h) {
6335            defer_atlas_glyph(&mut self.atlas_mask_deferred, keyp);
6336            return None;
6337        }
6338        let x = self.atlas_mask.next_x;
6339        let y = self.atlas_mask.next_y;
6340        self.atlas_mask.next_x += w + 1;
6341        self.atlas_mask.row_h = self.atlas_mask.row_h.max(h + 1);
6342
6343        let layout = wgpu::TexelCopyBufferLayout {
6344            offset: 0,
6345            bytes_per_row: Some(w),
6346            rows_per_image: Some(h),
6347        };
6348        let size = wgpu::Extent3d {
6349            width: w,
6350            height: h,
6351            depth_or_array_layers: 1,
6352        };
6353        self.queue.write_texture(
6354            wgpu::TexelCopyTextureInfoBase {
6355                texture: &self.atlas_mask.tex,
6356                mip_level: 0,
6357                origin: wgpu::Origin3d { x, y, z: 0 },
6358                aspect: wgpu::TextureAspect::All,
6359            },
6360            &coverage,
6361            layout,
6362            size,
6363        );
6364
6365        let info = GlyphInfo {
6366            u0: x as f32 / self.atlas_mask.size as f32,
6367            v0: y as f32 / self.atlas_mask.size as f32,
6368            u1: (x + w) as f32 / self.atlas_mask.size as f32,
6369            v1: (y + h) as f32 / self.atlas_mask.size as f32,
6370            w: w as f32,
6371            h: h as f32,
6372            last_touch: touch,
6373            last_used_frame: self.frame_index,
6374        };
6375        self.atlas_mask.used_area = self
6376            .atlas_mask
6377            .used_area
6378            .saturating_add(u64::from(w).saturating_mul(u64::from(h)));
6379        self.atlas_mask.map.insert(keyp, info);
6380        Some(info)
6381    }
6382
6383    fn upload_glyph_color(
6384        &mut self,
6385        cache_key: repose_text::CacheKey,
6386        px: f32,
6387    ) -> Option<GlyphInfo> {
6388        let px = quantize_glyph_px(px);
6389        let keyp = repose_text::CacheKey {
6390            font_size_bits: px.to_bits(),
6391            ..cache_key
6392        };
6393        let touch = self.next_cache_touch();
6394        if let Some(info) = self.atlas_color.map.get_mut(&keyp) {
6395            info.last_touch = touch;
6396            info.last_used_frame = self.frame_index;
6397            return Some(*info);
6398        }
6399        if self.atlas_color_failures.contains(&keyp) || self.atlas_color_deferred.contains(&keyp) {
6400            return None;
6401        }
6402        let Some(gb) = repose_text::rasterize_cache_key(keyp) else {
6403            self.mark_atlas_color_failure(keyp);
6404            return None;
6405        };
6406        if !matches!(gb.content, repose_text::SwashContent::Color) {
6407            self.mark_atlas_color_failure(keyp);
6408            return None;
6409        }
6410        let w = gb.w.max(1);
6411        let h = gb.h.max(1);
6412        let expected = checked_image_bytes_usize(w, h, 4).ok()?;
6413        if gb.data.len() < expected {
6414            self.mark_atlas_color_failure(keyp);
6415            return None;
6416        }
6417        if w >= MAX_GLYPH_ATLAS_SIZE || h >= MAX_GLYPH_ATLAS_SIZE {
6418            self.mark_atlas_color_failure(keyp);
6419            return None;
6420        }
6421        if !self.alloc_or_schedule_color(w, h) {
6422            defer_atlas_glyph(&mut self.atlas_color_deferred, keyp);
6423            return None;
6424        }
6425        let x = self.atlas_color.next_x;
6426        let y = self.atlas_color.next_y;
6427        self.atlas_color.next_x += w + 1;
6428        self.atlas_color.row_h = self.atlas_color.row_h.max(h + 1);
6429
6430        let layout = wgpu::TexelCopyBufferLayout {
6431            offset: 0,
6432            bytes_per_row: Some(w * 4),
6433            rows_per_image: Some(h),
6434        };
6435        let size = wgpu::Extent3d {
6436            width: w,
6437            height: h,
6438            depth_or_array_layers: 1,
6439        };
6440        self.queue.write_texture(
6441            wgpu::TexelCopyTextureInfoBase {
6442                texture: &self.atlas_color.tex,
6443                mip_level: 0,
6444                origin: wgpu::Origin3d { x, y, z: 0 },
6445                aspect: wgpu::TextureAspect::All,
6446            },
6447            &gb.data,
6448            layout,
6449            size,
6450        );
6451        let info = GlyphInfo {
6452            u0: x as f32 / self.atlas_color.size as f32,
6453            v0: y as f32 / self.atlas_color.size as f32,
6454            u1: (x + w) as f32 / self.atlas_color.size as f32,
6455            v1: (y + h) as f32 / self.atlas_color.size as f32,
6456            w: w as f32,
6457            h: h as f32,
6458            last_touch: touch,
6459            last_used_frame: self.frame_index,
6460        };
6461        self.atlas_color.used_area = self
6462            .atlas_color
6463            .used_area
6464            .saturating_add(u64::from(w).saturating_mul(u64::from(h)));
6465        self.atlas_color.map.insert(keyp, info);
6466        Some(info)
6467    }
6468
6469    fn alloc_space_mask(&mut self, w: u32, h: u32) -> bool {
6470        let Some(x_end) = self
6471            .atlas_mask
6472            .next_x
6473            .checked_add(w)
6474            .and_then(|v| v.checked_add(1))
6475        else {
6476            return false;
6477        };
6478        if x_end >= self.atlas_mask.size {
6479            self.atlas_mask.next_x = 1;
6480            let Some(next_y) = self
6481                .atlas_mask
6482                .next_y
6483                .checked_add(self.atlas_mask.row_h)
6484                .and_then(|v| v.checked_add(1))
6485            else {
6486                return false;
6487            };
6488            self.atlas_mask.next_y = next_y;
6489            self.atlas_mask.row_h = 0;
6490        }
6491        let Some(y_end) = self
6492            .atlas_mask
6493            .next_y
6494            .checked_add(h)
6495            .and_then(|v| v.checked_add(1))
6496        else {
6497            return false;
6498        };
6499        y_end < self.atlas_mask.size
6500    }
6501
6502    fn alloc_space_color(&mut self, w: u32, h: u32) -> bool {
6503        let Some(x_end) = self
6504            .atlas_color
6505            .next_x
6506            .checked_add(w)
6507            .and_then(|v| v.checked_add(1))
6508        else {
6509            return false;
6510        };
6511        if x_end >= self.atlas_color.size {
6512            self.atlas_color.next_x = 1;
6513            let Some(next_y) = self
6514                .atlas_color
6515                .next_y
6516                .checked_add(self.atlas_color.row_h)
6517                .and_then(|v| v.checked_add(1))
6518            else {
6519                return false;
6520            };
6521            self.atlas_color.next_y = next_y;
6522            self.atlas_color.row_h = 0;
6523        }
6524        let Some(y_end) = self
6525            .atlas_color
6526            .next_y
6527            .checked_add(h)
6528            .and_then(|v| v.checked_add(1))
6529        else {
6530            return false;
6531        };
6532        y_end < self.atlas_color.size
6533    }
6534
6535    /// Place a `w` x `h` cell in the mask atlas, or schedule the work needed to
6536    /// make room. Growth and eviction both re-pack the atlas, which would move
6537    /// glyphs already batched into the frame in flight, so they are deferred to
6538    /// the next `begin_frame`; the caller treats a `false` as "skip this glyph".
6539    fn alloc_or_schedule_mask(&mut self, w: u32, h: u32) -> bool {
6540        if self.atlas_mask.map.len() >= MAX_GLYPH_ATLAS_ENTRIES
6541            || self
6542                .atlas_mask
6543                .used_area
6544                .saturating_add(u64::from(w) * u64::from(h))
6545                >= self.atlas_mask.size as u64 * self.atlas_mask.size as u64
6546        {
6547            self.atlas_mask_pending.compact = true;
6548            return false;
6549        }
6550        if self.alloc_space_mask(w, h) {
6551            return true;
6552        }
6553        if self.atlas_mask.size < MAX_GLYPH_ATLAS_SIZE {
6554            self.atlas_mask_pending.grow = true;
6555        } else {
6556            self.atlas_mask_pending.compact = true;
6557        }
6558        false
6559    }
6560
6561    /// See [`Self::alloc_or_schedule_mask`].
6562    fn alloc_or_schedule_color(&mut self, w: u32, h: u32) -> bool {
6563        if self.atlas_color.map.len() >= MAX_GLYPH_ATLAS_ENTRIES
6564            || self
6565                .atlas_color
6566                .used_area
6567                .saturating_add(u64::from(w) * u64::from(h))
6568                >= self.atlas_color.size as u64 * self.atlas_color.size as u64
6569        {
6570            self.atlas_color_pending.compact = true;
6571            return false;
6572        }
6573        if self.alloc_space_color(w, h) {
6574            return true;
6575        }
6576        if self.atlas_color.size < MAX_GLYPH_ATLAS_SIZE {
6577            self.atlas_color_pending.grow = true;
6578        } else {
6579            self.atlas_color_pending.compact = true;
6580        }
6581        false
6582    }
6583}
6584
6585/// Packed brush fields shared by the shape instances (border, ellipse,
6586/// ellipse border, arc). Gradient endpoints are shape-local px; the shaders
6587/// recenter `(0,0)` at the shape top-left. Radial packs `center` into
6588/// `grad_p0` and `radius` into `grad_p1.x`; sweep packs `center` into
6589/// `grad_p0`.
6590///
6591/// `rect` is the shape's scene-space bounds and `transform` the accumulated
6592/// scene transform.
6593/// Endpoints are converted from shape-local px through the inverse linear
6594/// part so rotation and uniform scale cancel against the shader's
6595/// un-rotation. Non-uniform scale and shear distort the gradient the same
6596/// way they distort the shape (the shader un-rotates but cannot un-scale
6597/// pixels).
6598fn brush_to_shape_fields(
6599    brush: &Brush,
6600    rect: &repose_core::Rect,
6601    transform: &Transform,
6602) -> (u32, u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2], u32) {
6603    let to_local = |p: Vec2| {
6604        let m = transform.linear();
6605        let det = m[0] * m[3] - m[1] * m[2];
6606        if det.abs() < 1e-12 {
6607            return [p.x, p.y];
6608        }
6609        [
6610            (m[3] * p.x - m[1] * p.y) / det,
6611            (-m[2] * p.x + m[0] * p.y) / det,
6612        ]
6613    };
6614    match brush {
6615        Brush::Solid(c) => (
6616            0u32,
6617            0u32,
6618            c.to_linear(),
6619            [0.0; 4],
6620            [0.0; 2],
6621            [0.0; 2],
6622            0u32,
6623        ),
6624        Brush::Linear {
6625            start,
6626            end,
6627            start_color,
6628            end_color,
6629        } => (
6630            1u32,
6631            0u32,
6632            start_color.to_linear(),
6633            end_color.to_linear(),
6634            to_local(*start),
6635            to_local(*end),
6636            0u32,
6637        ),
6638        Brush::LinearNormalized {
6639            start,
6640            end,
6641            start_color,
6642            end_color,
6643        } => (
6644            1u32,
6645            0u32,
6646            start_color.to_linear(),
6647            end_color.to_linear(),
6648            to_local(Vec2 {
6649                x: start.x * rect.w,
6650                y: start.y * rect.h,
6651            }),
6652            to_local(Vec2 {
6653                x: end.x * rect.w,
6654                y: end.y * rect.h,
6655            }),
6656            0u32,
6657        ),
6658        Brush::Radial {
6659            center,
6660            radius,
6661            start_color,
6662            end_color,
6663        } => (
6664            1u32,
6665            1u32,
6666            start_color.to_linear(),
6667            end_color.to_linear(),
6668            to_local(*center),
6669            [radius.max(0.0), 0.0],
6670            0u32,
6671        ),
6672        Brush::Sweep {
6673            center,
6674            start_color,
6675            end_color,
6676        } => (
6677            1u32,
6678            2u32,
6679            start_color.to_linear(),
6680            end_color.to_linear(),
6681            to_local(*center),
6682            [0.0, 0.0],
6683            0u32,
6684        ),
6685        _ => (0u32, 0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2], 0u32),
6686    }
6687}
6688
6689fn brush_to_instance_fields(brush: &Brush) -> (u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2]) {
6690    match brush {
6691        Brush::Solid(c) => (
6692            0u32,
6693            c.to_linear(),
6694            [0.0, 0.0, 0.0, 0.0],
6695            [0.0, 0.0],
6696            [0.0, 1.0],
6697        ),
6698        Brush::Linear {
6699            start,
6700            end,
6701            start_color,
6702            end_color,
6703        } => (
6704            1u32,
6705            start_color.to_linear(),
6706            end_color.to_linear(),
6707            [start.x, start.y],
6708            [end.x, end.y],
6709        ),
6710        Brush::LinearNormalized {
6711            start,
6712            end,
6713            start_color,
6714            end_color,
6715        } => (
6716            1u32,
6717            start_color.to_linear(),
6718            end_color.to_linear(),
6719            [start.x, start.y],
6720            [end.x, end.y],
6721        ),
6722        Brush::Radial { start_color, .. } => (
6723            0u32,
6724            start_color.to_linear(),
6725            [0.0, 0.0, 0.0, 0.0],
6726            [0.0, 0.0],
6727            [0.0, 1.0],
6728        ),
6729        Brush::Sweep { start_color, .. } => (
6730            0u32,
6731            start_color.to_linear(),
6732            [0.0, 0.0, 0.0, 0.0],
6733            [0.0, 0.0],
6734            [0.0, 1.0],
6735        ),
6736        _ => (0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
6737    }
6738}
6739
6740/// Fallback color when a [`Brush`] reaches a solid-only path (glyph atlas
6741/// uploads for gradient text). Uses the gradient's start color.
6742#[allow(dead_code)]
6743fn brush_to_solid_color(brush: &Brush) -> [f32; 4] {
6744    match brush {
6745        Brush::Solid(c) => c.to_linear(),
6746        Brush::Linear { start_color, .. } => start_color.to_linear(),
6747        Brush::LinearNormalized { start_color, .. } => start_color.to_linear(),
6748        Brush::Radial { start_color, .. } => start_color.to_linear(),
6749        Brush::Sweep { start_color, .. } => start_color.to_linear(),
6750        _ => [0.0; 4],
6751    }
6752}
6753
6754fn init_atlas_mask(device: &wgpu::Device) -> AtlasA8 {
6755    let size = 1024u32;
6756    let tex = device.create_texture(&wgpu::TextureDescriptor {
6757        label: Some("glyph atlas A8"),
6758        size: wgpu::Extent3d {
6759            width: size,
6760            height: size,
6761            depth_or_array_layers: 1,
6762        },
6763        mip_level_count: 1,
6764        sample_count: 1,
6765        dimension: wgpu::TextureDimension::D2,
6766        format: wgpu::TextureFormat::R8Unorm,
6767        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
6768        view_formats: &[],
6769    });
6770    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
6771    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
6772        label: Some("glyph atlas sampler A8"),
6773        address_mode_u: wgpu::AddressMode::ClampToEdge,
6774        address_mode_v: wgpu::AddressMode::ClampToEdge,
6775        address_mode_w: wgpu::AddressMode::ClampToEdge,
6776        mag_filter: wgpu::FilterMode::Linear,
6777        min_filter: wgpu::FilterMode::Linear,
6778        mipmap_filter: wgpu::MipmapFilterMode::Linear,
6779        ..Default::default()
6780    });
6781
6782    AtlasA8 {
6783        tex,
6784        view,
6785        sampler,
6786        size,
6787        next_x: 1,
6788        next_y: 1,
6789        row_h: 0,
6790        map: HashMap::new(),
6791        used_area: 0,
6792    }
6793}
6794
6795fn init_atlas_color(device: &wgpu::Device) -> AtlasRGBA {
6796    let size = 1024u32;
6797    let tex = device.create_texture(&wgpu::TextureDescriptor {
6798        label: Some("glyph atlas RGBA"),
6799        size: wgpu::Extent3d {
6800            width: size,
6801            height: size,
6802            depth_or_array_layers: 1,
6803        },
6804        mip_level_count: 1,
6805        sample_count: 1,
6806        dimension: wgpu::TextureDimension::D2,
6807        format: wgpu::TextureFormat::Rgba8UnormSrgb,
6808        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
6809        view_formats: &[],
6810    });
6811    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
6812    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
6813        label: Some("glyph atlas sampler RGBA"),
6814        address_mode_u: wgpu::AddressMode::ClampToEdge,
6815        address_mode_v: wgpu::AddressMode::ClampToEdge,
6816        address_mode_w: wgpu::AddressMode::ClampToEdge,
6817        mag_filter: wgpu::FilterMode::Linear,
6818        min_filter: wgpu::FilterMode::Linear,
6819        mipmap_filter: wgpu::MipmapFilterMode::Linear,
6820        ..Default::default()
6821    });
6822    AtlasRGBA {
6823        tex,
6824        view,
6825        sampler,
6826        size,
6827        next_x: 1,
6828        next_y: 1,
6829        row_h: 0,
6830        map: HashMap::new(),
6831        used_area: 0,
6832    }
6833}
6834
6835#[cfg(feature = "winit-surface")]
6836impl WgpuSurfaceBackend {
6837    /// Drop the surface, keeping device/queue/pipelines. Releases the old
6838    /// native-window binding. Call `recreate_surface` to present again.
6839    pub fn take_surface(&mut self) -> Option<wgpu::Surface<'static>> {
6840        self.surface.take()
6841    }
6842
6843    /// Create a fresh surface for `window` on the retained instance and
6844    /// configure it. Recovers from `CurrentSurfaceTexture::Lost`, where
6845    /// reconfiguring the old surface object cannot help.
6846    pub fn recreate_surface(&mut self, window: &Arc<winit::window::Window>) -> anyhow::Result<()> {
6847        self.window = Some(window.clone());
6848        let size = window.inner_size();
6849        if size.width == 0 || size.height == 0 {
6850            self.surface = None;
6851            self.pending_reconfigure = false;
6852            self.renderer.resize(0, 0);
6853            anyhow::bail!("window has zero size; surface recreation deferred");
6854        }
6855        let Some(instance) = self.instance.as_ref() else {
6856            anyhow::bail!("no wgpu instance retained; cannot recreate surface")
6857        };
6858        let Some(config) = self.surface_config.as_mut() else {
6859            anyhow::bail!("no surface config retained; cannot recreate surface")
6860        };
6861        let max = self.renderer.device.limits().max_texture_dimension_2d;
6862        let width = size.width.min(max);
6863        let height = size.height.min(max);
6864        config.width = width;
6865        config.height = height;
6866        let surface = instance.create_surface(window.clone())?;
6867        surface.configure(&self.renderer.device, config);
6868        self.surface = Some(surface);
6869        self.pending_reconfigure = false;
6870        self.renderer.resize(width, height);
6871        Ok(())
6872    }
6873}
6874
6875#[cfg(feature = "winit-surface")]
6876impl RenderBackend for WgpuSurfaceBackend {
6877    fn configure_surface(&mut self, width: u32, height: u32) {
6878        if width == 0 || height == 0 {
6879            self.renderer.resize(0, 0);
6880            self.pending_reconfigure = true;
6881            return;
6882        }
6883        let max = self.renderer.device.limits().max_texture_dimension_2d;
6884        let width = width.min(max);
6885        let height = height.min(max);
6886        if self.renderer.output_width == width && self.renderer.output_height == height {
6887            if let Some(ref mut config) = self.surface_config {
6888                config.width = width;
6889                config.height = height;
6890            }
6891            return;
6892        }
6893        if let Some(ref mut config) = self.surface_config {
6894            config.width = width;
6895            config.height = height;
6896        }
6897        self.renderer.resize(width, height);
6898        if let (Some(surface), Some(config)) = (self.surface.as_ref(), self.surface_config.as_ref())
6899        {
6900            surface.configure(&self.renderer.device, config);
6901        }
6902    }
6903
6904    fn frame(&mut self, scene: &Scene, _glyph_cfg: GlyphRasterConfig) -> bool {
6905        if self.pending_reconfigure {
6906            if let (Some(surface), Some(config)) =
6907                (self.surface.as_ref(), self.surface_config.as_ref())
6908            {
6909                surface.configure(&self.renderer.device, config);
6910            }
6911            self.pending_reconfigure = false;
6912        }
6913
6914        if self.renderer.output_width == 0 || self.renderer.output_height == 0 {
6915            request_frame();
6916            return false;
6917        }
6918        if let Err(error) = self.renderer.begin_frame() {
6919            log::warn!("begin renderer frame: {error:#}");
6920            request_frame();
6921            return false;
6922        }
6923
6924        let Some(surface) = self.surface.as_ref() else {
6925            self.renderer.end_frame();
6926            request_frame();
6927            return false;
6928        };
6929        let frame = match surface.get_current_texture() {
6930            wgpu::CurrentSurfaceTexture::Success(f) => f,
6931            wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
6932                self.pending_reconfigure = true;
6933                f
6934            }
6935            other @ (wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost) => {
6936                self.surface = None;
6937                let result = self
6938                    .window
6939                    .clone()
6940                    .ok_or_else(|| anyhow::anyhow!("no window retained for surface recreation"))
6941                    .and_then(|window| self.recreate_surface(&window));
6942                if let Err(error) = result {
6943                    log::warn!("surface {other:?}; recreation deferred: {error:#}");
6944                }
6945                self.renderer.end_frame();
6946                request_frame();
6947                return false;
6948            }
6949            other => {
6950                match other {
6951                    wgpu::CurrentSurfaceTexture::Validation => {
6952                        log::warn!("surface {other:?}; reconfiguring next frame");
6953                        self.pending_reconfigure = true;
6954                    }
6955                    wgpu::CurrentSurfaceTexture::Timeout
6956                    | wgpu::CurrentSurfaceTexture::Occluded => {
6957                        log::debug!("surface {other:?}; retrying next frame");
6958                    }
6959                    _ => {}
6960                }
6961                self.renderer.end_frame();
6962                request_frame();
6963                return false;
6964            }
6965        };
6966
6967        // The swapchain extent and the renderer's size are updated from
6968        // separate events, so they can disagree for a frame mid-resize (most
6969        // visibly when dragging a window into a portrait shape). With MSAA on,
6970        // a color attachment whose extent differs from its resolve target
6971        // fails wgpu validation, which drops the whole pass *including its
6972        // clear*, so the just-reconfigured swapchain keeps undefined contents
6973        // and presents as green/black streaks. Adopt the swapchain's size and
6974        // retry next frame instead of rendering an invalid pass.
6975        let (swap_w, swap_h) = (frame.texture.width(), frame.texture.height());
6976        if swap_w != self.renderer.output_width || swap_h != self.renderer.output_height {
6977            log::debug!(
6978                "swapchain {swap_w}x{swap_h} != renderer {}x{}; resyncing",
6979                self.renderer.output_width,
6980                self.renderer.output_height
6981            );
6982            drop(frame);
6983            if let Some(config) = self.surface_config.as_mut() {
6984                config.width = swap_w;
6985                config.height = swap_h;
6986            }
6987            self.renderer.resize(swap_w, swap_h);
6988            self.pending_reconfigure = true;
6989            self.renderer.end_frame();
6990            request_frame();
6991            return false;
6992        }
6993
6994        let swap_view = if let Some(view_format) = self
6995            .surface_config
6996            .as_ref()
6997            .and_then(|c| c.view_formats.iter().find(|f| f.is_srgb()).copied())
6998        {
6999            frame.texture.create_view(&wgpu::TextureViewDescriptor {
7000                format: Some(view_format),
7001                ..Default::default()
7002            })
7003        } else {
7004            frame
7005                .texture
7006                .create_view(&wgpu::TextureViewDescriptor::default())
7007        };
7008        let mut encoder =
7009            self.renderer
7010                .device
7011                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
7012                    label: Some("frame encoder"),
7013                });
7014
7015        self.renderer.render_scene_to_encoder_with_texture(
7016            scene,
7017            &mut encoder,
7018            &swap_view,
7019            Some(&frame.texture),
7020            None,
7021        );
7022
7023        //NOTE: The WebGL HAL present path (fullscreen triangle / blit) does not
7024        // restore gl.colorMask. Hence this is needed to prevent frames from going transparent.
7025        {
7026            let _reset = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
7027                label: Some("webgl color_mask reset before present"),
7028                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7029                    view: &swap_view,
7030                    resolve_target: None,
7031                    ops: wgpu::Operations {
7032                        load: wgpu::LoadOp::Load,
7033                        store: wgpu::StoreOp::Store,
7034                    },
7035                    depth_slice: None,
7036                })],
7037                depth_stencil_attachment: None,
7038                timestamp_writes: None,
7039                occlusion_query_set: None,
7040                multiview_mask: None,
7041            });
7042        }
7043
7044        self.renderer
7045            .queue
7046            .submit(std::iter::once(encoder.finish()));
7047        if let Err(e) = catch_unwind(AssertUnwindSafe(|| self.renderer.queue.present(frame))) {
7048            log::warn!("queue.present panicked: {:?}", e);
7049            self.renderer.end_frame();
7050            request_frame();
7051            return false;
7052        }
7053        self.renderer.end_frame();
7054        true
7055    }
7056}
7057
7058impl WgpuSceneRenderer {
7059    /// Open a translator-owned flatten layer for a perspective `PushTransform`.
7060    ///
7061    /// True perspective cannot ride the affine instance fast path, so the
7062    /// subtree renders flat into an offscreen layer and is composited back
7063    /// projectively on the matching pop (CSS-style flattening). The layer
7064    /// rect is the currently visible scissor in this target: content outside
7065    /// it is invisible in the parent, so clipping it in the layer changes
7066    /// nothing. Children keep the node's affine part on the stack (so
7067    /// `combine` stays affine-only) plus a layer-local shift, exactly like
7068    /// producer-owned blur layers — which is exact under rigid ancestors
7069    /// (translations commute) and the documented layer contract otherwise.
7070    #[allow(clippy::too_many_arguments)]
7071    fn replay_active_clips(
7072        &mut self,
7073        clips: &[ActiveClip],
7074        parent_transform: &Transform,
7075        origin: (f32, f32),
7076        target_size: (f32, f32),
7077        pass: &mut Pass,
7078        encoder: &mut wgpu::CommandEncoder,
7079    ) -> Vec<ActiveClip> {
7080        let mut current = repose_core::Rect {
7081            x: 0.0,
7082            y: 0.0,
7083            w: target_size.0,
7084            h: target_size.1,
7085        };
7086        let mut replayed = Vec::with_capacity(clips.len());
7087        for (index, clip) in clips.iter().enumerate() {
7088            let has_inverse = clips[..index].iter().any(ActiveClip::difference);
7089            let mut blocked = clip.blocked();
7090            if has_inverse && !blocked {
7091                log::error!(
7092                    "unsupported clip nesting: a Difference clip cannot contain another clip"
7093                );
7094                blocked = true;
7095            }
7096            let difference = clip.difference();
7097            let mut command = None;
7098            match clip {
7099                ActiveClip::Rect {
7100                    off: _,
7101                    cnt: _,
7102                    rect,
7103                    radii,
7104                    ..
7105                } => {
7106                    let local_rect = parent_rect_to_layer_rect(*rect, parent_transform, origin)
7107                        .unwrap_or(repose_core::Rect {
7108                            x: 0.0,
7109                            y: 0.0,
7110                            w: 0.0,
7111                            h: 0.0,
7112                        });
7113                    let local_radii =
7114                        parent_radii_to_layer_radii(*radii, parent_transform).unwrap_or([0.0; 4]);
7115                    let next = if difference {
7116                        current
7117                    } else {
7118                        intersect(current, local_rect)
7119                    };
7120                    let scissor = rect_to_scissor(
7121                        next,
7122                        target_size.0.max(1.0) as u32,
7123                        target_size.1.max(1.0) as u32,
7124                    );
7125                    let has_area = local_rect.x.is_finite()
7126                        && local_rect.y.is_finite()
7127                        && local_rect.w.is_finite()
7128                        && local_rect.h.is_finite()
7129                        && local_rect.w > 0.0
7130                        && local_rect.h > 0.0;
7131                    if blocked || !has_area || (!difference && scissor.2 == 0) {
7132                        blocked = blocked || !difference;
7133                    } else {
7134                        let ndc = rect_to_ndc(local_rect, target_size.0, target_size.1);
7135                        let instance = ClipInstance {
7136                            xywh: [ndc[0] + ndc[2] * 0.5, ndc[1] + ndc[3] * 0.5, ndc[2], ndc[3]],
7137                            radii: local_radii,
7138                            fwd_mat: [1.0, 0.0, 0.0, 1.0],
7139                        };
7140                        let bytes = bytemuck::bytes_of(&instance);
7141                        if self
7142                            .clip_ring
7143                            .grow_to_fit(
7144                                &self.device,
7145                                encoder,
7146                                u64::try_from(bytes.len()).unwrap_or(u64::MAX),
7147                            )
7148                            .is_ok()
7149                            && let Ok(new_off) = self.clip_ring.alloc_write(&self.queue, bytes)
7150                        {
7151                            command = Some(Cmd::ClipPush {
7152                                off: new_off,
7153                                cnt: 1,
7154                                scissor,
7155                                difference,
7156                                applied: true,
7157                            });
7158                            replayed.push(ActiveClip::Rect {
7159                                off: new_off,
7160                                cnt: 1,
7161                                rect: local_rect,
7162                                radii: local_radii,
7163                                scissor,
7164                                difference,
7165                                applied: true,
7166                                blocked: false,
7167                            });
7168                        } else {
7169                            blocked = blocked || !difference;
7170                        }
7171                    }
7172                    current = next;
7173                }
7174                ActiveClip::Vector {
7175                    mesh,
7176                    source,
7177                    affine,
7178                    ..
7179                } => {
7180                    let local_affine =
7181                        parent_affine_to_layer_affine(*affine, parent_transform, origin)
7182                            .unwrap_or([0.0; 6]);
7183                    let aabb = mesh_aabb(source, local_affine);
7184                    let next = if difference {
7185                        current
7186                    } else {
7187                        intersect(current, aabb)
7188                    };
7189                    let scissor = rect_to_scissor(
7190                        next,
7191                        target_size.0.max(1.0) as u32,
7192                        target_size.1.max(1.0) as u32,
7193                    );
7194                    let has_area = aabb.x.is_finite()
7195                        && aabb.y.is_finite()
7196                        && aabb.w.is_finite()
7197                        && aabb.h.is_finite()
7198                        && aabb.w > 0.0
7199                        && aabb.h > 0.0
7200                        && mesh
7201                            .as_ref()
7202                            .is_some_and(|mesh| mesh.vertex_count > 0 && mesh.index_count > 0);
7203                    if blocked || !has_area || (!difference && scissor.2 == 0) {
7204                        blocked = blocked || !difference;
7205                    } else if let Some(mesh_gpu) = mesh.clone()
7206                        && let Some(uoff) = self.alloc_mesh_uniform(mesh_uniform_from_paint(
7207                            local_affine,
7208                            &repose_core::PaintDesc::Solid,
7209                        ))
7210                    {
7211                        command = Some(Cmd::VectorClipPush {
7212                            mesh: mesh_gpu.clone(),
7213                            uoff,
7214                            scissor,
7215                            difference,
7216                            applied: true,
7217                        });
7218                        replayed.push(ActiveClip::Vector {
7219                            mesh: Some(mesh_gpu),
7220                            source: source.clone(),
7221                            uoff,
7222                            affine: local_affine,
7223                            scissor,
7224                            difference,
7225                            applied: true,
7226                            blocked: false,
7227                        });
7228                    } else {
7229                        blocked = blocked || !difference;
7230                    }
7231                    current = next;
7232                }
7233            }
7234            if blocked {
7235                current = repose_core::Rect {
7236                    x: 0.0,
7237                    y: 0.0,
7238                    w: 0.0,
7239                    h: 0.0,
7240                };
7241            }
7242            if command.is_none() {
7243                replayed.push(match clip {
7244                    ActiveClip::Rect {
7245                        rect,
7246                        radii,
7247                        scissor,
7248                        difference,
7249                        ..
7250                    } => ActiveClip::Rect {
7251                        off: 0,
7252                        cnt: 0,
7253                        rect: *rect,
7254                        radii: *radii,
7255                        scissor: *scissor,
7256                        difference: *difference,
7257                        applied: false,
7258                        blocked,
7259                    },
7260                    ActiveClip::Vector {
7261                        mesh,
7262                        source,
7263                        affine,
7264                        scissor,
7265                        difference,
7266                        ..
7267                    } => ActiveClip::Vector {
7268                        mesh: mesh.clone(),
7269                        source: source.clone(),
7270                        uoff: 0,
7271                        affine: parent_affine_to_layer_affine(*affine, parent_transform, origin)
7272                            .unwrap_or([0.0; 6]),
7273                        scissor: *scissor,
7274                        difference: *difference,
7275                        applied: false,
7276                        blocked,
7277                    },
7278                });
7279            }
7280            if let Some(command) = command {
7281                pass.cmds.push(command);
7282            }
7283            pass.active_scissor = if blocked {
7284                None
7285            } else {
7286                let scissor = rect_to_scissor(
7287                    current,
7288                    target_size.0.max(1.0) as u32,
7289                    target_size.1.max(1.0) as u32,
7290                );
7291                (scissor.2 > 0 && scissor.3 > 0).then_some(scissor)
7292            };
7293        }
7294        replayed
7295    }
7296
7297    #[allow(clippy::too_many_arguments)]
7298    fn push_perspective_layer(
7299        &mut self,
7300        node: Transform,
7301        top: Transform,
7302        transform_stack: &mut Vec<Transform>,
7303        scissor_stack: &mut Vec<repose_core::Rect>,
7304        root_clip_rect: &mut repose_core::Rect,
7305        current_target_size: &mut (f32, f32),
7306        current_pass: &mut Pass,
7307        active_clips: &mut Vec<ActiveClip>,
7308        passes: &mut Vec<Pass>,
7309        target_stack: &mut Vec<PassTarget>,
7310        flatten_stack: &mut Vec<FlattenRecord>,
7311        id_head: &mut u32,
7312        pass_id_head: &mut u64,
7313        ids_used: &mut Vec<u32>,
7314        encoder: &mut wgpu::CommandEncoder,
7315    ) {
7316        // Full projective map: affine ancestors over the node's map.
7317        // Ancestors are affine by construction (perspective always flattens
7318        // at push, and only stripped affines reach the stack).
7319        let map =
7320            Transform::compose_projective(&top.projective_matrix(), &node.projective_matrix());
7321        let scr = scissor_stack.last().copied().unwrap_or(*root_clip_rect);
7322        let max_dimension = self.device.limits().max_texture_dimension_2d as f32;
7323        if !scr.w.is_finite()
7324            || !scr.h.is_finite()
7325            || scr.w < 1.0
7326            || scr.h < 1.0
7327            || scr.w > max_dimension
7328            || scr.h > max_dimension
7329        {
7330            return;
7331        }
7332        let w = scr.w.ceil();
7333        let h = scr.h.ceil();
7334        let layer_rect = repose_core::Rect {
7335            x: scr.x,
7336            y: scr.y,
7337            w,
7338            h,
7339        };
7340        if ids_used.len() >= MAX_TRANSIENT_GRAPHICS_LAYERS {
7341            return;
7342        }
7343        let layer_id = *id_head;
7344        *id_head = id_head.wrapping_add(1);
7345        ids_used.push(layer_id);
7346
7347        let stack_len = transform_stack.len();
7348        // Children render with the ancestors' map only: the node's own
7349        // affine part lives in `map` and applies once, at composite time.
7350        // (Pushing the stripped affine here too would foreshorten twice.)
7351        transform_stack.push(top);
7352        transform_stack.push(Transform::translate(-layer_rect.x, -layer_rect.y));
7353
7354        let saved_scissor_stack = std::mem::replace(
7355            scissor_stack,
7356            vec![repose_core::Rect {
7357                x: 0.0,
7358                y: 0.0,
7359                w,
7360                h,
7361            }],
7362        );
7363        let saved_root = std::mem::replace(
7364            root_clip_rect,
7365            repose_core::Rect {
7366                x: 0.0,
7367                y: 0.0,
7368                w,
7369                h,
7370            },
7371        );
7372        let saved_size = std::mem::replace(current_target_size, (w, h));
7373        let prev_target = current_pass.target;
7374        let saved_active_scissor = current_pass.active_scissor;
7375        let saved_clips = std::mem::take(active_clips);
7376        let layer_pass_id = *pass_id_head;
7377        *pass_id_head += 1;
7378        let mut layer_pass = Pass {
7379            id: layer_pass_id,
7380            target: PassTarget::Layer(layer_id),
7381            initial_scissor: (0, 0, w as u32, h as u32),
7382            active_scissor: Some((0, 0, w as u32, h as u32)),
7383            clear_color: Some([0.0, 0.0, 0.0, 0.0]),
7384            cmds: Vec::new(),
7385        };
7386        let layer_clips = self.replay_active_clips(
7387            &saved_clips,
7388            &top,
7389            (layer_rect.x, layer_rect.y),
7390            (w, h),
7391            &mut layer_pass,
7392            encoder,
7393        );
7394        *active_clips = layer_clips;
7395        let saved = std::mem::replace(current_pass, layer_pass);
7396        passes.push(saved);
7397        target_stack.push(prev_target);
7398        self.get_or_create_layer(layer_id, w as u32, h as u32, layer_rect, true);
7399        *current_target_size = (w, h);
7400        flatten_stack.push(FlattenRecord {
7401            stack_len,
7402            layer_id,
7403            map,
7404            layer_rect,
7405            saved_scissor_stack,
7406            saved_root,
7407            saved_size,
7408            saved_active_scissor,
7409            saved_clips,
7410        });
7411    }
7412
7413    /// Close a flatten layer: restore the parent target and composite the
7414    /// layer texture through the recorded projective map.
7415    #[allow(clippy::too_many_arguments)]
7416    fn pop_perspective_layer(
7417        &mut self,
7418        rec: FlattenRecord,
7419        scissor_stack: &mut Vec<repose_core::Rect>,
7420        root_clip_rect: &mut repose_core::Rect,
7421        current_target_size: &mut (f32, f32),
7422        current_pass: &mut Pass,
7423        active_clips: &mut Vec<ActiveClip>,
7424        passes: &mut Vec<Pass>,
7425        target_stack: &mut Vec<PassTarget>,
7426        pass_id_head: &mut u64,
7427        encoder: &mut wgpu::CommandEncoder,
7428    ) {
7429        *scissor_stack = rec.saved_scissor_stack;
7430        *root_clip_rect = rec.saved_root;
7431        *current_target_size = rec.saved_size;
7432        *active_clips = rec.saved_clips;
7433        let resumed_pass_id = *pass_id_head;
7434        *pass_id_head += 1;
7435        let saved = std::mem::replace(
7436            current_pass,
7437            Pass {
7438                id: resumed_pass_id,
7439                target: target_stack.pop().unwrap_or(PassTarget::Surface),
7440                initial_scissor: rec.saved_active_scissor.unwrap_or((0, 0, 1, 1)),
7441                active_scissor: rec.saved_active_scissor,
7442                clear_color: None,
7443                cmds: Vec::new(),
7444            },
7445        );
7446        passes.push(saved);
7447
7448        // Project the layer-rect corners (parent space) to NDC in the
7449        // resumed (parent) target, keeping each corner's homogeneous w for
7450        // perspective-correct sampling.
7451        let (tw, th) = rec.saved_size;
7452        let r = rec.layer_rect;
7453        let corners = [
7454            (r.x, r.y),
7455            (r.x + r.w, r.y),
7456            (r.x + r.w, r.y + r.h),
7457            (r.x, r.y + r.h),
7458        ];
7459        let mut ndc = [[0.0f32; 2]; 4];
7460        let mut ws = [1.0f32; 4];
7461        let mut all_behind = true;
7462        for (i, (x, y)) in corners.iter().enumerate() {
7463            let w_raw = rec.map[6] * x + rec.map[7] * y + rec.map[8];
7464            let w = if w_raw.abs() < 1e-6 {
7465                if w_raw < 0.0 { -1e-6 } else { 1e-6 }
7466            } else {
7467                w_raw
7468            };
7469            if w > 0.0 {
7470                all_behind = false;
7471            }
7472            let px = (rec.map[0] * x + rec.map[1] * y + rec.map[2]) / w;
7473            let py = (rec.map[3] * x + rec.map[4] * y + rec.map[5]) / w;
7474            ndc[i] = [px / tw * 2.0 - 1.0, 1.0 - py / th * 2.0];
7475            ws[i] = w;
7476        }
7477        if all_behind {
7478            // Entire subtree behind the viewer: nothing to composite (the
7479            // layer pass still ran, but its output is correctly discarded).
7480            return;
7481        }
7482        let Some(layer) = self.layer_pool.get(&rec.layer_id) else {
7483            return;
7484        };
7485        let uv_u1 = layer.rect_px.2 / layer.width.max(1) as f32;
7486        let uv_v1 = layer.rect_px.3 / layer.height.max(1) as f32;
7487        let inst = ProjectiveInstance {
7488            c0: ndc[0],
7489            c1: ndc[1],
7490            c2: ndc[2],
7491            c3: ndc[3],
7492            uv: [0.0, 0.0, uv_u1, uv_v1],
7493            w: ws,
7494            alpha: 1.0,
7495            _pad: [0.0; 3],
7496        };
7497        if self
7498            .projective_ring
7499            .grow_to_fit(
7500                &self.device,
7501                encoder,
7502                std::mem::size_of::<ProjectiveInstance>() as u64,
7503            )
7504            .is_err()
7505        {
7506            return;
7507        }
7508        let bytes = bytemuck::bytes_of(&inst);
7509        let Ok(off) = self.projective_ring.alloc_write(&self.queue, bytes) else {
7510            return;
7511        };
7512        current_pass.cmds.push(Cmd::CompositeProjective {
7513            off,
7514            cnt: 1,
7515            layer_id: rec.layer_id,
7516        });
7517    }
7518
7519    fn mesh_cache_key(mesh: &Arc<repose_core::VectorMeshData>) -> MeshCacheKey {
7520        MeshCacheKey {
7521            source: Arc::as_ptr(mesh) as usize,
7522            index: 0,
7523            kind: 1,
7524            vertex_len: mesh.vertices.len(),
7525            index_len: mesh.indices.len(),
7526        }
7527    }
7528
7529    fn overlay_mesh_cache_key(
7530        meshes: &Arc<[repose_core::VectorMeshData]>,
7531        index: usize,
7532    ) -> MeshCacheKey {
7533        let mesh = &meshes[index];
7534        MeshCacheKey {
7535            source: meshes.as_ptr() as usize,
7536            index,
7537            kind: 2,
7538            vertex_len: mesh.vertices.len(),
7539            index_len: mesh.indices.len(),
7540        }
7541    }
7542
7543    fn mesh_owner_matches(owner: &MeshOwner, other: &MeshOwner) -> bool {
7544        match (owner, other) {
7545            (MeshOwner::Object(a), MeshOwner::Object(b)) => Arc::ptr_eq(a, b),
7546            (MeshOwner::Overlay(a), MeshOwner::Overlay(b)) => Arc::ptr_eq(a, b),
7547            _ => false,
7548        }
7549    }
7550
7551    fn evict_mesh_cache(&mut self) -> bool {
7552        let victim = self
7553            .mesh_cache
7554            .iter()
7555            .filter(|(_, entry)| entry.last_used_frame != self.frame_index)
7556            .min_by_key(|(_, entry)| entry.last_touch_frame)
7557            .map(|(key, _)| *key);
7558        let Some(victim) = victim else {
7559            return false;
7560        };
7561        let Some(entry) = self.mesh_cache.remove(&victim) else {
7562            return false;
7563        };
7564        self.mesh_cache_bytes_total = self.mesh_cache_bytes_total.saturating_sub(entry.gpu.bytes);
7565        true
7566    }
7567
7568    fn reserve_mesh_cache(&mut self, bytes: u64) -> bool {
7569        while self.mesh_cache.len() >= MAX_MESH_CACHE_ENTRIES
7570            || self.mesh_cache_bytes_total.saturating_add(bytes) > MAX_MESH_CACHE_BYTES
7571        {
7572            if !self.evict_mesh_cache() {
7573                return false;
7574            }
7575        }
7576        true
7577    }
7578
7579    fn reserve_mesh_gpu_budget(&mut self, bytes: u64) -> bool {
7580        while self.gpu_bytes_total().saturating_add(bytes) > self.gpu_budget_bytes {
7581            if !self.evict_mesh_cache() {
7582                return false;
7583            }
7584        }
7585        true
7586    }
7587
7588    fn upload_mesh_geometry_with_owner(
7589        &mut self,
7590        mesh: &repose_core::VectorMeshData,
7591        key: MeshCacheKey,
7592        owner: MeshOwner,
7593    ) -> Option<MeshGpu> {
7594        let touch = self.next_cache_touch();
7595        let cached = self
7596            .mesh_cache
7597            .get(&key)
7598            .filter(|entry| Self::mesh_owner_matches(&entry.owner, &owner))
7599            .map(|entry| entry.gpu.clone());
7600        if let Some(gpu) = cached {
7601            if let Some(entry) = self.mesh_cache.get_mut(&key) {
7602                entry.last_used_frame = self.frame_index;
7603                entry.last_touch_frame = touch;
7604            }
7605            return Some(gpu);
7606        }
7607        if let Some(entry) = self.mesh_cache.remove(&key) {
7608            self.mesh_cache_bytes_total =
7609                self.mesh_cache_bytes_total.saturating_sub(entry.gpu.bytes);
7610        }
7611        if mesh.vertices.is_empty() || mesh.indices.is_empty() {
7612            log::warn!("vector mesh has no geometry");
7613            return None;
7614        }
7615        if mesh.indices.len() % 3 != 0 {
7616            log::warn!("vector mesh index count is not a triangle list");
7617            return None;
7618        }
7619        let vertex_count = u32::try_from(mesh.vertices.len()).ok()?;
7620        let index_count = u32::try_from(mesh.indices.len()).ok()?;
7621        if mesh.indices.iter().any(|&index| index >= vertex_count) {
7622            log::warn!("vector mesh index is outside its vertex buffer");
7623            return None;
7624        }
7625        if mesh.vertices.iter().any(|vertex| {
7626            vertex.pos.iter().any(|value| !value.is_finite())
7627                || vertex.color.iter().any(|value| !value.is_finite())
7628                || vertex.uv.iter().any(|value| !value.is_finite())
7629        }) {
7630            log::warn!("vector mesh contains non-finite vertex data");
7631            return None;
7632        }
7633        let verts: Vec<MeshVertex> = mesh
7634            .vertices
7635            .iter()
7636            .map(|v| MeshVertex {
7637                pos: v.pos,
7638                color: v.color,
7639                uv: v.uv,
7640            })
7641            .collect();
7642        let vbytes = bytemuck::cast_slice(&verts);
7643        let ibytes = bytemuck::cast_slice(&mesh.indices[..]);
7644        let bytes = u64::try_from(vbytes.len())
7645            .ok()?
7646            .checked_add(u64::try_from(ibytes.len()).ok()?)?;
7647        let max_buffer_size = self.device.limits().max_buffer_size;
7648        if vbytes.len() as u64 > max_buffer_size || ibytes.len() as u64 > max_buffer_size {
7649            return None;
7650        }
7651        let cacheable = bytes <= MAX_MESH_CACHE_BYTES
7652            && self.reserve_mesh_cache(bytes)
7653            && self.reserve_mesh_gpu_budget(bytes);
7654        let vertices = self.device.create_buffer(&wgpu::BufferDescriptor {
7655            label: Some("vector mesh vertices"),
7656            size: vbytes.len().max(4) as u64,
7657            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
7658            mapped_at_creation: false,
7659        });
7660        let indices = self.device.create_buffer(&wgpu::BufferDescriptor {
7661            label: Some("vector mesh indices"),
7662            size: ibytes.len().max(4) as u64,
7663            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
7664            mapped_at_creation: false,
7665        });
7666        self.queue.write_buffer(&vertices, 0, vbytes);
7667        self.queue.write_buffer(&indices, 0, ibytes);
7668        let gpu = MeshGpu {
7669            vertices,
7670            indices,
7671            vertex_count,
7672            index_count,
7673            bytes,
7674        };
7675        if cacheable {
7676            let touch = self.next_cache_touch();
7677            self.mesh_cache.insert(
7678                key,
7679                MeshCacheEntry {
7680                    owner,
7681                    gpu: gpu.clone(),
7682                    last_used_frame: self.frame_index,
7683                    last_touch_frame: touch,
7684                },
7685            );
7686            self.mesh_cache_bytes_total = self.mesh_cache_bytes_total.saturating_add(bytes);
7687        }
7688        Some(gpu)
7689    }
7690
7691    fn upload_mesh_geometry(&mut self, mesh: &Arc<repose_core::VectorMeshData>) -> Option<MeshGpu> {
7692        self.upload_mesh_geometry_with_owner(
7693            mesh,
7694            Self::mesh_cache_key(mesh),
7695            MeshOwner::Object(mesh.clone()),
7696        )
7697    }
7698
7699    fn upload_overlay_mesh(
7700        &mut self,
7701        meshes: &Arc<[repose_core::VectorMeshData]>,
7702        index: usize,
7703    ) -> Option<MeshGpu> {
7704        self.upload_mesh_geometry_with_owner(
7705            &meshes[index],
7706            Self::overlay_mesh_cache_key(meshes, index),
7707            MeshOwner::Overlay(meshes.clone()),
7708        )
7709    }
7710
7711    fn alloc_mesh_uniform(&mut self, u: MeshUniform) -> Option<u64> {
7712        let size = std::mem::size_of::<MeshUniform>() as u64;
7713        let next = self
7714            .mesh_uniform_head
7715            .checked_add(self.mesh_uniform_alignment)?;
7716        if next > self.mesh_uniform_cap || next > u64::from(u32::MAX) {
7717            log::warn!("mesh uniform buffer exhausted; remaining meshes skipped");
7718            return None;
7719        }
7720        let slot = self.mesh_uniform_head;
7721        if slot.checked_add(size)? > self.mesh_uniform_cap {
7722            return None;
7723        }
7724        self.queue
7725            .write_buffer(&self.mesh_uniform_buf, slot, bytemuck::bytes_of(&u));
7726        self.mesh_uniform_head = next;
7727        Some(slot)
7728    }
7729
7730    /// Render one backdrop-dependent blend mesh: isolate the mesh into a
7731    /// translator-owned graphics layer, then composite it over the current
7732    /// target with the backdrop-blend shader. Works for surface parents and
7733    /// layer parents alike: the snapshot copy, isolation layer, and
7734    /// composite quad are all expressed in the parent target's pixel space.
7735    /// Callers must invoke this while the current pass targets the recorded
7736    /// parent: the translator never splits passes between here and the
7737    /// appended composite (only `BeginLayer`/perspective push new passes,
7738    /// and neither can intervene mid-call). The executor re-checks this
7739    /// (`BlendLayer.parent`) and skips a misplaced composite rather than
7740    /// drawing over the wrong target.
7741    #[allow(clippy::too_many_arguments)]
7742    fn emit_isolated_blend(
7743        &mut self,
7744        mesh: std::sync::Arc<repose_core::VectorMeshData>,
7745        transform: [f32; 6],
7746        paint: repose_core::PaintDesc,
7747        blend: repose_core::BlendMode,
7748        current_transform: &repose_core::Transform,
7749        current_pass: &mut Pass,
7750        active_clips: &[ActiveClip],
7751        passes: &mut Vec<Pass>,
7752        id_head: &mut u32,
7753        pass_id_head: &mut u64,
7754        ids_used: &mut Vec<u32>,
7755        scissor: (u32, u32, u32, u32),
7756        encoder: &mut wgpu::CommandEncoder,
7757        fb_w: f32,
7758        fb_h: f32,
7759    ) {
7760        if scissor.2 == 0 || scissor.3 == 0 || active_clips.iter().any(ActiveClip::blocked) {
7761            return;
7762        }
7763        let parent_target = current_pass.target;
7764        let (parent_w, parent_h) = match parent_target {
7765            PassTarget::Surface => (fb_w, fb_h),
7766            PassTarget::Layer(id) => match self.layer_pool.get(&id) {
7767                Some(layer) => (layer.width as f32, layer.height as f32),
7768                None => return,
7769            },
7770        };
7771        let affine = combine_mesh_affine(current_transform, transform);
7772        let local_rect = intersect(
7773            mesh_aabb(&mesh, affine),
7774            repose_core::Rect {
7775                x: 0.0,
7776                y: 0.0,
7777                w: parent_w,
7778                h: parent_h,
7779            },
7780        );
7781        if !local_rect.x.is_finite()
7782            || !local_rect.y.is_finite()
7783            || !local_rect.w.is_finite()
7784            || !local_rect.h.is_finite()
7785            || local_rect.w <= 0.0
7786            || local_rect.h <= 0.0
7787            || local_rect.w > self.device.limits().max_texture_dimension_2d as f32
7788            || local_rect.h > self.device.limits().max_texture_dimension_2d as f32
7789        {
7790            return;
7791        }
7792        let layer_rect = repose_core::Rect {
7793            x: local_rect.x.floor(),
7794            y: local_rect.y.floor(),
7795            w: (local_rect.x + local_rect.w).ceil() - local_rect.x.floor(),
7796            h: (local_rect.y + local_rect.h).ceil() - local_rect.y.floor(),
7797        };
7798        let w = layer_rect.w;
7799        let h = layer_rect.h;
7800        let local_rect = layer_rect;
7801        if ids_used.len() >= MAX_TRANSIENT_GRAPHICS_LAYERS {
7802            return;
7803        }
7804        let layer_id = *id_head;
7805        *id_head = id_head.wrapping_add(1);
7806        ids_used.push(layer_id);
7807
7808        // Snapshot the parent target region before compositing: the blend
7809        // shader samples it as the backdrop. The copy runs at execution
7810        // time, after all earlier passes have rendered.
7811        let snapshot_format = match parent_target {
7812            PassTarget::Surface if self.working_space => wgpu::TextureFormat::Rgba16Float,
7813            PassTarget::Surface => self.output_format,
7814            PassTarget::Layer(_) => self.layer_target_format(),
7815        };
7816        if !self.alloc_blend_snapshot(layer_id, w as u32, h as u32, snapshot_format) {
7817            log::warn!("blend snapshot budget exhausted; blend skipped");
7818            return;
7819        }
7820        let source_pass_id = *pass_id_head;
7821        *pass_id_head += 1;
7822        let resumed_pass_id = *pass_id_head;
7823        *pass_id_head += 1;
7824        self.blend_copies.push(BlendCopy {
7825            pass_id: resumed_pass_id,
7826            blend_id: layer_id,
7827            target: parent_target,
7828            region: layer_rect,
7829        });
7830
7831        // Render the mesh alone into the layer (layer-local shift so the
7832        // layer owns exactly the mesh bbox), then resume the parent pass.
7833        // The composite runs in the resumed parent pass.
7834        let mut layer_pass = Pass {
7835            id: source_pass_id,
7836            target: PassTarget::Layer(layer_id),
7837            initial_scissor: (0, 0, w as u32, h as u32),
7838            active_scissor: Some((0, 0, w as u32, h as u32)),
7839            clear_color: Some([0.0, 0.0, 0.0, 0.0]),
7840            cmds: Vec::new(),
7841        };
7842        self.replay_active_clips(
7843            active_clips,
7844            current_transform,
7845            (local_rect.x, local_rect.y),
7846            (w, h),
7847            &mut layer_pass,
7848            encoder,
7849        );
7850        self.get_or_create_layer(layer_id, w as u32, h as u32, layer_rect, true);
7851        let shift = repose_core::Transform::translate(-local_rect.x, -local_rect.y);
7852        let local = current_transform.combine(&shift);
7853        self.emit_vector_mesh(
7854            &local,
7855            &mesh,
7856            transform,
7857            &paint,
7858            repose_core::BlendMode::Alpha,
7859            &mut layer_pass.cmds,
7860        );
7861        let saved = std::mem::replace(
7862            current_pass,
7863            Pass {
7864                id: resumed_pass_id,
7865                target: parent_target,
7866                initial_scissor: scissor,
7867                active_scissor: Some(scissor),
7868                clear_color: None,
7869                cmds: Vec::new(),
7870            },
7871        );
7872        passes.push(layer_pass);
7873        passes.push(saved);
7874
7875        // Composite quad over the mesh bbox in the parent target's pixel
7876        // space. The executor maps this NDC against `parent`, so layer
7877        // parents at any origin work.
7878        let ndc = {
7879            let cx = local_rect.x + local_rect.w * 0.5;
7880            let cy = local_rect.y + local_rect.h * 0.5;
7881            let ndc_cx = (cx / parent_w) * 2.0 - 1.0;
7882            let ndc_cy = 1.0 - (cy / parent_h) * 2.0;
7883            let ndc_w = (local_rect.w / parent_w) * 2.0;
7884            let ndc_h = (local_rect.h / parent_h) * 2.0;
7885            [ndc_cx, ndc_cy, ndc_w, ndc_h]
7886        };
7887        let inst = BlendInstance {
7888            xywh: ndc,
7889            uv: [0.0, 0.0, 1.0, 1.0],
7890            color: [1.0, 1.0, 1.0, 1.0],
7891            fwd_mat: [1.0, 0.0, 0.0, 1.0],
7892            mode: blend.shader_mode(),
7893            _pad: [0.0; 3],
7894        };
7895        if self
7896            .blend_ring
7897            .grow_to_fit(
7898                &self.device,
7899                encoder,
7900                std::mem::size_of::<BlendInstance>() as u64,
7901            )
7902            .is_err()
7903        {
7904            return;
7905        }
7906        let bytes = bytemuck::bytes_of(&inst);
7907        let Ok(off) = self.blend_ring.alloc_write(&self.queue, bytes) else {
7908            return;
7909        };
7910        current_pass.cmds.push(Cmd::BlendLayer {
7911            off,
7912            cnt: 1,
7913            src_layer: layer_id,
7914            dst_layer: Some(layer_id),
7915            parent: parent_target,
7916            scissor,
7917        });
7918    }
7919
7920    fn recycle_blend_snapshot(&mut self, snapshot: BlendSnapshot) {
7921        let key = snapshot.key;
7922        if let Some(old) = self.blend_snapshot_pool.insert(key, snapshot) {
7923            self.blend_snapshot_bytes_total =
7924                self.blend_snapshot_bytes_total.saturating_sub(old.bytes);
7925        }
7926        while self.blend_snapshot_bytes_total > MAX_BLEND_SNAPSHOT_BYTES
7927            || self.blend_snapshot_pool.len() > MAX_BLEND_SNAPSHOT_POOL_ENTRIES
7928        {
7929            let Some(victim) = self.blend_snapshot_pool.keys().next().copied() else {
7930                break;
7931            };
7932            if let Some(snapshot) = self.blend_snapshot_pool.remove(&victim) {
7933                self.blend_snapshot_bytes_total = self
7934                    .blend_snapshot_bytes_total
7935                    .saturating_sub(snapshot.bytes);
7936            }
7937        }
7938    }
7939
7940    fn recycle_blend_snapshots(&mut self) {
7941        for (_, snapshot) in std::mem::take(&mut self.blend_snapshots) {
7942            self.recycle_blend_snapshot(snapshot);
7943        }
7944    }
7945
7946    fn alloc_blend_snapshot(
7947        &mut self,
7948        layer_id: u32,
7949        w: u32,
7950        h: u32,
7951        format: wgpu::TextureFormat,
7952    ) -> bool {
7953        let key = BlendSnapshotKey {
7954            format,
7955            width: w,
7956            height: h,
7957        };
7958        if self
7959            .blend_snapshots
7960            .get(&layer_id)
7961            .is_some_and(|snapshot| snapshot.key == key)
7962        {
7963            return true;
7964        }
7965        if w == 0 || h == 0 {
7966            return false;
7967        }
7968        if let Some(old) = self.blend_snapshots.remove(&layer_id) {
7969            self.recycle_blend_snapshot(old);
7970        }
7971        if let Some(snapshot) = self.blend_snapshot_pool.remove(&key) {
7972            self.blend_snapshots.insert(layer_id, snapshot);
7973            return true;
7974        }
7975        let Ok(bytes) = texture_storage_bytes(format, w, h) else {
7976            return false;
7977        };
7978        if bytes > MAX_BLEND_SNAPSHOT_BYTES {
7979            return false;
7980        }
7981        while self.blend_snapshot_bytes_total.saturating_add(bytes) > MAX_BLEND_SNAPSHOT_BYTES
7982            || self.gpu_bytes_total().saturating_add(bytes) > self.gpu_budget_bytes
7983        {
7984            let Some(victim) = self.blend_snapshot_pool.keys().next().copied() else {
7985                return false;
7986            };
7987            let Some(snapshot) = self.blend_snapshot_pool.remove(&victim) else {
7988                return false;
7989            };
7990            self.blend_snapshot_bytes_total = self
7991                .blend_snapshot_bytes_total
7992                .saturating_sub(snapshot.bytes);
7993        }
7994        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
7995            label: Some("blend backdrop snapshot"),
7996            size: wgpu::Extent3d {
7997                width: w,
7998                height: h,
7999                depth_or_array_layers: 1,
8000            },
8001            mip_level_count: 1,
8002            sample_count: 1,
8003            dimension: wgpu::TextureDimension::D2,
8004            format,
8005            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
8006            view_formats: &[],
8007        });
8008        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
8009        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
8010            label: Some("blend snapshot bind"),
8011            layout: &self.image_bind_layout_rgba,
8012            entries: &[
8013                wgpu::BindGroupEntry {
8014                    binding: 0,
8015                    resource: wgpu::BindingResource::TextureView(&view),
8016                },
8017                wgpu::BindGroupEntry {
8018                    binding: 1,
8019                    resource: wgpu::BindingResource::Sampler(&self.layer_sampler),
8020                },
8021            ],
8022        });
8023        self.blend_snapshot_bytes_total = self.blend_snapshot_bytes_total.saturating_add(bytes);
8024        self.blend_snapshots.insert(
8025            layer_id,
8026            BlendSnapshot {
8027                key,
8028                texture: tex,
8029                bind,
8030                bytes,
8031            },
8032        );
8033        true
8034    }
8035
8036    fn remove_blend_snapshot(&mut self, layer_id: u32) {
8037        if let Some(snapshot) = self.blend_snapshots.remove(&layer_id) {
8038            self.recycle_blend_snapshot(snapshot);
8039        }
8040    }
8041
8042    fn blend_snapshot_texture(&self, layer_id: u32) -> Option<wgpu::Texture> {
8043        self.blend_snapshots
8044            .get(&layer_id)
8045            .map(|s| s.texture.clone())
8046    }
8047
8048    #[allow(clippy::too_many_arguments)]
8049    fn emit_vector_mesh(
8050        &mut self,
8051        current_transform: &Transform,
8052        mesh: &Arc<repose_core::VectorMeshData>,
8053        transform: [f32; 6],
8054        paint: &repose_core::PaintDesc,
8055        blend: repose_core::BlendMode,
8056        cmds: &mut Vec<Cmd>,
8057    ) {
8058        let affine = combine_mesh_affine(current_transform, transform);
8059        let Some(mesh_gpu) = self.upload_mesh_geometry(mesh) else {
8060            return;
8061        };
8062        let Some(uoff) = self.alloc_mesh_uniform(mesh_uniform_from_paint(affine, paint)) else {
8063            return;
8064        };
8065        cmds.push(Cmd::VectorMesh {
8066            mesh: mesh_gpu,
8067            uoff,
8068            blend,
8069        });
8070    }
8071
8072    fn touch_callback_scope(&mut self, scope: CallbackScopeKey) {
8073        self.callback_scope_clock = self.callback_scope_clock.wrapping_add(1);
8074        let tick = self.callback_scope_clock;
8075        self.callback_scope_uses.insert(
8076            scope,
8077            CallbackScopeUse {
8078                tick,
8079                frame: self.frame_index,
8080            },
8081        );
8082    }
8083
8084    fn remove_callback_scope(&mut self, scope: &CallbackScopeKey) {
8085        self.callback_scoped_resources.remove(scope);
8086        self.callback_scope_uses.remove(scope);
8087        self.callback_scope_payloads.remove(scope);
8088    }
8089
8090    fn evict_callback_scope(&mut self, protected: &HashSet<CallbackScopeKey>) -> bool {
8091        let candidate = self
8092            .callback_scoped_resources
8093            .keys()
8094            .filter(|scope| !protected.contains(*scope))
8095            .min_by_key(|scope| {
8096                self.callback_scope_uses
8097                    .get(*scope)
8098                    .map(|usage| usage.tick)
8099                    .unwrap_or(0)
8100            })
8101            .copied();
8102        let Some(scope) = candidate else {
8103            return false;
8104        };
8105        self.remove_callback_scope(&scope);
8106        true
8107    }
8108
8109    fn prune_callback_scopes(&mut self) {
8110        let current_frame = self.frame_index;
8111        let mut dead: Vec<(CallbackScopeKey, u64)> = self
8112            .callback_scope_uses
8113            .iter()
8114            .filter_map(|(scope, usage)| {
8115                let payload_dead = self
8116                    .callback_scope_payloads
8117                    .get(scope)
8118                    .is_none_or(|weak| weak.upgrade().is_none());
8119                (payload_dead && !scope.callback.reusable).then_some((*scope, usage.tick))
8120            })
8121            .collect();
8122        dead.sort_by_key(|(_, tick)| *tick);
8123        for (scope, _) in dead {
8124            self.remove_callback_scope(&scope);
8125        }
8126
8127        if self.callback_scoped_resources.len() <= MAX_CALLBACK_SCOPES {
8128            return;
8129        }
8130        let mut inactive: Vec<(CallbackScopeKey, u64)> = self
8131            .callback_scope_uses
8132            .iter()
8133            .filter_map(|(scope, usage)| {
8134                (usage.frame != current_frame).then_some((*scope, usage.tick))
8135            })
8136            .collect();
8137        inactive.sort_by_key(|(_, tick)| *tick);
8138        for (scope, _) in inactive {
8139            if self.callback_scoped_resources.len() <= MAX_CALLBACK_SCOPES {
8140                break;
8141            }
8142            self.remove_callback_scope(&scope);
8143        }
8144    }
8145
8146    pub fn begin_frame(&mut self) -> anyhow::Result<()> {
8147        if self.frame_active {
8148            self.last_render_error = Some("renderer frame is already active".into());
8149            anyhow::bail!("renderer frame is already active");
8150        }
8151        self.last_render_error = None;
8152        self.frame_active = true;
8153        self.frame_index = self.frame_index.wrapping_add(1);
8154        self.apply_atlas_pending();
8155        self.slug_cache.next_frame();
8156        if let Some(composite) = self.callback_resources.get_mut::<DepthComposite>() {
8157            composite.begin_frame();
8158        }
8159        for resources in self.callback_scoped_resources.values_mut() {
8160            if let Some(composite) = resources.get_mut::<DepthComposite>() {
8161                composite.begin_frame();
8162            }
8163        }
8164        Ok(())
8165    }
8166
8167    pub fn end_frame(&mut self) {
8168        if !self.frame_active {
8169            return;
8170        }
8171        if let Some(composite) = self.callback_resources.get_mut::<DepthComposite>() {
8172            composite.end_frame();
8173        }
8174        for resources in self.callback_scoped_resources.values_mut() {
8175            if let Some(composite) = resources.get_mut::<DepthComposite>() {
8176                composite.end_frame();
8177            }
8178        }
8179        self.prune_callback_scopes();
8180        self.evict_unused_images();
8181        self.frame_active = false;
8182    }
8183
8184    pub fn last_render_error(&self) -> Option<&str> {
8185        self.last_render_error.as_deref()
8186    }
8187
8188    pub fn render_scene_to_encoder(
8189        &mut self,
8190        scene: &Scene,
8191        encoder: &mut wgpu::CommandEncoder,
8192        target_view: &wgpu::TextureView,
8193        clear_color_override: Option<[f64; 4]>,
8194    ) {
8195        self.render_scene_to_encoder_with_texture(
8196            scene,
8197            encoder,
8198            target_view,
8199            None,
8200            clear_color_override,
8201        )
8202    }
8203
8204    /// Same as [`render_scene_to_encoder`](Self::render_scene_to_encoder),
8205    /// plus the render target texture for isolated-blend backdrop snapshots.
8206    /// Pass `None` when the texture is unavailable (swapchain views); then
8207    /// surface-targeted isolated blends are skipped (the isolated source
8208    /// layer composites nowhere, so the mesh disappears).
8209    pub fn render_scene_to_encoder_with_texture(
8210        &mut self,
8211        scene: &Scene,
8212        encoder: &mut wgpu::CommandEncoder,
8213        target_view: &wgpu::TextureView,
8214        target_texture: Option<&wgpu::Texture>,
8215        clear_color_override: Option<[f64; 4]>,
8216    ) {
8217        if self.output_width == 0 || self.output_height == 0 {
8218            return;
8219        }
8220        self.sync_text_caches();
8221        /// AABB of a rect under the *plain affine* part of a transform
8222        /// (linear + translation, no origin re-pivot).
8223        fn affine_aabb(transform: &Transform, rect: &repose_core::Rect) -> repose_core::Rect {
8224            let m = transform.linear();
8225            let (tx, ty) = (transform.translate_x, transform.translate_y);
8226            let corners = [
8227                (rect.x, rect.y),
8228                (rect.x + rect.w, rect.y),
8229                (rect.x, rect.y + rect.h),
8230                (rect.x + rect.w, rect.y + rect.h),
8231            ];
8232            let mut min_x = f32::INFINITY;
8233            let mut min_y = f32::INFINITY;
8234            let mut max_x = f32::NEG_INFINITY;
8235            let mut max_y = f32::NEG_INFINITY;
8236            for (x, y) in corners {
8237                let wx = m[0] * x + m[1] * y + tx;
8238                let wy = m[2] * x + m[3] * y + ty;
8239                min_x = min_x.min(wx);
8240                min_y = min_y.min(wy);
8241                max_x = max_x.max(wx);
8242                max_y = max_y.max(wy);
8243            }
8244            repose_core::Rect {
8245                x: min_x,
8246                y: min_y,
8247                w: (max_x - min_x).max(0.0),
8248                h: (max_y - min_y).max(0.0),
8249            }
8250        }
8251
8252        fn visible(aabb: repose_core::Rect, clip: repose_core::Rect) -> bool {
8253            if !aabb.x.is_finite()
8254                || !aabb.y.is_finite()
8255                || !aabb.w.is_finite()
8256                || !aabb.h.is_finite()
8257                || aabb.w <= 0.0
8258                || aabb.h <= 0.0
8259            {
8260                return false;
8261            }
8262            let aabb = repose_core::Rect {
8263                x: aabb.x - 1.0,
8264                y: aabb.y - 1.0,
8265                w: aabb.w + 2.0,
8266                h: aabb.h + 2.0,
8267            };
8268            let clipped = intersect(aabb, clip);
8269            clipped.x.is_finite()
8270                && clipped.y.is_finite()
8271                && clipped.w.is_finite()
8272                && clipped.h.is_finite()
8273                && clipped.w > 0.0
8274                && clipped.h > 0.0
8275        }
8276
8277        fn to_ndc(x: f32, y: f32, w: f32, h: f32, fb_w: f32, fb_h: f32) -> [f32; 4] {
8278            let x0 = (x / fb_w) * 2.0 - 1.0;
8279            let y0 = 1.0 - (y / fb_h) * 2.0;
8280            let x1 = ((x + w) / fb_w) * 2.0 - 1.0;
8281            let y1 = 1.0 - ((y + h) / fb_h) * 2.0;
8282            let min_x = x0.min(x1);
8283            let min_y = y0.min(y1);
8284            let w_ndc = (x1 - x0).abs();
8285            let h_ndc = (y1 - y0).abs();
8286            [min_x, min_y, w_ndc, h_ndc]
8287        }
8288
8289        /// Convert a local-space rect + transform to NDC center-based position+size
8290        /// plus the forward rotation/shear 2x2 (row-major `[m00, m01, m10, m11]`,
8291        /// scale-free: scale rides in the NDC size). Shaders apply it to quad
8292        /// corners and its adjugate/determinant inverse to sample positions.
8293        fn rect_to_instance_ndc(
8294            rect: repose_core::Rect,
8295            transform: &Transform,
8296            fb_w: f32,
8297            fb_h: f32,
8298        ) -> ([f32; 4], [f32; 4]) {
8299            let cx = rect.x + rect.w * 0.5;
8300            let cy = rect.y + rect.h * 0.5;
8301
8302            let m = transform.linear();
8303            let tx = m[0] * cx + m[1] * cy + transform.translate_x;
8304            let ty = m[2] * cx + m[3] * cy + transform.translate_y;
8305
8306            let ndc_cx = (tx / fb_w) * 2.0 - 1.0;
8307            let ndc_cy = 1.0 - (ty / fb_h) * 2.0;
8308            // NDC size (after scale only, no rotation - rotation is done in shader)
8309            let ndc_w = (rect.w * transform.scale_x / fb_w) * 2.0;
8310            let ndc_h = (rect.h * transform.scale_y / fb_h) * 2.0;
8311
8312            ([ndc_cx, ndc_cy, ndc_w, ndc_h], forward_rs_mat(transform))
8313        }
8314
8315        /// Forward rotation+shear 2x2 (row-major, scale-free) for instance
8316        /// attributes. Identity for untransformed content; degenerate shear
8317        /// (only from absurd inputs) falls back to identity.
8318        fn forward_rs_mat(transform: &Transform) -> [f32; 4] {
8319            let c = transform.rotate.cos();
8320            let s = transform.rotate.sin();
8321            let (hx, hy) = (transform.shear_x, transform.shear_y);
8322            let m = [c - s * hy, c * hx - s, s + c * hy, s * hx + c];
8323            if (m[0] * m[3] - m[1] * m[2]).abs() < 1e-6 {
8324                return [1.0, 0.0, 0.0, 1.0];
8325            }
8326            m
8327        }
8328
8329        fn to_scissor(r: &repose_core::Rect, fb_w: u32, fb_h: u32) -> (u32, u32, u32, u32) {
8330            if fb_w == 0
8331                || fb_h == 0
8332                || !r.x.is_finite()
8333                || !r.y.is_finite()
8334                || !r.w.is_finite()
8335                || !r.h.is_finite()
8336                || r.w <= 0.0
8337                || r.h <= 0.0
8338            {
8339                return (0, 0, 0, 0);
8340            }
8341            // Inward: a clip must not admit any pixel outside the rect it
8342            // came from. Rounding out let clipped content bleed a pixel past
8343            // every clip edge, which shows up as a 1px line hugging the edge
8344            // of a letterboxed or clipped view.
8345            let x0 = r.x.ceil().max(0.0).min(fb_w as f32);
8346            let y0 = r.y.ceil().max(0.0).min(fb_h as f32);
8347            let x1 = (r.x + r.w).floor().max(x0).min(fb_w as f32);
8348            let y1 = (r.y + r.h).floor().max(y0).min(fb_h as f32);
8349            if x1 <= x0 || y1 <= y0 {
8350                return (0, 0, 0, 0);
8351            }
8352            (x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32)
8353        }
8354
8355        let fb_w = self.output_width as f32;
8356        let fb_h = self.output_height as f32;
8357
8358        let mut passes: Vec<Pass> = Vec::with_capacity(1);
8359        let clear_color = clear_color_override
8360            .map(|color| {
8361                let alpha = color[3];
8362                [color[0] * alpha, color[1] * alpha, color[2] * alpha, alpha]
8363            })
8364            .unwrap_or_else(|| {
8365                let color = scene.clear_color.to_linear();
8366                let alpha = color[3] as f64;
8367                [
8368                    color[0] as f64 * alpha,
8369                    color[1] as f64 * alpha,
8370                    color[2] as f64 * alpha,
8371                    alpha,
8372                ]
8373            });
8374        let mut next_pass_id = 0u64;
8375        let mut current_pass: Pass = Pass {
8376            id: next_pass_id,
8377            target: PassTarget::Surface,
8378            initial_scissor: (0, 0, self.output_width, self.output_height),
8379            active_scissor: Some((0, 0, self.output_width, self.output_height)),
8380            clear_color: Some([
8381                clear_color[0] as f32,
8382                clear_color[1] as f32,
8383                clear_color[2] as f32,
8384                clear_color[3] as f32,
8385            ]),
8386            cmds: Vec::with_capacity(scene.nodes.len()),
8387        };
8388        next_pass_id += 1;
8389        let mut target_stack: Vec<PassTarget> = Vec::new();
8390        let mut layer_stack: Vec<LayerState> = Vec::new();
8391        let mut current_target_size: (f32, f32) = (fb_w, fb_h);
8392
8393        struct Batch {
8394            rects: Vec<RectInstance>,
8395            borders: Vec<BorderInstance>,
8396            ellipses: Vec<EllipseInstance>,
8397            e_borders: Vec<EllipseBorderInstance>,
8398            arcs: Vec<ArcInstance>,
8399            masks: Vec<GlyphInstance>,
8400            colors: Vec<GlyphInstance>,
8401        }
8402
8403        impl Batch {
8404            fn new() -> Self {
8405                Self {
8406                    rects: vec![],
8407                    borders: vec![],
8408                    ellipses: vec![],
8409                    e_borders: vec![],
8410                    arcs: vec![],
8411                    masks: vec![],
8412                    colors: vec![],
8413                }
8414            }
8415
8416            fn is_empty(&self) -> bool {
8417                self.rects.is_empty()
8418                    && self.borders.is_empty()
8419                    && self.ellipses.is_empty()
8420                    && self.e_borders.is_empty()
8421                    && self.arcs.is_empty()
8422                    && self.masks.is_empty()
8423                    && self.colors.is_empty()
8424            }
8425
8426            fn flush(
8427                &mut self,
8428                pipes: (
8429                    &mut InstancedPipe<RectInstance>,
8430                    &mut InstancedPipe<BorderInstance>,
8431                    &mut InstancedPipe<EllipseInstance>,
8432                    &mut InstancedPipe<EllipseBorderInstance>,
8433                    &mut InstancedPipe<ArcInstance>,
8434                ),
8435                glyph_pipes: (
8436                    &mut InstancedPipe<GlyphInstance>,
8437                    &mut InstancedPipe<GlyphInstance>,
8438                ),
8439                device: &wgpu::Device,
8440                queue: &wgpu::Queue,
8441                encoder: &mut wgpu::CommandEncoder,
8442                cmds: &mut Vec<Cmd>,
8443            ) {
8444                let (rects, borders, ellipses, e_borders, arcs) = pipes;
8445                let (masks, colors) = glyph_pipes;
8446
8447                macro_rules! flush_one {
8448                    ($buf:ident, $pipe:expr, $variant:ident) => {
8449                        if !self.$buf.is_empty() {
8450                            if let Some((off, cnt)) =
8451                                $pipe.upload(device, queue, encoder, &self.$buf)
8452                            {
8453                                cmds.push(Cmd::$variant { off, cnt });
8454                            }
8455                            self.$buf.clear();
8456                        }
8457                    };
8458                }
8459
8460                flush_one!(rects, rects, Rect);
8461                flush_one!(borders, borders, Border);
8462                flush_one!(ellipses, ellipses, Ellipse);
8463                flush_one!(e_borders, e_borders, EllipseBorder);
8464                flush_one!(arcs, arcs, Arc);
8465                flush_one!(masks, masks, GlyphsMask);
8466                flush_one!(colors, colors, GlyphsColor);
8467            }
8468        }
8469
8470        self.rects.reset();
8471        self.borders.reset();
8472        self.ellipses.reset();
8473        self.ellipse_borders.reset();
8474        self.arcs.reset();
8475        self.glyph_mask.reset();
8476        self.glyph_color.reset();
8477        self.clip_ring.reset();
8478        self.blur_ring.reset();
8479        self.nv12.reset();
8480
8481        self.slug_ring.reset();
8482        self.mesh_uniform_head = 0;
8483        self.projective_ring.reset();
8484        self.blend_ring.reset();
8485        for id in self.flatten_layer_ids.drain(..) {
8486            if let Some(layer) = self.layer_pool.remove(&id) {
8487                self.layer_bytes_total = self.layer_bytes_total.saturating_sub(layer.bytes);
8488                if layer.transient {
8489                    self.transient_layer_bytes_total =
8490                        self.transient_layer_bytes_total.saturating_sub(layer.bytes);
8491                }
8492            }
8493        }
8494        self.producer_layer_ids.clear();
8495        self.recycle_blend_snapshots();
8496        self.blend_copies.clear();
8497        let mut batch = Batch::new();
8498        let mut image_run: Option<(u64, ImageFilter, bool, Vec<GlyphInstance>)> = None;
8499        let mut nv12_run: Option<(u64, ImageFilter, Vec<Nv12Instance>)> = None;
8500        let mut slug_verts_local: Vec<slug::TessVertex> = Vec::new();
8501        let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
8502        let mut flatten_stack: Vec<FlattenRecord> = Vec::new();
8503        let mut flatten_id_head: u32 = FLATTEN_ID_BASE;
8504        let mut flatten_ids_used: Vec<u32> = Vec::new();
8505        let mut scissor_stack: Vec<repose_core::Rect> = Vec::with_capacity(8);
8506        let mut active_clips: Vec<ActiveClip> = Vec::with_capacity(8);
8507        let mut root_clip_rect = repose_core::Rect {
8508            x: 0.0,
8509            y: 0.0,
8510            w: fb_w,
8511            h: fb_h,
8512        };
8513
8514        let mut current_prim: Option<&'static str> = None;
8515
8516        macro_rules! flush_if_prim_changed {
8517            ($prim:literal, $pipe:expr) => {
8518                if current_prim != Some($prim) {
8519                    flush_batch!();
8520                    current_prim = Some($prim);
8521                }
8522            };
8523        }
8524
8525        macro_rules! flush_image_runs {
8526            () => {{
8527                if let Some((handle, filter, tile, instances)) = image_run.take()
8528                    && let Some((off, cnt)) =
8529                        self.glyph_color
8530                            .upload(&self.device, &self.queue, encoder, &instances)
8531                {
8532                    current_pass.cmds.push(Cmd::ImageRgba {
8533                        off,
8534                        cnt,
8535                        handle,
8536                        filter,
8537                        tile,
8538                    });
8539                }
8540                if let Some((handle, filter, instances)) = nv12_run.take()
8541                    && let Some((off, cnt)) =
8542                        self.nv12
8543                            .upload(&self.device, &self.queue, encoder, &instances)
8544                {
8545                    current_pass.cmds.push(Cmd::ImageNv12 {
8546                        off,
8547                        cnt,
8548                        handle,
8549                        filter,
8550                    });
8551                }
8552            }};
8553        }
8554
8555        macro_rules! flush_primitives {
8556            () => {{
8557                if !batch.is_empty() {
8558                    batch.flush(
8559                        (
8560                            &mut self.rects,
8561                            &mut self.borders,
8562                            &mut self.ellipses,
8563                            &mut self.ellipse_borders,
8564                            &mut self.arcs,
8565                        ),
8566                        (&mut self.glyph_mask, &mut self.glyph_color),
8567                        &self.device,
8568                        &self.queue,
8569                        encoder,
8570                        &mut current_pass.cmds,
8571                    )
8572                }
8573            }};
8574        }
8575
8576        macro_rules! flush_batch {
8577            () => {{
8578                flush_image_runs!();
8579                flush_primitives!();
8580            }};
8581        }
8582        let mut pending_layer_composite: Option<Cmd> = None;
8583        let shadow_layer_ids: HashSet<u32> = scene
8584            .nodes
8585            .iter()
8586            .filter_map(|node| match node {
8587                SceneNode::CompositeShadow { layer_id, .. } => Some(*layer_id),
8588                _ => None,
8589            })
8590            .collect();
8591        macro_rules! flush_pending_layer {
8592            () => {
8593                if let Some(command) = pending_layer_composite.take() {
8594                    current_pass.cmds.push(command);
8595                }
8596            };
8597        }
8598        for node in &scene.nodes {
8599            let t_identity = Transform::identity();
8600            let current_transform = transform_stack.last().unwrap_or(&t_identity);
8601
8602            match node {
8603                SceneNode::Rect {
8604                    rect,
8605                    brush,
8606                    radius,
8607                } => {
8608                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
8609                    if !visible(affine_aabb(current_transform, rect), clip) {
8610                        continue;
8611                    }
8612                    flush_if_prim_changed!("rect", &self.rects);
8613                    let (ndc, fwd_mat) = rect_to_instance_ndc(
8614                        *rect,
8615                        current_transform,
8616                        current_target_size.0,
8617                        current_target_size.1,
8618                    );
8619                    let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
8620                        brush_to_shape_fields(brush, rect, current_transform);
8621                    batch.rects.push(RectInstance {
8622                        xywh: ndc,
8623                        radii: radius.map(|r| r.0),
8624                        brush_type,
8625                        grad_kind,
8626                        _pad: [0.0; 2],
8627                        color0,
8628                        color1,
8629                        grad_p0,
8630                        grad_p1,
8631                        tile_mode,
8632                        _pad2: [0.0; 3],
8633                        fwd_mat,
8634                    });
8635                }
8636                SceneNode::Border {
8637                    rect,
8638                    brush,
8639                    width,
8640                    radius,
8641                } => {
8642                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
8643                    if !visible(affine_aabb(current_transform, rect), clip) {
8644                        continue;
8645                    }
8646                    flush_if_prim_changed!("border", &self.borders);
8647                    let (ndc, fwd_mat) = rect_to_instance_ndc(
8648                        *rect,
8649                        current_transform,
8650                        current_target_size.0,
8651                        current_target_size.1,
8652                    );
8653                    let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
8654                        brush_to_shape_fields(brush, rect, current_transform);
8655                    batch.borders.push(BorderInstance {
8656                        xywh: ndc,
8657                        radii: radius.map(|r| r.0),
8658                        stroke: width.0,
8659                        brush_type,
8660                        _pad: [0.0; 2],
8661                        grad_kind,
8662                        color0,
8663                        color1,
8664                        grad_p0,
8665                        grad_p1,
8666                        tile_mode,
8667                        _pad2: [0.0; 3],
8668                        fwd_mat,
8669                    });
8670                }
8671                SceneNode::Ellipse { rect, brush } => {
8672                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
8673                    if !visible(affine_aabb(current_transform, rect), clip) {
8674                        continue;
8675                    }
8676                    flush_if_prim_changed!("ellipse", &self.ellipses);
8677                    let (ndc, fwd_mat) = rect_to_instance_ndc(
8678                        *rect,
8679                        current_transform,
8680                        current_target_size.0,
8681                        current_target_size.1,
8682                    );
8683                    let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
8684                        brush_to_shape_fields(brush, rect, current_transform);
8685                    batch.ellipses.push(EllipseInstance {
8686                        xywh: ndc,
8687                        brush_type,
8688                        grad_kind,
8689                        _pad: [0.0; 2],
8690                        color0,
8691                        color1,
8692                        grad_p0,
8693                        grad_p1,
8694                        tile_mode,
8695                        _pad2: [0.0; 3],
8696                        fwd_mat,
8697                    });
8698                }
8699                SceneNode::EllipseBorder { rect, brush, width } => {
8700                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
8701                    let cull_pad = width.0.abs() * 0.5 + 2.0;
8702                    let cull_rect = repose_core::Rect {
8703                        x: rect.x - cull_pad,
8704                        y: rect.y - cull_pad,
8705                        w: rect.w + cull_pad * 2.0,
8706                        h: rect.h + cull_pad * 2.0,
8707                    };
8708                    if !visible(affine_aabb(current_transform, &cull_rect), clip) {
8709                        continue;
8710                    }
8711                    flush_if_prim_changed!("ellipse_border", &self.ellipse_borders);
8712                    let (ndc, fwd_mat) = rect_to_instance_ndc(
8713                        *rect,
8714                        current_transform,
8715                        current_target_size.0,
8716                        current_target_size.1,
8717                    );
8718                    let pad_px = width.0 * 0.5 + 2.0;
8719                    let pad = (pad_px / current_target_size.0.max(1.0)) * 2.0;
8720                    let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
8721                        brush_to_shape_fields(brush, rect, current_transform);
8722                    batch.e_borders.push(EllipseBorderInstance {
8723                        xywh: ndc,
8724                        stroke: width.0,
8725                        pad,
8726                        brush_type,
8727                        grad_kind,
8728                        color0,
8729                        color1,
8730                        grad_p0,
8731                        grad_p1,
8732                        tile_mode,
8733                        _pad2: [0.0; 3],
8734                        fwd_mat,
8735                    });
8736                }
8737                SceneNode::Arc {
8738                    rect,
8739                    start_angle,
8740                    sweep_angle,
8741                    stroke_width,
8742                    brush,
8743                    cap,
8744                } => {
8745                    if !start_angle.is_finite()
8746                        || !sweep_angle.is_finite()
8747                        || !stroke_width.0.is_finite()
8748                        || !rect.x.is_finite()
8749                        || !rect.y.is_finite()
8750                        || !rect.w.is_finite()
8751                        || !rect.h.is_finite()
8752                        || rect.w <= 0.0
8753                        || rect.h <= 0.0
8754                        || stroke_width.0 <= 0.0
8755                        || sweep_angle.abs() <= 1e-6
8756                    {
8757                        continue;
8758                    }
8759                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
8760                    let cull_pad = stroke_width.0.abs() * 0.5 + 2.0;
8761                    let cull_rect = repose_core::Rect {
8762                        x: rect.x - cull_pad,
8763                        y: rect.y - cull_pad,
8764                        w: rect.w + cull_pad * 2.0,
8765                        h: rect.h + cull_pad * 2.0,
8766                    };
8767                    if !visible(affine_aabb(current_transform, &cull_rect), clip) {
8768                        continue;
8769                    }
8770                    flush_if_prim_changed!("arc", &self.arcs);
8771                    let (ndc, fwd_mat) = rect_to_instance_ndc(
8772                        *rect,
8773                        current_transform,
8774                        current_target_size.0.max(1.0),
8775                        current_target_size.1.max(1.0),
8776                    );
8777                    let pad_px = stroke_width.0 * 0.5 + 2.0;
8778                    let pad = (pad_px / current_target_size.0.max(1.0)) * 2.0;
8779                    let cap_val = match cap {
8780                        StrokeCap::Butt => 0.0,
8781                        StrokeCap::Round => 1.0,
8782                        StrokeCap::Square => 2.0,
8783                    };
8784                    let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
8785                        brush_to_shape_fields(brush, rect, current_transform);
8786                    let (start, sweep) = if *sweep_angle < 0.0 {
8787                        (*start_angle + *sweep_angle, -*sweep_angle)
8788                    } else {
8789                        (*start_angle, *sweep_angle)
8790                    };
8791                    batch.arcs.push(ArcInstance {
8792                        xywh: ndc,
8793                        start_angle: start,
8794                        sweep_angle: sweep,
8795                        stroke: stroke_width.0,
8796                        pad,
8797                        brush_type,
8798                        grad_kind,
8799                        _pad0: [0.0; 2],
8800                        color0,
8801                        color1,
8802                        grad_p0,
8803                        grad_p1,
8804                        tile_mode,
8805                        cap: cap_val,
8806                        _pad1: [0.0; 2],
8807                        fwd_mat,
8808                    });
8809                }
8810                SceneNode::Text {
8811                    rect,
8812                    text,
8813                    color,
8814                    size,
8815                    font_family,
8816                    text_align: _,
8817                    font_weight,
8818                    font_style,
8819                    text_decoration,
8820                    letter_spacing,
8821                    line_height: _,
8822                    extra_style,
8823                    url: _,
8824                    font_variation_settings,
8825                } => {
8826                    let px = size.0;
8827                    if !px.is_finite()
8828                        || px <= 0.0
8829                        || !rect.x.is_finite()
8830                        || !rect.y.is_finite()
8831                        || !rect.w.is_finite()
8832                        || !rect.h.is_finite()
8833                    {
8834                        continue;
8835                    }
8836                    let margin = px * 4.0 + letter_spacing.0.abs() * 2.0;
8837                    let text_bounds = repose_core::Rect {
8838                        x: rect.x - margin,
8839                        y: rect.y - margin,
8840                        w: rect.w + margin * 2.0,
8841                        h: rect.h + margin * 2.0,
8842                    };
8843                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
8844                    if !visible(affine_aabb(current_transform, &text_bounds), clip) {
8845                        continue;
8846                    }
8847                    flush_batch!(); // flush any prior primitives
8848
8849                    let lh_ratio = rect.h / px;
8850                    let fw = font_weight.0;
8851                    let fs = if *font_style == FontStyle::Italic {
8852                        1
8853                    } else {
8854                        0
8855                    };
8856                    let shaped = self.shape_text_cached(
8857                        text,
8858                        px,
8859                        lh_ratio,
8860                        *font_family,
8861                        fw,
8862                        fs,
8863                        letter_spacing.0,
8864                        font_variation_settings.as_ref(),
8865                    );
8866                    let baseline_y = shaped.glyphs.first().map(|g| rect.y + g.y);
8867
8868                    let fwd = forward_rs_mat(current_transform);
8869                    let has_linear = fwd != [1.0, 0.0, 0.0, 1.0];
8870
8871                    let lin = current_transform.linear();
8872                    let tr_x = current_transform.translate_x;
8873                    let tr_y = current_transform.translate_y;
8874
8875                    let make_glyph_instance =
8876                        |gx: f32, gy: f32, gw: f32, gh: f32| -> ([f32; 4], [f32; 4]) {
8877                            if has_linear {
8878                                let gc_x = gx + gw * 0.5;
8879                                let gc_y = gy + gh * 0.5;
8880                                let wc_x = lin[0] * gc_x + lin[1] * gc_y + tr_x;
8881                                let wc_y = lin[2] * gc_x + lin[3] * gc_y + tr_y;
8882                                let ww = gw * current_transform.scale_x;
8883                                let wh = gh * current_transform.scale_y;
8884                                let ex = fwd[0].abs() * ww * 0.5 + fwd[1].abs() * wh * 0.5;
8885                                let ey = fwd[2].abs() * ww * 0.5 + fwd[3].abs() * wh * 0.5;
8886                                let ndc_tl = to_ndc(
8887                                    wc_x - ex,
8888                                    wc_y - ey,
8889                                    ex * 2.0,
8890                                    ey * 2.0,
8891                                    current_target_size.0,
8892                                    current_target_size.1,
8893                                );
8894                                let ndc = [
8895                                    ndc_tl[0] + ndc_tl[2] * 0.5,
8896                                    ndc_tl[1] + ndc_tl[3] * 0.5,
8897                                    ndc_tl[2],
8898                                    ndc_tl[3],
8899                                ];
8900                                (ndc, fwd)
8901                            } else {
8902                                let (sx, sy) = if current_transform.scale_x == 1.0
8903                                    && current_transform.scale_y == 1.0
8904                                {
8905                                    (gx.round(), gy.round())
8906                                } else {
8907                                    (gx, gy)
8908                                };
8909                                rect_to_instance_ndc(
8910                                    repose_core::Rect {
8911                                        x: sx,
8912                                        y: sy,
8913                                        w: gw,
8914                                        h: gh,
8915                                    },
8916                                    current_transform,
8917                                    current_target_size.0,
8918                                    current_target_size.1,
8919                                )
8920                            }
8921                        };
8922
8923                    let baseline_shift_y: f32 = px * extra_style.baseline_shift.0;
8924
8925                    let (
8926                        draws_fill,
8927                        is_stroke,
8928                        stroke_width,
8929                        stroke_cap,
8930                        stroke_join,
8931                        stroke_miter,
8932                        stroke_path_effect,
8933                    ) = match &extra_style.draw_style {
8934                        repose_core::DrawStyle::Stroke {
8935                            width,
8936                            cap,
8937                            join,
8938                            miter,
8939                            path_effect,
8940                        } => (
8941                            false,
8942                            true,
8943                            *width,
8944                            *cap,
8945                            *join,
8946                            *miter,
8947                            path_effect.clone(),
8948                        ),
8949                        repose_core::DrawStyle::FillAndStroke {
8950                            width,
8951                            cap,
8952                            join,
8953                            miter,
8954                            path_effect,
8955                        } => (true, true, *width, *cap, *join, *miter, path_effect.clone()),
8956                        _ => (
8957                            true,
8958                            false,
8959                            0.0,
8960                            repose_core::StrokeCap::Butt,
8961                            repose_core::StrokeJoin::Miter,
8962                            4.0,
8963                            None,
8964                        ),
8965                    };
8966                    let stroke_tess_key = if is_stroke {
8967                        Some(slug::StrokeTessKey::new(
8968                            stroke_width,
8969                            stroke_cap,
8970                            stroke_join,
8971                            stroke_miter,
8972                            &stroke_path_effect,
8973                        ))
8974                    } else {
8975                        None
8976                    };
8977
8978                    let mut legacy_shaped = None;
8979                    for (glyph_index, sg) in shaped.glyphs.iter().enumerate() {
8980                        let mut fill_cached = false;
8981                        let mut stroke_cached = false;
8982                        if self.slug_enabled {
8983                            let ck = sg.cache_key;
8984                            let color_linear = color.to_linear();
8985                            let fill_key = draws_fill.then(|| {
8986                                Self::slug_draw_key(
8987                                    ck,
8988                                    true,
8989                                    None,
8990                                    current_transform,
8991                                    (rect.x + sg.x, rect.y + sg.y + baseline_shift_y),
8992                                    px,
8993                                    current_target_size,
8994                                    color_linear,
8995                                )
8996                            });
8997                            let stroke_key = is_stroke.then(|| {
8998                                Self::slug_draw_key(
8999                                    ck,
9000                                    false,
9001                                    stroke_tess_key.as_ref(),
9002                                    current_transform,
9003                                    (rect.x + sg.x, rect.y + sg.y + baseline_shift_y),
9004                                    px,
9005                                    current_target_size,
9006                                    color_linear,
9007                                )
9008                            });
9009                            let mut all_cached = true;
9010                            if let Some(key) = fill_key {
9011                                if let Some(vertices) = self.get_cached_slug_vertices(&key) {
9012                                    slug_verts_local.extend_from_slice(&vertices);
9013                                    fill_cached = true;
9014                                } else {
9015                                    all_cached = false;
9016                                }
9017                            }
9018                            if let Some(key) = stroke_key {
9019                                if let Some(vertices) = self.get_cached_slug_vertices(&key) {
9020                                    slug_verts_local.extend_from_slice(&vertices);
9021                                    stroke_cached = true;
9022                                } else {
9023                                    all_cached = false;
9024                                }
9025                            }
9026                            if all_cached {
9027                                continue;
9028                            }
9029                            let outline = self.cached_glyph_outline(ck);
9030                            if let Some(commands) = outline.as_ref() {
9031                                let need_tessellate = self.slug_cache.get(&ck).is_none_or(|g| {
9032                                    (draws_fill && g.fill_vertices.is_none())
9033                                        || (is_stroke
9034                                            && !g
9035                                                .stroke_variants
9036                                                .contains_key(stroke_tess_key.as_ref().unwrap()))
9037                                });
9038                                if need_tessellate {
9039                                    let font_size_px = f32::from_bits(ck.font_size_bits);
9040                                    if draws_fill {
9041                                        self.slug_cache.get_or_insert(
9042                                            ck,
9043                                            font_size_px,
9044                                            commands.as_ref(),
9045                                        );
9046                                    }
9047                                    if is_stroke {
9048                                        self.slug_cache.get_or_insert_stroke(
9049                                            ck,
9050                                            font_size_px,
9051                                            commands.as_ref(),
9052                                            stroke_width,
9053                                            stroke_cap,
9054                                            stroke_join,
9055                                            stroke_miter,
9056                                            &stroke_path_effect,
9057                                        );
9058                                    }
9059                                } else {
9060                                    self.slug_cache.touch(&ck);
9061                                }
9062                            }
9063                            if let Some(_commands) = outline.as_ref()
9064                                && let Some(entry) = self.slug_cache.get(&ck)
9065                            {
9066                                let ox = rect.x + sg.x;
9067                                let oy = rect.y + sg.y + baseline_shift_y;
9068                                let scx = current_transform.scale_x;
9069                                let scy = current_transform.scale_y;
9070                                let ttx = current_transform.translate_x;
9071                                let tty = current_transform.translate_y;
9072
9073                                let tf = |x: f32, y: f32| -> (f32, f32) {
9074                                    if has_linear {
9075                                        (
9076                                            lin[0] * x + lin[1] * y + ttx,
9077                                            lin[2] * x + lin[3] * y + tty,
9078                                        )
9079                                    } else {
9080                                        (x * scx + ttx, y * scy + tty)
9081                                    }
9082                                };
9083
9084                                let tw = current_target_size.0;
9085                                let th = current_target_size.1;
9086
9087                                let fill_vertices = if draws_fill && !fill_cached {
9088                                    entry.fill_vertices.clone().unwrap_or_default()
9089                                } else {
9090                                    Vec::new()
9091                                };
9092                                let stroke_vertices = if is_stroke && !stroke_cached {
9093                                    stroke_tess_key
9094                                        .as_ref()
9095                                        .and_then(|key| entry.stroke_variants.get(key))
9096                                        .cloned()
9097                                        .unwrap_or_default()
9098                                } else {
9099                                    Vec::new()
9100                                };
9101                                let mut emit = |verts: Vec<[f32; 2]>, cache_key: SlugDrawKey| {
9102                                    let transformed: Vec<slug::TessVertex> = verts
9103                                        .iter()
9104                                        .map(|v| {
9105                                            let (sx, sy) = tf(ox + v[0] * px, oy - v[1] * px);
9106                                            slug::TessVertex {
9107                                                ndc_pos: [
9108                                                    sx / tw * 2.0 - 1.0,
9109                                                    -(sy / th) * 2.0 + 1.0,
9110                                                ],
9111                                                color: color_linear,
9112                                            }
9113                                        })
9114                                        .collect();
9115                                    let vertices =
9116                                        self.cached_slug_vertices(cache_key, transformed);
9117                                    slug_verts_local.extend_from_slice(&vertices);
9118                                };
9119                                if draws_fill
9120                                    && !fill_cached
9121                                    && let Some(key) = fill_key
9122                                {
9123                                    emit(fill_vertices, key);
9124                                }
9125                                if is_stroke
9126                                    && !stroke_cached
9127                                    && let Some(key) = stroke_key
9128                                {
9129                                    emit(stroke_vertices, key);
9130                                }
9131
9132                                if !draws_fill {
9133                                    // Stroke glyphs cannot use atlas fallback...
9134                                    continue;
9135                                }
9136                                continue;
9137                            }
9138                        }
9139
9140                        if !draws_fill {
9141                            continue;
9142                        }
9143                        if fill_cached {
9144                            continue;
9145                        }
9146                        if legacy_shaped.is_none() {
9147                            legacy_shaped = Some(self.legacy_shape_text_cached(
9148                                text,
9149                                px,
9150                                lh_ratio,
9151                                *font_family,
9152                                fw,
9153                                fs,
9154                                letter_spacing.0,
9155                                font_variation_settings.as_ref(),
9156                            ));
9157                        }
9158                        let Some(legacy_glyph) = legacy_shaped
9159                            .as_ref()
9160                            .and_then(|glyphs| glyphs.get(glyph_index))
9161                        else {
9162                            continue;
9163                        };
9164                        let gx = rect.x + legacy_glyph.x + legacy_glyph.bearing_x;
9165                        let gy =
9166                            rect.y + legacy_glyph.y - legacy_glyph.bearing_y + baseline_shift_y;
9167                        if let Some(info) =
9168                            self.upload_glyph_color(legacy_glyph.cache_key, legacy_glyph.px)
9169                        {
9170                            let (ndc, fwd_mat) = make_glyph_instance(gx, gy, info.w, info.h);
9171                            batch.colors.push(GlyphInstance {
9172                                xywh: ndc,
9173                                uv: [info.u0, info.v1, info.u1, info.v0],
9174                                color: color.to_linear(),
9175                                fwd_mat,
9176                            });
9177                        } else if let Some(info) =
9178                            self.upload_glyph_mask(legacy_glyph.cache_key, legacy_glyph.px)
9179                        {
9180                            let (ndc, fwd_mat) = make_glyph_instance(gx, gy, info.w, info.h);
9181                            batch.masks.push(GlyphInstance {
9182                                xywh: ndc,
9183                                uv: [info.u0, info.v1, info.u1, info.v0],
9184                                color: color.to_linear(),
9185                                fwd_mat,
9186                            });
9187                        }
9188                    }
9189
9190                    // Upload slug vertices if any
9191                    if !slug_verts_local.is_empty() {
9192                        let bytes = bytemuck::cast_slice(&slug_verts_local);
9193                        let byte_len = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
9194                        let Ok(count) = u32::try_from(slug_verts_local.len()) else {
9195                            slug_verts_local.clear();
9196                            continue;
9197                        };
9198                        if self
9199                            .slug_ring
9200                            .grow_to_fit(&self.device, encoder, byte_len)
9201                            .is_ok()
9202                            && let Ok(off) = self.slug_ring.alloc_write(&self.queue, bytes)
9203                        {
9204                            current_pass
9205                                .cmds
9206                                .push(Cmd::GlyphsVector { off, cnt: count });
9207                        }
9208                        slug_verts_local.clear();
9209                    }
9210
9211                    // Text decoration: underline / strikethrough
9212                    if (text_decoration.underline || text_decoration.strikethrough)
9213                        && let Some(baseline_y) = baseline_y
9214                    {
9215                        flush_batch!();
9216                        current_prim = Some("rect");
9217                        let deco_color = text_decoration.color.unwrap_or(*color);
9218                        let thickness = (px * 0.07).max(1.0);
9219
9220                        if text_decoration.underline {
9221                            let dy = baseline_y + px * 0.1;
9222                            let (ndc, fwd_mat) = rect_to_instance_ndc(
9223                                repose_core::Rect {
9224                                    x: rect.x,
9225                                    y: dy,
9226                                    w: rect.w,
9227                                    h: thickness,
9228                                },
9229                                current_transform,
9230                                current_target_size.0,
9231                                current_target_size.1,
9232                            );
9233                            batch.rects.push(RectInstance {
9234                                xywh: ndc,
9235                                radii: [0.0; 4],
9236                                brush_type: 0,
9237                                grad_kind: 0,
9238                                _pad: [0.0; 2],
9239                                color0: deco_color.to_linear(),
9240                                color1: [0.0; 4],
9241                                grad_p0: [0.0; 2],
9242                                grad_p1: [0.0; 2],
9243                                tile_mode: 0,
9244                                _pad2: [0.0; 3],
9245                                fwd_mat,
9246                            });
9247                        }
9248                        if text_decoration.strikethrough {
9249                            let sy = baseline_y - px * 0.3;
9250                            let (ndc, fwd_mat) = rect_to_instance_ndc(
9251                                repose_core::Rect {
9252                                    x: rect.x,
9253                                    y: sy,
9254                                    w: rect.w,
9255                                    h: thickness,
9256                                },
9257                                current_transform,
9258                                current_target_size.0,
9259                                current_target_size.1,
9260                            );
9261                            batch.rects.push(RectInstance {
9262                                xywh: ndc,
9263                                radii: [0.0; 4],
9264                                brush_type: 0,
9265                                grad_kind: 0,
9266                                _pad: [0.0; 2],
9267                                color0: deco_color.to_linear(),
9268                                color1: [0.0; 4],
9269                                grad_p0: [0.0; 2],
9270                                grad_p1: [0.0; 2],
9271                                tile_mode: 0,
9272                                _pad2: [0.0; 3],
9273                                fwd_mat,
9274                            });
9275                        }
9276                    }
9277                }
9278                SceneNode::Image {
9279                    rect,
9280                    handle,
9281                    tint,
9282                    fit,
9283                    filter,
9284                    source_rect,
9285                    alignment,
9286                } => {
9287                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9288                    if !visible(affine_aabb(current_transform, rect), clip) {
9289                        continue;
9290                    }
9291                    flush_batch!();
9292                    let (img_w, img_h, is_nv12) = match self.resolve_image_for_draw(*handle) {
9293                        Some(wh) => wh,
9294                        None => {
9295                            log::warn!("Image handle {} not found", handle);
9296                            continue;
9297                        }
9298                    };
9299                    let Some(source) = resolve_image_source(*source_rect, img_w, img_h) else {
9300                        log::warn!(
9301                            "Image handle {} has invalid source rect {:?}",
9302                            handle,
9303                            source_rect
9304                        );
9305                        continue;
9306                    };
9307                    let Some((draw_rect, uv_rect)) =
9308                        image_fit_geometry(*rect, *fit, *alignment, &source)
9309                    else {
9310                        continue;
9311                    };
9312                    let (ndc_center, fwd_mat) = rect_to_instance_ndc(
9313                        draw_rect,
9314                        current_transform,
9315                        current_target_size.0,
9316                        current_target_size.1,
9317                    );
9318                    if is_nv12 {
9319                        if image_run.is_some() {
9320                            flush_image_runs!();
9321                        }
9322                        let (uv_x_offset, uv_y_offset) =
9323                            if let Some(ImageTex::Nv12 {
9324                                w, h, color_info, ..
9325                            }) = self.images.get(handle)
9326                            {
9327                                let chroma_w = w.div_ceil(2).max(1) as f32;
9328                                let chroma_h = h.div_ceil(2).max(1) as f32;
9329                                match color_info.chroma_siting {
9330                                    ChromaSiting::Center => (0.0, 0.0),
9331                                    ChromaSiting::Left => (-0.5 / chroma_w, 0.0),
9332                                    ChromaSiting::TopLeft => (-0.5 / chroma_w, -0.5 / chroma_h),
9333                                }
9334                            } else {
9335                                (0.0, 0.0)
9336                            };
9337                        let inst = Nv12Instance {
9338                            xywh: ndc_center,
9339                            uv: uv_rect,
9340                            color: tint.to_linear(),
9341                            uv_x_offset,
9342                            uv_y_offset,
9343                            fwd_mat,
9344                        };
9345                        if nv12_run
9346                            .as_ref()
9347                            .is_some_and(|(run_handle, run_filter, _)| {
9348                                *run_handle != *handle || *run_filter != *filter
9349                            })
9350                        {
9351                            flush_image_runs!();
9352                        }
9353                        match &mut nv12_run {
9354                            Some((run_handle, run_filter, instances))
9355                                if *run_handle == *handle && *run_filter == *filter =>
9356                            {
9357                                instances.push(inst)
9358                            }
9359                            _ => nv12_run = Some((*handle, *filter, vec![inst])),
9360                        }
9361                    } else {
9362                        if nv12_run.is_some() {
9363                            flush_image_runs!();
9364                        }
9365                        let tile = matches!(fit, ImageFit::Tile);
9366                        let inst = GlyphInstance {
9367                            xywh: ndc_center,
9368                            uv: uv_rect,
9369                            color: tint.to_linear(),
9370                            fwd_mat,
9371                        };
9372                        if image_run.as_ref().is_some_and(
9373                            |(run_handle, run_filter, run_tile, _)| {
9374                                *run_handle != *handle
9375                                    || *run_filter != *filter
9376                                    || *run_tile != tile
9377                            },
9378                        ) {
9379                            flush_image_runs!();
9380                        }
9381                        match &mut image_run {
9382                            Some((run_handle, run_filter, run_tile, instances))
9383                                if *run_handle == *handle
9384                                    && *run_filter == *filter
9385                                    && *run_tile == tile =>
9386                            {
9387                                instances.push(inst)
9388                            }
9389                            _ => image_run = Some((*handle, *filter, tile, vec![inst])),
9390                        }
9391                    }
9392                }
9393                SceneNode::Coverage {
9394                    rect,
9395                    handle,
9396                    color,
9397                } => {
9398                    flush_batch!();
9399                    // Unknown handles are skipped (same policy as images);
9400                    // the lookup also marks the tile used for eviction.
9401                    let Some((tile_w, tile_h)) = self.coverage_dimensions(*handle) else {
9402                        log::warn!("Coverage handle {handle} not found");
9403                        continue;
9404                    };
9405                    // The tile composites at its registered size; `rect`
9406                    // positions its top-left.
9407                    let draw_rect = repose_core::Rect {
9408                        x: rect.x,
9409                        y: rect.y,
9410                        w: tile_w as f32,
9411                        h: tile_h as f32,
9412                    };
9413                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9414                    if !visible(affine_aabb(current_transform, &draw_rect), clip) {
9415                        continue;
9416                    }
9417                    let (ndc_center, fwd_mat) = rect_to_instance_ndc(
9418                        draw_rect,
9419                        current_transform,
9420                        current_target_size.0,
9421                        current_target_size.1,
9422                    );
9423                    let inst = GlyphInstance {
9424                        xywh: ndc_center,
9425                        uv: [0.0, 1.0, 1.0, 0.0],
9426                        color: color.to_linear(),
9427                        fwd_mat,
9428                    };
9429                    if let Some((off, _)) =
9430                        self.glyph_color
9431                            .upload(&self.device, &self.queue, encoder, &[inst])
9432                    {
9433                        current_pass.cmds.push(Cmd::Coverage {
9434                            off,
9435                            cnt: 1,
9436                            handle: *handle,
9437                        });
9438                    }
9439                }
9440                SceneNode::PushClip { rect, radius, op } => {
9441                    flush_batch!();
9442
9443                    let is_diff = matches!(op, repose_core::ClipOp::Difference);
9444                    let t_identity = Transform::identity();
9445                    let current_transform = transform_stack.last().unwrap_or(&t_identity);
9446                    let transformed = affine_aabb(current_transform, rect);
9447                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9448                    let next_scissor = if is_diff {
9449                        top
9450                    } else {
9451                        intersect(top, transformed)
9452                    };
9453                    scissor_stack.push(next_scissor);
9454                    let scissor = to_scissor(
9455                        &next_scissor,
9456                        current_target_size.0 as u32,
9457                        current_target_size.1 as u32,
9458                    );
9459                    let has_inverse = active_clips.iter().any(ActiveClip::difference);
9460                    let mut blocked =
9461                        current_pass.active_scissor.is_none() || (has_inverse && !is_diff);
9462                    if has_inverse && is_diff {
9463                        log::error!(
9464                            "unsupported clip nesting: Difference cannot contain a Difference clip"
9465                        );
9466                        blocked = true;
9467                    }
9468                    let has_area = transformed.x.is_finite()
9469                        && transformed.y.is_finite()
9470                        && transformed.w.is_finite()
9471                        && transformed.h.is_finite()
9472                        && transformed.w > 0.0
9473                        && transformed.h > 0.0;
9474                    let mut applied = false;
9475                    let mut off = 0;
9476                    if !blocked && has_area && (is_diff || scissor.2 > 0 && scissor.3 > 0) {
9477                        let clip_ndc_tl = to_ndc(
9478                            transformed.x,
9479                            transformed.y,
9480                            transformed.w,
9481                            transformed.h,
9482                            current_target_size.0,
9483                            current_target_size.1,
9484                        );
9485                        let inst = ClipInstance {
9486                            xywh: [
9487                                clip_ndc_tl[0] + clip_ndc_tl[2] * 0.5,
9488                                clip_ndc_tl[1] + clip_ndc_tl[3] * 0.5,
9489                                clip_ndc_tl[2],
9490                                clip_ndc_tl[3],
9491                            ],
9492                            radii: radius.map(|r| r.0),
9493                            fwd_mat: [1.0, 0.0, 0.0, 1.0],
9494                        };
9495                        let bytes = bytemuck::bytes_of(&inst);
9496                        if self
9497                            .clip_ring
9498                            .grow_to_fit(
9499                                &self.device,
9500                                encoder,
9501                                u64::try_from(bytes.len()).unwrap_or(u64::MAX),
9502                            )
9503                            .is_ok()
9504                            && let Ok(new_off) = self.clip_ring.alloc_write(&self.queue, bytes)
9505                        {
9506                            off = new_off;
9507                            applied = true;
9508                            current_pass.cmds.push(Cmd::ClipPush {
9509                                off,
9510                                cnt: 1,
9511                                scissor,
9512                                difference: is_diff,
9513                                applied,
9514                            });
9515                        } else {
9516                            blocked = !is_diff;
9517                        }
9518                    } else if !is_diff {
9519                        blocked = true;
9520                    }
9521                    current_pass.active_scissor = if blocked {
9522                        None
9523                    } else {
9524                        (scissor.2 > 0 && scissor.3 > 0).then_some(scissor)
9525                    };
9526                    active_clips.push(ActiveClip::Rect {
9527                        off,
9528                        cnt: u32::from(applied),
9529                        rect: transformed,
9530                        radii: radius.map(|r| r.0),
9531                        scissor,
9532                        difference: is_diff,
9533                        applied,
9534                        blocked,
9535                    });
9536                }
9537                SceneNode::PopClip => {
9538                    flush_batch!();
9539
9540                    if scissor_stack.is_empty() || active_clips.is_empty() {
9541                        log::error!("PopClip does not match an active clip");
9542                    } else {
9543                        scissor_stack.pop();
9544                    }
9545                    let clip = active_clips.pop();
9546                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9547                    let scissor = to_scissor(
9548                        &top,
9549                        current_target_size.0 as u32,
9550                        current_target_size.1 as u32,
9551                    );
9552                    current_pass.active_scissor =
9553                        (scissor.2 > 0 && scissor.3 > 0).then_some(scissor);
9554                    match clip {
9555                        Some(ActiveClip::Rect {
9556                            off,
9557                            cnt,
9558                            scissor: stencil_scissor,
9559                            difference,
9560                            applied,
9561                            ..
9562                        }) if applied && cnt > 0 => {
9563                            current_pass.cmds.push(Cmd::ClipPop {
9564                                off,
9565                                cnt,
9566                                scissor,
9567                                stencil_scissor,
9568                                difference,
9569                                applied: true,
9570                            });
9571                        }
9572                        Some(ActiveClip::Vector { .. }) => {
9573                            log::error!("PopClip matched a vector clip");
9574                        }
9575                        None => {}
9576                        _ => {}
9577                    }
9578                }
9579                SceneNode::Shadow {
9580                    rect,
9581                    radius,
9582                    elevation: _,
9583                    color,
9584                } => {
9585                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9586                    if !visible(affine_aabb(current_transform, rect), clip) {
9587                        continue;
9588                    }
9589                    flush_if_prim_changed!("rect", &self.rects);
9590                    let (ndc, fwd_mat) = rect_to_instance_ndc(
9591                        *rect,
9592                        current_transform,
9593                        current_target_size.0,
9594                        current_target_size.1,
9595                    );
9596                    let (brush_type, color0, _color1, _grad_p0, _grad_p1) =
9597                        brush_to_instance_fields(&Brush::Solid(*color));
9598                    batch.rects.push(RectInstance {
9599                        xywh: ndc,
9600                        radii: radius.map(|r| r.0),
9601                        brush_type,
9602                        grad_kind: 0,
9603                        _pad: [0.0; 2],
9604                        color0,
9605                        color1: [0.0; 4],
9606                        grad_p0: [0.0; 2],
9607                        grad_p1: [0.0; 2],
9608                        tile_mode: 0,
9609                        _pad2: [0.0; 3],
9610                        fwd_mat,
9611                    });
9612                }
9613                SceneNode::PushTransform { transform } => {
9614                    flush_batch!(); // flush before transform change
9615                    if transform.has_perspective() {
9616                        // True perspective cannot ride the affine fast path:
9617                        // flatten the subtree into an offscreen layer and
9618                        // composite it back projectively (CSS-style). See
9619                        // `push_perspective_layer`.
9620                        let top = *transform_stack.last().unwrap_or(&t_identity);
9621                        self.push_perspective_layer(
9622                            *transform,
9623                            top,
9624                            &mut transform_stack,
9625                            &mut scissor_stack,
9626                            &mut root_clip_rect,
9627                            &mut current_target_size,
9628                            &mut current_pass,
9629                            &mut active_clips,
9630                            &mut passes,
9631                            &mut target_stack,
9632                            &mut flatten_stack,
9633                            &mut flatten_id_head,
9634                            &mut next_pass_id,
9635                            &mut flatten_ids_used,
9636                            encoder,
9637                        );
9638                    } else {
9639                        let combined = current_transform.combine(transform);
9640                        transform_stack.push(combined);
9641                    }
9642                }
9643                SceneNode::PopTransform => {
9644                    flush_batch!(); // flush before transform change
9645                    if let Some(rec) = flatten_stack.last() {
9646                        // A flatten level closes when the stack is back to the
9647                        // two entries this flatten pushed (stripped transform +
9648                        // layer-local shift); deeper plain pushes close first.
9649                        if transform_stack.len() == rec.stack_len + 2 {
9650                            let rec = flatten_stack.pop().expect("checked above");
9651                            transform_stack.pop();
9652                            transform_stack.pop();
9653                            self.pop_perspective_layer(
9654                                rec,
9655                                &mut scissor_stack,
9656                                &mut root_clip_rect,
9657                                &mut current_target_size,
9658                                &mut current_pass,
9659                                &mut active_clips,
9660                                &mut passes,
9661                                &mut target_stack,
9662                                &mut next_pass_id,
9663                                encoder,
9664                            );
9665                            continue;
9666                        }
9667                    }
9668                    transform_stack.pop();
9669                }
9670                SceneNode::BeginLayer {
9671                    rect,
9672                    layer_id,
9673                    alpha,
9674                    blur_radius_x,
9675                    blur_radius_y,
9676                    rectangle_edge,
9677                } => {
9678                    flush_batch!();
9679                    let width_f = rect.w.round();
9680                    let height_f = rect.h.round();
9681                    let max_dimension = self.device.limits().max_texture_dimension_2d as f32;
9682                    if !width_f.is_finite()
9683                        || !height_f.is_finite()
9684                        || width_f < 1.0
9685                        || height_f < 1.0
9686                        || width_f > max_dimension
9687                        || height_f > max_dimension
9688                    {
9689                        log::warn!("BeginLayer dimensions are invalid");
9690                        continue;
9691                    }
9692                    let width = width_f as u32;
9693                    let height = height_f as u32;
9694                    let parent_scissors =
9695                        std::mem::replace(&mut scissor_stack, Vec::with_capacity(8));
9696                    let parent_root_clip = std::mem::replace(
9697                        &mut root_clip_rect,
9698                        repose_core::Rect {
9699                            x: 0.0,
9700                            y: 0.0,
9701                            w: width as f32,
9702                            h: height as f32,
9703                        },
9704                    );
9705                    scissor_stack.push(root_clip_rect);
9706                    let layer_pass_id = next_pass_id;
9707                    next_pass_id += 1;
9708                    let parent_clips = std::mem::take(&mut active_clips);
9709                    let mut layer_pass = Pass {
9710                        id: layer_pass_id,
9711                        target: PassTarget::Layer(*layer_id),
9712                        initial_scissor: (0, 0, width, height),
9713                        active_scissor: Some((0, 0, width, height)),
9714                        clear_color: Some([0.0, 0.0, 0.0, 0.0]),
9715                        cmds: Vec::new(),
9716                    };
9717                    let parent_transform = *current_transform;
9718                    active_clips = self.replay_active_clips(
9719                        &parent_clips,
9720                        &parent_transform,
9721                        (rect.x, rect.y),
9722                        (width as f32, height as f32),
9723                        &mut layer_pass,
9724                        encoder,
9725                    );
9726                    let saved = std::mem::replace(&mut current_pass, layer_pass);
9727                    if let Some(top) = transform_stack.last_mut() {
9728                        *top = Transform::identity();
9729                    }
9730                    layer_stack.push(LayerState {
9731                        layer_id: *layer_id,
9732                        parent_target: saved.target,
9733                        parent_scissors,
9734                        parent_root_clip,
9735                        parent_size: current_target_size,
9736                        parent_transform,
9737                        parent_scissor: saved.active_scissor,
9738                        parent_clips,
9739                        alpha: *alpha,
9740                        blur: (blur_radius_x.0, blur_radius_y.0),
9741                        rectangle_edge: *rectangle_edge,
9742                    });
9743                    passes.push(saved);
9744                    if self.get_or_create_layer(*layer_id, width, height, *rect, false) {
9745                        self.producer_layer_ids.insert(*layer_id);
9746                    }
9747                    current_target_size = (width as f32, height as f32);
9748                }
9749                SceneNode::EndLayer { layer_id } => {
9750                    flush_batch!();
9751                    if layer_stack.last().map(|state| state.layer_id) != Some(*layer_id) {
9752                        log::warn!("EndLayer {} does not match active layer", layer_id);
9753                        continue;
9754                    }
9755                    let state = layer_stack.pop().expect("checked above");
9756                    scissor_stack = state.parent_scissors;
9757                    root_clip_rect = state.parent_root_clip;
9758                    active_clips = state.parent_clips;
9759                    if let Some(top) = transform_stack.last_mut() {
9760                        *top = state.parent_transform;
9761                    }
9762                    current_target_size = state.parent_size;
9763                    let resumed_pass_id = next_pass_id;
9764                    next_pass_id += 1;
9765                    let saved = std::mem::replace(
9766                        &mut current_pass,
9767                        Pass {
9768                            id: resumed_pass_id,
9769                            target: state.parent_target,
9770                            initial_scissor: state.parent_scissor.unwrap_or((0, 0, 1, 1)),
9771                            active_scissor: state.parent_scissor,
9772                            clear_color: None,
9773                            cmds: Vec::new(),
9774                        },
9775                    );
9776                    passes.push(saved);
9777                    let Some(layer) = self.layer_pool.get(layer_id).cloned() else {
9778                        continue;
9779                    };
9780                    let layer_rect = repose_core::Rect {
9781                        x: layer.rect_px.0,
9782                        y: layer.rect_px.1,
9783                        w: layer.rect_px.2,
9784                        h: layer.rect_px.3,
9785                    };
9786                    let local_layer_rect = affine_aabb(&state.parent_transform, &layer_rect);
9787                    let (parent_width, parent_height) = state.parent_size;
9788                    let parent_clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9789                    let composite_rect = if state.blur.0 > 0.0 || state.blur.1 > 0.0 {
9790                        repose_core::Rect {
9791                            x: local_layer_rect.x - state.blur.0 * 1.5,
9792                            y: local_layer_rect.y - state.blur.1 * 1.5,
9793                            w: local_layer_rect.w + state.blur.0 * 3.0,
9794                            h: local_layer_rect.h + state.blur.1 * 3.0,
9795                        }
9796                    } else {
9797                        local_layer_rect
9798                    };
9799                    let shadow_follows = shadow_layer_ids.contains(layer_id);
9800                    if !visible(composite_rect, parent_clip) {
9801                        continue;
9802                    }
9803                    if state.blur.0 > 0.0 || state.blur.1 > 0.0 {
9804                        let blur_x = state.blur.0 * 1.5;
9805                        let blur_y = state.blur.1 * 1.5;
9806                        let rect = repose_core::Rect {
9807                            x: local_layer_rect.x - blur_x,
9808                            y: local_layer_rect.y - blur_y,
9809                            w: local_layer_rect.w + blur_x * 2.0,
9810                            h: local_layer_rect.h + blur_y * 2.0,
9811                        };
9812                        let ndc =
9813                            to_ndc(rect.x, rect.y, rect.w, rect.h, parent_width, parent_height);
9814                        let inst = BlurInstance {
9815                            xywh: [ndc[0] + ndc[2] * 0.5, ndc[1] + ndc[3] * 0.5, ndc[2], ndc[3]],
9816                            uv: [0.0, 0.0, 1.0, 1.0],
9817                            color: [1.0, 1.0, 1.0, state.alpha],
9818                            blur_uv: [
9819                                (state.blur.0 * 1.5) / layer.width.max(1) as f32,
9820                                (state.blur.1 * 1.5) / layer.height.max(1) as f32,
9821                            ],
9822                            fwd_mat: [1.0, 0.0, 0.0, 1.0],
9823                            edge_mode: if state.rectangle_edge { 0 } else { 1 },
9824                            _pad: [0.0; 3],
9825                        };
9826                        if self
9827                            .blur_ring
9828                            .grow_to_fit(
9829                                &self.device,
9830                                encoder,
9831                                std::mem::size_of::<BlurInstance>() as u64,
9832                            )
9833                            .is_ok()
9834                            && let Ok(off) = self
9835                                .blur_ring
9836                                .alloc_write(&self.queue, bytemuck::bytes_of(&inst))
9837                        {
9838                            let command = Cmd::CompositeBlur {
9839                                off,
9840                                cnt: 1,
9841                                layer_id: *layer_id,
9842                            };
9843                            if shadow_follows {
9844                                pending_layer_composite = Some(command);
9845                            } else {
9846                                current_pass.cmds.push(command);
9847                            }
9848                        }
9849                    } else {
9850                        let ndc = to_ndc(
9851                            local_layer_rect.x,
9852                            local_layer_rect.y,
9853                            local_layer_rect.w,
9854                            local_layer_rect.h,
9855                            parent_width,
9856                            parent_height,
9857                        );
9858                        let inst = GlyphInstance {
9859                            xywh: [ndc[0] + ndc[2] * 0.5, ndc[1] + ndc[3] * 0.5, ndc[2], ndc[3]],
9860                            uv: [0.0, 1.0, 1.0, 0.0],
9861                            color: [1.0, 1.0, 1.0, state.alpha],
9862                            fwd_mat: [1.0, 0.0, 0.0, 1.0],
9863                        };
9864                        if let Some((off, cnt)) =
9865                            self.glyph_color
9866                                .upload(&self.device, &self.queue, encoder, &[inst])
9867                        {
9868                            let command = Cmd::CompositeLayer {
9869                                off,
9870                                cnt,
9871                                layer_id: *layer_id,
9872                            };
9873                            if shadow_follows {
9874                                pending_layer_composite = Some(command);
9875                            } else {
9876                                current_pass.cmds.push(command);
9877                            }
9878                        }
9879                    }
9880                }
9881                SceneNode::CompositeShadow {
9882                    layer_id,
9883                    blur_px,
9884                    offset_px,
9885                    color,
9886                } => {
9887                    if let Some(layer) = self.layer_pool.get(layer_id).cloned() {
9888                        let layer_rect = repose_core::Rect {
9889                            x: layer.rect_px.0,
9890                            y: layer.rect_px.1,
9891                            w: layer.rect_px.2,
9892                            h: layer.rect_px.3,
9893                        };
9894                        let local_layer_rect =
9895                            affine_aabb(transform_stack.last().unwrap_or(&t_identity), &layer_rect);
9896                        let blur_x = blur_px.0.max(0.0) * 1.5;
9897                        let blur_y = blur_px.0.max(0.0) * 1.5;
9898                        let sx = local_layer_rect.x + offset_px.0.0 - blur_x;
9899                        let sy = local_layer_rect.y + offset_px.1.0 - blur_y;
9900                        let sw = local_layer_rect.w + blur_x * 2.0;
9901                        let sh = local_layer_rect.h + blur_y * 2.0;
9902                        let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9903                        if !visible(
9904                            repose_core::Rect {
9905                                x: sx,
9906                                y: sy,
9907                                w: sw,
9908                                h: sh,
9909                            },
9910                            clip,
9911                        ) {
9912                            flush_pending_layer!();
9913                            continue;
9914                        }
9915                        flush_batch!();
9916                        let bw_uv = blur_x / layer.width.max(1) as f32;
9917                        let bh_uv = blur_y / layer.height.max(1) as f32;
9918                        let ndc_tl =
9919                            to_ndc(sx, sy, sw, sh, current_target_size.0, current_target_size.1);
9920                        let inst = BlurInstance {
9921                            xywh: [
9922                                ndc_tl[0] + ndc_tl[2] * 0.5,
9923                                ndc_tl[1] + ndc_tl[3] * 0.5,
9924                                ndc_tl[2],
9925                                ndc_tl[3],
9926                            ],
9927                            uv: [0.0, 0.0, 1.0, 1.0],
9928                            color: color.to_linear(),
9929                            blur_uv: [bw_uv, bh_uv],
9930                            fwd_mat: [1.0, 0.0, 0.0, 1.0],
9931                            edge_mode: 0,
9932                            _pad: [0.0; 3],
9933                        };
9934                        if self
9935                            .blur_ring
9936                            .grow_to_fit(
9937                                &self.device,
9938                                encoder,
9939                                std::mem::size_of::<BlurInstance>() as u64,
9940                            )
9941                            .is_ok()
9942                            && let Ok(off) = self
9943                                .blur_ring
9944                                .alloc_write(&self.queue, bytemuck::bytes_of(&inst))
9945                        {
9946                            current_pass.cmds.push(Cmd::CompositeShadow {
9947                                off,
9948                                cnt: 1,
9949                                layer_id: *layer_id,
9950                            });
9951                        }
9952                    }
9953                    flush_pending_layer!();
9954                }
9955                SceneNode::VectorMesh {
9956                    mesh,
9957                    transform,
9958                    paint,
9959                    clip: _,
9960                    blend,
9961                } => {
9962                    let affine = combine_mesh_affine(current_transform, *transform);
9963                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9964                    if !visible(mesh_aabb(mesh, affine), clip) {
9965                        continue;
9966                    }
9967                    flush_batch!();
9968                    let paint_alpha = match paint {
9969                        repose_core::PaintDesc::Linear {
9970                            start_color,
9971                            end_color,
9972                            ..
9973                        }
9974                        | repose_core::PaintDesc::Radial {
9975                            start_color,
9976                            end_color,
9977                            ..
9978                        }
9979                        | repose_core::PaintDesc::Sweep {
9980                            start_color,
9981                            end_color,
9982                            ..
9983                        } => Some(start_color.3.min(end_color.3) as f32 / 255.0),
9984                        _ => None,
9985                    };
9986                    let translucent_fixed_blend =
9987                        matches!(
9988                            blend,
9989                            repose_core::BlendMode::Add
9990                                | repose_core::BlendMode::Multiply
9991                                | repose_core::BlendMode::Screen
9992                                | repose_core::BlendMode::Darken
9993                                | repose_core::BlendMode::Lighten
9994                        ) && (mesh.vertices.iter().any(|vertex| vertex.color[3] < 0.9999)
9995                            || paint_alpha.is_some_and(|alpha| alpha < 0.9999));
9996                    if blend.needs_isolation() || translucent_fixed_blend {
9997                        let blend_scissor = to_scissor(
9998                            &scissor_stack.last().copied().unwrap_or(root_clip_rect),
9999                            current_target_size.0 as u32,
10000                            current_target_size.1 as u32,
10001                        );
10002                        self.emit_isolated_blend(
10003                            mesh.clone(),
10004                            *transform,
10005                            *paint,
10006                            *blend,
10007                            current_transform,
10008                            &mut current_pass,
10009                            &active_clips,
10010                            &mut passes,
10011                            &mut flatten_id_head,
10012                            &mut next_pass_id,
10013                            &mut flatten_ids_used,
10014                            blend_scissor,
10015                            encoder,
10016                            fb_w,
10017                            fb_h,
10018                        );
10019                    } else {
10020                        let t_identity = Transform::identity();
10021                        let current_transform = transform_stack.last().unwrap_or(&t_identity);
10022                        self.emit_vector_mesh(
10023                            current_transform,
10024                            mesh,
10025                            *transform,
10026                            paint,
10027                            *blend,
10028                            &mut current_pass.cmds,
10029                        );
10030                    }
10031                }
10032                SceneNode::VectorOverlay { meshes } => {
10033                    let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10034                    let has_visible = meshes
10035                        .iter()
10036                        .any(|mesh| visible(mesh_aabb(mesh, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]), clip));
10037                    if !has_visible {
10038                        continue;
10039                    }
10040                    flush_batch!();
10041                    for (index, m) in meshes.iter().enumerate() {
10042                        if !visible(mesh_aabb(m, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]), clip) {
10043                            continue;
10044                        }
10045                        let Some(mesh_gpu) = self.upload_overlay_mesh(meshes, index) else {
10046                            continue;
10047                        };
10048                        let Some(uoff) = self.alloc_mesh_uniform(MeshUniform::identity()) else {
10049                            continue;
10050                        };
10051                        current_pass.cmds.push(Cmd::VectorOverlay {
10052                            mesh: mesh_gpu,
10053                            uoff,
10054                        });
10055                    }
10056                }
10057                SceneNode::PushVectorClip { mesh, op } => {
10058                    flush_batch!();
10059                    let difference = matches!(op, repose_core::ClipOp::Difference);
10060                    let t_identity = Transform::identity();
10061                    let current_transform = transform_stack.last().unwrap_or(&t_identity);
10062                    let affine =
10063                        combine_mesh_affine(current_transform, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
10064                    let aabb = mesh_aabb(mesh, affine);
10065                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10066                    let next = if difference {
10067                        top
10068                    } else {
10069                        intersect(top, aabb)
10070                    };
10071                    scissor_stack.push(next);
10072                    let scissor = to_scissor(
10073                        &next,
10074                        current_target_size.0 as u32,
10075                        current_target_size.1 as u32,
10076                    );
10077                    let has_inverse = active_clips.iter().any(ActiveClip::difference);
10078                    let mut blocked = current_pass.active_scissor.is_none() || has_inverse;
10079                    if has_inverse {
10080                        log::error!(
10081                            "unsupported clip nesting: a Difference clip cannot contain another vector clip"
10082                        );
10083                    }
10084                    let has_area = aabb.x.is_finite()
10085                        && aabb.y.is_finite()
10086                        && aabb.w.is_finite()
10087                        && aabb.h.is_finite()
10088                        && aabb.w > 0.0
10089                        && aabb.h > 0.0;
10090                    let mut applied = false;
10091                    let mut geometry = None;
10092                    let mut uoff = 0;
10093                    if !blocked
10094                        && has_area
10095                        && (if difference {
10096                            visible(aabb, top)
10097                        } else {
10098                            scissor.2 > 0 && scissor.3 > 0
10099                        })
10100                    {
10101                        if let Some(mesh_gpu) = self.upload_mesh_geometry(mesh)
10102                            && let Some(slot) = self.alloc_mesh_uniform(mesh_uniform_from_paint(
10103                                affine,
10104                                &repose_core::PaintDesc::Solid,
10105                            ))
10106                        {
10107                            geometry = Some(mesh_gpu.clone());
10108                            uoff = slot;
10109                            applied = true;
10110                            current_pass.cmds.push(Cmd::VectorClipPush {
10111                                mesh: mesh_gpu,
10112                                uoff,
10113                                scissor,
10114                                difference,
10115                                applied,
10116                            });
10117                        } else {
10118                            blocked = !difference;
10119                        }
10120                    } else if !difference {
10121                        blocked = true;
10122                    }
10123                    current_pass.active_scissor = if blocked {
10124                        None
10125                    } else {
10126                        (scissor.2 > 0 && scissor.3 > 0).then_some(scissor)
10127                    };
10128                    active_clips.push(ActiveClip::Vector {
10129                        mesh: geometry,
10130                        source: mesh.clone(),
10131                        uoff,
10132                        affine,
10133                        scissor,
10134                        difference,
10135                        applied,
10136                        blocked,
10137                    });
10138                }
10139                SceneNode::PopVectorClip => {
10140                    flush_batch!();
10141                    if scissor_stack.is_empty() || active_clips.is_empty() {
10142                        log::error!("PopVectorClip does not match an active clip");
10143                    } else {
10144                        scissor_stack.pop();
10145                    }
10146                    let clip = active_clips.pop();
10147                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10148                    let scissor = to_scissor(
10149                        &top,
10150                        current_target_size.0 as u32,
10151                        current_target_size.1 as u32,
10152                    );
10153                    current_pass.active_scissor =
10154                        (scissor.2 > 0 && scissor.3 > 0).then_some(scissor);
10155                    match clip {
10156                        Some(ActiveClip::Vector {
10157                            mesh: Some(mesh),
10158                            uoff,
10159                            scissor: stencil_scissor,
10160                            difference,
10161                            applied,
10162                            ..
10163                        }) if applied && mesh.vertex_count > 0 && mesh.index_count > 0 => {
10164                            current_pass.cmds.push(Cmd::VectorClipPop {
10165                                mesh,
10166                                uoff,
10167                                scissor,
10168                                stencil_scissor,
10169                                difference,
10170                                applied: true,
10171                            });
10172                        }
10173                        Some(ActiveClip::Rect { .. }) => {
10174                            log::error!("PopVectorClip matched a rounded-rect clip");
10175                        }
10176                        None => {}
10177                        _ => {}
10178                    }
10179                }
10180                SceneNode::Callback { rect, payload } => {
10181                    let t = transform_stack
10182                        .last()
10183                        .copied()
10184                        .unwrap_or(Transform::identity());
10185                    let transformed = affine_aabb(&t, rect);
10186                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10187                    let clip_rect = intersect(transformed, top);
10188                    let scissor = to_scissor(
10189                        &clip_rect,
10190                        current_target_size.0 as u32,
10191                        current_target_size.1 as u32,
10192                    );
10193                    let restore_scissor = to_scissor(
10194                        &top,
10195                        current_target_size.0 as u32,
10196                        current_target_size.1 as u32,
10197                    );
10198                    if current_pass.active_scissor.is_none()
10199                        || !visible(transformed, top)
10200                        || scissor.2 == 0
10201                        || scissor.3 == 0
10202                    {
10203                        continue;
10204                    }
10205                    flush_batch!();
10206                    current_pass.cmds.push(Cmd::Callback {
10207                        rect: transformed,
10208                        clip_rect,
10209                        scissor,
10210                        restore_scissor,
10211                        callback_id: Arc::as_ptr(payload) as *const () as usize,
10212                        payload: payload.clone(),
10213                    });
10214                }
10215                _ => {}
10216            }
10217        }
10218
10219        flush_pending_layer!();
10220        flush_batch!();
10221        passes.push(current_pass);
10222
10223        {
10224            let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
10225            let mut prepare_list: Vec<(usize, Arc<Callback>)> = Vec::new();
10226            for pass in &passes {
10227                for command in &pass.cmds {
10228                    if let Cmd::Callback { payload, .. } = command
10229                        && payload.downcast_ref::<Callback>().is_some()
10230                    {
10231                        let ptr = Arc::as_ptr(payload) as *const () as usize;
10232                        if seen.insert(ptr)
10233                            && let Ok(cb_arc) = payload.clone().downcast::<Callback>()
10234                        {
10235                            prepare_list.push((ptr, cb_arc));
10236                        }
10237                    }
10238                }
10239            }
10240            if !prepare_list.is_empty() {
10241                let mut callback_targets: HashMap<usize, Vec<(PassTarget, ScreenDescriptor)>> =
10242                    HashMap::new();
10243                for pass in &passes {
10244                    let (size, target_format, sample_count) = match pass.target {
10245                        PassTarget::Surface => (
10246                            [self.output_width, self.output_height],
10247                            if self.working_space {
10248                                wgpu::TextureFormat::Rgba16Float
10249                            } else {
10250                                self.output_format
10251                            },
10252                            self.active_surface_msaa_samples(),
10253                        ),
10254                        PassTarget::Layer(layer_id) => {
10255                            let layer = self.layer_pool.get(&layer_id);
10256                            (
10257                                layer.map_or([self.output_width, self.output_height], |layer| {
10258                                    [layer.width, layer.height]
10259                                }),
10260                                self.layer_target_format(),
10261                                1,
10262                            )
10263                        }
10264                    };
10265                    for command in &pass.cmds {
10266                        if let Cmd::Callback { payload, .. } = command {
10267                            let descriptor = ScreenDescriptor {
10268                                size_in_pixels: size,
10269                                pixels_per_point: self.pixels_per_point,
10270                                target_format,
10271                                sample_count,
10272                            };
10273                            let targets = callback_targets
10274                                .entry(Arc::as_ptr(payload) as *const () as usize)
10275                                .or_default();
10276                            if !targets.iter().any(|(target, target_desc)| {
10277                                *target == pass.target
10278                                    && target_desc.size_in_pixels == descriptor.size_in_pixels
10279                                    && target_desc.pixels_per_point == descriptor.pixels_per_point
10280                                    && target_desc.target_format == descriptor.target_format
10281                                    && target_desc.sample_count == descriptor.sample_count
10282                            }) {
10283                                targets.push((pass.target, descriptor));
10284                            }
10285                        }
10286                    }
10287                }
10288
10289                let default_descriptor = ScreenDescriptor {
10290                    size_in_pixels: [self.output_width, self.output_height],
10291                    pixels_per_point: self.pixels_per_point,
10292                    target_format: if self.working_space {
10293                        wgpu::TextureFormat::Rgba16Float
10294                    } else {
10295                        self.output_format
10296                    },
10297                    sample_count: self.active_surface_msaa_samples(),
10298                };
10299                let default_descriptors = [(PassTarget::Surface, default_descriptor)];
10300                let mut prepare_encoder =
10301                    self.device
10302                        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
10303                            label: Some("callback prepare"),
10304                        });
10305                let mut finish_encoder =
10306                    self.device
10307                        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
10308                            label: Some("callback finish prepare"),
10309                        });
10310                let mut active_callback_scopes = HashSet::new();
10311                for (key, cb) in &prepare_list {
10312                    let descriptors = callback_targets
10313                        .get(key)
10314                        .map(Vec::as_slice)
10315                        .unwrap_or(&default_descriptors);
10316                    for &(target, screen_desc) in descriptors {
10317                        let scope = callback_scope_key(cb, *key, target, &screen_desc);
10318                        active_callback_scopes.insert(scope);
10319                        if !scope.callback.reusable
10320                            && self
10321                                .callback_scope_payloads
10322                                .get(&scope)
10323                                .is_some_and(|payload| payload.upgrade().is_none())
10324                        {
10325                            self.remove_callback_scope(&scope);
10326                        }
10327                        let payload: Weak<Callback> = Arc::downgrade(cb);
10328                        self.callback_scope_payloads.insert(scope, payload);
10329                    }
10330                }
10331                let mut prepare_buffers = Vec::new();
10332                let mut finish_buffers = Vec::new();
10333                for (key, cb) in &prepare_list {
10334                    let descriptors = callback_targets
10335                        .get(key)
10336                        .map(Vec::as_slice)
10337                        .unwrap_or(&default_descriptors);
10338                    for &(target, screen_desc) in descriptors {
10339                        let scope = callback_scope_key(cb, *key, target, &screen_desc);
10340                        self.touch_callback_scope(scope);
10341                        if !self.callback_scoped_resources.contains_key(&scope)
10342                            && self.callback_scoped_resources.len() >= MAX_CALLBACK_SCOPES
10343                        {
10344                            self.evict_callback_scope(&active_callback_scopes);
10345                        }
10346                        let resources = self.callback_scoped_resources.entry(scope).or_default();
10347                        prepare_buffers.extend(cb.0.prepare(
10348                            &self.device,
10349                            &self.queue,
10350                            &mut prepare_encoder,
10351                            &screen_desc,
10352                            resources,
10353                        ));
10354                    }
10355                }
10356                for (key, cb) in &prepare_list {
10357                    let descriptors = callback_targets
10358                        .get(key)
10359                        .map(Vec::as_slice)
10360                        .unwrap_or(&default_descriptors);
10361                    for &(target, screen_desc) in descriptors {
10362                        let scope = callback_scope_key(cb, *key, target, &screen_desc);
10363                        self.touch_callback_scope(scope);
10364                        if let Some(resources) = self.callback_scoped_resources.get_mut(&scope) {
10365                            finish_buffers.extend(cb.0.finish_prepare(
10366                                &self.device,
10367                                &self.queue,
10368                                &mut finish_encoder,
10369                                &screen_desc,
10370                                resources,
10371                            ));
10372                        }
10373                    }
10374                }
10375                let mut callback_buffers =
10376                    Vec::with_capacity(2 + prepare_buffers.len() + finish_buffers.len());
10377                callback_buffers.push(prepare_encoder.finish());
10378                callback_buffers.extend(prepare_buffers);
10379                callback_buffers.push(finish_encoder.finish());
10380                callback_buffers.extend(finish_buffers);
10381                self.queue.submit(callback_buffers);
10382            }
10383        }
10384
10385        let globals_bytes = std::mem::size_of::<Globals>() as u64;
10386        if !self.ensure_globals_staging(passes.len()) {
10387            return;
10388        }
10389        for (i, pass) in passes.iter().enumerate() {
10390            let (target_w, target_h) = match pass.target {
10391                PassTarget::Surface => (fb_w, fb_h),
10392                PassTarget::Layer(layer_id) => {
10393                    let lt = self.layer_pool.get(&layer_id);
10394                    (
10395                        lt.map_or(fb_w, |l| l.width as f32),
10396                        lt.map_or(fb_h, |l| l.height as f32),
10397                    )
10398                }
10399            };
10400            self.queue.write_buffer(
10401                &self.globals_staging,
10402                (i as u64) * globals_bytes,
10403                bytemuck::bytes_of(&make_globals(target_w, target_h)),
10404            );
10405        }
10406
10407        let bind_mask = self.atlas_bind_group_mask();
10408        let bind_color = self.atlas_bind_group_color();
10409        let mut clip_depth: u32 = 0;
10410        let mut clip_depth_stack: Vec<u32> = Vec::new();
10411
10412        let snapshot_source = target_texture.cloned();
10413        for (pass_index, pass) in std::mem::take(&mut passes).into_iter().enumerate() {
10414            // Populate backdrop snapshots for blends composited in this
10415            // pass: copy the parent target region into the snapshot
10416            // texture. Passes execute in order, so the parent holds the
10417            // true backdrop. Surface parents need the target texture (no
10418            // swapchain sampling mid-frame); layer parents copy from the
10419            // pool. Either way the region is parent-target pixels. Copies
10420            // for other passes stay queued (`remaining`).
10421            let needs_snapshot = pass
10422                .cmds
10423                .iter()
10424                .any(|c| matches!(c, Cmd::BlendLayer { .. }));
10425            if needs_snapshot {
10426                let copies = std::mem::take(&mut self.blend_copies);
10427                let has_surface_copy = copies.iter().any(|copy| copy.target == PassTarget::Surface);
10428                let mut resolved_surface = None;
10429                if has_surface_copy
10430                    && !self.working_space
10431                    && let (Some(msaa_view), Some(resolve_view)) =
10432                        (self.msaa_view.as_ref(), self.surface_resolve_view.as_ref())
10433                {
10434                    let resolve_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
10435                        label: Some("surface blend resolve"),
10436                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10437                            view: msaa_view,
10438                            resolve_target: Some(resolve_view),
10439                            ops: wgpu::Operations {
10440                                load: wgpu::LoadOp::Load,
10441                                store: wgpu::StoreOp::Store,
10442                            },
10443                            depth_slice: None,
10444                        })],
10445                        depth_stencil_attachment: None,
10446                        timestamp_writes: None,
10447                        occlusion_query_set: None,
10448                        multiview_mask: None,
10449                    });
10450                    drop(resolve_pass);
10451                    resolved_surface = self.surface_resolve_tex.clone();
10452                }
10453                let mut remaining = Vec::with_capacity(copies.len());
10454                for copy in copies {
10455                    if copy.pass_id != pass.id {
10456                        remaining.push(copy);
10457                        continue;
10458                    }
10459                    let BlendCopy {
10460                        blend_id,
10461                        target,
10462                        region,
10463                        ..
10464                    } = copy;
10465                    let (src_tex, tw, th) = match target {
10466                        PassTarget::Layer(parent_id) => match self.layer_pool.get(&parent_id) {
10467                            Some(lt) => (lt.texture.clone(), lt.width, lt.height),
10468                            None => continue,
10469                        },
10470                        PassTarget::Surface => {
10471                            let texture = if self.working_space {
10472                                self.ws_tex.clone()
10473                            } else if self.msaa_view.is_some() {
10474                                resolved_surface.clone()
10475                            } else {
10476                                snapshot_source.clone()
10477                            };
10478                            match texture {
10479                                Some(texture) => (texture, self.output_width, self.output_height),
10480                                None => {
10481                                    self.remove_blend_snapshot(blend_id);
10482                                    continue;
10483                                }
10484                            }
10485                        }
10486                    };
10487                    let Some(dst_tex) = self.blend_snapshot_texture(blend_id) else {
10488                        continue;
10489                    };
10490                    if !src_tex.usage().contains(wgpu::TextureUsages::COPY_SRC)
10491                        || !dst_tex.usage().contains(wgpu::TextureUsages::COPY_DST)
10492                    {
10493                        self.remove_blend_snapshot(blend_id);
10494                        continue;
10495                    }
10496                    let Some((sx, sy, cw, ch)) = checked_copy_region(region, tw, th) else {
10497                        self.remove_blend_snapshot(blend_id);
10498                        continue;
10499                    };
10500                    encoder.copy_texture_to_texture(
10501                        wgpu::TexelCopyTextureInfo {
10502                            texture: &src_tex,
10503                            mip_level: 0,
10504                            origin: wgpu::Origin3d { x: sx, y: sy, z: 0 },
10505                            aspect: wgpu::TextureAspect::All,
10506                        },
10507                        wgpu::TexelCopyTextureInfo {
10508                            texture: &dst_tex,
10509                            mip_level: 0,
10510                            origin: wgpu::Origin3d::ZERO,
10511                            aspect: wgpu::TextureAspect::All,
10512                        },
10513                        wgpu::Extent3d {
10514                            width: cw,
10515                            height: ch,
10516                            depth_or_array_layers: 1,
10517                        },
10518                    );
10519                }
10520                self.blend_copies = remaining;
10521            }
10522            let (color_view, resolve_target, depth_stencil_view, is_layer, working_target) =
10523                match pass.target {
10524                    PassTarget::Surface => {
10525                        let swap_view = target_view.clone();
10526                        let use_ws = self.working_space && self.ws_view.is_some();
10527                        let (color, resolve) = if use_ws {
10528                            let ws_view = self.ws_view.as_ref().unwrap();
10529                            if let Some(msaa_view) = &self.ws_msaa_view {
10530                                (msaa_view.clone(), Some(ws_view.clone()))
10531                            } else {
10532                                (ws_view.clone(), None)
10533                            }
10534                        } else if let Some(msaa_view) = &self.msaa_view {
10535                            (msaa_view.clone(), Some(swap_view))
10536                        } else {
10537                            (swap_view, None)
10538                        };
10539                        (
10540                            color,
10541                            resolve,
10542                            self.depth_stencil_view.clone(),
10543                            false,
10544                            use_ws,
10545                        )
10546                    }
10547                    PassTarget::Layer(layer_id) => {
10548                        if let Some(lt) = self.layer_pool.get(&layer_id) {
10549                            (
10550                                lt.view.clone(),
10551                                None,
10552                                lt.depth_stencil_view.clone(),
10553                                true,
10554                                false,
10555                            )
10556                        } else {
10557                            log::warn!("missing layer target {layer_id}");
10558                            continue;
10559                        }
10560                    }
10561                };
10562
10563            encoder.copy_buffer_to_buffer(
10564                &self.globals_staging,
10565                (pass_index as u64) * globals_bytes,
10566                &self.globals_buf,
10567                0,
10568                globals_bytes,
10569            );
10570
10571            if is_layer {
10572                clip_depth_stack.push(clip_depth);
10573                clip_depth = 0;
10574            }
10575
10576            let (tw, th) = match pass.target {
10577                PassTarget::Surface => (self.output_width, self.output_height),
10578                PassTarget::Layer(layer_id) => self
10579                    .layer_pool
10580                    .get(&layer_id)
10581                    .map(|l| (l.width, l.height))
10582                    .unwrap_or((self.output_width, self.output_height)),
10583            };
10584            let initial_scissor = clamp_scissor(
10585                pass.initial_scissor.0,
10586                pass.initial_scissor.1,
10587                pass.initial_scissor.2,
10588                pass.initial_scissor.3,
10589                tw,
10590                th,
10591            );
10592            let mut active_scissor = pass
10593                .active_scissor
10594                .map(|scissor| clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th));
10595            if active_scissor.is_some_and(|scissor| scissor.2 == 0 || scissor.3 == 0) {
10596                active_scissor = None;
10597            }
10598
10599            let pipes: &Pipelines = if is_layer {
10600                if self.working_space {
10601                    &self.working_space_layer_pipes
10602                } else {
10603                    &self.layer_pipes
10604                }
10605            } else if working_target {
10606                &self.working_space_pipes
10607            } else {
10608                &self.surface_pipes
10609            };
10610
10611            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
10612                label: Some("pass"),
10613                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10614                    view: &color_view,
10615                    resolve_target: resolve_target.as_ref(),
10616                    ops: wgpu::Operations {
10617                        load: match pass.clear_color {
10618                            Some(c) => wgpu::LoadOp::Clear(wgpu::Color {
10619                                r: c[0] as f64,
10620                                g: c[1] as f64,
10621                                b: c[2] as f64,
10622                                a: c[3] as f64,
10623                            }),
10624                            None => wgpu::LoadOp::Load,
10625                        },
10626                        store: wgpu::StoreOp::Store,
10627                    },
10628                    depth_slice: None,
10629                })],
10630                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
10631                    view: &depth_stencil_view,
10632                    depth_ops: None,
10633                    stencil_ops: Some(wgpu::Operations {
10634                        load: if pass.clear_color.is_some() {
10635                            wgpu::LoadOp::Clear(STENCIL_BASE)
10636                        } else {
10637                            wgpu::LoadOp::Load
10638                        },
10639                        store: wgpu::StoreOp::Store,
10640                    }),
10641                }),
10642                timestamp_writes: None,
10643                occlusion_query_set: None,
10644                multiview_mask: None,
10645            });
10646
10647            rpass.set_bind_group(0, &self.globals_bind, &[]);
10648            rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
10649            let initial_draw_scissor = if initial_scissor.2 == 0 || initial_scissor.3 == 0 {
10650                (0, 0, 1, 1)
10651            } else {
10652                initial_scissor
10653            };
10654            rpass.set_scissor_rect(
10655                initial_draw_scissor.0,
10656                initial_draw_scissor.1,
10657                initial_draw_scissor.2,
10658                initial_draw_scissor.3,
10659            );
10660
10661            macro_rules! draw_simple {
10662                ($pipeline:expr, $ring:expr, $inst:ty, $off:ident, $n:ident) => {{
10663                    rpass.set_pipeline($pipeline);
10664                    let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
10665                    rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
10666                    rpass.draw(0..6, 0..$n);
10667                }};
10668            }
10669
10670            macro_rules! draw_with_bind {
10671                ($pipeline:expr, $ring:expr, $inst:ty, $bind:expr, $off:ident, $n:ident) => {{
10672                    rpass.set_pipeline($pipeline);
10673                    rpass.set_bind_group(1, $bind, &[]);
10674                    let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
10675                    rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
10676                    rpass.draw(0..6, 0..$n);
10677                }};
10678            }
10679
10680            macro_rules! draw_indexed_mesh {
10681                ($pipeline:expr, $uoff:ident, $mesh:ident) => {{
10682                    rpass.set_pipeline($pipeline);
10683                    rpass.set_bind_group(1, &self.mesh_bind, &[$uoff as u32]);
10684                    let vbytes =
10685                        ($mesh.vertex_count as u64) * std::mem::size_of::<MeshVertex>() as u64;
10686                    rpass.set_vertex_buffer(0, $mesh.vertices.slice(0..vbytes));
10687                    let ibytes = ($mesh.index_count as u64) * std::mem::size_of::<u32>() as u64;
10688                    rpass.set_index_buffer(
10689                        $mesh.indices.slice(0..ibytes),
10690                        wgpu::IndexFormat::Uint32,
10691                    );
10692                    rpass.draw_indexed(0..$mesh.index_count, 0, 0..1);
10693                }};
10694            }
10695
10696            for cmd in pass.cmds {
10697                if active_scissor.is_none()
10698                    && !matches!(
10699                        &cmd,
10700                        Cmd::ClipPush { .. }
10701                            | Cmd::ClipPop { .. }
10702                            | Cmd::VectorClipPush { .. }
10703                            | Cmd::VectorClipPop { .. }
10704                    )
10705                {
10706                    continue;
10707                }
10708                match cmd {
10709                    Cmd::ClipPush {
10710                        off,
10711                        cnt: n,
10712                        scissor,
10713                        difference,
10714                        applied,
10715                    } => {
10716                        let scissor =
10717                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
10718                        let usable = applied && scissor.2 > 0 && scissor.3 > 0;
10719                        if usable {
10720                            rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
10721                            active_scissor = Some(scissor);
10722                            rpass.set_pipeline(if difference {
10723                                &pipes.clip_dec
10724                            } else {
10725                                &pipes.clip_bin
10726                            });
10727
10728                            let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
10729                            rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
10730                            rpass.draw(0..6, 0..n);
10731                            if !difference {
10732                                clip_depth = (clip_depth + 1).min(STENCIL_MAX_DEPTH);
10733                            }
10734                        } else {
10735                            active_scissor = None;
10736                        }
10737                        rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
10738                    }
10739
10740                    Cmd::ClipPop {
10741                        off,
10742                        cnt: n,
10743                        scissor,
10744                        stencil_scissor,
10745                        difference,
10746                        applied,
10747                    } => {
10748                        let scissor =
10749                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
10750                        let dec = clamp_scissor(
10751                            stencil_scissor.0,
10752                            stencil_scissor.1,
10753                            stencil_scissor.2,
10754                            stencil_scissor.3,
10755                            tw,
10756                            th,
10757                        );
10758                        if scissor.2 > 0 && scissor.3 > 0 {
10759                            rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
10760                            active_scissor = Some(scissor);
10761                        } else {
10762                            active_scissor = None;
10763                        }
10764                        if applied && n > 0 && dec.2 > 0 && dec.3 > 0 {
10765                            rpass.set_scissor_rect(dec.0, dec.1, dec.2, dec.3);
10766                            rpass.set_pipeline(if difference {
10767                                &pipes.clip_bin
10768                            } else {
10769                                &pipes.clip_dec
10770                            });
10771                            let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
10772                            rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
10773                            rpass.draw(0..6, 0..n);
10774                            if scissor.2 > 0 && scissor.3 > 0 {
10775                                rpass.set_scissor_rect(
10776                                    scissor.0,
10777                                    scissor.1,
10778                                    scissor.2,
10779                                    scissor.3,
10780                                );
10781                            }
10782                            if !difference {
10783                                clip_depth = clip_depth.saturating_sub(1);
10784                            }
10785                        }
10786                        rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
10787                    }
10788
10789                    Cmd::Rect { off, cnt: n } => {
10790                        draw_simple!(&pipes.rects, self.rects.ring, RectInstance, off, n);
10791                    }
10792
10793                    Cmd::Border { off, cnt: n } => {
10794                        draw_simple!(&pipes.borders, self.borders.ring, BorderInstance, off, n);
10795                    }
10796
10797                    Cmd::GlyphsMask { off, cnt: n } => {
10798                        draw_with_bind!(
10799                            &pipes.text_mask,
10800                            self.glyph_mask.ring,
10801                            GlyphInstance,
10802                            &bind_mask,
10803                            off,
10804                            n
10805                        );
10806                    }
10807
10808                    Cmd::GlyphsColor { off, cnt: n } => {
10809                        draw_with_bind!(
10810                            &pipes.text_color,
10811                            self.glyph_color.ring,
10812                            GlyphInstance,
10813                            &bind_color,
10814                            off,
10815                            n
10816                        );
10817                    }
10818
10819                    Cmd::GlyphsVector { off, cnt: n } => {
10820                        if let Some(slug_pipe) = pipes.slug.as_ref() {
10821                            rpass.set_pipeline(slug_pipe);
10822                            let bytes = (n as u64) * std::mem::size_of::<slug::TessVertex>() as u64;
10823                            rpass.set_vertex_buffer(0, self.slug_ring.buf.slice(off..off + bytes));
10824                            rpass.draw(0..n, 0..1);
10825                        }
10826                    }
10827
10828                    Cmd::ImageRgba {
10829                        off,
10830                        cnt: n,
10831                        handle,
10832                        filter,
10833                        tile,
10834                    } => {
10835                        let bind_opt = match self.images.get(&handle) {
10836                            Some(ImageTex::Rgba { binds, .. }) => Some(binds.get(filter, tile)),
10837                            Some(ImageTex::User { binds, .. }) => Some(binds.get(filter, tile)),
10838                            _ => None,
10839                        };
10840                        if let Some(bind) = bind_opt {
10841                            draw_with_bind!(
10842                                &pipes.image_rgba,
10843                                self.glyph_color.ring,
10844                                GlyphInstance,
10845                                bind,
10846                                off,
10847                                n
10848                            );
10849                        }
10850                    }
10851                    Cmd::Coverage {
10852                        off,
10853                        cnt: n,
10854                        handle,
10855                    } => {
10856                        if let Some(tile) = self.coverages.get(&handle) {
10857                            draw_with_bind!(
10858                                &pipes.coverage,
10859                                self.glyph_color.ring,
10860                                GlyphInstance,
10861                                &tile.bind,
10862                                off,
10863                                n
10864                            );
10865                        }
10866                    }
10867
10868                    Cmd::ImageNv12 {
10869                        off,
10870                        cnt: n,
10871                        handle,
10872                        filter,
10873                    } => {
10874                        if let Some(ImageTex::Nv12 { binds, .. }) = self.images.get(&handle) {
10875                            draw_with_bind!(
10876                                &pipes.image_nv12,
10877                                self.nv12.ring,
10878                                Nv12Instance,
10879                                binds.get(filter, false),
10880                                off,
10881                                n
10882                            );
10883                        }
10884                    }
10885
10886                    Cmd::Ellipse { off, cnt: n } => {
10887                        draw_simple!(&pipes.ellipses, self.ellipses.ring, EllipseInstance, off, n);
10888                    }
10889
10890                    Cmd::EllipseBorder { off, cnt: n } => {
10891                        draw_simple!(
10892                            &pipes.ellipse_borders,
10893                            self.ellipse_borders.ring,
10894                            EllipseBorderInstance,
10895                            off,
10896                            n
10897                        );
10898                    }
10899
10900                    Cmd::Arc { off, cnt: n } => {
10901                        draw_simple!(&pipes.arcs, self.arcs.ring, ArcInstance, off, n);
10902                    }
10903
10904                    Cmd::CompositeLayer {
10905                        off,
10906                        cnt: n,
10907                        layer_id,
10908                    } => {
10909                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
10910                            draw_with_bind!(
10911                                &pipes.image_rgba,
10912                                self.glyph_color.ring,
10913                                GlyphInstance,
10914                                &lt.bind,
10915                                off,
10916                                n
10917                            );
10918                        }
10919                    }
10920                    Cmd::CompositeShadow {
10921                        off,
10922                        cnt: n,
10923                        layer_id,
10924                    } => {
10925                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
10926                            draw_with_bind!(
10927                                &pipes.blur,
10928                                self.blur_ring,
10929                                BlurInstance,
10930                                &lt.bind_linear,
10931                                off,
10932                                n
10933                            );
10934                        }
10935                    }
10936                    Cmd::CompositeBlur {
10937                        off,
10938                        cnt: n,
10939                        layer_id,
10940                    } => {
10941                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
10942                            draw_with_bind!(
10943                                &pipes.blur_content,
10944                                self.blur_ring,
10945                                BlurInstance,
10946                                &lt.bind_linear,
10947                                off,
10948                                n
10949                            );
10950                        }
10951                    }
10952                    Cmd::CompositeProjective {
10953                        off,
10954                        cnt: n,
10955                        layer_id,
10956                    } => {
10957                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
10958                            // The layer texture is sampled with the rgba
10959                            // (non-linear-filter) binding, like the sharp
10960                            // composite path.
10961                            draw_with_bind!(
10962                                &pipes.projective_layer,
10963                                self.projective_ring,
10964                                ProjectiveInstance,
10965                                &lt.bind,
10966                                off,
10967                                n
10968                            );
10969                        }
10970                    }
10971
10972                    Cmd::BlendLayer {
10973                        off,
10974                        cnt: n,
10975                        src_layer,
10976                        dst_layer,
10977                        parent,
10978                        scissor,
10979                    } => {
10980                        let src = self.layer_pool.get(&src_layer).cloned();
10981                        // Backdrop is the snapshot copied from the parent
10982                        // target before this pass opened (a texture cannot be
10983                        // sampled mid-pass while bound as a render target).
10984                        // A missing snapshot (surface parent without texture,
10985                        // evicted layer) skips the draw: the mesh vanishes
10986                        // rather than misrendering.
10987                        let dst = dst_layer.and_then(|id| self.blend_snapshots.get(&id).cloned());
10988                        let in_parent = match parent {
10989                            PassTarget::Surface => !is_layer,
10990                            PassTarget::Layer(id) => {
10991                                matches!(pass.target, PassTarget::Layer(pid) if pid == id)
10992                            }
10993                        };
10994                        if let (Some(src_lt), Some(dst_snap)) = (src, dst)
10995                            && in_parent
10996                        {
10997                            let bytes = (n as u64) * std::mem::size_of::<BlendInstance>() as u64;
10998                            rpass.set_pipeline(&pipes.blend_layer);
10999                            let scissor =
11000                                clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
11001                            if scissor.2 > 0 && scissor.3 > 0 {
11002                                rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11003                                rpass.set_bind_group(0, &self.globals_bind, &[]);
11004                                rpass.set_bind_group(1, &src_lt.bind, &[]);
11005                                rpass.set_bind_group(2, &dst_snap.bind, &[]);
11006                                rpass.set_vertex_buffer(
11007                                    0,
11008                                    self.blend_ring.buf.slice(off..off + bytes),
11009                                );
11010                                rpass.draw(0..6, 0..n);
11011                            }
11012                        }
11013                    }
11014
11015                    Cmd::VectorMesh { mesh, uoff, blend } => {
11016                        let pipe = match blend {
11017                            repose_core::BlendMode::Add => &pipes.mesh_add,
11018                            repose_core::BlendMode::Multiply => &pipes.mesh_multiply,
11019                            repose_core::BlendMode::Screen => &pipes.mesh_screen,
11020                            repose_core::BlendMode::Darken => &pipes.mesh_darken,
11021                            repose_core::BlendMode::Lighten => &pipes.mesh_lighten,
11022                            _ => &pipes.mesh,
11023                        };
11024                        draw_indexed_mesh!(pipe, uoff, mesh);
11025                    }
11026
11027                    Cmd::VectorOverlay { mesh, uoff } => {
11028                        rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11029                        draw_indexed_mesh!(&pipes.mesh_overlay, uoff, mesh);
11030                    }
11031
11032                    Cmd::VectorClipPush {
11033                        mesh,
11034                        uoff,
11035                        scissor,
11036                        difference,
11037                        applied,
11038                    } => {
11039                        let scissor =
11040                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
11041                        let usable = applied && scissor.2 > 0 && scissor.3 > 0;
11042                        if usable {
11043                            rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11044                            active_scissor = Some(scissor);
11045                            let pipe = if difference {
11046                                &pipes.mesh_clip_dec
11047                            } else {
11048                                &pipes.mesh_clip_inc
11049                            };
11050                            draw_indexed_mesh!(pipe, uoff, mesh);
11051                            if !difference {
11052                                clip_depth = (clip_depth + 1).min(STENCIL_MAX_DEPTH);
11053                            }
11054                        } else {
11055                            active_scissor = None;
11056                        }
11057                        rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11058                    }
11059
11060                    Cmd::VectorClipPop {
11061                        mesh,
11062                        uoff,
11063                        scissor,
11064                        stencil_scissor,
11065                        difference,
11066                        applied,
11067                    } => {
11068                        let scissor =
11069                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
11070                        let dec = clamp_scissor(
11071                            stencil_scissor.0,
11072                            stencil_scissor.1,
11073                            stencil_scissor.2,
11074                            stencil_scissor.3,
11075                            tw,
11076                            th,
11077                        );
11078                        if scissor.2 > 0 && scissor.3 > 0 {
11079                            rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11080                            active_scissor = Some(scissor);
11081                        } else {
11082                            active_scissor = None;
11083                        }
11084                        if applied && dec.2 > 0 && dec.3 > 0 {
11085                            rpass.set_scissor_rect(dec.0, dec.1, dec.2, dec.3);
11086                            let pipe = if difference {
11087                                &pipes.mesh_clip_inc
11088                            } else {
11089                                &pipes.mesh_clip_dec
11090                            };
11091                            draw_indexed_mesh!(pipe, uoff, mesh);
11092                            if scissor.2 > 0 && scissor.3 > 0 {
11093                                rpass.set_scissor_rect(
11094                                    scissor.0,
11095                                    scissor.1,
11096                                    scissor.2,
11097                                    scissor.3,
11098                                );
11099                            }
11100                            if !difference {
11101                                clip_depth = clip_depth.saturating_sub(1);
11102                            }
11103                        }
11104                        rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11105                    }
11106
11107                    Cmd::Callback {
11108                        rect,
11109                        clip_rect,
11110                        scissor,
11111                        restore_scissor,
11112                        callback_id,
11113                        payload,
11114                    } => {
11115                        if let Some(cb) = payload.downcast_ref::<Callback>() {
11116                            // Round INWARD. A callback may only paint inside its
11117                            // own rect, and a fullscreen pass draws a single
11118                            // clip-space quad spanning twice the viewport, so an
11119                            // outward-rounded viewport overshot the rect and the
11120                            // outward-rounded scissor let that overshoot through.
11121                            let vp_x = rect.x.ceil().max(0.0);
11122                            let vp_y = rect.y.ceil().max(0.0);
11123                            let vp_w = ((rect.x + rect.w).floor() - vp_x).max(1.0);
11124                            let vp_h = ((rect.y + rect.h).floor() - vp_y).max(1.0);
11125                            if vp_w > 0.0 && vp_h > 0.0 && clip_rect.w > 0.0 && clip_rect.h > 0.0 {
11126                                rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11127                                rpass.set_viewport(vp_x, vp_y, vp_w, vp_h, 0.0, 1.0);
11128                                let info = repose_core::PaintCallbackInfo {
11129                                    viewport: rect,
11130                                    clip_rect,
11131                                    pixels_per_point: self.pixels_per_point,
11132                                    screen_size_px: [tw, th],
11133                                };
11134                                let descriptor = ScreenDescriptor {
11135                                    size_in_pixels: [tw, th],
11136                                    pixels_per_point: self.pixels_per_point,
11137                                    target_format: if is_layer {
11138                                        self.layer_target_format()
11139                                    } else if working_target {
11140                                        wgpu::TextureFormat::Rgba16Float
11141                                    } else {
11142                                        self.output_format
11143                                    },
11144                                    sample_count: if is_layer {
11145                                        1
11146                                    } else {
11147                                        self.active_surface_msaa_samples()
11148                                    },
11149                                };
11150                                let scope =
11151                                    callback_scope_key(cb, callback_id, pass.target, &descriptor);
11152                                let resources = self
11153                                    .callback_scoped_resources
11154                                    .get(&scope)
11155                                    .unwrap_or(&self.callback_resources);
11156                                let mut callback_pass = CallbackRenderPass::new(
11157                                    &mut rpass,
11158                                    descriptor.target_format,
11159                                    descriptor.sample_count,
11160                                );
11161                                cb.0.paint(info, &mut callback_pass, resources);
11162                                rpass.set_viewport(0.0, 0.0, tw as f32, th as f32, 0.0, 1.0);
11163                                let restore = if restore_scissor.2 > 0 && restore_scissor.3 > 0 {
11164                                    restore_scissor
11165                                } else {
11166                                    (0, 0, 1, 1)
11167                                };
11168                                rpass.set_scissor_rect(restore.0, restore.1, restore.2, restore.3);
11169                                rpass.set_bind_group(0, &self.globals_bind, &[]);
11170                                rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11171                            }
11172                        } else {
11173                            log::warn!("Unknown paint callback payload");
11174                        }
11175                    }
11176                }
11177            }
11178            if is_layer {
11179                clip_depth = clip_depth_stack.pop().unwrap_or(0);
11180            }
11181        }
11182
11183        let transient_ids: HashSet<u32> = flatten_ids_used.iter().copied().collect();
11184        let producer_ids = std::mem::take(&mut self.producer_layer_ids);
11185        let stale_layers: Vec<u32> = self
11186            .layer_pool
11187            .iter()
11188            .filter_map(|(id, _)| {
11189                (!producer_ids.contains(id) && !transient_ids.contains(id)).then_some(*id)
11190            })
11191            .collect();
11192        for id in stale_layers {
11193            if let Some(layer) = self.layer_pool.remove(&id) {
11194                self.layer_bytes_total = self.layer_bytes_total.saturating_sub(layer.bytes);
11195                if layer.transient {
11196                    self.transient_layer_bytes_total =
11197                        self.transient_layer_bytes_total.saturating_sub(layer.bytes);
11198                }
11199            }
11200        }
11201        self.flatten_layer_ids = flatten_ids_used;
11202
11203        // Display pass: linear working space -> sRGB OETF -> swapchain
11204        if self.working_space
11205            && let (Some(_ws_view), Some(ws_bind), Some(display_pipeline)) =
11206                (&self.ws_view, &self.ws_bind, &self.display_pipeline)
11207        {
11208            let swap_view = target_view.clone();
11209            let mut display_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
11210                label: Some("display transform"),
11211                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11212                    view: &swap_view,
11213                    resolve_target: None,
11214                    ops: wgpu::Operations {
11215                        load: wgpu::LoadOp::Load,
11216                        store: wgpu::StoreOp::Store,
11217                    },
11218                    depth_slice: None,
11219                })],
11220                depth_stencil_attachment: None,
11221                timestamp_writes: None,
11222                occlusion_query_set: None,
11223                multiview_mask: None,
11224            });
11225            display_pass.set_pipeline(display_pipeline);
11226            display_pass.set_bind_group(1, ws_bind, &[]);
11227            display_pass.draw(0..3, 0..1);
11228        }
11229        self.recycle_blend_snapshots();
11230    }
11231
11232    /// Render a scene into an externally-provided texture view.
11233    /// Use this when embedding Repose in a host that owns the GPU.
11234    /// The host is responsible for submitting the encoder and handling present.
11235    pub fn render_to_view(
11236        &mut self,
11237        scene: &Scene,
11238        encoder: &mut wgpu::CommandEncoder,
11239        target_view: &wgpu::TextureView,
11240        width: u32,
11241        height: u32,
11242        clear_color: Option<[f64; 4]>,
11243    ) {
11244        self.resize(width, height);
11245
11246        if let Err(error) = self.begin_frame() {
11247            log::error!("render_to_view: {error:#}");
11248            return;
11249        }
11250
11251        if self.output_width == 0 || self.output_height == 0 {
11252            self.end_frame();
11253            return;
11254        }
11255
11256        self.render_scene_to_encoder(scene, encoder, target_view, clear_color);
11257        self.end_frame();
11258    }
11259}
11260
11261#[cfg(target_os = "linux")]
11262fn validate_dmabuf_plane(
11263    fd: &std::os::unix::io::OwnedFd,
11264    offset: u64,
11265    stride: u32,
11266    width: u32,
11267    height: u32,
11268    bytes_per_pixel: u32,
11269) -> anyhow::Result<u64> {
11270    if width == 0 || height == 0 || bytes_per_pixel == 0 {
11271        anyhow::bail!("DMA-BUF plane dimensions and bytes per pixel must be non-zero");
11272    }
11273    if offset % 4 != 0 || stride % 4 != 0 {
11274        anyhow::bail!("DMA-BUF offset and stride must be 4-byte aligned");
11275    }
11276    let row_bytes = (width as u64)
11277        .checked_mul(bytes_per_pixel as u64)
11278        .ok_or_else(|| anyhow::anyhow!("DMA-BUF row size overflow"))?;
11279    if (stride as u64) < row_bytes {
11280        anyhow::bail!(
11281            "DMA-BUF stride {} is smaller than row size {row_bytes}",
11282            stride
11283        );
11284    }
11285    let span = (height as u64 - 1)
11286        .checked_mul(stride as u64)
11287        .and_then(|value| value.checked_add(row_bytes))
11288        .ok_or_else(|| anyhow::anyhow!("DMA-BUF plane size overflow"))?;
11289    let file = std::fs::File::from(
11290        fd.try_clone()
11291            .map_err(|error| anyhow::anyhow!("duplicate DMA-BUF fd: {error}"))?,
11292    );
11293    let file_size = file
11294        .metadata()
11295        .map_err(|error| anyhow::anyhow!("stat DMA-BUF fd: {error}"))?
11296        .len();
11297    if offset > file_size || span > file_size - offset {
11298        anyhow::bail!("DMA-BUF plane offset/size {offset}+{span} exceeds file size {file_size}");
11299    }
11300    Ok(file_size)
11301}
11302
11303fn is_filterable_color_format(format: wgpu::TextureFormat) -> bool {
11304    matches!(
11305        format,
11306        wgpu::TextureFormat::Rgba8Unorm
11307            | wgpu::TextureFormat::Rgba8UnormSrgb
11308            | wgpu::TextureFormat::Bgra8Unorm
11309            | wgpu::TextureFormat::Bgra8UnormSrgb
11310            | wgpu::TextureFormat::Rgba16Unorm
11311            | wgpu::TextureFormat::Rgba16Float
11312            | wgpu::TextureFormat::Rgba32Float
11313            | wgpu::TextureFormat::Rgb10a2Unorm
11314    )
11315}
11316
11317fn native_format_supported(format: wgpu::TextureFormat, features: wgpu::Features) -> bool {
11318    if !is_filterable_color_format(format) {
11319        return false;
11320    }
11321    match format {
11322        wgpu::TextureFormat::Rgba16Unorm => {
11323            features.contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
11324        }
11325        wgpu::TextureFormat::Rgba32Float => features.contains(wgpu::Features::FLOAT32_FILTERABLE),
11326        _ => true,
11327    }
11328}
11329
11330fn validate_sampler_descriptor(
11331    device: &wgpu::Device,
11332    sampler: &wgpu::SamplerDescriptor<'_>,
11333) -> anyhow::Result<()> {
11334    if sampler.compare.is_some()
11335        || !matches!(
11336            sampler.mag_filter,
11337            wgpu::FilterMode::Nearest | wgpu::FilterMode::Linear
11338        )
11339        || !matches!(
11340            sampler.min_filter,
11341            wgpu::FilterMode::Nearest | wgpu::FilterMode::Linear
11342        )
11343        || sampler.anisotropy_clamp == 0
11344        || !sampler.lod_min_clamp.is_finite()
11345        || !sampler.lod_max_clamp.is_finite()
11346        || sampler.lod_min_clamp < 0.0
11347        || sampler.lod_max_clamp < sampler.lod_min_clamp
11348    {
11349        anyhow::bail!("native sampler must be non-comparison linear filtering with finite LODs");
11350    }
11351    if matches!(sampler.address_mode_u, wgpu::AddressMode::ClampToBorder)
11352        || matches!(sampler.address_mode_v, wgpu::AddressMode::ClampToBorder)
11353        || matches!(sampler.address_mode_w, wgpu::AddressMode::ClampToBorder)
11354    {
11355        if !device
11356            .features()
11357            .contains(wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER)
11358        {
11359            anyhow::bail!("native sampler uses unsupported clamp-to-border addressing");
11360        }
11361    }
11362    Ok(())
11363}
11364
11365fn validate_texture_dimensions(
11366    device: &wgpu::Device,
11367    width: u32,
11368    height: u32,
11369) -> anyhow::Result<()> {
11370    if width == 0
11371        || height == 0
11372        || width > device.limits().max_texture_dimension_2d
11373        || height > device.limits().max_texture_dimension_2d
11374    {
11375        anyhow::bail!("texture dimensions are outside the device limit");
11376    }
11377    Ok(())
11378}
11379
11380fn validate_image_handle(handle: u64) -> anyhow::Result<()> {
11381    if handle == 0 {
11382        anyhow::bail!("image handle 0 is reserved");
11383    }
11384    Ok(())
11385}
11386
11387fn checked_image_bytes(w: u32, h: u32, bytes_per_pixel: u32) -> anyhow::Result<u64> {
11388    if w == 0 || h == 0 || bytes_per_pixel == 0 {
11389        anyhow::bail!("image dimensions and bytes per pixel must be non-zero");
11390    }
11391    (w as u64)
11392        .checked_mul(h as u64)
11393        .and_then(|size| size.checked_mul(bytes_per_pixel as u64))
11394        .ok_or_else(|| anyhow::anyhow!("image byte size overflow"))
11395}
11396
11397fn texture_storage_bytes(
11398    format: wgpu::TextureFormat,
11399    width: u32,
11400    height: u32,
11401) -> anyhow::Result<u64> {
11402    if width == 0 || height == 0 {
11403        anyhow::bail!("texture dimensions must be non-zero");
11404    }
11405    let bytes = format.theoretical_memory_footprint(wgpu::Extent3d {
11406        width,
11407        height,
11408        depth_or_array_layers: 1,
11409    });
11410    if bytes == 0 {
11411        anyhow::bail!("texture format has no storage size");
11412    }
11413    Ok(bytes)
11414}
11415
11416fn texture_storage_bytes_with_samples(
11417    format: wgpu::TextureFormat,
11418    width: u32,
11419    height: u32,
11420    sample_count: u32,
11421) -> anyhow::Result<u64> {
11422    texture_storage_bytes(format, width, height)?
11423        .checked_mul(u64::from(sample_count.max(1)))
11424        .ok_or_else(|| anyhow::anyhow!("multisample texture size overflow"))
11425}
11426
11427fn checked_image_bytes_usize(w: u32, h: u32, bytes_per_pixel: u32) -> anyhow::Result<usize> {
11428    usize::try_from(checked_image_bytes(w, h, bytes_per_pixel)?)
11429        .map_err(|_| anyhow::anyhow!("image byte size exceeds addressable memory"))
11430}
11431
11432fn clamp_scissor(x: u32, y: u32, w: u32, h: u32, tw: u32, th: u32) -> (u32, u32, u32, u32) {
11433    if w == 0 || h == 0 || tw == 0 || th == 0 {
11434        return (0, 0, 0, 0);
11435    }
11436    let x = x.min(tw - 1);
11437    let y = y.min(th - 1);
11438    let w = w.min(tw - x);
11439    let h = h.min(th - y);
11440    (x, y, w, h)
11441}
11442
11443fn rect_to_scissor(r: repose_core::Rect, width: u32, height: u32) -> (u32, u32, u32, u32) {
11444    if width == 0
11445        || height == 0
11446        || !r.x.is_finite()
11447        || !r.y.is_finite()
11448        || !r.w.is_finite()
11449        || !r.h.is_finite()
11450        || r.w <= 0.0
11451        || r.h <= 0.0
11452    {
11453        return (0, 0, 0, 0);
11454    }
11455    // Inward, for the same reason as `to_scissor`: a clip covers only whole
11456    // pixels inside its rect.
11457    let x0 = r.x.ceil().max(0.0).min(width as f32);
11458    let y0 = r.y.ceil().max(0.0).min(height as f32);
11459    let x1 = (r.x + r.w).floor().max(x0).min(width as f32);
11460    let y1 = (r.y + r.h).floor().max(y0).min(height as f32);
11461    if x1 <= x0 || y1 <= y0 {
11462        return (0, 0, 0, 0);
11463    }
11464    (x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32)
11465}
11466
11467fn rect_to_ndc(r: repose_core::Rect, width: f32, height: f32) -> [f32; 4] {
11468    if width <= 0.0 || height <= 0.0 || !r.x.is_finite() || !r.y.is_finite() {
11469        return [0.0, 0.0, 0.0, 0.0];
11470    }
11471    let x0 = r.x / width * 2.0 - 1.0;
11472    let y0 = 1.0 - r.y / height * 2.0;
11473    let x1 = (r.x + r.w) / width * 2.0 - 1.0;
11474    let y1 = 1.0 - (r.y + r.h) / height * 2.0;
11475    [x0.min(x1), y0.min(y1), (x1 - x0).abs(), (y1 - y0).abs()]
11476}
11477
11478fn parent_rect_to_layer_rect(
11479    rect: repose_core::Rect,
11480    parent_transform: &Transform,
11481    origin: (f32, f32),
11482) -> Option<repose_core::Rect> {
11483    let inverse = parent_transform.inverse_linear()?;
11484    let tx = parent_transform.translate_x;
11485    let ty = parent_transform.translate_y;
11486    let corners = [
11487        (rect.x, rect.y),
11488        (rect.x + rect.w, rect.y),
11489        (rect.x, rect.y + rect.h),
11490        (rect.x + rect.w, rect.y + rect.h),
11491    ];
11492    let mut min_x = f32::INFINITY;
11493    let mut min_y = f32::INFINITY;
11494    let mut max_x = f32::NEG_INFINITY;
11495    let mut max_y = f32::NEG_INFINITY;
11496    for (x, y) in corners {
11497        let local_x = x - tx;
11498        let local_y = y - ty;
11499        let transformed_x = inverse[0] * local_x + inverse[1] * local_y;
11500        let transformed_y = inverse[2] * local_x + inverse[3] * local_y;
11501        min_x = min_x.min(transformed_x);
11502        min_y = min_y.min(transformed_y);
11503        max_x = max_x.max(transformed_x);
11504        max_y = max_y.max(transformed_y);
11505    }
11506    let result = repose_core::Rect {
11507        x: min_x - origin.0,
11508        y: min_y - origin.1,
11509        w: (max_x - min_x).max(0.0),
11510        h: (max_y - min_y).max(0.0),
11511    };
11512    (result.x.is_finite() && result.y.is_finite() && result.w.is_finite() && result.h.is_finite())
11513        .then_some(result)
11514}
11515
11516fn parent_radii_to_layer_radii(radii: [f32; 4], parent_transform: &Transform) -> Option<[f32; 4]> {
11517    let m = parent_transform.linear();
11518    let sx = (m[0] * m[0] + m[2] * m[2]).sqrt();
11519    let sy = (m[1] * m[1] + m[3] * m[3]).sqrt();
11520    (sx.is_finite() && sy.is_finite() && sx > 1e-6 && sy > 1e-6)
11521        .then(|| [radii[0] / sx, radii[1] / sy, radii[2] / sx, radii[3] / sy])
11522}
11523
11524fn parent_affine_to_layer_affine(
11525    affine: [f32; 6],
11526    parent_transform: &Transform,
11527    origin: (f32, f32),
11528) -> Option<[f32; 6]> {
11529    let inverse = parent_transform.inverse_linear()?;
11530    let tx = affine[4] - parent_transform.translate_x;
11531    let ty = affine[5] - parent_transform.translate_y;
11532    Some([
11533        inverse[0] * affine[0] + inverse[1] * affine[2],
11534        inverse[0] * affine[1] + inverse[1] * affine[3],
11535        inverse[2] * affine[0] + inverse[3] * affine[2],
11536        inverse[2] * affine[1] + inverse[3] * affine[3],
11537        inverse[0] * tx + inverse[1] * ty - origin.0,
11538        inverse[2] * tx + inverse[3] * ty - origin.1,
11539    ])
11540}
11541
11542fn checked_copy_region(
11543    region: repose_core::Rect,
11544    target_width: u32,
11545    target_height: u32,
11546) -> Option<(u32, u32, u32, u32)> {
11547    if target_width == 0
11548        || target_height == 0
11549        || !region.x.is_finite()
11550        || !region.y.is_finite()
11551        || !region.w.is_finite()
11552        || !region.h.is_finite()
11553        || region.w <= 0.0
11554        || region.h <= 0.0
11555    {
11556        return None;
11557    }
11558    let x0 = region.x.floor().max(0.0).min(target_width as f32) as u32;
11559    let y0 = region.y.floor().max(0.0).min(target_height as f32) as u32;
11560    let x1 = (region.x + region.w)
11561        .ceil()
11562        .max(x0 as f32)
11563        .min(target_width as f32) as u32;
11564    let y1 = (region.y + region.h)
11565        .ceil()
11566        .max(y0 as f32)
11567        .min(target_height as f32) as u32;
11568    (x1 > x0 && y1 > y0).then_some((x0, y0, x1 - x0, y1 - y0))
11569}
11570
11571fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> repose_core::Rect {
11572    let x0 = a.x.max(b.x);
11573    let y0 = a.y.max(b.y);
11574    let x1 = (a.x + a.w).min(b.x + b.w);
11575    let y1 = (a.y + a.h).min(b.y + b.h);
11576    repose_core::Rect {
11577        x: x0,
11578        y: y0,
11579        w: (x1 - x0).max(0.0),
11580        h: (y1 - y0).max(0.0),
11581    }
11582}
11583
11584#[cfg(test)]
11585mod tests {
11586    use super::*;
11587
11588    #[test]
11589    fn image_source_rect_uses_texel_edges() {
11590        let source = resolve_image_source(Some(ImageSourceRect::new(2, 1, 3, 2)), 10, 8)
11591            .expect("source should be valid");
11592        assert_eq!(source.uv_min, [0.2, 0.125]);
11593        assert_eq!(source.uv_max, [0.5, 0.375]);
11594        assert_eq!(source.width, 3.0);
11595        assert_eq!(source.height, 2.0);
11596        assert!(resolve_image_source(Some(ImageSourceRect::new(8, 0, 3, 1)), 10, 8).is_none());
11597        assert!(resolve_image_source(Some(ImageSourceRect::new(0, 0, 0, 1)), 10, 8).is_none());
11598    }
11599
11600    #[test]
11601    fn image_fit_stays_inside_selected_source() {
11602        let source = resolve_image_source(Some(ImageSourceRect::new(2, 1, 4, 2)), 10, 8)
11603            .expect("source should be valid");
11604        let rect = repose_core::Rect {
11605            x: 20.0,
11606            y: 30.0,
11607            w: 8.0,
11608            h: 8.0,
11609        };
11610        let (_, uv) = image_fit_geometry(rect, ImageFit::Cover, ImageAlignment::Center, &source)
11611            .expect("cover geometry should be valid");
11612        assert!(uv[0] >= source.uv_min[0] && uv[2] <= source.uv_max[0]);
11613        assert!(uv[1] >= source.uv_min[1] && uv[3] <= source.uv_max[1]);
11614    }
11615
11616    #[test]
11617    fn ring_alignment_rejects_bad_input() {
11618        assert_eq!(align_up(0, 4).unwrap(), 0);
11619        assert_eq!(align_up(1, 4).unwrap(), 4);
11620        assert!(align_up(1, 3).is_err());
11621        assert!(align_up(u64::MAX, 4).is_err());
11622    }
11623
11624    /// A repack moves every surviving glyph, so eviction must never touch one
11625    /// the in-flight frame already holds UVs for, and must never empty the map.
11626    /// Glyph 1 is the coldest overall but in flight, so it has to survive even
11627    /// though glyphs 3 and 4 are warmer and evictable.
11628    #[test]
11629    fn atlas_eviction_spares_the_frame_in_flight() {
11630        fn key(glyph_id: u32) -> repose_text::CacheKey {
11631            repose_text::CacheKey {
11632                font_id: 1,
11633                glyph_id,
11634                font_size_bits: 16.0f32.to_bits(),
11635            }
11636        }
11637        let mut map: HashMap<repose_text::CacheKey, GlyphInfo> = HashMap::new();
11638        for (glyph_id, last_touch, last_used_frame) in
11639            [(1u32, 10u64, 9u64), (2, 20, 7), (3, 30, 7), (4, 40, 7)]
11640        {
11641            map.insert(
11642                key(glyph_id),
11643                GlyphInfo {
11644                    u0: 0.0,
11645                    v0: 0.0,
11646                    u1: 0.1,
11647                    v1: 0.1,
11648                    w: 10.0,
11649                    h: 10.0,
11650                    last_touch,
11651                    last_used_frame,
11652                },
11653            );
11654        }
11655        let mut used_area = 400;
11656        assert!(evict_atlas_lru(&mut map, &mut used_area, 9, 250));
11657        assert_eq!(used_area, 200);
11658        assert!(map.contains_key(&key(1)), "in-flight glyph was evicted");
11659        assert!(!map.contains_key(&key(2)) && !map.contains_key(&key(3)));
11660        assert!(map.contains_key(&key(4)));
11661    }
11662
11663    #[test]
11664    fn atlas_eviction_refuses_to_empty_the_atlas() {
11665        let only = repose_text::CacheKey {
11666            font_id: 1,
11667            glyph_id: 1,
11668            font_size_bits: 16.0f32.to_bits(),
11669        };
11670        let mut map: HashMap<repose_text::CacheKey, GlyphInfo> = HashMap::new();
11671        map.insert(
11672            only,
11673            GlyphInfo {
11674                u0: 0.0,
11675                v0: 0.0,
11676                u1: 0.1,
11677                v1: 0.1,
11678                w: 10.0,
11679                h: 10.0,
11680                last_touch: 1,
11681                last_used_frame: 1,
11682            },
11683        );
11684        let mut used_area = 100;
11685        assert!(!evict_atlas_lru(&mut map, &mut used_area, 2, 0));
11686        assert_eq!(map.len(), 1);
11687        assert_eq!(used_area, 100);
11688    }
11689
11690    #[test]
11691    fn empty_and_negative_rects_are_suppressed() {
11692        assert_eq!(
11693            rect_to_scissor(
11694                repose_core::Rect {
11695                    x: 0.0,
11696                    y: 0.0,
11697                    w: 0.0,
11698                    h: 4.0,
11699                },
11700                16,
11701                16,
11702            ),
11703            (0, 0, 0, 0)
11704        );
11705        assert_eq!(
11706            rect_to_scissor(
11707                repose_core::Rect {
11708                    x: 0.0,
11709                    y: 0.0,
11710                    w: -1.0,
11711                    h: 4.0,
11712                },
11713                16,
11714                16,
11715            ),
11716            (0, 0, 0, 0)
11717        );
11718    }
11719
11720    #[test]
11721    fn copy_region_is_checked() {
11722        assert_eq!(
11723            checked_copy_region(
11724                repose_core::Rect {
11725                    x: 2.25,
11726                    y: 3.5,
11727                    w: 4.0,
11728                    h: 5.0,
11729                },
11730                16,
11731                16,
11732            ),
11733            Some((2, 3, 5, 6))
11734        );
11735        assert_eq!(
11736            checked_copy_region(
11737                repose_core::Rect {
11738                    x: 20.0,
11739                    y: 0.0,
11740                    w: 2.0,
11741                    h: 2.0,
11742                },
11743                16,
11744                16,
11745            ),
11746            None
11747        );
11748    }
11749
11750    #[test]
11751    fn p010_uses_ten_bit_sample_range() {
11752        let raw = make_yuv_transform_raw(ColorInfo::default(), PixelFormat::P010);
11753        let expected = 65535.0 / (64.0 * 1020.0);
11754        assert!((raw.b[3] - expected).abs() < 1e-6);
11755        assert_eq!(canonical_fourcc(PixelFormat::P010), P010_FOURCC);
11756        assert_eq!(canonical_fourcc(PixelFormat::Nv12), NV12_FOURCC);
11757        assert_eq!(
11758            make_yuv_transform_raw(
11759                ColorInfo {
11760                    transfer: repose_core::color::Transfer::Pq,
11761                    ..ColorInfo::default()
11762                },
11763                PixelFormat::Nv12,
11764            )
11765            .transfer[0],
11766            3.0
11767        );
11768        assert_eq!(
11769            make_yuv_transform_raw(
11770                ColorInfo {
11771                    transfer: repose_core::color::Transfer::Hlg,
11772                    ..ColorInfo::default()
11773                },
11774                PixelFormat::Nv12,
11775            )
11776            .transfer[0],
11777            4.0
11778        );
11779    }
11780}