Skip to main content

repose_render_wgpu/
lib.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::num::NonZero;
4#[cfg(feature = "winit-surface")]
5use std::panic::{AssertUnwindSafe, catch_unwind};
6use std::sync::Arc;
7
8use repose_core::color::{ChromaSiting, ColorInfo, PixelFormat};
9use repose_core::request_frame;
10use repose_core::{
11    Brush, FontStyle, GlyphRasterConfig, PresentModePref, RenderBackend, Scene, SceneNode,
12    StrokeCap, Transform, Vec2,
13};
14use wgpu::Instance;
15
16mod slug;
17
18mod commands;
19pub use commands::apply_render_commands;
20
21pub mod offscreen;
22
23mod callback;
24pub use callback::{Callback, CallbackResources, ScreenDescriptor, WgpuCallback};
25
26mod depth_composite;
27pub use depth_composite::DepthComposite;
28
29#[derive(Clone)]
30struct UploadRing {
31    buf: wgpu::Buffer,
32    cap: u64,
33    head: u64,
34    usage: wgpu::BufferUsages,
35}
36
37impl UploadRing {
38    fn new(device: &wgpu::Device, label: &str, cap: u64, usage: wgpu::BufferUsages) -> Self {
39        let buf = device.create_buffer(&wgpu::BufferDescriptor {
40            label: Some(label),
41            size: cap,
42            usage,
43            mapped_at_creation: false,
44        });
45        Self {
46            buf,
47            cap,
48            head: 0,
49            usage,
50        }
51    }
52
53    fn reset(&mut self) {
54        self.head = 0;
55    }
56
57    fn grow_to_fit(&mut self, device: &wgpu::Device, needed: u64) {
58        let start = (self.head + 3) & !3;
59        let aligned_needed = (needed + 3) & !3;
60        // Need start + needed within cap, accounting for alignment padding
61        if start + needed <= self.cap {
62            return;
63        }
64        let required = start + needed;
65        let mut new_cap = required.next_power_of_two().max(self.cap * 2).max(256);
66        new_cap = (new_cap + 3) & !3;
67        if new_cap < aligned_needed {
68            new_cap = aligned_needed.next_power_of_two();
69        }
70        self.buf = device.create_buffer(&wgpu::BufferDescriptor {
71            label: Some("upload ring (grown)"),
72            size: new_cap,
73            usage: self.usage,
74            mapped_at_creation: false,
75        });
76        self.cap = new_cap;
77        if start + needed > self.cap {
78            self.head = 0;
79        }
80    }
81
82    fn alloc_write(&mut self, queue: &wgpu::Queue, bytes: &[u8]) -> (u64, u64) {
83        let len = bytes.len() as u64;
84        let start = (self.head + 3) & !3; // align to 4
85        let end = start + len;
86        if end > self.cap {
87            // Instead of panicking, grow and reset
88            log::error!(
89                "UploadRing overflow: start={start} len={len} cap={} - growing",
90                self.cap
91            );
92            if len > self.cap {
93                // Need larger buffer; create via grow_to_fit side-effect not available here (no device)
94                // Fallback: truncate write to avoid UB, return dummy range
95                return (0, 0);
96            }
97            // Wrap to beginning if alignment pushed us over
98            let wrapped_start = 0;
99            let wrapped_end = len;
100            if wrapped_end <= self.cap {
101                queue.write_buffer(&self.buf, wrapped_start, bytes);
102                self.head = wrapped_end;
103                return (wrapped_start, len);
104            }
105            return (0, 0);
106        }
107        queue.write_buffer(&self.buf, start, bytes);
108        self.head = end;
109        (start, len)
110    }
111}
112
113struct InstancedPipe<I: bytemuck::Pod> {
114    ring: UploadRing,
115    _marker: std::marker::PhantomData<I>,
116}
117
118impl<I: bytemuck::Pod> InstancedPipe<I> {
119    fn new(ring: UploadRing) -> Self {
120        Self {
121            ring,
122            _marker: std::marker::PhantomData,
123        }
124    }
125
126    fn upload(
127        &mut self,
128        device: &wgpu::Device,
129        queue: &wgpu::Queue,
130        data: &[I],
131    ) -> Option<(u64, u32)> {
132        if data.is_empty() {
133            return None;
134        }
135        let bytes = bytemuck::cast_slice(data);
136        self.ring.grow_to_fit(device, bytes.len() as u64);
137        let (off, wrote) = self.ring.alloc_write(queue, bytes);
138        if wrote as usize != bytes.len() {
139            log::error!(
140                "upload skipped: batch {}B exceeds ring {}B",
141                bytes.len(),
142                self.ring.cap
143            );
144            return None;
145        }
146        Some((off, data.len() as u32))
147    }
148
149    fn reset(&mut self) {
150        self.ring.reset();
151    }
152}
153
154#[repr(C)]
155#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
156struct Globals {
157    ndc_to_px: [f32; 2],
158    _pad: [f32; 2],
159}
160
161fn make_globals(target_w: f32, target_h: f32) -> Globals {
162    Globals {
163        ndc_to_px: [target_w * 0.5, target_h * 0.5],
164        _pad: [0.0, 0.0],
165    }
166}
167
168pub struct WgpuSceneRenderer {
169    pub device: wgpu::Device,
170    pub queue: wgpu::Queue,
171    pub output_format: wgpu::TextureFormat,
172    pub output_width: u32,
173    pub output_height: u32,
174    /// Pixels per point (DPI scale) for `ScreenDescriptor` / `PaintCallbackInfo`.
175    pub pixels_per_point: f32,
176
177    // Render pipelines. Two sets: one for the MSAA surface pass, one for
178    // graphics-layer render-to-texture passes (sample_count = 1).
179    surface_pipes: Pipelines,
180    layer_pipes: Pipelines,
181
182    // Instanced draw rings
183    rects: InstancedPipe<RectInstance>,
184    borders: InstancedPipe<BorderInstance>,
185    ellipses: InstancedPipe<EllipseInstance>,
186    ellipse_borders: InstancedPipe<EllipseBorderInstance>,
187    arcs: InstancedPipe<ArcInstance>,
188    glyph_mask: InstancedPipe<GlyphInstance>,
189    glyph_color: InstancedPipe<GlyphInstance>,
190
191    // Image bind layouts and shared sampler
192    image_bind_layout_rgba: wgpu::BindGroupLayout,
193    image_bind_layout_nv12: wgpu::BindGroupLayout,
194    image_sampler: wgpu::Sampler,
195    layer_sampler: wgpu::Sampler,
196    layer_sampler_linear: wgpu::Sampler,
197
198    // Blur composite ring (for graphics-layer drop shadows)
199    blur_ring: UploadRing,
200
201    text_bind_layout: wgpu::BindGroupLayout,
202
203    // Stencil clip ring
204    clip_ring: UploadRing,
205
206    // Projective layer-composite ring (one ProjectiveInstance per flattened
207    // perspective subtree)
208    projective_ring: UploadRing,
209
210    // Backdrop-blend composite ring (one BlendInstance per isolated blend)
211    blend_ring: UploadRing,
212
213    // Tessellated vector glyph pipeline (always enabled)
214    slug_enabled: bool,
215    slug_ring: UploadRing,
216    slug_cache: slug::GlyphSlugCache,
217
218    // Instanced NV12 ring
219    nv12: InstancedPipe<Nv12Instance>,
220
221    // Tessellated vector mesh rendering (host-provided, e.g. lyon output).
222    mesh_verts: UploadRing,
223    mesh_indices: UploadRing,
224    mesh_uniform_buf: wgpu::Buffer,
225    mesh_bind_layout: wgpu::BindGroupLayout,
226    mesh_bind: wgpu::BindGroup,
227    mesh_uniform_head: u64,
228    /// CPU mirror of the active vector-clip stack: (voff, vcnt, ioff, icnt,
229    /// uoff, difference) of each pushed mask so `PopVectorClip` can re-draw
230    /// it to decrement the stencil.
231    mesh_clip_stack: Vec<(u64, u32, u64, u32, u64, bool)>,
232
233    /// Translator-owned flatten layer ids used by the previous frame;
234    /// drained from the layer pool at the start of each translation (they
235    /// are single-frame by construction).
236    flatten_layer_ids: Vec<u32>,
237
238    /// Backdrop snapshots keyed by isolated-blend layer id. Filled during
239    /// translation (texture allocated) and populated by a texture copy at
240    /// execution time, before the blend composite draws.
241    blend_snapshots: std::collections::HashMap<u32, BlendSnapshot>,
242    /// (blend layer id, parent target) copies to run before the pass that
243    /// composites the blend. Executed between passes: copies the current
244    /// target region into the snapshot texture.
245    blend_copies: Vec<(u32, PassTarget, repose_core::Rect)>,
246
247    msaa_samples: u32,
248
249    // Depth-stencil target
250    depth_stencil_tex: wgpu::Texture,
251    depth_stencil_view: wgpu::TextureView,
252
253    // Optional MSAA color target
254    msaa_tex: Option<wgpu::Texture>,
255    msaa_view: Option<wgpu::TextureView>,
256
257    globals_buf: wgpu::Buffer,
258    globals_bind: wgpu::BindGroup,
259
260    // Glyph atlas
261    atlas_mask: AtlasA8,
262    atlas_color: AtlasRGBA,
263
264    // Image management
265    next_image_handle: u64,
266    images: HashMap<u64, ImageTex>,
267    retained: HashMap<u64, RetainedImage>,
268
269    // A8 coverage-tile management (host-rasterized masks composited tinted;
270    // no retained CPU copies — tiles are immutable and re-registered).
271    next_coverage_handle: u64,
272    coverages: HashMap<u64, CoverageTex>,
273
274    // Eviction stats
275    frame_index: u64,
276    image_bytes_total: u64,
277    image_evict_after_frames: u64,
278    image_budget_bytes: u64,
279
280    // Graphics layer pool. Maps `SceneNode::BeginLayer::layer_id` to a
281    // cached offscreen render target.
282    layer_pool: HashMap<u32, LayerTarget>,
283
284    // Linear working-space mode (default off -> fast playback path).
285    // When enabled, the scene is rendered into an Rgba16Float intermediate
286    // texture, then a final full-screen pass applies the display OETF.
287    working_space: bool,
288    ws_tex: Option<wgpu::Texture>,
289    ws_view: Option<wgpu::TextureView>,
290    ws_bind: Option<wgpu::BindGroup>,
291    display_pipeline: Option<wgpu::RenderPipeline>,
292    display_layout: Option<wgpu::BindGroupLayout>,
293
294    pub callback_resources: CallbackResources,
295}
296
297pub struct WgpuSurfaceBackend {
298    pub surface: Option<wgpu::Surface<'static>>,
299    pub surface_config: Option<wgpu::SurfaceConfiguration>,
300    pub renderer: WgpuSceneRenderer,
301}
302
303impl std::ops::Deref for WgpuSurfaceBackend {
304    type Target = WgpuSceneRenderer;
305    fn deref(&self) -> &Self::Target {
306        &self.renderer
307    }
308}
309impl std::ops::DerefMut for WgpuSurfaceBackend {
310    fn deref_mut(&mut self) -> &mut Self::Target {
311        &mut self.renderer
312    }
313}
314
315#[cfg(feature = "winit-surface")]
316pub type WgpuBackend = WgpuSurfaceBackend;
317
318impl Drop for WgpuSceneRenderer {
319    fn drop(&mut self) {
320        let _ = self.device.poll(wgpu::PollType::Poll);
321        #[cfg(not(target_arch = "wasm32"))]
322        {
323            let _ = self.device.poll(wgpu::PollType::Wait {
324                submission_index: None,
325                timeout: Some(std::time::Duration::from_millis(100)),
326            });
327        }
328    }
329}
330
331#[derive(Clone)]
332struct LayerTarget {
333    texture: wgpu::Texture,
334    view: wgpu::TextureView,
335    bind: wgpu::BindGroup,
336    bind_linear: wgpu::BindGroup,
337    depth_stencil_view: wgpu::TextureView,
338    width: u32,
339    height: u32,
340    rect_px: (f32, f32, f32, f32),
341}
342
343/// Backdrop snapshot for one isolated blend: a copy of the current target
344/// region taken before the source layer is composited, sampled as the
345/// "backdrop" input of the blend shader.
346#[derive(Clone)]
347struct BlendSnapshot {
348    texture: wgpu::Texture,
349    bind: wgpu::BindGroup,
350    width: u32,
351    height: u32,
352}
353
354/// Identifies which render target a `Pass` draws into.
355#[derive(Clone, Copy)]
356enum PassTarget {
357    Surface,
358    Layer(u32),
359}
360
361/// A bundle of render pipelines for a single sample-count target. Created
362/// twice: once with `sample_count = msaa_samples` for the surface pass, and
363/// once with `sample_count = 1` for graphics-layer render-to-texture passes
364/// (where MSAA is wasted).
365struct Pipelines {
366    rects: wgpu::RenderPipeline,
367    borders: wgpu::RenderPipeline,
368    ellipses: wgpu::RenderPipeline,
369    ellipse_borders: wgpu::RenderPipeline,
370    arcs: wgpu::RenderPipeline,
371    text_mask: wgpu::RenderPipeline,
372    text_color: wgpu::RenderPipeline,
373    image_rgba: wgpu::RenderPipeline,
374    /// Tinted A8 coverage composite (`coverage.wgsl`): same vertex
375    /// attributes and bind groups as the text/color path, sampling a
376    /// single-channel tile registered with `register_coverage_a8`.
377    coverage: wgpu::RenderPipeline,
378    image_nv12: wgpu::RenderPipeline,
379    blur: wgpu::RenderPipeline,
380    blur_content: wgpu::RenderPipeline,
381    clip_bin: wgpu::RenderPipeline,
382    clip_dec: wgpu::RenderPipeline,
383    slug: Option<wgpu::RenderPipeline>,
384    /// Tessellated vector mesh (fill/stroke). Uses an `Equal` stencil compare
385    /// so world content is correctly masked to the active `PushVectorClip`
386    /// shape (outside any clip the stencil is 0 == ref 0, so it draws).
387    mesh: wgpu::RenderPipeline,
388    /// Screen-space overlay meshes: `LessEqual` compare so they always draw
389    /// regardless of any active vector clip.
390    mesh_overlay: wgpu::RenderPipeline,
391    /// Fixed-function blend variants of the mesh pipeline, selected by
392    /// `BlendMode`. Backdrop-dependent modes (overlay, color-dodge/burn,
393    /// hard/soft-light, exclusion, hue/saturation/color/luminosity) use the
394    /// `blend_layer` shader instead.
395    mesh_add: wgpu::RenderPipeline,
396    mesh_multiply: wgpu::RenderPipeline,
397    mesh_screen: wgpu::RenderPipeline,
398    mesh_darken: wgpu::RenderPipeline,
399    mesh_lighten: wgpu::RenderPipeline,
400    /// Backdrop-blend composite: samples an isolated source layer and the
401    /// current target, applies the CSS blend formula selected by a uniform,
402    /// and composites premultiplied source-over.
403    blend_layer: wgpu::RenderPipeline,
404    /// Stencil increment for vector clips.
405    mesh_clip_inc: wgpu::RenderPipeline,
406    /// Stencil decrement for vector clips.
407    mesh_clip_dec: wgpu::RenderPipeline,
408    /// Projective layer composite (perspective flattening): samples a
409    /// graphics-layer texture through a 2D projective map. Drawn with a
410    /// `ProjectiveInstance` from `projective_ring`.
411    projective_layer: wgpu::RenderPipeline,
412}
413
414impl Pipelines {
415    fn create(
416        device: &wgpu::Device,
417        format: wgpu::TextureFormat,
418        sample_count: u32,
419        globals_layout: &wgpu::BindGroupLayout,
420        text_bind_layout: &wgpu::BindGroupLayout,
421        image_bind_layout_nv12: &wgpu::BindGroupLayout,
422        clip_pipeline_layout: &wgpu::PipelineLayout,
423        stencil_for_content: &wgpu::DepthStencilState,
424        stencil_for_clip_inc: &wgpu::DepthStencilState,
425        stencil_for_clip_dec: &wgpu::DepthStencilState,
426        clip_color_target: &wgpu::ColorTargetState,
427        clip_vertex_layout: &wgpu::VertexBufferLayout,
428        mesh_bind_layout: &wgpu::BindGroupLayout,
429    ) -> Self {
430        let msaa_state = wgpu::MultisampleState {
431            count: sample_count,
432            mask: !0,
433            alpha_to_coverage_enabled: false,
434        };
435
436        macro_rules! make_content_pipeline {
437            ($name:ident, $shader:literal, $inst_type:ty, $attrs:expr) => {
438                let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
439                    label: Some(concat!($shader, ".wgsl")),
440                    source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(concat!(
441                        "shaders/", $shader, ".wgsl"
442                    )))),
443                });
444                let pipeline_layout =
445                    device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
446                        label: Some(concat!($shader, " pipeline layout")),
447                        bind_group_layouts: &[Some(globals_layout)],
448                        immediate_size: 0,
449                    });
450                let $name = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
451                    label: Some(concat!($shader, " pipeline")),
452                    layout: Some(&pipeline_layout),
453                    vertex: wgpu::VertexState {
454                        module: &shader_module,
455                        entry_point: Some("vs_main"),
456                        buffers: &[Some(wgpu::VertexBufferLayout {
457                            array_stride: std::mem::size_of::<$inst_type>() as u64,
458                            step_mode: wgpu::VertexStepMode::Instance,
459                            attributes: $attrs,
460                        })],
461                        compilation_options: wgpu::PipelineCompilationOptions::default(),
462                    },
463                    fragment: Some(wgpu::FragmentState {
464                        module: &shader_module,
465                        entry_point: Some("fs_main"),
466                        targets: &[Some(wgpu::ColorTargetState {
467                            format,
468                            blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
469                            write_mask: wgpu::ColorWrites::ALL,
470                        })],
471                        compilation_options: wgpu::PipelineCompilationOptions::default(),
472                    }),
473                    primitive: wgpu::PrimitiveState::default(),
474                    depth_stencil: Some(stencil_for_content.clone()),
475                    multisample: msaa_state,
476                    multiview_mask: None,
477                    cache: None,
478                });
479            };
480        }
481
482        let rect_attrs: &[wgpu::VertexAttribute] = &[
483            wgpu::VertexAttribute {
484                shader_location: 0,
485                offset: 0,
486                format: wgpu::VertexFormat::Float32x4,
487            },
488            wgpu::VertexAttribute {
489                shader_location: 1,
490                offset: 16,
491                format: wgpu::VertexFormat::Float32x4,
492            },
493            wgpu::VertexAttribute {
494                shader_location: 2,
495                offset: 32,
496                format: wgpu::VertexFormat::Uint32,
497            },
498            wgpu::VertexAttribute {
499                shader_location: 3,
500                offset: 36,
501                format: wgpu::VertexFormat::Uint32,
502            },
503            wgpu::VertexAttribute {
504                shader_location: 4,
505                offset: 48,
506                format: wgpu::VertexFormat::Float32x4,
507            },
508            wgpu::VertexAttribute {
509                shader_location: 5,
510                offset: 64,
511                format: wgpu::VertexFormat::Float32x4,
512            },
513            wgpu::VertexAttribute {
514                shader_location: 6,
515                offset: 80,
516                format: wgpu::VertexFormat::Float32x2,
517            },
518            wgpu::VertexAttribute {
519                shader_location: 7,
520                offset: 88,
521                format: wgpu::VertexFormat::Float32x2,
522            },
523            wgpu::VertexAttribute {
524                shader_location: 8,
525                offset: 96,
526                format: wgpu::VertexFormat::Uint32,
527            },
528            wgpu::VertexAttribute {
529                shader_location: 9,
530                offset: 112,
531                format: wgpu::VertexFormat::Float32x4,
532            },
533        ];
534        let border_attrs: &[wgpu::VertexAttribute] = &[
535            wgpu::VertexAttribute {
536                shader_location: 0,
537                offset: 0,
538                format: wgpu::VertexFormat::Float32x4,
539            },
540            wgpu::VertexAttribute {
541                shader_location: 1,
542                offset: 16,
543                format: wgpu::VertexFormat::Float32x4,
544            },
545            wgpu::VertexAttribute {
546                shader_location: 2,
547                offset: 32,
548                format: wgpu::VertexFormat::Float32,
549            },
550            wgpu::VertexAttribute {
551                shader_location: 3,
552                offset: 36,
553                format: wgpu::VertexFormat::Uint32,
554            },
555            wgpu::VertexAttribute {
556                shader_location: 4,
557                offset: 48,
558                format: wgpu::VertexFormat::Uint32,
559            },
560            wgpu::VertexAttribute {
561                shader_location: 5,
562                offset: 52,
563                format: wgpu::VertexFormat::Float32x4,
564            },
565            wgpu::VertexAttribute {
566                shader_location: 6,
567                offset: 68,
568                format: wgpu::VertexFormat::Float32x4,
569            },
570            wgpu::VertexAttribute {
571                shader_location: 7,
572                offset: 84,
573                format: wgpu::VertexFormat::Float32x2,
574            },
575            wgpu::VertexAttribute {
576                shader_location: 8,
577                offset: 92,
578                format: wgpu::VertexFormat::Float32x2,
579            },
580            wgpu::VertexAttribute {
581                shader_location: 9,
582                offset: 100,
583                format: wgpu::VertexFormat::Uint32,
584            },
585            wgpu::VertexAttribute {
586                shader_location: 10,
587                offset: 116,
588                format: wgpu::VertexFormat::Float32x4,
589            },
590        ];
591        let ellipse_attrs: &[wgpu::VertexAttribute] = &[
592            wgpu::VertexAttribute {
593                shader_location: 0,
594                offset: 0,
595                format: wgpu::VertexFormat::Float32x4,
596            },
597            wgpu::VertexAttribute {
598                shader_location: 1,
599                offset: 16,
600                format: wgpu::VertexFormat::Uint32,
601            },
602            wgpu::VertexAttribute {
603                shader_location: 2,
604                offset: 20,
605                format: wgpu::VertexFormat::Uint32,
606            },
607            wgpu::VertexAttribute {
608                shader_location: 3,
609                offset: 32,
610                format: wgpu::VertexFormat::Float32x4,
611            },
612            wgpu::VertexAttribute {
613                shader_location: 4,
614                offset: 48,
615                format: wgpu::VertexFormat::Float32x4,
616            },
617            wgpu::VertexAttribute {
618                shader_location: 5,
619                offset: 64,
620                format: wgpu::VertexFormat::Float32x2,
621            },
622            wgpu::VertexAttribute {
623                shader_location: 6,
624                offset: 72,
625                format: wgpu::VertexFormat::Float32x2,
626            },
627            wgpu::VertexAttribute {
628                shader_location: 7,
629                offset: 80,
630                format: wgpu::VertexFormat::Uint32,
631            },
632            wgpu::VertexAttribute {
633                shader_location: 8,
634                offset: 96,
635                format: wgpu::VertexFormat::Float32x4,
636            },
637        ];
638        let ellipse_border_attrs: &[wgpu::VertexAttribute] = &[
639            wgpu::VertexAttribute {
640                shader_location: 0,
641                offset: 0,
642                format: wgpu::VertexFormat::Float32x4,
643            },
644            wgpu::VertexAttribute {
645                shader_location: 1,
646                offset: 16,
647                format: wgpu::VertexFormat::Float32,
648            },
649            wgpu::VertexAttribute {
650                shader_location: 2,
651                offset: 20,
652                format: wgpu::VertexFormat::Float32,
653            },
654            wgpu::VertexAttribute {
655                shader_location: 3,
656                offset: 24,
657                format: wgpu::VertexFormat::Uint32,
658            },
659            wgpu::VertexAttribute {
660                shader_location: 4,
661                offset: 28,
662                format: wgpu::VertexFormat::Uint32,
663            },
664            wgpu::VertexAttribute {
665                shader_location: 5,
666                offset: 32,
667                format: wgpu::VertexFormat::Float32x4,
668            },
669            wgpu::VertexAttribute {
670                shader_location: 6,
671                offset: 48,
672                format: wgpu::VertexFormat::Float32x4,
673            },
674            wgpu::VertexAttribute {
675                shader_location: 7,
676                offset: 64,
677                format: wgpu::VertexFormat::Float32x2,
678            },
679            wgpu::VertexAttribute {
680                shader_location: 8,
681                offset: 72,
682                format: wgpu::VertexFormat::Float32x2,
683            },
684            wgpu::VertexAttribute {
685                shader_location: 9,
686                offset: 80,
687                format: wgpu::VertexFormat::Uint32,
688            },
689            wgpu::VertexAttribute {
690                shader_location: 10,
691                offset: 96,
692                format: wgpu::VertexFormat::Float32x4,
693            },
694        ];
695
696        make_content_pipeline!(rects, "rect", RectInstance, rect_attrs);
697        make_content_pipeline!(borders, "border", BorderInstance, border_attrs);
698        make_content_pipeline!(ellipses, "ellipse", EllipseInstance, ellipse_attrs);
699        make_content_pipeline!(
700            ellipse_borders,
701            "ellipse_border",
702            EllipseBorderInstance,
703            ellipse_border_attrs
704        );
705
706        let arc_attrs: &[wgpu::VertexAttribute] = &[
707            wgpu::VertexAttribute {
708                shader_location: 0,
709                offset: 0,
710                format: wgpu::VertexFormat::Float32x4,
711            },
712            wgpu::VertexAttribute {
713                shader_location: 1,
714                offset: 16,
715                format: wgpu::VertexFormat::Float32,
716            },
717            wgpu::VertexAttribute {
718                shader_location: 2,
719                offset: 20,
720                format: wgpu::VertexFormat::Float32,
721            },
722            wgpu::VertexAttribute {
723                shader_location: 3,
724                offset: 24,
725                format: wgpu::VertexFormat::Float32,
726            },
727            wgpu::VertexAttribute {
728                shader_location: 4,
729                offset: 28,
730                format: wgpu::VertexFormat::Float32,
731            },
732            wgpu::VertexAttribute {
733                shader_location: 5,
734                offset: 32,
735                format: wgpu::VertexFormat::Uint32,
736            },
737            wgpu::VertexAttribute {
738                shader_location: 6,
739                offset: 36,
740                format: wgpu::VertexFormat::Uint32,
741            },
742            wgpu::VertexAttribute {
743                shader_location: 7,
744                offset: 48,
745                format: wgpu::VertexFormat::Float32x4,
746            },
747            wgpu::VertexAttribute {
748                shader_location: 8,
749                offset: 64,
750                format: wgpu::VertexFormat::Float32x4,
751            },
752            wgpu::VertexAttribute {
753                shader_location: 9,
754                offset: 80,
755                format: wgpu::VertexFormat::Float32x2,
756            },
757            wgpu::VertexAttribute {
758                shader_location: 10,
759                offset: 88,
760                format: wgpu::VertexFormat::Float32x2,
761            },
762            wgpu::VertexAttribute {
763                shader_location: 11,
764                offset: 96,
765                format: wgpu::VertexFormat::Uint32,
766            },
767            wgpu::VertexAttribute {
768                shader_location: 12,
769                offset: 100,
770                format: wgpu::VertexFormat::Float32,
771            },
772            wgpu::VertexAttribute {
773                shader_location: 13,
774                offset: 112,
775                format: wgpu::VertexFormat::Float32x4,
776            },
777        ];
778
779        make_content_pipeline!(arcs, "arc", ArcInstance, arc_attrs);
780
781        // Text (mask)
782        let text_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
783            label: Some("text.wgsl"),
784            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/text.wgsl"))),
785        });
786        // Text (color)
787        let text_color_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
788            label: Some("text_color.wgsl"),
789            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
790                "shaders/text_color.wgsl"
791            ))),
792        });
793        let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
794            label: Some("text pipeline layout"),
795            bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
796            immediate_size: 0,
797        });
798        let glyph_vertex = wgpu::VertexBufferLayout {
799            array_stride: std::mem::size_of::<GlyphInstance>() as u64,
800            step_mode: wgpu::VertexStepMode::Instance,
801            attributes: &[
802                wgpu::VertexAttribute {
803                    shader_location: 0,
804                    offset: 0,
805                    format: wgpu::VertexFormat::Float32x4,
806                },
807                wgpu::VertexAttribute {
808                    shader_location: 1,
809                    offset: 16,
810                    format: wgpu::VertexFormat::Float32x4,
811                },
812                wgpu::VertexAttribute {
813                    shader_location: 2,
814                    offset: 32,
815                    format: wgpu::VertexFormat::Float32x4,
816                },
817                wgpu::VertexAttribute {
818                    shader_location: 3,
819                    offset: 48,
820                    format: wgpu::VertexFormat::Float32x4,
821                },
822            ],
823        };
824        let text_mask = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
825            label: Some("text pipeline (mask)"),
826            layout: Some(&text_pipeline_layout),
827            vertex: wgpu::VertexState {
828                module: &text_mask_shader,
829                entry_point: Some("vs_main"),
830                buffers: &[Some(glyph_vertex.clone())],
831                compilation_options: wgpu::PipelineCompilationOptions::default(),
832            },
833            fragment: Some(wgpu::FragmentState {
834                module: &text_mask_shader,
835                entry_point: Some("fs_main"),
836                targets: &[Some(wgpu::ColorTargetState {
837                    format,
838                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
839                    write_mask: wgpu::ColorWrites::ALL,
840                })],
841                compilation_options: wgpu::PipelineCompilationOptions::default(),
842            }),
843            primitive: wgpu::PrimitiveState::default(),
844            depth_stencil: Some(stencil_for_content.clone()),
845            multisample: msaa_state,
846            multiview_mask: None,
847            cache: None,
848        });
849        let text_color = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
850            label: Some("text pipeline (color)"),
851            layout: Some(&text_pipeline_layout),
852            vertex: wgpu::VertexState {
853                module: &text_color_shader,
854                entry_point: Some("vs_main"),
855                buffers: &[Some(glyph_vertex.clone())],
856                compilation_options: wgpu::PipelineCompilationOptions::default(),
857            },
858            fragment: Some(wgpu::FragmentState {
859                module: &text_color_shader,
860                entry_point: Some("fs_main"),
861                targets: &[Some(wgpu::ColorTargetState {
862                    format,
863                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
864                    write_mask: wgpu::ColorWrites::ALL,
865                })],
866                compilation_options: wgpu::PipelineCompilationOptions::default(),
867            }),
868            primitive: wgpu::PrimitiveState::default(),
869            depth_stencil: Some(stencil_for_content.clone()),
870            multisample: msaa_state,
871            multiview_mask: None,
872            cache: None,
873        });
874        // image_rgba reuses the text color pipeline (same vertex/bindings).
875        let image_rgba = text_color.clone();
876
877        // Tinted A8 coverage composite. Same vertex attributes (GlyphInstance)
878        // and bind groups (globals + texture/sampler) as the text color path,
879        // sampling R8 tiles uploaded via `register_coverage_a8`.
880        let coverage_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
881            label: Some("coverage.wgsl"),
882            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/coverage.wgsl"))),
883        });
884        let coverage = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
885            label: Some("coverage pipeline (tinted a8)"),
886            layout: Some(&text_pipeline_layout),
887            vertex: wgpu::VertexState {
888                module: &coverage_shader,
889                entry_point: Some("vs_main"),
890                buffers: &[Some(glyph_vertex.clone())],
891                compilation_options: wgpu::PipelineCompilationOptions::default(),
892            },
893            fragment: Some(wgpu::FragmentState {
894                module: &coverage_shader,
895                entry_point: Some("fs_main"),
896                targets: &[Some(wgpu::ColorTargetState {
897                    format,
898                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
899                    write_mask: wgpu::ColorWrites::ALL,
900                })],
901                compilation_options: wgpu::PipelineCompilationOptions::default(),
902            }),
903            primitive: wgpu::PrimitiveState::default(),
904            depth_stencil: Some(stencil_for_content.clone()),
905            multisample: msaa_state,
906            multiview_mask: None,
907            cache: None,
908        });
909
910        // Blur composite pipeline (graphics-layer drop shadow)
911        let blur_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
912            label: Some("blur_shadow.wgsl"),
913            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
914                "shaders/blur_shadow.wgsl"
915            ))),
916        });
917        let blur_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
918            label: Some("blur pipeline layout"),
919            bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
920            immediate_size: 0,
921        });
922        let blur = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
923            label: Some("blur pipeline"),
924            layout: Some(&blur_pipeline_layout),
925            vertex: wgpu::VertexState {
926                module: &blur_shader,
927                entry_point: Some("vs_main"),
928                buffers: &[Some(wgpu::VertexBufferLayout {
929                    array_stride: std::mem::size_of::<BlurInstance>() as u64,
930                    step_mode: wgpu::VertexStepMode::Instance,
931                    attributes: &[
932                        wgpu::VertexAttribute {
933                            shader_location: 0,
934                            offset: 0,
935                            format: wgpu::VertexFormat::Float32x4,
936                        },
937                        wgpu::VertexAttribute {
938                            shader_location: 1,
939                            offset: 16,
940                            format: wgpu::VertexFormat::Float32x4,
941                        },
942                        wgpu::VertexAttribute {
943                            shader_location: 2,
944                            offset: 32,
945                            format: wgpu::VertexFormat::Float32x4,
946                        },
947                        wgpu::VertexAttribute {
948                            shader_location: 3,
949                            offset: 48,
950                            format: wgpu::VertexFormat::Float32x2,
951                        },
952                        wgpu::VertexAttribute {
953                            shader_location: 4,
954                            offset: 56,
955                            format: wgpu::VertexFormat::Float32x4,
956                        },
957                    ],
958                })],
959                compilation_options: wgpu::PipelineCompilationOptions::default(),
960            },
961            fragment: Some(wgpu::FragmentState {
962                module: &blur_shader,
963                entry_point: Some("fs_main"),
964                targets: &[Some(wgpu::ColorTargetState {
965                    format,
966                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
967                    write_mask: wgpu::ColorWrites::ALL,
968                })],
969                compilation_options: wgpu::PipelineCompilationOptions::default(),
970            }),
971            primitive: wgpu::PrimitiveState::default(),
972            depth_stencil: Some(stencil_for_content.clone()),
973            multisample: msaa_state,
974            multiview_mask: None,
975            cache: None,
976        });
977
978        // Content blur pipeline (full RGBA gaussian blur)
979        let blur_content_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
980            label: Some("blur_content.wgsl"),
981            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
982                "shaders/blur_content.wgsl"
983            ))),
984        });
985        let blur_content = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
986            label: Some("blur content pipeline"),
987            layout: Some(&blur_pipeline_layout),
988            vertex: wgpu::VertexState {
989                module: &blur_content_shader,
990                entry_point: Some("vs_main"),
991                buffers: &[Some(wgpu::VertexBufferLayout {
992                    array_stride: std::mem::size_of::<BlurInstance>() as u64,
993                    step_mode: wgpu::VertexStepMode::Instance,
994                    attributes: &[
995                        wgpu::VertexAttribute {
996                            shader_location: 0,
997                            offset: 0,
998                            format: wgpu::VertexFormat::Float32x4,
999                        },
1000                        wgpu::VertexAttribute {
1001                            shader_location: 1,
1002                            offset: 16,
1003                            format: wgpu::VertexFormat::Float32x4,
1004                        },
1005                        wgpu::VertexAttribute {
1006                            shader_location: 2,
1007                            offset: 32,
1008                            format: wgpu::VertexFormat::Float32x4,
1009                        },
1010                        wgpu::VertexAttribute {
1011                            shader_location: 3,
1012                            offset: 48,
1013                            format: wgpu::VertexFormat::Float32x2,
1014                        },
1015                        wgpu::VertexAttribute {
1016                            shader_location: 4,
1017                            offset: 56,
1018                            format: wgpu::VertexFormat::Float32x4,
1019                        },
1020                    ],
1021                })],
1022                compilation_options: wgpu::PipelineCompilationOptions::default(),
1023            },
1024            fragment: Some(wgpu::FragmentState {
1025                module: &blur_content_shader,
1026                entry_point: Some("fs_main"),
1027                targets: &[Some(wgpu::ColorTargetState {
1028                    format,
1029                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1030                    write_mask: wgpu::ColorWrites::ALL,
1031                })],
1032                compilation_options: wgpu::PipelineCompilationOptions::default(),
1033            }),
1034            primitive: wgpu::PrimitiveState::default(),
1035            depth_stencil: Some(stencil_for_content.clone()),
1036            multisample: msaa_state,
1037            multiview_mask: None,
1038            cache: None,
1039        });
1040
1041        // NV12 Image Pipeline
1042        let image_nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1043            label: Some("image_nv12.wgsl"),
1044            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1045                "shaders/image_nv12.wgsl"
1046            ))),
1047        });
1048        let image_nv12_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1049            label: Some("image nv12 pipeline layout"),
1050            bind_group_layouts: &[Some(globals_layout), Some(image_bind_layout_nv12)],
1051            immediate_size: 0,
1052        });
1053        let image_nv12 = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1054            label: Some("image nv12 pipeline"),
1055            layout: Some(&image_nv12_layout),
1056            vertex: wgpu::VertexState {
1057                module: &image_nv12_shader,
1058                entry_point: Some("vs_main"),
1059                buffers: &[Some(wgpu::VertexBufferLayout {
1060                    array_stride: std::mem::size_of::<Nv12Instance>() as u64,
1061                    step_mode: wgpu::VertexStepMode::Instance,
1062                    attributes: &[
1063                        wgpu::VertexAttribute {
1064                            shader_location: 0,
1065                            offset: 0,
1066                            format: wgpu::VertexFormat::Float32x4,
1067                        },
1068                        wgpu::VertexAttribute {
1069                            shader_location: 1,
1070                            offset: 16,
1071                            format: wgpu::VertexFormat::Float32x4,
1072                        },
1073                        wgpu::VertexAttribute {
1074                            shader_location: 2,
1075                            offset: 32,
1076                            format: wgpu::VertexFormat::Float32x4,
1077                        },
1078                        wgpu::VertexAttribute {
1079                            shader_location: 3,
1080                            offset: 48,
1081                            format: wgpu::VertexFormat::Float32,
1082                        },
1083                        wgpu::VertexAttribute {
1084                            shader_location: 4,
1085                            offset: 52,
1086                            format: wgpu::VertexFormat::Float32x4,
1087                        },
1088                    ],
1089                })],
1090                compilation_options: wgpu::PipelineCompilationOptions::default(),
1091            },
1092            fragment: Some(wgpu::FragmentState {
1093                module: &image_nv12_shader,
1094                entry_point: Some("fs_main"),
1095                targets: &[Some(wgpu::ColorTargetState {
1096                    format,
1097                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1098                    write_mask: wgpu::ColorWrites::ALL,
1099                })],
1100                compilation_options: wgpu::PipelineCompilationOptions::default(),
1101            }),
1102            primitive: wgpu::PrimitiveState::default(),
1103            depth_stencil: Some(stencil_for_content.clone()),
1104            multisample: msaa_state,
1105            multiview_mask: None,
1106            cache: None,
1107        });
1108
1109        // Clipping
1110        let clip_shader_bin = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1111            label: Some("clip_round_rect_bin.wgsl"),
1112            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1113                "shaders/clip_round_rect_bin.wgsl"
1114            ))),
1115        });
1116        let clip_bin = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1117            label: Some("clip pipeline (bin)"),
1118            layout: Some(clip_pipeline_layout),
1119            vertex: wgpu::VertexState {
1120                module: &clip_shader_bin,
1121                entry_point: Some("vs_main"),
1122                buffers: &[Some(clip_vertex_layout.clone())],
1123                compilation_options: wgpu::PipelineCompilationOptions::default(),
1124            },
1125            fragment: Some(wgpu::FragmentState {
1126                module: &clip_shader_bin,
1127                entry_point: Some("fs_main"),
1128                targets: &[Some(clip_color_target.clone())],
1129                compilation_options: wgpu::PipelineCompilationOptions::default(),
1130            }),
1131            primitive: wgpu::PrimitiveState::default(),
1132            depth_stencil: Some(stencil_for_clip_inc.clone()),
1133            multisample: wgpu::MultisampleState {
1134                count: sample_count,
1135                mask: !0,
1136                alpha_to_coverage_enabled: false,
1137            },
1138            multiview_mask: None,
1139            cache: None,
1140        });
1141        let clip_dec = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1142            label: Some("clip pipeline (dec)"),
1143            layout: Some(clip_pipeline_layout),
1144            vertex: wgpu::VertexState {
1145                module: &clip_shader_bin,
1146                entry_point: Some("vs_main"),
1147                buffers: &[Some(clip_vertex_layout.clone())],
1148                compilation_options: wgpu::PipelineCompilationOptions::default(),
1149            },
1150            fragment: Some(wgpu::FragmentState {
1151                module: &clip_shader_bin,
1152                entry_point: Some("fs_main"),
1153                targets: &[Some(clip_color_target.clone())],
1154                compilation_options: wgpu::PipelineCompilationOptions::default(),
1155            }),
1156            primitive: wgpu::PrimitiveState::default(),
1157            depth_stencil: Some(stencil_for_clip_dec.clone()),
1158            multisample: wgpu::MultisampleState {
1159                count: sample_count,
1160                mask: !0,
1161                alpha_to_coverage_enabled: false,
1162            },
1163            multiview_mask: None,
1164            cache: None,
1165        });
1166
1167        let slug = Some(slug::create_pipeline(
1168            device,
1169            format,
1170            sample_count,
1171            stencil_for_content,
1172        ));
1173
1174        // Tessellated vector mesh pipeline (host-supplied vertex/index data).
1175        let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1176            label: Some("mesh.wgsl"),
1177            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/mesh.wgsl"))),
1178        });
1179        let mesh_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1180            label: Some("mesh pipeline layout"),
1181            bind_group_layouts: &[Some(globals_layout), Some(mesh_bind_layout)],
1182            immediate_size: 0,
1183        });
1184        let mesh_vertex_layout = wgpu::VertexBufferLayout {
1185            array_stride: std::mem::size_of::<MeshVertex>() as u64,
1186            step_mode: wgpu::VertexStepMode::Vertex,
1187            attributes: &[
1188                wgpu::VertexAttribute {
1189                    shader_location: 0,
1190                    offset: 0,
1191                    format: wgpu::VertexFormat::Float32x2,
1192                },
1193                wgpu::VertexAttribute {
1194                    shader_location: 1,
1195                    offset: 8,
1196                    format: wgpu::VertexFormat::Float32x4,
1197                },
1198                wgpu::VertexAttribute {
1199                    shader_location: 2,
1200                    offset: 24,
1201                    format: wgpu::VertexFormat::Float32x2,
1202                },
1203            ],
1204        };
1205        let make_mesh_pipeline =
1206            |label: &str, depth: &wgpu::DepthStencilState, color: &wgpu::ColorTargetState| {
1207                device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1208                    label: Some(label),
1209                    layout: Some(&mesh_pipeline_layout),
1210                    vertex: wgpu::VertexState {
1211                        module: &mesh_shader,
1212                        entry_point: Some("vs_main"),
1213                        buffers: &[Some(mesh_vertex_layout.clone())],
1214                        compilation_options: wgpu::PipelineCompilationOptions::default(),
1215                    },
1216                    fragment: Some(wgpu::FragmentState {
1217                        module: &mesh_shader,
1218                        entry_point: Some("fs_main"),
1219                        targets: &[Some(color.clone())],
1220                        compilation_options: wgpu::PipelineCompilationOptions::default(),
1221                    }),
1222                    primitive: wgpu::PrimitiveState {
1223                        topology: wgpu::PrimitiveTopology::TriangleList,
1224                        ..Default::default()
1225                    },
1226                    depth_stencil: Some(depth.clone()),
1227                    multisample: msaa_state,
1228                    multiview_mask: None,
1229                    cache: None,
1230                })
1231            };
1232        let mesh_color_target = wgpu::ColorTargetState {
1233            format,
1234            blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1235            write_mask: wgpu::ColorWrites::ALL,
1236        };
1237        let mut stencil_for_mesh = stencil_for_content.clone();
1238        stencil_for_mesh.stencil.front.compare = wgpu::CompareFunction::Equal;
1239        stencil_for_mesh.stencil.back.compare = wgpu::CompareFunction::Equal;
1240        let mesh = make_mesh_pipeline("mesh pipeline", &stencil_for_mesh, &mesh_color_target);
1241        let mesh_overlay = make_mesh_pipeline(
1242            "mesh overlay pipeline",
1243            stencil_for_content,
1244            &mesh_color_target,
1245        );
1246        let mesh_clip_inc = make_mesh_pipeline(
1247            "mesh clip (inc) pipeline",
1248            stencil_for_clip_inc,
1249            clip_color_target,
1250        );
1251        let mesh_clip_dec = make_mesh_pipeline(
1252            "mesh clip (dec) pipeline",
1253            stencil_for_clip_dec,
1254            clip_color_target,
1255        );
1256        let mesh_blend_target = |blend: wgpu::BlendState| wgpu::ColorTargetState {
1257            format,
1258            blend: Some(blend),
1259            write_mask: wgpu::ColorWrites::ALL,
1260        };
1261        let premult_alpha = wgpu::BlendComponent::OVER;
1262        let mesh_add = make_mesh_pipeline(
1263            "mesh pipeline (add)",
1264            &stencil_for_mesh,
1265            &mesh_blend_target(wgpu::BlendState {
1266                color: wgpu::BlendComponent {
1267                    src_factor: wgpu::BlendFactor::One,
1268                    dst_factor: wgpu::BlendFactor::One,
1269                    operation: wgpu::BlendOperation::Add,
1270                },
1271                alpha: premult_alpha,
1272            }),
1273        );
1274        let mesh_multiply = make_mesh_pipeline(
1275            "mesh pipeline (multiply)",
1276            &stencil_for_mesh,
1277            &mesh_blend_target(wgpu::BlendState {
1278                color: wgpu::BlendComponent {
1279                    src_factor: wgpu::BlendFactor::Dst,
1280                    dst_factor: wgpu::BlendFactor::Zero,
1281                    operation: wgpu::BlendOperation::Add,
1282                },
1283                alpha: premult_alpha,
1284            }),
1285        );
1286        let mesh_screen = make_mesh_pipeline(
1287            "mesh pipeline (screen)",
1288            &stencil_for_mesh,
1289            &mesh_blend_target(wgpu::BlendState {
1290                color: wgpu::BlendComponent {
1291                    src_factor: wgpu::BlendFactor::One,
1292                    dst_factor: wgpu::BlendFactor::OneMinusSrc,
1293                    operation: wgpu::BlendOperation::Add,
1294                },
1295                alpha: premult_alpha,
1296            }),
1297        );
1298        let mesh_darken = make_mesh_pipeline(
1299            "mesh pipeline (darken)",
1300            &stencil_for_mesh,
1301            &mesh_blend_target(wgpu::BlendState {
1302                color: wgpu::BlendComponent {
1303                    src_factor: wgpu::BlendFactor::One,
1304                    dst_factor: wgpu::BlendFactor::One,
1305                    operation: wgpu::BlendOperation::Min,
1306                },
1307                alpha: premult_alpha,
1308            }),
1309        );
1310        let mesh_lighten = make_mesh_pipeline(
1311            "mesh pipeline (lighten)",
1312            &stencil_for_mesh,
1313            &mesh_blend_target(wgpu::BlendState {
1314                color: wgpu::BlendComponent {
1315                    src_factor: wgpu::BlendFactor::One,
1316                    dst_factor: wgpu::BlendFactor::One,
1317                    operation: wgpu::BlendOperation::Max,
1318                },
1319                alpha: premult_alpha,
1320            }),
1321        );
1322        // Projective layer composite (perspective flattening). Same
1323        // bind groups as the text/image path (globals + layer texture), with
1324        // per-instance projected corners. Like `image_rgba` it draws into the
1325        // parent target, so it shares the content stencil state.
1326        let projective_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1327            label: Some("projective_layer.wgsl"),
1328            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1329                "shaders/projective_layer.wgsl"
1330            ))),
1331        });
1332        let projective_pipeline_layout =
1333            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1334                label: Some("projective layer pipeline layout"),
1335                bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
1336                immediate_size: 0,
1337            });
1338        let projective_vertex_layout = wgpu::VertexBufferLayout {
1339            array_stride: std::mem::size_of::<ProjectiveInstance>() as u64,
1340            step_mode: wgpu::VertexStepMode::Instance,
1341            attributes: &[
1342                wgpu::VertexAttribute {
1343                    shader_location: 0,
1344                    offset: 0,
1345                    format: wgpu::VertexFormat::Float32x2,
1346                },
1347                wgpu::VertexAttribute {
1348                    shader_location: 1,
1349                    offset: 8,
1350                    format: wgpu::VertexFormat::Float32x2,
1351                },
1352                wgpu::VertexAttribute {
1353                    shader_location: 2,
1354                    offset: 16,
1355                    format: wgpu::VertexFormat::Float32x2,
1356                },
1357                wgpu::VertexAttribute {
1358                    shader_location: 3,
1359                    offset: 24,
1360                    format: wgpu::VertexFormat::Float32x2,
1361                },
1362                wgpu::VertexAttribute {
1363                    shader_location: 4,
1364                    offset: 32,
1365                    format: wgpu::VertexFormat::Float32x4,
1366                },
1367                wgpu::VertexAttribute {
1368                    shader_location: 5,
1369                    offset: 48,
1370                    format: wgpu::VertexFormat::Float32x4,
1371                },
1372                wgpu::VertexAttribute {
1373                    shader_location: 6,
1374                    offset: 64,
1375                    format: wgpu::VertexFormat::Float32,
1376                },
1377            ],
1378        };
1379        let projective_layer = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1380            label: Some("projective layer composite pipeline"),
1381            layout: Some(&projective_pipeline_layout),
1382            vertex: wgpu::VertexState {
1383                module: &projective_shader,
1384                entry_point: Some("vs_main"),
1385                buffers: &[Some(projective_vertex_layout)],
1386                compilation_options: wgpu::PipelineCompilationOptions::default(),
1387            },
1388            fragment: Some(wgpu::FragmentState {
1389                module: &projective_shader,
1390                entry_point: Some("fs_main"),
1391                targets: &[Some(wgpu::ColorTargetState {
1392                    format,
1393                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1394                    write_mask: wgpu::ColorWrites::ALL,
1395                })],
1396                compilation_options: wgpu::PipelineCompilationOptions::default(),
1397            }),
1398            primitive: wgpu::PrimitiveState::default(),
1399            depth_stencil: Some(stencil_for_content.clone()),
1400            multisample: msaa_state,
1401            multiview_mask: None,
1402            cache: None,
1403        });
1404
1405        let blend_layer_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1406            label: Some("blend_layer.wgsl"),
1407            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1408                "shaders/blend_layer.wgsl"
1409            ))),
1410        });
1411        let blend_layer_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1412            label: Some("blend layer pipeline layout"),
1413            bind_group_layouts: &[
1414                Some(globals_layout),
1415                Some(text_bind_layout),
1416                Some(text_bind_layout),
1417            ],
1418            immediate_size: 0,
1419        });
1420        let blend_vertex_layout = wgpu::VertexBufferLayout {
1421            array_stride: std::mem::size_of::<BlendInstance>() as u64,
1422            step_mode: wgpu::VertexStepMode::Instance,
1423            attributes: &[
1424                wgpu::VertexAttribute {
1425                    shader_location: 0,
1426                    offset: 0,
1427                    format: wgpu::VertexFormat::Float32x4,
1428                },
1429                wgpu::VertexAttribute {
1430                    shader_location: 1,
1431                    offset: 16,
1432                    format: wgpu::VertexFormat::Float32x4,
1433                },
1434                wgpu::VertexAttribute {
1435                    shader_location: 2,
1436                    offset: 32,
1437                    format: wgpu::VertexFormat::Float32x4,
1438                },
1439                wgpu::VertexAttribute {
1440                    shader_location: 3,
1441                    offset: 48,
1442                    format: wgpu::VertexFormat::Float32x4,
1443                },
1444                wgpu::VertexAttribute {
1445                    shader_location: 4,
1446                    offset: 64,
1447                    format: wgpu::VertexFormat::Uint32,
1448                },
1449            ],
1450        };
1451        let blend_layer = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1452            label: Some("blend layer pipeline"),
1453            layout: Some(&blend_layer_layout),
1454            vertex: wgpu::VertexState {
1455                module: &blend_layer_shader,
1456                entry_point: Some("vs_main"),
1457                buffers: &[Some(blend_vertex_layout)],
1458                compilation_options: wgpu::PipelineCompilationOptions::default(),
1459            },
1460            fragment: Some(wgpu::FragmentState {
1461                module: &blend_layer_shader,
1462                entry_point: Some("fs_main"),
1463                targets: &[Some(wgpu::ColorTargetState {
1464                    format,
1465                    blend: None,
1466                    write_mask: wgpu::ColorWrites::ALL,
1467                })],
1468                compilation_options: wgpu::PipelineCompilationOptions::default(),
1469            }),
1470            primitive: wgpu::PrimitiveState::default(),
1471            depth_stencil: Some(stencil_for_content.clone()),
1472            multisample: msaa_state,
1473            multiview_mask: None,
1474            cache: None,
1475        });
1476
1477        Self {
1478            rects,
1479            borders,
1480            ellipses,
1481            ellipse_borders,
1482            arcs,
1483            text_mask,
1484            text_color,
1485            image_rgba,
1486            image_nv12,
1487            coverage,
1488            blur,
1489            blur_content,
1490            clip_bin,
1491            clip_dec,
1492            slug,
1493            mesh,
1494            mesh_add,
1495            mesh_multiply,
1496            mesh_screen,
1497            mesh_darken,
1498            mesh_lighten,
1499            blend_layer,
1500            mesh_overlay,
1501            mesh_clip_inc,
1502            mesh_clip_dec,
1503            projective_layer,
1504        }
1505    }
1506}
1507
1508/// A segment of the frame that draws into a single render target.
1509struct Pass {
1510    target: PassTarget,
1511    /// The initial scissor to apply to the rpass when it is opened.
1512    initial_scissor: (u32, u32, u32, u32),
1513    /// `None` means `LoadOp::Load` (resume existing content);
1514    /// `Some(c)` means `LoadOp::Clear(c)`.
1515    clear_color: Option<[f32; 4]>,
1516    cmds: Vec<Cmd>,
1517}
1518
1519/// One translator-flattened perspective layer (see
1520/// `push_perspective_layer`): the projective map and everything needed to
1521/// restore the parent target on the matching pop.
1522struct FlattenRecord {
1523    /// `transform_stack.len()` before this flatten pushed its two entries
1524    /// (stripped affine + layer-local shift).
1525    stack_len: usize,
1526    layer_id: u32,
1527    /// Full projective map (row-major 3x3): affine ancestors over the
1528    /// perspective node, in parent-target coordinates.
1529    map: [f32; 9],
1530    /// Layer rect in the parent target's coordinates.
1531    layer_rect: repose_core::Rect,
1532    saved_scissor: Vec<repose_core::Rect>,
1533    saved_root: repose_core::Rect,
1534    saved_size: (f32, f32),
1535}
1536
1537/// First translator-owned flatten layer id. Producer ids start at 1 per
1538/// scene, so this range never collides; ids are drained from the layer pool
1539/// after each frame, so reuse across frames is safe.
1540const FLATTEN_ID_BASE: u32 = 0xF000_0000;
1541
1542#[allow(non_snake_case)]
1543enum Cmd {
1544    ClipPush {
1545        off: u64,
1546        cnt: u32,
1547        scissor: (u32, u32, u32, u32),
1548        difference: bool,
1549        rounded: bool,
1550    },
1551    ClipPop {
1552        off: u64,
1553        cnt: u32,
1554        scissor: (u32, u32, u32, u32),
1555        difference: bool,
1556    },
1557    Rect {
1558        off: u64,
1559        cnt: u32,
1560    },
1561    Border {
1562        off: u64,
1563        cnt: u32,
1564    },
1565    Ellipse {
1566        off: u64,
1567        cnt: u32,
1568    },
1569    EllipseBorder {
1570        off: u64,
1571        cnt: u32,
1572    },
1573    Arc {
1574        off: u64,
1575        cnt: u32,
1576    },
1577    GlyphsMask {
1578        off: u64,
1579        cnt: u32,
1580    },
1581    GlyphsColor {
1582        off: u64,
1583        cnt: u32,
1584    },
1585    GlyphsVector {
1586        off: u64,
1587        cnt: u32,
1588    },
1589    ImageRgba {
1590        off: u64,
1591        cnt: u32,
1592        handle: u64,
1593    },
1594    /// Composite a tinted A8 coverage tile (`SceneNode::Coverage`). The
1595    /// instance lives in `self.glyph_color.ring` (a `GlyphInstance`); the
1596    /// bind comes from the coverage registry.
1597    Coverage {
1598        off: u64,
1599        cnt: u32,
1600        handle: u64,
1601    },
1602    ImageNv12 {
1603        off: u64,
1604        cnt: u32,
1605        handle: u64,
1606    },
1607    /// Composite a previously-rendered graphics layer back into the
1608    /// current target as a textured quad. The quad's vertex buffer
1609    /// lives in `self.glyph_color.ring` (a `GlyphInstance`).
1610    CompositeLayer {
1611        off: u64,
1612        cnt: u32,
1613        layer_id: u32,
1614    },
1615    /// Composite a blurred drop shadow of a previously-rendered graphics
1616    /// layer. The quad's vertex buffer lives in `self.blur_ring` (a
1617    /// `BlurInstance`).
1618    CompositeShadow {
1619        off: u64,
1620        cnt: u32,
1621        layer_id: u32,
1622    },
1623    /// Apply gaussian blur to a layer and composite the blurred result.
1624    /// Uses the `blur_content` pipeline (full RGBA blur).
1625    CompositeBlur {
1626        off: u64,
1627        cnt: u32,
1628        layer_id: u32,
1629    },
1630    /// Composite a flattened perspective layer through its projective map.
1631    /// The instance lives in `self.projective_ring` (a `ProjectiveInstance`
1632    /// with CPU-projected NDC corners); sampled from the layer's texture
1633    /// with perspective-correct UVs by the `projective_layer` pipeline.
1634    CompositeProjective {
1635        off: u64,
1636        cnt: u32,
1637        layer_id: u32,
1638    },
1639    /// Draw a tessellated vector mesh (solid or gradient paint).
1640    VectorMesh {
1641        voff: u64,
1642        vcnt: u32,
1643        ioff: u64,
1644        icnt: u32,
1645        uoff: u64,
1646        blend: repose_core::BlendMode,
1647    },
1648    /// Composite an isolated source layer over the current target with a
1649    /// backdrop-dependent CSS blend mode. The shader samples both textures
1650    /// and composites source-over by hand, so the pass runs REPLACE.
1651    /// `parent` records which target the composite draws into (surface or
1652    /// layer); the instance NDC is mapped against it, so any parent origin
1653    /// works.
1654    BlendLayer {
1655        off: u64,
1656        cnt: u32,
1657        src_layer: u32,
1658        dst_layer: Option<u32>,
1659        parent: PassTarget,
1660    },
1661    /// Draw a screen-space overlay mesh (identity transform, device pixels).
1662    VectorOverlay {
1663        voff: u64,
1664        vcnt: u32,
1665        ioff: u64,
1666        icnt: u32,
1667        uoff: u64,
1668    },
1669    /// Increment the stencil buffer with a tessellated vector mask.
1670    /// `difference` marks an inverse (`\iclip`-style) mask: content draws
1671    /// *outside* it. The counting still balances (push increments, pop
1672    /// decrements); only the depth bookkeeping differs (see executor).
1673    VectorClipPush {
1674        voff: u64,
1675        vcnt: u32,
1676        ioff: u64,
1677        icnt: u32,
1678        uoff: u64,
1679        scissor: (u32, u32, u32, u32),
1680        difference: bool,
1681    },
1682    /// Decrement the stencil buffer with the matching vector mask.
1683    VectorClipPop {
1684        voff: u64,
1685        vcnt: u32,
1686        ioff: u64,
1687        icnt: u32,
1688        uoff: u64,
1689        scissor: (u32, u32, u32, u32),
1690        difference: bool,
1691    },
1692    Callback {
1693        rect: repose_core::Rect,
1694        payload: repose_core::PaintCallbackPayload,
1695    },
1696}
1697
1698/// A registered A8 coverage tile: single-channel mask sampled as coverage
1699/// by `SceneNode::Coverage`. Tiles are immutable; producers re-register on
1700/// geometry change and `remove_coverage` stale handles (unused tiles also
1701/// age out via the image eviction policy).
1702struct CoverageTex {
1703    // Held to keep the GPU texture alive (freed on remove/evict).
1704    #[allow(dead_code)]
1705    tex: wgpu::Texture,
1706    bind: wgpu::BindGroup,
1707    w: u32,
1708    h: u32,
1709    last_used_frame: u64,
1710    bytes: u64,
1711}
1712
1713enum ImageTex {
1714    Rgba {
1715        tex: wgpu::Texture,
1716        bind: wgpu::BindGroup,
1717        w: u32,
1718        h: u32,
1719        format: wgpu::TextureFormat,
1720        last_used_frame: u64,
1721        bytes: u64,
1722    },
1723    /// For a user-provided texture view.
1724    User {
1725        bind: wgpu::BindGroup,
1726        w: u32,
1727        h: u32,
1728        last_used_frame: u64,
1729        bytes: u64,
1730    },
1731    Nv12 {
1732        tex_y: wgpu::Texture,
1733        tex_uv: wgpu::Texture,
1734        bind: wgpu::BindGroup,
1735        yuv_buf: wgpu::Buffer,
1736        w: u32,
1737        h: u32,
1738        color_info: ColorInfo,
1739        last_used_frame: u64,
1740        bytes: u64,
1741    },
1742}
1743
1744#[derive(Clone)]
1745struct RetainedImage {
1746    w: u32,
1747    h: u32,
1748    format: wgpu::TextureFormat,
1749    rgba: Vec<u8>,
1750}
1751
1752struct AtlasA8 {
1753    tex: wgpu::Texture,
1754    view: wgpu::TextureView,
1755    sampler: wgpu::Sampler,
1756    size: u32,
1757    next_x: u32,
1758    next_y: u32,
1759    row_h: u32,
1760    map: HashMap<(repose_text::GlyphKey, u32), GlyphInfo>,
1761}
1762
1763struct AtlasRGBA {
1764    tex: wgpu::Texture,
1765    view: wgpu::TextureView,
1766    sampler: wgpu::Sampler,
1767    size: u32,
1768    next_x: u32,
1769    next_y: u32,
1770    row_h: u32,
1771    map: HashMap<(repose_text::GlyphKey, u32), GlyphInfo>,
1772}
1773
1774#[derive(Clone, Copy)]
1775struct GlyphInfo {
1776    u0: f32,
1777    v0: f32,
1778    u1: f32,
1779    v1: f32,
1780    w: f32,
1781    h: f32,
1782}
1783
1784#[repr(C)]
1785#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1786struct RectInstance {
1787    xywh: [f32; 4],
1788    radii: [f32; 4],
1789    brush_type: u32,
1790    grad_kind: u32,
1791    _pad: [f32; 2],
1792    color0: [f32; 4],
1793    color1: [f32; 4],
1794    grad_p0: [f32; 2],
1795    grad_p1: [f32; 2],
1796    tile_mode: u32,
1797    _pad2: [f32; 3],
1798    fwd_mat: [f32; 4],
1799}
1800
1801#[repr(C)]
1802#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1803struct BorderInstance {
1804    xywh: [f32; 4],
1805    radii: [f32; 4],
1806    stroke: f32,
1807    brush_type: u32,
1808    _pad: [f32; 2],
1809    grad_kind: u32,
1810    color0: [f32; 4],
1811    color1: [f32; 4],
1812    grad_p0: [f32; 2],
1813    grad_p1: [f32; 2],
1814    tile_mode: u32,
1815    _pad2: [f32; 3],
1816    fwd_mat: [f32; 4],
1817}
1818
1819#[repr(C)]
1820#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1821struct EllipseInstance {
1822    xywh: [f32; 4],
1823    brush_type: u32,
1824    grad_kind: u32,
1825    _pad: [f32; 2],
1826    color0: [f32; 4],
1827    color1: [f32; 4],
1828    grad_p0: [f32; 2],
1829    grad_p1: [f32; 2],
1830    tile_mode: u32,
1831    _pad2: [f32; 3],
1832    fwd_mat: [f32; 4],
1833}
1834
1835#[repr(C)]
1836#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1837struct EllipseBorderInstance {
1838    xywh: [f32; 4],
1839    stroke: f32,
1840    pad: f32,
1841    brush_type: u32,
1842    grad_kind: u32,
1843    color0: [f32; 4],
1844    color1: [f32; 4],
1845    grad_p0: [f32; 2],
1846    grad_p1: [f32; 2],
1847    tile_mode: u32,
1848    _pad2: [f32; 3],
1849    fwd_mat: [f32; 4],
1850}
1851
1852#[repr(C)]
1853#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1854struct ArcInstance {
1855    xywh: [f32; 4],
1856    start_angle: f32,
1857    sweep_angle: f32,
1858    stroke: f32,
1859    pad: f32,
1860    brush_type: u32,
1861    grad_kind: u32,
1862    _pad0: [f32; 2],
1863    color0: [f32; 4],
1864    color1: [f32; 4],
1865    grad_p0: [f32; 2],
1866    grad_p1: [f32; 2],
1867    tile_mode: u32,
1868    cap: f32, // 0=Butt, 1=Round, 2=Square
1869    _pad1: [f32; 2],
1870    fwd_mat: [f32; 4],
1871}
1872
1873#[repr(C)]
1874#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1875struct GlyphInstance {
1876    xywh: [f32; 4],
1877    uv: [f32; 4],
1878    color: [f32; 4],
1879    fwd_mat: [f32; 4],
1880}
1881
1882#[repr(C)]
1883#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1884struct BlurInstance {
1885    xywh: [f32; 4],
1886    uv: [f32; 4],
1887    color: [f32; 4],
1888    blur_uv: [f32; 2],
1889    fwd_mat: [f32; 4],
1890}
1891
1892/// Projective layer-composite instance: the four layer-rect corners projected
1893/// to NDC (`c0..c3`, counter-clockwise from top-left) with their homogeneous
1894/// `w`, the layer-texture uv bounds, and a group alpha. Matches
1895/// `projective_layer.wgsl` (offsets: c0@0 c1@8 c2@16 c3@24 uv@32 w@48
1896/// alpha@64; stride 80).
1897#[repr(C)]
1898#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1899struct ProjectiveInstance {
1900    c0: [f32; 2],
1901    c1: [f32; 2],
1902    c2: [f32; 2],
1903    c3: [f32; 2],
1904    uv: [f32; 4],
1905    w: [f32; 4],
1906    alpha: f32,
1907    _pad: [f32; 3],
1908}
1909
1910/// CPU-computed Y′CbCr -> R′G′B′ transform uploaded as a uniform buffer.
1911/// Layout matches the WGSL `YuvTransform` struct (4 × vec4<f32>).
1912#[repr(C)]
1913#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1914struct YuvTransformRaw {
1915    row0: [f32; 4],
1916    row1: [f32; 4],
1917    row2: [f32; 4],
1918    b: [f32; 4],
1919}
1920
1921#[repr(C)]
1922#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1923struct Nv12Instance {
1924    xywh: [f32; 4],
1925    uv: [f32; 4],
1926    color: [f32; 4], // tint
1927    uv_x_offset: f32,
1928    fwd_mat: [f32; 4],
1929    _pad: [f32; 1],
1930}
1931
1932#[repr(C)]
1933#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1934struct ClipInstance {
1935    xywh: [f32; 4],
1936    radii: [f32; 4],
1937    fwd_mat: [f32; 4],
1938}
1939
1940/// Backdrop-blend composite instance: a `GlyphInstance`-shaped quad plus
1941/// the `BlendMode` discriminant consumed by `blend_layer.wgsl`.
1942#[repr(C)]
1943#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1944struct BlendInstance {
1945    xywh: [f32; 4],
1946    uv: [f32; 4],
1947    color: [f32; 4],
1948    fwd_mat: [f32; 4],
1949    mode: u32,
1950    _pad: [f32; 3],
1951}
1952
1953#[repr(C)]
1954#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1955struct MeshVertex {
1956    pos: [f32; 2],
1957    color: [f32; 4],
1958    uv: [f32; 2],
1959}
1960
1961#[repr(C)]
1962#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1963struct MeshUniform {
1964    m0: [f32; 4],
1965    m1: [f32; 4],
1966    paint: [u32; 4],
1967    color0: [f32; 4],
1968    color1: [f32; 4],
1969    grad_start: [f32; 2],
1970    _p3: [f32; 2],
1971    grad_end: [f32; 2],
1972    _p4: [f32; 2],
1973}
1974
1975/// Dynamic uniform slots are aligned to 256 bytes by wgpu.
1976const MESH_UNIFORM_SLOT: u64 = 256;
1977const MESH_UNIFORM_CAP: u64 = 4 * 1024 * 1024;
1978
1979impl MeshUniform {
1980    fn identity() -> Self {
1981        Self {
1982            m0: [1.0, 0.0, 0.0, 0.0],
1983            m1: [0.0, 1.0, 0.0, 0.0],
1984            paint: [0; 4],
1985            color0: [0.0; 4],
1986            color1: [0.0; 4],
1987            grad_start: [0.0; 2],
1988            _p3: [0.0; 2],
1989            grad_end: [0.0; 2],
1990            _p4: [0.0; 2],
1991        }
1992    }
1993}
1994
1995fn mesh_uniform_from_paint(affine: [f32; 6], paint: &repose_core::PaintDesc) -> MeshUniform {
1996    let (paint_type, paint_kind, color0, color1, grad_start, grad_end) = match paint {
1997        repose_core::PaintDesc::Solid => (0u32, 0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
1998        repose_core::PaintDesc::Linear {
1999            start,
2000            end,
2001            start_color,
2002            end_color,
2003        } => (
2004            1u32,
2005            0u32,
2006            start_color.to_linear(),
2007            end_color.to_linear(),
2008            [start.x, start.y],
2009            [end.x, end.y],
2010        ),
2011        repose_core::PaintDesc::Radial {
2012            center,
2013            radius,
2014            start_color,
2015            end_color,
2016        } => (
2017            1u32,
2018            1u32,
2019            start_color.to_linear(),
2020            end_color.to_linear(),
2021            [center.x, center.y],
2022            [radius.max(0.0), 0.0],
2023        ),
2024        repose_core::PaintDesc::Sweep {
2025            center,
2026            start_color,
2027            end_color,
2028        } => (
2029            1u32,
2030            2u32,
2031            start_color.to_linear(),
2032            end_color.to_linear(),
2033            [center.x, center.y],
2034            [0.0, 0.0],
2035        ),
2036        _ => (0u32, 0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
2037    };
2038    MeshUniform {
2039        m0: [affine[0], affine[1], affine[2], 0.0],
2040        m1: [affine[3], affine[4], affine[5], 0.0],
2041        paint: [paint_type, paint_kind, 0, 0],
2042        color0,
2043        color1,
2044        grad_start,
2045        _p3: [0.0; 2],
2046        grad_end,
2047        _p4: [0.0; 2],
2048    }
2049}
2050
2051fn combine_mesh_affine(current: &Transform, mesh: [f32; 6]) -> [f32; 6] {
2052    let cm = current.linear();
2053    let (cm00, cm01, cm10, cm11) = (cm[0], cm[1], cm[2], cm[3]);
2054    let mm00 = mesh[0];
2055    let mm01 = mesh[1];
2056    let mm10 = mesh[2];
2057    let mm11 = mesh[3];
2058    let mtx = mesh[4];
2059    let mty = mesh[5];
2060    let r00 = cm00 * mm00 + cm01 * mm10;
2061    let r01 = cm00 * mm01 + cm01 * mm11;
2062    let r10 = cm10 * mm00 + cm11 * mm10;
2063    let r11 = cm10 * mm01 + cm11 * mm11;
2064    let tx = cm00 * mtx + cm01 * mty + current.translate_x;
2065    let ty = cm10 * mtx + cm11 * mty + current.translate_y;
2066    // Canonical slot order consumed by `MeshUniform`/shader and `mesh_aabb`:
2067    // [A, B, tx, C, D, ty] where world = [[A,B],[C,D]] * local + (tx, ty).
2068    [r00, r01, tx, r10, r11, ty]
2069}
2070
2071fn mesh_aabb(mesh: &repose_core::VectorMeshData, affine: [f32; 6]) -> repose_core::Rect {
2072    let mut min_x = f32::MAX;
2073    let mut min_y = f32::MAX;
2074    let mut max_x = f32::MIN;
2075    let mut max_y = f32::MIN;
2076    for v in mesh.vertices.iter() {
2077        let x = affine[0] * v.pos[0] + affine[1] * v.pos[1] + affine[2];
2078        let y = affine[3] * v.pos[0] + affine[4] * v.pos[1] + affine[5];
2079        min_x = min_x.min(x);
2080        min_y = min_y.min(y);
2081        max_x = max_x.max(x);
2082        max_y = max_y.max(y);
2083    }
2084    let w = (max_x - min_x).max(0.0);
2085    let h = (max_y - min_y).max(0.0);
2086    if !min_x.is_finite() || !min_y.is_finite() {
2087        return repose_core::Rect {
2088            x: 0.0,
2089            y: 0.0,
2090            w: 0.0,
2091            h: 0.0,
2092        };
2093    }
2094    repose_core::Rect {
2095        x: min_x,
2096        y: min_y,
2097        w,
2098        h,
2099    }
2100}
2101
2102fn swash_to_a8_coverage(content: repose_text::SwashContent, data: &[u8]) -> Option<Vec<u8>> {
2103    match content {
2104        repose_text::SwashContent::Mask => Some(data.to_vec()),
2105        repose_text::SwashContent::SubpixelMask => {
2106            let mut out = Vec::with_capacity(data.len() / 4);
2107            for px in data.as_chunks::<4>().0 {
2108                let r = px[0];
2109                let g = px[1];
2110                let b = px[2];
2111                out.push(r.max(g).max(b));
2112            }
2113            Some(out)
2114        }
2115        repose_text::SwashContent::Color => None,
2116    }
2117}
2118
2119impl WgpuSceneRenderer {
2120    pub fn from_device(
2121        device: wgpu::Device,
2122        queue: wgpu::Queue,
2123        output_format: wgpu::TextureFormat,
2124        msaa_samples: u32,
2125    ) -> Self {
2126        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2127            label: Some("globals layout"),
2128            entries: &[wgpu::BindGroupLayoutEntry {
2129                binding: 0,
2130                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
2131                ty: wgpu::BindingType::Buffer {
2132                    ty: wgpu::BufferBindingType::Uniform,
2133                    has_dynamic_offset: false,
2134                    min_binding_size: None,
2135                },
2136                count: None,
2137            }],
2138        });
2139
2140        let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
2141            label: Some("globals buf"),
2142            size: std::mem::size_of::<Globals>() as u64,
2143            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2144            mapped_at_creation: false,
2145        });
2146
2147        let globals_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
2148            label: Some("globals bind"),
2149            layout: &globals_layout,
2150            entries: &[wgpu::BindGroupEntry {
2151                binding: 0,
2152                resource: globals_buf.as_entire_binding(),
2153            }],
2154        });
2155
2156        let ds_format = wgpu::TextureFormat::Depth24PlusStencil8;
2157
2158        let stencil_for_content = wgpu::DepthStencilState {
2159            format: ds_format,
2160            depth_write_enabled: Some(false),
2161            depth_compare: Some(wgpu::CompareFunction::Always),
2162            stencil: wgpu::StencilState {
2163                front: wgpu::StencilFaceState {
2164                    // Equal (not LessEqual): inverse (`Difference`) vector
2165                    // masks work by keeping the depth while incrementing the
2166                    // masked pixels, so content must test exact equality.
2167                    // Outcomes match LessEqual everywhere except pre-existing
2168                    // stencil leaks, which now fail visibly instead of
2169                    // drawing through (unbalanced clips already warn).
2170                    compare: wgpu::CompareFunction::Equal,
2171                    fail_op: wgpu::StencilOperation::Keep,
2172                    depth_fail_op: wgpu::StencilOperation::Keep,
2173                    pass_op: wgpu::StencilOperation::Keep,
2174                },
2175                back: wgpu::StencilFaceState {
2176                    compare: wgpu::CompareFunction::Equal,
2177                    fail_op: wgpu::StencilOperation::Keep,
2178                    depth_fail_op: wgpu::StencilOperation::Keep,
2179                    pass_op: wgpu::StencilOperation::Keep,
2180                },
2181                read_mask: 0xFF,
2182                write_mask: 0x00,
2183            },
2184            bias: wgpu::DepthBiasState::default(),
2185        };
2186
2187        let stencil_for_clip_inc = wgpu::DepthStencilState {
2188            format: ds_format,
2189            depth_write_enabled: Some(false),
2190            depth_compare: Some(wgpu::CompareFunction::Always),
2191            stencil: wgpu::StencilState {
2192                front: wgpu::StencilFaceState {
2193                    compare: wgpu::CompareFunction::Equal,
2194                    fail_op: wgpu::StencilOperation::Keep,
2195                    depth_fail_op: wgpu::StencilOperation::Keep,
2196                    pass_op: wgpu::StencilOperation::IncrementClamp,
2197                },
2198                back: wgpu::StencilFaceState {
2199                    compare: wgpu::CompareFunction::Equal,
2200                    fail_op: wgpu::StencilOperation::Keep,
2201                    depth_fail_op: wgpu::StencilOperation::Keep,
2202                    pass_op: wgpu::StencilOperation::IncrementClamp,
2203                },
2204                read_mask: 0xFF,
2205                write_mask: 0xFF,
2206            },
2207            bias: wgpu::DepthBiasState::default(),
2208        };
2209
2210        let stencil_for_clip_dec = wgpu::DepthStencilState {
2211            format: ds_format,
2212            depth_write_enabled: Some(false),
2213            depth_compare: Some(wgpu::CompareFunction::Always),
2214            stencil: wgpu::StencilState {
2215                front: wgpu::StencilFaceState {
2216                    compare: wgpu::CompareFunction::Equal,
2217                    fail_op: wgpu::StencilOperation::Keep,
2218                    depth_fail_op: wgpu::StencilOperation::Keep,
2219                    pass_op: wgpu::StencilOperation::DecrementClamp,
2220                },
2221                back: wgpu::StencilFaceState {
2222                    compare: wgpu::CompareFunction::Equal,
2223                    fail_op: wgpu::StencilOperation::Keep,
2224                    depth_fail_op: wgpu::StencilOperation::Keep,
2225                    pass_op: wgpu::StencilOperation::DecrementClamp,
2226                },
2227                read_mask: 0xFF,
2228                write_mask: 0xFF,
2229            },
2230            bias: wgpu::DepthBiasState::default(),
2231        };
2232
2233        let _multisample_state = wgpu::MultisampleState {
2234            count: msaa_samples,
2235            mask: !0,
2236            alpha_to_coverage_enabled: false,
2237        };
2238
2239        // PIPELINES
2240
2241        // Single shared sampler for images/text
2242        let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2243            label: Some("image/text sampler"),
2244            address_mode_u: wgpu::AddressMode::ClampToEdge,
2245            address_mode_v: wgpu::AddressMode::ClampToEdge,
2246            mag_filter: wgpu::FilterMode::Linear,
2247            min_filter: wgpu::FilterMode::Linear,
2248            mipmap_filter: wgpu::MipmapFilterMode::Linear,
2249            ..Default::default()
2250        });
2251
2252        // linear filtering only blurs them; nearest keeps the blit crisp.
2253        let layer_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2254            label: Some("layer nearest sampler"),
2255            address_mode_u: wgpu::AddressMode::ClampToEdge,
2256            address_mode_v: wgpu::AddressMode::ClampToEdge,
2257            mag_filter: wgpu::FilterMode::Nearest,
2258            min_filter: wgpu::FilterMode::Nearest,
2259            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
2260            ..Default::default()
2261        });
2262
2263        // Linear taps for Gaussian blur/shadow passes; nearest is kept for
2264        // the sharp 1:1 layer composite.
2265        let layer_sampler_linear = device.create_sampler(&wgpu::SamplerDescriptor {
2266            label: Some("layer linear sampler"),
2267            address_mode_u: wgpu::AddressMode::ClampToEdge,
2268            address_mode_v: wgpu::AddressMode::ClampToEdge,
2269            mag_filter: wgpu::FilterMode::Linear,
2270            min_filter: wgpu::FilterMode::Linear,
2271            mipmap_filter: wgpu::MipmapFilterMode::Linear,
2272            ..Default::default()
2273        });
2274
2275        // Layout for Text / RGBA Images (Texture + Sampler)
2276        let text_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2277            label: Some("text/rgba bind layout"),
2278            entries: &[
2279                wgpu::BindGroupLayoutEntry {
2280                    binding: 0,
2281                    visibility: wgpu::ShaderStages::FRAGMENT,
2282                    ty: wgpu::BindingType::Texture {
2283                        multisampled: false,
2284                        view_dimension: wgpu::TextureViewDimension::D2,
2285                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
2286                    },
2287                    count: None,
2288                },
2289                wgpu::BindGroupLayoutEntry {
2290                    binding: 1,
2291                    visibility: wgpu::ShaderStages::FRAGMENT,
2292                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2293                    count: None,
2294                },
2295            ],
2296        });
2297        // We reuse this for RGBA images for simplicity, or create a distinct one
2298        let image_bind_layout_rgba = text_bind_layout.clone();
2299
2300        // Layout for NV12 Images (TextureY + TextureUV + Sampler + YuvTransform uniform)
2301        let image_bind_layout_nv12 =
2302            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2303                label: Some("image bind layout nv12"),
2304                entries: &[
2305                    // Y plane
2306                    wgpu::BindGroupLayoutEntry {
2307                        binding: 0,
2308                        visibility: wgpu::ShaderStages::FRAGMENT,
2309                        ty: wgpu::BindingType::Texture {
2310                            multisampled: false,
2311                            view_dimension: wgpu::TextureViewDimension::D2,
2312                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
2313                        },
2314                        count: None,
2315                    },
2316                    // UV plane
2317                    wgpu::BindGroupLayoutEntry {
2318                        binding: 1,
2319                        visibility: wgpu::ShaderStages::FRAGMENT,
2320                        ty: wgpu::BindingType::Texture {
2321                            multisampled: false,
2322                            view_dimension: wgpu::TextureViewDimension::D2,
2323                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
2324                        },
2325                        count: None,
2326                    },
2327                    // Sampler
2328                    wgpu::BindGroupLayoutEntry {
2329                        binding: 2,
2330                        visibility: wgpu::ShaderStages::FRAGMENT,
2331                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2332                        count: None,
2333                    },
2334                    // YUV transform uniform buffer
2335                    wgpu::BindGroupLayoutEntry {
2336                        binding: 3,
2337                        visibility: wgpu::ShaderStages::FRAGMENT,
2338                        ty: wgpu::BindingType::Buffer {
2339                            ty: wgpu::BufferBindingType::Uniform,
2340                            has_dynamic_offset: false,
2341                            min_binding_size: None,
2342                        },
2343                        count: None,
2344                    },
2345                ],
2346            });
2347
2348        // Clipping layout
2349        let clip_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2350            label: Some("clip pipeline layout"),
2351            bind_group_layouts: &[Some(&globals_layout)],
2352            immediate_size: 0,
2353        });
2354        let clip_vertex_layout = wgpu::VertexBufferLayout {
2355            array_stride: std::mem::size_of::<ClipInstance>() as u64,
2356            step_mode: wgpu::VertexStepMode::Instance,
2357            attributes: &[
2358                wgpu::VertexAttribute {
2359                    shader_location: 0,
2360                    offset: 0,
2361                    format: wgpu::VertexFormat::Float32x4,
2362                },
2363                wgpu::VertexAttribute {
2364                    shader_location: 1,
2365                    offset: 16,
2366                    format: wgpu::VertexFormat::Float32x4,
2367                },
2368                wgpu::VertexAttribute {
2369                    shader_location: 2,
2370                    offset: 32,
2371                    format: wgpu::VertexFormat::Float32x4,
2372                },
2373            ],
2374        };
2375        let clip_color_target = wgpu::ColorTargetState {
2376            format: output_format,
2377            blend: None,
2378            write_mask: wgpu::ColorWrites::empty(),
2379        };
2380
2381        // Bind layout for per-draw vector mesh uniforms (dynamic offset).
2382        let mesh_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2383            label: Some("mesh uniform layout"),
2384            entries: &[wgpu::BindGroupLayoutEntry {
2385                binding: 0,
2386                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
2387                ty: wgpu::BindingType::Buffer {
2388                    ty: wgpu::BufferBindingType::Uniform,
2389                    has_dynamic_offset: true,
2390                    min_binding_size: NonZero::new(MESH_UNIFORM_SLOT),
2391                },
2392                count: None,
2393            }],
2394        });
2395        let mesh_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
2396            label: Some("mesh uniform buffer"),
2397            size: MESH_UNIFORM_CAP,
2398            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2399            mapped_at_creation: false,
2400        });
2401        let mesh_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
2402            label: Some("mesh uniform bind"),
2403            layout: &mesh_bind_layout,
2404            entries: &[wgpu::BindGroupEntry {
2405                binding: 0,
2406                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2407                    buffer: &mesh_uniform_buf,
2408                    offset: 0,
2409                    size: NonZero::new(MESH_UNIFORM_SLOT),
2410                }),
2411            }],
2412        });
2413
2414        // Two sets of pipelines: one for the MSAA surface pass, one for layer
2415        // render-to-texture passes (sample_count = 1).
2416        let surface_pipes = Pipelines::create(
2417            &device,
2418            output_format,
2419            msaa_samples,
2420            &globals_layout,
2421            &text_bind_layout,
2422            &image_bind_layout_nv12,
2423            &clip_pipeline_layout,
2424            &stencil_for_content,
2425            &stencil_for_clip_inc,
2426            &stencil_for_clip_dec,
2427            &clip_color_target,
2428            &clip_vertex_layout,
2429            &mesh_bind_layout,
2430        );
2431        let layer_pipes = Pipelines::create(
2432            &device,
2433            output_format,
2434            1,
2435            &globals_layout,
2436            &text_bind_layout,
2437            &image_bind_layout_nv12,
2438            &clip_pipeline_layout,
2439            &stencil_for_content,
2440            &stencil_for_clip_inc,
2441            &stencil_for_clip_dec,
2442            &clip_color_target,
2443            &clip_vertex_layout,
2444            &mesh_bind_layout,
2445        );
2446
2447        // Vector glyph rendering always available with tessellation+MSAA approach.
2448        let slug_enabled = true;
2449
2450        // Blur composite ring (for graphics-layer drop shadows)
2451        let blur_ring = UploadRing::new(
2452            &device,
2453            "blur ring",
2454            1024 * 1024,
2455            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2456        );
2457
2458        // Atlases
2459        let atlas_mask = init_atlas_mask(&device);
2460        let atlas_color = init_atlas_color(&device);
2461
2462        // Upload rings
2463        let ring_rect = UploadRing::new(
2464            &device,
2465            "ring rect",
2466            1 << 20,
2467            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2468        );
2469        let ring_border = UploadRing::new(
2470            &device,
2471            "ring border",
2472            1 << 20,
2473            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2474        );
2475        let ring_ellipse = UploadRing::new(
2476            &device,
2477            "ring ellipse",
2478            1 << 20,
2479            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2480        );
2481        let ring_ellipse_border = UploadRing::new(
2482            &device,
2483            "ring ellipse border",
2484            1 << 20,
2485            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2486        );
2487        let ring_arc = UploadRing::new(
2488            &device,
2489            "ring arc",
2490            1 << 20,
2491            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2492        );
2493        let ring_glyph_mask = UploadRing::new(
2494            &device,
2495            "ring glyph mask",
2496            1 << 20,
2497            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2498        );
2499        let ring_glyph_color = UploadRing::new(
2500            &device,
2501            "ring glyph color",
2502            1 << 20,
2503            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2504        );
2505        let ring_slug = UploadRing::new(
2506            &device,
2507            "ring slug",
2508            1 << 22,
2509            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2510        );
2511        let ring_clip = UploadRing::new(
2512            &device,
2513            "ring clip",
2514            1 << 16,
2515            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2516        );
2517        let blend_ring = UploadRing::new(
2518            &device,
2519            "ring blend",
2520            1 << 16,
2521            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2522        );
2523        let ring_projective = UploadRing::new(
2524            &device,
2525            "ring projective",
2526            1 << 16,
2527            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2528        );
2529        let ring_nv12 = UploadRing::new(
2530            &device,
2531            "ring nv12",
2532            1 << 20,
2533            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2534        );
2535        let ring_mesh_verts = UploadRing::new(
2536            &device,
2537            "ring mesh verts",
2538            1 << 22,
2539            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2540        );
2541        let ring_mesh_indices = UploadRing::new(
2542            &device,
2543            "ring mesh indices",
2544            1 << 22,
2545            wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
2546        );
2547
2548        // Placeholder textures
2549        let depth_stencil_tex = device.create_texture(&wgpu::TextureDescriptor {
2550            label: Some("temp ds"),
2551            size: wgpu::Extent3d {
2552                width: 1,
2553                height: 1,
2554                depth_or_array_layers: 1,
2555            },
2556            mip_level_count: 1,
2557            sample_count: 1,
2558            dimension: wgpu::TextureDimension::D2,
2559            format: wgpu::TextureFormat::Depth24PlusStencil8,
2560            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2561            view_formats: &[],
2562        });
2563        let depth_stencil_view =
2564            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
2565
2566        let mut renderer = WgpuSceneRenderer {
2567            device,
2568            queue,
2569            output_format,
2570            output_width: 0,
2571            output_height: 0,
2572            pixels_per_point: 1.0,
2573
2574            surface_pipes,
2575            layer_pipes,
2576
2577            rects: InstancedPipe::new(ring_rect),
2578            borders: InstancedPipe::new(ring_border),
2579            ellipses: InstancedPipe::new(ring_ellipse),
2580            ellipse_borders: InstancedPipe::new(ring_ellipse_border),
2581            arcs: InstancedPipe::new(ring_arc),
2582            glyph_mask: InstancedPipe::new(ring_glyph_mask),
2583            glyph_color: InstancedPipe::new(ring_glyph_color),
2584
2585            text_bind_layout,
2586
2587            image_bind_layout_rgba,
2588            image_bind_layout_nv12,
2589            image_sampler,
2590            layer_sampler,
2591            layer_sampler_linear,
2592
2593            blur_ring,
2594
2595            slug_enabled,
2596            slug_ring: ring_slug,
2597            slug_cache: slug::GlyphSlugCache::new(),
2598
2599            clip_ring: ring_clip,
2600
2601            nv12: InstancedPipe::new(ring_nv12),
2602
2603            mesh_verts: ring_mesh_verts,
2604            mesh_indices: ring_mesh_indices,
2605            mesh_uniform_buf,
2606            mesh_bind_layout,
2607            mesh_bind,
2608            mesh_uniform_head: 0,
2609            mesh_clip_stack: Vec::new(),
2610
2611            projective_ring: ring_projective,
2612            blend_ring,
2613            flatten_layer_ids: Vec::new(),
2614            blend_snapshots: std::collections::HashMap::new(),
2615            blend_copies: Vec::new(),
2616
2617            msaa_samples,
2618            depth_stencil_tex,
2619            depth_stencil_view,
2620            msaa_tex: None,
2621            msaa_view: None,
2622            globals_bind,
2623            globals_buf,
2624
2625            atlas_mask,
2626            atlas_color,
2627
2628            next_image_handle: 1,
2629            images: HashMap::new(),
2630            retained: HashMap::new(),
2631
2632            next_coverage_handle: 1,
2633            coverages: HashMap::new(),
2634
2635            frame_index: 0,
2636            image_bytes_total: 0,
2637            image_evict_after_frames: 600,         // ~10s @ 60fps
2638            image_budget_bytes: 512 * 1024 * 1024, // 512 MB
2639            layer_pool: HashMap::new(),
2640
2641            working_space: false,
2642            ws_tex: None,
2643            ws_view: None,
2644            ws_bind: None,
2645            display_pipeline: None,
2646            display_layout: None,
2647
2648            callback_resources: CallbackResources::default(),
2649        };
2650
2651        renderer.recreate_msaa_and_depth_stencil();
2652        renderer
2653    }
2654}
2655
2656impl WgpuSurfaceBackend {
2657    #[cfg(feature = "winit-surface")]
2658    pub async fn new_async(
2659        window: Arc<winit::window::Window>,
2660    ) -> anyhow::Result<WgpuSurfaceBackend> {
2661        Self::new_async_with_options(window, 4, PresentModePref::Auto).await
2662    }
2663
2664    /// Create a windowed surface backend, honoring the requested MSAA sample
2665    /// count (falling back to the largest supported count <= `msaa_samples`).
2666    #[cfg(feature = "winit-surface")]
2667    pub async fn new_async_with_msaa(
2668        window: Arc<winit::window::Window>,
2669        msaa_samples: u32,
2670    ) -> anyhow::Result<WgpuSurfaceBackend> {
2671        Self::new_async_with_options(window, msaa_samples, PresentModePref::Auto).await
2672    }
2673
2674    /// Create a windowed surface backend, honoring the requested MSAA sample
2675    /// count and present-mode preference.
2676    #[cfg(feature = "winit-surface")]
2677    pub async fn new_async_with_options(
2678        window: Arc<winit::window::Window>,
2679        msaa_samples: u32,
2680        present_mode: PresentModePref,
2681    ) -> anyhow::Result<WgpuSurfaceBackend> {
2682        let instance: Instance = if cfg!(target_arch = "wasm32") {
2683            let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
2684            desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
2685            wgpu::util::new_instance_with_webgpu_detection(desc).await
2686        } else {
2687            wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle())
2688        };
2689
2690        let surface = instance.create_surface(window.clone())?;
2691
2692        let adapter = instance
2693            .request_adapter(&wgpu::RequestAdapterOptions {
2694                power_preference: wgpu::PowerPreference::HighPerformance,
2695                compatible_surface: Some(&surface),
2696                force_fallback_adapter: false,
2697                apply_limit_buckets: false,
2698            })
2699            .await
2700            .map_err(|e| anyhow::anyhow!("No suitable adapter: {e:?}"))?;
2701
2702        let limits = adapter.limits();
2703
2704        #[cfg(target_os = "linux")]
2705        let features = {
2706            let af = adapter.features();
2707            let mut f = wgpu::Features::empty();
2708            if af.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_FD) {
2709                f |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_FD;
2710            }
2711            if af.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF) {
2712                f |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF;
2713            }
2714            f
2715        };
2716        #[cfg(not(target_os = "linux"))]
2717        let features = wgpu::Features::empty();
2718
2719        let (device, queue) = adapter
2720            .request_device(&wgpu::DeviceDescriptor {
2721                label: Some("repose-rs device"),
2722                required_features: features,
2723                required_limits: limits,
2724                experimental_features: wgpu::ExperimentalFeatures::disabled(),
2725                memory_hints: wgpu::MemoryHints::default(),
2726                trace: wgpu::Trace::Off,
2727            })
2728            .await
2729            .map_err(|e| anyhow::anyhow!("request_device failed: {e:?}"))?;
2730
2731        let size = window.inner_size();
2732
2733        let caps = surface.get_capabilities(&adapter);
2734
2735        let (format, view_format) = if cfg!(target_arch = "wasm32")
2736            && adapter
2737                .get_downlevel_capabilities()
2738                .flags
2739                .contains(wgpu::DownlevelFlags::SURFACE_VIEW_FORMATS)
2740        {
2741            let non_srgb = caps
2742                .formats
2743                .iter()
2744                .copied()
2745                .find(|f| !f.is_srgb())
2746                .unwrap_or(caps.formats[0]);
2747            (non_srgb, Some(non_srgb.add_srgb_suffix()))
2748        } else if cfg!(target_arch = "wasm32") {
2749            let fmt = caps
2750                .formats
2751                .iter()
2752                .copied()
2753                .find(|f| f.is_srgb())
2754                .unwrap_or(caps.formats[0]);
2755            (fmt, None)
2756        } else {
2757            let fmt = caps
2758                .formats
2759                .iter()
2760                .copied()
2761                .find(|f| f.is_srgb())
2762                .unwrap_or(caps.formats[0]);
2763            (fmt, None)
2764        };
2765
2766        let present_mode = pick_present_mode(&caps, present_mode);
2767        let alpha_mode = caps.alpha_modes[0];
2768
2769        let render_format = view_format.unwrap_or(format);
2770        let msaa_samples = pick_surface_msaa(&adapter, format, msaa_samples);
2771        let renderer = WgpuSceneRenderer::from_device(device, queue, render_format, msaa_samples);
2772
2773        let view_formats = view_format.into_iter().collect::<Vec<_>>();
2774
2775        let config = wgpu::SurfaceConfiguration {
2776            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2777            format,
2778            width: size.width.max(1),
2779            height: size.height.max(1),
2780            present_mode,
2781            alpha_mode,
2782            color_space: wgpu::SurfaceColorSpace::Auto,
2783            view_formats,
2784            desired_maximum_frame_latency: 1,
2785        };
2786        surface.configure(&renderer.device, &config);
2787
2788        Ok(WgpuSurfaceBackend {
2789            surface: Some(surface),
2790            surface_config: Some(config),
2791            renderer,
2792        })
2793    }
2794
2795    #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
2796    pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<WgpuSurfaceBackend> {
2797        pollster::block_on(Self::new_async(window))
2798    }
2799
2800    #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
2801    pub fn new_with_msaa(
2802        window: Arc<winit::window::Window>,
2803        msaa_samples: u32,
2804    ) -> anyhow::Result<WgpuSurfaceBackend> {
2805        pollster::block_on(Self::new_async_with_msaa(window, msaa_samples))
2806    }
2807
2808    #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
2809    pub fn new_with_options(
2810        window: Arc<winit::window::Window>,
2811        msaa_samples: u32,
2812        present_mode: PresentModePref,
2813    ) -> anyhow::Result<WgpuSurfaceBackend> {
2814        pollster::block_on(Self::new_async_with_options(
2815            window,
2816            msaa_samples,
2817            present_mode,
2818        ))
2819    }
2820
2821    #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
2822    pub fn new(_window: Arc<winit::window::Window>) -> anyhow::Result<WgpuSurfaceBackend> {
2823        anyhow::bail!("Use WgpuSurfaceBackend::new_async(window).await on wasm32")
2824    }
2825
2826    #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
2827    pub fn new_with_msaa(
2828        _window: Arc<winit::window::Window>,
2829        _msaa_samples: u32,
2830    ) -> anyhow::Result<WgpuSurfaceBackend> {
2831        anyhow::bail!("Use WgpuSurfaceBackend::new_async_with_msaa(window, msaa).await on wasm32")
2832    }
2833
2834    #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
2835    pub fn new_with_options(
2836        _window: Arc<winit::window::Window>,
2837        _msaa_samples: u32,
2838        _present_mode: PresentModePref,
2839    ) -> anyhow::Result<WgpuSurfaceBackend> {
2840        anyhow::bail!(
2841            "Use WgpuSurfaceBackend::new_async_with_options(window, msaa, mode).await on wasm32"
2842        )
2843    }
2844}
2845
2846/// Pick the swapchain present mode honoring `pref`, falling back to an "auto"
2847/// Fifo-first selection when the preferred mode is unavailable.
2848fn pick_present_mode(caps: &wgpu::SurfaceCapabilities, pref: PresentModePref) -> wgpu::PresentMode {
2849    let auto = || {
2850        caps.present_modes
2851            .iter()
2852            .copied()
2853            .find(|m| *m == wgpu::PresentMode::Fifo)
2854            .or_else(|| {
2855                caps.present_modes
2856                    .iter()
2857                    .copied()
2858                    .find(|m| *m == wgpu::PresentMode::Mailbox)
2859            })
2860            .unwrap_or(wgpu::PresentMode::Immediate)
2861    };
2862    match pref {
2863        PresentModePref::Auto => auto(),
2864        PresentModePref::Fifo if caps.present_modes.contains(&wgpu::PresentMode::Fifo) => {
2865            wgpu::PresentMode::Fifo
2866        }
2867        PresentModePref::Mailbox if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) => {
2868            wgpu::PresentMode::Mailbox
2869        }
2870        PresentModePref::Immediate
2871            if caps.present_modes.contains(&wgpu::PresentMode::Immediate) =>
2872        {
2873            wgpu::PresentMode::Immediate
2874        }
2875        _ => auto(),
2876    }
2877}
2878
2879/// Pick the MSAA sample count for the surface pass, honoring `requested` and
2880/// falling back to the largest supported count <= it.
2881pub fn pick_surface_msaa(
2882    adapter: &wgpu::Adapter,
2883    format: wgpu::TextureFormat,
2884    requested: u32,
2885) -> u32 {
2886    let requested = requested.max(1);
2887    let color_feat = adapter.get_texture_format_features(format);
2888    let depth_feat = adapter.get_texture_format_features(wgpu::TextureFormat::Depth24PlusStencil8);
2889    let supported = |n: u32| {
2890        color_feat.flags.sample_count_supported(n)
2891            && color_feat
2892                .flags
2893                .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE)
2894            && depth_feat.flags.sample_count_supported(n)
2895    };
2896    let mut candidates = vec![requested];
2897    for n in [8, 4, 2, 1] {
2898        if n < requested {
2899            candidates.push(n);
2900        }
2901    }
2902    let chosen = candidates.into_iter().find(|&n| supported(n)).unwrap_or(1);
2903    if chosen != requested {
2904        log::info!("requested MSAA x{requested}, using x{chosen}");
2905    }
2906    chosen
2907}
2908
2909impl WgpuSceneRenderer {
2910    // Image API
2911
2912    pub fn set_image_from_bytes(
2913        &mut self,
2914        handle: u64,
2915        data: &[u8],
2916        srgb: bool,
2917    ) -> anyhow::Result<()> {
2918        let img = image::load_from_memory(data)?;
2919        let rgba = img.to_rgba8();
2920        let (w, h) = rgba.dimensions();
2921        self.set_image_rgba8(handle, w, h, &rgba, srgb)
2922    }
2923
2924    pub fn set_image_rgba8(
2925        &mut self,
2926        handle: u64,
2927        w: u32,
2928        h: u32,
2929        rgba: &[u8],
2930        srgb: bool,
2931    ) -> anyhow::Result<()> {
2932        let expected = (w as usize) * (h as usize) * 4;
2933        if rgba.len() < expected {
2934            return Err(anyhow::anyhow!(
2935                "RGBA buffer too small: {} < {}",
2936                rgba.len(),
2937                expected
2938            ));
2939        }
2940
2941        let format = if srgb {
2942            wgpu::TextureFormat::Rgba8UnormSrgb
2943        } else {
2944            wgpu::TextureFormat::Rgba8Unorm
2945        };
2946
2947        let needs_recreate = match self.images.get(&handle) {
2948            Some(ImageTex::Rgba {
2949                w: cw,
2950                h: ch,
2951                format: cf,
2952                ..
2953            }) => *cw != w || *ch != h || *cf != format,
2954            _ => true,
2955        };
2956
2957        if needs_recreate {
2958            self.remove_image(handle);
2959
2960            let (tex, bind) = self.create_rgba_tex(w, h, format);
2961            let bytes = (w as u64) * (h as u64) * 4;
2962            self.image_bytes_total += bytes;
2963
2964            self.images.insert(
2965                handle,
2966                ImageTex::Rgba {
2967                    tex,
2968                    bind,
2969                    w,
2970                    h,
2971                    format,
2972                    last_used_frame: self.frame_index,
2973                    bytes,
2974                },
2975            );
2976        }
2977
2978        self.retained.insert(
2979            handle,
2980            RetainedImage {
2981                w,
2982                h,
2983                format,
2984                rgba: rgba[..expected].to_vec(),
2985            },
2986        );
2987
2988        let tex = match self.images.get(&handle) {
2989            Some(ImageTex::Rgba { tex, .. }) => tex,
2990            _ => unreachable!(),
2991        };
2992
2993        self.queue.write_texture(
2994            wgpu::TexelCopyTextureInfo {
2995                texture: tex,
2996                mip_level: 0,
2997                origin: wgpu::Origin3d::ZERO,
2998                aspect: wgpu::TextureAspect::All,
2999            },
3000            &rgba[..expected],
3001            wgpu::TexelCopyBufferLayout {
3002                offset: 0,
3003                bytes_per_row: Some(4 * w),
3004                rows_per_image: Some(h),
3005            },
3006            wgpu::Extent3d {
3007                width: w,
3008                height: h,
3009                depth_or_array_layers: 1,
3010            },
3011        );
3012
3013        // Ensure budget limits
3014        self.evict_budget_excess();
3015
3016        Ok(())
3017    }
3018
3019    /// Create (but do not populate) the GPU texture, view and bind group for an
3020    /// RGBA image. Pixels are written separately via `write_texture`.
3021    fn create_rgba_tex(
3022        &self,
3023        w: u32,
3024        h: u32,
3025        format: wgpu::TextureFormat,
3026    ) -> (wgpu::Texture, wgpu::BindGroup) {
3027        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
3028            label: Some("user image rgba"),
3029            size: wgpu::Extent3d {
3030                width: w,
3031                height: h,
3032                depth_or_array_layers: 1,
3033            },
3034            mip_level_count: 1,
3035            sample_count: 1,
3036            dimension: wgpu::TextureDimension::D2,
3037            format,
3038            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3039            view_formats: &[],
3040        });
3041        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
3042
3043        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3044            label: Some("image bind rgba"),
3045            layout: &self.image_bind_layout_rgba,
3046            entries: &[
3047                wgpu::BindGroupEntry {
3048                    binding: 0,
3049                    resource: wgpu::BindingResource::TextureView(&view),
3050                },
3051                wgpu::BindGroupEntry {
3052                    binding: 1,
3053                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
3054                },
3055            ],
3056        });
3057
3058        (tex, bind)
3059    }
3060
3061    /// Register an externally-created `wgpu::TextureView` as an image (zero-copy).
3062    pub fn register_native_texture(
3063        &mut self,
3064        view: &wgpu::TextureView,
3065        width: u32,
3066        height: u32,
3067    ) -> u64 {
3068        let handle = self.next_image_handle;
3069        self.next_image_handle += 1;
3070        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3071            label: Some("user native image"),
3072            layout: &self.image_bind_layout_rgba,
3073            entries: &[
3074                wgpu::BindGroupEntry {
3075                    binding: 0,
3076                    resource: wgpu::BindingResource::TextureView(view),
3077                },
3078                wgpu::BindGroupEntry {
3079                    binding: 1,
3080                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
3081                },
3082            ],
3083        });
3084        self.images.insert(
3085            handle,
3086            ImageTex::User {
3087                bind,
3088                w: width,
3089                h: height,
3090                last_used_frame: self.frame_index,
3091                bytes: 0,
3092            },
3093        );
3094        handle
3095    }
3096
3097    /// Like `register_native_texture` but with custom sampler descriptor.
3098    pub fn register_native_texture_with_sampler(
3099        &mut self,
3100        view: &wgpu::TextureView,
3101        sampler_desc: wgpu::SamplerDescriptor<'_>,
3102        width: u32,
3103        height: u32,
3104    ) -> u64 {
3105        let handle = self.next_image_handle;
3106        self.next_image_handle += 1;
3107        let sampler = self.device.create_sampler(&sampler_desc);
3108        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3109            label: Some("user native image sampleropts"),
3110            layout: &self.image_bind_layout_rgba,
3111            entries: &[
3112                wgpu::BindGroupEntry {
3113                    binding: 0,
3114                    resource: wgpu::BindingResource::TextureView(view),
3115                },
3116                wgpu::BindGroupEntry {
3117                    binding: 1,
3118                    resource: wgpu::BindingResource::Sampler(&sampler),
3119                },
3120            ],
3121        });
3122        self.images.insert(
3123            handle,
3124            ImageTex::User {
3125                bind,
3126                w: width,
3127                h: height,
3128                last_used_frame: self.frame_index,
3129                bytes: 0,
3130            },
3131        );
3132        handle
3133    }
3134
3135    /// Update an existing native texture handle with a new view (reuse handle).
3136    pub fn update_native_texture(&mut self, handle: u64, view: &wgpu::TextureView) {
3137        let Some(entry) = self.images.get_mut(&handle) else {
3138            log::warn!("update_native_texture: handle {handle} not found");
3139            return;
3140        };
3141        let w = match entry {
3142            ImageTex::User { w, .. } => *w,
3143            ImageTex::Rgba { w, .. } => *w,
3144            _ => {
3145                log::warn!("update_native_texture: handle {handle} is not rgba/user");
3146                return;
3147            }
3148        };
3149        let h = match entry {
3150            ImageTex::User { h, .. } => *h,
3151            ImageTex::Rgba { h, .. } => *h,
3152            _ => 0,
3153        };
3154        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3155            label: Some("user native image update"),
3156            layout: &self.image_bind_layout_rgba,
3157            entries: &[
3158                wgpu::BindGroupEntry {
3159                    binding: 0,
3160                    resource: wgpu::BindingResource::TextureView(view),
3161                },
3162                wgpu::BindGroupEntry {
3163                    binding: 1,
3164                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
3165                },
3166            ],
3167        });
3168        *entry = ImageTex::User {
3169            bind,
3170            w,
3171            h,
3172            last_used_frame: self.frame_index,
3173            bytes: 0,
3174        };
3175    }
3176
3177    pub fn set_image_nv12(
3178        &mut self,
3179        handle: u64,
3180        w: u32,
3181        h: u32,
3182        y: &[u8],
3183        uv: &[u8],
3184        color_info: ColorInfo,
3185    ) -> anyhow::Result<()> {
3186        let y_expected = (w as usize) * (h as usize);
3187        let uv_w = w.div_ceil(2);
3188        let uv_h = h.div_ceil(2);
3189        let uv_expected = (uv_w as usize) * (uv_h as usize) * 2;
3190
3191        if y.len() < y_expected {
3192            return Err(anyhow::anyhow!("Y plane too small"));
3193        }
3194        if uv.len() < uv_expected {
3195            return Err(anyhow::anyhow!("UV plane too small"));
3196        }
3197
3198        let needs_recreate = match self.images.get(&handle) {
3199            Some(ImageTex::Nv12 { w: ww, h: hh, .. }) => *ww != w || *hh != h,
3200            _ => true,
3201        };
3202
3203        // Compute the YUV->RGB transform on the CPU.
3204        let yuv = color_info.to_yuv_transform();
3205        let yuv_raw = YuvTransformRaw {
3206            row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
3207            row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
3208            row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
3209            b: [yuv.b[0], yuv.b[1], yuv.b[2], 0.0],
3210        };
3211
3212        if needs_recreate {
3213            self.remove_image(handle);
3214
3215            let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
3216                label: Some("nv12 Y"),
3217                size: wgpu::Extent3d {
3218                    width: w,
3219                    height: h,
3220                    depth_or_array_layers: 1,
3221                },
3222                mip_level_count: 1,
3223                sample_count: 1,
3224                dimension: wgpu::TextureDimension::D2,
3225                format: wgpu::TextureFormat::R8Unorm,
3226                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3227                view_formats: &[],
3228            });
3229            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
3230
3231            let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
3232                label: Some("nv12 UV"),
3233                size: wgpu::Extent3d {
3234                    width: uv_w,
3235                    height: uv_h,
3236                    depth_or_array_layers: 1,
3237                },
3238                mip_level_count: 1,
3239                sample_count: 1,
3240                dimension: wgpu::TextureDimension::D2,
3241                format: wgpu::TextureFormat::Rg8Unorm,
3242                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3243                view_formats: &[],
3244            });
3245            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
3246
3247            // Create a uniform buffer for the YUV transform (per-image).
3248            let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
3249                label: Some("nv12 yuv transform"),
3250                size: std::mem::size_of::<YuvTransformRaw>() as u64,
3251                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3252                mapped_at_creation: false,
3253            });
3254
3255            // Write initial transform.
3256            self.queue
3257                .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
3258
3259            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3260                label: Some("nv12 bind"),
3261                layout: &self.image_bind_layout_nv12,
3262                entries: &[
3263                    wgpu::BindGroupEntry {
3264                        binding: 0,
3265                        resource: wgpu::BindingResource::TextureView(&view_y),
3266                    },
3267                    wgpu::BindGroupEntry {
3268                        binding: 1,
3269                        resource: wgpu::BindingResource::TextureView(&view_uv),
3270                    },
3271                    wgpu::BindGroupEntry {
3272                        binding: 2,
3273                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
3274                    },
3275                    wgpu::BindGroupEntry {
3276                        binding: 3,
3277                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3278                            buffer: &yuv_buf,
3279                            offset: 0,
3280                            size: None,
3281                        }),
3282                    },
3283                ],
3284            });
3285
3286            let bytes = (w as u64) * (h as u64)
3287                + (uv_w as u64) * (uv_h as u64) * 2
3288                + std::mem::size_of::<YuvTransformRaw>() as u64;
3289            self.image_bytes_total += bytes;
3290
3291            self.images.insert(
3292                handle,
3293                ImageTex::Nv12 {
3294                    tex_y,
3295                    tex_uv,
3296                    bind,
3297                    yuv_buf,
3298                    w,
3299                    h,
3300                    color_info,
3301                    last_used_frame: self.frame_index,
3302                    bytes,
3303                },
3304            );
3305        } else {
3306            // Re-use existing textures; just update the YUV transform if needed.
3307            if let Some(ImageTex::Nv12 { yuv_buf, .. }) = self.images.get(&handle) {
3308                self.queue
3309                    .write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
3310            }
3311        }
3312
3313        let (tex_y, tex_uv, _bind) = match self.images.get(&handle) {
3314            Some(ImageTex::Nv12 {
3315                tex_y,
3316                tex_uv,
3317                bind,
3318                ..
3319            }) => (tex_y, tex_uv, bind),
3320            _ => return Err(anyhow::anyhow!("Handle is not NV12")),
3321        };
3322
3323        self.queue.write_texture(
3324            wgpu::TexelCopyTextureInfo {
3325                texture: tex_y,
3326                mip_level: 0,
3327                origin: wgpu::Origin3d::ZERO,
3328                aspect: wgpu::TextureAspect::All,
3329            },
3330            &y[..y_expected],
3331            wgpu::TexelCopyBufferLayout {
3332                offset: 0,
3333                bytes_per_row: Some(w),
3334                rows_per_image: Some(h),
3335            },
3336            wgpu::Extent3d {
3337                width: w,
3338                height: h,
3339                depth_or_array_layers: 1,
3340            },
3341        );
3342
3343        self.queue.write_texture(
3344            wgpu::TexelCopyTextureInfo {
3345                texture: tex_uv,
3346                mip_level: 0,
3347                origin: wgpu::Origin3d::ZERO,
3348                aspect: wgpu::TextureAspect::All,
3349            },
3350            &uv[..uv_expected],
3351            wgpu::TexelCopyBufferLayout {
3352                offset: 0,
3353                bytes_per_row: Some(2 * uv_w),
3354                rows_per_image: Some(uv_h),
3355            },
3356            wgpu::Extent3d {
3357                width: uv_w,
3358                height: uv_h,
3359                depth_or_array_layers: 1,
3360            },
3361        );
3362
3363        self.evict_budget_excess();
3364        Ok(())
3365    }
3366
3367    pub fn set_image_planes(
3368        &mut self,
3369        handle: u64,
3370        w: u32,
3371        h: u32,
3372        pixel_format: PixelFormat,
3373        planes: &[&[u8]],
3374        color_info: ColorInfo,
3375    ) -> anyhow::Result<()> {
3376        match pixel_format {
3377            PixelFormat::Nv12 => {
3378                let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
3379                let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
3380                self.set_image_nv12(handle, w, h, y, uv, color_info)
3381            }
3382            PixelFormat::P010 => {
3383                let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
3384                let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
3385                self.set_image_p010(handle, w, h, y, uv, color_info)
3386            }
3387            PixelFormat::I420 | PixelFormat::I444 => Err(anyhow::anyhow!(
3388                "I420/I444 not implemented and unlikely -> cheap to convert to NV12 (better for the GPU too)"
3389            )),
3390            PixelFormat::Rgba => {
3391                let rgba = planes
3392                    .first()
3393                    .ok_or(anyhow::anyhow!("missing RGBA plane"))?;
3394                self.set_image_rgba8(handle, w, h, rgba, false)
3395            }
3396        }
3397    }
3398
3399    fn set_image_p010(
3400        &mut self,
3401        handle: u64,
3402        w: u32,
3403        h: u32,
3404        y: &[u8],
3405        uv: &[u8],
3406        color_info: ColorInfo,
3407    ) -> anyhow::Result<()> {
3408        let uv_w = w.div_ceil(2);
3409        let uv_h = h.div_ceil(2);
3410
3411        let y_expected = (w as usize) * (h as usize) * 2;
3412        let uv_expected = (uv_w as usize) * (uv_h as usize) * 4;
3413
3414        if y.len() < y_expected {
3415            return Err(anyhow::anyhow!("P010 Y plane too small"));
3416        }
3417        if uv.len() < uv_expected {
3418            return Err(anyhow::anyhow!("P010 UV plane too small"));
3419        }
3420
3421        // P010 reuses the NV12 pipeline (same bind group layout -> wgpu
3422        // abstracts the storage format so R16Unorm/Rg16Unorm are
3423        // filterable float textures just like R8Unorm/Rg8Unorm).
3424        let needs_recreate = match self.images.get(&handle) {
3425            Some(ImageTex::Nv12 { w: ww, h: hh, .. }) => *ww != w || *hh != h,
3426            _ => true,
3427        };
3428
3429        let yuv = color_info.to_yuv_transform();
3430        let yuv_raw = YuvTransformRaw {
3431            row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
3432            row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
3433            row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
3434            b: [yuv.b[0], yuv.b[1], yuv.b[2], 0.0],
3435        };
3436
3437        if needs_recreate {
3438            self.remove_image(handle);
3439
3440            let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
3441                label: Some("p010 Y"),
3442                size: wgpu::Extent3d {
3443                    width: w,
3444                    height: h,
3445                    depth_or_array_layers: 1,
3446                },
3447                mip_level_count: 1,
3448                sample_count: 1,
3449                dimension: wgpu::TextureDimension::D2,
3450                format: wgpu::TextureFormat::R16Unorm,
3451                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3452                view_formats: &[],
3453            });
3454            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
3455
3456            let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
3457                label: Some("p010 UV"),
3458                size: wgpu::Extent3d {
3459                    width: uv_w,
3460                    height: uv_h,
3461                    depth_or_array_layers: 1,
3462                },
3463                mip_level_count: 1,
3464                sample_count: 1,
3465                dimension: wgpu::TextureDimension::D2,
3466                format: wgpu::TextureFormat::Rg16Unorm,
3467                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3468                view_formats: &[],
3469            });
3470            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
3471
3472            let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
3473                label: Some("p010 yuv transform"),
3474                size: std::mem::size_of::<YuvTransformRaw>() as u64,
3475                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3476                mapped_at_creation: false,
3477            });
3478            self.queue
3479                .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
3480
3481            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3482                label: Some("p010 bind"),
3483                layout: &self.image_bind_layout_nv12,
3484                entries: &[
3485                    wgpu::BindGroupEntry {
3486                        binding: 0,
3487                        resource: wgpu::BindingResource::TextureView(&view_y),
3488                    },
3489                    wgpu::BindGroupEntry {
3490                        binding: 1,
3491                        resource: wgpu::BindingResource::TextureView(&view_uv),
3492                    },
3493                    wgpu::BindGroupEntry {
3494                        binding: 2,
3495                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
3496                    },
3497                    wgpu::BindGroupEntry {
3498                        binding: 3,
3499                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3500                            buffer: &yuv_buf,
3501                            offset: 0,
3502                            size: None,
3503                        }),
3504                    },
3505                ],
3506            });
3507
3508            let bytes = (w as u64) * (h as u64) * 2
3509                + (uv_w as u64) * (uv_h as u64) * 4
3510                + std::mem::size_of::<YuvTransformRaw>() as u64;
3511            self.image_bytes_total += bytes;
3512
3513            self.images.insert(
3514                handle,
3515                ImageTex::Nv12 {
3516                    tex_y,
3517                    tex_uv,
3518                    bind,
3519                    yuv_buf,
3520                    w,
3521                    h,
3522                    color_info,
3523                    last_used_frame: self.frame_index,
3524                    bytes,
3525                },
3526            );
3527        } else {
3528            if let Some(ImageTex::Nv12 { yuv_buf, .. }) = self.images.get(&handle) {
3529                self.queue
3530                    .write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
3531            }
3532        }
3533
3534        let (tex_y, tex_uv, _bind) = match self.images.get(&handle) {
3535            Some(ImageTex::Nv12 {
3536                tex_y,
3537                tex_uv,
3538                bind,
3539                ..
3540            }) => (tex_y, tex_uv, bind),
3541            _ => return Err(anyhow::anyhow!("Handle is not P010/NV12")),
3542        };
3543
3544        self.queue.write_texture(
3545            wgpu::TexelCopyTextureInfo {
3546                texture: tex_y,
3547                mip_level: 0,
3548                origin: wgpu::Origin3d::ZERO,
3549                aspect: wgpu::TextureAspect::All,
3550            },
3551            &y[..y_expected],
3552            wgpu::TexelCopyBufferLayout {
3553                offset: 0,
3554                bytes_per_row: Some(w * 2),
3555                rows_per_image: Some(h),
3556            },
3557            wgpu::Extent3d {
3558                width: w,
3559                height: h,
3560                depth_or_array_layers: 1,
3561            },
3562        );
3563        self.queue.write_texture(
3564            wgpu::TexelCopyTextureInfo {
3565                texture: tex_uv,
3566                mip_level: 0,
3567                origin: wgpu::Origin3d::ZERO,
3568                aspect: wgpu::TextureAspect::All,
3569            },
3570            &uv[..uv_expected],
3571            wgpu::TexelCopyBufferLayout {
3572                offset: 0,
3573                bytes_per_row: Some(uv_w * 4),
3574                rows_per_image: Some(uv_h),
3575            },
3576            wgpu::Extent3d {
3577                width: uv_w,
3578                height: uv_h,
3579                depth_or_array_layers: 1,
3580            },
3581        );
3582
3583        self.evict_budget_excess();
3584        Ok(())
3585    }
3586
3587    #[cfg(target_os = "linux")]
3588    pub fn set_image_dmabuf(
3589        &mut self,
3590        handle: u64,
3591        w: u32,
3592        h: u32,
3593        fds: Vec<std::os::unix::io::OwnedFd>,
3594        modifier: u64,
3595        strides: Vec<u32>,
3596        offsets: Vec<u64>,
3597        color_info: ColorInfo,
3598    ) -> anyhow::Result<()> {
3599        log::info!(
3600            "set_image_dmabuf handle={handle} {}x{} fds={} modifier=0x{modifier:x}",
3601            w,
3602            h,
3603            fds.len()
3604        );
3605
3606        self.remove_image(handle);
3607
3608        let yuv = color_info.to_yuv_transform();
3609        let yuv_raw = YuvTransformRaw {
3610            row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
3611            row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
3612            row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
3613            b: [yuv.b[0], yuv.b[1], yuv.b[2], 0.0],
3614        };
3615
3616        if fds.len() != 2 {
3617            return Err(anyhow::anyhow!(
3618                "unsupported fd count {} - need exactly 2 for separate Y/UV planes",
3619                fds.len()
3620            ));
3621        }
3622
3623        let uv_w = w.div_ceil(2);
3624        let uv_h = h.div_ceil(2);
3625
3626        let hal_y_desc = wgpu::hal::TextureDescriptor {
3627            label: Some("dmabuf y"),
3628            size: wgpu::Extent3d {
3629                width: w,
3630                height: h,
3631                depth_or_array_layers: 1,
3632            },
3633            mip_level_count: 1,
3634            sample_count: 1,
3635            dimension: wgpu::TextureDimension::D2,
3636            format: wgpu::TextureFormat::R8Unorm,
3637            usage: wgpu::wgt::TextureUses::RESOURCE,
3638            memory_flags: wgpu::hal::MemoryFlags::empty(),
3639            view_formats: vec![],
3640        };
3641        let hal_uv_desc = wgpu::hal::TextureDescriptor {
3642            label: Some("dmabuf uv"),
3643            size: wgpu::Extent3d {
3644                width: uv_w,
3645                height: uv_h,
3646                depth_or_array_layers: 1,
3647            },
3648            mip_level_count: 1,
3649            sample_count: 1,
3650            dimension: wgpu::TextureDimension::D2,
3651            format: wgpu::TextureFormat::Rg8Unorm,
3652            usage: wgpu::wgt::TextureUses::RESOURCE,
3653            memory_flags: wgpu::hal::MemoryFlags::empty(),
3654            view_formats: vec![],
3655        };
3656
3657        let wgpu_y_desc = wgpu::TextureDescriptor {
3658            label: Some("dmabuf y"),
3659            size: wgpu::Extent3d {
3660                width: w,
3661                height: h,
3662                depth_or_array_layers: 1,
3663            },
3664            mip_level_count: 1,
3665            sample_count: 1,
3666            dimension: wgpu::TextureDimension::D2,
3667            format: wgpu::TextureFormat::R8Unorm,
3668            usage: wgpu::TextureUsages::TEXTURE_BINDING,
3669            view_formats: &[],
3670        };
3671        let wgpu_uv_desc = wgpu::TextureDescriptor {
3672            label: Some("dmabuf uv"),
3673            size: wgpu::Extent3d {
3674                width: uv_w,
3675                height: uv_h,
3676                depth_or_array_layers: 1,
3677            },
3678            mip_level_count: 1,
3679            sample_count: 1,
3680            dimension: wgpu::TextureDimension::D2,
3681            format: wgpu::TextureFormat::Rg8Unorm,
3682            usage: wgpu::TextureUsages::TEXTURE_BINDING,
3683            view_formats: &[],
3684        };
3685
3686        let (tex_y, view_y, tex_uv, view_uv) = unsafe {
3687            let hal_guard = self
3688                .device
3689                .as_hal::<wgpu::hal::vulkan::Api>()
3690                .ok_or_else(|| {
3691                    log::warn!("as_hal::<vulkan::Api> returned None");
3692                    anyhow::anyhow!("Device is not Vulkan")
3693                })?;
3694
3695            let mut fds = fds;
3696            let uv_fd = fds.remove(1);
3697            let y_fd = fds.remove(0);
3698
3699            let yt = hal_guard
3700                .texture_from_dmabuf_fd(y_fd, &hal_y_desc, modifier, strides[0] as u64, offsets[0])
3701                .map_err(|e| anyhow::anyhow!("import Y dmabuf: {e:?}"))?;
3702            log::info!("imported Y dmabuf OK");
3703
3704            let uvt = hal_guard
3705                .texture_from_dmabuf_fd(
3706                    uv_fd,
3707                    &hal_uv_desc,
3708                    modifier,
3709                    strides[1] as u64,
3710                    offsets[1],
3711                )
3712                .map_err(|e| anyhow::anyhow!("import UV dmabuf: {e:?}"))?;
3713            log::info!("imported UV dmabuf OK");
3714
3715            drop(hal_guard);
3716
3717            let tex_y = self
3718                .device
3719                .create_texture_from_hal::<wgpu::hal::vulkan::Api>(
3720                    yt,
3721                    &wgpu_y_desc,
3722                    wgpu::wgt::TextureUses::UNINITIALIZED,
3723                );
3724            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
3725
3726            let tex_uv = self
3727                .device
3728                .create_texture_from_hal::<wgpu::hal::vulkan::Api>(
3729                    uvt,
3730                    &wgpu_uv_desc,
3731                    wgpu::wgt::TextureUses::UNINITIALIZED,
3732                );
3733            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
3734
3735            (tex_y, view_y, tex_uv, view_uv)
3736        };
3737
3738        let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
3739            label: Some("dmabuf yuv transform"),
3740            size: std::mem::size_of::<YuvTransformRaw>() as u64,
3741            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3742            mapped_at_creation: false,
3743        });
3744        self.queue
3745            .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
3746
3747        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3748            label: Some("dmabuf nv12 bind"),
3749            layout: &self.image_bind_layout_nv12,
3750            entries: &[
3751                wgpu::BindGroupEntry {
3752                    binding: 0,
3753                    resource: wgpu::BindingResource::TextureView(&view_y),
3754                },
3755                wgpu::BindGroupEntry {
3756                    binding: 1,
3757                    resource: wgpu::BindingResource::TextureView(&view_uv),
3758                },
3759                wgpu::BindGroupEntry {
3760                    binding: 2,
3761                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
3762                },
3763                wgpu::BindGroupEntry {
3764                    binding: 3,
3765                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3766                        buffer: &yuv_buf,
3767                        offset: 0,
3768                        size: None,
3769                    }),
3770                },
3771            ],
3772        });
3773
3774        let bytes = (w as u64) * (h as u64)
3775            + (uv_w as u64) * (uv_h as u64) * 2
3776            + std::mem::size_of::<YuvTransformRaw>() as u64;
3777
3778        self.images.insert(
3779            handle,
3780            ImageTex::Nv12 {
3781                tex_y,
3782                tex_uv,
3783                bind,
3784                yuv_buf,
3785                w,
3786                h,
3787                color_info,
3788                last_used_frame: self.frame_index,
3789                bytes,
3790            },
3791        );
3792
3793        self.evict_budget_excess();
3794        Ok(())
3795    }
3796
3797    pub fn remove_image(&mut self, handle: u64) {
3798        if let Some(img) = self.images.remove(&handle) {
3799            let b = match &img {
3800                ImageTex::Rgba { bytes, .. } => *bytes,
3801                ImageTex::Nv12 { bytes, .. } => *bytes,
3802                ImageTex::User { bytes, .. } => *bytes,
3803            };
3804            self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
3805        }
3806        self.retained.remove(&handle);
3807    }
3808
3809    fn evict_image_gpu(&mut self, handle: u64) -> u64 {
3810        let Some(img) = self.images.remove(&handle) else {
3811            return 0;
3812        };
3813        let b = match &img {
3814            ImageTex::Rgba { bytes, .. } => *bytes,
3815            ImageTex::Nv12 { bytes, .. } => *bytes,
3816            ImageTex::User { bytes, .. } => *bytes,
3817        };
3818        self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
3819        b
3820    }
3821
3822    fn revive_retained_image(&mut self, handle: u64) -> bool {
3823        if self.images.contains_key(&handle) {
3824            return true;
3825        }
3826        let Some(r) = self.retained.get(&handle).cloned() else {
3827            return false;
3828        };
3829        let (tex, bind) = self.create_rgba_tex(r.w, r.h, r.format);
3830
3831        self.queue.write_texture(
3832            wgpu::TexelCopyTextureInfo {
3833                texture: &tex,
3834                mip_level: 0,
3835                origin: wgpu::Origin3d::ZERO,
3836                aspect: wgpu::TextureAspect::All,
3837            },
3838            &r.rgba,
3839            wgpu::TexelCopyBufferLayout {
3840                offset: 0,
3841                bytes_per_row: Some(4 * r.w),
3842                rows_per_image: Some(r.h),
3843            },
3844            wgpu::Extent3d {
3845                width: r.w,
3846                height: r.h,
3847                depth_or_array_layers: 1,
3848            },
3849        );
3850
3851        let bytes = (r.w as u64) * (r.h as u64) * 4;
3852        self.image_bytes_total += bytes;
3853        self.images.insert(
3854            handle,
3855            ImageTex::Rgba {
3856                tex,
3857                bind,
3858                w: r.w,
3859                h: r.h,
3860                format: r.format,
3861                last_used_frame: self.frame_index,
3862                bytes,
3863            },
3864        );
3865        true
3866    }
3867
3868    fn resolve_image_for_draw(&mut self, handle: u64) -> Option<(u32, u32, bool)> {
3869        if let Some(t) = self.images.get_mut(&handle) {
3870            return match t {
3871                ImageTex::Rgba {
3872                    w,
3873                    h,
3874                    last_used_frame,
3875                    ..
3876                } => {
3877                    *last_used_frame = self.frame_index;
3878                    Some((*w, *h, false))
3879                }
3880                ImageTex::User {
3881                    w,
3882                    h,
3883                    last_used_frame,
3884                    ..
3885                } => {
3886                    *last_used_frame = self.frame_index;
3887                    Some((*w, *h, false))
3888                }
3889                ImageTex::Nv12 {
3890                    w,
3891                    h,
3892                    last_used_frame,
3893                    ..
3894                } => {
3895                    *last_used_frame = self.frame_index;
3896                    Some((*w, *h, true))
3897                }
3898            };
3899        }
3900        if self.revive_retained_image(handle)
3901            && let Some(ImageTex::Rgba {
3902                w,
3903                h,
3904                last_used_frame,
3905                ..
3906            }) = self.images.get_mut(&handle)
3907        {
3908            *last_used_frame = self.frame_index;
3909            return Some((*w, *h, false));
3910        }
3911        None
3912    }
3913
3914    // Legacy support from Step 1 instructions (temporary until platform render logic is fully swapped)
3915    pub fn register_image_from_bytes(&mut self, data: &[u8], srgb: bool) -> u64 {
3916        let handle = self.next_image_handle;
3917        self.next_image_handle += 1;
3918        if let Err(e) = self.set_image_from_bytes(handle, data, srgb) {
3919            log::error!("Failed to register image: {e}");
3920        }
3921        handle
3922    }
3923
3924    /// Register raw RGBA8 pixels (`w * h * 4` bytes) as an image, returning
3925    /// its handle for `SceneNode::Image`. Used by CPU-rasterized overlays
3926    /// (e.g. subtitle bitmap layers) that have no encoded image bytes.
3927    /// Pass `srgb = true` for sRGB overlays composited over video.
3928    pub fn register_image_rgba8(&mut self, w: u32, h: u32, rgba: &[u8], srgb: bool) -> u64 {
3929        let handle = self.next_image_handle;
3930        self.next_image_handle += 1;
3931        if let Err(e) = self.set_image_rgba8(handle, w, h, rgba, srgb) {
3932            log::error!("Failed to register image: {e}");
3933        }
3934        handle
3935    }
3936
3937    /// Register an 8-bit coverage tile (`w * h` bytes, 0 = empty, 255 =
3938    /// fully covered) for `SceneNode::Coverage`, returning its handle.
3939    /// Coverage tiles are immutable: re-register on geometry change and
3940    /// `remove_coverage` handles you no longer emit (stale tiles also age
3941    /// out under the image eviction policy).
3942    pub fn register_coverage_a8(&mut self, w: u32, h: u32, coverage: &[u8]) -> u64 {
3943        let expected = (w as usize) * (h as usize);
3944        if coverage.len() < expected || w == 0 || h == 0 {
3945            log::error!("Coverage buffer too small: {} < {expected}", coverage.len());
3946            return 0;
3947        }
3948        let handle = self.next_coverage_handle;
3949        self.next_coverage_handle += 1;
3950
3951        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
3952            label: Some("coverage tile a8"),
3953            size: wgpu::Extent3d {
3954                width: w,
3955                height: h,
3956                depth_or_array_layers: 1,
3957            },
3958            mip_level_count: 1,
3959            sample_count: 1,
3960            dimension: wgpu::TextureDimension::D2,
3961            format: wgpu::TextureFormat::R8Unorm,
3962            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3963            view_formats: &[],
3964        });
3965        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
3966        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3967            label: Some("coverage bind a8"),
3968            layout: &self.image_bind_layout_rgba,
3969            entries: &[
3970                wgpu::BindGroupEntry {
3971                    binding: 0,
3972                    resource: wgpu::BindingResource::TextureView(&view),
3973                },
3974                wgpu::BindGroupEntry {
3975                    binding: 1,
3976                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
3977                },
3978            ],
3979        });
3980        self.queue.write_texture(
3981            wgpu::TexelCopyTextureInfo {
3982                texture: &tex,
3983                mip_level: 0,
3984                origin: wgpu::Origin3d::ZERO,
3985                aspect: wgpu::TextureAspect::All,
3986            },
3987            &coverage[..expected],
3988            wgpu::TexelCopyBufferLayout {
3989                offset: 0,
3990                bytes_per_row: Some(w),
3991                rows_per_image: Some(h),
3992            },
3993            wgpu::Extent3d {
3994                width: w,
3995                height: h,
3996                depth_or_array_layers: 1,
3997            },
3998        );
3999        let bytes = (w as u64) * (h as u64);
4000        self.image_bytes_total += bytes;
4001        self.coverages.insert(
4002            handle,
4003            CoverageTex {
4004                tex,
4005                bind,
4006                w,
4007                h,
4008                last_used_frame: self.frame_index,
4009                bytes,
4010            },
4011        );
4012        self.evict_budget_excess();
4013        handle
4014    }
4015
4016    /// Remove a coverage tile registered with [`register_coverage_a8`](Self::register_coverage_a8).
4017    pub fn remove_coverage(&mut self, handle: u64) {
4018        if let Some(tile) = self.coverages.remove(&handle) {
4019            self.image_bytes_total = self.image_bytes_total.saturating_sub(tile.bytes);
4020        }
4021    }
4022
4023    /// Tile dimensions, marking the handle used (keeps it alive under the
4024    /// eviction policy). Returns `None` for unknown handles.
4025    pub fn coverage_dimensions(&mut self, handle: u64) -> Option<(u32, u32)> {
4026        if let Some(tile) = self.coverages.get_mut(&handle) {
4027            tile.last_used_frame = self.frame_index;
4028            return Some((tile.w, tile.h));
4029        }
4030        None
4031    }
4032
4033    fn evict_unused_images(&mut self) {
4034        let now = self.frame_index;
4035        let evict_after = self.image_evict_after_frames;
4036
4037        // Time based eviction. Eviction only frees GPU memory: retained RGBA
4038        // sources stay so the image can be lazily re-uploaded when drawn again.
4039        let mut to_evict = Vec::new();
4040        for (h, t) in self.images.iter() {
4041            let last = match t {
4042                ImageTex::Rgba {
4043                    last_used_frame, ..
4044                } => *last_used_frame,
4045                ImageTex::User {
4046                    last_used_frame, ..
4047                } => *last_used_frame,
4048                ImageTex::Nv12 {
4049                    last_used_frame, ..
4050                } => *last_used_frame,
4051            };
4052            if now.saturating_sub(last) > evict_after {
4053                to_evict.push(*h);
4054            }
4055        }
4056        for h in to_evict {
4057            if self.retained.contains_key(&h) {
4058                self.evict_image_gpu(h);
4059            } else {
4060                self.remove_image(h);
4061            }
4062        }
4063
4064        // Coverage tiles have no retained CPU copies: age-out removes them.
4065        let mut stale = Vec::new();
4066        for (h, t) in self.coverages.iter() {
4067            if now.saturating_sub(t.last_used_frame) > evict_after {
4068                stale.push(*h);
4069            }
4070        }
4071        for h in stale {
4072            self.remove_coverage(h);
4073        }
4074
4075        self.evict_budget_excess();
4076    }
4077
4078    fn evict_budget_excess(&mut self) {
4079        if self.image_bytes_total <= self.image_budget_bytes {
4080            return;
4081        }
4082        // Collect (handle, last_used, bytes)
4083        let mut candidates: Vec<(u64, u64, u64)> = self
4084            .images
4085            .iter()
4086            .map(|(h, t)| {
4087                let (last, bytes) = match t {
4088                    ImageTex::Rgba {
4089                        last_used_frame,
4090                        bytes,
4091                        ..
4092                    } => (*last_used_frame, *bytes),
4093                    ImageTex::User {
4094                        last_used_frame,
4095                        bytes,
4096                        ..
4097                    } => (*last_used_frame, *bytes),
4098                    ImageTex::Nv12 {
4099                        last_used_frame,
4100                        bytes,
4101                        ..
4102                    } => (*last_used_frame, *bytes),
4103                };
4104                (*h, last, bytes)
4105            })
4106            .collect();
4107
4108        // Sort by last_used ascending (LRU first)
4109        candidates.sort_by_key(|k| k.1);
4110
4111        let now = self.frame_index;
4112        for (h, last, _bytes) in candidates {
4113            if self.image_bytes_total <= self.image_budget_bytes {
4114                break;
4115            }
4116            // Don't evict something used this frame
4117            if last == now {
4118                continue;
4119            }
4120            if self.retained.contains_key(&h) {
4121                self.evict_image_gpu(h);
4122            } else {
4123                self.remove_image(h);
4124            }
4125        }
4126    }
4127
4128    /// Set pixels per point (DPI scale) for callback `ScreenDescriptor` / `PaintCallbackInfo`.
4129    pub fn set_pixels_per_point(&mut self, ppp: f32) {
4130        self.pixels_per_point = ppp.clamp(0.5, 8.0);
4131    }
4132
4133    /// Enable or disable linear working-space rendering.
4134    /// When enabled, the scene is rendered into an Rgba16Float intermediate
4135    /// and a final full-screen pass applies the display OETF.
4136    pub fn set_working_space(&mut self, enabled: bool) {
4137        if enabled == self.working_space {
4138            return;
4139        }
4140        self.working_space = enabled;
4141        if enabled {
4142            self.ensure_display_pipeline();
4143            self.recreate_working_space_texture();
4144        } else {
4145            self.ws_tex = None;
4146            self.ws_view = None;
4147            self.ws_bind = None;
4148        }
4149    }
4150
4151    fn ensure_display_pipeline(&mut self) {
4152        if self.display_pipeline.is_some() {
4153            return;
4154        }
4155
4156        let layout = self
4157            .device
4158            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4159                label: Some("display transform layout"),
4160                entries: &[
4161                    wgpu::BindGroupLayoutEntry {
4162                        binding: 0,
4163                        visibility: wgpu::ShaderStages::FRAGMENT,
4164                        ty: wgpu::BindingType::Texture {
4165                            multisampled: false,
4166                            view_dimension: wgpu::TextureViewDimension::D2,
4167                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
4168                        },
4169                        count: None,
4170                    },
4171                    wgpu::BindGroupLayoutEntry {
4172                        binding: 1,
4173                        visibility: wgpu::ShaderStages::FRAGMENT,
4174                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
4175                        count: None,
4176                    },
4177                ],
4178            });
4179        self.display_layout = Some(layout);
4180
4181        let shader = self
4182            .device
4183            .create_shader_module(wgpu::ShaderModuleDescriptor {
4184                label: Some("display_transform.wgsl"),
4185                source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
4186                    "shaders/display_transform.wgsl"
4187                ))),
4188            });
4189
4190        let pipeline_layout = self
4191            .device
4192            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
4193                label: Some("display transform pipeline layout"),
4194                bind_group_layouts: &[None, self.display_layout.as_ref()],
4195                immediate_size: 0,
4196            });
4197
4198        let pipeline = self
4199            .device
4200            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
4201                label: Some("display transform pipeline"),
4202                layout: Some(&pipeline_layout),
4203                vertex: wgpu::VertexState {
4204                    module: &shader,
4205                    entry_point: Some("vs_main"),
4206                    buffers: &[],
4207                    compilation_options: wgpu::PipelineCompilationOptions::default(),
4208                },
4209                fragment: Some(wgpu::FragmentState {
4210                    module: &shader,
4211                    entry_point: Some("fs_main"),
4212                    targets: &[Some(wgpu::ColorTargetState {
4213                        format: self.output_format,
4214                        blend: None,
4215                        write_mask: wgpu::ColorWrites::ALL,
4216                    })],
4217                    compilation_options: wgpu::PipelineCompilationOptions::default(),
4218                }),
4219                primitive: wgpu::PrimitiveState::default(),
4220                depth_stencil: None,
4221                multisample: wgpu::MultisampleState::default(),
4222                multiview_mask: None,
4223                cache: None,
4224            });
4225        self.display_pipeline = Some(pipeline);
4226    }
4227
4228    /// Resize the render target dimensions.
4229    ///
4230    /// Recreates MSAA, depth-stencil, and working-space textures to match the
4231    /// new size..
4232    pub fn resize(&mut self, width: u32, height: u32) {
4233        self.output_width = width;
4234        self.output_height = height;
4235        self.recreate_msaa_and_depth_stencil();
4236        self.recreate_working_space_texture();
4237    }
4238
4239    fn recreate_working_space_texture(&mut self) {
4240        if !self.working_space {
4241            return;
4242        }
4243        let w = self.output_width.max(1);
4244        let h = self.output_height.max(1);
4245
4246        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
4247            label: Some("working space"),
4248            size: wgpu::Extent3d {
4249                width: w,
4250                height: h,
4251                depth_or_array_layers: 1,
4252            },
4253            mip_level_count: 1,
4254            sample_count: 1,
4255            dimension: wgpu::TextureDimension::D2,
4256            format: wgpu::TextureFormat::Rgba16Float,
4257            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
4258            view_formats: &[],
4259        });
4260        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
4261
4262        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4263            label: Some("working space bind"),
4264            layout: self.display_layout.as_ref().unwrap(),
4265            entries: &[
4266                wgpu::BindGroupEntry {
4267                    binding: 0,
4268                    resource: wgpu::BindingResource::TextureView(&view),
4269                },
4270                wgpu::BindGroupEntry {
4271                    binding: 1,
4272                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
4273                },
4274            ],
4275        });
4276
4277        self.ws_tex = Some(tex);
4278        self.ws_view = Some(view);
4279        self.ws_bind = Some(bind);
4280    }
4281
4282    fn recreate_msaa_and_depth_stencil(&mut self) {
4283        if self.msaa_samples > 1 {
4284            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
4285                label: Some("msaa color"),
4286                size: wgpu::Extent3d {
4287                    width: self.output_width.max(1),
4288                    height: self.output_height.max(1),
4289                    depth_or_array_layers: 1,
4290                },
4291                mip_level_count: 1,
4292                sample_count: self.msaa_samples,
4293                dimension: wgpu::TextureDimension::D2,
4294                format: self.output_format,
4295                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
4296                view_formats: &[],
4297            });
4298            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
4299            self.msaa_tex = Some(tex);
4300            self.msaa_view = Some(view);
4301        } else {
4302            self.msaa_tex = None;
4303            self.msaa_view = None;
4304        }
4305
4306        self.depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
4307            label: Some("depth-stencil (stencil clips)"),
4308            size: wgpu::Extent3d {
4309                width: self.output_width.max(1),
4310                height: self.output_height.max(1),
4311                depth_or_array_layers: 1,
4312            },
4313            mip_level_count: 1,
4314            sample_count: self.msaa_samples,
4315            dimension: wgpu::TextureDimension::D2,
4316            format: wgpu::TextureFormat::Depth24PlusStencil8,
4317            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
4318            view_formats: &[],
4319        });
4320        self.depth_stencil_view = self
4321            .depth_stencil_tex
4322            .create_view(&wgpu::TextureViewDescriptor::default());
4323    }
4324
4325    fn get_or_create_layer(
4326        &mut self,
4327        layer_id: u32,
4328        width: u32,
4329        height: u32,
4330        rect: repose_core::Rect,
4331    ) {
4332        let needs_alloc = match self.layer_pool.get(&layer_id) {
4333            Some(lt) => lt.width != width || lt.height != height,
4334            None => true,
4335        };
4336        if !needs_alloc {
4337            if let Some(lt) = self.layer_pool.get_mut(&layer_id) {
4338                lt.rect_px = (rect.x, rect.y, rect.w, rect.h);
4339            }
4340            return;
4341        }
4342        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
4343            label: Some("graphics layer"),
4344            size: wgpu::Extent3d {
4345                width: width.max(1),
4346                height: height.max(1),
4347                depth_or_array_layers: 1,
4348            },
4349            mip_level_count: 1,
4350            sample_count: 1,
4351            dimension: wgpu::TextureDimension::D2,
4352            format: self.output_format,
4353            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
4354                | wgpu::TextureUsages::TEXTURE_BINDING
4355                | wgpu::TextureUsages::COPY_SRC,
4356            view_formats: &[],
4357        });
4358        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
4359        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4360            label: Some("layer bind"),
4361            layout: &self.image_bind_layout_rgba,
4362            entries: &[
4363                wgpu::BindGroupEntry {
4364                    binding: 0,
4365                    resource: wgpu::BindingResource::TextureView(&view),
4366                },
4367                wgpu::BindGroupEntry {
4368                    binding: 1,
4369                    resource: wgpu::BindingResource::Sampler(&self.layer_sampler),
4370                },
4371            ],
4372        });
4373        let bind_linear = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4374            label: Some("layer bind linear"),
4375            layout: &self.image_bind_layout_rgba,
4376            entries: &[
4377                wgpu::BindGroupEntry {
4378                    binding: 0,
4379                    resource: wgpu::BindingResource::TextureView(&view),
4380                },
4381                wgpu::BindGroupEntry {
4382                    binding: 1,
4383                    resource: wgpu::BindingResource::Sampler(&self.layer_sampler_linear),
4384                },
4385            ],
4386        });
4387        let depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
4388            label: Some("graphics layer depth-stencil"),
4389            size: wgpu::Extent3d {
4390                width: width.max(1),
4391                height: height.max(1),
4392                depth_or_array_layers: 1,
4393            },
4394            mip_level_count: 1,
4395            sample_count: 1,
4396            dimension: wgpu::TextureDimension::D2,
4397            format: wgpu::TextureFormat::Depth24PlusStencil8,
4398            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
4399            view_formats: &[],
4400        });
4401        let depth_stencil_view =
4402            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
4403        self.layer_pool.insert(
4404            layer_id,
4405            LayerTarget {
4406                texture: tex,
4407                view,
4408                bind,
4409                bind_linear,
4410                depth_stencil_view,
4411                width,
4412                height,
4413                rect_px: (rect.x, rect.y, rect.w, rect.h),
4414            },
4415        );
4416    }
4417
4418    fn atlas_bind_group_mask(&self) -> wgpu::BindGroup {
4419        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4420            label: Some("atlas bind"),
4421            layout: &self.text_bind_layout,
4422            entries: &[
4423                wgpu::BindGroupEntry {
4424                    binding: 0,
4425                    resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
4426                },
4427                wgpu::BindGroupEntry {
4428                    binding: 1,
4429                    resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
4430                },
4431            ],
4432        })
4433    }
4434
4435    fn atlas_bind_group_color(&self) -> wgpu::BindGroup {
4436        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4437            label: Some("atlas bind color"),
4438            layout: &self.text_bind_layout,
4439            entries: &[
4440                wgpu::BindGroupEntry {
4441                    binding: 0,
4442                    resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
4443                },
4444                wgpu::BindGroupEntry {
4445                    binding: 1,
4446                    resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
4447                },
4448            ],
4449        })
4450    }
4451
4452    fn upload_glyph_mask(&mut self, key: repose_text::GlyphKey, px: f32) -> Option<GlyphInfo> {
4453        let keyp = (key, px.to_bits());
4454        if let Some(info) = self.atlas_mask.map.get(&keyp) {
4455            return Some(*info);
4456        }
4457
4458        let gb = repose_text::rasterize(key, px)?;
4459        if gb.w == 0 || gb.h == 0 || gb.data.is_empty() {
4460            return None;
4461        }
4462
4463        let coverage = swash_to_a8_coverage(gb.content, &gb.data)?;
4464
4465        let w = gb.w.max(1);
4466        let h = gb.h.max(1);
4467
4468        if !self.alloc_space_mask(w, h) {
4469            self.grow_mask_and_rebuild();
4470        }
4471        if !self.alloc_space_mask(w, h) {
4472            return None;
4473        }
4474        let x = self.atlas_mask.next_x;
4475        let y = self.atlas_mask.next_y;
4476        self.atlas_mask.next_x += w + 1;
4477        self.atlas_mask.row_h = self.atlas_mask.row_h.max(h + 1);
4478
4479        let layout = wgpu::TexelCopyBufferLayout {
4480            offset: 0,
4481            bytes_per_row: Some(w),
4482            rows_per_image: Some(h),
4483        };
4484        let size = wgpu::Extent3d {
4485            width: w,
4486            height: h,
4487            depth_or_array_layers: 1,
4488        };
4489        self.queue.write_texture(
4490            wgpu::TexelCopyTextureInfoBase {
4491                texture: &self.atlas_mask.tex,
4492                mip_level: 0,
4493                origin: wgpu::Origin3d { x, y, z: 0 },
4494                aspect: wgpu::TextureAspect::All,
4495            },
4496            &coverage,
4497            layout,
4498            size,
4499        );
4500
4501        let info = GlyphInfo {
4502            u0: x as f32 / self.atlas_mask.size as f32,
4503            v0: y as f32 / self.atlas_mask.size as f32,
4504            u1: (x + w) as f32 / self.atlas_mask.size as f32,
4505            v1: (y + h) as f32 / self.atlas_mask.size as f32,
4506            w: w as f32,
4507            h: h as f32,
4508        };
4509        self.atlas_mask.map.insert(keyp, info);
4510        Some(info)
4511    }
4512
4513    fn upload_glyph_color(&mut self, key: repose_text::GlyphKey, px: f32) -> Option<GlyphInfo> {
4514        let keyp = (key, px.to_bits());
4515        if let Some(info) = self.atlas_color.map.get(&keyp) {
4516            return Some(*info);
4517        }
4518        let gb = repose_text::rasterize(key, px)?;
4519        if !matches!(gb.content, repose_text::SwashContent::Color) {
4520            return None;
4521        }
4522        let w = gb.w.max(1);
4523        let h = gb.h.max(1);
4524        if !self.alloc_space_color(w, h) {
4525            self.grow_color_and_rebuild();
4526        }
4527        if !self.alloc_space_color(w, h) {
4528            return None;
4529        }
4530        let x = self.atlas_color.next_x;
4531        let y = self.atlas_color.next_y;
4532        self.atlas_color.next_x += w + 1;
4533        self.atlas_color.row_h = self.atlas_color.row_h.max(h + 1);
4534
4535        let layout = wgpu::TexelCopyBufferLayout {
4536            offset: 0,
4537            bytes_per_row: Some(w * 4),
4538            rows_per_image: Some(h),
4539        };
4540        let size = wgpu::Extent3d {
4541            width: w,
4542            height: h,
4543            depth_or_array_layers: 1,
4544        };
4545        self.queue.write_texture(
4546            wgpu::TexelCopyTextureInfoBase {
4547                texture: &self.atlas_color.tex,
4548                mip_level: 0,
4549                origin: wgpu::Origin3d { x, y, z: 0 },
4550                aspect: wgpu::TextureAspect::All,
4551            },
4552            &gb.data,
4553            layout,
4554            size,
4555        );
4556        let info = GlyphInfo {
4557            u0: x as f32 / self.atlas_color.size as f32,
4558            v0: y as f32 / self.atlas_color.size as f32,
4559            u1: (x + w) as f32 / self.atlas_color.size as f32,
4560            v1: (y + h) as f32 / self.atlas_color.size as f32,
4561            w: w as f32,
4562            h: h as f32,
4563        };
4564        self.atlas_color.map.insert(keyp, info);
4565        Some(info)
4566    }
4567
4568    fn alloc_space_mask(&mut self, w: u32, h: u32) -> bool {
4569        if self.atlas_mask.next_x + w + 1 >= self.atlas_mask.size {
4570            self.atlas_mask.next_x = 1;
4571            self.atlas_mask.next_y += self.atlas_mask.row_h + 1;
4572            self.atlas_mask.row_h = 0;
4573        }
4574        if self.atlas_mask.next_y + h + 1 >= self.atlas_mask.size {
4575            return false;
4576        }
4577        true
4578    }
4579
4580    fn grow_mask_and_rebuild(&mut self) {
4581        let new_size = (self.atlas_mask.size * 2).min(4096);
4582        if new_size == self.atlas_mask.size {
4583            return;
4584        }
4585        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
4586            label: Some("glyph atlas A8 (grown)"),
4587            size: wgpu::Extent3d {
4588                width: new_size,
4589                height: new_size,
4590                depth_or_array_layers: 1,
4591            },
4592            mip_level_count: 1,
4593            sample_count: 1,
4594            dimension: wgpu::TextureDimension::D2,
4595            format: wgpu::TextureFormat::R8Unorm,
4596            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4597            view_formats: &[],
4598        });
4599        self.atlas_mask.tex = tex;
4600        self.atlas_mask.view = self
4601            .atlas_mask
4602            .tex
4603            .create_view(&wgpu::TextureViewDescriptor::default());
4604        self.atlas_mask.size = new_size;
4605        self.atlas_mask.next_x = 1;
4606        self.atlas_mask.next_y = 1;
4607        self.atlas_mask.row_h = 0;
4608        let keys: Vec<(repose_text::GlyphKey, u32)> = self.atlas_mask.map.keys().copied().collect();
4609        self.atlas_mask.map.clear();
4610        for (k, px_bits) in keys {
4611            let _ = self.upload_glyph_mask(k, f32::from_bits(px_bits));
4612        }
4613    }
4614
4615    fn alloc_space_color(&mut self, w: u32, h: u32) -> bool {
4616        if self.atlas_color.next_x + w + 1 >= self.atlas_color.size {
4617            self.atlas_color.next_x = 1;
4618            self.atlas_color.next_y += self.atlas_color.row_h + 1;
4619            self.atlas_color.row_h = 0;
4620        }
4621        if self.atlas_color.next_y + h + 1 >= self.atlas_color.size {
4622            return false;
4623        }
4624        true
4625    }
4626
4627    fn grow_color_and_rebuild(&mut self) {
4628        let new_size = (self.atlas_color.size * 2).min(4096);
4629        if new_size == self.atlas_color.size {
4630            return;
4631        }
4632        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
4633            label: Some("glyph atlas RGBA (grown)"),
4634            size: wgpu::Extent3d {
4635                width: new_size,
4636                height: new_size,
4637                depth_or_array_layers: 1,
4638            },
4639            mip_level_count: 1,
4640            sample_count: 1,
4641            dimension: wgpu::TextureDimension::D2,
4642            format: wgpu::TextureFormat::Rgba8UnormSrgb,
4643            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4644            view_formats: &[],
4645        });
4646        self.atlas_color.tex = tex;
4647        self.atlas_color.view = self
4648            .atlas_color
4649            .tex
4650            .create_view(&wgpu::TextureViewDescriptor::default());
4651        self.atlas_color.size = new_size;
4652        self.atlas_color.next_x = 1;
4653        self.atlas_color.next_y = 1;
4654        self.atlas_color.row_h = 0;
4655        let keys: Vec<(repose_text::GlyphKey, u32)> =
4656            self.atlas_color.map.keys().copied().collect();
4657        self.atlas_color.map.clear();
4658        for (k, px_bits) in keys {
4659            let _ = self.upload_glyph_color(k, f32::from_bits(px_bits));
4660        }
4661    }
4662}
4663
4664/// Packed brush fields shared by the shape instances (border, ellipse,
4665/// ellipse border, arc). Gradient endpoints are shape-local px; the shaders
4666/// recenter `(0,0)` at the shape top-left. Radial packs `center` into
4667/// `grad_p0` and `radius` into `grad_p1.x`; sweep packs `center` into
4668/// `grad_p0`.
4669///
4670/// `rect` is the shape's scene-space bounds (only used for the solid
4671/// fallback path) and `transform` the accumulated scene transform.
4672/// Endpoints are converted from shape-local px through the inverse linear
4673/// part so rotation and uniform scale cancel against the shader's
4674/// un-rotation. Non-uniform scale and shear distort the gradient the same
4675/// way they distort the shape (the shader un-rotates but cannot un-scale
4676/// pixels).
4677fn brush_to_shape_fields(
4678    brush: &Brush,
4679    _rect: &repose_core::Rect,
4680    transform: &Transform,
4681) -> (u32, u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2], u32) {
4682    let to_local = |p: Vec2| {
4683        let m = transform.linear();
4684        let det = m[0] * m[3] - m[1] * m[2];
4685        if det.abs() < 1e-12 {
4686            return [p.x, p.y];
4687        }
4688        [
4689            (m[3] * p.x - m[1] * p.y) / det,
4690            (-m[2] * p.x + m[0] * p.y) / det,
4691        ]
4692    };
4693    match brush {
4694        Brush::Solid(c) => (
4695            0u32,
4696            0u32,
4697            c.to_linear(),
4698            [0.0; 4],
4699            [0.0; 2],
4700            [0.0; 2],
4701            0u32,
4702        ),
4703        Brush::Linear {
4704            start,
4705            end,
4706            start_color,
4707            end_color,
4708        } => (
4709            1u32,
4710            0u32,
4711            start_color.to_linear(),
4712            end_color.to_linear(),
4713            to_local(*start),
4714            to_local(*end),
4715            0u32,
4716        ),
4717        Brush::Radial {
4718            center,
4719            radius,
4720            start_color,
4721            end_color,
4722        } => (
4723            1u32,
4724            1u32,
4725            start_color.to_linear(),
4726            end_color.to_linear(),
4727            to_local(*center),
4728            [radius.max(0.0), 0.0],
4729            0u32,
4730        ),
4731        Brush::Sweep {
4732            center,
4733            start_color,
4734            end_color,
4735        } => (
4736            1u32,
4737            2u32,
4738            start_color.to_linear(),
4739            end_color.to_linear(),
4740            to_local(*center),
4741            [0.0, 0.0],
4742            0u32,
4743        ),
4744        _ => (0u32, 0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2], 0u32),
4745    }
4746}
4747
4748fn brush_to_instance_fields(brush: &Brush) -> (u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2]) {
4749    match brush {
4750        Brush::Solid(c) => (
4751            0u32,
4752            c.to_linear(),
4753            [0.0, 0.0, 0.0, 0.0],
4754            [0.0, 0.0],
4755            [0.0, 1.0],
4756        ),
4757        Brush::Linear {
4758            start,
4759            end,
4760            start_color,
4761            end_color,
4762        } => (
4763            1u32,
4764            start_color.to_linear(),
4765            end_color.to_linear(),
4766            [start.x, start.y],
4767            [end.x, end.y],
4768        ),
4769        Brush::Radial { start_color, .. } => (
4770            0u32,
4771            start_color.to_linear(),
4772            [0.0, 0.0, 0.0, 0.0],
4773            [0.0, 0.0],
4774            [0.0, 1.0],
4775        ),
4776        Brush::Sweep { start_color, .. } => (
4777            0u32,
4778            start_color.to_linear(),
4779            [0.0, 0.0, 0.0, 0.0],
4780            [0.0, 0.0],
4781            [0.0, 1.0],
4782        ),
4783        _ => (0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
4784    }
4785}
4786
4787/// Fallback color when a [`Brush`] reaches a solid-only path (glyph atlas
4788/// uploads for gradient text). Uses the gradient's start color.
4789#[allow(dead_code)]
4790fn brush_to_solid_color(brush: &Brush) -> [f32; 4] {
4791    match brush {
4792        Brush::Solid(c) => c.to_linear(),
4793        Brush::Linear { start_color, .. } => start_color.to_linear(),
4794        Brush::Radial { start_color, .. } => start_color.to_linear(),
4795        Brush::Sweep { start_color, .. } => start_color.to_linear(),
4796        _ => [0.0; 4],
4797    }
4798}
4799
4800fn init_atlas_mask(device: &wgpu::Device) -> AtlasA8 {
4801    let size = 1024u32;
4802    let tex = device.create_texture(&wgpu::TextureDescriptor {
4803        label: Some("glyph atlas A8"),
4804        size: wgpu::Extent3d {
4805            width: size,
4806            height: size,
4807            depth_or_array_layers: 1,
4808        },
4809        mip_level_count: 1,
4810        sample_count: 1,
4811        dimension: wgpu::TextureDimension::D2,
4812        format: wgpu::TextureFormat::R8Unorm,
4813        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4814        view_formats: &[],
4815    });
4816    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
4817    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
4818        label: Some("glyph atlas sampler A8"),
4819        address_mode_u: wgpu::AddressMode::ClampToEdge,
4820        address_mode_v: wgpu::AddressMode::ClampToEdge,
4821        address_mode_w: wgpu::AddressMode::ClampToEdge,
4822        mag_filter: wgpu::FilterMode::Linear,
4823        min_filter: wgpu::FilterMode::Linear,
4824        mipmap_filter: wgpu::MipmapFilterMode::Linear,
4825        ..Default::default()
4826    });
4827
4828    AtlasA8 {
4829        tex,
4830        view,
4831        sampler,
4832        size,
4833        next_x: 1,
4834        next_y: 1,
4835        row_h: 0,
4836        map: HashMap::new(),
4837    }
4838}
4839
4840fn init_atlas_color(device: &wgpu::Device) -> AtlasRGBA {
4841    let size = 1024u32;
4842    let tex = device.create_texture(&wgpu::TextureDescriptor {
4843        label: Some("glyph atlas RGBA"),
4844        size: wgpu::Extent3d {
4845            width: size,
4846            height: size,
4847            depth_or_array_layers: 1,
4848        },
4849        mip_level_count: 1,
4850        sample_count: 1,
4851        dimension: wgpu::TextureDimension::D2,
4852        format: wgpu::TextureFormat::Rgba8UnormSrgb,
4853        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4854        view_formats: &[],
4855    });
4856    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
4857    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
4858        label: Some("glyph atlas sampler RGBA"),
4859        address_mode_u: wgpu::AddressMode::ClampToEdge,
4860        address_mode_v: wgpu::AddressMode::ClampToEdge,
4861        address_mode_w: wgpu::AddressMode::ClampToEdge,
4862        mag_filter: wgpu::FilterMode::Linear,
4863        min_filter: wgpu::FilterMode::Linear,
4864        mipmap_filter: wgpu::MipmapFilterMode::Linear,
4865        ..Default::default()
4866    });
4867    AtlasRGBA {
4868        tex,
4869        view,
4870        sampler,
4871        size,
4872        next_x: 1,
4873        next_y: 1,
4874        row_h: 0,
4875        map: HashMap::new(),
4876    }
4877}
4878
4879#[cfg(feature = "winit-surface")]
4880impl RenderBackend for WgpuSurfaceBackend {
4881    fn configure_surface(&mut self, width: u32, height: u32) {
4882        if width == 0 || height == 0 {
4883            return;
4884        }
4885        self.renderer.output_width = width;
4886        self.renderer.output_height = height;
4887        if let Some(ref mut config) = self.surface_config {
4888            config.width = width;
4889            config.height = height;
4890        }
4891        if let (Some(surface), Some(config)) = (self.surface.as_ref(), self.surface_config.as_ref())
4892        {
4893            surface.configure(&self.renderer.device, config);
4894        }
4895        self.renderer.recreate_msaa_and_depth_stencil();
4896        self.renderer.recreate_working_space_texture();
4897    }
4898
4899    fn frame(&mut self, scene: &Scene, _glyph_cfg: GlyphRasterConfig) {
4900        let surface = self.surface.as_ref().expect("WgpuSurfaceBackend::frame() requires a surface (use from_device + render_to_view instead)");
4901        let surface_config = self
4902            .surface_config
4903            .as_ref()
4904            .expect("surface_config required for frame()");
4905
4906        self.renderer.frame_index = self.renderer.frame_index.wrapping_add(1);
4907        self.renderer.slug_cache.next_frame();
4908
4909        if self.renderer.output_width == 0 || self.renderer.output_height == 0 {
4910            return;
4911        }
4912
4913        let mut retries = 0u32;
4914        const MAX_RETRIES: u32 = 4;
4915        let frame = loop {
4916            match surface.get_current_texture() {
4917                wgpu::CurrentSurfaceTexture::Success(f) => break f,
4918                wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
4919                    log::warn!("suboptimal surface; reconfiguring");
4920                    surface.configure(&self.renderer.device, surface_config);
4921                    break f;
4922                }
4923                wgpu::CurrentSurfaceTexture::Outdated => {
4924                    retries += 1;
4925                    if retries >= MAX_RETRIES {
4926                        log::warn!(
4927                            "surface outdated persisted after {MAX_RETRIES} retries; skipping frame"
4928                        );
4929                        return;
4930                    }
4931                    log::warn!("surface outdated; reconfiguring");
4932                    surface.configure(&self.renderer.device, surface_config);
4933                }
4934                wgpu::CurrentSurfaceTexture::Lost => {
4935                    retries += 1;
4936                    if retries >= MAX_RETRIES {
4937                        log::warn!(
4938                            "surface lost persisted after {MAX_RETRIES} retries; skipping frame"
4939                        );
4940                        return;
4941                    }
4942                    log::warn!("surface lost; reconfiguring");
4943                    surface.configure(&self.renderer.device, surface_config);
4944                }
4945                wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
4946                    request_frame();
4947                    return;
4948                }
4949                wgpu::CurrentSurfaceTexture::Validation => {
4950                    retries += 1;
4951                    if retries >= MAX_RETRIES {
4952                        log::warn!(
4953                            "surface validation persisted after {MAX_RETRIES} retries; skipping frame"
4954                        );
4955                        return;
4956                    }
4957                    surface.configure(&self.renderer.device, surface_config);
4958                }
4959            }
4960        };
4961
4962        let swap_view = if let Some(view_format) = self
4963            .surface_config
4964            .as_ref()
4965            .and_then(|c| c.view_formats.iter().find(|f| f.is_srgb()).copied())
4966        {
4967            frame.texture.create_view(&wgpu::TextureViewDescriptor {
4968                format: Some(view_format),
4969                ..Default::default()
4970            })
4971        } else {
4972            frame
4973                .texture
4974                .create_view(&wgpu::TextureViewDescriptor::default())
4975        };
4976        let mut encoder =
4977            self.renderer
4978                .device
4979                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4980                    label: Some("frame encoder"),
4981                });
4982
4983        let clear_color = Some([
4984            scene.clear_color.0 as f64 / 255.0,
4985            scene.clear_color.1 as f64 / 255.0,
4986            scene.clear_color.2 as f64 / 255.0,
4987            scene.clear_color.3 as f64 / 255.0,
4988        ]);
4989
4990        self.renderer
4991            .render_scene_to_encoder(scene, &mut encoder, &swap_view, clear_color);
4992
4993        //NOTE: The WebGL HAL present path (fullscreen triangle / blit) does not
4994        // restore gl.colorMask. Hence this is needed to prevent frames from going transparent.
4995        {
4996            let _reset = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
4997                label: Some("webgl color_mask reset before present"),
4998                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
4999                    view: &swap_view,
5000                    resolve_target: None,
5001                    ops: wgpu::Operations {
5002                        load: wgpu::LoadOp::Load,
5003                        store: wgpu::StoreOp::Store,
5004                    },
5005                    depth_slice: None,
5006                })],
5007                depth_stencil_attachment: None,
5008                timestamp_writes: None,
5009                occlusion_query_set: None,
5010                multiview_mask: None,
5011            });
5012        }
5013
5014        self.renderer
5015            .queue
5016            .submit(std::iter::once(encoder.finish()));
5017        if let Err(e) = catch_unwind(AssertUnwindSafe(|| self.renderer.queue.present(frame))) {
5018            log::warn!("queue.present panicked: {:?}", e);
5019        }
5020    }
5021}
5022
5023impl WgpuSceneRenderer {
5024    /// Open a translator-owned flatten layer for a perspective `PushTransform`.
5025    ///
5026    /// True perspective cannot ride the affine instance fast path, so the
5027    /// subtree renders flat into an offscreen layer and is composited back
5028    /// projectively on the matching pop (CSS-style flattening). The layer
5029    /// rect is the currently visible scissor in this target: content outside
5030    /// it is invisible in the parent, so clipping it in the layer changes
5031    /// nothing. Children keep the node's affine part on the stack (so
5032    /// `combine` stays affine-only) plus a layer-local shift, exactly like
5033    /// producer-owned blur layers — which is exact under rigid ancestors
5034    /// (translations commute) and the documented layer contract otherwise.
5035    #[allow(clippy::too_many_arguments)]
5036    fn push_perspective_layer(
5037        &mut self,
5038        node: Transform,
5039        top: Transform,
5040        transform_stack: &mut Vec<Transform>,
5041        scissor_stack: &mut Vec<repose_core::Rect>,
5042        root_clip_rect: &mut repose_core::Rect,
5043        current_target_size: &mut (f32, f32),
5044        current_pass: &mut Pass,
5045        passes: &mut Vec<Pass>,
5046        target_stack: &mut Vec<PassTarget>,
5047        flatten_stack: &mut Vec<FlattenRecord>,
5048        id_head: &mut u32,
5049        ids_used: &mut Vec<u32>,
5050    ) {
5051        // Full projective map: affine ancestors over the node's map.
5052        // Ancestors are affine by construction (perspective always flattens
5053        // at push, and only stripped affines reach the stack).
5054        let map =
5055            Transform::compose_projective(&top.projective_matrix(), &node.projective_matrix());
5056        let scr = scissor_stack.last().copied().unwrap_or(*root_clip_rect);
5057        let w = scr.w.ceil().max(1.0);
5058        let h = scr.h.ceil().max(1.0);
5059        let layer_rect = repose_core::Rect {
5060            x: scr.x,
5061            y: scr.y,
5062            w,
5063            h,
5064        };
5065        // Translator-owned ids live far above producer ids (which start at 1
5066        // per scene) and are drained from the pool after each frame.
5067        let layer_id = *id_head;
5068        *id_head = id_head.wrapping_add(1);
5069        ids_used.push(layer_id);
5070
5071        let stack_len = transform_stack.len();
5072        // Children render with the ancestors' map only: the node's own
5073        // affine part lives in `map` and applies once, at composite time.
5074        // (Pushing the stripped affine here too would foreshorten twice.)
5075        transform_stack.push(top);
5076        transform_stack.push(Transform::translate(-layer_rect.x, -layer_rect.y));
5077
5078        let saved_scissor = std::mem::replace(
5079            scissor_stack,
5080            vec![repose_core::Rect {
5081                x: 0.0,
5082                y: 0.0,
5083                w,
5084                h,
5085            }],
5086        );
5087        let saved_root = std::mem::replace(
5088            root_clip_rect,
5089            repose_core::Rect {
5090                x: 0.0,
5091                y: 0.0,
5092                w,
5093                h,
5094            },
5095        );
5096        let saved_size = std::mem::replace(current_target_size, (w, h));
5097        let prev_target = current_pass.target;
5098        let saved = std::mem::replace(
5099            current_pass,
5100            Pass {
5101                target: PassTarget::Layer(layer_id),
5102                initial_scissor: (0, 0, w as u32, h as u32),
5103                clear_color: Some([0.0, 0.0, 0.0, 0.0]),
5104                cmds: Vec::new(),
5105            },
5106        );
5107        passes.push(saved);
5108        target_stack.push(prev_target);
5109        self.get_or_create_layer(layer_id, w as u32, h as u32, layer_rect);
5110        *current_target_size = (w, h);
5111        flatten_stack.push(FlattenRecord {
5112            stack_len,
5113            layer_id,
5114            map,
5115            layer_rect,
5116            saved_scissor,
5117            saved_root,
5118            saved_size,
5119        });
5120    }
5121
5122    /// Close a flatten layer: restore the parent target and composite the
5123    /// layer texture through the recorded projective map.
5124    #[allow(clippy::too_many_arguments)]
5125    fn pop_perspective_layer(
5126        &mut self,
5127        rec: FlattenRecord,
5128        scissor_stack: &mut Vec<repose_core::Rect>,
5129        root_clip_rect: &mut repose_core::Rect,
5130        current_target_size: &mut (f32, f32),
5131        current_pass: &mut Pass,
5132        passes: &mut Vec<Pass>,
5133        target_stack: &mut Vec<PassTarget>,
5134    ) {
5135        *scissor_stack = rec.saved_scissor;
5136        *root_clip_rect = rec.saved_root;
5137        *current_target_size = rec.saved_size;
5138        let saved = std::mem::replace(
5139            current_pass,
5140            Pass {
5141                target: target_stack.pop().unwrap_or(PassTarget::Surface),
5142                initial_scissor: (0, 0, self.output_width, self.output_height),
5143                clear_color: None,
5144                cmds: Vec::new(),
5145            },
5146        );
5147        passes.push(saved);
5148
5149        // Project the layer-rect corners (parent space) to NDC in the
5150        // resumed (parent) target, keeping each corner's homogeneous w for
5151        // perspective-correct sampling.
5152        let (tw, th) = rec.saved_size;
5153        let r = rec.layer_rect;
5154        let corners = [
5155            (r.x, r.y),
5156            (r.x + r.w, r.y),
5157            (r.x + r.w, r.y + r.h),
5158            (r.x, r.y + r.h),
5159        ];
5160        let mut ndc = [[0.0f32; 2]; 4];
5161        let mut ws = [1.0f32; 4];
5162        let mut all_behind = true;
5163        for (i, (x, y)) in corners.iter().enumerate() {
5164            let w_raw = rec.map[6] * x + rec.map[7] * y + rec.map[8];
5165            let w = if w_raw.abs() < 1e-6 {
5166                if w_raw < 0.0 { -1e-6 } else { 1e-6 }
5167            } else {
5168                w_raw
5169            };
5170            if w > 0.0 {
5171                all_behind = false;
5172            }
5173            let px = (rec.map[0] * x + rec.map[1] * y + rec.map[2]) / w;
5174            let py = (rec.map[3] * x + rec.map[4] * y + rec.map[5]) / w;
5175            ndc[i] = [px / tw * 2.0 - 1.0, 1.0 - py / th * 2.0];
5176            ws[i] = w;
5177        }
5178        if all_behind {
5179            // Entire subtree behind the viewer: nothing to composite (the
5180            // layer pass still ran, but its output is correctly discarded).
5181            return;
5182        }
5183        let layer = self.layer_pool.get(&rec.layer_id).expect("flatten layer");
5184        let uv_u1 = layer.rect_px.2 / layer.width.max(1) as f32;
5185        let uv_v1 = layer.rect_px.3 / layer.height.max(1) as f32;
5186        let inst = ProjectiveInstance {
5187            c0: ndc[0],
5188            c1: ndc[1],
5189            c2: ndc[2],
5190            c3: ndc[3],
5191            uv: [0.0, 0.0, uv_u1, uv_v1],
5192            w: ws,
5193            alpha: 1.0,
5194            _pad: [0.0; 3],
5195        };
5196        self.projective_ring.grow_to_fit(
5197            &self.device,
5198            std::mem::size_of::<ProjectiveInstance>() as u64,
5199        );
5200        let bytes = bytemuck::bytes_of(&inst);
5201        let (off, _) = self.projective_ring.alloc_write(&self.queue, bytes);
5202        current_pass.cmds.push(Cmd::CompositeProjective {
5203            off,
5204            cnt: 1,
5205            layer_id: rec.layer_id,
5206        });
5207    }
5208
5209    fn upload_mesh_geometry(&mut self, mesh: &repose_core::VectorMeshData) -> (u64, u32, u64, u32) {
5210        let verts: Vec<MeshVertex> = mesh
5211            .vertices
5212            .iter()
5213            .map(|v| MeshVertex {
5214                pos: v.pos,
5215                color: v.color,
5216                uv: v.uv,
5217            })
5218            .collect();
5219        let vbytes = bytemuck::cast_slice(&verts);
5220        self.mesh_verts
5221            .grow_to_fit(&self.device, vbytes.len() as u64);
5222        let (voff, _) = self.mesh_verts.alloc_write(&self.queue, vbytes);
5223        let ibytes = bytemuck::cast_slice(&mesh.indices);
5224        self.mesh_indices
5225            .grow_to_fit(&self.device, ibytes.len() as u64);
5226        let (ioff, _) = self.mesh_indices.alloc_write(&self.queue, ibytes);
5227        (voff, verts.len() as u32, ioff, mesh.indices.len() as u32)
5228    }
5229
5230    fn alloc_mesh_uniform(&mut self, u: MeshUniform) -> u64 {
5231        if self.mesh_uniform_head + MESH_UNIFORM_SLOT > MESH_UNIFORM_CAP {
5232            log::warn!("mesh uniform buffer overflow; regenerating");
5233            self.recreate_mesh_uniform_buffer();
5234        }
5235        let slot = self.mesh_uniform_head;
5236        self.queue
5237            .write_buffer(&self.mesh_uniform_buf, slot, bytemuck::bytes_of(&u));
5238        self.mesh_uniform_head = slot + MESH_UNIFORM_SLOT;
5239        slot
5240    }
5241
5242    fn recreate_mesh_uniform_buffer(&mut self) {
5243        let new_cap = self.mesh_uniform_head + MESH_UNIFORM_SLOT;
5244        self.mesh_uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
5245            label: Some("mesh uniform buffer"),
5246            size: new_cap,
5247            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
5248            mapped_at_creation: false,
5249        });
5250        self.mesh_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5251            label: Some("mesh uniform bind"),
5252            layout: &self.mesh_bind_layout,
5253            entries: &[wgpu::BindGroupEntry {
5254                binding: 0,
5255                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
5256                    buffer: &self.mesh_uniform_buf,
5257                    offset: 0,
5258                    size: NonZero::new(MESH_UNIFORM_SLOT),
5259                }),
5260            }],
5261        });
5262        self.mesh_uniform_head = 0;
5263    }
5264
5265    /// Render one backdrop-dependent blend mesh: isolate the mesh into a
5266    /// translator-owned graphics layer, then composite it over the current
5267    /// target with the backdrop-blend shader. Works for surface parents and
5268    /// layer parents alike: the snapshot copy, isolation layer, and
5269    /// composite quad are all expressed in the parent target's pixel space.
5270    /// Callers must invoke this while the current pass targets the recorded
5271    /// parent: the translator never splits passes between here and the
5272    /// appended composite (only `BeginLayer`/perspective push new passes,
5273    /// and neither can intervene mid-call). The executor re-checks this
5274    /// (`BlendLayer.parent`) and skips a misplaced composite rather than
5275    /// drawing over the wrong target.
5276    #[allow(clippy::too_many_arguments)]
5277    fn emit_isolated_blend(
5278        &mut self,
5279        mesh: std::sync::Arc<repose_core::VectorMeshData>,
5280        transform: [f32; 6],
5281        paint: repose_core::PaintDesc,
5282        blend: repose_core::BlendMode,
5283        current_transform: &repose_core::Transform,
5284        current_pass: &mut Pass,
5285        passes: &mut Vec<Pass>,
5286        target_stack: &mut Vec<PassTarget>,
5287        id_head: &mut u32,
5288        ids_used: &mut Vec<u32>,
5289        current_target_size: &mut (f32, f32),
5290        fb_w: f32,
5291        fb_h: f32,
5292    ) {
5293        let parent_target = target_stack.last().copied().unwrap_or(PassTarget::Surface);
5294        // Layer parents store surface-positioned rects (`rect_px`) with
5295        // layer-local content, so a surface-space aabb maps into the layer
5296        // by subtracting the layer origin. Surface parents are identity.
5297        let parent_origin = match parent_target {
5298            PassTarget::Surface => (0.0, 0.0),
5299            PassTarget::Layer(id) => match self.layer_pool.get(&id) {
5300                Some(lt) => (lt.rect_px.0, lt.rect_px.1),
5301                None => (0.0, 0.0),
5302            },
5303        };
5304        let affine = combine_mesh_affine(current_transform, transform);
5305        let aabb = mesh_aabb(&mesh, affine);
5306        if aabb.w <= 0.0 || aabb.h <= 0.0 {
5307            return;
5308        }
5309        // Surface-space bbox -> parent-target pixels.
5310        let local_rect = repose_core::Rect {
5311            x: aabb.x - parent_origin.0,
5312            y: aabb.y - parent_origin.1,
5313            w: aabb.w,
5314            h: aabb.h,
5315        };
5316        let w = local_rect.w.ceil().max(1.0);
5317        let h = local_rect.h.ceil().max(1.0);
5318        let layer_rect = repose_core::Rect {
5319            x: local_rect.x,
5320            y: local_rect.y,
5321            w,
5322            h,
5323        };
5324        let layer_id = *id_head;
5325        *id_head = id_head.wrapping_add(1);
5326        ids_used.push(layer_id);
5327
5328        // Snapshot the parent target region before compositing: the blend
5329        // shader samples it as the backdrop. The copy runs at execution
5330        // time, after all earlier passes have rendered.
5331        self.alloc_blend_snapshot(layer_id, w as u32, h as u32);
5332        self.blend_copies
5333            .push((layer_id, parent_target, layer_rect));
5334
5335        // Render the mesh alone into the layer (layer-local shift so the
5336        // layer owns exactly the mesh bbox), then resume the parent pass.
5337        // The composite runs in the resumed parent pass.
5338        let mut layer_cmds = Vec::new();
5339        self.get_or_create_layer(layer_id, w as u32, h as u32, layer_rect);
5340        let shift = repose_core::Transform::translate(-local_rect.x, -local_rect.y);
5341        let local = current_transform.combine(&shift);
5342        self.emit_vector_mesh(
5343            &local,
5344            &mesh,
5345            transform,
5346            &paint,
5347            repose_core::BlendMode::Alpha,
5348            &mut layer_cmds,
5349        );
5350        let saved = std::mem::replace(
5351            current_pass,
5352            Pass {
5353                target: parent_target,
5354                initial_scissor: (0, 0, self.output_width, self.output_height),
5355                clear_color: None,
5356                cmds: Vec::new(),
5357            },
5358        );
5359        passes.push(Pass {
5360            target: PassTarget::Layer(layer_id),
5361            initial_scissor: (0, 0, w as u32, h as u32),
5362            clear_color: Some([0.0, 0.0, 0.0, 0.0]),
5363            cmds: layer_cmds,
5364        });
5365        passes.push(saved);
5366        *current_target_size = (fb_w, fb_h);
5367
5368        // Composite quad over the mesh bbox in the parent target's pixel
5369        // space. The executor maps this NDC against `parent`, so layer
5370        // parents at any origin work.
5371        let (parent_w, parent_h) = match parent_target {
5372            PassTarget::Surface => (fb_w, fb_h),
5373            PassTarget::Layer(id) => match self.layer_pool.get(&id) {
5374                Some(lt) => (lt.width as f32, lt.height as f32),
5375                None => (fb_w, fb_h),
5376            },
5377        };
5378        let ndc = {
5379            let cx = local_rect.x + local_rect.w * 0.5;
5380            let cy = local_rect.y + local_rect.h * 0.5;
5381            let ndc_cx = (cx / parent_w) * 2.0 - 1.0;
5382            let ndc_cy = 1.0 - (cy / parent_h) * 2.0;
5383            let ndc_w = (local_rect.w / parent_w) * 2.0;
5384            let ndc_h = (local_rect.h / parent_h) * 2.0;
5385            [ndc_cx, ndc_cy, ndc_w, ndc_h]
5386        };
5387        let inst = BlendInstance {
5388            xywh: ndc,
5389            uv: [0.0, 0.0, 1.0, 1.0],
5390            color: [1.0, 1.0, 1.0, 1.0],
5391            fwd_mat: [1.0, 0.0, 0.0, 1.0],
5392            mode: blend.shader_mode(),
5393            _pad: [0.0; 3],
5394        };
5395        self.blend_ring.grow_to_fit(
5396            &self.device,
5397            std::mem::size_of::<BlendInstance>() as u64,
5398        );
5399        let bytes = bytemuck::bytes_of(&inst);
5400        let (off, _) = self.blend_ring.alloc_write(&self.queue, bytes);
5401        current_pass.cmds.push(Cmd::BlendLayer {
5402            off,
5403            cnt: 1,
5404            src_layer: layer_id,
5405            dst_layer: Some(layer_id),
5406            parent: parent_target,
5407        });
5408    }
5409
5410    /// Allocate (or reuse) the backdrop snapshot texture for an isolated
5411    /// blend layer.
5412    fn alloc_blend_snapshot(&mut self, layer_id: u32, w: u32, h: u32) {
5413        let reuse = self
5414            .blend_snapshots
5415            .get(&layer_id)
5416            .is_some_and(|s| s.width == w && s.height == h);
5417        if reuse {
5418            return;
5419        }
5420        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5421            label: Some("blend backdrop snapshot"),
5422            size: wgpu::Extent3d {
5423                width: w.max(1),
5424                height: h.max(1),
5425                depth_or_array_layers: 1,
5426            },
5427            mip_level_count: 1,
5428            sample_count: 1,
5429            dimension: wgpu::TextureDimension::D2,
5430            format: self.output_format,
5431            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
5432            view_formats: &[],
5433        });
5434        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5435        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5436            label: Some("blend snapshot bind"),
5437            layout: &self.image_bind_layout_rgba,
5438            entries: &[
5439                wgpu::BindGroupEntry {
5440                    binding: 0,
5441                    resource: wgpu::BindingResource::TextureView(&view),
5442                },
5443                wgpu::BindGroupEntry {
5444                    binding: 1,
5445                    resource: wgpu::BindingResource::Sampler(&self.layer_sampler),
5446                },
5447            ],
5448        });
5449        let _ = view;
5450        self.blend_snapshots.insert(
5451            layer_id,
5452            BlendSnapshot {
5453                texture: tex,
5454                bind,
5455                width: w,
5456                height: h,
5457            },
5458        );
5459    }
5460
5461    fn blend_snapshot_texture(&self, layer_id: u32) -> Option<wgpu::Texture> {
5462        self.blend_snapshots.get(&layer_id).map(|s| s.texture.clone())
5463    }
5464
5465    #[allow(clippy::too_many_arguments)]
5466    fn emit_vector_mesh(
5467        &mut self,
5468        current_transform: &Transform,
5469        mesh: &repose_core::VectorMeshData,
5470        transform: [f32; 6],
5471        paint: &repose_core::PaintDesc,
5472        blend: repose_core::BlendMode,
5473        cmds: &mut Vec<Cmd>,
5474    ) {
5475        let affine = combine_mesh_affine(current_transform, transform);
5476        let (voff, vcnt, ioff, icnt) = self.upload_mesh_geometry(mesh);
5477        let uoff = self.alloc_mesh_uniform(mesh_uniform_from_paint(affine, paint));
5478        cmds.push(Cmd::VectorMesh {
5479            voff,
5480            vcnt,
5481            ioff,
5482            icnt,
5483            uoff,
5484            blend,
5485        });
5486    }
5487
5488    pub fn render_scene_to_encoder(
5489        &mut self,
5490        scene: &Scene,
5491        encoder: &mut wgpu::CommandEncoder,
5492        target_view: &wgpu::TextureView,
5493        clear_color_override: Option<[f64; 4]>,
5494    ) {
5495        self.render_scene_to_encoder_with_texture(
5496            scene,
5497            encoder,
5498            target_view,
5499            None,
5500            clear_color_override,
5501        )
5502    }
5503
5504    /// Same as [`render_scene_to_encoder`](Self::render_scene_to_encoder),
5505    /// plus the render target texture for isolated-blend backdrop snapshots.
5506    /// Pass `None` when the texture is unavailable (swapchain views); then
5507    /// surface-targeted isolated blends are skipped (the isolated source
5508    /// layer composites nowhere, so the mesh disappears).
5509    pub fn render_scene_to_encoder_with_texture(
5510        &mut self,
5511        scene: &Scene,
5512        encoder: &mut wgpu::CommandEncoder,
5513        target_view: &wgpu::TextureView,
5514        target_texture: Option<&wgpu::Texture>,
5515        clear_color_override: Option<[f64; 4]>,
5516    ) {
5517        /// AABB of a rect under the *plain affine* part of a transform
5518        /// (linear + translation, no origin re-pivot).
5519        fn affine_aabb(transform: &Transform, rect: &repose_core::Rect) -> repose_core::Rect {
5520            let m = transform.linear();
5521            let (tx, ty) = (transform.translate_x, transform.translate_y);
5522            let corners = [
5523                (rect.x, rect.y),
5524                (rect.x + rect.w, rect.y),
5525                (rect.x, rect.y + rect.h),
5526                (rect.x + rect.w, rect.y + rect.h),
5527            ];
5528            let mut min_x = f32::MAX;
5529            let mut min_y = f32::MAX;
5530            let mut max_x = f32::MIN;
5531            let mut max_y = f32::MIN;
5532            for (x, y) in corners {
5533                let wx = m[0] * x + m[1] * y + tx;
5534                let wy = m[2] * x + m[3] * y + ty;
5535                min_x = min_x.min(wx);
5536                min_y = min_y.min(wy);
5537                max_x = max_x.max(wx);
5538                max_y = max_y.max(wy);
5539            }
5540            repose_core::Rect {
5541                x: min_x,
5542                y: min_y,
5543                w: (max_x - min_x).max(0.0),
5544                h: (max_y - min_y).max(0.0),
5545            }
5546        }
5547
5548        fn to_ndc(x: f32, y: f32, w: f32, h: f32, fb_w: f32, fb_h: f32) -> [f32; 4] {
5549            let x0 = (x / fb_w) * 2.0 - 1.0;
5550            let y0 = 1.0 - (y / fb_h) * 2.0;
5551            let x1 = ((x + w) / fb_w) * 2.0 - 1.0;
5552            let y1 = 1.0 - ((y + h) / fb_h) * 2.0;
5553            let min_x = x0.min(x1);
5554            let min_y = y0.min(y1);
5555            let w_ndc = (x1 - x0).abs();
5556            let h_ndc = (y1 - y0).abs();
5557            [min_x, min_y, w_ndc, h_ndc]
5558        }
5559
5560        /// Convert a local-space rect + transform to NDC center-based position+size
5561        /// plus the forward rotation/shear 2x2 (row-major `[m00, m01, m10, m11]`,
5562        /// scale-free: scale rides in the NDC size). Shaders apply it to quad
5563        /// corners and its adjugate/determinant inverse to sample positions.
5564        fn rect_to_instance_ndc(
5565            rect: repose_core::Rect,
5566            transform: &Transform,
5567            fb_w: f32,
5568            fb_h: f32,
5569        ) -> ([f32; 4], [f32; 4]) {
5570            let cx = rect.x + rect.w * 0.5;
5571            let cy = rect.y + rect.h * 0.5;
5572
5573            let m = transform.linear();
5574            let tx = m[0] * cx + m[1] * cy + transform.translate_x;
5575            let ty = m[2] * cx + m[3] * cy + transform.translate_y;
5576
5577            let ndc_cx = (tx / fb_w) * 2.0 - 1.0;
5578            let ndc_cy = 1.0 - (ty / fb_h) * 2.0;
5579            // NDC size (after scale only, no rotation - rotation is done in shader)
5580            let ndc_w = (rect.w * transform.scale_x / fb_w) * 2.0;
5581            let ndc_h = (rect.h * transform.scale_y / fb_h) * 2.0;
5582
5583            ([ndc_cx, ndc_cy, ndc_w, ndc_h], forward_rs_mat(transform))
5584        }
5585
5586        /// Forward rotation+shear 2x2 (row-major, scale-free) for instance
5587        /// attributes. Identity for untransformed content; degenerate shear
5588        /// (only from absurd inputs) falls back to identity.
5589        fn forward_rs_mat(transform: &Transform) -> [f32; 4] {
5590            let c = transform.rotate.cos();
5591            let s = transform.rotate.sin();
5592            let (hx, hy) = (transform.shear_x, transform.shear_y);
5593            let m = [c - s * hy, c * hx - s, s + c * hy, s * hx + c];
5594            if (m[0] * m[3] - m[1] * m[2]).abs() < 1e-6 {
5595                return [1.0, 0.0, 0.0, 1.0];
5596            }
5597            m
5598        }
5599
5600        fn to_scissor(r: &repose_core::Rect, fb_w: u32, fb_h: u32) -> (u32, u32, u32, u32) {
5601            let mut x = r.x.floor() as i64;
5602            let mut y = r.y.floor() as i64;
5603            let fb_wi = fb_w as i64;
5604            let fb_hi = fb_h as i64;
5605            x = x.clamp(0, fb_wi.saturating_sub(1));
5606            y = y.clamp(0, fb_hi.saturating_sub(1));
5607            let w_req = r.w.ceil().max(1.0) as i64;
5608            let h_req = r.h.ceil().max(1.0) as i64;
5609            let w = (w_req).min(fb_wi - x).max(1);
5610            let h = (h_req).min(fb_hi - y).max(1);
5611            (x as u32, y as u32, w as u32, h as u32)
5612        }
5613
5614        let fb_w = self.output_width as f32;
5615        let fb_h = self.output_height as f32;
5616
5617        let mut passes: Vec<Pass> = Vec::with_capacity(1);
5618        let clear_color = clear_color_override.unwrap_or_else(|| {
5619            // Scene clear colors are sRGB bytes like every other `Color`;
5620            // linearize so the sRGB target re-encodes them exactly (passing
5621            // raw bytes double-encoded: (10,20,30) read back (56,79,96)).
5622            let lin = scene.clear_color.to_linear();
5623            [lin[0] as f64, lin[1] as f64, lin[2] as f64, lin[3] as f64]
5624        });
5625        let mut current_pass: Pass = Pass {
5626            target: PassTarget::Surface,
5627            initial_scissor: (0, 0, self.output_width, self.output_height),
5628            clear_color: Some([
5629                clear_color[0] as f32,
5630                clear_color[1] as f32,
5631                clear_color[2] as f32,
5632                clear_color[3] as f32,
5633            ]),
5634            cmds: Vec::with_capacity(scene.nodes.len()),
5635        };
5636        let mut target_stack: Vec<PassTarget> = Vec::new();
5637        let mut layer_alphas: Vec<(u32, f32, (u32, u32, u32, u32))> = Vec::new();
5638        let mut layer_blurs: Vec<(u32, f32, f32)> = Vec::new();
5639        let mut current_target_size: (f32, f32) = (fb_w, fb_h);
5640
5641        struct Batch {
5642            rects: Vec<RectInstance>,
5643            borders: Vec<BorderInstance>,
5644            ellipses: Vec<EllipseInstance>,
5645            e_borders: Vec<EllipseBorderInstance>,
5646            arcs: Vec<ArcInstance>,
5647            masks: Vec<GlyphInstance>,
5648            colors: Vec<GlyphInstance>,
5649            nv12s: Vec<Nv12Instance>,
5650        }
5651
5652        impl Batch {
5653            fn new() -> Self {
5654                Self {
5655                    rects: vec![],
5656                    borders: vec![],
5657                    ellipses: vec![],
5658                    e_borders: vec![],
5659                    arcs: vec![],
5660                    masks: vec![],
5661                    colors: vec![],
5662                    nv12s: vec![],
5663                }
5664            }
5665
5666            fn is_empty(&self) -> bool {
5667                self.rects.is_empty()
5668                    && self.borders.is_empty()
5669                    && self.ellipses.is_empty()
5670                    && self.e_borders.is_empty()
5671                    && self.arcs.is_empty()
5672                    && self.masks.is_empty()
5673                    && self.colors.is_empty()
5674                    && self.nv12s.is_empty()
5675            }
5676
5677            fn flush(
5678                &mut self,
5679                pipes: (
5680                    &mut InstancedPipe<RectInstance>,
5681                    &mut InstancedPipe<BorderInstance>,
5682                    &mut InstancedPipe<EllipseInstance>,
5683                    &mut InstancedPipe<EllipseBorderInstance>,
5684                    &mut InstancedPipe<ArcInstance>,
5685                ),
5686                glyph_pipes: (
5687                    &mut InstancedPipe<GlyphInstance>,
5688                    &mut InstancedPipe<GlyphInstance>,
5689                ),
5690                nv12_pipe: &mut InstancedPipe<Nv12Instance>,
5691                device: &wgpu::Device,
5692                queue: &wgpu::Queue,
5693                cmds: &mut Vec<Cmd>,
5694            ) {
5695                let (rects, borders, ellipses, e_borders, arcs) = pipes;
5696                let (masks, colors) = glyph_pipes;
5697
5698                macro_rules! flush_one {
5699                    ($buf:ident, $pipe:expr, $variant:ident) => {
5700                        if !self.$buf.is_empty() {
5701                            if let Some((off, cnt)) = $pipe.upload(device, queue, &self.$buf) {
5702                                cmds.push(Cmd::$variant { off, cnt });
5703                            }
5704                            self.$buf.clear();
5705                        }
5706                    };
5707                }
5708
5709                flush_one!(rects, rects, Rect);
5710                flush_one!(borders, borders, Border);
5711                flush_one!(ellipses, ellipses, Ellipse);
5712                flush_one!(e_borders, e_borders, EllipseBorder);
5713                flush_one!(arcs, arcs, Arc);
5714                flush_one!(masks, masks, GlyphsMask);
5715                flush_one!(colors, colors, GlyphsColor);
5716
5717                if !self.nv12s.is_empty() {
5718                    if let Some((off, cnt)) = nv12_pipe.upload(device, queue, &self.nv12s) {
5719                        let _ = (off, cnt);
5720                    }
5721                    self.nv12s.clear();
5722                }
5723            }
5724        }
5725
5726        self.rects.reset();
5727        self.borders.reset();
5728        self.ellipses.reset();
5729        self.ellipse_borders.reset();
5730        self.arcs.reset();
5731        self.glyph_mask.reset();
5732        self.glyph_color.reset();
5733        self.clip_ring.reset();
5734        self.blur_ring.reset();
5735        self.nv12.reset();
5736
5737        self.slug_ring.reset();
5738        self.mesh_verts.reset();
5739        self.mesh_indices.reset();
5740        self.mesh_uniform_head = 0;
5741        self.mesh_clip_stack.clear();
5742        self.projective_ring.reset();
5743        self.blend_ring.reset();
5744        // Translator-owned flatten layers are single-frame by construction:
5745        // drop last frame's textures before translating (their composites
5746        // were submitted last frame, so GPU-side refs are independent).
5747        for id in self.flatten_layer_ids.drain(..) {
5748            self.layer_pool.remove(&id);
5749        }
5750        self.blend_snapshots.clear();
5751        self.blend_copies.clear();
5752        let mut batch = Batch::new();
5753        let mut slug_verts_local: Vec<slug::TessVertex> = Vec::new();
5754        let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
5755        let mut flatten_stack: Vec<FlattenRecord> = Vec::new();
5756        let mut flatten_id_head: u32 = FLATTEN_ID_BASE;
5757        let mut flatten_ids_used: Vec<u32> = Vec::new();
5758        let mut scissor_stack: Vec<repose_core::Rect> = Vec::with_capacity(8);
5759        // NOTE: Records the clip instance range + flags of each active rounded-rect clip
5760        // so PopClip can re-stamp the stencil with a decrement pass (mirroring
5761        // VectorClipPop). Keys: (off, cnt, difference, rounded).
5762        let mut clip_cmd_stack: Vec<(u64, u32, bool)> = Vec::with_capacity(8);
5763        let mut root_clip_rect = repose_core::Rect {
5764            x: 0.0,
5765            y: 0.0,
5766            w: fb_w,
5767            h: fb_h,
5768        };
5769        let mut saved_scissor_stack: Vec<repose_core::Rect> = Vec::new();
5770        let mut saved_root_clip_rect = root_clip_rect;
5771
5772        let mut current_prim: Option<&'static str> = None;
5773
5774        macro_rules! flush_if_prim_changed {
5775            ($prim:literal, $pipe:expr) => {
5776                if current_prim != Some($prim) {
5777                    flush_batch!();
5778                    current_prim = Some($prim);
5779                }
5780            };
5781        }
5782
5783        macro_rules! flush_batch {
5784            () => {
5785                if !batch.is_empty() {
5786                    batch.flush(
5787                        (
5788                            &mut self.rects,
5789                            &mut self.borders,
5790                            &mut self.ellipses,
5791                            &mut self.ellipse_borders,
5792                            &mut self.arcs,
5793                        ),
5794                        (&mut self.glyph_mask, &mut self.glyph_color),
5795                        &mut self.nv12,
5796                        &self.device,
5797                        &self.queue,
5798                        &mut current_pass.cmds,
5799                    )
5800                }
5801            };
5802        }
5803        for node in &scene.nodes {
5804            let t_identity = Transform::identity();
5805            let current_transform = transform_stack.last().unwrap_or(&t_identity);
5806
5807            match node {
5808                SceneNode::Rect {
5809                    rect,
5810                    brush,
5811                    radius,
5812                } => {
5813                    flush_if_prim_changed!("rect", &self.rects);
5814                    let (ndc, fwd_mat) = rect_to_instance_ndc(
5815                        *rect,
5816                        current_transform,
5817                        current_target_size.0,
5818                        current_target_size.1,
5819                    );
5820                    let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
5821                        brush_to_shape_fields(brush, rect, current_transform);
5822                    batch.rects.push(RectInstance {
5823                        xywh: ndc,
5824                        radii: radius.map(|r| r.0),
5825                        brush_type,
5826                        grad_kind,
5827                        _pad: [0.0; 2],
5828                        color0,
5829                        color1,
5830                        grad_p0,
5831                        grad_p1,
5832                        tile_mode,
5833                        _pad2: [0.0; 3],
5834                        fwd_mat,
5835                    });
5836                }
5837                SceneNode::Border {
5838                    rect,
5839                    brush,
5840                    width,
5841                    radius,
5842                } => {
5843                    flush_if_prim_changed!("border", &self.borders);
5844                    let (ndc, fwd_mat) = rect_to_instance_ndc(
5845                        *rect,
5846                        current_transform,
5847                        current_target_size.0,
5848                        current_target_size.1,
5849                    );
5850                    let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
5851                        brush_to_shape_fields(brush, rect, current_transform);
5852                    batch.borders.push(BorderInstance {
5853                        xywh: ndc,
5854                        radii: radius.map(|r| r.0),
5855                        stroke: width.0,
5856                        brush_type,
5857                        _pad: [0.0; 2],
5858                        grad_kind,
5859                        color0,
5860                        color1,
5861                        grad_p0,
5862                        grad_p1,
5863                        tile_mode,
5864                        _pad2: [0.0; 3],
5865                        fwd_mat,
5866                    });
5867                }
5868                SceneNode::Ellipse { rect, brush } => {
5869                    flush_if_prim_changed!("ellipse", &self.ellipses);
5870                    let (ndc, fwd_mat) = rect_to_instance_ndc(
5871                        *rect,
5872                        current_transform,
5873                        current_target_size.0,
5874                        current_target_size.1,
5875                    );
5876                    let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
5877                        brush_to_shape_fields(brush, rect, current_transform);
5878                    batch.ellipses.push(EllipseInstance {
5879                        xywh: ndc,
5880                        brush_type,
5881                        grad_kind,
5882                        _pad: [0.0; 2],
5883                        color0,
5884                        color1,
5885                        grad_p0,
5886                        grad_p1,
5887                        tile_mode,
5888                        _pad2: [0.0; 3],
5889                        fwd_mat,
5890                    });
5891                }
5892                SceneNode::EllipseBorder { rect, brush, width } => {
5893                    flush_if_prim_changed!("ellipse_border", &self.ellipse_borders);
5894                    let (ndc, fwd_mat) = rect_to_instance_ndc(
5895                        *rect,
5896                        current_transform,
5897                        current_target_size.0,
5898                        current_target_size.1,
5899                    );
5900                    let pad_px = width.0 * 0.5 + 2.0;
5901                    let pad = (pad_px / current_target_size.0) * 2.0;
5902                    let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
5903                        brush_to_shape_fields(brush, rect, current_transform);
5904                    batch.e_borders.push(EllipseBorderInstance {
5905                        xywh: ndc,
5906                        stroke: width.0,
5907                        pad,
5908                        brush_type,
5909                        grad_kind,
5910                        color0,
5911                        color1,
5912                        grad_p0,
5913                        grad_p1,
5914                        tile_mode,
5915                        _pad2: [0.0; 3],
5916                        fwd_mat,
5917                    });
5918                }
5919                SceneNode::Arc {
5920                    rect,
5921                    start_angle,
5922                    sweep_angle,
5923                    stroke_width,
5924                    brush,
5925                    cap,
5926                } => {
5927                    flush_if_prim_changed!("arc", &self.arcs);
5928                    let (ndc, fwd_mat) = rect_to_instance_ndc(
5929                        *rect,
5930                        current_transform,
5931                        current_target_size.0,
5932                        current_target_size.1,
5933                    );
5934                    let pad_px = stroke_width.0 * 0.5 + 2.0;
5935                    let pad = (pad_px / current_target_size.0) * 2.0;
5936                    let cap_val = match cap {
5937                        StrokeCap::Butt => 0.0,
5938                        StrokeCap::Round => 1.0,
5939                        StrokeCap::Square => 2.0,
5940                    };
5941                    let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
5942                        brush_to_shape_fields(brush, rect, current_transform);
5943                    batch.arcs.push(ArcInstance {
5944                        xywh: ndc,
5945                        start_angle: *start_angle,
5946                        sweep_angle: *sweep_angle,
5947                        stroke: stroke_width.0,
5948                        pad,
5949                        brush_type,
5950                        grad_kind,
5951                        _pad0: [0.0; 2],
5952                        color0,
5953                        color1,
5954                        grad_p0,
5955                        grad_p1,
5956                        tile_mode,
5957                        cap: cap_val,
5958                        _pad1: [0.0; 2],
5959                        fwd_mat,
5960                    });
5961                }
5962                SceneNode::Text {
5963                    rect,
5964                    text,
5965                    color,
5966                    size,
5967                    font_family,
5968                    text_align: _,
5969                    font_weight,
5970                    font_style,
5971                    text_decoration,
5972                    letter_spacing,
5973                    line_height: _,
5974                    extra_style,
5975                    url: _,
5976                    font_variation_settings,
5977                } => {
5978                    flush_batch!(); // flush any prior primitives
5979
5980                    let px = size.0;
5981                    let lh_ratio = rect.h / px;
5982                    let fw = font_weight.0;
5983                    let fs = if *font_style == FontStyle::Italic {
5984                        1
5985                    } else {
5986                        0
5987                    };
5988                    let shaped = repose_text::shape_line(
5989                        text.as_ref(),
5990                        px,
5991                        lh_ratio,
5992                        *font_family,
5993                        fw,
5994                        fs,
5995                        letter_spacing.0,
5996                        font_variation_settings.as_deref(),
5997                    );
5998                    let baseline_y = shaped.first().map(|g| rect.y + g.y);
5999
6000                    let fwd = forward_rs_mat(current_transform);
6001                    let has_linear = fwd != [1.0, 0.0, 0.0, 1.0];
6002
6003                    let lin = current_transform.linear();
6004                    let tr_x = current_transform.translate_x;
6005                    let tr_y = current_transform.translate_y;
6006
6007                    let make_glyph_instance =
6008                        |gx: f32, gy: f32, gw: f32, gh: f32| -> ([f32; 4], [f32; 4]) {
6009                            if has_linear {
6010                                let gc_x = gx + gw * 0.5;
6011                                let gc_y = gy + gh * 0.5;
6012                                let wc_x = lin[0] * gc_x + lin[1] * gc_y + tr_x;
6013                                let wc_y = lin[2] * gc_x + lin[3] * gc_y + tr_y;
6014                                let ww = gw * current_transform.scale_x;
6015                                let wh = gh * current_transform.scale_y;
6016                                let ex = fwd[0].abs() * ww * 0.5 + fwd[1].abs() * wh * 0.5;
6017                                let ey = fwd[2].abs() * ww * 0.5 + fwd[3].abs() * wh * 0.5;
6018                                let ndc_tl = to_ndc(
6019                                    wc_x - ex,
6020                                    wc_y - ey,
6021                                    ex * 2.0,
6022                                    ey * 2.0,
6023                                    current_target_size.0,
6024                                    current_target_size.1,
6025                                );
6026                                let ndc = [
6027                                    ndc_tl[0] + ndc_tl[2] * 0.5,
6028                                    ndc_tl[1] + ndc_tl[3] * 0.5,
6029                                    ndc_tl[2],
6030                                    ndc_tl[3],
6031                                ];
6032                                (ndc, fwd)
6033                            } else {
6034                                let (sx, sy) = if current_transform.scale_x == 1.0
6035                                    && current_transform.scale_y == 1.0
6036                                {
6037                                    (gx.round(), gy.round())
6038                                } else {
6039                                    (gx, gy)
6040                                };
6041                                rect_to_instance_ndc(
6042                                    repose_core::Rect {
6043                                        x: sx,
6044                                        y: sy,
6045                                        w: gw,
6046                                        h: gh,
6047                                    },
6048                                    current_transform,
6049                                    current_target_size.0,
6050                                    current_target_size.1,
6051                                )
6052                            }
6053                        };
6054
6055                    let baseline_shift_y: f32 = px * extra_style.baseline_shift.0;
6056
6057                    let (
6058                        draws_fill,
6059                        is_stroke,
6060                        stroke_width,
6061                        stroke_cap,
6062                        stroke_join,
6063                        stroke_miter,
6064                        stroke_path_effect,
6065                    ) = match &extra_style.draw_style {
6066                        repose_core::DrawStyle::Stroke {
6067                            width,
6068                            cap,
6069                            join,
6070                            miter,
6071                            path_effect,
6072                        } => (
6073                            false,
6074                            true,
6075                            *width,
6076                            *cap,
6077                            *join,
6078                            *miter,
6079                            path_effect.clone(),
6080                        ),
6081                        repose_core::DrawStyle::FillAndStroke {
6082                            width,
6083                            cap,
6084                            join,
6085                            miter,
6086                            path_effect,
6087                        } => (true, true, *width, *cap, *join, *miter, path_effect.clone()),
6088                        _ => (
6089                            true,
6090                            false,
6091                            0.0,
6092                            repose_core::StrokeCap::Butt,
6093                            repose_core::StrokeJoin::Miter,
6094                            4.0,
6095                            None,
6096                        ),
6097                    };
6098                    let stroke_tess_key = if is_stroke {
6099                        Some(slug::StrokeTessKey::new(
6100                            stroke_width,
6101                            stroke_cap,
6102                            stroke_join,
6103                            stroke_miter,
6104                            &stroke_path_effect,
6105                        ))
6106                    } else {
6107                        None
6108                    };
6109
6110                    for sg in shaped {
6111                        let gx = rect.x + sg.x + sg.bearing_x;
6112                        let gy = rect.y + sg.y - sg.bearing_y + baseline_shift_y;
6113
6114                        // Vector glyph path: tessellated geometry with MSAA.
6115                        if self.slug_enabled {
6116                            let ck = repose_text::lookup_cache_key(sg.key, sg.px);
6117                            if let Some(ref ck) = ck {
6118                                // Check if cached.
6119                                let need_tessellate = self.slug_cache.get(ck).is_none_or(|g| {
6120                                    (draws_fill && g.fill_vertices.is_none())
6121                                        || (is_stroke
6122                                            && !g
6123                                                .stroke_variants
6124                                                .contains_key(stroke_tess_key.as_ref().unwrap()))
6125                                });
6126                                if need_tessellate {
6127                                    if let Some((ck2, commands)) =
6128                                        repose_text::lookup_and_extract_outline(sg.key, sg.px)
6129                                    {
6130                                        let font_size_px = f32::from_bits(ck2.font_size_bits);
6131                                        if draws_fill {
6132                                            self.slug_cache.get_or_insert(
6133                                                ck2,
6134                                                font_size_px,
6135                                                &commands,
6136                                            );
6137                                        }
6138                                        if is_stroke {
6139                                            self.slug_cache.get_or_insert_stroke(
6140                                                ck2,
6141                                                font_size_px,
6142                                                &commands,
6143                                                stroke_width,
6144                                                stroke_cap,
6145                                                stroke_join,
6146                                                stroke_miter,
6147                                                &stroke_path_effect,
6148                                            );
6149                                        }
6150                                    }
6151                                } else {
6152                                    self.slug_cache.touch(ck);
6153                                }
6154                            }
6155                            if let Some(entry) = ck.as_ref().and_then(|ck| self.slug_cache.get(ck))
6156                            {
6157                                let ox = rect.x + sg.x;
6158                                let oy = rect.y + sg.y + baseline_shift_y;
6159                                let scx = current_transform.scale_x;
6160                                let scy = current_transform.scale_y;
6161                                let ttx = current_transform.translate_x;
6162                                let tty = current_transform.translate_y;
6163
6164                                let tf = |x: f32, y: f32| -> (f32, f32) {
6165                                    if has_linear {
6166                                        (
6167                                            lin[0] * x + lin[1] * y + ttx,
6168                                            lin[2] * x + lin[3] * y + tty,
6169                                        )
6170                                    } else {
6171                                        (x * scx + ttx, y * scy + tty)
6172                                    }
6173                                };
6174
6175                                let tw = current_target_size.0;
6176                                let th = current_target_size.1;
6177
6178                                let mut emit = |verts: &[[f32; 2]]| {
6179                                    for &v in verts {
6180                                        let (sx, sy) = tf(ox + v[0] * px, oy - v[1] * px);
6181                                        let ndc_x = sx / tw * 2.0 - 1.0;
6182                                        let ndc_y = -(sy / th) * 2.0 + 1.0;
6183                                        slug_verts_local.push(slug::TessVertex {
6184                                            ndc_pos: [ndc_x, ndc_y],
6185                                            color: color.to_linear(),
6186                                        });
6187                                    }
6188                                };
6189                                if draws_fill {
6190                                    emit(entry.fill_vertices.as_deref().unwrap_or(&[]));
6191                                }
6192                                if is_stroke {
6193                                    let key = stroke_tess_key.as_ref().unwrap();
6194                                    emit(
6195                                        entry
6196                                            .stroke_variants
6197                                            .get(key)
6198                                            .map(|v| v.as_slice())
6199                                            .unwrap_or(&[]),
6200                                    );
6201                                }
6202
6203                                if !draws_fill {
6204                                    // Stroke glyphs cannot use atlas fallback...
6205                                    continue;
6206                                }
6207                                continue;
6208                            }
6209                        }
6210
6211                        if !draws_fill {
6212                            // Don't use atlas fallback for strokes too
6213                            continue;
6214                        }
6215
6216                        if let Some(info) = self.upload_glyph_color(sg.key, sg.px) {
6217                            let (ndc, fwd_mat) = make_glyph_instance(gx, gy, info.w, info.h);
6218                            batch.colors.push(GlyphInstance {
6219                                xywh: ndc,
6220                                uv: [info.u0, info.v1, info.u1, info.v0],
6221                                color: color.to_linear(),
6222                                fwd_mat,
6223                            });
6224                        } else if let Some(info) = self.upload_glyph_mask(sg.key, sg.px) {
6225                            let (ndc, fwd_mat) = make_glyph_instance(gx, gy, info.w, info.h);
6226                            batch.masks.push(GlyphInstance {
6227                                xywh: ndc,
6228                                uv: [info.u0, info.v1, info.u1, info.v0],
6229                                color: color.to_linear(),
6230                                fwd_mat,
6231                            });
6232                        }
6233                    }
6234
6235                    // Upload slug vertices if any
6236                    if !slug_verts_local.is_empty() {
6237                        let bytes = bytemuck::cast_slice(&slug_verts_local);
6238                        self.slug_ring.grow_to_fit(&self.device, bytes.len() as u64);
6239                        let (off, _) = self.slug_ring.alloc_write(&self.queue, bytes);
6240                        current_pass.cmds.push(Cmd::GlyphsVector {
6241                            off,
6242                            cnt: slug_verts_local.len() as u32,
6243                        });
6244                        slug_verts_local.clear();
6245                    }
6246
6247                    // Text decoration: underline / strikethrough
6248                    if (text_decoration.underline || text_decoration.strikethrough)
6249                        && let Some(baseline_y) = baseline_y
6250                    {
6251                        flush_batch!();
6252                        current_prim = Some("rect");
6253                        let deco_color = text_decoration.color.unwrap_or(*color);
6254                        let thickness = (px * 0.07).max(1.0);
6255
6256                        if text_decoration.underline {
6257                            let dy = baseline_y + px * 0.1;
6258                            let (ndc, fwd_mat) = rect_to_instance_ndc(
6259                                repose_core::Rect {
6260                                    x: rect.x,
6261                                    y: dy,
6262                                    w: rect.w,
6263                                    h: thickness,
6264                                },
6265                                current_transform,
6266                                current_target_size.0,
6267                                current_target_size.1,
6268                            );
6269                            batch.rects.push(RectInstance {
6270                                xywh: ndc,
6271                                radii: [0.0; 4],
6272                                brush_type: 0,
6273                                grad_kind: 0,
6274                                _pad: [0.0; 2],
6275                                color0: deco_color.to_linear(),
6276                                color1: [0.0; 4],
6277                                grad_p0: [0.0; 2],
6278                                grad_p1: [0.0; 2],
6279                                tile_mode: 0,
6280                                _pad2: [0.0; 3],
6281                                fwd_mat,
6282                            });
6283                        }
6284                        if text_decoration.strikethrough {
6285                            let sy = baseline_y - px * 0.3;
6286                            let (ndc, fwd_mat) = rect_to_instance_ndc(
6287                                repose_core::Rect {
6288                                    x: rect.x,
6289                                    y: sy,
6290                                    w: rect.w,
6291                                    h: thickness,
6292                                },
6293                                current_transform,
6294                                current_target_size.0,
6295                                current_target_size.1,
6296                            );
6297                            batch.rects.push(RectInstance {
6298                                xywh: ndc,
6299                                radii: [0.0; 4],
6300                                brush_type: 0,
6301                                grad_kind: 0,
6302                                _pad: [0.0; 2],
6303                                color0: deco_color.to_linear(),
6304                                color1: [0.0; 4],
6305                                grad_p0: [0.0; 2],
6306                                grad_p1: [0.0; 2],
6307                                tile_mode: 0,
6308                                _pad2: [0.0; 3],
6309                                fwd_mat,
6310                            });
6311                        }
6312                    }
6313                }
6314                SceneNode::Image {
6315                    rect,
6316                    handle,
6317                    tint,
6318                    fit,
6319                } => {
6320                    flush_batch!();
6321
6322                    // Update usage timestamp for eviction, lazily re-uploading
6323                    // evicted RGBA images from their retained source.
6324                    let (img_w, img_h, is_nv12) = match self.resolve_image_for_draw(*handle) {
6325                        Some(wh) => wh,
6326                        None => {
6327                            log::warn!("Image handle {} not found", handle);
6328                            continue;
6329                        }
6330                    };
6331
6332                    let src_w = img_w as f32;
6333                    let src_h = img_h as f32;
6334
6335                    let dst_w = rect.w.max(0.0);
6336                    let dst_h = rect.h.max(0.0);
6337                    if dst_w <= 0.0 || dst_h <= 0.0 {
6338                        continue;
6339                    }
6340
6341                    let (draw_rect, uv_rect) = match fit {
6342                        repose_core::view::ImageFit::Contain => {
6343                            let scale = (dst_w / src_w).min(dst_h / src_h);
6344                            let w = src_w * scale;
6345                            let h = src_h * scale;
6346                            (
6347                                repose_core::Rect {
6348                                    x: rect.x + (dst_w - w) * 0.5,
6349                                    y: rect.y + (dst_h - h) * 0.5,
6350                                    w,
6351                                    h,
6352                                },
6353                                [0.0, 1.0, 1.0, 0.0],
6354                            )
6355                        }
6356                        repose_core::view::ImageFit::Cover => {
6357                            let scale = (dst_w / src_w).max(dst_h / src_h);
6358                            let content_w = src_w * scale;
6359                            let content_h = src_h * scale;
6360                            let overflow_x = (content_w - dst_w) * 0.5;
6361                            let overflow_y = (content_h - dst_h) * 0.5;
6362                            let u0 = (overflow_x / content_w).clamp(0.0, 1.0);
6363                            let v0 = (overflow_y / content_h).clamp(0.0, 1.0);
6364                            let u1 = ((overflow_x + dst_w) / content_w).clamp(0.0, 1.0);
6365                            let v1 = ((overflow_y + dst_h) / content_h).clamp(0.0, 1.0);
6366                            (*rect, [u0, 1.0 - v0, u1, 1.0 - v1])
6367                        }
6368                        repose_core::view::ImageFit::FitWidth => {
6369                            let scale = dst_w / src_w;
6370                            (
6371                                repose_core::Rect {
6372                                    x: rect.x,
6373                                    y: rect.y + (dst_h - src_h * scale) * 0.5,
6374                                    w: dst_w,
6375                                    h: src_h * scale,
6376                                },
6377                                [0.0, 1.0, 1.0, 0.0],
6378                            )
6379                        }
6380                        repose_core::view::ImageFit::FitHeight => {
6381                            let scale = dst_h / src_h;
6382                            (
6383                                repose_core::Rect {
6384                                    x: rect.x + (dst_w - src_w * scale) * 0.5,
6385                                    y: rect.y,
6386                                    w: src_w * scale,
6387                                    h: dst_h,
6388                                },
6389                                [0.0, 1.0, 1.0, 0.0],
6390                            )
6391                        }
6392                        repose_core::view::ImageFit::FillBounds => (*rect, [0.0, 1.0, 1.0, 0.0]),
6393                        repose_core::view::ImageFit::Inside => {
6394                            let scale = (dst_w / src_w).min(dst_h / src_h).min(1.0);
6395                            let w = src_w * scale;
6396                            let h = src_h * scale;
6397                            (
6398                                repose_core::Rect {
6399                                    x: rect.x + (dst_w - w) * 0.5,
6400                                    y: rect.y + (dst_h - h) * 0.5,
6401                                    w,
6402                                    h,
6403                                },
6404                                [0.0, 1.0, 1.0, 0.0],
6405                            )
6406                        }
6407                        repose_core::view::ImageFit::None => {
6408                            (
6409                                repose_core::Rect {
6410                                    x: rect.x,
6411                                    y: rect.y,
6412                                    w: src_w.min(dst_w),
6413                                    h: src_h.min(dst_h),
6414                                },
6415                                // If larger than dst, crop top-left of source:
6416                                [
6417                                    0.0,
6418                                    1.0,
6419                                    (dst_w / src_w).min(1.0),
6420                                    1.0 - (dst_h / src_h).min(1.0),
6421                                ],
6422                            )
6423                        }
6424                        _ => continue,
6425                    };
6426
6427                    let (ndc_center, fwd_mat) = rect_to_instance_ndc(
6428                        draw_rect,
6429                        current_transform,
6430                        current_target_size.0,
6431                        current_target_size.1,
6432                    );
6433
6434                    if is_nv12 {
6435                        let uv_x_offset = if let Some(ImageTex::Nv12 { w, color_info, .. }) =
6436                            self.images.get(handle)
6437                        {
6438                            match color_info.chroma_siting {
6439                                ChromaSiting::Center | ChromaSiting::TopLeft => 0.0,
6440                                ChromaSiting::Left => -1.0 / *w as f32,
6441                            }
6442                        } else {
6443                            0.0
6444                        };
6445
6446                        let inst = Nv12Instance {
6447                            xywh: ndc_center,
6448                            uv: uv_rect,
6449                            color: tint.to_linear(),
6450                            uv_x_offset,
6451                            fwd_mat,
6452                            _pad: [0.0],
6453                        };
6454                        if let Some((off, _)) = self.nv12.upload(&self.device, &self.queue, &[inst])
6455                        {
6456                            current_pass.cmds.push(Cmd::ImageNv12 {
6457                                off,
6458                                cnt: 1,
6459                                handle: *handle,
6460                            });
6461                        }
6462                    } else {
6463                        // RGBA uses GlyphInstance struct (reused pipeline)
6464                        let inst = GlyphInstance {
6465                            xywh: ndc_center,
6466                            uv: uv_rect,
6467                            color: tint.to_linear(),
6468                            fwd_mat,
6469                        };
6470                        if let Some((off, _)) =
6471                            self.glyph_color.upload(&self.device, &self.queue, &[inst])
6472                        {
6473                            current_pass.cmds.push(Cmd::ImageRgba {
6474                                off,
6475                                cnt: 1,
6476                                handle: *handle,
6477                            });
6478                        }
6479                    }
6480                }
6481                SceneNode::Coverage {
6482                    rect,
6483                    handle,
6484                    color,
6485                } => {
6486                    flush_batch!();
6487                    // Unknown handles are skipped (same policy as images);
6488                    // the lookup also marks the tile used for eviction.
6489                    let Some((tile_w, tile_h)) = self.coverage_dimensions(*handle) else {
6490                        log::warn!("Coverage handle {handle} not found");
6491                        continue;
6492                    };
6493                    // The tile composites at its registered size; `rect`
6494                    // positions its top-left.
6495                    let draw_rect = repose_core::Rect {
6496                        x: rect.x,
6497                        y: rect.y,
6498                        w: tile_w as f32,
6499                        h: tile_h as f32,
6500                    };
6501                    let (ndc_center, fwd_mat) = rect_to_instance_ndc(
6502                        draw_rect,
6503                        current_transform,
6504                        current_target_size.0,
6505                        current_target_size.1,
6506                    );
6507                    let inst = GlyphInstance {
6508                        xywh: ndc_center,
6509                        uv: [0.0, 1.0, 1.0, 0.0],
6510                        color: color.to_linear(),
6511                        fwd_mat,
6512                    };
6513                    if let Some((off, _)) =
6514                        self.glyph_color.upload(&self.device, &self.queue, &[inst])
6515                    {
6516                        current_pass.cmds.push(Cmd::Coverage {
6517                            off,
6518                            cnt: 1,
6519                            handle: *handle,
6520                        });
6521                    }
6522                }
6523                SceneNode::PushClip { rect, radius, op } => {
6524                    flush_batch!(); // flush content before entering clip
6525
6526                    let is_diff = matches!(op, repose_core::ClipOp::Difference);
6527
6528                    let t_identity = Transform::identity();
6529                    let current_transform = transform_stack.last().unwrap_or(&t_identity);
6530                    let transformed = affine_aabb(current_transform, rect);
6531
6532                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
6533                    let next_scissor = if is_diff {
6534                        top
6535                    } else {
6536                        intersect(top, transformed)
6537                    };
6538                    scissor_stack.push(next_scissor);
6539                    let scissor = to_scissor(
6540                        &next_scissor,
6541                        current_target_size.0 as u32,
6542                        current_target_size.1 as u32,
6543                    );
6544
6545                    let clip_ndc_tl = to_ndc(
6546                        transformed.x,
6547                        transformed.y,
6548                        transformed.w,
6549                        transformed.h,
6550                        current_target_size.0,
6551                        current_target_size.1,
6552                    );
6553                    let inst = ClipInstance {
6554                        xywh: [
6555                            clip_ndc_tl[0] + clip_ndc_tl[2] * 0.5,
6556                            clip_ndc_tl[1] + clip_ndc_tl[3] * 0.5,
6557                            clip_ndc_tl[2],
6558                            clip_ndc_tl[3],
6559                        ],
6560                        radii: radius.map(|r| r.0),
6561                        fwd_mat: [1.0, 0.0, 0.0, 1.0],
6562                    };
6563                    let bytes = bytemuck::bytes_of(&inst);
6564                    self.clip_ring.grow_to_fit(&self.device, bytes.len() as u64);
6565                    let (off, _) = self.clip_ring.alloc_write(&self.queue, bytes);
6566
6567                    let rounded = radius.iter().any(|&r| r.0 > 0.5);
6568
6569                    current_pass.cmds.push(Cmd::ClipPush {
6570                        off,
6571                        cnt: 1,
6572                        scissor,
6573                        difference: is_diff,
6574                        rounded,
6575                    });
6576                    clip_cmd_stack.push((off, 1, is_diff));
6577                }
6578                SceneNode::PopClip => {
6579                    flush_batch!();
6580
6581                    if !scissor_stack.is_empty() {
6582                        scissor_stack.pop();
6583                    } else {
6584                        log::warn!("PopClip with empty stack");
6585                    }
6586
6587                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
6588                    let scissor = to_scissor(
6589                        &top,
6590                        current_target_size.0 as u32,
6591                        current_target_size.1 as u32,
6592                    );
6593                    let (off, cnt, difference) = clip_cmd_stack.pop().unwrap_or((0, 0, false));
6594                    current_pass.cmds.push(Cmd::ClipPop {
6595                        off,
6596                        cnt,
6597                        scissor,
6598                        difference,
6599                    });
6600                }
6601                SceneNode::Shadow {
6602                    rect,
6603                    radius,
6604                    elevation: _,
6605                    color,
6606                } => {
6607                    flush_if_prim_changed!("rect", &self.rects);
6608                    let (ndc, fwd_mat) = rect_to_instance_ndc(
6609                        *rect,
6610                        current_transform,
6611                        current_target_size.0,
6612                        current_target_size.1,
6613                    );
6614                    let (brush_type, color0, _color1, _grad_p0, _grad_p1) =
6615                        brush_to_instance_fields(&Brush::Solid(*color));
6616                    batch.rects.push(RectInstance {
6617                        xywh: ndc,
6618                        radii: radius.map(|r| r.0),
6619                        brush_type,
6620                        grad_kind: 0,
6621                        _pad: [0.0; 2],
6622                        color0,
6623                        color1: [0.0; 4],
6624                        grad_p0: [0.0; 2],
6625                        grad_p1: [0.0; 2],
6626                        tile_mode: 0,
6627                        _pad2: [0.0; 3],
6628                        fwd_mat,
6629                    });
6630                }
6631                SceneNode::PushTransform { transform } => {
6632                    flush_batch!(); // flush before transform change
6633                    if transform.has_perspective() {
6634                        // True perspective cannot ride the affine fast path:
6635                        // flatten the subtree into an offscreen layer and
6636                        // composite it back projectively (CSS-style). See
6637                        // `push_perspective_layer`.
6638                        let top = *transform_stack.last().unwrap_or(&t_identity);
6639                        self.push_perspective_layer(
6640                            *transform,
6641                            top,
6642                            &mut transform_stack,
6643                            &mut scissor_stack,
6644                            &mut root_clip_rect,
6645                            &mut current_target_size,
6646                            &mut current_pass,
6647                            &mut passes,
6648                            &mut target_stack,
6649                            &mut flatten_stack,
6650                            &mut flatten_id_head,
6651                            &mut flatten_ids_used,
6652                        );
6653                    } else {
6654                        let combined = current_transform.combine(transform);
6655                        transform_stack.push(combined);
6656                    }
6657                }
6658                SceneNode::PopTransform => {
6659                    flush_batch!(); // flush before transform change
6660                    if let Some(rec) = flatten_stack.last() {
6661                        // A flatten level closes when the stack is back to the
6662                        // two entries this flatten pushed (stripped transform +
6663                        // layer-local shift); deeper plain pushes close first.
6664                        if transform_stack.len() == rec.stack_len + 2 {
6665                            let rec = flatten_stack.pop().expect("checked above");
6666                            transform_stack.pop();
6667                            transform_stack.pop();
6668                            self.pop_perspective_layer(
6669                                rec,
6670                                &mut scissor_stack,
6671                                &mut root_clip_rect,
6672                                &mut current_target_size,
6673                                &mut current_pass,
6674                                &mut passes,
6675                                &mut target_stack,
6676                            );
6677                            continue;
6678                        }
6679                    }
6680                    transform_stack.pop();
6681                }
6682                SceneNode::BeginLayer {
6683                    rect,
6684                    layer_id,
6685                    alpha,
6686                    blur_radius_x,
6687                    blur_radius_y,
6688                    rectangle_edge: _,
6689                } => {
6690                    flush_batch!();
6691                    // Layer rect is already snapped to whole pixels in layout;
6692                    // round() keeps any bypass of that snap consistent.
6693                    let w = (rect.w.round().max(1.0)) as u32;
6694                    let h = (rect.h.round().max(1.0)) as u32;
6695                    saved_scissor_stack =
6696                        std::mem::replace(&mut scissor_stack, Vec::with_capacity(8));
6697                    saved_root_clip_rect = std::mem::replace(
6698                        &mut root_clip_rect,
6699                        repose_core::Rect {
6700                            x: 0.0,
6701                            y: 0.0,
6702                            w: w as f32,
6703                            h: h as f32,
6704                        },
6705                    );
6706                    scissor_stack.push(root_clip_rect);
6707                    // Close out the current pass, start a new one for the layer.
6708                    let prev_target = current_pass.target;
6709                    let prev_scissor = current_pass.initial_scissor;
6710                    let saved = std::mem::replace(
6711                        &mut current_pass,
6712                        Pass {
6713                            target: PassTarget::Layer(*layer_id),
6714                            initial_scissor: (0, 0, w, h),
6715                            clear_color: Some([0.0, 0.0, 0.0, 0.0]),
6716                            cmds: Vec::new(),
6717                        },
6718                    );
6719                    passes.push(saved);
6720                    target_stack.push(prev_target);
6721                    let _ = prev_scissor; // initial_scissor of resumed pass is restored at EndLayer
6722                    // Get or create the layer's offscreen texture now so that
6723                    // subsequent scissor ops / draws have a valid target.
6724                    self.get_or_create_layer(*layer_id, w, h, *rect);
6725                    current_target_size = (w as f32, h as f32);
6726                    layer_alphas.push((*layer_id, *alpha, current_pass.initial_scissor));
6727                    // Store blur info for post-processing after EndLayer
6728                    if blur_radius_x.0 > 0.0 || blur_radius_y.0 > 0.0 {
6729                        layer_blurs.push((*layer_id, blur_radius_x.0, blur_radius_y.0));
6730                    }
6731                }
6732                SceneNode::EndLayer { layer_id } => {
6733                    flush_batch!();
6734                    scissor_stack = std::mem::take(&mut saved_scissor_stack);
6735                    root_clip_rect = saved_root_clip_rect;
6736                    // Finish the layer's pass, start a new one on the previous target.
6737                    let saved = std::mem::replace(
6738                        &mut current_pass,
6739                        Pass {
6740                            target: target_stack.pop().unwrap_or(PassTarget::Surface),
6741                            initial_scissor: (0, 0, self.output_width, self.output_height),
6742                            clear_color: None, // LoadOp::Load - don't wipe earlier surface content
6743                            cmds: Vec::new(),
6744                        },
6745                    );
6746                    passes.push(saved);
6747                    current_target_size = (fb_w, fb_h);
6748                    // Issue a composite quad for the just-finished layer in the new pass.
6749                    if let Some((_, layer_alpha, _)) = layer_alphas
6750                        .iter()
6751                        .find(|(id, _, _)| id == layer_id)
6752                        .copied()
6753                    {
6754                        let layer = self.layer_pool.get(layer_id).expect("layer target");
6755                        let ndc_tl = to_ndc(
6756                            layer.rect_px.0,
6757                            layer.rect_px.1,
6758                            layer.rect_px.2,
6759                            layer.rect_px.3,
6760                            fb_w,
6761                            fb_h,
6762                        );
6763                        let uv_u1 = layer.rect_px.2 / layer.width.max(1) as f32;
6764                        let uv_v1 = layer.rect_px.3 / layer.height.max(1) as f32;
6765                        // Check if this layer needs content blur
6766                        let blur_px_val = layer_blurs
6767                            .iter()
6768                            .find(|(id, _, _)| id == layer_id)
6769                            .map(|(_, bx, by)| (*bx, *by));
6770                        if let Some((blur_x, blur_y)) =
6771                            blur_px_val.filter(|(bx, by)| *bx > 0.0 || *by > 0.0)
6772                        {
6773                            // Content blur: draw blurred version using the blur_content pipeline
6774                            let bw_uv = (blur_x * 1.5) / layer.width.max(1) as f32;
6775                            let bh_uv = (blur_y * 1.5) / layer.height.max(1) as f32;
6776                            let inst = BlurInstance {
6777                                xywh: [
6778                                    ndc_tl[0] + ndc_tl[2] * 0.5,
6779                                    ndc_tl[1] + ndc_tl[3] * 0.5,
6780                                    ndc_tl[2],
6781                                    ndc_tl[3],
6782                                ],
6783                                uv: [0.0, 0.0, uv_u1, uv_v1],
6784                                color: [1.0, 1.0, 1.0, layer_alpha],
6785                                blur_uv: [bw_uv, bh_uv],
6786                                fwd_mat: [1.0, 0.0, 0.0, 1.0],
6787                            };
6788                            self.blur_ring.grow_to_fit(
6789                                &self.device,
6790                                std::mem::size_of::<BlurInstance>() as u64,
6791                            );
6792                            let bytes = bytemuck::bytes_of(&inst);
6793                            let (off, _) = self.blur_ring.alloc_write(&self.queue, bytes);
6794                            current_pass.cmds.push(Cmd::CompositeBlur {
6795                                off,
6796                                cnt: 1,
6797                                layer_id: *layer_id,
6798                            });
6799                        } else {
6800                            // Normal sharp composite
6801                            let inst = GlyphInstance {
6802                                xywh: [
6803                                    ndc_tl[0] + ndc_tl[2] * 0.5,
6804                                    ndc_tl[1] + ndc_tl[3] * 0.5,
6805                                    ndc_tl[2],
6806                                    ndc_tl[3],
6807                                ],
6808                                uv: [0.0, uv_v1, uv_u1, 0.0],
6809                                color: [1.0, 1.0, 1.0, layer_alpha],
6810                                fwd_mat: [1.0, 0.0, 0.0, 1.0],
6811                            };
6812                            if let Some((off, cnt)) =
6813                                self.glyph_color.upload(&self.device, &self.queue, &[inst])
6814                            {
6815                                current_pass.cmds.push(Cmd::CompositeLayer {
6816                                    off,
6817                                    cnt,
6818                                    layer_id: *layer_id,
6819                                });
6820                            }
6821                        }
6822                    }
6823                }
6824                SceneNode::CompositeShadow {
6825                    layer_id,
6826                    blur_px,
6827                    offset_px,
6828                    color,
6829                } => {
6830                    flush_batch!();
6831                    if let Some(layer) = self.layer_pool.get(layer_id).cloned() {
6832                        // Shadow rect = layer rect + offset.
6833                        let sx = layer.rect_px.0 + offset_px.0.0;
6834                        let sy = layer.rect_px.1 + offset_px.1.0;
6835                        let sw = layer.rect_px.2;
6836                        let sh = layer.rect_px.3;
6837                        // The blur in UV space is 1.5 * blur_px / texture_size
6838                        // (the 1.5 matches the 3x3 Gaussian span).
6839                        let bw_uv = (blur_px.0 * 1.5) / layer.width.max(1) as f32;
6840                        let bh_uv = (blur_px.0 * 1.5) / layer.height.max(1) as f32;
6841                        let shadow_u1 = layer.rect_px.2 / layer.width.max(1) as f32;
6842                        let shadow_v1 = layer.rect_px.3 / layer.height.max(1) as f32;
6843                        let ndc_tl = to_ndc(sx, sy, sw, sh, fb_w, fb_h);
6844                        let inst = BlurInstance {
6845                            xywh: [
6846                                ndc_tl[0] + ndc_tl[2] * 0.5,
6847                                ndc_tl[1] + ndc_tl[3] * 0.5,
6848                                ndc_tl[2],
6849                                ndc_tl[3],
6850                            ],
6851                            uv: [0.0, 0.0, shadow_u1, shadow_v1],
6852                            color: [
6853                                color.0 as f32 / 255.0,
6854                                color.1 as f32 / 255.0,
6855                                color.2 as f32 / 255.0,
6856                                color.3 as f32 / 255.0,
6857                            ],
6858                            blur_uv: [bw_uv, bh_uv],
6859                            fwd_mat: [1.0, 0.0, 0.0, 1.0],
6860                        };
6861                        self.blur_ring
6862                            .grow_to_fit(&self.device, std::mem::size_of::<BlurInstance>() as u64);
6863                        let bytes = bytemuck::bytes_of(&inst);
6864                        let (off, _) = self.blur_ring.alloc_write(&self.queue, bytes);
6865                        current_pass.cmds.push(Cmd::CompositeShadow {
6866                            off,
6867                            cnt: 1,
6868                            layer_id: *layer_id,
6869                        });
6870                    }
6871                }
6872                SceneNode::VectorMesh {
6873                    mesh,
6874                    transform,
6875                    paint,
6876                    clip: _,
6877                    blend,
6878                } => {
6879                    flush_batch!();
6880                    if blend.needs_isolation() {
6881                        self.emit_isolated_blend(
6882                            mesh.clone(),
6883                            *transform,
6884                            *paint,
6885                            *blend,
6886                            current_transform,
6887                            &mut current_pass,
6888                            &mut passes,
6889                            &mut target_stack,
6890                            &mut flatten_id_head,
6891                            &mut flatten_ids_used,
6892                            &mut current_target_size,
6893                            fb_w,
6894                            fb_h,
6895                        );
6896                    } else {
6897                        let t_identity = Transform::identity();
6898                        let current_transform =
6899                            transform_stack.last().unwrap_or(&t_identity);
6900                        self.emit_vector_mesh(
6901                            current_transform,
6902                            mesh,
6903                            *transform,
6904                            paint,
6905                            *blend,
6906                            &mut current_pass.cmds,
6907                        );
6908                    }
6909                }
6910                SceneNode::VectorOverlay { meshes } => {
6911                    flush_batch!();
6912                    for m in meshes.iter() {
6913                        let (voff, vcnt, ioff, icnt) = self.upload_mesh_geometry(m);
6914                        let uoff = self.alloc_mesh_uniform(MeshUniform::identity());
6915                        current_pass.cmds.push(Cmd::VectorOverlay {
6916                            voff,
6917                            vcnt,
6918                            ioff,
6919                            icnt,
6920                            uoff,
6921                        });
6922                    }
6923                }
6924                SceneNode::PushVectorClip { mesh, op } => {
6925                    flush_batch!();
6926                    let difference = matches!(op, repose_core::ClipOp::Difference);
6927                    let t_identity = Transform::identity();
6928                    let current_transform = transform_stack.last().unwrap_or(&t_identity);
6929                    let affine =
6930                        combine_mesh_affine(current_transform, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
6931                    let aabb = mesh_aabb(mesh, affine);
6932                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
6933                    // An intersect mask can only remove pixels, so the scissor
6934                    // tightens; a difference mask removes the *inside*, so the
6935                    // scissor stays (content outside the mask must still draw).
6936                    let next = if difference {
6937                        top
6938                    } else {
6939                        intersect(top, aabb)
6940                    };
6941                    scissor_stack.push(next);
6942                    let scissor = to_scissor(
6943                        &next,
6944                        current_target_size.0 as u32,
6945                        current_target_size.1 as u32,
6946                    );
6947                    let (voff, vcnt, ioff, icnt) = self.upload_mesh_geometry(mesh);
6948                    let uoff = self.alloc_mesh_uniform(mesh_uniform_from_paint(
6949                        affine,
6950                        &repose_core::PaintDesc::Solid,
6951                    ));
6952                    current_pass.cmds.push(Cmd::VectorClipPush {
6953                        voff,
6954                        vcnt,
6955                        ioff,
6956                        icnt,
6957                        uoff,
6958                        scissor,
6959                        difference,
6960                    });
6961                    self.mesh_clip_stack
6962                        .push((voff, vcnt, ioff, icnt, uoff, difference));
6963                }
6964                SceneNode::PopVectorClip => {
6965                    flush_batch!();
6966                    if !scissor_stack.is_empty() {
6967                        scissor_stack.pop();
6968                    } else {
6969                        log::warn!("PopVectorClip with empty scissor stack");
6970                    }
6971                    if let Some((voff, vcnt, ioff, icnt, uoff, difference)) =
6972                        self.mesh_clip_stack.pop()
6973                    {
6974                        let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
6975                        let scissor = to_scissor(
6976                            &top,
6977                            current_target_size.0 as u32,
6978                            current_target_size.1 as u32,
6979                        );
6980                        current_pass.cmds.push(Cmd::VectorClipPop {
6981                            voff,
6982                            vcnt,
6983                            ioff,
6984                            icnt,
6985                            uoff,
6986                            scissor,
6987                            difference,
6988                        });
6989                    } else {
6990                        log::warn!("PopVectorClip with empty clip stack");
6991                    }
6992                }
6993                SceneNode::Callback { rect, payload } => {
6994                    flush_batch!();
6995                    let t = transform_stack
6996                        .last()
6997                        .copied()
6998                        .unwrap_or(Transform::identity());
6999                    let transformed = affine_aabb(&t, rect);
7000                    current_pass.cmds.push(Cmd::Callback {
7001                        rect: transformed,
7002                        payload: payload.clone(),
7003                    });
7004                }
7005                _ => {}
7006            }
7007        }
7008
7009        flush_batch!();
7010
7011        {
7012            let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
7013            let mut prepare_list: Vec<Arc<Callback>> = Vec::new();
7014            for node in &scene.nodes {
7015                if let SceneNode::Callback { payload, .. } = node
7016                    && payload.downcast_ref::<Callback>().is_some()
7017                {
7018                    let ptr = Arc::as_ptr(payload) as *const () as usize;
7019                    if seen.insert(ptr)
7020                        && let Ok(cb_arc) = payload.clone().downcast::<Callback>()
7021                    {
7022                        prepare_list.push(cb_arc);
7023                    }
7024                }
7025            }
7026            if !prepare_list.is_empty() {
7027                let screen_desc = ScreenDescriptor {
7028                    size_in_pixels: [self.output_width, self.output_height],
7029                    pixels_per_point: self.pixels_per_point,
7030                    target_format: self.output_format,
7031                    sample_count: self.msaa_samples.max(1),
7032                };
7033                let mut user_cmd_bufs: Vec<wgpu::CommandBuffer> = Vec::new();
7034                for cb in &prepare_list {
7035                    user_cmd_bufs.extend(cb.0.prepare(
7036                        &self.device,
7037                        &self.queue,
7038                        encoder,
7039                        &screen_desc,
7040                        &mut self.callback_resources,
7041                    ));
7042                }
7043                for cb in &prepare_list {
7044                    user_cmd_bufs.extend(cb.0.finish_prepare(
7045                        &self.device,
7046                        &self.queue,
7047                        encoder,
7048                        &screen_desc,
7049                        &mut self.callback_resources,
7050                    ));
7051                }
7052                // NOTE: For now submit immediately via queue
7053                // so they execute before main render pass.
7054                if !user_cmd_bufs.is_empty() {
7055                    self.queue.submit(user_cmd_bufs);
7056                }
7057            }
7058        }
7059
7060        // Push the final pass.
7061        passes.push(current_pass);
7062
7063        let globals_bytes = std::mem::size_of::<Globals>() as u64;
7064        let globals_staging = self.device.create_buffer(&wgpu::BufferDescriptor {
7065            label: Some("globals staging"),
7066            size: (passes.len().max(1) as u64) * globals_bytes,
7067            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
7068            mapped_at_creation: false,
7069        });
7070        for (i, pass) in passes.iter().enumerate() {
7071            let (target_w, target_h) = match pass.target {
7072                PassTarget::Surface => (fb_w, fb_h),
7073                PassTarget::Layer(layer_id) => {
7074                    let lt = self.layer_pool.get(&layer_id);
7075                    (
7076                        lt.map_or(fb_w, |l| l.width as f32),
7077                        lt.map_or(fb_h, |l| l.height as f32),
7078                    )
7079                }
7080            };
7081            self.queue.write_buffer(
7082                &globals_staging,
7083                (i as u64) * globals_bytes,
7084                bytemuck::bytes_of(&make_globals(target_w, target_h)),
7085            );
7086        }
7087
7088        let bind_mask = self.atlas_bind_group_mask();
7089        let bind_color = self.atlas_bind_group_color();
7090        let mut clip_depth: u32 = 0;
7091        let mut clip_depth_stack: Vec<u32> = Vec::new();
7092
7093        let snapshot_source = target_texture.cloned();
7094        for (pass_index, pass) in std::mem::take(&mut passes).into_iter().enumerate() {
7095            // Populate backdrop snapshots for blends composited in this
7096            // pass: copy the parent target region into the snapshot
7097            // texture. Passes execute in order, so the parent holds the
7098            // true backdrop. Surface parents need the target texture (no
7099            // swapchain sampling mid-frame); layer parents copy from the
7100            // pool. Either way the region is parent-target pixels. Copies
7101            // for other passes stay queued (`remaining`).
7102            let needs_snapshot = pass.cmds.iter().any(|c| matches!(c, Cmd::BlendLayer { .. }));
7103            if needs_snapshot {
7104                let copies = std::mem::take(&mut self.blend_copies);
7105                let mut remaining = Vec::with_capacity(copies.len());
7106                for (blend_id, target, region) in copies {
7107                    let (src_tex, tw, th) = match target {
7108                        PassTarget::Layer(parent_id) => match self.layer_pool.get(&parent_id) {
7109                            Some(lt) => (lt.texture.clone(), lt.width, lt.height),
7110                            None => continue,
7111                        },
7112                        PassTarget::Surface => match &snapshot_source {
7113                            Some(t) => (t.clone(), self.output_width, self.output_height),
7114                            None => {
7115                                remaining.push((blend_id, target, region));
7116                                continue;
7117                            }
7118                        },
7119                    };
7120                    let Some(dst_tex) = self.blend_snapshot_texture(blend_id) else {
7121                        continue;
7122                    };
7123                    let sx = (region.x.max(0.0) as u32).min(tw.saturating_sub(1));
7124                    let sy = (region.y.max(0.0) as u32).min(th.saturating_sub(1));
7125                    let cw = (region.w.ceil() as u32)
7126                        .max(1)
7127                        .min(tw.saturating_sub(sx).max(1));
7128                    let ch = (region.h.ceil() as u32)
7129                        .max(1)
7130                        .min(th.saturating_sub(sy).max(1));
7131                    encoder.copy_texture_to_texture(
7132                        wgpu::TexelCopyTextureInfo {
7133                            texture: &src_tex,
7134                            mip_level: 0,
7135                            origin: wgpu::Origin3d { x: sx, y: sy, z: 0 },
7136                            aspect: wgpu::TextureAspect::All,
7137                        },
7138                        wgpu::TexelCopyTextureInfo {
7139                            texture: &dst_tex,
7140                            mip_level: 0,
7141                            origin: wgpu::Origin3d::ZERO,
7142                            aspect: wgpu::TextureAspect::All,
7143                        },
7144                        wgpu::Extent3d {
7145                            width: cw,
7146                            height: ch,
7147                            depth_or_array_layers: 1,
7148                        },
7149                    );
7150                }
7151                self.blend_copies = remaining;
7152            }
7153            let (color_view, resolve_target, depth_stencil_view, is_layer) = match pass.target {
7154                PassTarget::Surface => {
7155                    let swap_view = target_view.clone();
7156                    let use_ws = self.working_space && self.ws_view.is_some();
7157                    let (color, resolve) = if use_ws {
7158                        let ws_view = self.ws_view.as_ref().unwrap();
7159                        if let Some(msaa_view) = &self.msaa_view {
7160                            // MSAA resolves to working-space texture
7161                            (msaa_view.clone(), Some(ws_view.clone()))
7162                        } else {
7163                            // Direct render to working-space texture
7164                            (ws_view.clone(), None)
7165                        }
7166                    } else if let Some(msaa_view) = &self.msaa_view {
7167                        (msaa_view.clone(), Some(swap_view))
7168                    } else {
7169                        (swap_view, None)
7170                    };
7171                    (color, resolve, self.depth_stencil_view.clone(), false)
7172                }
7173                PassTarget::Layer(layer_id) => {
7174                    if let Some(lt) = self.layer_pool.get(&layer_id) {
7175                        (lt.view.clone(), None, lt.depth_stencil_view.clone(), true)
7176                    } else {
7177                        log::warn!("missing layer target {layer_id}");
7178                        continue;
7179                    }
7180                }
7181            };
7182
7183            encoder.copy_buffer_to_buffer(
7184                &globals_staging,
7185                (pass_index as u64) * globals_bytes,
7186                &self.globals_buf,
7187                0,
7188                globals_bytes,
7189            );
7190
7191            if is_layer {
7192                clip_depth_stack.push(clip_depth);
7193                clip_depth = 0;
7194            }
7195
7196            let (tw, th) = match pass.target {
7197                PassTarget::Surface => (self.output_width, self.output_height),
7198                PassTarget::Layer(layer_id) => self
7199                    .layer_pool
7200                    .get(&layer_id)
7201                    .map(|l| (l.width, l.height))
7202                    .unwrap_or((self.output_width, self.output_height)),
7203            };
7204            let initial_scissor = clamp_scissor(
7205                pass.initial_scissor.0,
7206                pass.initial_scissor.1,
7207                pass.initial_scissor.2,
7208                pass.initial_scissor.3,
7209                tw,
7210                th,
7211            );
7212
7213            let pipes: &Pipelines = if is_layer {
7214                &self.layer_pipes
7215            } else {
7216                &self.surface_pipes
7217            };
7218
7219            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
7220                label: Some("pass"),
7221                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7222                    view: &color_view,
7223                    resolve_target: resolve_target.as_ref(),
7224                    ops: wgpu::Operations {
7225                        load: match pass.clear_color {
7226                            Some(c) => wgpu::LoadOp::Clear(wgpu::Color {
7227                                r: c[0] as f64,
7228                                g: c[1] as f64,
7229                                b: c[2] as f64,
7230                                a: c[3] as f64,
7231                            }),
7232                            None => wgpu::LoadOp::Load,
7233                        },
7234                        store: wgpu::StoreOp::Store,
7235                    },
7236                    depth_slice: None,
7237                })],
7238                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
7239                    view: &depth_stencil_view,
7240                    depth_ops: None,
7241                    stencil_ops: Some(wgpu::Operations {
7242                        load: if is_layer || pass.clear_color.is_some() {
7243                            wgpu::LoadOp::Clear(0)
7244                        } else {
7245                            wgpu::LoadOp::Load
7246                        },
7247                        store: wgpu::StoreOp::Store,
7248                    }),
7249                }),
7250                timestamp_writes: None,
7251                occlusion_query_set: None,
7252                multiview_mask: None,
7253            });
7254
7255            rpass.set_bind_group(0, &self.globals_bind, &[]);
7256            rpass.set_stencil_reference(clip_depth);
7257            rpass.set_scissor_rect(
7258                initial_scissor.0,
7259                initial_scissor.1,
7260                initial_scissor.2,
7261                initial_scissor.3,
7262            );
7263
7264            macro_rules! draw_simple {
7265                ($pipeline:expr, $ring:expr, $inst:ty, $off:ident, $n:ident) => {{
7266                    rpass.set_pipeline($pipeline);
7267                    let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
7268                    rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
7269                    rpass.draw(0..6, 0..$n);
7270                }};
7271            }
7272
7273            macro_rules! draw_with_bind {
7274                ($pipeline:expr, $ring:expr, $inst:ty, $bind:expr, $off:ident, $n:ident) => {{
7275                    rpass.set_pipeline($pipeline);
7276                    rpass.set_bind_group(1, $bind, &[]);
7277                    let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
7278                    rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
7279                    rpass.draw(0..6, 0..$n);
7280                }};
7281            }
7282
7283            macro_rules! draw_indexed_mesh {
7284                ($pipeline:expr, $uoff:ident, $voff:ident, $vcnt:ident, $ioff:ident, $icnt:ident) => {{
7285                    rpass.set_pipeline($pipeline);
7286                    rpass.set_bind_group(1, &self.mesh_bind, &[$uoff as u32]);
7287                    let vbytes = ($vcnt as u64) * std::mem::size_of::<MeshVertex>() as u64;
7288                    rpass.set_vertex_buffer(0, self.mesh_verts.buf.slice($voff..$voff + vbytes));
7289                    let ibytes = ($icnt as u64) * std::mem::size_of::<u32>() as u64;
7290                    rpass.set_index_buffer(
7291                        self.mesh_indices.buf.slice($ioff..$ioff + ibytes),
7292                        wgpu::IndexFormat::Uint32,
7293                    );
7294                    rpass.draw_indexed(0..$icnt, 0, 0..1);
7295                }};
7296            }
7297
7298            for cmd in pass.cmds {
7299                match cmd {
7300                    Cmd::ClipPush {
7301                        off,
7302                        cnt: n,
7303                        scissor,
7304                        difference,
7305                        rounded: _,
7306                    } => {
7307                        let scissor =
7308                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
7309                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
7310                        rpass.set_stencil_reference(clip_depth);
7311
7312                        if difference {
7313                            rpass.set_pipeline(&pipes.clip_dec);
7314                        } else {
7315                            // Deliberately whole-pixel (bin) gating at every
7316                            // sample count. Clipped content blends with its
7317                            // own smooth AA identically on all samples, while
7318                            // alpha-to-coverage gates per-sample and leaves a
7319                            // GPU-sample-pattern-dependent bright rim along
7320                            // rounded corners at fractional geometry. MSAA
7321                            // still smooths every content edge inside the
7322                            // clip region.
7323                            rpass.set_pipeline(&pipes.clip_bin);
7324                        }
7325
7326                        let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
7327                        rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
7328                        rpass.draw(0..6, 0..n);
7329
7330                        if !difference {
7331                            clip_depth = (clip_depth + 1).min(255);
7332                            rpass.set_stencil_reference(clip_depth);
7333                        }
7334                    }
7335
7336                    Cmd::ClipPop {
7337                        off,
7338                        cnt: n,
7339                        scissor,
7340                        difference,
7341                    } => {
7342                        let scissor =
7343                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
7344                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
7345
7346                        if !difference && n > 0 {
7347                            rpass.set_stencil_reference(clip_depth);
7348                            rpass.set_pipeline(&pipes.clip_dec);
7349                            let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
7350                            rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
7351                            rpass.draw(0..6, 0..n);
7352                            clip_depth = clip_depth.saturating_sub(1);
7353                        } else if !difference {
7354                            clip_depth = clip_depth.saturating_sub(1);
7355                        }
7356                        rpass.set_stencil_reference(clip_depth);
7357                    }
7358
7359                    Cmd::Rect { off, cnt: n } => {
7360                        draw_simple!(&pipes.rects, self.rects.ring, RectInstance, off, n);
7361                    }
7362
7363                    Cmd::Border { off, cnt: n } => {
7364                        draw_simple!(&pipes.borders, self.borders.ring, BorderInstance, off, n);
7365                    }
7366
7367                    Cmd::GlyphsMask { off, cnt: n } => {
7368                        draw_with_bind!(
7369                            &pipes.text_mask,
7370                            self.glyph_mask.ring,
7371                            GlyphInstance,
7372                            &bind_mask,
7373                            off,
7374                            n
7375                        );
7376                    }
7377
7378                    Cmd::GlyphsColor { off, cnt: n } => {
7379                        draw_with_bind!(
7380                            &pipes.text_color,
7381                            self.glyph_color.ring,
7382                            GlyphInstance,
7383                            &bind_color,
7384                            off,
7385                            n
7386                        );
7387                    }
7388
7389                    Cmd::GlyphsVector { off, cnt: n } => {
7390                        if let Some(slug_pipe) = pipes.slug.as_ref() {
7391                            rpass.set_pipeline(slug_pipe);
7392                            let bytes = (n as u64) * std::mem::size_of::<slug::TessVertex>() as u64;
7393                            rpass.set_vertex_buffer(0, self.slug_ring.buf.slice(off..off + bytes));
7394                            rpass.draw(0..n, 0..1);
7395                        }
7396                    }
7397
7398                    Cmd::ImageRgba {
7399                        off,
7400                        cnt: n,
7401                        handle,
7402                    } => {
7403                        let bind_opt = match self.images.get(&handle) {
7404                            Some(ImageTex::Rgba { bind, .. }) => Some(bind),
7405                            Some(ImageTex::User { bind, .. }) => Some(bind),
7406                            _ => None,
7407                        };
7408                        if let Some(bind) = bind_opt {
7409                            draw_with_bind!(
7410                                &pipes.image_rgba,
7411                                self.glyph_color.ring,
7412                                GlyphInstance,
7413                                bind,
7414                                off,
7415                                n
7416                            );
7417                        }
7418                    }
7419                    Cmd::Coverage {
7420                        off,
7421                        cnt: n,
7422                        handle,
7423                    } => {
7424                        if let Some(tile) = self.coverages.get(&handle) {
7425                            draw_with_bind!(
7426                                &pipes.coverage,
7427                                self.glyph_color.ring,
7428                                GlyphInstance,
7429                                &tile.bind,
7430                                off,
7431                                n
7432                            );
7433                        }
7434                    }
7435
7436                    Cmd::ImageNv12 {
7437                        off,
7438                        cnt: n,
7439                        handle,
7440                    } => {
7441                        if let Some(ImageTex::Nv12 { bind, .. }) = self.images.get(&handle) {
7442                            draw_with_bind!(
7443                                &pipes.image_nv12,
7444                                self.nv12.ring,
7445                                Nv12Instance,
7446                                bind,
7447                                off,
7448                                n
7449                            );
7450                        }
7451                    }
7452
7453                    Cmd::Ellipse { off, cnt: n } => {
7454                        draw_simple!(&pipes.ellipses, self.ellipses.ring, EllipseInstance, off, n);
7455                    }
7456
7457                    Cmd::EllipseBorder { off, cnt: n } => {
7458                        draw_simple!(
7459                            &pipes.ellipse_borders,
7460                            self.ellipse_borders.ring,
7461                            EllipseBorderInstance,
7462                            off,
7463                            n
7464                        );
7465                    }
7466
7467                    Cmd::Arc { off, cnt: n } => {
7468                        draw_simple!(&pipes.arcs, self.arcs.ring, ArcInstance, off, n);
7469                    }
7470
7471                    Cmd::CompositeLayer {
7472                        off,
7473                        cnt: n,
7474                        layer_id,
7475                    } => {
7476                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
7477                            draw_with_bind!(
7478                                &pipes.image_rgba,
7479                                self.glyph_color.ring,
7480                                GlyphInstance,
7481                                &lt.bind,
7482                                off,
7483                                n
7484                            );
7485                        }
7486                    }
7487                    Cmd::CompositeShadow {
7488                        off,
7489                        cnt: n,
7490                        layer_id,
7491                    } => {
7492                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
7493                            draw_with_bind!(
7494                                &pipes.blur,
7495                                self.blur_ring,
7496                                BlurInstance,
7497                                &lt.bind_linear,
7498                                off,
7499                                n
7500                            );
7501                        }
7502                    }
7503                    Cmd::CompositeBlur {
7504                        off,
7505                        cnt: n,
7506                        layer_id,
7507                    } => {
7508                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
7509                            draw_with_bind!(
7510                                &pipes.blur_content,
7511                                self.blur_ring,
7512                                BlurInstance,
7513                                &lt.bind_linear,
7514                                off,
7515                                n
7516                            );
7517                        }
7518                    }
7519                    Cmd::CompositeProjective {
7520                        off,
7521                        cnt: n,
7522                        layer_id,
7523                    } => {
7524                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
7525                            // The layer texture is sampled with the rgba
7526                            // (non-linear-filter) binding, like the sharp
7527                            // composite path.
7528                            draw_with_bind!(
7529                                &pipes.projective_layer,
7530                                self.projective_ring,
7531                                ProjectiveInstance,
7532                                &lt.bind,
7533                                off,
7534                                n
7535                            );
7536                        }
7537                    }
7538
7539                    Cmd::BlendLayer {
7540                        off,
7541                        cnt: n,
7542                        src_layer,
7543                        dst_layer,
7544                        parent,
7545                    } => {
7546                        let src = self.layer_pool.get(&src_layer).cloned();
7547                        // Backdrop is the snapshot copied from the parent
7548                        // target before this pass opened (a texture cannot be
7549                        // sampled mid-pass while bound as a render target).
7550                        // A missing snapshot (surface parent without texture,
7551                        // evicted layer) skips the draw: the mesh vanishes
7552                        // rather than misrendering.
7553                        let dst = dst_layer
7554                            .and_then(|id| self.blend_snapshots.get(&id).cloned());
7555                        let in_parent = match parent {
7556                            PassTarget::Surface => !is_layer,
7557                            PassTarget::Layer(id) => {
7558                                matches!(pass.target, PassTarget::Layer(pid) if pid == id)
7559                            }
7560                        };
7561                        if let (Some(src_lt), Some(dst_snap)) = (src, dst)
7562                            && in_parent
7563                        {
7564                            let bytes =
7565                                (n as u64) * std::mem::size_of::<BlendInstance>() as u64;
7566                            rpass.set_pipeline(&pipes.blend_layer);
7567                            rpass.set_scissor_rect(0, 0, tw, th);
7568                            rpass.set_bind_group(0, &self.globals_bind, &[]);
7569                            rpass.set_bind_group(1, &src_lt.bind, &[]);
7570                            rpass.set_bind_group(2, &dst_snap.bind, &[]);
7571                            rpass.set_vertex_buffer(
7572                                0,
7573                                self.blend_ring.buf.slice(off..off + bytes),
7574                            );
7575                            rpass.draw(0..6, 0..n);
7576                        }
7577                    }
7578
7579                    Cmd::VectorMesh {
7580                        voff,
7581                        vcnt,
7582                        ioff,
7583                        icnt,
7584                        uoff,
7585                        blend,
7586                    } => {
7587                        let pipe = match blend {
7588                            repose_core::BlendMode::Add => &pipes.mesh_add,
7589                            repose_core::BlendMode::Multiply => &pipes.mesh_multiply,
7590                            repose_core::BlendMode::Screen => &pipes.mesh_screen,
7591                            repose_core::BlendMode::Darken => &pipes.mesh_darken,
7592                            repose_core::BlendMode::Lighten => &pipes.mesh_lighten,
7593                            _ => &pipes.mesh,
7594                        };
7595                        draw_indexed_mesh!(pipe, uoff, voff, vcnt, ioff, icnt);
7596                    }
7597
7598                    Cmd::VectorOverlay {
7599                        voff,
7600                        vcnt,
7601                        ioff,
7602                        icnt,
7603                        uoff,
7604                    } => {
7605                        draw_indexed_mesh!(&pipes.mesh_overlay, uoff, voff, vcnt, ioff, icnt);
7606                    }
7607
7608                    Cmd::VectorClipPush {
7609                        voff,
7610                        vcnt,
7611                        ioff,
7612                        icnt,
7613                        uoff,
7614                        scissor,
7615                        difference,
7616                    } => {
7617                        let scissor =
7618                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
7619                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
7620                        rpass.set_stencil_reference(clip_depth);
7621                        draw_indexed_mesh!(&pipes.mesh_clip_inc, uoff, voff, vcnt, ioff, icnt);
7622                        if !difference {
7623                            clip_depth = (clip_depth + 1).min(255);
7624                            rpass.set_stencil_reference(clip_depth);
7625                        }
7626                        // Difference masks increment without bumping the depth:
7627                        // content keeps testing `Equal(depth)`, which now fails
7628                        // exactly inside the mask. Exact for a lone mask and
7629                        // for a mask inside intersect clips.
7630                    }
7631
7632                    Cmd::VectorClipPop {
7633                        voff,
7634                        vcnt,
7635                        ioff,
7636                        icnt,
7637                        uoff,
7638                        scissor,
7639                        difference,
7640                    } => {
7641                        // Decrement the mask while the stencil reference is
7642                        // still at the depth it was incremented to, so the
7643                        // equal-compare fires; then step the clip depth down.
7644                        // A difference mask incremented *above* the depth, so
7645                        // test depth+1 and leave the depth unchanged.
7646                        if difference {
7647                            rpass.set_stencil_reference((clip_depth + 1).min(255));
7648                        } else {
7649                            rpass.set_stencil_reference(clip_depth);
7650                        }
7651                        let scissor =
7652                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
7653                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
7654                        draw_indexed_mesh!(&pipes.mesh_clip_dec, uoff, voff, vcnt, ioff, icnt);
7655                        if !difference {
7656                            clip_depth = clip_depth.saturating_sub(1);
7657                        }
7658                        rpass.set_stencil_reference(clip_depth);
7659                    }
7660
7661                    Cmd::Callback { rect, payload } => {
7662                        if let Some(cb) = payload.downcast_ref::<Callback>() {
7663                            let vp_x = rect.x.floor().max(0.0);
7664                            let vp_y = rect.y.floor().max(0.0);
7665                            let vp_w = rect.w.ceil().max(1.0);
7666                            let vp_h = rect.h.ceil().max(1.0);
7667                            if vp_w > 0.0 && vp_h > 0.0 {
7668                                rpass.set_viewport(vp_x, vp_y, vp_w, vp_h, 0.0, 1.0);
7669                                let info = repose_core::PaintCallbackInfo {
7670                                    viewport: rect,
7671                                    clip_rect: rect,
7672                                    pixels_per_point: self.pixels_per_point,
7673                                    screen_size_px: [tw, th],
7674                                };
7675                                let rpass_static: &mut wgpu::RenderPass<'static> = unsafe {
7676                                    std::mem::transmute::<
7677                                        &mut wgpu::RenderPass<'_>,
7678                                        &mut wgpu::RenderPass<'static>,
7679                                    >(&mut rpass)
7680                                };
7681                                cb.0.paint(info, rpass_static, &self.callback_resources);
7682                                rpass.set_viewport(0.0, 0.0, tw as f32, th as f32, 0.0, 1.0);
7683                                rpass.set_bind_group(0, &self.globals_bind, &[]);
7684                                rpass.set_stencil_reference(clip_depth);
7685                            }
7686                        } else {
7687                            log::warn!("Unknown paint callback payload");
7688                        }
7689                    }
7690                }
7691            }
7692            if is_layer {
7693                clip_depth = clip_depth_stack.pop().unwrap_or(0);
7694            }
7695        }
7696
7697        // frame's ids so the next translation drains their textures.
7698        self.flatten_layer_ids = flatten_ids_used;
7699
7700        // Display pass: linear working space -> sRGB OETF -> swapchain
7701        if self.working_space
7702            && let (Some(_ws_view), Some(ws_bind), Some(display_pipeline)) =
7703                (&self.ws_view, &self.ws_bind, &self.display_pipeline)
7704        {
7705            let swap_view = target_view.clone();
7706            let mut display_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
7707                label: Some("display transform"),
7708                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7709                    view: &swap_view,
7710                    resolve_target: None,
7711                    ops: wgpu::Operations {
7712                        load: wgpu::LoadOp::Load,
7713                        store: wgpu::StoreOp::Store,
7714                    },
7715                    depth_slice: None,
7716                })],
7717                depth_stencil_attachment: None,
7718                timestamp_writes: None,
7719                occlusion_query_set: None,
7720                multiview_mask: None,
7721            });
7722            display_pass.set_pipeline(display_pipeline);
7723            display_pass.set_bind_group(1, ws_bind, &[]);
7724            display_pass.draw(0..3, 0..1);
7725        }
7726
7727        // Frame end maintenance: Evict unused images
7728        self.evict_unused_images();
7729    }
7730
7731    /// Render a scene into an externally-provided texture view.
7732    /// Use this when embedding Repose in a host that owns the GPU.
7733    /// The host is responsible for submitting the encoder and handling present.
7734    pub fn render_to_view(
7735        &mut self,
7736        scene: &Scene,
7737        encoder: &mut wgpu::CommandEncoder,
7738        target_view: &wgpu::TextureView,
7739        width: u32,
7740        height: u32,
7741        clear_color: Option<[f64; 4]>,
7742    ) {
7743        self.resize(width, height);
7744
7745        self.frame_index = self.frame_index.wrapping_add(1);
7746        self.slug_cache.next_frame();
7747
7748        if width == 0 || height == 0 {
7749            return;
7750        }
7751
7752        self.render_scene_to_encoder(scene, encoder, target_view, clear_color);
7753    }
7754}
7755
7756fn clamp_scissor(x: u32, y: u32, w: u32, h: u32, tw: u32, th: u32) -> (u32, u32, u32, u32) {
7757    let x = x.min(tw.saturating_sub(1));
7758    let y = y.min(th.saturating_sub(1));
7759    let w = w.min(tw.saturating_sub(x)).max(1);
7760    let h = h.min(th.saturating_sub(y)).max(1);
7761    (x, y, w, h)
7762}
7763
7764fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> repose_core::Rect {
7765    let x0 = a.x.max(b.x);
7766    let y0 = a.y.max(b.y);
7767    let x1 = (a.x + a.w).min(b.x + b.w);
7768    let y1 = (a.y + a.h).min(b.y + b.h);
7769    repose_core::Rect {
7770        x: x0,
7771        y: y0,
7772        w: (x1 - x0).max(0.0),
7773        h: (y1 - y0).max(0.0),
7774    }
7775}