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