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