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