Skip to main content

repose_render_wgpu/
lib.rs

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