Skip to main content

repose_render_wgpu/
lib.rs

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