Skip to main content

repose_render_wgpu/
lib.rs

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