Skip to main content

repose_render_wgpu/
lib.rs

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