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