Skip to main content

repose_render_wgpu/
lib.rs

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