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::Float32x4,
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::Float32x4,
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::Float32x4,
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::Float32x4,
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::Float32x4,
573            },
574            wgpu::VertexAttribute {
575                shader_location: 7,
576                offset: 64,
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::Float32x4,
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::Float32x4,
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::Float32x4,
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::Float32x4,
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    fwd_mat: [f32; 4],
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    fwd_mat: [f32; 4],
1305}
1306
1307#[repr(C)]
1308#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1309struct EllipseInstance {
1310    xywh: [f32; 4],
1311    color: [f32; 4],
1312    fwd_mat: [f32; 4],
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    fwd_mat: [f32; 4],
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    fwd_mat: [f32; 4],
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    fwd_mat: [f32; 4],
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    fwd_mat: [f32; 4],
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    fwd_mat: [f32; 4],
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    fwd_mat: [f32; 4],
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 cm = current.linear();
1462    let (cm00, cm01, cm10, cm11) = (cm[0], cm[1], cm[2], cm[3]);
1463    let mm00 = mesh[0];
1464    let mm01 = mesh[1];
1465    let mm10 = mesh[2];
1466    let mm11 = mesh[3];
1467    let mtx = mesh[4];
1468    let mty = mesh[5];
1469    let r00 = cm00 * mm00 + cm01 * mm10;
1470    let r01 = cm00 * mm01 + cm01 * mm11;
1471    let r10 = cm10 * mm00 + cm11 * mm10;
1472    let r11 = cm10 * mm01 + cm11 * mm11;
1473    let tx = cm00 * mtx + cm01 * mty + current.translate_x;
1474    let ty = cm10 * mtx + cm11 * mty + current.translate_y;
1475    // Canonical slot order consumed by `MeshUniform`/shader and `mesh_aabb`:
1476    // [A, B, tx, C, D, ty] where world = [[A,B],[C,D]] * local + (tx, ty).
1477    [r00, r01, tx, r10, r11, ty]
1478}
1479
1480fn mesh_aabb(mesh: &repose_core::VectorMeshData, affine: [f32; 6]) -> repose_core::Rect {
1481    let mut min_x = f32::MAX;
1482    let mut min_y = f32::MAX;
1483    let mut max_x = f32::MIN;
1484    let mut max_y = f32::MIN;
1485    for v in mesh.vertices.iter() {
1486        let x = affine[0] * v.pos[0] + affine[1] * v.pos[1] + affine[2];
1487        let y = affine[3] * v.pos[0] + affine[4] * v.pos[1] + affine[5];
1488        min_x = min_x.min(x);
1489        min_y = min_y.min(y);
1490        max_x = max_x.max(x);
1491        max_y = max_y.max(y);
1492    }
1493    let w = (max_x - min_x).max(0.0);
1494    let h = (max_y - min_y).max(0.0);
1495    if !min_x.is_finite() || !min_y.is_finite() {
1496        return repose_core::Rect {
1497            x: 0.0,
1498            y: 0.0,
1499            w: 0.0,
1500            h: 0.0,
1501        };
1502    }
1503    repose_core::Rect {
1504        x: min_x,
1505        y: min_y,
1506        w,
1507        h,
1508    }
1509}
1510
1511fn swash_to_a8_coverage(content: repose_text::SwashContent, data: &[u8]) -> Option<Vec<u8>> {
1512    match content {
1513        repose_text::SwashContent::Mask => Some(data.to_vec()),
1514        repose_text::SwashContent::SubpixelMask => {
1515            let mut out = Vec::with_capacity(data.len() / 4);
1516            for px in data.as_chunks::<4>().0 {
1517                let r = px[0];
1518                let g = px[1];
1519                let b = px[2];
1520                out.push(r.max(g).max(b));
1521            }
1522            Some(out)
1523        }
1524        repose_text::SwashContent::Color => None,
1525    }
1526}
1527
1528impl WgpuSceneRenderer {
1529    pub fn from_device(
1530        device: wgpu::Device,
1531        queue: wgpu::Queue,
1532        output_format: wgpu::TextureFormat,
1533        msaa_samples: u32,
1534    ) -> Self {
1535        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1536            label: Some("globals layout"),
1537            entries: &[wgpu::BindGroupLayoutEntry {
1538                binding: 0,
1539                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1540                ty: wgpu::BindingType::Buffer {
1541                    ty: wgpu::BufferBindingType::Uniform,
1542                    has_dynamic_offset: false,
1543                    min_binding_size: None,
1544                },
1545                count: None,
1546            }],
1547        });
1548
1549        let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
1550            label: Some("globals buf"),
1551            size: std::mem::size_of::<Globals>() as u64,
1552            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1553            mapped_at_creation: false,
1554        });
1555
1556        let globals_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1557            label: Some("globals bind"),
1558            layout: &globals_layout,
1559            entries: &[wgpu::BindGroupEntry {
1560                binding: 0,
1561                resource: globals_buf.as_entire_binding(),
1562            }],
1563        });
1564
1565        let ds_format = wgpu::TextureFormat::Depth24PlusStencil8;
1566
1567        let stencil_for_content = wgpu::DepthStencilState {
1568            format: ds_format,
1569            depth_write_enabled: Some(false),
1570            depth_compare: Some(wgpu::CompareFunction::Always),
1571            stencil: wgpu::StencilState {
1572                front: wgpu::StencilFaceState {
1573                    compare: wgpu::CompareFunction::LessEqual,
1574                    fail_op: wgpu::StencilOperation::Keep,
1575                    depth_fail_op: wgpu::StencilOperation::Keep,
1576                    pass_op: wgpu::StencilOperation::Keep,
1577                },
1578                back: 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                read_mask: 0xFF,
1585                write_mask: 0x00,
1586            },
1587            bias: wgpu::DepthBiasState::default(),
1588        };
1589
1590        let stencil_for_clip_inc = wgpu::DepthStencilState {
1591            format: ds_format,
1592            depth_write_enabled: Some(false),
1593            depth_compare: Some(wgpu::CompareFunction::Always),
1594            stencil: wgpu::StencilState {
1595                front: wgpu::StencilFaceState {
1596                    compare: wgpu::CompareFunction::Equal,
1597                    fail_op: wgpu::StencilOperation::Keep,
1598                    depth_fail_op: wgpu::StencilOperation::Keep,
1599                    pass_op: wgpu::StencilOperation::IncrementClamp,
1600                },
1601                back: 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                read_mask: 0xFF,
1608                write_mask: 0xFF,
1609            },
1610            bias: wgpu::DepthBiasState::default(),
1611        };
1612
1613        let stencil_for_clip_dec = wgpu::DepthStencilState {
1614            format: ds_format,
1615            depth_write_enabled: Some(false),
1616            depth_compare: Some(wgpu::CompareFunction::Always),
1617            stencil: wgpu::StencilState {
1618                front: wgpu::StencilFaceState {
1619                    compare: wgpu::CompareFunction::Equal,
1620                    fail_op: wgpu::StencilOperation::Keep,
1621                    depth_fail_op: wgpu::StencilOperation::Keep,
1622                    pass_op: wgpu::StencilOperation::DecrementClamp,
1623                },
1624                back: 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                read_mask: 0xFF,
1631                write_mask: 0xFF,
1632            },
1633            bias: wgpu::DepthBiasState::default(),
1634        };
1635
1636        let _multisample_state = wgpu::MultisampleState {
1637            count: msaa_samples,
1638            mask: !0,
1639            alpha_to_coverage_enabled: false,
1640        };
1641
1642        // PIPELINES
1643
1644        // Single shared sampler for images/text
1645        let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1646            label: Some("image/text sampler"),
1647            address_mode_u: wgpu::AddressMode::ClampToEdge,
1648            address_mode_v: wgpu::AddressMode::ClampToEdge,
1649            mag_filter: wgpu::FilterMode::Linear,
1650            min_filter: wgpu::FilterMode::Linear,
1651            mipmap_filter: wgpu::MipmapFilterMode::Linear,
1652            ..Default::default()
1653        });
1654
1655        // linear filtering only blurs them; nearest keeps the blit crisp.
1656        let layer_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1657            label: Some("layer nearest sampler"),
1658            address_mode_u: wgpu::AddressMode::ClampToEdge,
1659            address_mode_v: wgpu::AddressMode::ClampToEdge,
1660            mag_filter: wgpu::FilterMode::Nearest,
1661            min_filter: wgpu::FilterMode::Nearest,
1662            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
1663            ..Default::default()
1664        });
1665
1666        // Linear taps for Gaussian blur/shadow passes; nearest is kept for
1667        // the sharp 1:1 layer composite.
1668        let layer_sampler_linear = device.create_sampler(&wgpu::SamplerDescriptor {
1669            label: Some("layer linear sampler"),
1670            address_mode_u: wgpu::AddressMode::ClampToEdge,
1671            address_mode_v: wgpu::AddressMode::ClampToEdge,
1672            mag_filter: wgpu::FilterMode::Linear,
1673            min_filter: wgpu::FilterMode::Linear,
1674            mipmap_filter: wgpu::MipmapFilterMode::Linear,
1675            ..Default::default()
1676        });
1677
1678        // Layout for Text / RGBA Images (Texture + Sampler)
1679        let text_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1680            label: Some("text/rgba bind layout"),
1681            entries: &[
1682                wgpu::BindGroupLayoutEntry {
1683                    binding: 0,
1684                    visibility: wgpu::ShaderStages::FRAGMENT,
1685                    ty: wgpu::BindingType::Texture {
1686                        multisampled: false,
1687                        view_dimension: wgpu::TextureViewDimension::D2,
1688                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
1689                    },
1690                    count: None,
1691                },
1692                wgpu::BindGroupLayoutEntry {
1693                    binding: 1,
1694                    visibility: wgpu::ShaderStages::FRAGMENT,
1695                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1696                    count: None,
1697                },
1698            ],
1699        });
1700        // We reuse this for RGBA images for simplicity, or create a distinct one
1701        let image_bind_layout_rgba = text_bind_layout.clone();
1702
1703        // Layout for NV12 Images (TextureY + TextureUV + Sampler + YuvTransform uniform)
1704        let image_bind_layout_nv12 =
1705            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1706                label: Some("image bind layout nv12"),
1707                entries: &[
1708                    // Y plane
1709                    wgpu::BindGroupLayoutEntry {
1710                        binding: 0,
1711                        visibility: wgpu::ShaderStages::FRAGMENT,
1712                        ty: wgpu::BindingType::Texture {
1713                            multisampled: false,
1714                            view_dimension: wgpu::TextureViewDimension::D2,
1715                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
1716                        },
1717                        count: None,
1718                    },
1719                    // UV plane
1720                    wgpu::BindGroupLayoutEntry {
1721                        binding: 1,
1722                        visibility: wgpu::ShaderStages::FRAGMENT,
1723                        ty: wgpu::BindingType::Texture {
1724                            multisampled: false,
1725                            view_dimension: wgpu::TextureViewDimension::D2,
1726                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
1727                        },
1728                        count: None,
1729                    },
1730                    // Sampler
1731                    wgpu::BindGroupLayoutEntry {
1732                        binding: 2,
1733                        visibility: wgpu::ShaderStages::FRAGMENT,
1734                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1735                        count: None,
1736                    },
1737                    // YUV transform uniform buffer
1738                    wgpu::BindGroupLayoutEntry {
1739                        binding: 3,
1740                        visibility: wgpu::ShaderStages::FRAGMENT,
1741                        ty: wgpu::BindingType::Buffer {
1742                            ty: wgpu::BufferBindingType::Uniform,
1743                            has_dynamic_offset: false,
1744                            min_binding_size: None,
1745                        },
1746                        count: None,
1747                    },
1748                ],
1749            });
1750
1751        // Clipping layout
1752        let clip_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1753            label: Some("clip pipeline layout"),
1754            bind_group_layouts: &[Some(&globals_layout)],
1755            immediate_size: 0,
1756        });
1757        let clip_vertex_layout = wgpu::VertexBufferLayout {
1758            array_stride: std::mem::size_of::<ClipInstance>() as u64,
1759            step_mode: wgpu::VertexStepMode::Instance,
1760            attributes: &[
1761                wgpu::VertexAttribute {
1762                    shader_location: 0,
1763                    offset: 0,
1764                    format: wgpu::VertexFormat::Float32x4,
1765                },
1766                wgpu::VertexAttribute {
1767                    shader_location: 1,
1768                    offset: 16,
1769                    format: wgpu::VertexFormat::Float32x4,
1770                },
1771                wgpu::VertexAttribute {
1772                    shader_location: 2,
1773                    offset: 32,
1774                    format: wgpu::VertexFormat::Float32x4,
1775                },
1776            ],
1777        };
1778        let clip_color_target = wgpu::ColorTargetState {
1779            format: output_format,
1780            blend: None,
1781            write_mask: wgpu::ColorWrites::empty(),
1782        };
1783
1784        // Bind layout for per-draw vector mesh uniforms (dynamic offset).
1785        let mesh_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1786            label: Some("mesh uniform layout"),
1787            entries: &[wgpu::BindGroupLayoutEntry {
1788                binding: 0,
1789                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1790                ty: wgpu::BindingType::Buffer {
1791                    ty: wgpu::BufferBindingType::Uniform,
1792                    has_dynamic_offset: true,
1793                    min_binding_size: NonZero::new(MESH_UNIFORM_SLOT),
1794                },
1795                count: None,
1796            }],
1797        });
1798        let mesh_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
1799            label: Some("mesh uniform buffer"),
1800            size: MESH_UNIFORM_CAP,
1801            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1802            mapped_at_creation: false,
1803        });
1804        let mesh_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1805            label: Some("mesh uniform bind"),
1806            layout: &mesh_bind_layout,
1807            entries: &[wgpu::BindGroupEntry {
1808                binding: 0,
1809                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1810                    buffer: &mesh_uniform_buf,
1811                    offset: 0,
1812                    size: NonZero::new(MESH_UNIFORM_SLOT),
1813                }),
1814            }],
1815        });
1816
1817        // Two sets of pipelines: one for the MSAA surface pass, one for layer
1818        // render-to-texture passes (sample_count = 1).
1819        let surface_pipes = Pipelines::create(
1820            &device,
1821            output_format,
1822            msaa_samples,
1823            &globals_layout,
1824            &text_bind_layout,
1825            &image_bind_layout_nv12,
1826            &clip_pipeline_layout,
1827            &stencil_for_content,
1828            &stencil_for_clip_inc,
1829            &stencil_for_clip_dec,
1830            &clip_color_target,
1831            &clip_vertex_layout,
1832            &mesh_bind_layout,
1833        );
1834        let layer_pipes = Pipelines::create(
1835            &device,
1836            output_format,
1837            1,
1838            &globals_layout,
1839            &text_bind_layout,
1840            &image_bind_layout_nv12,
1841            &clip_pipeline_layout,
1842            &stencil_for_content,
1843            &stencil_for_clip_inc,
1844            &stencil_for_clip_dec,
1845            &clip_color_target,
1846            &clip_vertex_layout,
1847            &mesh_bind_layout,
1848        );
1849
1850        // Vector glyph rendering always available with tessellation+MSAA approach.
1851        let slug_enabled = true;
1852
1853        // Blur composite ring (for graphics-layer drop shadows)
1854        let blur_ring = UploadRing::new(
1855            &device,
1856            "blur ring",
1857            1024 * 1024,
1858            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1859        );
1860
1861        // Atlases
1862        let atlas_mask = init_atlas_mask(&device);
1863        let atlas_color = init_atlas_color(&device);
1864
1865        // Upload rings
1866        let ring_rect = UploadRing::new(
1867            &device,
1868            "ring rect",
1869            1 << 20,
1870            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1871        );
1872        let ring_border = UploadRing::new(
1873            &device,
1874            "ring border",
1875            1 << 20,
1876            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1877        );
1878        let ring_ellipse = UploadRing::new(
1879            &device,
1880            "ring ellipse",
1881            1 << 20,
1882            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1883        );
1884        let ring_ellipse_border = UploadRing::new(
1885            &device,
1886            "ring ellipse border",
1887            1 << 20,
1888            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1889        );
1890        let ring_arc = UploadRing::new(
1891            &device,
1892            "ring arc",
1893            1 << 20,
1894            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1895        );
1896        let ring_glyph_mask = UploadRing::new(
1897            &device,
1898            "ring glyph mask",
1899            1 << 20,
1900            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1901        );
1902        let ring_glyph_color = UploadRing::new(
1903            &device,
1904            "ring glyph color",
1905            1 << 20,
1906            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1907        );
1908        let ring_slug = UploadRing::new(
1909            &device,
1910            "ring slug",
1911            1 << 22,
1912            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1913        );
1914        let ring_clip = UploadRing::new(
1915            &device,
1916            "ring clip",
1917            1 << 16,
1918            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1919        );
1920        let ring_nv12 = UploadRing::new(
1921            &device,
1922            "ring nv12",
1923            1 << 20,
1924            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1925        );
1926        let ring_mesh_verts = UploadRing::new(
1927            &device,
1928            "ring mesh verts",
1929            1 << 22,
1930            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1931        );
1932        let ring_mesh_indices = UploadRing::new(
1933            &device,
1934            "ring mesh indices",
1935            1 << 22,
1936            wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1937        );
1938
1939        // Placeholder textures
1940        let depth_stencil_tex = device.create_texture(&wgpu::TextureDescriptor {
1941            label: Some("temp ds"),
1942            size: wgpu::Extent3d {
1943                width: 1,
1944                height: 1,
1945                depth_or_array_layers: 1,
1946            },
1947            mip_level_count: 1,
1948            sample_count: 1,
1949            dimension: wgpu::TextureDimension::D2,
1950            format: wgpu::TextureFormat::Depth24PlusStencil8,
1951            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1952            view_formats: &[],
1953        });
1954        let depth_stencil_view =
1955            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
1956
1957        let mut renderer = WgpuSceneRenderer {
1958            device,
1959            queue,
1960            output_format,
1961            output_width: 0,
1962            output_height: 0,
1963            pixels_per_point: 1.0,
1964
1965            surface_pipes,
1966            layer_pipes,
1967
1968            rects: InstancedPipe::new(ring_rect),
1969            borders: InstancedPipe::new(ring_border),
1970            ellipses: InstancedPipe::new(ring_ellipse),
1971            ellipse_borders: InstancedPipe::new(ring_ellipse_border),
1972            arcs: InstancedPipe::new(ring_arc),
1973            glyph_mask: InstancedPipe::new(ring_glyph_mask),
1974            glyph_color: InstancedPipe::new(ring_glyph_color),
1975
1976            text_bind_layout,
1977
1978            image_bind_layout_rgba,
1979            image_bind_layout_nv12,
1980            image_sampler,
1981            layer_sampler,
1982            layer_sampler_linear,
1983
1984            blur_ring,
1985
1986            slug_enabled,
1987            slug_ring: ring_slug,
1988            slug_cache: slug::GlyphSlugCache::new(),
1989
1990            clip_ring: ring_clip,
1991
1992            nv12: InstancedPipe::new(ring_nv12),
1993
1994            mesh_verts: ring_mesh_verts,
1995            mesh_indices: ring_mesh_indices,
1996            mesh_uniform_buf,
1997            mesh_bind_layout,
1998            mesh_bind,
1999            mesh_uniform_head: 0,
2000            mesh_clip_stack: Vec::new(),
2001
2002            msaa_samples,
2003            depth_stencil_tex,
2004            depth_stencil_view,
2005            msaa_tex: None,
2006            msaa_view: None,
2007            globals_bind,
2008            globals_buf,
2009
2010            atlas_mask,
2011            atlas_color,
2012
2013            next_image_handle: 1,
2014            images: HashMap::new(),
2015            retained: HashMap::new(),
2016
2017            frame_index: 0,
2018            image_bytes_total: 0,
2019            image_evict_after_frames: 600,         // ~10s @ 60fps
2020            image_budget_bytes: 512 * 1024 * 1024, // 512 MB
2021            layer_pool: HashMap::new(),
2022
2023            working_space: false,
2024            ws_tex: None,
2025            ws_view: None,
2026            ws_bind: None,
2027            display_pipeline: None,
2028            display_layout: None,
2029
2030            callback_resources: CallbackResources::default(),
2031        };
2032
2033        renderer.recreate_msaa_and_depth_stencil();
2034        renderer
2035    }
2036}
2037
2038impl WgpuSurfaceBackend {
2039    #[cfg(feature = "winit-surface")]
2040    pub async fn new_async(
2041        window: Arc<winit::window::Window>,
2042    ) -> anyhow::Result<WgpuSurfaceBackend> {
2043        Self::new_async_with_options(window, 4, PresentModePref::Auto).await
2044    }
2045
2046    /// Create a windowed surface backend, honoring the requested MSAA sample
2047    /// count (falling back to the largest supported count <= `msaa_samples`).
2048    #[cfg(feature = "winit-surface")]
2049    pub async fn new_async_with_msaa(
2050        window: Arc<winit::window::Window>,
2051        msaa_samples: u32,
2052    ) -> anyhow::Result<WgpuSurfaceBackend> {
2053        Self::new_async_with_options(window, msaa_samples, PresentModePref::Auto).await
2054    }
2055
2056    /// Create a windowed surface backend, honoring the requested MSAA sample
2057    /// count and present-mode preference.
2058    #[cfg(feature = "winit-surface")]
2059    pub async fn new_async_with_options(
2060        window: Arc<winit::window::Window>,
2061        msaa_samples: u32,
2062        present_mode: PresentModePref,
2063    ) -> anyhow::Result<WgpuSurfaceBackend> {
2064        let instance: Instance = if cfg!(target_arch = "wasm32") {
2065            let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
2066            desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
2067            wgpu::util::new_instance_with_webgpu_detection(desc).await
2068        } else {
2069            wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle())
2070        };
2071
2072        let surface = instance.create_surface(window.clone())?;
2073
2074        let adapter = instance
2075            .request_adapter(&wgpu::RequestAdapterOptions {
2076                power_preference: wgpu::PowerPreference::HighPerformance,
2077                compatible_surface: Some(&surface),
2078                force_fallback_adapter: false,
2079                apply_limit_buckets: false,
2080            })
2081            .await
2082            .map_err(|e| anyhow::anyhow!("No suitable adapter: {e:?}"))?;
2083
2084        let limits = adapter.limits();
2085
2086        #[cfg(target_os = "linux")]
2087        let features = {
2088            let af = adapter.features();
2089            let mut f = wgpu::Features::empty();
2090            if af.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_FD) {
2091                f |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_FD;
2092            }
2093            if af.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF) {
2094                f |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF;
2095            }
2096            f
2097        };
2098        #[cfg(not(target_os = "linux"))]
2099        let features = wgpu::Features::empty();
2100
2101        let (device, queue) = adapter
2102            .request_device(&wgpu::DeviceDescriptor {
2103                label: Some("repose-rs device"),
2104                required_features: features,
2105                required_limits: limits,
2106                experimental_features: wgpu::ExperimentalFeatures::disabled(),
2107                memory_hints: wgpu::MemoryHints::default(),
2108                trace: wgpu::Trace::Off,
2109            })
2110            .await
2111            .map_err(|e| anyhow::anyhow!("request_device failed: {e:?}"))?;
2112
2113        let size = window.inner_size();
2114
2115        let caps = surface.get_capabilities(&adapter);
2116
2117        let (format, view_format) = if cfg!(target_arch = "wasm32")
2118            && adapter
2119                .get_downlevel_capabilities()
2120                .flags
2121                .contains(wgpu::DownlevelFlags::SURFACE_VIEW_FORMATS)
2122        {
2123            let non_srgb = caps
2124                .formats
2125                .iter()
2126                .copied()
2127                .find(|f| !f.is_srgb())
2128                .unwrap_or(caps.formats[0]);
2129            (non_srgb, Some(non_srgb.add_srgb_suffix()))
2130        } else if cfg!(target_arch = "wasm32") {
2131            let fmt = caps
2132                .formats
2133                .iter()
2134                .copied()
2135                .find(|f| f.is_srgb())
2136                .unwrap_or(caps.formats[0]);
2137            (fmt, None)
2138        } else {
2139            let fmt = caps
2140                .formats
2141                .iter()
2142                .copied()
2143                .find(|f| f.is_srgb())
2144                .unwrap_or(caps.formats[0]);
2145            (fmt, None)
2146        };
2147
2148        let present_mode = pick_present_mode(&caps, present_mode);
2149        let alpha_mode = caps.alpha_modes[0];
2150
2151        let render_format = view_format.unwrap_or(format);
2152        let msaa_samples = pick_surface_msaa(&adapter, format, msaa_samples);
2153        let renderer = WgpuSceneRenderer::from_device(device, queue, render_format, msaa_samples);
2154
2155        let view_formats = view_format.into_iter().collect::<Vec<_>>();
2156
2157        let config = wgpu::SurfaceConfiguration {
2158            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2159            format,
2160            width: size.width.max(1),
2161            height: size.height.max(1),
2162            present_mode,
2163            alpha_mode,
2164            color_space: wgpu::SurfaceColorSpace::Auto,
2165            view_formats,
2166            desired_maximum_frame_latency: 1,
2167        };
2168        surface.configure(&renderer.device, &config);
2169
2170        Ok(WgpuSurfaceBackend {
2171            surface: Some(surface),
2172            surface_config: Some(config),
2173            renderer,
2174        })
2175    }
2176
2177    #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
2178    pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<WgpuSurfaceBackend> {
2179        pollster::block_on(Self::new_async(window))
2180    }
2181
2182    #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
2183    pub fn new_with_msaa(
2184        window: Arc<winit::window::Window>,
2185        msaa_samples: u32,
2186    ) -> anyhow::Result<WgpuSurfaceBackend> {
2187        pollster::block_on(Self::new_async_with_msaa(window, msaa_samples))
2188    }
2189
2190    #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
2191    pub fn new_with_options(
2192        window: Arc<winit::window::Window>,
2193        msaa_samples: u32,
2194        present_mode: PresentModePref,
2195    ) -> anyhow::Result<WgpuSurfaceBackend> {
2196        pollster::block_on(Self::new_async_with_options(
2197            window,
2198            msaa_samples,
2199            present_mode,
2200        ))
2201    }
2202
2203    #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
2204    pub fn new(_window: Arc<winit::window::Window>) -> anyhow::Result<WgpuSurfaceBackend> {
2205        anyhow::bail!("Use WgpuSurfaceBackend::new_async(window).await on wasm32")
2206    }
2207
2208    #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
2209    pub fn new_with_msaa(
2210        _window: Arc<winit::window::Window>,
2211        _msaa_samples: u32,
2212    ) -> anyhow::Result<WgpuSurfaceBackend> {
2213        anyhow::bail!("Use WgpuSurfaceBackend::new_async_with_msaa(window, msaa).await on wasm32")
2214    }
2215
2216    #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
2217    pub fn new_with_options(
2218        _window: Arc<winit::window::Window>,
2219        _msaa_samples: u32,
2220        _present_mode: PresentModePref,
2221    ) -> anyhow::Result<WgpuSurfaceBackend> {
2222        anyhow::bail!(
2223            "Use WgpuSurfaceBackend::new_async_with_options(window, msaa, mode).await on wasm32"
2224        )
2225    }
2226}
2227
2228/// Pick the swapchain present mode honoring `pref`, falling back to an "auto"
2229/// Fifo-first selection when the preferred mode is unavailable.
2230fn pick_present_mode(caps: &wgpu::SurfaceCapabilities, pref: PresentModePref) -> wgpu::PresentMode {
2231    let auto = || {
2232        caps.present_modes
2233            .iter()
2234            .copied()
2235            .find(|m| *m == wgpu::PresentMode::Fifo)
2236            .or_else(|| {
2237                caps.present_modes
2238                    .iter()
2239                    .copied()
2240                    .find(|m| *m == wgpu::PresentMode::Mailbox)
2241            })
2242            .unwrap_or(wgpu::PresentMode::Immediate)
2243    };
2244    match pref {
2245        PresentModePref::Auto => auto(),
2246        PresentModePref::Fifo if caps.present_modes.contains(&wgpu::PresentMode::Fifo) => {
2247            wgpu::PresentMode::Fifo
2248        }
2249        PresentModePref::Mailbox if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) => {
2250            wgpu::PresentMode::Mailbox
2251        }
2252        PresentModePref::Immediate
2253            if caps.present_modes.contains(&wgpu::PresentMode::Immediate) =>
2254        {
2255            wgpu::PresentMode::Immediate
2256        }
2257        _ => auto(),
2258    }
2259}
2260
2261/// Pick the MSAA sample count for the surface pass, honoring `requested` and
2262/// falling back to the largest supported count <= it.
2263pub fn pick_surface_msaa(
2264    adapter: &wgpu::Adapter,
2265    format: wgpu::TextureFormat,
2266    requested: u32,
2267) -> u32 {
2268    let requested = requested.max(1);
2269    let color_feat = adapter.get_texture_format_features(format);
2270    let depth_feat = adapter.get_texture_format_features(wgpu::TextureFormat::Depth24PlusStencil8);
2271    let supported = |n: u32| {
2272        color_feat.flags.sample_count_supported(n)
2273            && color_feat
2274                .flags
2275                .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE)
2276            && depth_feat.flags.sample_count_supported(n)
2277    };
2278    let mut candidates = vec![requested];
2279    for n in [8, 4, 2, 1] {
2280        if n < requested {
2281            candidates.push(n);
2282        }
2283    }
2284    let chosen = candidates.into_iter().find(|&n| supported(n)).unwrap_or(1);
2285    if chosen != requested {
2286        log::info!("requested MSAA x{requested}, using x{chosen}");
2287    }
2288    chosen
2289}
2290
2291impl WgpuSceneRenderer {
2292    // Image API
2293
2294    pub fn set_image_from_bytes(
2295        &mut self,
2296        handle: u64,
2297        data: &[u8],
2298        srgb: bool,
2299    ) -> anyhow::Result<()> {
2300        let img = image::load_from_memory(data)?;
2301        let rgba = img.to_rgba8();
2302        let (w, h) = rgba.dimensions();
2303        self.set_image_rgba8(handle, w, h, &rgba, srgb)
2304    }
2305
2306    pub fn set_image_rgba8(
2307        &mut self,
2308        handle: u64,
2309        w: u32,
2310        h: u32,
2311        rgba: &[u8],
2312        srgb: bool,
2313    ) -> anyhow::Result<()> {
2314        let expected = (w as usize) * (h as usize) * 4;
2315        if rgba.len() < expected {
2316            return Err(anyhow::anyhow!(
2317                "RGBA buffer too small: {} < {}",
2318                rgba.len(),
2319                expected
2320            ));
2321        }
2322
2323        let format = if srgb {
2324            wgpu::TextureFormat::Rgba8UnormSrgb
2325        } else {
2326            wgpu::TextureFormat::Rgba8Unorm
2327        };
2328
2329        let needs_recreate = match self.images.get(&handle) {
2330            Some(ImageTex::Rgba {
2331                w: cw,
2332                h: ch,
2333                format: cf,
2334                ..
2335            }) => *cw != w || *ch != h || *cf != format,
2336            _ => true,
2337        };
2338
2339        if needs_recreate {
2340            self.remove_image(handle);
2341
2342            let (tex, bind) = self.create_rgba_tex(w, h, format);
2343            let bytes = (w as u64) * (h as u64) * 4;
2344            self.image_bytes_total += bytes;
2345
2346            self.images.insert(
2347                handle,
2348                ImageTex::Rgba {
2349                    tex,
2350                    bind,
2351                    w,
2352                    h,
2353                    format,
2354                    last_used_frame: self.frame_index,
2355                    bytes,
2356                },
2357            );
2358        }
2359
2360        self.retained.insert(
2361            handle,
2362            RetainedImage {
2363                w,
2364                h,
2365                format,
2366                rgba: rgba[..expected].to_vec(),
2367            },
2368        );
2369
2370        let tex = match self.images.get(&handle) {
2371            Some(ImageTex::Rgba { tex, .. }) => tex,
2372            _ => unreachable!(),
2373        };
2374
2375        self.queue.write_texture(
2376            wgpu::TexelCopyTextureInfo {
2377                texture: tex,
2378                mip_level: 0,
2379                origin: wgpu::Origin3d::ZERO,
2380                aspect: wgpu::TextureAspect::All,
2381            },
2382            &rgba[..expected],
2383            wgpu::TexelCopyBufferLayout {
2384                offset: 0,
2385                bytes_per_row: Some(4 * w),
2386                rows_per_image: Some(h),
2387            },
2388            wgpu::Extent3d {
2389                width: w,
2390                height: h,
2391                depth_or_array_layers: 1,
2392            },
2393        );
2394
2395        // Ensure budget limits
2396        self.evict_budget_excess();
2397
2398        Ok(())
2399    }
2400
2401    /// Create (but do not populate) the GPU texture, view and bind group for an
2402    /// RGBA image. Pixels are written separately via `write_texture`.
2403    fn create_rgba_tex(
2404        &self,
2405        w: u32,
2406        h: u32,
2407        format: wgpu::TextureFormat,
2408    ) -> (wgpu::Texture, wgpu::BindGroup) {
2409        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
2410            label: Some("user image rgba"),
2411            size: wgpu::Extent3d {
2412                width: w,
2413                height: h,
2414                depth_or_array_layers: 1,
2415            },
2416            mip_level_count: 1,
2417            sample_count: 1,
2418            dimension: wgpu::TextureDimension::D2,
2419            format,
2420            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2421            view_formats: &[],
2422        });
2423        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
2424
2425        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2426            label: Some("image bind rgba"),
2427            layout: &self.image_bind_layout_rgba,
2428            entries: &[
2429                wgpu::BindGroupEntry {
2430                    binding: 0,
2431                    resource: wgpu::BindingResource::TextureView(&view),
2432                },
2433                wgpu::BindGroupEntry {
2434                    binding: 1,
2435                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
2436                },
2437            ],
2438        });
2439
2440        (tex, bind)
2441    }
2442
2443    /// Register an externally-created `wgpu::TextureView` as an image (zero-copy).
2444    pub fn register_native_texture(
2445        &mut self,
2446        view: &wgpu::TextureView,
2447        width: u32,
2448        height: u32,
2449    ) -> u64 {
2450        let handle = self.next_image_handle;
2451        self.next_image_handle += 1;
2452        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2453            label: Some("user native image"),
2454            layout: &self.image_bind_layout_rgba,
2455            entries: &[
2456                wgpu::BindGroupEntry {
2457                    binding: 0,
2458                    resource: wgpu::BindingResource::TextureView(view),
2459                },
2460                wgpu::BindGroupEntry {
2461                    binding: 1,
2462                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
2463                },
2464            ],
2465        });
2466        self.images.insert(
2467            handle,
2468            ImageTex::User {
2469                bind,
2470                w: width,
2471                h: height,
2472                last_used_frame: self.frame_index,
2473                bytes: 0,
2474            },
2475        );
2476        handle
2477    }
2478
2479    /// Like `register_native_texture` but with custom sampler descriptor.
2480    pub fn register_native_texture_with_sampler(
2481        &mut self,
2482        view: &wgpu::TextureView,
2483        sampler_desc: wgpu::SamplerDescriptor<'_>,
2484        width: u32,
2485        height: u32,
2486    ) -> u64 {
2487        let handle = self.next_image_handle;
2488        self.next_image_handle += 1;
2489        let sampler = self.device.create_sampler(&sampler_desc);
2490        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2491            label: Some("user native image sampleropts"),
2492            layout: &self.image_bind_layout_rgba,
2493            entries: &[
2494                wgpu::BindGroupEntry {
2495                    binding: 0,
2496                    resource: wgpu::BindingResource::TextureView(view),
2497                },
2498                wgpu::BindGroupEntry {
2499                    binding: 1,
2500                    resource: wgpu::BindingResource::Sampler(&sampler),
2501                },
2502            ],
2503        });
2504        self.images.insert(
2505            handle,
2506            ImageTex::User {
2507                bind,
2508                w: width,
2509                h: height,
2510                last_used_frame: self.frame_index,
2511                bytes: 0,
2512            },
2513        );
2514        handle
2515    }
2516
2517    /// Update an existing native texture handle with a new view (reuse handle).
2518    pub fn update_native_texture(&mut self, handle: u64, view: &wgpu::TextureView) {
2519        let Some(entry) = self.images.get_mut(&handle) else {
2520            log::warn!("update_native_texture: handle {handle} not found");
2521            return;
2522        };
2523        let w = match entry {
2524            ImageTex::User { w, .. } => *w,
2525            ImageTex::Rgba { w, .. } => *w,
2526            _ => {
2527                log::warn!("update_native_texture: handle {handle} is not rgba/user");
2528                return;
2529            }
2530        };
2531        let h = match entry {
2532            ImageTex::User { h, .. } => *h,
2533            ImageTex::Rgba { h, .. } => *h,
2534            _ => 0,
2535        };
2536        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2537            label: Some("user native image update"),
2538            layout: &self.image_bind_layout_rgba,
2539            entries: &[
2540                wgpu::BindGroupEntry {
2541                    binding: 0,
2542                    resource: wgpu::BindingResource::TextureView(view),
2543                },
2544                wgpu::BindGroupEntry {
2545                    binding: 1,
2546                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
2547                },
2548            ],
2549        });
2550        *entry = ImageTex::User {
2551            bind,
2552            w,
2553            h,
2554            last_used_frame: self.frame_index,
2555            bytes: 0,
2556        };
2557    }
2558
2559    pub fn set_image_nv12(
2560        &mut self,
2561        handle: u64,
2562        w: u32,
2563        h: u32,
2564        y: &[u8],
2565        uv: &[u8],
2566        color_info: ColorInfo,
2567    ) -> anyhow::Result<()> {
2568        let y_expected = (w as usize) * (h as usize);
2569        let uv_w = w.div_ceil(2);
2570        let uv_h = h.div_ceil(2);
2571        let uv_expected = (uv_w as usize) * (uv_h as usize) * 2;
2572
2573        if y.len() < y_expected {
2574            return Err(anyhow::anyhow!("Y plane too small"));
2575        }
2576        if uv.len() < uv_expected {
2577            return Err(anyhow::anyhow!("UV plane too small"));
2578        }
2579
2580        let needs_recreate = match self.images.get(&handle) {
2581            Some(ImageTex::Nv12 { w: ww, h: hh, .. }) => *ww != w || *hh != h,
2582            _ => true,
2583        };
2584
2585        // Compute the YUV->RGB transform on the CPU.
2586        let yuv = color_info.to_yuv_transform();
2587        let yuv_raw = YuvTransformRaw {
2588            row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
2589            row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
2590            row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
2591            b: [yuv.b[0], yuv.b[1], yuv.b[2], 0.0],
2592        };
2593
2594        if needs_recreate {
2595            self.remove_image(handle);
2596
2597            let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
2598                label: Some("nv12 Y"),
2599                size: wgpu::Extent3d {
2600                    width: w,
2601                    height: h,
2602                    depth_or_array_layers: 1,
2603                },
2604                mip_level_count: 1,
2605                sample_count: 1,
2606                dimension: wgpu::TextureDimension::D2,
2607                format: wgpu::TextureFormat::R8Unorm,
2608                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2609                view_formats: &[],
2610            });
2611            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
2612
2613            let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
2614                label: Some("nv12 UV"),
2615                size: wgpu::Extent3d {
2616                    width: uv_w,
2617                    height: uv_h,
2618                    depth_or_array_layers: 1,
2619                },
2620                mip_level_count: 1,
2621                sample_count: 1,
2622                dimension: wgpu::TextureDimension::D2,
2623                format: wgpu::TextureFormat::Rg8Unorm,
2624                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2625                view_formats: &[],
2626            });
2627            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
2628
2629            // Create a uniform buffer for the YUV transform (per-image).
2630            let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
2631                label: Some("nv12 yuv transform"),
2632                size: std::mem::size_of::<YuvTransformRaw>() as u64,
2633                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2634                mapped_at_creation: false,
2635            });
2636
2637            // Write initial transform.
2638            self.queue
2639                .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
2640
2641            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2642                label: Some("nv12 bind"),
2643                layout: &self.image_bind_layout_nv12,
2644                entries: &[
2645                    wgpu::BindGroupEntry {
2646                        binding: 0,
2647                        resource: wgpu::BindingResource::TextureView(&view_y),
2648                    },
2649                    wgpu::BindGroupEntry {
2650                        binding: 1,
2651                        resource: wgpu::BindingResource::TextureView(&view_uv),
2652                    },
2653                    wgpu::BindGroupEntry {
2654                        binding: 2,
2655                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
2656                    },
2657                    wgpu::BindGroupEntry {
2658                        binding: 3,
2659                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2660                            buffer: &yuv_buf,
2661                            offset: 0,
2662                            size: None,
2663                        }),
2664                    },
2665                ],
2666            });
2667
2668            let bytes = (w as u64) * (h as u64)
2669                + (uv_w as u64) * (uv_h as u64) * 2
2670                + std::mem::size_of::<YuvTransformRaw>() as u64;
2671            self.image_bytes_total += bytes;
2672
2673            self.images.insert(
2674                handle,
2675                ImageTex::Nv12 {
2676                    tex_y,
2677                    tex_uv,
2678                    bind,
2679                    yuv_buf,
2680                    w,
2681                    h,
2682                    color_info,
2683                    last_used_frame: self.frame_index,
2684                    bytes,
2685                },
2686            );
2687        } else {
2688            // Re-use existing textures; just update the YUV transform if needed.
2689            if let Some(ImageTex::Nv12 { yuv_buf, .. }) = self.images.get(&handle) {
2690                self.queue
2691                    .write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
2692            }
2693        }
2694
2695        let (tex_y, tex_uv, _bind) = match self.images.get(&handle) {
2696            Some(ImageTex::Nv12 {
2697                tex_y,
2698                tex_uv,
2699                bind,
2700                ..
2701            }) => (tex_y, tex_uv, bind),
2702            _ => return Err(anyhow::anyhow!("Handle is not NV12")),
2703        };
2704
2705        self.queue.write_texture(
2706            wgpu::TexelCopyTextureInfo {
2707                texture: tex_y,
2708                mip_level: 0,
2709                origin: wgpu::Origin3d::ZERO,
2710                aspect: wgpu::TextureAspect::All,
2711            },
2712            &y[..y_expected],
2713            wgpu::TexelCopyBufferLayout {
2714                offset: 0,
2715                bytes_per_row: Some(w),
2716                rows_per_image: Some(h),
2717            },
2718            wgpu::Extent3d {
2719                width: w,
2720                height: h,
2721                depth_or_array_layers: 1,
2722            },
2723        );
2724
2725        self.queue.write_texture(
2726            wgpu::TexelCopyTextureInfo {
2727                texture: tex_uv,
2728                mip_level: 0,
2729                origin: wgpu::Origin3d::ZERO,
2730                aspect: wgpu::TextureAspect::All,
2731            },
2732            &uv[..uv_expected],
2733            wgpu::TexelCopyBufferLayout {
2734                offset: 0,
2735                bytes_per_row: Some(2 * uv_w),
2736                rows_per_image: Some(uv_h),
2737            },
2738            wgpu::Extent3d {
2739                width: uv_w,
2740                height: uv_h,
2741                depth_or_array_layers: 1,
2742            },
2743        );
2744
2745        self.evict_budget_excess();
2746        Ok(())
2747    }
2748
2749    pub fn set_image_planes(
2750        &mut self,
2751        handle: u64,
2752        w: u32,
2753        h: u32,
2754        pixel_format: PixelFormat,
2755        planes: &[&[u8]],
2756        color_info: ColorInfo,
2757    ) -> anyhow::Result<()> {
2758        match pixel_format {
2759            PixelFormat::Nv12 => {
2760                let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
2761                let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
2762                self.set_image_nv12(handle, w, h, y, uv, color_info)
2763            }
2764            PixelFormat::P010 => {
2765                let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
2766                let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
2767                self.set_image_p010(handle, w, h, y, uv, color_info)
2768            }
2769            PixelFormat::I420 | PixelFormat::I444 => Err(anyhow::anyhow!(
2770                "I420/I444 not implemented and unlikely -> cheap to convert to NV12 (better for the GPU too)"
2771            )),
2772            PixelFormat::Rgba => {
2773                let rgba = planes
2774                    .first()
2775                    .ok_or(anyhow::anyhow!("missing RGBA plane"))?;
2776                self.set_image_rgba8(handle, w, h, rgba, false)
2777            }
2778        }
2779    }
2780
2781    fn set_image_p010(
2782        &mut self,
2783        handle: u64,
2784        w: u32,
2785        h: u32,
2786        y: &[u8],
2787        uv: &[u8],
2788        color_info: ColorInfo,
2789    ) -> anyhow::Result<()> {
2790        let uv_w = w.div_ceil(2);
2791        let uv_h = h.div_ceil(2);
2792
2793        let y_expected = (w as usize) * 2;
2794        let uv_expected = (uv_w as usize) * (uv_h as usize) * 4;
2795
2796        if y.len() < y_expected {
2797            return Err(anyhow::anyhow!("P010 Y plane too small"));
2798        }
2799        if uv.len() < uv_expected {
2800            return Err(anyhow::anyhow!("P010 UV plane too small"));
2801        }
2802
2803        // P010 reuses the NV12 pipeline (same bind group layout -> wgpu
2804        // abstracts the storage format so R16Unorm/Rg16Unorm are
2805        // filterable float textures just like R8Unorm/Rg8Unorm).
2806        let needs_recreate = match self.images.get(&handle) {
2807            Some(ImageTex::Nv12 { w: ww, h: hh, .. }) => *ww != w || *hh != h,
2808            _ => true,
2809        };
2810
2811        let yuv = color_info.to_yuv_transform();
2812        let yuv_raw = YuvTransformRaw {
2813            row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
2814            row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
2815            row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
2816            b: [yuv.b[0], yuv.b[1], yuv.b[2], 0.0],
2817        };
2818
2819        if needs_recreate {
2820            self.remove_image(handle);
2821
2822            let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
2823                label: Some("p010 Y"),
2824                size: wgpu::Extent3d {
2825                    width: w,
2826                    height: h,
2827                    depth_or_array_layers: 1,
2828                },
2829                mip_level_count: 1,
2830                sample_count: 1,
2831                dimension: wgpu::TextureDimension::D2,
2832                format: wgpu::TextureFormat::R16Unorm,
2833                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2834                view_formats: &[],
2835            });
2836            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
2837
2838            let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
2839                label: Some("p010 UV"),
2840                size: wgpu::Extent3d {
2841                    width: uv_w,
2842                    height: uv_h,
2843                    depth_or_array_layers: 1,
2844                },
2845                mip_level_count: 1,
2846                sample_count: 1,
2847                dimension: wgpu::TextureDimension::D2,
2848                format: wgpu::TextureFormat::Rg16Unorm,
2849                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2850                view_formats: &[],
2851            });
2852            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
2853
2854            let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
2855                label: Some("p010 yuv transform"),
2856                size: std::mem::size_of::<YuvTransformRaw>() as u64,
2857                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2858                mapped_at_creation: false,
2859            });
2860            self.queue
2861                .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
2862
2863            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2864                label: Some("p010 bind"),
2865                layout: &self.image_bind_layout_nv12,
2866                entries: &[
2867                    wgpu::BindGroupEntry {
2868                        binding: 0,
2869                        resource: wgpu::BindingResource::TextureView(&view_y),
2870                    },
2871                    wgpu::BindGroupEntry {
2872                        binding: 1,
2873                        resource: wgpu::BindingResource::TextureView(&view_uv),
2874                    },
2875                    wgpu::BindGroupEntry {
2876                        binding: 2,
2877                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
2878                    },
2879                    wgpu::BindGroupEntry {
2880                        binding: 3,
2881                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2882                            buffer: &yuv_buf,
2883                            offset: 0,
2884                            size: None,
2885                        }),
2886                    },
2887                ],
2888            });
2889
2890            let bytes = (w as u64) * 2
2891                + (uv_w as u64) * (uv_h as u64) * 4
2892                + std::mem::size_of::<YuvTransformRaw>() as u64;
2893            self.image_bytes_total += bytes;
2894
2895            self.images.insert(
2896                handle,
2897                ImageTex::Nv12 {
2898                    tex_y,
2899                    tex_uv,
2900                    bind,
2901                    yuv_buf,
2902                    w,
2903                    h,
2904                    color_info,
2905                    last_used_frame: self.frame_index,
2906                    bytes,
2907                },
2908            );
2909        } else {
2910            if let Some(ImageTex::Nv12 { yuv_buf, .. }) = self.images.get(&handle) {
2911                self.queue
2912                    .write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
2913            }
2914        }
2915
2916        let (tex_y, tex_uv, _bind) = match self.images.get(&handle) {
2917            Some(ImageTex::Nv12 {
2918                tex_y,
2919                tex_uv,
2920                bind,
2921                ..
2922            }) => (tex_y, tex_uv, bind),
2923            _ => return Err(anyhow::anyhow!("Handle is not P010/NV12")),
2924        };
2925
2926        self.queue.write_texture(
2927            wgpu::TexelCopyTextureInfo {
2928                texture: tex_y,
2929                mip_level: 0,
2930                origin: wgpu::Origin3d::ZERO,
2931                aspect: wgpu::TextureAspect::All,
2932            },
2933            &y[..y_expected],
2934            wgpu::TexelCopyBufferLayout {
2935                offset: 0,
2936                bytes_per_row: Some(w * 2),
2937                rows_per_image: Some(h),
2938            },
2939            wgpu::Extent3d {
2940                width: w,
2941                height: h,
2942                depth_or_array_layers: 1,
2943            },
2944        );
2945        self.queue.write_texture(
2946            wgpu::TexelCopyTextureInfo {
2947                texture: tex_uv,
2948                mip_level: 0,
2949                origin: wgpu::Origin3d::ZERO,
2950                aspect: wgpu::TextureAspect::All,
2951            },
2952            &uv[..uv_expected],
2953            wgpu::TexelCopyBufferLayout {
2954                offset: 0,
2955                bytes_per_row: Some(uv_w * 4),
2956                rows_per_image: Some(uv_h),
2957            },
2958            wgpu::Extent3d {
2959                width: uv_w,
2960                height: uv_h,
2961                depth_or_array_layers: 1,
2962            },
2963        );
2964
2965        self.evict_budget_excess();
2966        Ok(())
2967    }
2968
2969    #[cfg(target_os = "linux")]
2970    pub fn set_image_dmabuf(
2971        &mut self,
2972        handle: u64,
2973        w: u32,
2974        h: u32,
2975        fds: Vec<std::os::unix::io::OwnedFd>,
2976        modifier: u64,
2977        strides: Vec<u32>,
2978        offsets: Vec<u64>,
2979        color_info: ColorInfo,
2980    ) -> anyhow::Result<()> {
2981        log::info!(
2982            "set_image_dmabuf handle={handle} {}x{} fds={} modifier=0x{modifier:x}",
2983            w,
2984            h,
2985            fds.len()
2986        );
2987
2988        self.remove_image(handle);
2989
2990        let yuv = color_info.to_yuv_transform();
2991        let yuv_raw = YuvTransformRaw {
2992            row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
2993            row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
2994            row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
2995            b: [yuv.b[0], yuv.b[1], yuv.b[2], 0.0],
2996        };
2997
2998        if fds.len() != 2 {
2999            return Err(anyhow::anyhow!(
3000                "unsupported fd count {} - need exactly 2 for separate Y/UV planes",
3001                fds.len()
3002            ));
3003        }
3004
3005        let uv_w = w.div_ceil(2);
3006        let uv_h = h.div_ceil(2);
3007
3008        let hal_y_desc = wgpu::hal::TextureDescriptor {
3009            label: Some("dmabuf y"),
3010            size: wgpu::Extent3d {
3011                width: w,
3012                height: h,
3013                depth_or_array_layers: 1,
3014            },
3015            mip_level_count: 1,
3016            sample_count: 1,
3017            dimension: wgpu::TextureDimension::D2,
3018            format: wgpu::TextureFormat::R8Unorm,
3019            usage: wgpu::wgt::TextureUses::RESOURCE,
3020            memory_flags: wgpu::hal::MemoryFlags::empty(),
3021            view_formats: vec![],
3022        };
3023        let hal_uv_desc = wgpu::hal::TextureDescriptor {
3024            label: Some("dmabuf uv"),
3025            size: wgpu::Extent3d {
3026                width: uv_w,
3027                height: uv_h,
3028                depth_or_array_layers: 1,
3029            },
3030            mip_level_count: 1,
3031            sample_count: 1,
3032            dimension: wgpu::TextureDimension::D2,
3033            format: wgpu::TextureFormat::Rg8Unorm,
3034            usage: wgpu::wgt::TextureUses::RESOURCE,
3035            memory_flags: wgpu::hal::MemoryFlags::empty(),
3036            view_formats: vec![],
3037        };
3038
3039        let wgpu_y_desc = wgpu::TextureDescriptor {
3040            label: Some("dmabuf y"),
3041            size: wgpu::Extent3d {
3042                width: w,
3043                height: h,
3044                depth_or_array_layers: 1,
3045            },
3046            mip_level_count: 1,
3047            sample_count: 1,
3048            dimension: wgpu::TextureDimension::D2,
3049            format: wgpu::TextureFormat::R8Unorm,
3050            usage: wgpu::TextureUsages::TEXTURE_BINDING,
3051            view_formats: &[],
3052        };
3053        let wgpu_uv_desc = wgpu::TextureDescriptor {
3054            label: Some("dmabuf uv"),
3055            size: wgpu::Extent3d {
3056                width: uv_w,
3057                height: uv_h,
3058                depth_or_array_layers: 1,
3059            },
3060            mip_level_count: 1,
3061            sample_count: 1,
3062            dimension: wgpu::TextureDimension::D2,
3063            format: wgpu::TextureFormat::Rg8Unorm,
3064            usage: wgpu::TextureUsages::TEXTURE_BINDING,
3065            view_formats: &[],
3066        };
3067
3068        let (tex_y, view_y, tex_uv, view_uv) = unsafe {
3069            let hal_guard = self
3070                .device
3071                .as_hal::<wgpu::hal::vulkan::Api>()
3072                .ok_or_else(|| {
3073                    log::warn!("as_hal::<vulkan::Api> returned None");
3074                    anyhow::anyhow!("Device is not Vulkan")
3075                })?;
3076
3077            let mut fds = fds;
3078            let uv_fd = fds.remove(1);
3079            let y_fd = fds.remove(0);
3080
3081            let yt = hal_guard
3082                .texture_from_dmabuf_fd(y_fd, &hal_y_desc, modifier, strides[0] as u64, offsets[0])
3083                .map_err(|e| anyhow::anyhow!("import Y dmabuf: {e:?}"))?;
3084            log::info!("imported Y dmabuf OK");
3085
3086            let uvt = hal_guard
3087                .texture_from_dmabuf_fd(
3088                    uv_fd,
3089                    &hal_uv_desc,
3090                    modifier,
3091                    strides[1] as u64,
3092                    offsets[1],
3093                )
3094                .map_err(|e| anyhow::anyhow!("import UV dmabuf: {e:?}"))?;
3095            log::info!("imported UV dmabuf OK");
3096
3097            drop(hal_guard);
3098
3099            let tex_y = self
3100                .device
3101                .create_texture_from_hal::<wgpu::hal::vulkan::Api>(
3102                    yt,
3103                    &wgpu_y_desc,
3104                    wgpu::wgt::TextureUses::UNINITIALIZED,
3105                );
3106            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
3107
3108            let tex_uv = self
3109                .device
3110                .create_texture_from_hal::<wgpu::hal::vulkan::Api>(
3111                    uvt,
3112                    &wgpu_uv_desc,
3113                    wgpu::wgt::TextureUses::UNINITIALIZED,
3114                );
3115            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
3116
3117            (tex_y, view_y, tex_uv, view_uv)
3118        };
3119
3120        let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
3121            label: Some("dmabuf yuv transform"),
3122            size: std::mem::size_of::<YuvTransformRaw>() as u64,
3123            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3124            mapped_at_creation: false,
3125        });
3126        self.queue
3127            .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
3128
3129        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3130            label: Some("dmabuf nv12 bind"),
3131            layout: &self.image_bind_layout_nv12,
3132            entries: &[
3133                wgpu::BindGroupEntry {
3134                    binding: 0,
3135                    resource: wgpu::BindingResource::TextureView(&view_y),
3136                },
3137                wgpu::BindGroupEntry {
3138                    binding: 1,
3139                    resource: wgpu::BindingResource::TextureView(&view_uv),
3140                },
3141                wgpu::BindGroupEntry {
3142                    binding: 2,
3143                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
3144                },
3145                wgpu::BindGroupEntry {
3146                    binding: 3,
3147                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3148                        buffer: &yuv_buf,
3149                        offset: 0,
3150                        size: None,
3151                    }),
3152                },
3153            ],
3154        });
3155
3156        let bytes = (w as u64) * (h as u64)
3157            + (uv_w as u64) * (uv_h as u64) * 2
3158            + std::mem::size_of::<YuvTransformRaw>() as u64;
3159
3160        self.images.insert(
3161            handle,
3162            ImageTex::Nv12 {
3163                tex_y,
3164                tex_uv,
3165                bind,
3166                yuv_buf,
3167                w,
3168                h,
3169                color_info,
3170                last_used_frame: self.frame_index,
3171                bytes,
3172            },
3173        );
3174
3175        self.evict_budget_excess();
3176        Ok(())
3177    }
3178
3179    pub fn remove_image(&mut self, handle: u64) {
3180        if let Some(img) = self.images.remove(&handle) {
3181            let b = match &img {
3182                ImageTex::Rgba { bytes, .. } => *bytes,
3183                ImageTex::Nv12 { bytes, .. } => *bytes,
3184                ImageTex::User { bytes, .. } => *bytes,
3185            };
3186            self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
3187        }
3188        self.retained.remove(&handle);
3189    }
3190
3191    fn evict_image_gpu(&mut self, handle: u64) -> u64 {
3192        let Some(img) = self.images.remove(&handle) else {
3193            return 0;
3194        };
3195        let b = match &img {
3196            ImageTex::Rgba { bytes, .. } => *bytes,
3197            ImageTex::Nv12 { bytes, .. } => *bytes,
3198            ImageTex::User { bytes, .. } => *bytes,
3199        };
3200        self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
3201        b
3202    }
3203
3204    fn revive_retained_image(&mut self, handle: u64) -> bool {
3205        if self.images.contains_key(&handle) {
3206            return true;
3207        }
3208        let Some(r) = self.retained.get(&handle).cloned() else {
3209            return false;
3210        };
3211        let (tex, bind) = self.create_rgba_tex(r.w, r.h, r.format);
3212
3213        self.queue.write_texture(
3214            wgpu::TexelCopyTextureInfo {
3215                texture: &tex,
3216                mip_level: 0,
3217                origin: wgpu::Origin3d::ZERO,
3218                aspect: wgpu::TextureAspect::All,
3219            },
3220            &r.rgba,
3221            wgpu::TexelCopyBufferLayout {
3222                offset: 0,
3223                bytes_per_row: Some(4 * r.w),
3224                rows_per_image: Some(r.h),
3225            },
3226            wgpu::Extent3d {
3227                width: r.w,
3228                height: r.h,
3229                depth_or_array_layers: 1,
3230            },
3231        );
3232
3233        let bytes = (r.w as u64) * (r.h as u64) * 4;
3234        self.image_bytes_total += bytes;
3235        self.images.insert(
3236            handle,
3237            ImageTex::Rgba {
3238                tex,
3239                bind,
3240                w: r.w,
3241                h: r.h,
3242                format: r.format,
3243                last_used_frame: self.frame_index,
3244                bytes,
3245            },
3246        );
3247        true
3248    }
3249
3250    fn resolve_image_for_draw(&mut self, handle: u64) -> Option<(u32, u32, bool)> {
3251        if let Some(t) = self.images.get_mut(&handle) {
3252            return match t {
3253                ImageTex::Rgba {
3254                    w,
3255                    h,
3256                    last_used_frame,
3257                    ..
3258                } => {
3259                    *last_used_frame = self.frame_index;
3260                    Some((*w, *h, false))
3261                }
3262                ImageTex::User {
3263                    w,
3264                    h,
3265                    last_used_frame,
3266                    ..
3267                } => {
3268                    *last_used_frame = self.frame_index;
3269                    Some((*w, *h, false))
3270                }
3271                ImageTex::Nv12 {
3272                    w,
3273                    h,
3274                    last_used_frame,
3275                    ..
3276                } => {
3277                    *last_used_frame = self.frame_index;
3278                    Some((*w, *h, true))
3279                }
3280            };
3281        }
3282        if self.revive_retained_image(handle)
3283            && let Some(ImageTex::Rgba {
3284                w,
3285                h,
3286                last_used_frame,
3287                ..
3288            }) = self.images.get_mut(&handle)
3289        {
3290            *last_used_frame = self.frame_index;
3291            return Some((*w, *h, false));
3292        }
3293        None
3294    }
3295
3296    // Legacy support from Step 1 instructions (temporary until platform render logic is fully swapped)
3297    pub fn register_image_from_bytes(&mut self, data: &[u8], srgb: bool) -> u64 {
3298        let handle = self.next_image_handle;
3299        self.next_image_handle += 1;
3300        if let Err(e) = self.set_image_from_bytes(handle, data, srgb) {
3301            log::error!("Failed to register image: {e}");
3302        }
3303        handle
3304    }
3305
3306    /// Register raw RGBA8 pixels (`w * h * 4` bytes) as an image, returning
3307    /// its handle for `SceneNode::Image`. Used by CPU-rasterized overlays
3308    /// (e.g. subtitle bitmap layers) that have no encoded image bytes.
3309    /// Pass `srgb = true` for sRGB overlays composited over video.
3310    pub fn register_image_rgba8(&mut self, w: u32, h: u32, rgba: &[u8], srgb: bool) -> u64 {
3311        let handle = self.next_image_handle;
3312        self.next_image_handle += 1;
3313        if let Err(e) = self.set_image_rgba8(handle, w, h, rgba, srgb) {
3314            log::error!("Failed to register image: {e}");
3315        }
3316        handle
3317    }
3318
3319    fn evict_unused_images(&mut self) {
3320        let now = self.frame_index;
3321        let evict_after = self.image_evict_after_frames;
3322
3323        // Time based eviction. Eviction only frees GPU memory: retained RGBA
3324        // sources stay so the image can be lazily re-uploaded when drawn again.
3325        let mut to_evict = Vec::new();
3326        for (h, t) in self.images.iter() {
3327            let last = match t {
3328                ImageTex::Rgba {
3329                    last_used_frame, ..
3330                } => *last_used_frame,
3331                ImageTex::User {
3332                    last_used_frame, ..
3333                } => *last_used_frame,
3334                ImageTex::Nv12 {
3335                    last_used_frame, ..
3336                } => *last_used_frame,
3337            };
3338            if now.saturating_sub(last) > evict_after {
3339                to_evict.push(*h);
3340            }
3341        }
3342        for h in to_evict {
3343            if self.retained.contains_key(&h) {
3344                self.evict_image_gpu(h);
3345            } else {
3346                self.remove_image(h);
3347            }
3348        }
3349
3350        self.evict_budget_excess();
3351    }
3352
3353    fn evict_budget_excess(&mut self) {
3354        if self.image_bytes_total <= self.image_budget_bytes {
3355            return;
3356        }
3357        // Collect (handle, last_used, bytes)
3358        let mut candidates: Vec<(u64, u64, u64)> = self
3359            .images
3360            .iter()
3361            .map(|(h, t)| {
3362                let (last, bytes) = match t {
3363                    ImageTex::Rgba {
3364                        last_used_frame,
3365                        bytes,
3366                        ..
3367                    } => (*last_used_frame, *bytes),
3368                    ImageTex::User {
3369                        last_used_frame,
3370                        bytes,
3371                        ..
3372                    } => (*last_used_frame, *bytes),
3373                    ImageTex::Nv12 {
3374                        last_used_frame,
3375                        bytes,
3376                        ..
3377                    } => (*last_used_frame, *bytes),
3378                };
3379                (*h, last, bytes)
3380            })
3381            .collect();
3382
3383        // Sort by last_used ascending (LRU first)
3384        candidates.sort_by_key(|k| k.1);
3385
3386        let now = self.frame_index;
3387        for (h, last, _bytes) in candidates {
3388            if self.image_bytes_total <= self.image_budget_bytes {
3389                break;
3390            }
3391            // Don't evict something used this frame
3392            if last == now {
3393                continue;
3394            }
3395            if self.retained.contains_key(&h) {
3396                self.evict_image_gpu(h);
3397            } else {
3398                self.remove_image(h);
3399            }
3400        }
3401    }
3402
3403    /// Set pixels per point (DPI scale) for callback `ScreenDescriptor` / `PaintCallbackInfo`.
3404    pub fn set_pixels_per_point(&mut self, ppp: f32) {
3405        self.pixels_per_point = ppp.clamp(0.5, 8.0);
3406    }
3407
3408    /// Enable or disable linear working-space rendering.
3409    /// When enabled, the scene is rendered into an Rgba16Float intermediate
3410    /// and a final full-screen pass applies the display OETF.
3411    pub fn set_working_space(&mut self, enabled: bool) {
3412        if enabled == self.working_space {
3413            return;
3414        }
3415        self.working_space = enabled;
3416        if enabled {
3417            self.ensure_display_pipeline();
3418            self.recreate_working_space_texture();
3419        } else {
3420            self.ws_tex = None;
3421            self.ws_view = None;
3422            self.ws_bind = None;
3423        }
3424    }
3425
3426    fn ensure_display_pipeline(&mut self) {
3427        if self.display_pipeline.is_some() {
3428            return;
3429        }
3430
3431        let layout = self
3432            .device
3433            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3434                label: Some("display transform layout"),
3435                entries: &[
3436                    wgpu::BindGroupLayoutEntry {
3437                        binding: 0,
3438                        visibility: wgpu::ShaderStages::FRAGMENT,
3439                        ty: wgpu::BindingType::Texture {
3440                            multisampled: false,
3441                            view_dimension: wgpu::TextureViewDimension::D2,
3442                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
3443                        },
3444                        count: None,
3445                    },
3446                    wgpu::BindGroupLayoutEntry {
3447                        binding: 1,
3448                        visibility: wgpu::ShaderStages::FRAGMENT,
3449                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
3450                        count: None,
3451                    },
3452                ],
3453            });
3454        self.display_layout = Some(layout);
3455
3456        let shader = self
3457            .device
3458            .create_shader_module(wgpu::ShaderModuleDescriptor {
3459                label: Some("display_transform.wgsl"),
3460                source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
3461                    "shaders/display_transform.wgsl"
3462                ))),
3463            });
3464
3465        let pipeline_layout = self
3466            .device
3467            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3468                label: Some("display transform pipeline layout"),
3469                bind_group_layouts: &[None, self.display_layout.as_ref()],
3470                immediate_size: 0,
3471            });
3472
3473        let pipeline = self
3474            .device
3475            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3476                label: Some("display transform pipeline"),
3477                layout: Some(&pipeline_layout),
3478                vertex: wgpu::VertexState {
3479                    module: &shader,
3480                    entry_point: Some("vs_main"),
3481                    buffers: &[],
3482                    compilation_options: wgpu::PipelineCompilationOptions::default(),
3483                },
3484                fragment: Some(wgpu::FragmentState {
3485                    module: &shader,
3486                    entry_point: Some("fs_main"),
3487                    targets: &[Some(wgpu::ColorTargetState {
3488                        format: self.output_format,
3489                        blend: None,
3490                        write_mask: wgpu::ColorWrites::ALL,
3491                    })],
3492                    compilation_options: wgpu::PipelineCompilationOptions::default(),
3493                }),
3494                primitive: wgpu::PrimitiveState::default(),
3495                depth_stencil: None,
3496                multisample: wgpu::MultisampleState::default(),
3497                multiview_mask: None,
3498                cache: None,
3499            });
3500        self.display_pipeline = Some(pipeline);
3501    }
3502
3503    /// Resize the render target dimensions.
3504    ///
3505    /// Recreates MSAA, depth-stencil, and working-space textures to match the
3506    /// new size..
3507    pub fn resize(&mut self, width: u32, height: u32) {
3508        self.output_width = width;
3509        self.output_height = height;
3510        self.recreate_msaa_and_depth_stencil();
3511        self.recreate_working_space_texture();
3512    }
3513
3514    fn recreate_working_space_texture(&mut self) {
3515        if !self.working_space {
3516            return;
3517        }
3518        let w = self.output_width.max(1);
3519        let h = self.output_height.max(1);
3520
3521        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
3522            label: Some("working space"),
3523            size: wgpu::Extent3d {
3524                width: w,
3525                height: h,
3526                depth_or_array_layers: 1,
3527            },
3528            mip_level_count: 1,
3529            sample_count: 1,
3530            dimension: wgpu::TextureDimension::D2,
3531            format: wgpu::TextureFormat::Rgba16Float,
3532            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
3533            view_formats: &[],
3534        });
3535        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
3536
3537        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3538            label: Some("working space bind"),
3539            layout: self.display_layout.as_ref().unwrap(),
3540            entries: &[
3541                wgpu::BindGroupEntry {
3542                    binding: 0,
3543                    resource: wgpu::BindingResource::TextureView(&view),
3544                },
3545                wgpu::BindGroupEntry {
3546                    binding: 1,
3547                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
3548                },
3549            ],
3550        });
3551
3552        self.ws_tex = Some(tex);
3553        self.ws_view = Some(view);
3554        self.ws_bind = Some(bind);
3555    }
3556
3557    fn recreate_msaa_and_depth_stencil(&mut self) {
3558        if self.msaa_samples > 1 {
3559            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
3560                label: Some("msaa color"),
3561                size: wgpu::Extent3d {
3562                    width: self.output_width.max(1),
3563                    height: self.output_height.max(1),
3564                    depth_or_array_layers: 1,
3565                },
3566                mip_level_count: 1,
3567                sample_count: self.msaa_samples,
3568                dimension: wgpu::TextureDimension::D2,
3569                format: self.output_format,
3570                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
3571                view_formats: &[],
3572            });
3573            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
3574            self.msaa_tex = Some(tex);
3575            self.msaa_view = Some(view);
3576        } else {
3577            self.msaa_tex = None;
3578            self.msaa_view = None;
3579        }
3580
3581        self.depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
3582            label: Some("depth-stencil (stencil clips)"),
3583            size: wgpu::Extent3d {
3584                width: self.output_width.max(1),
3585                height: self.output_height.max(1),
3586                depth_or_array_layers: 1,
3587            },
3588            mip_level_count: 1,
3589            sample_count: self.msaa_samples,
3590            dimension: wgpu::TextureDimension::D2,
3591            format: wgpu::TextureFormat::Depth24PlusStencil8,
3592            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
3593            view_formats: &[],
3594        });
3595        self.depth_stencil_view = self
3596            .depth_stencil_tex
3597            .create_view(&wgpu::TextureViewDescriptor::default());
3598    }
3599
3600    fn get_or_create_layer(
3601        &mut self,
3602        layer_id: u32,
3603        width: u32,
3604        height: u32,
3605        rect: repose_core::Rect,
3606    ) {
3607        let needs_alloc = match self.layer_pool.get(&layer_id) {
3608            Some(lt) => lt.width != width || lt.height != height,
3609            None => true,
3610        };
3611        if !needs_alloc {
3612            if let Some(lt) = self.layer_pool.get_mut(&layer_id) {
3613                lt.rect_px = (rect.x, rect.y, rect.w, rect.h);
3614            }
3615            return;
3616        }
3617        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
3618            label: Some("graphics layer"),
3619            size: wgpu::Extent3d {
3620                width: width.max(1),
3621                height: height.max(1),
3622                depth_or_array_layers: 1,
3623            },
3624            mip_level_count: 1,
3625            sample_count: 1,
3626            dimension: wgpu::TextureDimension::D2,
3627            format: self.output_format,
3628            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
3629            view_formats: &[],
3630        });
3631        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
3632        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3633            label: Some("layer bind"),
3634            layout: &self.image_bind_layout_rgba,
3635            entries: &[
3636                wgpu::BindGroupEntry {
3637                    binding: 0,
3638                    resource: wgpu::BindingResource::TextureView(&view),
3639                },
3640                wgpu::BindGroupEntry {
3641                    binding: 1,
3642                    resource: wgpu::BindingResource::Sampler(&self.layer_sampler),
3643                },
3644            ],
3645        });
3646        let bind_linear = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3647            label: Some("layer bind linear"),
3648            layout: &self.image_bind_layout_rgba,
3649            entries: &[
3650                wgpu::BindGroupEntry {
3651                    binding: 0,
3652                    resource: wgpu::BindingResource::TextureView(&view),
3653                },
3654                wgpu::BindGroupEntry {
3655                    binding: 1,
3656                    resource: wgpu::BindingResource::Sampler(&self.layer_sampler_linear),
3657                },
3658            ],
3659        });
3660        let depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
3661            label: Some("graphics layer depth-stencil"),
3662            size: wgpu::Extent3d {
3663                width: width.max(1),
3664                height: height.max(1),
3665                depth_or_array_layers: 1,
3666            },
3667            mip_level_count: 1,
3668            sample_count: 1,
3669            dimension: wgpu::TextureDimension::D2,
3670            format: wgpu::TextureFormat::Depth24PlusStencil8,
3671            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
3672            view_formats: &[],
3673        });
3674        let depth_stencil_view =
3675            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
3676        self.layer_pool.insert(
3677            layer_id,
3678            LayerTarget {
3679                view,
3680                bind,
3681                bind_linear,
3682                depth_stencil_view,
3683                width,
3684                height,
3685                rect_px: (rect.x, rect.y, rect.w, rect.h),
3686            },
3687        );
3688    }
3689
3690    fn atlas_bind_group_mask(&self) -> wgpu::BindGroup {
3691        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3692            label: Some("atlas bind"),
3693            layout: &self.text_bind_layout,
3694            entries: &[
3695                wgpu::BindGroupEntry {
3696                    binding: 0,
3697                    resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
3698                },
3699                wgpu::BindGroupEntry {
3700                    binding: 1,
3701                    resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
3702                },
3703            ],
3704        })
3705    }
3706
3707    fn atlas_bind_group_color(&self) -> wgpu::BindGroup {
3708        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3709            label: Some("atlas bind color"),
3710            layout: &self.text_bind_layout,
3711            entries: &[
3712                wgpu::BindGroupEntry {
3713                    binding: 0,
3714                    resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
3715                },
3716                wgpu::BindGroupEntry {
3717                    binding: 1,
3718                    resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
3719                },
3720            ],
3721        })
3722    }
3723
3724    fn upload_glyph_mask(&mut self, key: repose_text::GlyphKey, px: f32) -> Option<GlyphInfo> {
3725        let keyp = (key, px.to_bits());
3726        if let Some(info) = self.atlas_mask.map.get(&keyp) {
3727            return Some(*info);
3728        }
3729
3730        let gb = repose_text::rasterize(key, px)?;
3731        if gb.w == 0 || gb.h == 0 || gb.data.is_empty() {
3732            return None;
3733        }
3734
3735        let coverage = swash_to_a8_coverage(gb.content, &gb.data)?;
3736
3737        let w = gb.w.max(1);
3738        let h = gb.h.max(1);
3739
3740        if !self.alloc_space_mask(w, h) {
3741            self.grow_mask_and_rebuild();
3742        }
3743        if !self.alloc_space_mask(w, h) {
3744            return None;
3745        }
3746        let x = self.atlas_mask.next_x;
3747        let y = self.atlas_mask.next_y;
3748        self.atlas_mask.next_x += w + 1;
3749        self.atlas_mask.row_h = self.atlas_mask.row_h.max(h + 1);
3750
3751        let layout = wgpu::TexelCopyBufferLayout {
3752            offset: 0,
3753            bytes_per_row: Some(w),
3754            rows_per_image: Some(h),
3755        };
3756        let size = wgpu::Extent3d {
3757            width: w,
3758            height: h,
3759            depth_or_array_layers: 1,
3760        };
3761        self.queue.write_texture(
3762            wgpu::TexelCopyTextureInfoBase {
3763                texture: &self.atlas_mask.tex,
3764                mip_level: 0,
3765                origin: wgpu::Origin3d { x, y, z: 0 },
3766                aspect: wgpu::TextureAspect::All,
3767            },
3768            &coverage,
3769            layout,
3770            size,
3771        );
3772
3773        let info = GlyphInfo {
3774            u0: x as f32 / self.atlas_mask.size as f32,
3775            v0: y as f32 / self.atlas_mask.size as f32,
3776            u1: (x + w) as f32 / self.atlas_mask.size as f32,
3777            v1: (y + h) as f32 / self.atlas_mask.size as f32,
3778            w: w as f32,
3779            h: h as f32,
3780        };
3781        self.atlas_mask.map.insert(keyp, info);
3782        Some(info)
3783    }
3784
3785    fn upload_glyph_color(&mut self, key: repose_text::GlyphKey, px: f32) -> Option<GlyphInfo> {
3786        let keyp = (key, px.to_bits());
3787        if let Some(info) = self.atlas_color.map.get(&keyp) {
3788            return Some(*info);
3789        }
3790        let gb = repose_text::rasterize(key, px)?;
3791        if !matches!(gb.content, repose_text::SwashContent::Color) {
3792            return None;
3793        }
3794        let w = gb.w.max(1);
3795        let h = gb.h.max(1);
3796        if !self.alloc_space_color(w, h) {
3797            self.grow_color_and_rebuild();
3798        }
3799        if !self.alloc_space_color(w, h) {
3800            return None;
3801        }
3802        let x = self.atlas_color.next_x;
3803        let y = self.atlas_color.next_y;
3804        self.atlas_color.next_x += w + 1;
3805        self.atlas_color.row_h = self.atlas_color.row_h.max(h + 1);
3806
3807        let layout = wgpu::TexelCopyBufferLayout {
3808            offset: 0,
3809            bytes_per_row: Some(w * 4),
3810            rows_per_image: Some(h),
3811        };
3812        let size = wgpu::Extent3d {
3813            width: w,
3814            height: h,
3815            depth_or_array_layers: 1,
3816        };
3817        self.queue.write_texture(
3818            wgpu::TexelCopyTextureInfoBase {
3819                texture: &self.atlas_color.tex,
3820                mip_level: 0,
3821                origin: wgpu::Origin3d { x, y, z: 0 },
3822                aspect: wgpu::TextureAspect::All,
3823            },
3824            &gb.data,
3825            layout,
3826            size,
3827        );
3828        let info = GlyphInfo {
3829            u0: x as f32 / self.atlas_color.size as f32,
3830            v0: y as f32 / self.atlas_color.size as f32,
3831            u1: (x + w) as f32 / self.atlas_color.size as f32,
3832            v1: (y + h) as f32 / self.atlas_color.size as f32,
3833            w: w as f32,
3834            h: h as f32,
3835        };
3836        self.atlas_color.map.insert(keyp, info);
3837        Some(info)
3838    }
3839
3840    fn alloc_space_mask(&mut self, w: u32, h: u32) -> bool {
3841        if self.atlas_mask.next_x + w + 1 >= self.atlas_mask.size {
3842            self.atlas_mask.next_x = 1;
3843            self.atlas_mask.next_y += self.atlas_mask.row_h + 1;
3844            self.atlas_mask.row_h = 0;
3845        }
3846        if self.atlas_mask.next_y + h + 1 >= self.atlas_mask.size {
3847            return false;
3848        }
3849        true
3850    }
3851
3852    fn grow_mask_and_rebuild(&mut self) {
3853        let new_size = (self.atlas_mask.size * 2).min(4096);
3854        if new_size == self.atlas_mask.size {
3855            return;
3856        }
3857        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
3858            label: Some("glyph atlas A8 (grown)"),
3859            size: wgpu::Extent3d {
3860                width: new_size,
3861                height: new_size,
3862                depth_or_array_layers: 1,
3863            },
3864            mip_level_count: 1,
3865            sample_count: 1,
3866            dimension: wgpu::TextureDimension::D2,
3867            format: wgpu::TextureFormat::R8Unorm,
3868            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3869            view_formats: &[],
3870        });
3871        self.atlas_mask.tex = tex;
3872        self.atlas_mask.view = self
3873            .atlas_mask
3874            .tex
3875            .create_view(&wgpu::TextureViewDescriptor::default());
3876        self.atlas_mask.size = new_size;
3877        self.atlas_mask.next_x = 1;
3878        self.atlas_mask.next_y = 1;
3879        self.atlas_mask.row_h = 0;
3880        let keys: Vec<(repose_text::GlyphKey, u32)> = self.atlas_mask.map.keys().copied().collect();
3881        self.atlas_mask.map.clear();
3882        for (k, px_bits) in keys {
3883            let _ = self.upload_glyph_mask(k, f32::from_bits(px_bits));
3884        }
3885    }
3886
3887    fn alloc_space_color(&mut self, w: u32, h: u32) -> bool {
3888        if self.atlas_color.next_x + w + 1 >= self.atlas_color.size {
3889            self.atlas_color.next_x = 1;
3890            self.atlas_color.next_y += self.atlas_color.row_h + 1;
3891            self.atlas_color.row_h = 0;
3892        }
3893        if self.atlas_color.next_y + h + 1 >= self.atlas_color.size {
3894            return false;
3895        }
3896        true
3897    }
3898
3899    fn grow_color_and_rebuild(&mut self) {
3900        let new_size = (self.atlas_color.size * 2).min(4096);
3901        if new_size == self.atlas_color.size {
3902            return;
3903        }
3904        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
3905            label: Some("glyph atlas RGBA (grown)"),
3906            size: wgpu::Extent3d {
3907                width: new_size,
3908                height: new_size,
3909                depth_or_array_layers: 1,
3910            },
3911            mip_level_count: 1,
3912            sample_count: 1,
3913            dimension: wgpu::TextureDimension::D2,
3914            format: wgpu::TextureFormat::Rgba8UnormSrgb,
3915            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3916            view_formats: &[],
3917        });
3918        self.atlas_color.tex = tex;
3919        self.atlas_color.view = self
3920            .atlas_color
3921            .tex
3922            .create_view(&wgpu::TextureViewDescriptor::default());
3923        self.atlas_color.size = new_size;
3924        self.atlas_color.next_x = 1;
3925        self.atlas_color.next_y = 1;
3926        self.atlas_color.row_h = 0;
3927        let keys: Vec<(repose_text::GlyphKey, u32)> =
3928            self.atlas_color.map.keys().copied().collect();
3929        self.atlas_color.map.clear();
3930        for (k, px_bits) in keys {
3931            let _ = self.upload_glyph_color(k, f32::from_bits(px_bits));
3932        }
3933    }
3934}
3935
3936fn brush_to_instance_fields(brush: &Brush) -> (u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2]) {
3937    match brush {
3938        Brush::Solid(c) => (
3939            0u32,
3940            c.to_linear(),
3941            [0.0, 0.0, 0.0, 0.0],
3942            [0.0, 0.0],
3943            [0.0, 1.0],
3944        ),
3945        Brush::Linear {
3946            start,
3947            end,
3948            start_color,
3949            end_color,
3950        } => (
3951            1u32,
3952            start_color.to_linear(),
3953            end_color.to_linear(),
3954            [start.x, start.y],
3955            [end.x, end.y],
3956        ),
3957        _ => (0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
3958    }
3959}
3960
3961fn brush_to_solid_color(brush: &Brush) -> [f32; 4] {
3962    match brush {
3963        Brush::Solid(c) => c.to_linear(),
3964        Brush::Linear { start_color, .. } => start_color.to_linear(),
3965        _ => [0.0; 4],
3966    }
3967}
3968
3969fn init_atlas_mask(device: &wgpu::Device) -> AtlasA8 {
3970    let size = 1024u32;
3971    let tex = device.create_texture(&wgpu::TextureDescriptor {
3972        label: Some("glyph atlas A8"),
3973        size: wgpu::Extent3d {
3974            width: size,
3975            height: size,
3976            depth_or_array_layers: 1,
3977        },
3978        mip_level_count: 1,
3979        sample_count: 1,
3980        dimension: wgpu::TextureDimension::D2,
3981        format: wgpu::TextureFormat::R8Unorm,
3982        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3983        view_formats: &[],
3984    });
3985    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
3986    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
3987        label: Some("glyph atlas sampler A8"),
3988        address_mode_u: wgpu::AddressMode::ClampToEdge,
3989        address_mode_v: wgpu::AddressMode::ClampToEdge,
3990        address_mode_w: wgpu::AddressMode::ClampToEdge,
3991        mag_filter: wgpu::FilterMode::Linear,
3992        min_filter: wgpu::FilterMode::Linear,
3993        mipmap_filter: wgpu::MipmapFilterMode::Linear,
3994        ..Default::default()
3995    });
3996
3997    AtlasA8 {
3998        tex,
3999        view,
4000        sampler,
4001        size,
4002        next_x: 1,
4003        next_y: 1,
4004        row_h: 0,
4005        map: HashMap::new(),
4006    }
4007}
4008
4009fn init_atlas_color(device: &wgpu::Device) -> AtlasRGBA {
4010    let size = 1024u32;
4011    let tex = device.create_texture(&wgpu::TextureDescriptor {
4012        label: Some("glyph atlas RGBA"),
4013        size: wgpu::Extent3d {
4014            width: size,
4015            height: size,
4016            depth_or_array_layers: 1,
4017        },
4018        mip_level_count: 1,
4019        sample_count: 1,
4020        dimension: wgpu::TextureDimension::D2,
4021        format: wgpu::TextureFormat::Rgba8UnormSrgb,
4022        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4023        view_formats: &[],
4024    });
4025    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
4026    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
4027        label: Some("glyph atlas sampler RGBA"),
4028        address_mode_u: wgpu::AddressMode::ClampToEdge,
4029        address_mode_v: wgpu::AddressMode::ClampToEdge,
4030        address_mode_w: wgpu::AddressMode::ClampToEdge,
4031        mag_filter: wgpu::FilterMode::Linear,
4032        min_filter: wgpu::FilterMode::Linear,
4033        mipmap_filter: wgpu::MipmapFilterMode::Linear,
4034        ..Default::default()
4035    });
4036    AtlasRGBA {
4037        tex,
4038        view,
4039        sampler,
4040        size,
4041        next_x: 1,
4042        next_y: 1,
4043        row_h: 0,
4044        map: HashMap::new(),
4045    }
4046}
4047
4048#[cfg(feature = "winit-surface")]
4049impl RenderBackend for WgpuSurfaceBackend {
4050    fn configure_surface(&mut self, width: u32, height: u32) {
4051        if width == 0 || height == 0 {
4052            return;
4053        }
4054        self.renderer.output_width = width;
4055        self.renderer.output_height = height;
4056        if let Some(ref mut config) = self.surface_config {
4057            config.width = width;
4058            config.height = height;
4059        }
4060        if let (Some(surface), Some(config)) = (self.surface.as_ref(), self.surface_config.as_ref())
4061        {
4062            surface.configure(&self.renderer.device, config);
4063        }
4064        self.renderer.recreate_msaa_and_depth_stencil();
4065        self.renderer.recreate_working_space_texture();
4066    }
4067
4068    fn frame(&mut self, scene: &Scene, _glyph_cfg: GlyphRasterConfig) {
4069        let surface = self.surface.as_ref().expect("WgpuSurfaceBackend::frame() requires a surface (use from_device + render_to_view instead)");
4070        let surface_config = self
4071            .surface_config
4072            .as_ref()
4073            .expect("surface_config required for frame()");
4074
4075        self.renderer.frame_index = self.renderer.frame_index.wrapping_add(1);
4076        self.renderer.slug_cache.next_frame();
4077
4078        if self.renderer.output_width == 0 || self.renderer.output_height == 0 {
4079            return;
4080        }
4081
4082        let mut retries = 0u32;
4083        const MAX_RETRIES: u32 = 4;
4084        let frame = loop {
4085            match surface.get_current_texture() {
4086                wgpu::CurrentSurfaceTexture::Success(f) => break f,
4087                wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
4088                    log::warn!("suboptimal surface; reconfiguring");
4089                    surface.configure(&self.renderer.device, surface_config);
4090                    break f;
4091                }
4092                wgpu::CurrentSurfaceTexture::Outdated => {
4093                    retries += 1;
4094                    if retries >= MAX_RETRIES {
4095                        log::warn!(
4096                            "surface outdated persisted after {MAX_RETRIES} retries; skipping frame"
4097                        );
4098                        return;
4099                    }
4100                    log::warn!("surface outdated; reconfiguring");
4101                    surface.configure(&self.renderer.device, surface_config);
4102                }
4103                wgpu::CurrentSurfaceTexture::Lost => {
4104                    retries += 1;
4105                    if retries >= MAX_RETRIES {
4106                        log::warn!(
4107                            "surface lost persisted after {MAX_RETRIES} retries; skipping frame"
4108                        );
4109                        return;
4110                    }
4111                    log::warn!("surface lost; reconfiguring");
4112                    surface.configure(&self.renderer.device, surface_config);
4113                }
4114                wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
4115                    request_frame();
4116                    return;
4117                }
4118                wgpu::CurrentSurfaceTexture::Validation => {
4119                    retries += 1;
4120                    if retries >= MAX_RETRIES {
4121                        log::warn!(
4122                            "surface validation persisted after {MAX_RETRIES} retries; skipping frame"
4123                        );
4124                        return;
4125                    }
4126                    surface.configure(&self.renderer.device, surface_config);
4127                }
4128            }
4129        };
4130
4131        let swap_view = if let Some(view_format) = self
4132            .surface_config
4133            .as_ref()
4134            .and_then(|c| c.view_formats.iter().find(|f| f.is_srgb()).copied())
4135        {
4136            frame.texture.create_view(&wgpu::TextureViewDescriptor {
4137                format: Some(view_format),
4138                ..Default::default()
4139            })
4140        } else {
4141            frame
4142                .texture
4143                .create_view(&wgpu::TextureViewDescriptor::default())
4144        };
4145        let mut encoder =
4146            self.renderer
4147                .device
4148                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4149                    label: Some("frame encoder"),
4150                });
4151
4152        let clear_color = Some([
4153            scene.clear_color.0 as f64 / 255.0,
4154            scene.clear_color.1 as f64 / 255.0,
4155            scene.clear_color.2 as f64 / 255.0,
4156            scene.clear_color.3 as f64 / 255.0,
4157        ]);
4158
4159        self.renderer
4160            .render_scene_to_encoder(scene, &mut encoder, &swap_view, clear_color);
4161
4162        //NOTE: The WebGL HAL present path (fullscreen triangle / blit) does not
4163        // restore gl.colorMask. Hence this is needed to prevent frames from going transparent.
4164        {
4165            let _reset = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
4166                label: Some("webgl color_mask reset before present"),
4167                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
4168                    view: &swap_view,
4169                    resolve_target: None,
4170                    ops: wgpu::Operations {
4171                        load: wgpu::LoadOp::Load,
4172                        store: wgpu::StoreOp::Store,
4173                    },
4174                    depth_slice: None,
4175                })],
4176                depth_stencil_attachment: None,
4177                timestamp_writes: None,
4178                occlusion_query_set: None,
4179                multiview_mask: None,
4180            });
4181        }
4182
4183        self.renderer
4184            .queue
4185            .submit(std::iter::once(encoder.finish()));
4186        if let Err(e) = catch_unwind(AssertUnwindSafe(|| self.renderer.queue.present(frame))) {
4187            log::warn!("queue.present panicked: {:?}", e);
4188        }
4189    }
4190}
4191
4192impl WgpuSceneRenderer {
4193    fn upload_mesh_geometry(&mut self, mesh: &repose_core::VectorMeshData) -> (u64, u32, u64, u32) {
4194        let verts: Vec<MeshVertex> = mesh
4195            .vertices
4196            .iter()
4197            .map(|v| MeshVertex {
4198                pos: v.pos,
4199                color: v.color,
4200                uv: v.uv,
4201            })
4202            .collect();
4203        let vbytes = bytemuck::cast_slice(&verts);
4204        self.mesh_verts
4205            .grow_to_fit(&self.device, vbytes.len() as u64);
4206        let (voff, _) = self.mesh_verts.alloc_write(&self.queue, vbytes);
4207        let ibytes = bytemuck::cast_slice(&mesh.indices);
4208        self.mesh_indices
4209            .grow_to_fit(&self.device, ibytes.len() as u64);
4210        let (ioff, _) = self.mesh_indices.alloc_write(&self.queue, ibytes);
4211        (voff, verts.len() as u32, ioff, mesh.indices.len() as u32)
4212    }
4213
4214    fn alloc_mesh_uniform(&mut self, u: MeshUniform) -> u64 {
4215        if self.mesh_uniform_head + MESH_UNIFORM_SLOT > MESH_UNIFORM_CAP {
4216            log::warn!("mesh uniform buffer overflow; regenerating");
4217            self.recreate_mesh_uniform_buffer();
4218        }
4219        let slot = self.mesh_uniform_head;
4220        self.queue
4221            .write_buffer(&self.mesh_uniform_buf, slot, bytemuck::bytes_of(&u));
4222        self.mesh_uniform_head = slot + MESH_UNIFORM_SLOT;
4223        slot
4224    }
4225
4226    fn recreate_mesh_uniform_buffer(&mut self) {
4227        let new_cap = self.mesh_uniform_head + MESH_UNIFORM_SLOT;
4228        self.mesh_uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
4229            label: Some("mesh uniform buffer"),
4230            size: new_cap,
4231            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4232            mapped_at_creation: false,
4233        });
4234        self.mesh_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4235            label: Some("mesh uniform bind"),
4236            layout: &self.mesh_bind_layout,
4237            entries: &[wgpu::BindGroupEntry {
4238                binding: 0,
4239                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
4240                    buffer: &self.mesh_uniform_buf,
4241                    offset: 0,
4242                    size: NonZero::new(MESH_UNIFORM_SLOT),
4243                }),
4244            }],
4245        });
4246        self.mesh_uniform_head = 0;
4247    }
4248
4249    #[allow(clippy::too_many_arguments)]
4250    fn emit_vector_mesh(
4251        &mut self,
4252        current_transform: &Transform,
4253        mesh: &repose_core::VectorMeshData,
4254        transform: [f32; 6],
4255        paint: &repose_core::PaintDesc,
4256        cmds: &mut Vec<Cmd>,
4257    ) {
4258        let affine = combine_mesh_affine(current_transform, transform);
4259        let (voff, vcnt, ioff, icnt) = self.upload_mesh_geometry(mesh);
4260        let uoff = self.alloc_mesh_uniform(mesh_uniform_from_paint(affine, paint));
4261        cmds.push(Cmd::VectorMesh {
4262            voff,
4263            vcnt,
4264            ioff,
4265            icnt,
4266            uoff,
4267        });
4268    }
4269
4270    pub fn render_scene_to_encoder(
4271        &mut self,
4272        scene: &Scene,
4273        encoder: &mut wgpu::CommandEncoder,
4274        target_view: &wgpu::TextureView,
4275        clear_color_override: Option<[f64; 4]>,
4276    ) {
4277        /// AABB of a rect under the *plain affine* part of a transform
4278        /// (linear + translation, no origin re-pivot).
4279        fn affine_aabb(transform: &Transform, rect: &repose_core::Rect) -> repose_core::Rect {
4280            let m = transform.linear();
4281            let (tx, ty) = (transform.translate_x, transform.translate_y);
4282            let corners = [
4283                (rect.x, rect.y),
4284                (rect.x + rect.w, rect.y),
4285                (rect.x, rect.y + rect.h),
4286                (rect.x + rect.w, rect.y + rect.h),
4287            ];
4288            let mut min_x = f32::MAX;
4289            let mut min_y = f32::MAX;
4290            let mut max_x = f32::MIN;
4291            let mut max_y = f32::MIN;
4292            for (x, y) in corners {
4293                let wx = m[0] * x + m[1] * y + tx;
4294                let wy = m[2] * x + m[3] * y + ty;
4295                min_x = min_x.min(wx);
4296                min_y = min_y.min(wy);
4297                max_x = max_x.max(wx);
4298                max_y = max_y.max(wy);
4299            }
4300            repose_core::Rect {
4301                x: min_x,
4302                y: min_y,
4303                w: (max_x - min_x).max(0.0),
4304                h: (max_y - min_y).max(0.0),
4305            }
4306        }
4307
4308        fn to_ndc(x: f32, y: f32, w: f32, h: f32, fb_w: f32, fb_h: f32) -> [f32; 4] {
4309            let x0 = (x / fb_w) * 2.0 - 1.0;
4310            let y0 = 1.0 - (y / fb_h) * 2.0;
4311            let x1 = ((x + w) / fb_w) * 2.0 - 1.0;
4312            let y1 = 1.0 - ((y + h) / fb_h) * 2.0;
4313            let min_x = x0.min(x1);
4314            let min_y = y0.min(y1);
4315            let w_ndc = (x1 - x0).abs();
4316            let h_ndc = (y1 - y0).abs();
4317            [min_x, min_y, w_ndc, h_ndc]
4318        }
4319
4320        /// Convert a local-space rect + transform to NDC center-based position+size
4321        /// plus the forward rotation/shear 2x2 (row-major `[m00, m01, m10, m11]`,
4322        /// scale-free: scale rides in the NDC size). Shaders apply it to quad
4323        /// corners and its adjugate/determinant inverse to sample positions.
4324        fn rect_to_instance_ndc(
4325            rect: repose_core::Rect,
4326            transform: &Transform,
4327            fb_w: f32,
4328            fb_h: f32,
4329        ) -> ([f32; 4], [f32; 4]) {
4330            let cx = rect.x + rect.w * 0.5;
4331            let cy = rect.y + rect.h * 0.5;
4332
4333            let m = transform.linear();
4334            let tx = m[0] * cx + m[1] * cy + transform.translate_x;
4335            let ty = m[2] * cx + m[3] * cy + transform.translate_y;
4336
4337            let ndc_cx = (tx / fb_w) * 2.0 - 1.0;
4338            let ndc_cy = 1.0 - (ty / fb_h) * 2.0;
4339            // NDC size (after scale only, no rotation - rotation is done in shader)
4340            let ndc_w = (rect.w * transform.scale_x / fb_w) * 2.0;
4341            let ndc_h = (rect.h * transform.scale_y / fb_h) * 2.0;
4342
4343            ([ndc_cx, ndc_cy, ndc_w, ndc_h], forward_rs_mat(transform))
4344        }
4345
4346        /// Forward rotation+shear 2x2 (row-major, scale-free) for instance
4347        /// attributes. Identity for untransformed content; degenerate shear
4348        /// (only from absurd inputs) falls back to identity.
4349        fn forward_rs_mat(transform: &Transform) -> [f32; 4] {
4350            let c = transform.rotate.cos();
4351            let s = transform.rotate.sin();
4352            let (hx, hy) = (transform.shear_x, transform.shear_y);
4353            let m = [c - s * hy, c * hx - s, s + c * hy, s * hx + c];
4354            if (m[0] * m[3] - m[1] * m[2]).abs() < 1e-6 {
4355                return [1.0, 0.0, 0.0, 1.0];
4356            }
4357            m
4358        }
4359
4360        fn to_scissor(r: &repose_core::Rect, fb_w: u32, fb_h: u32) -> (u32, u32, u32, u32) {
4361            let mut x = r.x.floor() as i64;
4362            let mut y = r.y.floor() as i64;
4363            let fb_wi = fb_w as i64;
4364            let fb_hi = fb_h as i64;
4365            x = x.clamp(0, fb_wi.saturating_sub(1));
4366            y = y.clamp(0, fb_hi.saturating_sub(1));
4367            let w_req = r.w.ceil().max(1.0) as i64;
4368            let h_req = r.h.ceil().max(1.0) as i64;
4369            let w = (w_req).min(fb_wi - x).max(1);
4370            let h = (h_req).min(fb_hi - y).max(1);
4371            (x as u32, y as u32, w as u32, h as u32)
4372        }
4373
4374        let fb_w = self.output_width as f32;
4375        let fb_h = self.output_height as f32;
4376
4377        let mut passes: Vec<Pass> = Vec::with_capacity(1);
4378        let clear_color = clear_color_override.unwrap_or_else(|| {
4379            [
4380                scene.clear_color.0 as f64 / 255.0,
4381                scene.clear_color.1 as f64 / 255.0,
4382                scene.clear_color.2 as f64 / 255.0,
4383                scene.clear_color.3 as f64 / 255.0,
4384            ]
4385        });
4386        let mut current_pass: Pass = Pass {
4387            target: PassTarget::Surface,
4388            initial_scissor: (0, 0, self.output_width, self.output_height),
4389            clear_color: Some([
4390                clear_color[0] as f32,
4391                clear_color[1] as f32,
4392                clear_color[2] as f32,
4393                clear_color[3] as f32,
4394            ]),
4395            cmds: Vec::with_capacity(scene.nodes.len()),
4396        };
4397        let mut target_stack: Vec<PassTarget> = Vec::new();
4398        let mut layer_alphas: Vec<(u32, f32, (u32, u32, u32, u32))> = Vec::new();
4399        let mut layer_blurs: Vec<(u32, f32, f32)> = Vec::new();
4400        let mut current_target_size: (f32, f32) = (fb_w, fb_h);
4401
4402        struct Batch {
4403            rects: Vec<RectInstance>,
4404            borders: Vec<BorderInstance>,
4405            ellipses: Vec<EllipseInstance>,
4406            e_borders: Vec<EllipseBorderInstance>,
4407            arcs: Vec<ArcInstance>,
4408            masks: Vec<GlyphInstance>,
4409            colors: Vec<GlyphInstance>,
4410            nv12s: Vec<Nv12Instance>,
4411        }
4412
4413        impl Batch {
4414            fn new() -> Self {
4415                Self {
4416                    rects: vec![],
4417                    borders: vec![],
4418                    ellipses: vec![],
4419                    e_borders: vec![],
4420                    arcs: vec![],
4421                    masks: vec![],
4422                    colors: vec![],
4423                    nv12s: vec![],
4424                }
4425            }
4426
4427            fn is_empty(&self) -> bool {
4428                self.rects.is_empty()
4429                    && self.borders.is_empty()
4430                    && self.ellipses.is_empty()
4431                    && self.e_borders.is_empty()
4432                    && self.arcs.is_empty()
4433                    && self.masks.is_empty()
4434                    && self.colors.is_empty()
4435                    && self.nv12s.is_empty()
4436            }
4437
4438            fn flush(
4439                &mut self,
4440                pipes: (
4441                    &mut InstancedPipe<RectInstance>,
4442                    &mut InstancedPipe<BorderInstance>,
4443                    &mut InstancedPipe<EllipseInstance>,
4444                    &mut InstancedPipe<EllipseBorderInstance>,
4445                    &mut InstancedPipe<ArcInstance>,
4446                ),
4447                glyph_pipes: (
4448                    &mut InstancedPipe<GlyphInstance>,
4449                    &mut InstancedPipe<GlyphInstance>,
4450                ),
4451                nv12_pipe: &mut InstancedPipe<Nv12Instance>,
4452                device: &wgpu::Device,
4453                queue: &wgpu::Queue,
4454                cmds: &mut Vec<Cmd>,
4455            ) {
4456                let (rects, borders, ellipses, e_borders, arcs) = pipes;
4457                let (masks, colors) = glyph_pipes;
4458
4459                macro_rules! flush_one {
4460                    ($buf:ident, $pipe:expr, $variant:ident) => {
4461                        if !self.$buf.is_empty() {
4462                            if let Some((off, cnt)) = $pipe.upload(device, queue, &self.$buf) {
4463                                cmds.push(Cmd::$variant { off, cnt });
4464                            }
4465                            self.$buf.clear();
4466                        }
4467                    };
4468                }
4469
4470                flush_one!(rects, rects, Rect);
4471                flush_one!(borders, borders, Border);
4472                flush_one!(ellipses, ellipses, Ellipse);
4473                flush_one!(e_borders, e_borders, EllipseBorder);
4474                flush_one!(arcs, arcs, Arc);
4475                flush_one!(masks, masks, GlyphsMask);
4476                flush_one!(colors, colors, GlyphsColor);
4477
4478                if !self.nv12s.is_empty() {
4479                    if let Some((off, cnt)) = nv12_pipe.upload(device, queue, &self.nv12s) {
4480                        let _ = (off, cnt);
4481                    }
4482                    self.nv12s.clear();
4483                }
4484            }
4485        }
4486
4487        self.rects.reset();
4488        self.borders.reset();
4489        self.ellipses.reset();
4490        self.ellipse_borders.reset();
4491        self.arcs.reset();
4492        self.glyph_mask.reset();
4493        self.glyph_color.reset();
4494        self.clip_ring.reset();
4495        self.blur_ring.reset();
4496        self.nv12.reset();
4497
4498        self.slug_ring.reset();
4499        self.mesh_verts.reset();
4500        self.mesh_indices.reset();
4501        self.mesh_uniform_head = 0;
4502        self.mesh_clip_stack.clear();
4503        let mut batch = Batch::new();
4504        let mut slug_verts_local: Vec<slug::TessVertex> = Vec::new();
4505        let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
4506        let mut scissor_stack: Vec<repose_core::Rect> = Vec::with_capacity(8);
4507        // NOTE: Records the clip instance range + flags of each active rounded-rect clip
4508        // so PopClip can re-stamp the stencil with a decrement pass (mirroring
4509        // VectorClipPop). Keys: (off, cnt, difference, rounded).
4510        let mut clip_cmd_stack: Vec<(u64, u32, bool)> = Vec::with_capacity(8);
4511        let mut root_clip_rect = repose_core::Rect {
4512            x: 0.0,
4513            y: 0.0,
4514            w: fb_w,
4515            h: fb_h,
4516        };
4517        let mut saved_scissor_stack: Vec<repose_core::Rect> = Vec::new();
4518        let mut saved_root_clip_rect = root_clip_rect;
4519
4520        let mut current_prim: Option<&'static str> = None;
4521
4522        macro_rules! flush_if_prim_changed {
4523            ($prim:literal, $pipe:expr) => {
4524                if current_prim != Some($prim) {
4525                    flush_batch!();
4526                    current_prim = Some($prim);
4527                }
4528            };
4529        }
4530
4531        macro_rules! flush_batch {
4532            () => {
4533                if !batch.is_empty() {
4534                    batch.flush(
4535                        (
4536                            &mut self.rects,
4537                            &mut self.borders,
4538                            &mut self.ellipses,
4539                            &mut self.ellipse_borders,
4540                            &mut self.arcs,
4541                        ),
4542                        (&mut self.glyph_mask, &mut self.glyph_color),
4543                        &mut self.nv12,
4544                        &self.device,
4545                        &self.queue,
4546                        &mut current_pass.cmds,
4547                    )
4548                }
4549            };
4550        }
4551        for node in &scene.nodes {
4552            let t_identity = Transform::identity();
4553            let current_transform = transform_stack.last().unwrap_or(&t_identity);
4554
4555            match node {
4556                SceneNode::Rect {
4557                    rect,
4558                    brush,
4559                    radius,
4560                } => {
4561                    flush_if_prim_changed!("rect", &self.rects);
4562                    let (ndc, fwd_mat) = rect_to_instance_ndc(
4563                        *rect,
4564                        current_transform,
4565                        current_target_size.0,
4566                        current_target_size.1,
4567                    );
4568                    let (brush_type, color0, color1, grad_start, grad_end) =
4569                        brush_to_instance_fields(brush);
4570                    batch.rects.push(RectInstance {
4571                        xywh: ndc,
4572                        radii: radius.map(|r| r.0),
4573                        brush_type,
4574                        _pad: [0.0; 3],
4575                        color0,
4576                        color1,
4577                        grad_start,
4578                        grad_end,
4579                        fwd_mat,
4580                    });
4581                }
4582                SceneNode::Border {
4583                    rect,
4584                    color,
4585                    width,
4586                    radius,
4587                } => {
4588                    flush_if_prim_changed!("border", &self.borders);
4589                    let (ndc, fwd_mat) = rect_to_instance_ndc(
4590                        *rect,
4591                        current_transform,
4592                        current_target_size.0,
4593                        current_target_size.1,
4594                    );
4595                    batch.borders.push(BorderInstance {
4596                        xywh: ndc,
4597                        radii: radius.map(|r| r.0),
4598                        stroke: width.0,
4599                        color: color.to_linear(),
4600                        fwd_mat,
4601                    });
4602                }
4603                SceneNode::Ellipse { rect, brush } => {
4604                    flush_if_prim_changed!("ellipse", &self.ellipses);
4605                    let (ndc, fwd_mat) = rect_to_instance_ndc(
4606                        *rect,
4607                        current_transform,
4608                        current_target_size.0,
4609                        current_target_size.1,
4610                    );
4611                    let color = brush_to_solid_color(brush);
4612                    batch.ellipses.push(EllipseInstance {
4613                        xywh: ndc,
4614                        color,
4615                        fwd_mat,
4616                    });
4617                }
4618                SceneNode::EllipseBorder { rect, color, width } => {
4619                    flush_if_prim_changed!("ellipse_border", &self.ellipse_borders);
4620                    let (ndc, fwd_mat) = rect_to_instance_ndc(
4621                        *rect,
4622                        current_transform,
4623                        current_target_size.0,
4624                        current_target_size.1,
4625                    );
4626                    let pad_px = width.0 * 0.5 + 2.0;
4627                    let pad = (pad_px / current_target_size.0) * 2.0;
4628                    batch.e_borders.push(EllipseBorderInstance {
4629                        xywh: ndc,
4630                        stroke: width.0,
4631                        pad,
4632                        color: color.to_linear(),
4633                        fwd_mat,
4634                    });
4635                }
4636                SceneNode::Arc {
4637                    rect,
4638                    start_angle,
4639                    sweep_angle,
4640                    stroke_width,
4641                    color,
4642                    cap,
4643                } => {
4644                    flush_if_prim_changed!("arc", &self.arcs);
4645                    let (ndc, fwd_mat) = rect_to_instance_ndc(
4646                        *rect,
4647                        current_transform,
4648                        current_target_size.0,
4649                        current_target_size.1,
4650                    );
4651                    let pad_px = stroke_width.0 * 0.5 + 2.0;
4652                    let pad = (pad_px / current_target_size.0) * 2.0;
4653                    let cap_val = match cap {
4654                        StrokeCap::Butt => 0.0,
4655                        StrokeCap::Round => 1.0,
4656                        StrokeCap::Square => 2.0,
4657                    };
4658                    batch.arcs.push(ArcInstance {
4659                        xywh: ndc,
4660                        start_angle: *start_angle,
4661                        sweep_angle: *sweep_angle,
4662                        stroke: stroke_width.0,
4663                        pad,
4664                        color: color.to_linear(),
4665                        fwd_mat,
4666                        cap: cap_val,
4667                    });
4668                }
4669                SceneNode::Text {
4670                    rect,
4671                    text,
4672                    color,
4673                    size,
4674                    font_family,
4675                    text_align: _,
4676                    font_weight,
4677                    font_style,
4678                    text_decoration,
4679                    letter_spacing,
4680                    line_height: _,
4681                    extra_style,
4682                    url: _,
4683                    font_variation_settings,
4684                } => {
4685                    flush_batch!(); // flush any prior primitives
4686
4687                    let px = size.0;
4688                    let lh_ratio = rect.h / px;
4689                    let fw = font_weight.0;
4690                    let fs = if *font_style == FontStyle::Italic {
4691                        1
4692                    } else {
4693                        0
4694                    };
4695                    let shaped = repose_text::shape_line(
4696                        text.as_ref(),
4697                        px,
4698                        lh_ratio,
4699                        *font_family,
4700                        fw,
4701                        fs,
4702                        letter_spacing.0,
4703                        font_variation_settings.as_deref(),
4704                    );
4705                    let baseline_y = shaped.first().map(|g| rect.y + g.y);
4706
4707                    let fwd = forward_rs_mat(current_transform);
4708                    let has_linear = fwd != [1.0, 0.0, 0.0, 1.0];
4709
4710                    let lin = current_transform.linear();
4711                    let tr_x = current_transform.translate_x;
4712                    let tr_y = current_transform.translate_y;
4713
4714                    let make_glyph_instance =
4715                        |gx: f32, gy: f32, gw: f32, gh: f32| -> ([f32; 4], [f32; 4]) {
4716                            if has_linear {
4717                                let gc_x = gx + gw * 0.5;
4718                                let gc_y = gy + gh * 0.5;
4719                                let wc_x = lin[0] * gc_x + lin[1] * gc_y + tr_x;
4720                                let wc_y = lin[2] * gc_x + lin[3] * gc_y + tr_y;
4721                                let ww = gw * current_transform.scale_x;
4722                                let wh = gh * current_transform.scale_y;
4723                                let ex = fwd[0].abs() * ww * 0.5 + fwd[1].abs() * wh * 0.5;
4724                                let ey = fwd[2].abs() * ww * 0.5 + fwd[3].abs() * wh * 0.5;
4725                                let ndc_tl = to_ndc(
4726                                    wc_x - ex,
4727                                    wc_y - ey,
4728                                    ex * 2.0,
4729                                    ey * 2.0,
4730                                    current_target_size.0,
4731                                    current_target_size.1,
4732                                );
4733                                let ndc = [
4734                                    ndc_tl[0] + ndc_tl[2] * 0.5,
4735                                    ndc_tl[1] + ndc_tl[3] * 0.5,
4736                                    ndc_tl[2],
4737                                    ndc_tl[3],
4738                                ];
4739                                (ndc, fwd)
4740                            } else {
4741                                let (sx, sy) = if current_transform.scale_x == 1.0
4742                                    && current_transform.scale_y == 1.0
4743                                {
4744                                    (gx.round(), gy.round())
4745                                } else {
4746                                    (gx, gy)
4747                                };
4748                                rect_to_instance_ndc(
4749                                    repose_core::Rect {
4750                                        x: sx,
4751                                        y: sy,
4752                                        w: gw,
4753                                        h: gh,
4754                                    },
4755                                    current_transform,
4756                                    current_target_size.0,
4757                                    current_target_size.1,
4758                                )
4759                            }
4760                        };
4761
4762                    let baseline_shift_y: f32 = px * extra_style.baseline_shift.0;
4763
4764                    let (
4765                        is_stroke,
4766                        stroke_width,
4767                        stroke_cap,
4768                        stroke_join,
4769                        stroke_miter,
4770                        stroke_path_effect,
4771                    ) = match &extra_style.draw_style {
4772                        repose_core::DrawStyle::Stroke {
4773                            width,
4774                            cap,
4775                            join,
4776                            miter,
4777                            path_effect,
4778                        } => (true, *width, *cap, *join, *miter, path_effect.clone()),
4779                        _ => (
4780                            false,
4781                            0.0,
4782                            repose_core::StrokeCap::Butt,
4783                            repose_core::StrokeJoin::Miter,
4784                            4.0,
4785                            None,
4786                        ),
4787                    };
4788                    let stroke_tess_key = if is_stroke {
4789                        Some(slug::StrokeTessKey::new(
4790                            stroke_width,
4791                            stroke_cap,
4792                            stroke_join,
4793                            stroke_miter,
4794                            &stroke_path_effect,
4795                        ))
4796                    } else {
4797                        None
4798                    };
4799
4800                    for sg in shaped {
4801                        let gx = rect.x + sg.x + sg.bearing_x;
4802                        let gy = rect.y + sg.y - sg.bearing_y + baseline_shift_y;
4803
4804                        // Vector glyph path: tessellated geometry with MSAA.
4805                        if self.slug_enabled {
4806                            let ck = repose_text::lookup_cache_key(sg.key, sg.px);
4807                            if let Some(ref ck) = ck {
4808                                // Check if cached.
4809                                let need_tessellate = self.slug_cache.get(ck).is_none_or(|g| {
4810                                    if is_stroke {
4811                                        let key = stroke_tess_key.as_ref().unwrap();
4812                                        !g.stroke_variants.contains_key(key)
4813                                    } else {
4814                                        g.fill_vertices.is_none()
4815                                    }
4816                                });
4817                                if need_tessellate {
4818                                    if let Some((ck2, commands)) =
4819                                        repose_text::lookup_and_extract_outline(sg.key, sg.px)
4820                                    {
4821                                        let font_size_px = f32::from_bits(ck2.font_size_bits);
4822                                        if is_stroke {
4823                                            self.slug_cache.get_or_insert_stroke(
4824                                                ck2,
4825                                                font_size_px,
4826                                                &commands,
4827                                                stroke_width,
4828                                                stroke_cap,
4829                                                stroke_join,
4830                                                stroke_miter,
4831                                                &stroke_path_effect,
4832                                            );
4833                                        } else {
4834                                            self.slug_cache.get_or_insert(
4835                                                ck2,
4836                                                font_size_px,
4837                                                &commands,
4838                                            );
4839                                        }
4840                                    }
4841                                } else {
4842                                    self.slug_cache.touch(ck);
4843                                }
4844                            }
4845                            if let Some(entry) = ck.as_ref().and_then(|ck| self.slug_cache.get(ck))
4846                            {
4847                                let ox = rect.x + sg.x;
4848                                let oy = rect.y + sg.y + baseline_shift_y;
4849                                let scx = current_transform.scale_x;
4850                                let scy = current_transform.scale_y;
4851                                let ttx = current_transform.translate_x;
4852                                let tty = current_transform.translate_y;
4853
4854                                let tf = |x: f32, y: f32| -> (f32, f32) {
4855                                    if has_linear {
4856                                        (
4857                                            lin[0] * x + lin[1] * y + ttx,
4858                                            lin[2] * x + lin[3] * y + tty,
4859                                        )
4860                                    } else {
4861                                        (x * scx + ttx, y * scy + tty)
4862                                    }
4863                                };
4864
4865                                let tw = current_target_size.0;
4866                                let th = current_target_size.1;
4867
4868                                let verts = if is_stroke {
4869                                    let key = stroke_tess_key.as_ref().unwrap();
4870                                    entry
4871                                        .stroke_variants
4872                                        .get(key)
4873                                        .map(|v| v.as_slice())
4874                                        .unwrap_or(&[])
4875                                } else {
4876                                    entry.fill_vertices.as_deref().unwrap_or(&[])
4877                                };
4878
4879                                for &v in verts {
4880                                    let (sx, sy) = tf(ox + v[0] * px, oy - v[1] * px);
4881                                    let ndc_x = sx / tw * 2.0 - 1.0;
4882                                    let ndc_y = -(sy / th) * 2.0 + 1.0;
4883                                    slug_verts_local.push(slug::TessVertex {
4884                                        ndc_pos: [ndc_x, ndc_y],
4885                                        color: color.to_linear(),
4886                                    });
4887                                }
4888
4889                                if is_stroke {
4890                                    // Stroke glyphs cannot use atlas fallback...
4891                                    continue;
4892                                }
4893                                continue;
4894                            }
4895                        }
4896
4897                        // Don't use atlas fallback for strokes too
4898                        if is_stroke {
4899                            continue;
4900                        }
4901
4902                        if let Some(info) = self.upload_glyph_color(sg.key, sg.px) {
4903                            let (ndc, fwd_mat) = make_glyph_instance(gx, gy, info.w, info.h);
4904                            batch.colors.push(GlyphInstance {
4905                                xywh: ndc,
4906                                uv: [info.u0, info.v1, info.u1, info.v0],
4907                                color: color.to_linear(),
4908                                fwd_mat,
4909                            });
4910                        } else if let Some(info) = self.upload_glyph_mask(sg.key, sg.px) {
4911                            let (ndc, fwd_mat) = make_glyph_instance(gx, gy, info.w, info.h);
4912                            batch.masks.push(GlyphInstance {
4913                                xywh: ndc,
4914                                uv: [info.u0, info.v1, info.u1, info.v0],
4915                                color: color.to_linear(),
4916                                fwd_mat,
4917                            });
4918                        }
4919                    }
4920
4921                    // Upload slug vertices if any
4922                    if !slug_verts_local.is_empty() {
4923                        let bytes = bytemuck::cast_slice(&slug_verts_local);
4924                        self.slug_ring.grow_to_fit(&self.device, bytes.len() as u64);
4925                        let (off, _) = self.slug_ring.alloc_write(&self.queue, bytes);
4926                        current_pass.cmds.push(Cmd::GlyphsVector {
4927                            off,
4928                            cnt: slug_verts_local.len() as u32,
4929                        });
4930                        slug_verts_local.clear();
4931                    }
4932
4933                    // Text decoration: underline / strikethrough
4934                    if (text_decoration.underline || text_decoration.strikethrough)
4935                        && let Some(baseline_y) = baseline_y
4936                    {
4937                        flush_batch!();
4938                        current_prim = Some("rect");
4939                        let deco_color = text_decoration.color.unwrap_or(*color);
4940                        let thickness = (px * 0.07).max(1.0);
4941
4942                        if text_decoration.underline {
4943                            let dy = baseline_y + px * 0.1;
4944                            let (ndc, fwd_mat) = rect_to_instance_ndc(
4945                                repose_core::Rect {
4946                                    x: rect.x,
4947                                    y: dy,
4948                                    w: rect.w,
4949                                    h: thickness,
4950                                },
4951                                current_transform,
4952                                current_target_size.0,
4953                                current_target_size.1,
4954                            );
4955                            batch.rects.push(RectInstance {
4956                                xywh: ndc,
4957                                radii: [0.0; 4],
4958                                brush_type: 0,
4959                                _pad: [0.0; 3],
4960                                color0: deco_color.to_linear(),
4961                                color1: [0.0; 4],
4962                                grad_start: [0.0; 2],
4963                                grad_end: [0.0; 2],
4964                                fwd_mat,
4965                            });
4966                        }
4967                        if text_decoration.strikethrough {
4968                            let sy = baseline_y - px * 0.3;
4969                            let (ndc, fwd_mat) = rect_to_instance_ndc(
4970                                repose_core::Rect {
4971                                    x: rect.x,
4972                                    y: sy,
4973                                    w: rect.w,
4974                                    h: thickness,
4975                                },
4976                                current_transform,
4977                                current_target_size.0,
4978                                current_target_size.1,
4979                            );
4980                            batch.rects.push(RectInstance {
4981                                xywh: ndc,
4982                                radii: [0.0; 4],
4983                                brush_type: 0,
4984                                _pad: [0.0; 3],
4985                                color0: deco_color.to_linear(),
4986                                color1: [0.0; 4],
4987                                grad_start: [0.0; 2],
4988                                grad_end: [0.0; 2],
4989                                fwd_mat,
4990                            });
4991                        }
4992                    }
4993                }
4994                SceneNode::Image {
4995                    rect,
4996                    handle,
4997                    tint,
4998                    fit,
4999                } => {
5000                    flush_batch!();
5001
5002                    // Update usage timestamp for eviction, lazily re-uploading
5003                    // evicted RGBA images from their retained source.
5004                    let (img_w, img_h, is_nv12) = match self.resolve_image_for_draw(*handle) {
5005                        Some(wh) => wh,
5006                        None => {
5007                            log::warn!("Image handle {} not found", handle);
5008                            continue;
5009                        }
5010                    };
5011
5012                    let src_w = img_w as f32;
5013                    let src_h = img_h as f32;
5014
5015                    let dst_w = rect.w.max(0.0);
5016                    let dst_h = rect.h.max(0.0);
5017                    if dst_w <= 0.0 || dst_h <= 0.0 {
5018                        continue;
5019                    }
5020
5021                    let (draw_rect, uv_rect) = match fit {
5022                        repose_core::view::ImageFit::Contain => {
5023                            let scale = (dst_w / src_w).min(dst_h / src_h);
5024                            let w = src_w * scale;
5025                            let h = src_h * scale;
5026                            (
5027                                repose_core::Rect {
5028                                    x: rect.x + (dst_w - w) * 0.5,
5029                                    y: rect.y + (dst_h - h) * 0.5,
5030                                    w,
5031                                    h,
5032                                },
5033                                [0.0, 1.0, 1.0, 0.0],
5034                            )
5035                        }
5036                        repose_core::view::ImageFit::Cover => {
5037                            let scale = (dst_w / src_w).max(dst_h / src_h);
5038                            let content_w = src_w * scale;
5039                            let content_h = src_h * scale;
5040                            let overflow_x = (content_w - dst_w) * 0.5;
5041                            let overflow_y = (content_h - dst_h) * 0.5;
5042                            let u0 = (overflow_x / content_w).clamp(0.0, 1.0);
5043                            let v0 = (overflow_y / content_h).clamp(0.0, 1.0);
5044                            let u1 = ((overflow_x + dst_w) / content_w).clamp(0.0, 1.0);
5045                            let v1 = ((overflow_y + dst_h) / content_h).clamp(0.0, 1.0);
5046                            (*rect, [u0, 1.0 - v0, u1, 1.0 - v1])
5047                        }
5048                        repose_core::view::ImageFit::FitWidth => {
5049                            let scale = dst_w / src_w;
5050                            (
5051                                repose_core::Rect {
5052                                    x: rect.x,
5053                                    y: rect.y + (dst_h - src_h * scale) * 0.5,
5054                                    w: dst_w,
5055                                    h: src_h * scale,
5056                                },
5057                                [0.0, 1.0, 1.0, 0.0],
5058                            )
5059                        }
5060                        repose_core::view::ImageFit::FitHeight => {
5061                            let scale = dst_h / src_h;
5062                            (
5063                                repose_core::Rect {
5064                                    x: rect.x + (dst_w - src_w * scale) * 0.5,
5065                                    y: rect.y,
5066                                    w: src_w * scale,
5067                                    h: dst_h,
5068                                },
5069                                [0.0, 1.0, 1.0, 0.0],
5070                            )
5071                        }
5072                        repose_core::view::ImageFit::FillBounds => (*rect, [0.0, 1.0, 1.0, 0.0]),
5073                        repose_core::view::ImageFit::Inside => {
5074                            let scale = (dst_w / src_w).min(dst_h / src_h).min(1.0);
5075                            let w = src_w * scale;
5076                            let h = src_h * scale;
5077                            (
5078                                repose_core::Rect {
5079                                    x: rect.x + (dst_w - w) * 0.5,
5080                                    y: rect.y + (dst_h - h) * 0.5,
5081                                    w,
5082                                    h,
5083                                },
5084                                [0.0, 1.0, 1.0, 0.0],
5085                            )
5086                        }
5087                        repose_core::view::ImageFit::None => {
5088                            (
5089                                repose_core::Rect {
5090                                    x: rect.x,
5091                                    y: rect.y,
5092                                    w: src_w.min(dst_w),
5093                                    h: src_h.min(dst_h),
5094                                },
5095                                // If larger than dst, crop top-left of source:
5096                                [
5097                                    0.0,
5098                                    1.0,
5099                                    (dst_w / src_w).min(1.0),
5100                                    1.0 - (dst_h / src_h).min(1.0),
5101                                ],
5102                            )
5103                        }
5104                        _ => continue,
5105                    };
5106
5107                    let (ndc_center, fwd_mat) = rect_to_instance_ndc(
5108                        draw_rect,
5109                        current_transform,
5110                        current_target_size.0,
5111                        current_target_size.1,
5112                    );
5113
5114                    if is_nv12 {
5115                        let uv_x_offset = if let Some(ImageTex::Nv12 { w, color_info, .. }) =
5116                            self.images.get(handle)
5117                        {
5118                            match color_info.chroma_siting {
5119                                ChromaSiting::Center | ChromaSiting::TopLeft => 0.0,
5120                                ChromaSiting::Left => -1.0 / *w as f32,
5121                            }
5122                        } else {
5123                            0.0
5124                        };
5125
5126                        let inst = Nv12Instance {
5127                            xywh: ndc_center,
5128                            uv: uv_rect,
5129                            color: tint.to_linear(),
5130                            uv_x_offset,
5131                            fwd_mat,
5132                            _pad: [0.0],
5133                        };
5134                        if let Some((off, _)) = self.nv12.upload(&self.device, &self.queue, &[inst])
5135                        {
5136                            current_pass.cmds.push(Cmd::ImageNv12 {
5137                                off,
5138                                cnt: 1,
5139                                handle: *handle,
5140                            });
5141                        }
5142                    } else {
5143                        // RGBA uses GlyphInstance struct (reused pipeline)
5144                        let inst = GlyphInstance {
5145                            xywh: ndc_center,
5146                            uv: uv_rect,
5147                            color: tint.to_linear(),
5148                            fwd_mat,
5149                        };
5150                        if let Some((off, _)) =
5151                            self.glyph_color.upload(&self.device, &self.queue, &[inst])
5152                        {
5153                            current_pass.cmds.push(Cmd::ImageRgba {
5154                                off,
5155                                cnt: 1,
5156                                handle: *handle,
5157                            });
5158                        }
5159                    }
5160                }
5161                SceneNode::PushClip { rect, radius, op } => {
5162                    flush_batch!(); // flush content before entering clip
5163
5164                    let is_diff = matches!(op, repose_core::ClipOp::Difference);
5165
5166                    let t_identity = Transform::identity();
5167                    let current_transform = transform_stack.last().unwrap_or(&t_identity);
5168                    let transformed = affine_aabb(current_transform, rect);
5169
5170                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
5171                    let next_scissor = if is_diff {
5172                        top
5173                    } else {
5174                        intersect(top, transformed)
5175                    };
5176                    scissor_stack.push(next_scissor);
5177                    let scissor = to_scissor(
5178                        &next_scissor,
5179                        current_target_size.0 as u32,
5180                        current_target_size.1 as u32,
5181                    );
5182
5183                    let clip_ndc_tl = to_ndc(
5184                        transformed.x,
5185                        transformed.y,
5186                        transformed.w,
5187                        transformed.h,
5188                        current_target_size.0,
5189                        current_target_size.1,
5190                    );
5191                    let inst = ClipInstance {
5192                        xywh: [
5193                            clip_ndc_tl[0] + clip_ndc_tl[2] * 0.5,
5194                            clip_ndc_tl[1] + clip_ndc_tl[3] * 0.5,
5195                            clip_ndc_tl[2],
5196                            clip_ndc_tl[3],
5197                        ],
5198                        radii: radius.map(|r| r.0),
5199                        fwd_mat: [1.0, 0.0, 0.0, 1.0],
5200                    };
5201                    let bytes = bytemuck::bytes_of(&inst);
5202                    self.clip_ring.grow_to_fit(&self.device, bytes.len() as u64);
5203                    let (off, _) = self.clip_ring.alloc_write(&self.queue, bytes);
5204
5205                    let rounded = radius.iter().any(|&r| r.0 > 0.5);
5206
5207                    current_pass.cmds.push(Cmd::ClipPush {
5208                        off,
5209                        cnt: 1,
5210                        scissor,
5211                        difference: is_diff,
5212                        rounded,
5213                    });
5214                    clip_cmd_stack.push((off, 1, is_diff));
5215                }
5216                SceneNode::PopClip => {
5217                    flush_batch!();
5218
5219                    if !scissor_stack.is_empty() {
5220                        scissor_stack.pop();
5221                    } else {
5222                        log::warn!("PopClip with empty stack");
5223                    }
5224
5225                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
5226                    let scissor = to_scissor(
5227                        &top,
5228                        current_target_size.0 as u32,
5229                        current_target_size.1 as u32,
5230                    );
5231                    let (off, cnt, difference) = clip_cmd_stack.pop().unwrap_or((0, 0, false));
5232                    current_pass.cmds.push(Cmd::ClipPop {
5233                        off,
5234                        cnt,
5235                        scissor,
5236                        difference,
5237                    });
5238                }
5239                SceneNode::Shadow {
5240                    rect,
5241                    radius,
5242                    elevation: _,
5243                    color,
5244                } => {
5245                    flush_if_prim_changed!("rect", &self.rects);
5246                    let (ndc, fwd_mat) = rect_to_instance_ndc(
5247                        *rect,
5248                        current_transform,
5249                        current_target_size.0,
5250                        current_target_size.1,
5251                    );
5252                    let (brush_type, color0, _color1, _grad_start, _grad_end) =
5253                        brush_to_instance_fields(&Brush::Solid(*color));
5254                    batch.rects.push(RectInstance {
5255                        xywh: ndc,
5256                        radii: radius.map(|r| r.0),
5257                        brush_type,
5258                        _pad: [0.0; 3],
5259                        color0,
5260                        color1: [0.0; 4],
5261                        grad_start: [0.0; 2],
5262                        grad_end: [0.0; 2],
5263                        fwd_mat,
5264                    });
5265                }
5266                SceneNode::PushTransform { transform } => {
5267                    flush_batch!(); // flush before transform change
5268                    let combined = current_transform.combine(transform);
5269                    transform_stack.push(combined);
5270                }
5271                SceneNode::PopTransform => {
5272                    flush_batch!(); // flush before transform change
5273                    transform_stack.pop();
5274                }
5275                SceneNode::BeginLayer {
5276                    rect,
5277                    layer_id,
5278                    alpha,
5279                    blur_radius_x,
5280                    blur_radius_y,
5281                    rectangle_edge: _,
5282                } => {
5283                    flush_batch!();
5284                    // Layer rect is already snapped to whole pixels in layout;
5285                    // round() keeps any bypass of that snap consistent.
5286                    let w = (rect.w.round().max(1.0)) as u32;
5287                    let h = (rect.h.round().max(1.0)) as u32;
5288                    saved_scissor_stack =
5289                        std::mem::replace(&mut scissor_stack, Vec::with_capacity(8));
5290                    saved_root_clip_rect = std::mem::replace(
5291                        &mut root_clip_rect,
5292                        repose_core::Rect {
5293                            x: 0.0,
5294                            y: 0.0,
5295                            w: w as f32,
5296                            h: h as f32,
5297                        },
5298                    );
5299                    scissor_stack.push(root_clip_rect);
5300                    // Close out the current pass, start a new one for the layer.
5301                    let prev_target = current_pass.target;
5302                    let prev_scissor = current_pass.initial_scissor;
5303                    let saved = std::mem::replace(
5304                        &mut current_pass,
5305                        Pass {
5306                            target: PassTarget::Layer(*layer_id),
5307                            initial_scissor: (0, 0, w, h),
5308                            clear_color: Some([0.0, 0.0, 0.0, 0.0]),
5309                            cmds: Vec::new(),
5310                        },
5311                    );
5312                    passes.push(saved);
5313                    target_stack.push(prev_target);
5314                    let _ = prev_scissor; // initial_scissor of resumed pass is restored at EndLayer
5315                    // Get or create the layer's offscreen texture now so that
5316                    // subsequent scissor ops / draws have a valid target.
5317                    self.get_or_create_layer(*layer_id, w, h, *rect);
5318                    current_target_size = (w as f32, h as f32);
5319                    layer_alphas.push((*layer_id, *alpha, current_pass.initial_scissor));
5320                    // Store blur info for post-processing after EndLayer
5321                    if blur_radius_x.0 > 0.0 || blur_radius_y.0 > 0.0 {
5322                        layer_blurs.push((*layer_id, blur_radius_x.0, blur_radius_y.0));
5323                    }
5324                }
5325                SceneNode::EndLayer { layer_id } => {
5326                    flush_batch!();
5327                    scissor_stack = std::mem::take(&mut saved_scissor_stack);
5328                    root_clip_rect = saved_root_clip_rect;
5329                    // Finish the layer's pass, start a new one on the previous target.
5330                    let saved = std::mem::replace(
5331                        &mut current_pass,
5332                        Pass {
5333                            target: target_stack.pop().unwrap_or(PassTarget::Surface),
5334                            initial_scissor: (0, 0, self.output_width, self.output_height),
5335                            clear_color: None, // LoadOp::Load - don't wipe earlier surface content
5336                            cmds: Vec::new(),
5337                        },
5338                    );
5339                    passes.push(saved);
5340                    current_target_size = (fb_w, fb_h);
5341                    // Issue a composite quad for the just-finished layer in the new pass.
5342                    if let Some((_, layer_alpha, _)) = layer_alphas
5343                        .iter()
5344                        .find(|(id, _, _)| id == layer_id)
5345                        .copied()
5346                    {
5347                        let layer = self.layer_pool.get(layer_id).expect("layer target");
5348                        let ndc_tl = to_ndc(
5349                            layer.rect_px.0,
5350                            layer.rect_px.1,
5351                            layer.rect_px.2,
5352                            layer.rect_px.3,
5353                            fb_w,
5354                            fb_h,
5355                        );
5356                        let uv_u1 = layer.rect_px.2 / layer.width.max(1) as f32;
5357                        let uv_v1 = layer.rect_px.3 / layer.height.max(1) as f32;
5358                        // Check if this layer needs content blur
5359                        let blur_px_val = layer_blurs
5360                            .iter()
5361                            .find(|(id, _, _)| id == layer_id)
5362                            .map(|(_, bx, by)| (*bx, *by));
5363                        if let Some((blur_x, blur_y)) =
5364                            blur_px_val.filter(|(bx, by)| *bx > 0.0 || *by > 0.0)
5365                        {
5366                            // Content blur: draw blurred version using the blur_content pipeline
5367                            let bw_uv = (blur_x * 1.5) / layer.width.max(1) as f32;
5368                            let bh_uv = (blur_y * 1.5) / layer.height.max(1) as f32;
5369                            let inst = BlurInstance {
5370                                xywh: [
5371                                    ndc_tl[0] + ndc_tl[2] * 0.5,
5372                                    ndc_tl[1] + ndc_tl[3] * 0.5,
5373                                    ndc_tl[2],
5374                                    ndc_tl[3],
5375                                ],
5376                                uv: [0.0, 0.0, uv_u1, uv_v1],
5377                                color: [1.0, 1.0, 1.0, layer_alpha],
5378                                blur_uv: [bw_uv, bh_uv],
5379                                fwd_mat: [1.0, 0.0, 0.0, 1.0],
5380                            };
5381                            self.blur_ring.grow_to_fit(
5382                                &self.device,
5383                                std::mem::size_of::<BlurInstance>() as u64,
5384                            );
5385                            let bytes = bytemuck::bytes_of(&inst);
5386                            let (off, _) = self.blur_ring.alloc_write(&self.queue, bytes);
5387                            current_pass.cmds.push(Cmd::CompositeBlur {
5388                                off,
5389                                cnt: 1,
5390                                layer_id: *layer_id,
5391                            });
5392                        } else {
5393                            // Normal sharp composite
5394                            let inst = GlyphInstance {
5395                                xywh: [
5396                                    ndc_tl[0] + ndc_tl[2] * 0.5,
5397                                    ndc_tl[1] + ndc_tl[3] * 0.5,
5398                                    ndc_tl[2],
5399                                    ndc_tl[3],
5400                                ],
5401                                uv: [0.0, uv_v1, uv_u1, 0.0],
5402                                color: [1.0, 1.0, 1.0, layer_alpha],
5403                                fwd_mat: [1.0, 0.0, 0.0, 1.0],
5404                            };
5405                            if let Some((off, cnt)) =
5406                                self.glyph_color.upload(&self.device, &self.queue, &[inst])
5407                            {
5408                                current_pass.cmds.push(Cmd::CompositeLayer {
5409                                    off,
5410                                    cnt,
5411                                    layer_id: *layer_id,
5412                                });
5413                            }
5414                        }
5415                    }
5416                }
5417                SceneNode::CompositeShadow {
5418                    layer_id,
5419                    blur_px,
5420                    offset_px,
5421                    color,
5422                } => {
5423                    flush_batch!();
5424                    if let Some(layer) = self.layer_pool.get(layer_id).cloned() {
5425                        // Shadow rect = layer rect + offset.
5426                        let sx = layer.rect_px.0 + offset_px.0.0;
5427                        let sy = layer.rect_px.1 + offset_px.1.0;
5428                        let sw = layer.rect_px.2;
5429                        let sh = layer.rect_px.3;
5430                        // The blur in UV space is 1.5 * blur_px / texture_size
5431                        // (the 1.5 matches the 3x3 Gaussian span).
5432                        let bw_uv = (blur_px.0 * 1.5) / layer.width.max(1) as f32;
5433                        let bh_uv = (blur_px.0 * 1.5) / layer.height.max(1) as f32;
5434                        let shadow_u1 = layer.rect_px.2 / layer.width.max(1) as f32;
5435                        let shadow_v1 = layer.rect_px.3 / layer.height.max(1) as f32;
5436                        let ndc_tl = to_ndc(sx, sy, sw, sh, fb_w, fb_h);
5437                        let inst = BlurInstance {
5438                            xywh: [
5439                                ndc_tl[0] + ndc_tl[2] * 0.5,
5440                                ndc_tl[1] + ndc_tl[3] * 0.5,
5441                                ndc_tl[2],
5442                                ndc_tl[3],
5443                            ],
5444                            uv: [0.0, 0.0, shadow_u1, shadow_v1],
5445                            color: [
5446                                color.0 as f32 / 255.0,
5447                                color.1 as f32 / 255.0,
5448                                color.2 as f32 / 255.0,
5449                                color.3 as f32 / 255.0,
5450                            ],
5451                            blur_uv: [bw_uv, bh_uv],
5452                            fwd_mat: [1.0, 0.0, 0.0, 1.0],
5453                        };
5454                        self.blur_ring
5455                            .grow_to_fit(&self.device, std::mem::size_of::<BlurInstance>() as u64);
5456                        let bytes = bytemuck::bytes_of(&inst);
5457                        let (off, _) = self.blur_ring.alloc_write(&self.queue, bytes);
5458                        current_pass.cmds.push(Cmd::CompositeShadow {
5459                            off,
5460                            cnt: 1,
5461                            layer_id: *layer_id,
5462                        });
5463                    }
5464                }
5465                SceneNode::VectorMesh {
5466                    mesh,
5467                    transform,
5468                    paint,
5469                    clip: _,
5470                    blend: _,
5471                } => {
5472                    flush_batch!();
5473                    let t_identity = Transform::identity();
5474                    let current_transform = transform_stack.last().unwrap_or(&t_identity);
5475                    self.emit_vector_mesh(
5476                        current_transform,
5477                        mesh,
5478                        *transform,
5479                        paint,
5480                        &mut current_pass.cmds,
5481                    );
5482                }
5483                SceneNode::VectorOverlay { meshes } => {
5484                    flush_batch!();
5485                    for m in meshes.iter() {
5486                        let (voff, vcnt, ioff, icnt) = self.upload_mesh_geometry(m);
5487                        let uoff = self.alloc_mesh_uniform(MeshUniform::identity());
5488                        current_pass.cmds.push(Cmd::VectorOverlay {
5489                            voff,
5490                            vcnt,
5491                            ioff,
5492                            icnt,
5493                            uoff,
5494                        });
5495                    }
5496                }
5497                SceneNode::PushVectorClip { mesh } => {
5498                    flush_batch!();
5499                    let t_identity = Transform::identity();
5500                    let current_transform = transform_stack.last().unwrap_or(&t_identity);
5501                    let affine =
5502                        combine_mesh_affine(current_transform, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
5503                    let aabb = mesh_aabb(mesh, affine);
5504                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
5505                    let next = intersect(top, aabb);
5506                    scissor_stack.push(next);
5507                    let scissor = to_scissor(
5508                        &next,
5509                        current_target_size.0 as u32,
5510                        current_target_size.1 as u32,
5511                    );
5512                    let (voff, vcnt, ioff, icnt) = self.upload_mesh_geometry(mesh);
5513                    let uoff = self.alloc_mesh_uniform(mesh_uniform_from_paint(
5514                        affine,
5515                        &repose_core::PaintDesc::Solid,
5516                    ));
5517                    current_pass.cmds.push(Cmd::VectorClipPush {
5518                        voff,
5519                        vcnt,
5520                        ioff,
5521                        icnt,
5522                        uoff,
5523                        scissor,
5524                    });
5525                    self.mesh_clip_stack.push((voff, vcnt, ioff, icnt, uoff));
5526                }
5527                SceneNode::PopVectorClip => {
5528                    flush_batch!();
5529                    if !scissor_stack.is_empty() {
5530                        scissor_stack.pop();
5531                    } else {
5532                        log::warn!("PopVectorClip with empty scissor stack");
5533                    }
5534                    if let Some((voff, vcnt, ioff, icnt, uoff)) = self.mesh_clip_stack.pop() {
5535                        let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
5536                        let scissor = to_scissor(
5537                            &top,
5538                            current_target_size.0 as u32,
5539                            current_target_size.1 as u32,
5540                        );
5541                        current_pass.cmds.push(Cmd::VectorClipPop {
5542                            voff,
5543                            vcnt,
5544                            ioff,
5545                            icnt,
5546                            uoff,
5547                            scissor,
5548                        });
5549                    } else {
5550                        log::warn!("PopVectorClip with empty clip stack");
5551                    }
5552                }
5553                SceneNode::Callback { rect, payload } => {
5554                    flush_batch!();
5555                    let t = transform_stack
5556                        .last()
5557                        .copied()
5558                        .unwrap_or(Transform::identity());
5559                    let transformed = affine_aabb(&t, rect);
5560                    current_pass.cmds.push(Cmd::Callback {
5561                        rect: transformed,
5562                        payload: payload.clone(),
5563                    });
5564                }
5565                _ => {}
5566            }
5567        }
5568
5569        flush_batch!();
5570
5571        {
5572            let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
5573            let mut prepare_list: Vec<Arc<Callback>> = Vec::new();
5574            for node in &scene.nodes {
5575                if let SceneNode::Callback { payload, .. } = node
5576                    && payload.downcast_ref::<Callback>().is_some()
5577                {
5578                    let ptr = Arc::as_ptr(payload) as *const () as usize;
5579                    if seen.insert(ptr)
5580                        && let Ok(cb_arc) = payload.clone().downcast::<Callback>()
5581                    {
5582                        prepare_list.push(cb_arc);
5583                    }
5584                }
5585            }
5586            if !prepare_list.is_empty() {
5587                let screen_desc = ScreenDescriptor {
5588                    size_in_pixels: [self.output_width, self.output_height],
5589                    pixels_per_point: self.pixels_per_point,
5590                    target_format: self.output_format,
5591                    sample_count: self.msaa_samples.max(1),
5592                };
5593                let mut user_cmd_bufs: Vec<wgpu::CommandBuffer> = Vec::new();
5594                for cb in &prepare_list {
5595                    user_cmd_bufs.extend(cb.0.prepare(
5596                        &self.device,
5597                        &self.queue,
5598                        encoder,
5599                        &screen_desc,
5600                        &mut self.callback_resources,
5601                    ));
5602                }
5603                for cb in &prepare_list {
5604                    user_cmd_bufs.extend(cb.0.finish_prepare(
5605                        &self.device,
5606                        &self.queue,
5607                        encoder,
5608                        &screen_desc,
5609                        &mut self.callback_resources,
5610                    ));
5611                }
5612                // NOTE: For now submit immediately via queue
5613                // so they execute before main render pass.
5614                if !user_cmd_bufs.is_empty() {
5615                    self.queue.submit(user_cmd_bufs);
5616                }
5617            }
5618        }
5619
5620        // Push the final pass.
5621        passes.push(current_pass);
5622
5623        let globals_bytes = std::mem::size_of::<Globals>() as u64;
5624        let globals_staging = self.device.create_buffer(&wgpu::BufferDescriptor {
5625            label: Some("globals staging"),
5626            size: (passes.len().max(1) as u64) * globals_bytes,
5627            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
5628            mapped_at_creation: false,
5629        });
5630        for (i, pass) in passes.iter().enumerate() {
5631            let (target_w, target_h) = match pass.target {
5632                PassTarget::Surface => (fb_w, fb_h),
5633                PassTarget::Layer(layer_id) => {
5634                    let lt = self.layer_pool.get(&layer_id);
5635                    (
5636                        lt.map_or(fb_w, |l| l.width as f32),
5637                        lt.map_or(fb_h, |l| l.height as f32),
5638                    )
5639                }
5640            };
5641            self.queue.write_buffer(
5642                &globals_staging,
5643                (i as u64) * globals_bytes,
5644                bytemuck::bytes_of(&make_globals(target_w, target_h)),
5645            );
5646        }
5647
5648        let bind_mask = self.atlas_bind_group_mask();
5649        let bind_color = self.atlas_bind_group_color();
5650        let mut clip_depth: u32 = 0;
5651
5652        for (pass_index, pass) in std::mem::take(&mut passes).into_iter().enumerate() {
5653            let (color_view, resolve_target, depth_stencil_view, is_layer) = match pass.target {
5654                PassTarget::Surface => {
5655                    let swap_view = target_view.clone();
5656                    let use_ws = self.working_space && self.ws_view.is_some();
5657                    let (color, resolve) = if use_ws {
5658                        let ws_view = self.ws_view.as_ref().unwrap();
5659                        if let Some(msaa_view) = &self.msaa_view {
5660                            // MSAA resolves to working-space texture
5661                            (msaa_view.clone(), Some(ws_view.clone()))
5662                        } else {
5663                            // Direct render to working-space texture
5664                            (ws_view.clone(), None)
5665                        }
5666                    } else if let Some(msaa_view) = &self.msaa_view {
5667                        (msaa_view.clone(), Some(swap_view))
5668                    } else {
5669                        (swap_view, None)
5670                    };
5671                    (color, resolve, self.depth_stencil_view.clone(), false)
5672                }
5673                PassTarget::Layer(layer_id) => {
5674                    if let Some(lt) = self.layer_pool.get(&layer_id) {
5675                        (lt.view.clone(), None, lt.depth_stencil_view.clone(), true)
5676                    } else {
5677                        log::warn!("missing layer target {layer_id}");
5678                        continue;
5679                    }
5680                }
5681            };
5682
5683            encoder.copy_buffer_to_buffer(
5684                &globals_staging,
5685                (pass_index as u64) * globals_bytes,
5686                &self.globals_buf,
5687                0,
5688                globals_bytes,
5689            );
5690
5691            if is_layer {
5692                clip_depth = 0;
5693            }
5694
5695            let (tw, th) = match pass.target {
5696                PassTarget::Surface => (self.output_width, self.output_height),
5697                PassTarget::Layer(layer_id) => self
5698                    .layer_pool
5699                    .get(&layer_id)
5700                    .map(|l| (l.width, l.height))
5701                    .unwrap_or((self.output_width, self.output_height)),
5702            };
5703            let initial_scissor = clamp_scissor(
5704                pass.initial_scissor.0,
5705                pass.initial_scissor.1,
5706                pass.initial_scissor.2,
5707                pass.initial_scissor.3,
5708                tw,
5709                th,
5710            );
5711
5712            let pipes: &Pipelines = if is_layer {
5713                &self.layer_pipes
5714            } else {
5715                &self.surface_pipes
5716            };
5717
5718            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
5719                label: Some("pass"),
5720                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
5721                    view: &color_view,
5722                    resolve_target: resolve_target.as_ref(),
5723                    ops: wgpu::Operations {
5724                        load: match pass.clear_color {
5725                            Some(c) => wgpu::LoadOp::Clear(wgpu::Color {
5726                                r: c[0] as f64,
5727                                g: c[1] as f64,
5728                                b: c[2] as f64,
5729                                a: c[3] as f64,
5730                            }),
5731                            None => wgpu::LoadOp::Load,
5732                        },
5733                        store: wgpu::StoreOp::Store,
5734                    },
5735                    depth_slice: None,
5736                })],
5737                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
5738                    view: &depth_stencil_view,
5739                    depth_ops: None,
5740                    stencil_ops: Some(wgpu::Operations {
5741                        load: if is_layer || pass.clear_color.is_some() {
5742                            wgpu::LoadOp::Clear(0)
5743                        } else {
5744                            wgpu::LoadOp::Load
5745                        },
5746                        store: wgpu::StoreOp::Store,
5747                    }),
5748                }),
5749                timestamp_writes: None,
5750                occlusion_query_set: None,
5751                multiview_mask: None,
5752            });
5753
5754            rpass.set_bind_group(0, &self.globals_bind, &[]);
5755            rpass.set_stencil_reference(clip_depth);
5756            rpass.set_scissor_rect(
5757                initial_scissor.0,
5758                initial_scissor.1,
5759                initial_scissor.2,
5760                initial_scissor.3,
5761            );
5762
5763            macro_rules! draw_simple {
5764                ($pipeline:expr, $ring:expr, $inst:ty, $off:ident, $n:ident) => {{
5765                    rpass.set_pipeline($pipeline);
5766                    let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
5767                    rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
5768                    rpass.draw(0..6, 0..$n);
5769                }};
5770            }
5771
5772            macro_rules! draw_with_bind {
5773                ($pipeline:expr, $ring:expr, $inst:ty, $bind:expr, $off:ident, $n:ident) => {{
5774                    rpass.set_pipeline($pipeline);
5775                    rpass.set_bind_group(1, $bind, &[]);
5776                    let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
5777                    rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
5778                    rpass.draw(0..6, 0..$n);
5779                }};
5780            }
5781
5782            macro_rules! draw_indexed_mesh {
5783                ($pipeline:expr, $uoff:ident, $voff:ident, $vcnt:ident, $ioff:ident, $icnt:ident) => {{
5784                    rpass.set_pipeline($pipeline);
5785                    rpass.set_bind_group(1, &self.mesh_bind, &[$uoff as u32]);
5786                    let vbytes = ($vcnt as u64) * std::mem::size_of::<MeshVertex>() as u64;
5787                    rpass.set_vertex_buffer(0, self.mesh_verts.buf.slice($voff..$voff + vbytes));
5788                    let ibytes = ($icnt as u64) * std::mem::size_of::<u32>() as u64;
5789                    rpass.set_index_buffer(
5790                        self.mesh_indices.buf.slice($ioff..$ioff + ibytes),
5791                        wgpu::IndexFormat::Uint32,
5792                    );
5793                    rpass.draw_indexed(0..$icnt, 0, 0..1);
5794                }};
5795            }
5796
5797            for cmd in pass.cmds {
5798                match cmd {
5799                    Cmd::ClipPush {
5800                        off,
5801                        cnt: n,
5802                        scissor,
5803                        difference,
5804                        rounded,
5805                    } => {
5806                        let scissor =
5807                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
5808                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
5809                        rpass.set_stencil_reference(clip_depth);
5810
5811                        if difference {
5812                            rpass.set_pipeline(&pipes.clip_dec);
5813                        } else if self.msaa_samples > 1 && !is_layer && rounded {
5814                            rpass.set_pipeline(&pipes.clip_a2c);
5815                        } else {
5816                            rpass.set_pipeline(&pipes.clip_bin);
5817                        }
5818
5819                        let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
5820                        rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
5821                        rpass.draw(0..6, 0..n);
5822
5823                        if !difference {
5824                            clip_depth = (clip_depth + 1).min(255);
5825                            rpass.set_stencil_reference(clip_depth);
5826                        }
5827                    }
5828
5829                    Cmd::ClipPop {
5830                        off,
5831                        cnt: n,
5832                        scissor,
5833                        difference,
5834                    } => {
5835                        let scissor =
5836                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
5837                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
5838
5839                        if !difference && n > 0 {
5840                            rpass.set_stencil_reference(clip_depth);
5841                            rpass.set_pipeline(&pipes.clip_dec);
5842                            let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
5843                            rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
5844                            rpass.draw(0..6, 0..n);
5845                            clip_depth = clip_depth.saturating_sub(1);
5846                        } else if !difference {
5847                            clip_depth = clip_depth.saturating_sub(1);
5848                        }
5849                        rpass.set_stencil_reference(clip_depth);
5850                    }
5851
5852                    Cmd::Rect { off, cnt: n } => {
5853                        draw_simple!(&pipes.rects, self.rects.ring, RectInstance, off, n);
5854                    }
5855
5856                    Cmd::Border { off, cnt: n } => {
5857                        draw_simple!(&pipes.borders, self.borders.ring, BorderInstance, off, n);
5858                    }
5859
5860                    Cmd::GlyphsMask { off, cnt: n } => {
5861                        draw_with_bind!(
5862                            &pipes.text_mask,
5863                            self.glyph_mask.ring,
5864                            GlyphInstance,
5865                            &bind_mask,
5866                            off,
5867                            n
5868                        );
5869                    }
5870
5871                    Cmd::GlyphsColor { off, cnt: n } => {
5872                        draw_with_bind!(
5873                            &pipes.text_color,
5874                            self.glyph_color.ring,
5875                            GlyphInstance,
5876                            &bind_color,
5877                            off,
5878                            n
5879                        );
5880                    }
5881
5882                    Cmd::GlyphsVector { off, cnt: n } => {
5883                        if let Some(slug_pipe) = pipes.slug.as_ref() {
5884                            rpass.set_pipeline(slug_pipe);
5885                            let bytes = (n as u64) * std::mem::size_of::<slug::TessVertex>() as u64;
5886                            rpass.set_vertex_buffer(0, self.slug_ring.buf.slice(off..off + bytes));
5887                            rpass.draw(0..n, 0..1);
5888                        }
5889                    }
5890
5891                    Cmd::ImageRgba {
5892                        off,
5893                        cnt: n,
5894                        handle,
5895                    } => {
5896                        let bind_opt = match self.images.get(&handle) {
5897                            Some(ImageTex::Rgba { bind, .. }) => Some(bind),
5898                            Some(ImageTex::User { bind, .. }) => Some(bind),
5899                            _ => None,
5900                        };
5901                        if let Some(bind) = bind_opt {
5902                            draw_with_bind!(
5903                                &pipes.image_rgba,
5904                                self.glyph_color.ring,
5905                                GlyphInstance,
5906                                bind,
5907                                off,
5908                                n
5909                            );
5910                        }
5911                    }
5912
5913                    Cmd::ImageNv12 {
5914                        off,
5915                        cnt: n,
5916                        handle,
5917                    } => {
5918                        if let Some(ImageTex::Nv12 { bind, .. }) = self.images.get(&handle) {
5919                            draw_with_bind!(
5920                                &pipes.image_nv12,
5921                                self.nv12.ring,
5922                                Nv12Instance,
5923                                bind,
5924                                off,
5925                                n
5926                            );
5927                        }
5928                    }
5929
5930                    Cmd::Ellipse { off, cnt: n } => {
5931                        draw_simple!(&pipes.ellipses, self.ellipses.ring, EllipseInstance, off, n);
5932                    }
5933
5934                    Cmd::EllipseBorder { off, cnt: n } => {
5935                        draw_simple!(
5936                            &pipes.ellipse_borders,
5937                            self.ellipse_borders.ring,
5938                            EllipseBorderInstance,
5939                            off,
5940                            n
5941                        );
5942                    }
5943
5944                    Cmd::Arc { off, cnt: n } => {
5945                        draw_simple!(&pipes.arcs, self.arcs.ring, ArcInstance, off, n);
5946                    }
5947
5948                    Cmd::CompositeLayer {
5949                        off,
5950                        cnt: n,
5951                        layer_id,
5952                    } => {
5953                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
5954                            draw_with_bind!(
5955                                &pipes.image_rgba,
5956                                self.glyph_color.ring,
5957                                GlyphInstance,
5958                                &lt.bind,
5959                                off,
5960                                n
5961                            );
5962                        }
5963                    }
5964                    Cmd::CompositeShadow {
5965                        off,
5966                        cnt: n,
5967                        layer_id,
5968                    } => {
5969                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
5970                            draw_with_bind!(
5971                                &pipes.blur,
5972                                self.blur_ring,
5973                                BlurInstance,
5974                                &lt.bind_linear,
5975                                off,
5976                                n
5977                            );
5978                        }
5979                    }
5980                    Cmd::CompositeBlur {
5981                        off,
5982                        cnt: n,
5983                        layer_id,
5984                    } => {
5985                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
5986                            draw_with_bind!(
5987                                &pipes.blur_content,
5988                                self.blur_ring,
5989                                BlurInstance,
5990                                &lt.bind_linear,
5991                                off,
5992                                n
5993                            );
5994                        }
5995                    }
5996
5997                    Cmd::VectorMesh {
5998                        voff,
5999                        vcnt,
6000                        ioff,
6001                        icnt,
6002                        uoff,
6003                    } => {
6004                        draw_indexed_mesh!(&pipes.mesh, uoff, voff, vcnt, ioff, icnt);
6005                    }
6006
6007                    Cmd::VectorOverlay {
6008                        voff,
6009                        vcnt,
6010                        ioff,
6011                        icnt,
6012                        uoff,
6013                    } => {
6014                        draw_indexed_mesh!(&pipes.mesh_overlay, uoff, voff, vcnt, ioff, icnt);
6015                    }
6016
6017                    Cmd::VectorClipPush {
6018                        voff,
6019                        vcnt,
6020                        ioff,
6021                        icnt,
6022                        uoff,
6023                        scissor,
6024                    } => {
6025                        let scissor =
6026                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
6027                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
6028                        rpass.set_stencil_reference(clip_depth);
6029                        draw_indexed_mesh!(&pipes.mesh_clip_inc, uoff, voff, vcnt, ioff, icnt);
6030                        clip_depth = (clip_depth + 1).min(255);
6031                        rpass.set_stencil_reference(clip_depth);
6032                    }
6033
6034                    Cmd::VectorClipPop {
6035                        voff,
6036                        vcnt,
6037                        ioff,
6038                        icnt,
6039                        uoff,
6040                        scissor,
6041                    } => {
6042                        // Decrement the mask while the stencil reference is
6043                        // still at the depth it was incremented to, so the
6044                        // equal-compare fires; then step the clip depth down.
6045                        rpass.set_stencil_reference(clip_depth);
6046                        let scissor =
6047                            clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
6048                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
6049                        draw_indexed_mesh!(&pipes.mesh_clip_dec, uoff, voff, vcnt, ioff, icnt);
6050                        clip_depth = clip_depth.saturating_sub(1);
6051                        rpass.set_stencil_reference(clip_depth);
6052                    }
6053
6054                    Cmd::Callback { rect, payload } => {
6055                        if let Some(cb) = payload.downcast_ref::<Callback>() {
6056                            let vp_x = rect.x.floor().max(0.0);
6057                            let vp_y = rect.y.floor().max(0.0);
6058                            let vp_w = rect.w.ceil().max(1.0);
6059                            let vp_h = rect.h.ceil().max(1.0);
6060                            if vp_w > 0.0 && vp_h > 0.0 {
6061                                rpass.set_viewport(vp_x, vp_y, vp_w, vp_h, 0.0, 1.0);
6062                                let info = repose_core::PaintCallbackInfo {
6063                                    viewport: rect,
6064                                    clip_rect: rect,
6065                                    pixels_per_point: self.pixels_per_point,
6066                                    screen_size_px: [tw, th],
6067                                };
6068                                let rpass_static: &mut wgpu::RenderPass<'static> = unsafe {
6069                                    std::mem::transmute::<
6070                                        &mut wgpu::RenderPass<'_>,
6071                                        &mut wgpu::RenderPass<'static>,
6072                                    >(&mut rpass)
6073                                };
6074                                cb.0.paint(info, rpass_static, &self.callback_resources);
6075                                rpass.set_viewport(0.0, 0.0, tw as f32, th as f32, 0.0, 1.0);
6076                                rpass.set_bind_group(0, &self.globals_bind, &[]);
6077                                rpass.set_stencil_reference(clip_depth);
6078                            }
6079                        } else {
6080                            log::warn!("Unknown paint callback payload");
6081                        }
6082                    }
6083                }
6084            }
6085        }
6086
6087        // Display pass: linear working space -> sRGB OETF -> swapchain
6088        if self.working_space
6089            && let (Some(_ws_view), Some(ws_bind), Some(display_pipeline)) =
6090                (&self.ws_view, &self.ws_bind, &self.display_pipeline)
6091        {
6092            let swap_view = target_view.clone();
6093            let mut display_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
6094                label: Some("display transform"),
6095                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
6096                    view: &swap_view,
6097                    resolve_target: None,
6098                    ops: wgpu::Operations {
6099                        load: wgpu::LoadOp::Load,
6100                        store: wgpu::StoreOp::Store,
6101                    },
6102                    depth_slice: None,
6103                })],
6104                depth_stencil_attachment: None,
6105                timestamp_writes: None,
6106                occlusion_query_set: None,
6107                multiview_mask: None,
6108            });
6109            display_pass.set_pipeline(display_pipeline);
6110            display_pass.set_bind_group(1, ws_bind, &[]);
6111            display_pass.draw(0..3, 0..1);
6112        }
6113
6114        // Frame end maintenance: Evict unused images
6115        self.evict_unused_images();
6116    }
6117
6118    /// Render a scene into an externally-provided texture view.
6119    /// Use this when embedding Repose in a host that owns the GPU.
6120    /// The host is responsible for submitting the encoder and handling present.
6121    pub fn render_to_view(
6122        &mut self,
6123        scene: &Scene,
6124        encoder: &mut wgpu::CommandEncoder,
6125        target_view: &wgpu::TextureView,
6126        width: u32,
6127        height: u32,
6128        clear_color: Option<[f64; 4]>,
6129    ) {
6130        self.resize(width, height);
6131
6132        self.frame_index = self.frame_index.wrapping_add(1);
6133        self.slug_cache.next_frame();
6134
6135        if width == 0 || height == 0 {
6136            return;
6137        }
6138
6139        self.render_scene_to_encoder(scene, encoder, target_view, clear_color);
6140    }
6141}
6142
6143fn clamp_scissor(x: u32, y: u32, w: u32, h: u32, tw: u32, th: u32) -> (u32, u32, u32, u32) {
6144    let x = x.min(tw.saturating_sub(1));
6145    let y = y.min(th.saturating_sub(1));
6146    let w = w.min(tw.saturating_sub(x)).max(1);
6147    let h = h.min(th.saturating_sub(y)).max(1);
6148    (x, y, w, h)
6149}
6150
6151fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> repose_core::Rect {
6152    let x0 = a.x.max(b.x);
6153    let y0 = a.y.max(b.y);
6154    let x1 = (a.x + a.w).min(b.x + b.w);
6155    let y1 = (a.y + a.h).min(b.y + b.h);
6156    repose_core::Rect {
6157        x: x0,
6158        y: y0,
6159        w: (x1 - x0).max(0.0),
6160        h: (y1 - y0).max(0.0),
6161    }
6162}