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