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