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