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