Skip to main content

repose_render_wgpu/
lib.rs

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