Skip to main content

repose_render_wgpu/
lib.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use repose_core::color::{ChromaSiting, ColorInfo, PixelFormat};
6use repose_core::request_frame;
7use repose_core::{
8    Brush, FontStyle, GlyphRasterConfig, RenderBackend, Scene, SceneNode, StrokeCap, Transform,
9};
10use std::panic::{AssertUnwindSafe, catch_unwind};
11use wgpu::Instance;
12
13mod slug;
14
15#[derive(Clone)]
16struct UploadRing {
17    buf: wgpu::Buffer,
18    cap: u64,
19    head: u64,
20}
21
22impl UploadRing {
23    fn new(device: &wgpu::Device, label: &str, cap: u64) -> Self {
24        let buf = device.create_buffer(&wgpu::BufferDescriptor {
25            label: Some(label),
26            size: cap,
27            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
28            mapped_at_creation: false,
29        });
30        Self { buf, cap, head: 0 }
31    }
32
33    fn reset(&mut self) {
34        self.head = 0;
35    }
36
37    fn grow_to_fit(&mut self, device: &wgpu::Device, needed: u64) {
38        let start = (self.head + 3) & !3;
39        if start + needed <= self.cap {
40            return;
41        }
42        let new_cap = (start + needed).next_power_of_two();
43        self.buf = device.create_buffer(&wgpu::BufferDescriptor {
44            label: Some("upload ring (grown)"),
45            size: new_cap,
46            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
47            mapped_at_creation: false,
48        });
49        self.cap = new_cap;
50    }
51
52    fn alloc_write(&mut self, queue: &wgpu::Queue, bytes: &[u8]) -> (u64, u64) {
53        let len = bytes.len() as u64;
54        let start = (self.head + 3) & !3; // align to 4
55        let end = start + len;
56        assert!(end <= self.cap, "ring overflow - call grow_to_fit first");
57        queue.write_buffer(&self.buf, start, bytes);
58        self.head = end;
59        (start, len)
60    }
61}
62
63struct InstancedPipe<I: bytemuck::Pod> {
64    ring: UploadRing,
65    _marker: std::marker::PhantomData<I>,
66}
67
68impl<I: bytemuck::Pod> InstancedPipe<I> {
69    fn new(ring: UploadRing) -> Self {
70        Self {
71            ring,
72            _marker: std::marker::PhantomData,
73        }
74    }
75
76    fn upload(
77        &mut self,
78        device: &wgpu::Device,
79        queue: &wgpu::Queue,
80        data: &[I],
81    ) -> Option<(u64, u32)> {
82        if data.is_empty() {
83            return None;
84        }
85        let bytes = bytemuck::cast_slice(data);
86        self.ring.grow_to_fit(device, bytes.len() as u64);
87        let (off, wrote) = self.ring.alloc_write(queue, bytes);
88        debug_assert_eq!(wrote as usize, bytes.len());
89        Some((off, data.len() as u32))
90    }
91
92    fn reset(&mut self) {
93        self.ring.reset();
94    }
95}
96
97#[repr(C)]
98#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
99struct Globals {
100    ndc_to_px: [f32; 2],
101    _pad: [f32; 2],
102}
103
104pub struct WgpuBackend {
105    surface: wgpu::Surface<'static>,
106    device: wgpu::Device,
107    queue: wgpu::Queue,
108    config: wgpu::SurfaceConfiguration,
109
110    // Render pipelines. Two sets: one for the MSAA surface pass, one for
111    // graphics-layer render-to-texture passes (sample_count = 1).
112    surface_pipes: Pipelines,
113    layer_pipes: Pipelines,
114
115    // Instanced draw rings
116    rects: InstancedPipe<RectInstance>,
117    borders: InstancedPipe<BorderInstance>,
118    ellipses: InstancedPipe<EllipseInstance>,
119    ellipse_borders: InstancedPipe<EllipseBorderInstance>,
120    arcs: InstancedPipe<ArcInstance>,
121    glyph_mask: InstancedPipe<GlyphInstance>,
122    glyph_color: InstancedPipe<GlyphInstance>,
123
124    // Image bind layouts and shared sampler
125    image_bind_layout_rgba: wgpu::BindGroupLayout,
126    image_bind_layout_nv12: wgpu::BindGroupLayout,
127    image_sampler: wgpu::Sampler,
128
129    // Blur composite ring (for graphics-layer drop shadows)
130    blur_ring: UploadRing,
131
132    text_bind_layout: wgpu::BindGroupLayout,
133
134    // Stencil clip ring
135    clip_ring: UploadRing,
136
137    // Tessellated vector glyph pipeline (always enabled)
138    slug_enabled: bool,
139    slug_ring: UploadRing,
140    slug_cache: slug::GlyphSlugCache,
141
142    // Instanced NV12 ring
143    nv12: InstancedPipe<Nv12Instance>,
144
145    msaa_samples: u32,
146
147    // Depth-stencil target
148    depth_stencil_tex: wgpu::Texture,
149    depth_stencil_view: wgpu::TextureView,
150
151    // Optional MSAA color target
152    msaa_tex: Option<wgpu::Texture>,
153    msaa_view: Option<wgpu::TextureView>,
154
155    globals_layout: wgpu::BindGroupLayout,
156    globals_buf: wgpu::Buffer,
157    globals_bind: wgpu::BindGroup,
158
159    // Glyph atlas
160    atlas_mask: AtlasA8,
161    atlas_color: AtlasRGBA,
162
163    // Image management
164    next_image_handle: u64,
165    images: HashMap<u64, ImageTex>,
166
167    // Eviction stats
168    frame_index: u64,
169    image_bytes_total: u64,
170    image_evict_after_frames: u64,
171    image_budget_bytes: u64,
172
173    // Graphics layer pool. Maps `SceneNode::BeginLayer::layer_id` to a
174    // cached offscreen render target.
175    layer_pool: HashMap<u32, LayerTarget>,
176
177    // Linear working-space mode (default off -> fast playback path).
178    // When enabled, the scene is rendered into an Rgba16Float intermediate
179    // texture, then a final full-screen pass applies the display OETF.
180    working_space: bool,
181    ws_tex: Option<wgpu::Texture>,
182    ws_view: Option<wgpu::TextureView>,
183    ws_bind: Option<wgpu::BindGroup>,
184    display_pipeline: Option<wgpu::RenderPipeline>,
185    display_layout: Option<wgpu::BindGroupLayout>,
186}
187
188impl Drop for WgpuBackend {
189    fn drop(&mut self) {
190        let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
191    }
192}
193
194#[derive(Clone)]
195struct LayerTarget {
196    texture: wgpu::Texture,
197    view: wgpu::TextureView,
198    bind: wgpu::BindGroup,
199    depth_stencil_tex: wgpu::Texture,
200    depth_stencil_view: wgpu::TextureView,
201    width: u32,
202    height: u32,
203    rect_px: (f32, f32, f32, f32),
204}
205
206/// Identifies which render target a `Pass` draws into.
207#[derive(Clone, Copy)]
208enum PassTarget {
209    Surface,
210    Layer(u32),
211}
212
213/// A bundle of render pipelines for a single sample-count target. Created
214/// twice: once with `sample_count = msaa_samples` for the surface pass, and
215/// once with `sample_count = 1` for graphics-layer render-to-texture passes
216/// (where MSAA is wasted).
217struct Pipelines {
218    rects: wgpu::RenderPipeline,
219    borders: wgpu::RenderPipeline,
220    ellipses: wgpu::RenderPipeline,
221    ellipse_borders: wgpu::RenderPipeline,
222    arcs: wgpu::RenderPipeline,
223    text_mask: wgpu::RenderPipeline,
224    text_color: wgpu::RenderPipeline,
225    image_rgba: wgpu::RenderPipeline,
226    image_nv12: wgpu::RenderPipeline,
227    blur: wgpu::RenderPipeline,
228    blur_content: wgpu::RenderPipeline,
229    clip_a2c: wgpu::RenderPipeline,
230    clip_bin: wgpu::RenderPipeline,
231    clip_dec: wgpu::RenderPipeline,
232    slug: Option<wgpu::RenderPipeline>,
233}
234
235impl Pipelines {
236    fn create(
237        device: &wgpu::Device,
238        format: wgpu::TextureFormat,
239        sample_count: u32,
240        globals_layout: &wgpu::BindGroupLayout,
241        text_bind_layout: &wgpu::BindGroupLayout,
242        image_bind_layout_nv12: &wgpu::BindGroupLayout,
243        clip_pipeline_layout: &wgpu::PipelineLayout,
244        stencil_for_content: &wgpu::DepthStencilState,
245        stencil_for_clip_inc: &wgpu::DepthStencilState,
246        stencil_for_clip_dec: &wgpu::DepthStencilState,
247        clip_color_target: &wgpu::ColorTargetState,
248        clip_vertex_layout: &wgpu::VertexBufferLayout,
249    ) -> Self {
250        let msaa_state = wgpu::MultisampleState {
251            count: sample_count,
252            mask: !0,
253            alpha_to_coverage_enabled: false,
254        };
255
256        macro_rules! make_content_pipeline {
257            ($name:ident, $shader:literal, $inst_type:ty, $attrs:expr) => {
258                let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
259                    label: Some(concat!($shader, ".wgsl")),
260                    source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(concat!(
261                        "shaders/", $shader, ".wgsl"
262                    )))),
263                });
264                let pipeline_layout =
265                    device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
266                        label: Some(concat!($shader, " pipeline layout")),
267                        bind_group_layouts: &[Some(globals_layout)],
268                        immediate_size: 0,
269                    });
270                let $name = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
271                    label: Some(concat!($shader, " pipeline")),
272                    layout: Some(&pipeline_layout),
273                    vertex: wgpu::VertexState {
274                        module: &shader_module,
275                        entry_point: Some("vs_main"),
276                        buffers: &[Some(wgpu::VertexBufferLayout {
277                            array_stride: std::mem::size_of::<$inst_type>() as u64,
278                            step_mode: wgpu::VertexStepMode::Instance,
279                            attributes: $attrs,
280                        })],
281                        compilation_options: wgpu::PipelineCompilationOptions::default(),
282                    },
283                    fragment: Some(wgpu::FragmentState {
284                        module: &shader_module,
285                        entry_point: Some("fs_main"),
286                        targets: &[Some(wgpu::ColorTargetState {
287                            format,
288                            blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
289                            write_mask: wgpu::ColorWrites::ALL,
290                        })],
291                        compilation_options: wgpu::PipelineCompilationOptions::default(),
292                    }),
293                    primitive: wgpu::PrimitiveState::default(),
294                    depth_stencil: Some(stencil_for_content.clone()),
295                    multisample: msaa_state,
296                    multiview_mask: None,
297                    cache: None,
298                });
299            };
300        }
301
302        let rect_attrs: &[wgpu::VertexAttribute] = &[
303            wgpu::VertexAttribute {
304                shader_location: 0,
305                offset: 0,
306                format: wgpu::VertexFormat::Float32x4,
307            },
308            wgpu::VertexAttribute {
309                shader_location: 1,
310                offset: 16,
311                format: wgpu::VertexFormat::Float32x4,
312            },
313            wgpu::VertexAttribute {
314                shader_location: 2,
315                offset: 32,
316                format: wgpu::VertexFormat::Uint32,
317            },
318            wgpu::VertexAttribute {
319                shader_location: 3,
320                offset: 48,
321                format: wgpu::VertexFormat::Float32x4,
322            },
323            wgpu::VertexAttribute {
324                shader_location: 4,
325                offset: 64,
326                format: wgpu::VertexFormat::Float32x4,
327            },
328            wgpu::VertexAttribute {
329                shader_location: 5,
330                offset: 80,
331                format: wgpu::VertexFormat::Float32x2,
332            },
333            wgpu::VertexAttribute {
334                shader_location: 6,
335                offset: 88,
336                format: wgpu::VertexFormat::Float32x2,
337            },
338            wgpu::VertexAttribute {
339                shader_location: 7,
340                offset: 96,
341                format: wgpu::VertexFormat::Float32x2,
342            },
343        ];
344        let border_attrs: &[wgpu::VertexAttribute] = &[
345            wgpu::VertexAttribute {
346                shader_location: 0,
347                offset: 0,
348                format: wgpu::VertexFormat::Float32x4,
349            },
350            wgpu::VertexAttribute {
351                shader_location: 1,
352                offset: 16,
353                format: wgpu::VertexFormat::Float32x4,
354            },
355            wgpu::VertexAttribute {
356                shader_location: 2,
357                offset: 32,
358                format: wgpu::VertexFormat::Float32,
359            },
360            wgpu::VertexAttribute {
361                shader_location: 3,
362                offset: 36,
363                format: wgpu::VertexFormat::Float32x4,
364            },
365            wgpu::VertexAttribute {
366                shader_location: 4,
367                offset: 52,
368                format: wgpu::VertexFormat::Float32x2,
369            },
370        ];
371        let ellipse_attrs: &[wgpu::VertexAttribute] = &[
372            wgpu::VertexAttribute {
373                shader_location: 0,
374                offset: 0,
375                format: wgpu::VertexFormat::Float32x4,
376            },
377            wgpu::VertexAttribute {
378                shader_location: 1,
379                offset: 16,
380                format: wgpu::VertexFormat::Float32x4,
381            },
382            wgpu::VertexAttribute {
383                shader_location: 2,
384                offset: 32,
385                format: wgpu::VertexFormat::Float32x2,
386            },
387        ];
388        let ellipse_border_attrs: &[wgpu::VertexAttribute] = &[
389            wgpu::VertexAttribute {
390                shader_location: 0,
391                offset: 0,
392                format: wgpu::VertexFormat::Float32x4,
393            },
394            wgpu::VertexAttribute {
395                shader_location: 1,
396                offset: 16,
397                format: wgpu::VertexFormat::Float32,
398            },
399            wgpu::VertexAttribute {
400                shader_location: 2,
401                offset: 20,
402                format: wgpu::VertexFormat::Float32,
403            },
404            wgpu::VertexAttribute {
405                shader_location: 3,
406                offset: 24,
407                format: wgpu::VertexFormat::Float32x4,
408            },
409            wgpu::VertexAttribute {
410                shader_location: 4,
411                offset: 40,
412                format: wgpu::VertexFormat::Float32x2,
413            },
414        ];
415
416        make_content_pipeline!(rects, "rect", RectInstance, rect_attrs);
417        make_content_pipeline!(borders, "border", BorderInstance, border_attrs);
418        make_content_pipeline!(ellipses, "ellipse", EllipseInstance, ellipse_attrs);
419        make_content_pipeline!(
420            ellipse_borders,
421            "ellipse_border",
422            EllipseBorderInstance,
423            ellipse_border_attrs
424        );
425
426        let arc_attrs: &[wgpu::VertexAttribute] = &[
427            wgpu::VertexAttribute {
428                shader_location: 0,
429                offset: 0,
430                format: wgpu::VertexFormat::Float32x4,
431            },
432            wgpu::VertexAttribute {
433                shader_location: 1,
434                offset: 16,
435                format: wgpu::VertexFormat::Float32,
436            },
437            wgpu::VertexAttribute {
438                shader_location: 2,
439                offset: 20,
440                format: wgpu::VertexFormat::Float32,
441            },
442            wgpu::VertexAttribute {
443                shader_location: 3,
444                offset: 24,
445                format: wgpu::VertexFormat::Float32,
446            },
447            wgpu::VertexAttribute {
448                shader_location: 4,
449                offset: 28,
450                format: wgpu::VertexFormat::Float32,
451            },
452            wgpu::VertexAttribute {
453                shader_location: 5,
454                offset: 32,
455                format: wgpu::VertexFormat::Float32x4,
456            },
457            wgpu::VertexAttribute {
458                shader_location: 6,
459                offset: 48,
460                format: wgpu::VertexFormat::Float32x2,
461            },
462            wgpu::VertexAttribute {
463                shader_location: 7,
464                offset: 56,
465                format: wgpu::VertexFormat::Float32,
466            },
467        ];
468
469        make_content_pipeline!(arcs, "arc", ArcInstance, arc_attrs);
470
471        // Text (mask)
472        let text_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
473            label: Some("text.wgsl"),
474            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/text.wgsl"))),
475        });
476        // Text (color)
477        let text_color_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
478            label: Some("text_color.wgsl"),
479            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
480                "shaders/text_color.wgsl"
481            ))),
482        });
483        let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
484            label: Some("text pipeline layout"),
485            bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
486            immediate_size: 0,
487        });
488        let glyph_vertex = wgpu::VertexBufferLayout {
489            array_stride: std::mem::size_of::<GlyphInstance>() as u64,
490            step_mode: wgpu::VertexStepMode::Instance,
491            attributes: &[
492                wgpu::VertexAttribute {
493                    shader_location: 0,
494                    offset: 0,
495                    format: wgpu::VertexFormat::Float32x4,
496                },
497                wgpu::VertexAttribute {
498                    shader_location: 1,
499                    offset: 16,
500                    format: wgpu::VertexFormat::Float32x4,
501                },
502                wgpu::VertexAttribute {
503                    shader_location: 2,
504                    offset: 32,
505                    format: wgpu::VertexFormat::Float32x4,
506                },
507                wgpu::VertexAttribute {
508                    shader_location: 3,
509                    offset: 48,
510                    format: wgpu::VertexFormat::Float32x2,
511                },
512            ],
513        };
514        let text_mask = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
515            label: Some("text pipeline (mask)"),
516            layout: Some(&text_pipeline_layout),
517            vertex: wgpu::VertexState {
518                module: &text_mask_shader,
519                entry_point: Some("vs_main"),
520                buffers: &[Some(glyph_vertex.clone())],
521                compilation_options: wgpu::PipelineCompilationOptions::default(),
522            },
523            fragment: Some(wgpu::FragmentState {
524                module: &text_mask_shader,
525                entry_point: Some("fs_main"),
526                targets: &[Some(wgpu::ColorTargetState {
527                    format,
528                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
529                    write_mask: wgpu::ColorWrites::ALL,
530                })],
531                compilation_options: wgpu::PipelineCompilationOptions::default(),
532            }),
533            primitive: wgpu::PrimitiveState::default(),
534            depth_stencil: Some(stencil_for_content.clone()),
535            multisample: msaa_state,
536            multiview_mask: None,
537            cache: None,
538        });
539        let text_color = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
540            label: Some("text pipeline (color)"),
541            layout: Some(&text_pipeline_layout),
542            vertex: wgpu::VertexState {
543                module: &text_color_shader,
544                entry_point: Some("vs_main"),
545                buffers: &[Some(glyph_vertex)],
546                compilation_options: wgpu::PipelineCompilationOptions::default(),
547            },
548            fragment: Some(wgpu::FragmentState {
549                module: &text_color_shader,
550                entry_point: Some("fs_main"),
551                targets: &[Some(wgpu::ColorTargetState {
552                    format,
553                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
554                    write_mask: wgpu::ColorWrites::ALL,
555                })],
556                compilation_options: wgpu::PipelineCompilationOptions::default(),
557            }),
558            primitive: wgpu::PrimitiveState::default(),
559            depth_stencil: Some(stencil_for_content.clone()),
560            multisample: msaa_state,
561            multiview_mask: None,
562            cache: None,
563        });
564        // image_rgba reuses the text color pipeline (same vertex/bindings).
565        let image_rgba = text_color.clone();
566
567        // Blur composite pipeline (graphics-layer drop shadow)
568        let blur_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
569            label: Some("blur_shadow.wgsl"),
570            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
571                "shaders/blur_shadow.wgsl"
572            ))),
573        });
574        let blur_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
575            label: Some("blur pipeline layout"),
576            bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
577            immediate_size: 0,
578        });
579        let blur = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
580            label: Some("blur pipeline"),
581            layout: Some(&blur_pipeline_layout),
582            vertex: wgpu::VertexState {
583                module: &blur_shader,
584                entry_point: Some("vs_main"),
585                buffers: &[Some(wgpu::VertexBufferLayout {
586                    array_stride: std::mem::size_of::<BlurInstance>() as u64,
587                    step_mode: wgpu::VertexStepMode::Instance,
588                    attributes: &[
589                        wgpu::VertexAttribute {
590                            shader_location: 0,
591                            offset: 0,
592                            format: wgpu::VertexFormat::Float32x4,
593                        },
594                        wgpu::VertexAttribute {
595                            shader_location: 1,
596                            offset: 16,
597                            format: wgpu::VertexFormat::Float32x4,
598                        },
599                        wgpu::VertexAttribute {
600                            shader_location: 2,
601                            offset: 32,
602                            format: wgpu::VertexFormat::Float32x4,
603                        },
604                        wgpu::VertexAttribute {
605                            shader_location: 3,
606                            offset: 48,
607                            format: wgpu::VertexFormat::Float32x2,
608                        },
609                        wgpu::VertexAttribute {
610                            shader_location: 4,
611                            offset: 56,
612                            format: wgpu::VertexFormat::Float32x2,
613                        },
614                    ],
615                })],
616                compilation_options: wgpu::PipelineCompilationOptions::default(),
617            },
618            fragment: Some(wgpu::FragmentState {
619                module: &blur_shader,
620                entry_point: Some("fs_main"),
621                targets: &[Some(wgpu::ColorTargetState {
622                    format,
623                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
624                    write_mask: wgpu::ColorWrites::ALL,
625                })],
626                compilation_options: wgpu::PipelineCompilationOptions::default(),
627            }),
628            primitive: wgpu::PrimitiveState::default(),
629            depth_stencil: Some(stencil_for_content.clone()),
630            multisample: msaa_state,
631            multiview_mask: None,
632            cache: None,
633        });
634
635        // Content blur pipeline (full RGBA gaussian blur)
636        let blur_content_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
637            label: Some("blur_content.wgsl"),
638            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
639                "shaders/blur_content.wgsl"
640            ))),
641        });
642        let blur_content = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
643            label: Some("blur content pipeline"),
644            layout: Some(&blur_pipeline_layout),
645            vertex: wgpu::VertexState {
646                module: &blur_content_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_content_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        // NV12 Image Pipeline
699        let image_nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
700            label: Some("image_nv12.wgsl"),
701            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
702                "shaders/image_nv12.wgsl"
703            ))),
704        });
705        let image_nv12_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
706            label: Some("image nv12 pipeline layout"),
707            bind_group_layouts: &[Some(globals_layout), Some(image_bind_layout_nv12)],
708            immediate_size: 0,
709        });
710        let image_nv12 = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
711            label: Some("image nv12 pipeline"),
712            layout: Some(&image_nv12_layout),
713            vertex: wgpu::VertexState {
714                module: &image_nv12_shader,
715                entry_point: Some("vs_main"),
716                buffers: &[Some(wgpu::VertexBufferLayout {
717                    array_stride: std::mem::size_of::<Nv12Instance>() as u64,
718                    step_mode: wgpu::VertexStepMode::Instance,
719                    attributes: &[
720                        wgpu::VertexAttribute {
721                            shader_location: 0,
722                            offset: 0,
723                            format: wgpu::VertexFormat::Float32x4,
724                        },
725                        wgpu::VertexAttribute {
726                            shader_location: 1,
727                            offset: 16,
728                            format: wgpu::VertexFormat::Float32x4,
729                        },
730                        wgpu::VertexAttribute {
731                            shader_location: 2,
732                            offset: 32,
733                            format: wgpu::VertexFormat::Float32x4,
734                        },
735                        wgpu::VertexAttribute {
736                            shader_location: 3,
737                            offset: 48,
738                            format: wgpu::VertexFormat::Float32,
739                        },
740                        wgpu::VertexAttribute {
741                            shader_location: 4,
742                            offset: 52,
743                            format: wgpu::VertexFormat::Float32x2,
744                        },
745                    ],
746                })],
747                compilation_options: wgpu::PipelineCompilationOptions::default(),
748            },
749            fragment: Some(wgpu::FragmentState {
750                module: &image_nv12_shader,
751                entry_point: Some("fs_main"),
752                targets: &[Some(wgpu::ColorTargetState {
753                    format,
754                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
755                    write_mask: wgpu::ColorWrites::ALL,
756                })],
757                compilation_options: wgpu::PipelineCompilationOptions::default(),
758            }),
759            primitive: wgpu::PrimitiveState::default(),
760            depth_stencil: Some(stencil_for_content.clone()),
761            multisample: msaa_state,
762            multiview_mask: None,
763            cache: None,
764        });
765
766        // Clipping
767        let clip_shader_a2c = device.create_shader_module(wgpu::ShaderModuleDescriptor {
768            label: Some("clip_round_rect_a2c.wgsl"),
769            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
770                "shaders/clip_round_rect_a2c.wgsl"
771            ))),
772        });
773        let clip_shader_bin = device.create_shader_module(wgpu::ShaderModuleDescriptor {
774            label: Some("clip_round_rect_bin.wgsl"),
775            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
776                "shaders/clip_round_rect_bin.wgsl"
777            ))),
778        });
779        let clip_a2c = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
780            label: Some("clip pipeline (a2c)"),
781            layout: Some(clip_pipeline_layout),
782            vertex: wgpu::VertexState {
783                module: &clip_shader_a2c,
784                entry_point: Some("vs_main"),
785                buffers: &[Some(clip_vertex_layout.clone())],
786                compilation_options: wgpu::PipelineCompilationOptions::default(),
787            },
788            fragment: Some(wgpu::FragmentState {
789                module: &clip_shader_a2c,
790                entry_point: Some("fs_main"),
791                targets: &[Some(clip_color_target.clone())],
792                compilation_options: wgpu::PipelineCompilationOptions::default(),
793            }),
794            primitive: wgpu::PrimitiveState::default(),
795            depth_stencil: Some(stencil_for_clip_inc.clone()),
796            multisample: wgpu::MultisampleState {
797                count: sample_count,
798                mask: !0,
799                alpha_to_coverage_enabled: sample_count > 1,
800            },
801            multiview_mask: None,
802            cache: None,
803        });
804        let clip_bin = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
805            label: Some("clip pipeline (bin)"),
806            layout: Some(clip_pipeline_layout),
807            vertex: wgpu::VertexState {
808                module: &clip_shader_bin,
809                entry_point: Some("vs_main"),
810                buffers: &[Some(clip_vertex_layout.clone())],
811                compilation_options: wgpu::PipelineCompilationOptions::default(),
812            },
813            fragment: Some(wgpu::FragmentState {
814                module: &clip_shader_bin,
815                entry_point: Some("fs_main"),
816                targets: &[Some(clip_color_target.clone())],
817                compilation_options: wgpu::PipelineCompilationOptions::default(),
818            }),
819            primitive: wgpu::PrimitiveState::default(),
820            depth_stencil: Some(stencil_for_clip_inc.clone()),
821            multisample: wgpu::MultisampleState {
822                count: sample_count,
823                mask: !0,
824                alpha_to_coverage_enabled: false,
825            },
826            multiview_mask: None,
827            cache: None,
828        });
829        let clip_dec = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
830            label: Some("clip pipeline (dec)"),
831            layout: Some(clip_pipeline_layout),
832            vertex: wgpu::VertexState {
833                module: &clip_shader_bin,
834                entry_point: Some("vs_main"),
835                buffers: &[Some(clip_vertex_layout.clone())],
836                compilation_options: wgpu::PipelineCompilationOptions::default(),
837            },
838            fragment: Some(wgpu::FragmentState {
839                module: &clip_shader_bin,
840                entry_point: Some("fs_main"),
841                targets: &[Some(clip_color_target.clone())],
842                compilation_options: wgpu::PipelineCompilationOptions::default(),
843            }),
844            primitive: wgpu::PrimitiveState::default(),
845            depth_stencil: Some(stencil_for_clip_dec.clone()),
846            multisample: wgpu::MultisampleState {
847                count: sample_count,
848                mask: !0,
849                alpha_to_coverage_enabled: false,
850            },
851            multiview_mask: None,
852            cache: None,
853        });
854
855        let slug = Some(slug::create_pipeline(
856            device,
857            format,
858            sample_count,
859            stencil_for_content,
860        ));
861
862        Self {
863            rects,
864            borders,
865            ellipses,
866            ellipse_borders,
867            arcs,
868            text_mask,
869            text_color,
870            image_rgba,
871            image_nv12,
872            blur,
873            blur_content,
874            clip_a2c,
875            clip_bin,
876            clip_dec,
877            slug,
878        }
879    }
880}
881
882/// A segment of the frame that draws into a single render target.
883struct Pass {
884    target: PassTarget,
885    /// The initial scissor to apply to the rpass when it is opened.
886    initial_scissor: (u32, u32, u32, u32),
887    /// `None` means `LoadOp::Load` (resume existing content);
888    /// `Some(c)` means `LoadOp::Clear(c)`.
889    clear_color: Option<[f32; 4]>,
890    cmds: Vec<Cmd>,
891}
892
893#[allow(non_snake_case)]
894enum Cmd {
895    ClipPush {
896        off: u64,
897        cnt: u32,
898        scissor: (u32, u32, u32, u32),
899        difference: bool,
900        rounded: bool,
901    },
902    ClipPop {
903        scissor: (u32, u32, u32, u32),
904    },
905    Rect {
906        off: u64,
907        cnt: u32,
908    },
909    Border {
910        off: u64,
911        cnt: u32,
912    },
913    Ellipse {
914        off: u64,
915        cnt: u32,
916    },
917    EllipseBorder {
918        off: u64,
919        cnt: u32,
920    },
921    Arc {
922        off: u64,
923        cnt: u32,
924    },
925    GlyphsMask {
926        off: u64,
927        cnt: u32,
928    },
929    GlyphsColor {
930        off: u64,
931        cnt: u32,
932    },
933    GlyphsVector {
934        off: u64,
935        cnt: u32,
936    },
937    ImageRgba {
938        off: u64,
939        cnt: u32,
940        handle: u64,
941    },
942    ImageNv12 {
943        off: u64,
944        cnt: u32,
945        handle: u64,
946    },
947    PushTransform(Transform),
948    PopTransform,
949    /// Composite a previously-rendered graphics layer back into the
950    /// current target as a textured quad. The quad's vertex buffer
951    /// lives in `self.glyph_color.ring` (a `GlyphInstance`).
952    CompositeLayer {
953        off: u64,
954        cnt: u32,
955        layer_id: u32,
956        alpha: f32,
957    },
958    /// Composite a blurred drop shadow of a previously-rendered graphics
959    /// layer. The quad's vertex buffer lives in `self.blur_ring` (a
960    /// `BlurInstance`).
961    CompositeShadow {
962        off: u64,
963        cnt: u32,
964        layer_id: u32,
965    },
966    /// Apply gaussian blur to a layer and composite the blurred result.
967    /// Uses the `blur_content` pipeline (full RGBA blur).
968    CompositeBlur {
969        off: u64,
970        cnt: u32,
971        layer_id: u32,
972    },
973}
974
975enum ImageTex {
976    Rgba {
977        tex: wgpu::Texture,
978        view: wgpu::TextureView,
979        bind: wgpu::BindGroup,
980        w: u32,
981        h: u32,
982        format: wgpu::TextureFormat,
983        last_used_frame: u64,
984        bytes: u64,
985    },
986    Nv12 {
987        tex_y: wgpu::Texture,
988        view_y: wgpu::TextureView,
989        tex_uv: wgpu::Texture,
990        view_uv: wgpu::TextureView,
991        bind: wgpu::BindGroup,
992        yuv_buf: wgpu::Buffer,
993        w: u32,
994        h: u32,
995        color_info: ColorInfo,
996        last_used_frame: u64,
997        bytes: u64,
998    },
999}
1000
1001struct AtlasA8 {
1002    tex: wgpu::Texture,
1003    view: wgpu::TextureView,
1004    sampler: wgpu::Sampler,
1005    size: u32,
1006    next_x: u32,
1007    next_y: u32,
1008    row_h: u32,
1009    map: HashMap<(repose_text::GlyphKey, u32), GlyphInfo>,
1010}
1011
1012struct AtlasRGBA {
1013    tex: wgpu::Texture,
1014    view: wgpu::TextureView,
1015    sampler: wgpu::Sampler,
1016    size: u32,
1017    next_x: u32,
1018    next_y: u32,
1019    row_h: u32,
1020    map: HashMap<(repose_text::GlyphKey, u32), GlyphInfo>,
1021}
1022
1023#[derive(Clone, Copy)]
1024struct GlyphInfo {
1025    u0: f32,
1026    v0: f32,
1027    u1: f32,
1028    v1: f32,
1029    w: f32,
1030    h: f32,
1031    bearing_x: f32,
1032    bearing_y: f32,
1033    advance: f32,
1034}
1035
1036#[repr(C)]
1037#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1038struct RectInstance {
1039    xywh: [f32; 4],
1040    radii: [f32; 4],
1041    brush_type: u32,
1042    _pad: [f32; 3],
1043    color0: [f32; 4],
1044    color1: [f32; 4],
1045    grad_start: [f32; 2],
1046    grad_end: [f32; 2],
1047    sin_cos: [f32; 2],
1048}
1049
1050#[repr(C)]
1051#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1052struct BorderInstance {
1053    xywh: [f32; 4],
1054    radii: [f32; 4],
1055    stroke: f32,
1056    color: [f32; 4],
1057    sin_cos: [f32; 2],
1058}
1059
1060#[repr(C)]
1061#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1062struct EllipseInstance {
1063    xywh: [f32; 4],
1064    color: [f32; 4],
1065    sin_cos: [f32; 2],
1066}
1067
1068#[repr(C)]
1069#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1070struct EllipseBorderInstance {
1071    xywh: [f32; 4],
1072    stroke: f32,
1073    pad: f32,
1074    color: [f32; 4],
1075    sin_cos: [f32; 2],
1076}
1077
1078#[repr(C)]
1079#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1080struct ArcInstance {
1081    xywh: [f32; 4],
1082    start_angle: f32,
1083    sweep_angle: f32,
1084    stroke: f32,
1085    pad: f32,
1086    color: [f32; 4],
1087    sin_cos: [f32; 2],
1088    cap: f32, // 0=Butt, 1=Round, 2=Square
1089}
1090
1091#[repr(C)]
1092#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1093struct GlyphInstance {
1094    xywh: [f32; 4],
1095    uv: [f32; 4],
1096    color: [f32; 4],
1097    sin_cos: [f32; 2],
1098}
1099
1100#[repr(C)]
1101#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1102struct BlurInstance {
1103    xywh: [f32; 4],
1104    uv: [f32; 4],
1105    color: [f32; 4],
1106    blur_uv: [f32; 2],
1107    sin_cos: [f32; 2],
1108}
1109
1110/// CPU-computed Y′CbCr → R′G′B′ transform uploaded as a uniform buffer.
1111/// Layout matches the WGSL `YuvTransform` struct (4 × vec4<f32>).
1112#[repr(C)]
1113#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1114struct YuvTransformRaw {
1115    row0: [f32; 4],
1116    row1: [f32; 4],
1117    row2: [f32; 4],
1118    b: [f32; 4],
1119}
1120
1121#[repr(C)]
1122#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1123struct Nv12Instance {
1124    xywh: [f32; 4],
1125    uv: [f32; 4],
1126    color: [f32; 4], // tint
1127    uv_x_offset: f32,
1128    sin_cos: [f32; 2],
1129    _pad: [f32; 1],
1130}
1131
1132#[repr(C)]
1133#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1134struct ClipInstance {
1135    xywh: [f32; 4],
1136    radii: [f32; 4],
1137    sin_cos: [f32; 2],
1138}
1139
1140fn swash_to_a8_coverage(content: repose_text::SwashContent, data: &[u8]) -> Option<Vec<u8>> {
1141    match content {
1142        repose_text::SwashContent::Mask => Some(data.to_vec()),
1143        repose_text::SwashContent::SubpixelMask => {
1144            let mut out = Vec::with_capacity(data.len() / 4);
1145            for px in data.chunks_exact(4) {
1146                let r = px[0];
1147                let g = px[1];
1148                let b = px[2];
1149                out.push(r.max(g).max(b));
1150            }
1151            Some(out)
1152        }
1153        repose_text::SwashContent::Color => None,
1154    }
1155}
1156
1157impl WgpuBackend {
1158    pub async fn new_async(window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
1159        let instance: Instance;
1160
1161        if cfg!(target_arch = "wasm32") {
1162            let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
1163            desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
1164            instance = wgpu::util::new_instance_with_webgpu_detection(desc).await;
1165        } else {
1166            instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
1167        };
1168
1169        let surface = instance.create_surface(window.clone())?;
1170
1171        let adapter = instance
1172            .request_adapter(&wgpu::RequestAdapterOptions {
1173                power_preference: wgpu::PowerPreference::HighPerformance,
1174                compatible_surface: Some(&surface),
1175                force_fallback_adapter: false,
1176                apply_limit_buckets: false,
1177            })
1178            .await
1179            .map_err(|e| anyhow::anyhow!("No suitable adapter: {e:?}"))?;
1180
1181        let limits = adapter.limits();
1182
1183        let (device, queue) = adapter
1184            .request_device(&wgpu::DeviceDescriptor {
1185                label: Some("repose-rs device"),
1186                required_features: wgpu::Features::empty(),
1187                required_limits: limits,
1188                experimental_features: wgpu::ExperimentalFeatures::disabled(),
1189                memory_hints: wgpu::MemoryHints::default(),
1190                trace: wgpu::Trace::Off,
1191            })
1192            .await
1193            .map_err(|e| anyhow::anyhow!("request_device failed: {e:?}"))?;
1194
1195        let size = window.inner_size();
1196
1197        let caps = surface.get_capabilities(&adapter);
1198        let format = caps
1199            .formats
1200            .iter()
1201            .copied()
1202            .find(|f| f.is_srgb())
1203            .unwrap_or(caps.formats[0]);
1204        let present_mode = caps
1205            .present_modes
1206            .iter()
1207            .copied()
1208            .find(|m| *m == wgpu::PresentMode::Mailbox || *m == wgpu::PresentMode::Immediate)
1209            .unwrap_or(wgpu::PresentMode::Fifo);
1210        let alpha_mode = caps.alpha_modes[0];
1211
1212        let config = wgpu::SurfaceConfiguration {
1213            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1214            format,
1215            width: size.width.max(1),
1216            height: size.height.max(1),
1217            present_mode,
1218            alpha_mode,
1219            color_space: wgpu::SurfaceColorSpace::Auto,
1220            view_formats: vec![],
1221            desired_maximum_frame_latency: 2,
1222        };
1223        surface.configure(&device, &config);
1224
1225        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1226            label: Some("globals layout"),
1227            entries: &[wgpu::BindGroupLayoutEntry {
1228                binding: 0,
1229                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1230                ty: wgpu::BindingType::Buffer {
1231                    ty: wgpu::BufferBindingType::Uniform,
1232                    has_dynamic_offset: false,
1233                    min_binding_size: None,
1234                },
1235                count: None,
1236            }],
1237        });
1238
1239        let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
1240            label: Some("globals buf"),
1241            size: std::mem::size_of::<Globals>() as u64,
1242            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1243            mapped_at_creation: false,
1244        });
1245
1246        let globals_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1247            label: Some("globals bind"),
1248            layout: &globals_layout,
1249            entries: &[wgpu::BindGroupEntry {
1250                binding: 0,
1251                resource: globals_buf.as_entire_binding(),
1252            }],
1253        });
1254
1255        // Pick MSAA sample count
1256        let fmt_features = adapter.get_texture_format_features(format);
1257        let msaa_samples = if fmt_features.flags.sample_count_supported(4)
1258            && fmt_features
1259                .flags
1260                .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE)
1261        {
1262            4
1263        } else {
1264            1
1265        };
1266
1267        let ds_format = wgpu::TextureFormat::Depth24PlusStencil8;
1268
1269        let stencil_for_content = wgpu::DepthStencilState {
1270            format: ds_format,
1271            depth_write_enabled: Some(false),
1272            depth_compare: Some(wgpu::CompareFunction::Always),
1273            stencil: wgpu::StencilState {
1274                front: wgpu::StencilFaceState {
1275                    compare: wgpu::CompareFunction::LessEqual,
1276                    fail_op: wgpu::StencilOperation::Keep,
1277                    depth_fail_op: wgpu::StencilOperation::Keep,
1278                    pass_op: wgpu::StencilOperation::Keep,
1279                },
1280                back: wgpu::StencilFaceState {
1281                    compare: wgpu::CompareFunction::LessEqual,
1282                    fail_op: wgpu::StencilOperation::Keep,
1283                    depth_fail_op: wgpu::StencilOperation::Keep,
1284                    pass_op: wgpu::StencilOperation::Keep,
1285                },
1286                read_mask: 0xFF,
1287                write_mask: 0x00,
1288            },
1289            bias: wgpu::DepthBiasState::default(),
1290        };
1291
1292        let stencil_for_clip_inc = wgpu::DepthStencilState {
1293            format: ds_format,
1294            depth_write_enabled: Some(false),
1295            depth_compare: Some(wgpu::CompareFunction::Always),
1296            stencil: wgpu::StencilState {
1297                front: wgpu::StencilFaceState {
1298                    compare: wgpu::CompareFunction::Equal,
1299                    fail_op: wgpu::StencilOperation::Keep,
1300                    depth_fail_op: wgpu::StencilOperation::Keep,
1301                    pass_op: wgpu::StencilOperation::IncrementClamp,
1302                },
1303                back: wgpu::StencilFaceState {
1304                    compare: wgpu::CompareFunction::Equal,
1305                    fail_op: wgpu::StencilOperation::Keep,
1306                    depth_fail_op: wgpu::StencilOperation::Keep,
1307                    pass_op: wgpu::StencilOperation::IncrementClamp,
1308                },
1309                read_mask: 0xFF,
1310                write_mask: 0xFF,
1311            },
1312            bias: wgpu::DepthBiasState::default(),
1313        };
1314
1315        let stencil_for_clip_dec = wgpu::DepthStencilState {
1316            format: ds_format,
1317            depth_write_enabled: Some(false),
1318            depth_compare: Some(wgpu::CompareFunction::Always),
1319            stencil: wgpu::StencilState {
1320                front: wgpu::StencilFaceState {
1321                    compare: wgpu::CompareFunction::Equal,
1322                    fail_op: wgpu::StencilOperation::Keep,
1323                    depth_fail_op: wgpu::StencilOperation::Keep,
1324                    pass_op: wgpu::StencilOperation::DecrementClamp,
1325                },
1326                back: wgpu::StencilFaceState {
1327                    compare: wgpu::CompareFunction::Equal,
1328                    fail_op: wgpu::StencilOperation::Keep,
1329                    depth_fail_op: wgpu::StencilOperation::Keep,
1330                    pass_op: wgpu::StencilOperation::DecrementClamp,
1331                },
1332                read_mask: 0xFF,
1333                write_mask: 0xFF,
1334            },
1335            bias: wgpu::DepthBiasState::default(),
1336        };
1337
1338        let _multisample_state = wgpu::MultisampleState {
1339            count: msaa_samples,
1340            mask: !0,
1341            alpha_to_coverage_enabled: false,
1342        };
1343
1344        // PIPELINES
1345
1346        // Single shared sampler for images/text
1347        let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1348            label: Some("image/text sampler"),
1349            address_mode_u: wgpu::AddressMode::ClampToEdge,
1350            address_mode_v: wgpu::AddressMode::ClampToEdge,
1351            mag_filter: wgpu::FilterMode::Linear,
1352            min_filter: wgpu::FilterMode::Linear,
1353            mipmap_filter: wgpu::MipmapFilterMode::Linear,
1354            ..Default::default()
1355        });
1356
1357        // Layout for Text / RGBA Images (Texture + Sampler)
1358        let text_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1359            label: Some("text/rgba bind layout"),
1360            entries: &[
1361                wgpu::BindGroupLayoutEntry {
1362                    binding: 0,
1363                    visibility: wgpu::ShaderStages::FRAGMENT,
1364                    ty: wgpu::BindingType::Texture {
1365                        multisampled: false,
1366                        view_dimension: wgpu::TextureViewDimension::D2,
1367                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
1368                    },
1369                    count: None,
1370                },
1371                wgpu::BindGroupLayoutEntry {
1372                    binding: 1,
1373                    visibility: wgpu::ShaderStages::FRAGMENT,
1374                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1375                    count: None,
1376                },
1377            ],
1378        });
1379        // We reuse this for RGBA images for simplicity, or create a distinct one
1380        let image_bind_layout_rgba = text_bind_layout.clone();
1381
1382        // Layout for NV12 Images (TextureY + TextureUV + Sampler + YuvTransform uniform)
1383        let image_bind_layout_nv12 =
1384            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1385                label: Some("image bind layout nv12"),
1386                entries: &[
1387                    // Y plane
1388                    wgpu::BindGroupLayoutEntry {
1389                        binding: 0,
1390                        visibility: wgpu::ShaderStages::FRAGMENT,
1391                        ty: wgpu::BindingType::Texture {
1392                            multisampled: false,
1393                            view_dimension: wgpu::TextureViewDimension::D2,
1394                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
1395                        },
1396                        count: None,
1397                    },
1398                    // UV plane
1399                    wgpu::BindGroupLayoutEntry {
1400                        binding: 1,
1401                        visibility: wgpu::ShaderStages::FRAGMENT,
1402                        ty: wgpu::BindingType::Texture {
1403                            multisampled: false,
1404                            view_dimension: wgpu::TextureViewDimension::D2,
1405                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
1406                        },
1407                        count: None,
1408                    },
1409                    // Sampler
1410                    wgpu::BindGroupLayoutEntry {
1411                        binding: 2,
1412                        visibility: wgpu::ShaderStages::FRAGMENT,
1413                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1414                        count: None,
1415                    },
1416                    // YUV transform uniform buffer
1417                    wgpu::BindGroupLayoutEntry {
1418                        binding: 3,
1419                        visibility: wgpu::ShaderStages::FRAGMENT,
1420                        ty: wgpu::BindingType::Buffer {
1421                            ty: wgpu::BufferBindingType::Uniform,
1422                            has_dynamic_offset: false,
1423                            min_binding_size: None,
1424                        },
1425                        count: None,
1426                    },
1427                ],
1428            });
1429
1430        // Clipping layout
1431        let clip_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1432            label: Some("clip pipeline layout"),
1433            bind_group_layouts: &[Some(&globals_layout)],
1434            immediate_size: 0,
1435        });
1436        let clip_vertex_layout = wgpu::VertexBufferLayout {
1437            array_stride: std::mem::size_of::<ClipInstance>() as u64,
1438            step_mode: wgpu::VertexStepMode::Instance,
1439            attributes: &[
1440                wgpu::VertexAttribute {
1441                    shader_location: 0,
1442                    offset: 0,
1443                    format: wgpu::VertexFormat::Float32x4,
1444                },
1445                wgpu::VertexAttribute {
1446                    shader_location: 1,
1447                    offset: 16,
1448                    format: wgpu::VertexFormat::Float32x4,
1449                },
1450                wgpu::VertexAttribute {
1451                    shader_location: 2,
1452                    offset: 32,
1453                    format: wgpu::VertexFormat::Float32x2,
1454                },
1455            ],
1456        };
1457        let clip_color_target = wgpu::ColorTargetState {
1458            format: config.format,
1459            blend: None,
1460            write_mask: wgpu::ColorWrites::empty(),
1461        };
1462
1463        // Two sets of pipelines: one for the MSAA surface pass, one for layer
1464        // render-to-texture passes (sample_count = 1).
1465        let surface_pipes = Pipelines::create(
1466            &device,
1467            config.format,
1468            msaa_samples,
1469            &globals_layout,
1470            &text_bind_layout,
1471            &image_bind_layout_nv12,
1472            &clip_pipeline_layout,
1473            &stencil_for_content,
1474            &stencil_for_clip_inc,
1475            &stencil_for_clip_dec,
1476            &clip_color_target,
1477            &clip_vertex_layout,
1478        );
1479        let layer_pipes = Pipelines::create(
1480            &device,
1481            config.format,
1482            1,
1483            &globals_layout,
1484            &text_bind_layout,
1485            &image_bind_layout_nv12,
1486            &clip_pipeline_layout,
1487            &stencil_for_content,
1488            &stencil_for_clip_inc,
1489            &stencil_for_clip_dec,
1490            &clip_color_target,
1491            &clip_vertex_layout,
1492        );
1493
1494        // Vector glyph rendering always available with tessellation+MSAA approach.
1495        let slug_enabled = true;
1496
1497        // Blur composite ring (for graphics-layer drop shadows)
1498        let blur_ring = UploadRing::new(&device, "blur ring", 1024 * 1024);
1499
1500        // Atlases
1501        let atlas_mask = Self::init_atlas_mask(&device)?;
1502        let atlas_color = Self::init_atlas_color(&device)?;
1503
1504        // Upload rings
1505        let ring_rect = UploadRing::new(&device, "ring rect", 1 << 20);
1506        let ring_border = UploadRing::new(&device, "ring border", 1 << 20);
1507        let ring_ellipse = UploadRing::new(&device, "ring ellipse", 1 << 20);
1508        let ring_ellipse_border = UploadRing::new(&device, "ring ellipse border", 1 << 20);
1509        let ring_arc = UploadRing::new(&device, "ring arc", 1 << 20);
1510        let ring_glyph_mask = UploadRing::new(&device, "ring glyph mask", 1 << 20);
1511        let ring_glyph_color = UploadRing::new(&device, "ring glyph color", 1 << 20);
1512        let ring_slug = UploadRing::new(&device, "ring slug", 1 << 22);
1513        let ring_clip = UploadRing::new(&device, "ring clip", 1 << 16);
1514        let ring_nv12 = UploadRing::new(&device, "ring nv12", 1 << 20);
1515
1516        // Placeholder textures
1517        let depth_stencil_tex = device.create_texture(&wgpu::TextureDescriptor {
1518            label: Some("temp ds"),
1519            size: wgpu::Extent3d {
1520                width: 1,
1521                height: 1,
1522                depth_or_array_layers: 1,
1523            },
1524            mip_level_count: 1,
1525            sample_count: 1,
1526            dimension: wgpu::TextureDimension::D2,
1527            format: wgpu::TextureFormat::Depth24PlusStencil8,
1528            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1529            view_formats: &[],
1530        });
1531        let depth_stencil_view =
1532            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
1533
1534        let mut backend = Self {
1535            surface,
1536            device,
1537            queue,
1538            config,
1539
1540            surface_pipes,
1541            layer_pipes,
1542
1543            rects: InstancedPipe::new(ring_rect),
1544            borders: InstancedPipe::new(ring_border),
1545            ellipses: InstancedPipe::new(ring_ellipse),
1546            ellipse_borders: InstancedPipe::new(ring_ellipse_border),
1547            arcs: InstancedPipe::new(ring_arc),
1548            glyph_mask: InstancedPipe::new(ring_glyph_mask),
1549            glyph_color: InstancedPipe::new(ring_glyph_color),
1550
1551            text_bind_layout,
1552
1553            image_bind_layout_rgba,
1554            image_bind_layout_nv12,
1555            image_sampler,
1556
1557            blur_ring,
1558
1559            slug_enabled,
1560            slug_ring: ring_slug,
1561            slug_cache: slug::GlyphSlugCache::new(),
1562
1563            clip_ring: ring_clip,
1564
1565            nv12: InstancedPipe::new(ring_nv12),
1566
1567            msaa_samples,
1568            depth_stencil_tex,
1569            depth_stencil_view,
1570            msaa_tex: None,
1571            msaa_view: None,
1572            globals_bind,
1573            globals_buf,
1574            globals_layout,
1575
1576            atlas_mask,
1577            atlas_color,
1578
1579            next_image_handle: 1,
1580            images: HashMap::new(),
1581
1582            frame_index: 0,
1583            image_bytes_total: 0,
1584            image_evict_after_frames: 600,         // ~10s @ 60fps
1585            image_budget_bytes: 512 * 1024 * 1024, // 512 MB
1586            layer_pool: HashMap::new(),
1587
1588            working_space: false,
1589            ws_tex: None,
1590            ws_view: None,
1591            ws_bind: None,
1592            display_pipeline: None,
1593            display_layout: None,
1594        };
1595
1596        backend.recreate_msaa_and_depth_stencil();
1597        Ok(backend)
1598    }
1599
1600    #[cfg(not(target_arch = "wasm32"))]
1601    pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
1602        pollster::block_on(Self::new_async(window))
1603    }
1604
1605    #[cfg(target_arch = "wasm32")]
1606    pub fn new(_window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
1607        anyhow::bail!("Use WgpuBackend::new_async(window).await on wasm32")
1608    }
1609
1610    // Image API
1611
1612    pub fn set_image_from_bytes(
1613        &mut self,
1614        handle: u64,
1615        data: &[u8],
1616        srgb: bool,
1617    ) -> anyhow::Result<()> {
1618        let img = image::load_from_memory(data)?;
1619        let rgba = img.to_rgba8();
1620        let (w, h) = rgba.dimensions();
1621        self.set_image_rgba8(handle, w, h, &rgba, srgb)
1622    }
1623
1624    pub fn set_image_rgba8(
1625        &mut self,
1626        handle: u64,
1627        w: u32,
1628        h: u32,
1629        rgba: &[u8],
1630        srgb: bool,
1631    ) -> anyhow::Result<()> {
1632        let expected = (w as usize) * (h as usize) * 4;
1633        if rgba.len() < expected {
1634            return Err(anyhow::anyhow!(
1635                "RGBA buffer too small: {} < {}",
1636                rgba.len(),
1637                expected
1638            ));
1639        }
1640
1641        let format = if srgb {
1642            wgpu::TextureFormat::Rgba8UnormSrgb
1643        } else {
1644            wgpu::TextureFormat::Rgba8Unorm
1645        };
1646
1647        let needs_recreate = match self.images.get(&handle) {
1648            Some(ImageTex::Rgba {
1649                w: cw,
1650                h: ch,
1651                format: cf,
1652                ..
1653            }) => *cw != w || *ch != h || *cf != format,
1654            _ => true,
1655        };
1656
1657        if needs_recreate {
1658            // Remove old to track budget correctly
1659            self.remove_image(handle);
1660
1661            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1662                label: Some("user image rgba"),
1663                size: wgpu::Extent3d {
1664                    width: w,
1665                    height: h,
1666                    depth_or_array_layers: 1,
1667                },
1668                mip_level_count: 1,
1669                sample_count: 1,
1670                dimension: wgpu::TextureDimension::D2,
1671                format,
1672                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1673                view_formats: &[],
1674            });
1675            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1676
1677            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1678                label: Some("image bind rgba"),
1679                layout: &self.image_bind_layout_rgba,
1680                entries: &[
1681                    wgpu::BindGroupEntry {
1682                        binding: 0,
1683                        resource: wgpu::BindingResource::TextureView(&view),
1684                    },
1685                    wgpu::BindGroupEntry {
1686                        binding: 1,
1687                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
1688                    },
1689                ],
1690            });
1691
1692            let bytes = (w as u64) * (h as u64) * 4;
1693            self.image_bytes_total += bytes;
1694
1695            self.images.insert(
1696                handle,
1697                ImageTex::Rgba {
1698                    tex,
1699                    view,
1700                    bind,
1701                    w,
1702                    h,
1703                    format,
1704                    last_used_frame: self.frame_index,
1705                    bytes,
1706                },
1707            );
1708        }
1709
1710        let tex = match self.images.get(&handle) {
1711            Some(ImageTex::Rgba { tex, .. }) => tex,
1712            _ => unreachable!(),
1713        };
1714
1715        self.queue.write_texture(
1716            wgpu::TexelCopyTextureInfo {
1717                texture: tex,
1718                mip_level: 0,
1719                origin: wgpu::Origin3d::ZERO,
1720                aspect: wgpu::TextureAspect::All,
1721            },
1722            &rgba[..expected],
1723            wgpu::TexelCopyBufferLayout {
1724                offset: 0,
1725                bytes_per_row: Some(4 * w),
1726                rows_per_image: Some(h),
1727            },
1728            wgpu::Extent3d {
1729                width: w,
1730                height: h,
1731                depth_or_array_layers: 1,
1732            },
1733        );
1734
1735        // Ensure budget limits
1736        self.evict_budget_excess();
1737
1738        Ok(())
1739    }
1740
1741    pub fn set_image_nv12(
1742        &mut self,
1743        handle: u64,
1744        w: u32,
1745        h: u32,
1746        y: &[u8],
1747        uv: &[u8],
1748        color_info: ColorInfo,
1749    ) -> anyhow::Result<()> {
1750        let y_expected = (w as usize) * (h as usize);
1751        let uv_w = (w / 2).max(1);
1752        let uv_h = (h / 2).max(1);
1753        let uv_expected = (uv_w as usize) * (uv_h as usize) * 2;
1754
1755        if y.len() < y_expected {
1756            return Err(anyhow::anyhow!("Y plane too small"));
1757        }
1758        if uv.len() < uv_expected {
1759            return Err(anyhow::anyhow!("UV plane too small"));
1760        }
1761
1762        let needs_recreate = match self.images.get(&handle) {
1763            Some(ImageTex::Nv12 { w: ww, h: hh, .. }) => *ww != w || *hh != h,
1764            _ => true,
1765        };
1766
1767        // Compute the YUV→RGB transform on the CPU.
1768        let yuv = color_info.to_yuv_transform();
1769        let yuv_raw = YuvTransformRaw {
1770            row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
1771            row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
1772            row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
1773            b: [yuv.b[0], yuv.b[1], yuv.b[2], 0.0],
1774        };
1775
1776        if needs_recreate {
1777            self.remove_image(handle);
1778
1779            let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
1780                label: Some("nv12 Y"),
1781                size: wgpu::Extent3d {
1782                    width: w,
1783                    height: h,
1784                    depth_or_array_layers: 1,
1785                },
1786                mip_level_count: 1,
1787                sample_count: 1,
1788                dimension: wgpu::TextureDimension::D2,
1789                format: wgpu::TextureFormat::R8Unorm,
1790                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1791                view_formats: &[],
1792            });
1793            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
1794
1795            let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
1796                label: Some("nv12 UV"),
1797                size: wgpu::Extent3d {
1798                    width: uv_w,
1799                    height: uv_h,
1800                    depth_or_array_layers: 1,
1801                },
1802                mip_level_count: 1,
1803                sample_count: 1,
1804                dimension: wgpu::TextureDimension::D2,
1805                format: wgpu::TextureFormat::Rg8Unorm,
1806                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1807                view_formats: &[],
1808            });
1809            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
1810
1811            // Create a uniform buffer for the YUV transform (per-image).
1812            let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1813                label: Some("nv12 yuv transform"),
1814                size: std::mem::size_of::<YuvTransformRaw>() as u64,
1815                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1816                mapped_at_creation: false,
1817            });
1818
1819            // Write initial transform.
1820            self.queue
1821                .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
1822
1823            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1824                label: Some("nv12 bind"),
1825                layout: &self.image_bind_layout_nv12,
1826                entries: &[
1827                    wgpu::BindGroupEntry {
1828                        binding: 0,
1829                        resource: wgpu::BindingResource::TextureView(&view_y),
1830                    },
1831                    wgpu::BindGroupEntry {
1832                        binding: 1,
1833                        resource: wgpu::BindingResource::TextureView(&view_uv),
1834                    },
1835                    wgpu::BindGroupEntry {
1836                        binding: 2,
1837                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
1838                    },
1839                    wgpu::BindGroupEntry {
1840                        binding: 3,
1841                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1842                            buffer: &yuv_buf,
1843                            offset: 0,
1844                            size: None,
1845                        }),
1846                    },
1847                ],
1848            });
1849
1850            let bytes = (w as u64) * (h as u64)
1851                + (uv_w as u64) * (uv_h as u64) * 2
1852                + std::mem::size_of::<YuvTransformRaw>() as u64;
1853            self.image_bytes_total += bytes;
1854
1855            self.images.insert(
1856                handle,
1857                ImageTex::Nv12 {
1858                    tex_y,
1859                    view_y,
1860                    tex_uv,
1861                    view_uv,
1862                    bind,
1863                    yuv_buf,
1864                    w,
1865                    h,
1866                    color_info,
1867                    last_used_frame: self.frame_index,
1868                    bytes,
1869                },
1870            );
1871        } else {
1872            // Re-use existing textures; just update the YUV transform if needed.
1873            if let Some(ImageTex::Nv12 { yuv_buf, .. }) = self.images.get(&handle) {
1874                self.queue
1875                    .write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
1876            }
1877        }
1878
1879        let (tex_y, tex_uv, _bind) = match self.images.get(&handle) {
1880            Some(ImageTex::Nv12 {
1881                tex_y,
1882                tex_uv,
1883                bind,
1884                ..
1885            }) => (tex_y, tex_uv, bind),
1886            _ => return Err(anyhow::anyhow!("Handle is not NV12")),
1887        };
1888
1889        self.queue.write_texture(
1890            wgpu::TexelCopyTextureInfo {
1891                texture: tex_y,
1892                mip_level: 0,
1893                origin: wgpu::Origin3d::ZERO,
1894                aspect: wgpu::TextureAspect::All,
1895            },
1896            &y[..y_expected],
1897            wgpu::TexelCopyBufferLayout {
1898                offset: 0,
1899                bytes_per_row: Some(w),
1900                rows_per_image: Some(h),
1901            },
1902            wgpu::Extent3d {
1903                width: w,
1904                height: h,
1905                depth_or_array_layers: 1,
1906            },
1907        );
1908
1909        self.queue.write_texture(
1910            wgpu::TexelCopyTextureInfo {
1911                texture: tex_uv,
1912                mip_level: 0,
1913                origin: wgpu::Origin3d::ZERO,
1914                aspect: wgpu::TextureAspect::All,
1915            },
1916            &uv[..uv_expected],
1917            wgpu::TexelCopyBufferLayout {
1918                offset: 0,
1919                bytes_per_row: Some(2 * uv_w),
1920                rows_per_image: Some(uv_h),
1921            },
1922            wgpu::Extent3d {
1923                width: uv_w,
1924                height: uv_h,
1925                depth_or_array_layers: 1,
1926            },
1927        );
1928
1929        self.evict_budget_excess();
1930        Ok(())
1931    }
1932
1933    pub fn set_image_planes(
1934        &mut self,
1935        handle: u64,
1936        w: u32,
1937        h: u32,
1938        pixel_format: PixelFormat,
1939        planes: &[Vec<u8>],
1940        color_info: ColorInfo,
1941    ) -> anyhow::Result<()> {
1942        match pixel_format {
1943            PixelFormat::Nv12 => {
1944                let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
1945                let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
1946                self.set_image_nv12(handle, w, h, y, uv, color_info)
1947            }
1948            PixelFormat::P010 => {
1949                let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
1950                let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
1951                self.set_image_p010(handle, w, h, y, uv, color_info)
1952            }
1953            PixelFormat::I420 | PixelFormat::I444 => Err(anyhow::anyhow!(
1954                "I420/I444 not implemented and unlikely -> cheap to convert to NV12 (better for the GPU too)"
1955            )),
1956            PixelFormat::Rgba => {
1957                let rgba = planes
1958                    .first()
1959                    .ok_or(anyhow::anyhow!("missing RGBA plane"))?;
1960                self.set_image_rgba8(handle, w, h, rgba, false)
1961            }
1962        }
1963    }
1964
1965    fn set_image_p010(
1966        &mut self,
1967        handle: u64,
1968        w: u32,
1969        h: u32,
1970        y: &[u8],
1971        uv: &[u8],
1972        color_info: ColorInfo,
1973    ) -> anyhow::Result<()> {
1974        let uv_w = (w / 2).max(1);
1975        let uv_h = (h / 2).max(1);
1976
1977        let y_expected = (w as usize) * 2;
1978        let uv_expected = (uv_w as usize) * (uv_h as usize) * 4;
1979
1980        if y.len() < y_expected {
1981            return Err(anyhow::anyhow!("P010 Y plane too small"));
1982        }
1983        if uv.len() < uv_expected {
1984            return Err(anyhow::anyhow!("P010 UV plane too small"));
1985        }
1986
1987        // P010 reuses the NV12 pipeline (same bind group layout -> wgpu
1988        // abstracts the storage format so R16Unorm/Rg16Unorm are
1989        // filterable float textures just like R8Unorm/Rg8Unorm).
1990        let needs_recreate = match self.images.get(&handle) {
1991            Some(ImageTex::Nv12 { w: ww, h: hh, .. }) => *ww != w || *hh != h,
1992            _ => true,
1993        };
1994
1995        let yuv = color_info.to_yuv_transform();
1996        let yuv_raw = YuvTransformRaw {
1997            row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
1998            row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
1999            row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
2000            b: [yuv.b[0], yuv.b[1], yuv.b[2], 0.0],
2001        };
2002
2003        if needs_recreate {
2004            self.remove_image(handle);
2005
2006            let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
2007                label: Some("p010 Y"),
2008                size: wgpu::Extent3d {
2009                    width: w,
2010                    height: h,
2011                    depth_or_array_layers: 1,
2012                },
2013                mip_level_count: 1,
2014                sample_count: 1,
2015                dimension: wgpu::TextureDimension::D2,
2016                format: wgpu::TextureFormat::R16Unorm,
2017                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2018                view_formats: &[],
2019            });
2020            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
2021
2022            let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
2023                label: Some("p010 UV"),
2024                size: wgpu::Extent3d {
2025                    width: uv_w,
2026                    height: uv_h,
2027                    depth_or_array_layers: 1,
2028                },
2029                mip_level_count: 1,
2030                sample_count: 1,
2031                dimension: wgpu::TextureDimension::D2,
2032                format: wgpu::TextureFormat::Rg16Unorm,
2033                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2034                view_formats: &[],
2035            });
2036            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
2037
2038            let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
2039                label: Some("p010 yuv transform"),
2040                size: std::mem::size_of::<YuvTransformRaw>() as u64,
2041                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2042                mapped_at_creation: false,
2043            });
2044            self.queue
2045                .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
2046
2047            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2048                label: Some("p010 bind"),
2049                layout: &self.image_bind_layout_nv12,
2050                entries: &[
2051                    wgpu::BindGroupEntry {
2052                        binding: 0,
2053                        resource: wgpu::BindingResource::TextureView(&view_y),
2054                    },
2055                    wgpu::BindGroupEntry {
2056                        binding: 1,
2057                        resource: wgpu::BindingResource::TextureView(&view_uv),
2058                    },
2059                    wgpu::BindGroupEntry {
2060                        binding: 2,
2061                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
2062                    },
2063                    wgpu::BindGroupEntry {
2064                        binding: 3,
2065                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2066                            buffer: &yuv_buf,
2067                            offset: 0,
2068                            size: None,
2069                        }),
2070                    },
2071                ],
2072            });
2073
2074            let bytes = (w as u64) * 2
2075                + (uv_w as u64) * (uv_h as u64) * 4
2076                + std::mem::size_of::<YuvTransformRaw>() as u64;
2077            self.image_bytes_total += bytes;
2078
2079            self.images.insert(
2080                handle,
2081                ImageTex::Nv12 {
2082                    tex_y,
2083                    view_y,
2084                    tex_uv,
2085                    view_uv,
2086                    bind,
2087                    yuv_buf,
2088                    w,
2089                    h,
2090                    color_info,
2091                    last_used_frame: self.frame_index,
2092                    bytes,
2093                },
2094            );
2095        } else {
2096            if let Some(ImageTex::Nv12 { yuv_buf, .. }) = self.images.get(&handle) {
2097                self.queue
2098                    .write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
2099            }
2100        }
2101
2102        let (tex_y, tex_uv, _bind) = match self.images.get(&handle) {
2103            Some(ImageTex::Nv12 {
2104                tex_y,
2105                tex_uv,
2106                bind,
2107                ..
2108            }) => (tex_y, tex_uv, bind),
2109            _ => return Err(anyhow::anyhow!("Handle is not P010/NV12")),
2110        };
2111
2112        self.queue.write_texture(
2113            wgpu::TexelCopyTextureInfo {
2114                texture: tex_y,
2115                mip_level: 0,
2116                origin: wgpu::Origin3d::ZERO,
2117                aspect: wgpu::TextureAspect::All,
2118            },
2119            &y[..y_expected],
2120            wgpu::TexelCopyBufferLayout {
2121                offset: 0,
2122                bytes_per_row: Some(w * 2),
2123                rows_per_image: Some(h),
2124            },
2125            wgpu::Extent3d {
2126                width: w,
2127                height: h,
2128                depth_or_array_layers: 1,
2129            },
2130        );
2131        self.queue.write_texture(
2132            wgpu::TexelCopyTextureInfo {
2133                texture: tex_uv,
2134                mip_level: 0,
2135                origin: wgpu::Origin3d::ZERO,
2136                aspect: wgpu::TextureAspect::All,
2137            },
2138            &uv[..uv_expected],
2139            wgpu::TexelCopyBufferLayout {
2140                offset: 0,
2141                bytes_per_row: Some(uv_w * 4),
2142                rows_per_image: Some(uv_h),
2143            },
2144            wgpu::Extent3d {
2145                width: uv_w,
2146                height: uv_h,
2147                depth_or_array_layers: 1,
2148            },
2149        );
2150
2151        self.evict_budget_excess();
2152        Ok(())
2153    }
2154
2155    pub fn remove_image(&mut self, handle: u64) {
2156        if let Some(img) = self.images.remove(&handle) {
2157            let b = match &img {
2158                ImageTex::Rgba { bytes, .. } => *bytes,
2159                ImageTex::Nv12 { bytes, .. } => *bytes,
2160            };
2161            self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
2162        }
2163    }
2164
2165    // Legacy support from Step 1 instructions (temporary until platform render logic is fully swapped)
2166    pub fn register_image_from_bytes(&mut self, data: &[u8], srgb: bool) -> u64 {
2167        let handle = self.next_image_handle;
2168        self.next_image_handle += 1;
2169        if let Err(e) = self.set_image_from_bytes(handle, data, srgb) {
2170            log::error!("Failed to register image: {e}");
2171        }
2172        handle
2173    }
2174
2175    fn evict_unused_images(&mut self) {
2176        let now = self.frame_index;
2177        let evict_after = self.image_evict_after_frames;
2178
2179        // Time based eviction
2180        let mut to_remove = Vec::new();
2181        for (h, t) in self.images.iter() {
2182            let last = match t {
2183                ImageTex::Rgba {
2184                    last_used_frame, ..
2185                } => *last_used_frame,
2186                ImageTex::Nv12 {
2187                    last_used_frame, ..
2188                } => *last_used_frame,
2189            };
2190            if now.saturating_sub(last) > evict_after {
2191                to_remove.push(*h);
2192            }
2193        }
2194        for h in to_remove {
2195            self.remove_image(h);
2196        }
2197
2198        self.evict_budget_excess();
2199    }
2200
2201    fn evict_budget_excess(&mut self) {
2202        if self.image_bytes_total <= self.image_budget_bytes {
2203            return;
2204        }
2205        // Collect (handle, last_used, bytes)
2206        let mut candidates: Vec<(u64, u64, u64)> = self
2207            .images
2208            .iter()
2209            .map(|(h, t)| {
2210                let (last, bytes) = match t {
2211                    ImageTex::Rgba {
2212                        last_used_frame,
2213                        bytes,
2214                        ..
2215                    } => (*last_used_frame, *bytes),
2216                    ImageTex::Nv12 {
2217                        last_used_frame,
2218                        bytes,
2219                        ..
2220                    } => (*last_used_frame, *bytes),
2221                };
2222                (*h, last, bytes)
2223            })
2224            .collect();
2225
2226        // Sort by last_used ascending (LRU first)
2227        candidates.sort_by_key(|k| k.1);
2228
2229        let now = self.frame_index;
2230        for (h, last, _bytes) in candidates {
2231            if self.image_bytes_total <= self.image_budget_bytes {
2232                break;
2233            }
2234            // Don't evict something used this frame
2235            if last == now {
2236                continue;
2237            }
2238            self.remove_image(h);
2239        }
2240    }
2241
2242    /// Enable or disable linear working-space rendering.
2243    /// When enabled, the scene is rendered into an Rgba16Float intermediate
2244    /// and a final full-screen pass applies the display OETF.
2245    pub fn set_working_space(&mut self, enabled: bool) {
2246        if enabled == self.working_space {
2247            return;
2248        }
2249        self.working_space = enabled;
2250        if enabled {
2251            self.ensure_display_pipeline();
2252            self.recreate_working_space_texture();
2253        } else {
2254            self.ws_tex = None;
2255            self.ws_view = None;
2256            self.ws_bind = None;
2257        }
2258    }
2259
2260    fn ensure_display_pipeline(&mut self) {
2261        if self.display_pipeline.is_some() {
2262            return;
2263        }
2264
2265        let layout = self
2266            .device
2267            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2268                label: Some("display transform layout"),
2269                entries: &[
2270                    wgpu::BindGroupLayoutEntry {
2271                        binding: 0,
2272                        visibility: wgpu::ShaderStages::FRAGMENT,
2273                        ty: wgpu::BindingType::Texture {
2274                            multisampled: false,
2275                            view_dimension: wgpu::TextureViewDimension::D2,
2276                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
2277                        },
2278                        count: None,
2279                    },
2280                    wgpu::BindGroupLayoutEntry {
2281                        binding: 1,
2282                        visibility: wgpu::ShaderStages::FRAGMENT,
2283                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2284                        count: None,
2285                    },
2286                ],
2287            });
2288        self.display_layout = Some(layout);
2289
2290        let shader = self
2291            .device
2292            .create_shader_module(wgpu::ShaderModuleDescriptor {
2293                label: Some("display_transform.wgsl"),
2294                source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
2295                    "shaders/display_transform.wgsl"
2296                ))),
2297            });
2298
2299        let pipeline_layout = self
2300            .device
2301            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2302                label: Some("display transform pipeline layout"),
2303                bind_group_layouts: &[None, self.display_layout.as_ref()],
2304                immediate_size: 0,
2305            });
2306
2307        let pipeline = self
2308            .device
2309            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2310                label: Some("display transform pipeline"),
2311                layout: Some(&pipeline_layout),
2312                vertex: wgpu::VertexState {
2313                    module: &shader,
2314                    entry_point: Some("vs_main"),
2315                    buffers: &[],
2316                    compilation_options: wgpu::PipelineCompilationOptions::default(),
2317                },
2318                fragment: Some(wgpu::FragmentState {
2319                    module: &shader,
2320                    entry_point: Some("fs_main"),
2321                    targets: &[Some(wgpu::ColorTargetState {
2322                        format: self.config.format,
2323                        blend: None,
2324                        write_mask: wgpu::ColorWrites::ALL,
2325                    })],
2326                    compilation_options: wgpu::PipelineCompilationOptions::default(),
2327                }),
2328                primitive: wgpu::PrimitiveState::default(),
2329                depth_stencil: None,
2330                multisample: wgpu::MultisampleState::default(),
2331                multiview_mask: None,
2332                cache: None,
2333            });
2334        self.display_pipeline = Some(pipeline);
2335    }
2336
2337    fn recreate_working_space_texture(&mut self) {
2338        if !self.working_space {
2339            return;
2340        }
2341        let w = self.config.width.max(1);
2342        let h = self.config.height.max(1);
2343
2344        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
2345            label: Some("working space"),
2346            size: wgpu::Extent3d {
2347                width: w,
2348                height: h,
2349                depth_or_array_layers: 1,
2350            },
2351            mip_level_count: 1,
2352            sample_count: 1,
2353            dimension: wgpu::TextureDimension::D2,
2354            format: wgpu::TextureFormat::Rgba16Float,
2355            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2356            view_formats: &[],
2357        });
2358        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
2359
2360        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2361            label: Some("working space bind"),
2362            layout: self.display_layout.as_ref().unwrap(),
2363            entries: &[
2364                wgpu::BindGroupEntry {
2365                    binding: 0,
2366                    resource: wgpu::BindingResource::TextureView(&view),
2367                },
2368                wgpu::BindGroupEntry {
2369                    binding: 1,
2370                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
2371                },
2372            ],
2373        });
2374
2375        self.ws_tex = Some(tex);
2376        self.ws_view = Some(view);
2377        self.ws_bind = Some(bind);
2378    }
2379
2380    fn recreate_msaa_and_depth_stencil(&mut self) {
2381        if self.msaa_samples > 1 {
2382            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
2383                label: Some("msaa color"),
2384                size: wgpu::Extent3d {
2385                    width: self.config.width.max(1),
2386                    height: self.config.height.max(1),
2387                    depth_or_array_layers: 1,
2388                },
2389                mip_level_count: 1,
2390                sample_count: self.msaa_samples,
2391                dimension: wgpu::TextureDimension::D2,
2392                format: self.config.format,
2393                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2394                view_formats: &[],
2395            });
2396            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
2397            self.msaa_tex = Some(tex);
2398            self.msaa_view = Some(view);
2399        } else {
2400            self.msaa_tex = None;
2401            self.msaa_view = None;
2402        }
2403
2404        self.depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
2405            label: Some("depth-stencil (stencil clips)"),
2406            size: wgpu::Extent3d {
2407                width: self.config.width.max(1),
2408                height: self.config.height.max(1),
2409                depth_or_array_layers: 1,
2410            },
2411            mip_level_count: 1,
2412            sample_count: self.msaa_samples,
2413            dimension: wgpu::TextureDimension::D2,
2414            format: wgpu::TextureFormat::Depth24PlusStencil8,
2415            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2416            view_formats: &[],
2417        });
2418        self.depth_stencil_view = self
2419            .depth_stencil_tex
2420            .create_view(&wgpu::TextureViewDescriptor::default());
2421    }
2422
2423    fn init_atlas_mask(device: &wgpu::Device) -> anyhow::Result<AtlasA8> {
2424        let size = 1024u32;
2425        let tex = device.create_texture(&wgpu::TextureDescriptor {
2426            label: Some("glyph atlas A8"),
2427            size: wgpu::Extent3d {
2428                width: size,
2429                height: size,
2430                depth_or_array_layers: 1,
2431            },
2432            mip_level_count: 1,
2433            sample_count: 1,
2434            dimension: wgpu::TextureDimension::D2,
2435            format: wgpu::TextureFormat::R8Unorm,
2436            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2437            view_formats: &[],
2438        });
2439        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
2440        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2441            label: Some("glyph atlas sampler A8"),
2442            address_mode_u: wgpu::AddressMode::ClampToEdge,
2443            address_mode_v: wgpu::AddressMode::ClampToEdge,
2444            address_mode_w: wgpu::AddressMode::ClampToEdge,
2445            mag_filter: wgpu::FilterMode::Linear,
2446            min_filter: wgpu::FilterMode::Linear,
2447            mipmap_filter: wgpu::MipmapFilterMode::Linear,
2448            ..Default::default()
2449        });
2450
2451        Ok(AtlasA8 {
2452            tex,
2453            view,
2454            sampler,
2455            size,
2456            next_x: 1,
2457            next_y: 1,
2458            row_h: 0,
2459            map: HashMap::new(),
2460        })
2461    }
2462
2463    fn init_atlas_color(device: &wgpu::Device) -> anyhow::Result<AtlasRGBA> {
2464        let size = 1024u32;
2465        let tex = device.create_texture(&wgpu::TextureDescriptor {
2466            label: Some("glyph atlas RGBA"),
2467            size: wgpu::Extent3d {
2468                width: size,
2469                height: size,
2470                depth_or_array_layers: 1,
2471            },
2472            mip_level_count: 1,
2473            sample_count: 1,
2474            dimension: wgpu::TextureDimension::D2,
2475            format: wgpu::TextureFormat::Rgba8UnormSrgb,
2476            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2477            view_formats: &[],
2478        });
2479        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
2480        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2481            label: Some("glyph atlas sampler RGBA"),
2482            address_mode_u: wgpu::AddressMode::ClampToEdge,
2483            address_mode_v: wgpu::AddressMode::ClampToEdge,
2484            address_mode_w: wgpu::AddressMode::ClampToEdge,
2485            mag_filter: wgpu::FilterMode::Linear,
2486            min_filter: wgpu::FilterMode::Linear,
2487            mipmap_filter: wgpu::MipmapFilterMode::Linear,
2488            ..Default::default()
2489        });
2490        Ok(AtlasRGBA {
2491            tex,
2492            view,
2493            sampler,
2494            size,
2495            next_x: 1,
2496            next_y: 1,
2497            row_h: 0,
2498            map: HashMap::new(),
2499        })
2500    }
2501
2502    fn get_or_create_layer(
2503        &mut self,
2504        layer_id: u32,
2505        width: u32,
2506        height: u32,
2507        rect: repose_core::Rect,
2508    ) {
2509        let needs_alloc = match self.layer_pool.get(&layer_id) {
2510            Some(lt) => lt.width != width || lt.height != height,
2511            None => true,
2512        };
2513        if !needs_alloc {
2514            return;
2515        }
2516        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
2517            label: Some("graphics layer"),
2518            size: wgpu::Extent3d {
2519                width: width.max(1),
2520                height: height.max(1),
2521                depth_or_array_layers: 1,
2522            },
2523            mip_level_count: 1,
2524            sample_count: 1,
2525            dimension: wgpu::TextureDimension::D2,
2526            format: self.config.format,
2527            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2528            view_formats: &[],
2529        });
2530        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
2531        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2532            label: Some("layer bind"),
2533            layout: &self.image_bind_layout_rgba,
2534            entries: &[
2535                wgpu::BindGroupEntry {
2536                    binding: 0,
2537                    resource: wgpu::BindingResource::TextureView(&view),
2538                },
2539                wgpu::BindGroupEntry {
2540                    binding: 1,
2541                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
2542                },
2543            ],
2544        });
2545        let depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
2546            label: Some("graphics layer depth-stencil"),
2547            size: wgpu::Extent3d {
2548                width: width.max(1),
2549                height: height.max(1),
2550                depth_or_array_layers: 1,
2551            },
2552            mip_level_count: 1,
2553            sample_count: 1,
2554            dimension: wgpu::TextureDimension::D2,
2555            format: wgpu::TextureFormat::Depth24PlusStencil8,
2556            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2557            view_formats: &[],
2558        });
2559        let depth_stencil_view =
2560            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
2561        self.layer_pool.insert(
2562            layer_id,
2563            LayerTarget {
2564                texture: tex,
2565                view,
2566                bind,
2567                depth_stencil_tex,
2568                depth_stencil_view,
2569                width,
2570                height,
2571                rect_px: (rect.x, rect.y, rect.w, rect.h),
2572            },
2573        );
2574    }
2575
2576    fn atlas_bind_group_mask(&self) -> wgpu::BindGroup {
2577        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2578            label: Some("atlas bind"),
2579            layout: &self.text_bind_layout,
2580            entries: &[
2581                wgpu::BindGroupEntry {
2582                    binding: 0,
2583                    resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
2584                },
2585                wgpu::BindGroupEntry {
2586                    binding: 1,
2587                    resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
2588                },
2589            ],
2590        })
2591    }
2592
2593    fn atlas_bind_group_color(&self) -> wgpu::BindGroup {
2594        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2595            label: Some("atlas bind color"),
2596            layout: &self.text_bind_layout,
2597            entries: &[
2598                wgpu::BindGroupEntry {
2599                    binding: 0,
2600                    resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
2601                },
2602                wgpu::BindGroupEntry {
2603                    binding: 1,
2604                    resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
2605                },
2606            ],
2607        })
2608    }
2609
2610    fn upload_glyph_mask(&mut self, key: repose_text::GlyphKey, px: f32) -> Option<GlyphInfo> {
2611        let keyp = (key, px.to_bits());
2612        if let Some(info) = self.atlas_mask.map.get(&keyp) {
2613            return Some(*info);
2614        }
2615
2616        let gb = repose_text::rasterize(key, px)?;
2617        if gb.w == 0 || gb.h == 0 || gb.data.is_empty() {
2618            return None;
2619        }
2620
2621        let coverage = swash_to_a8_coverage(gb.content, &gb.data)?;
2622
2623        let w = gb.w.max(1);
2624        let h = gb.h.max(1);
2625
2626        if !self.alloc_space_mask(w, h) {
2627            self.grow_mask_and_rebuild();
2628        }
2629        if !self.alloc_space_mask(w, h) {
2630            return None;
2631        }
2632        let x = self.atlas_mask.next_x;
2633        let y = self.atlas_mask.next_y;
2634        self.atlas_mask.next_x += w + 1;
2635        self.atlas_mask.row_h = self.atlas_mask.row_h.max(h + 1);
2636
2637        let layout = wgpu::TexelCopyBufferLayout {
2638            offset: 0,
2639            bytes_per_row: Some(w),
2640            rows_per_image: Some(h),
2641        };
2642        let size = wgpu::Extent3d {
2643            width: w,
2644            height: h,
2645            depth_or_array_layers: 1,
2646        };
2647        self.queue.write_texture(
2648            wgpu::TexelCopyTextureInfoBase {
2649                texture: &self.atlas_mask.tex,
2650                mip_level: 0,
2651                origin: wgpu::Origin3d { x, y, z: 0 },
2652                aspect: wgpu::TextureAspect::All,
2653            },
2654            &coverage,
2655            layout,
2656            size,
2657        );
2658
2659        let info = GlyphInfo {
2660            u0: x as f32 / self.atlas_mask.size as f32,
2661            v0: y as f32 / self.atlas_mask.size as f32,
2662            u1: (x + w) as f32 / self.atlas_mask.size as f32,
2663            v1: (y + h) as f32 / self.atlas_mask.size as f32,
2664            w: w as f32,
2665            h: h as f32,
2666            bearing_x: 0.0,
2667            bearing_y: 0.0,
2668            advance: 0.0,
2669        };
2670        self.atlas_mask.map.insert(keyp, info);
2671        Some(info)
2672    }
2673
2674    fn upload_glyph_color(&mut self, key: repose_text::GlyphKey, px: f32) -> Option<GlyphInfo> {
2675        let keyp = (key, px.to_bits());
2676        if let Some(info) = self.atlas_color.map.get(&keyp) {
2677            return Some(*info);
2678        }
2679        let gb = repose_text::rasterize(key, px)?;
2680        if !matches!(gb.content, repose_text::SwashContent::Color) {
2681            return None;
2682        }
2683        let w = gb.w.max(1);
2684        let h = gb.h.max(1);
2685        if !self.alloc_space_color(w, h) {
2686            self.grow_color_and_rebuild();
2687        }
2688        if !self.alloc_space_color(w, h) {
2689            return None;
2690        }
2691        let x = self.atlas_color.next_x;
2692        let y = self.atlas_color.next_y;
2693        self.atlas_color.next_x += w + 1;
2694        self.atlas_color.row_h = self.atlas_color.row_h.max(h + 1);
2695
2696        let layout = wgpu::TexelCopyBufferLayout {
2697            offset: 0,
2698            bytes_per_row: Some(w * 4),
2699            rows_per_image: Some(h),
2700        };
2701        let size = wgpu::Extent3d {
2702            width: w,
2703            height: h,
2704            depth_or_array_layers: 1,
2705        };
2706        self.queue.write_texture(
2707            wgpu::TexelCopyTextureInfoBase {
2708                texture: &self.atlas_color.tex,
2709                mip_level: 0,
2710                origin: wgpu::Origin3d { x, y, z: 0 },
2711                aspect: wgpu::TextureAspect::All,
2712            },
2713            &gb.data,
2714            layout,
2715            size,
2716        );
2717        let info = GlyphInfo {
2718            u0: x as f32 / self.atlas_color.size as f32,
2719            v0: y as f32 / self.atlas_color.size as f32,
2720            u1: (x + w) as f32 / self.atlas_color.size as f32,
2721            v1: (y + h) as f32 / self.atlas_color.size as f32,
2722            w: w as f32,
2723            h: h as f32,
2724            bearing_x: 0.0,
2725            bearing_y: 0.0,
2726            advance: 0.0,
2727        };
2728        self.atlas_color.map.insert(keyp, info);
2729        Some(info)
2730    }
2731
2732    fn alloc_space_mask(&mut self, w: u32, h: u32) -> bool {
2733        if self.atlas_mask.next_x + w + 1 >= self.atlas_mask.size {
2734            self.atlas_mask.next_x = 1;
2735            self.atlas_mask.next_y += self.atlas_mask.row_h + 1;
2736            self.atlas_mask.row_h = 0;
2737        }
2738        if self.atlas_mask.next_y + h + 1 >= self.atlas_mask.size {
2739            return false;
2740        }
2741        true
2742    }
2743
2744    fn grow_mask_and_rebuild(&mut self) {
2745        let new_size = (self.atlas_mask.size * 2).min(4096);
2746        if new_size == self.atlas_mask.size {
2747            return;
2748        }
2749        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
2750            label: Some("glyph atlas A8 (grown)"),
2751            size: wgpu::Extent3d {
2752                width: new_size,
2753                height: new_size,
2754                depth_or_array_layers: 1,
2755            },
2756            mip_level_count: 1,
2757            sample_count: 1,
2758            dimension: wgpu::TextureDimension::D2,
2759            format: wgpu::TextureFormat::R8Unorm,
2760            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2761            view_formats: &[],
2762        });
2763        self.atlas_mask.tex = tex;
2764        self.atlas_mask.view = self
2765            .atlas_mask
2766            .tex
2767            .create_view(&wgpu::TextureViewDescriptor::default());
2768        self.atlas_mask.size = new_size;
2769        self.atlas_mask.next_x = 1;
2770        self.atlas_mask.next_y = 1;
2771        self.atlas_mask.row_h = 0;
2772        let keys: Vec<(repose_text::GlyphKey, u32)> = self.atlas_mask.map.keys().copied().collect();
2773        self.atlas_mask.map.clear();
2774        for (k, px_bits) in keys {
2775            let _ = self.upload_glyph_mask(k, f32::from_bits(px_bits));
2776        }
2777    }
2778
2779    fn alloc_space_color(&mut self, w: u32, h: u32) -> bool {
2780        if self.atlas_color.next_x + w + 1 >= self.atlas_color.size {
2781            self.atlas_color.next_x = 1;
2782            self.atlas_color.next_y += self.atlas_color.row_h + 1;
2783            self.atlas_color.row_h = 0;
2784        }
2785        if self.atlas_color.next_y + h + 1 >= self.atlas_color.size {
2786            return false;
2787        }
2788        true
2789    }
2790
2791    fn grow_color_and_rebuild(&mut self) {
2792        let new_size = (self.atlas_color.size * 2).min(4096);
2793        if new_size == self.atlas_color.size {
2794            return;
2795        }
2796        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
2797            label: Some("glyph atlas RGBA (grown)"),
2798            size: wgpu::Extent3d {
2799                width: new_size,
2800                height: new_size,
2801                depth_or_array_layers: 1,
2802            },
2803            mip_level_count: 1,
2804            sample_count: 1,
2805            dimension: wgpu::TextureDimension::D2,
2806            format: wgpu::TextureFormat::Rgba8UnormSrgb,
2807            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2808            view_formats: &[],
2809        });
2810        self.atlas_color.tex = tex;
2811        self.atlas_color.view = self
2812            .atlas_color
2813            .tex
2814            .create_view(&wgpu::TextureViewDescriptor::default());
2815        self.atlas_color.size = new_size;
2816        self.atlas_color.next_x = 1;
2817        self.atlas_color.next_y = 1;
2818        self.atlas_color.row_h = 0;
2819        let keys: Vec<(repose_text::GlyphKey, u32)> =
2820            self.atlas_color.map.keys().copied().collect();
2821        self.atlas_color.map.clear();
2822        for (k, px_bits) in keys {
2823            let _ = self.upload_glyph_color(k, f32::from_bits(px_bits));
2824        }
2825    }
2826}
2827
2828fn brush_to_instance_fields(brush: &Brush) -> (u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2]) {
2829    match brush {
2830        Brush::Solid(c) => (
2831            0u32,
2832            c.to_linear(),
2833            [0.0, 0.0, 0.0, 0.0],
2834            [0.0, 0.0],
2835            [0.0, 1.0],
2836        ),
2837        Brush::Linear {
2838            start,
2839            end,
2840            start_color,
2841            end_color,
2842        } => (
2843            1u32,
2844            start_color.to_linear(),
2845            end_color.to_linear(),
2846            [start.x, start.y],
2847            [end.x, end.y],
2848        ),
2849        _ => (0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
2850    }
2851}
2852
2853fn brush_to_solid_color(brush: &Brush) -> [f32; 4] {
2854    match brush {
2855        Brush::Solid(c) => c.to_linear(),
2856        Brush::Linear { start_color, .. } => start_color.to_linear(),
2857        _ => [0.0; 4],
2858    }
2859}
2860
2861impl RenderBackend for WgpuBackend {
2862    fn configure_surface(&mut self, width: u32, height: u32) {
2863        if width == 0 || height == 0 {
2864            return;
2865        }
2866        self.config.width = width;
2867        self.config.height = height;
2868        self.surface.configure(&self.device, &self.config);
2869        self.recreate_msaa_and_depth_stencil();
2870        self.recreate_working_space_texture();
2871    }
2872
2873    fn frame(&mut self, scene: &Scene, _glyph_cfg: GlyphRasterConfig) {
2874        // Frame start maintenance
2875        self.frame_index = self.frame_index.wrapping_add(1);
2876        self.slug_cache.next_frame();
2877
2878        if self.config.width == 0 || self.config.height == 0 {
2879            return;
2880        }
2881        let mut retries = 0u32;
2882        const MAX_RETRIES: u32 = 4;
2883        let frame = loop {
2884            match self.surface.get_current_texture() {
2885                wgpu::CurrentSurfaceTexture::Success(f) => break f,
2886                wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
2887                    log::warn!("suboptimal surface; reconfiguring");
2888                    self.surface.configure(&self.device, &self.config);
2889                    break f;
2890                }
2891                wgpu::CurrentSurfaceTexture::Outdated => {
2892                    retries += 1;
2893                    if retries >= MAX_RETRIES {
2894                        log::warn!(
2895                            "surface outdated persisted after {MAX_RETRIES} retries; skipping frame"
2896                        );
2897                        return;
2898                    }
2899                    log::warn!("surface outdated; reconfiguring");
2900                    self.surface.configure(&self.device, &self.config);
2901                }
2902                wgpu::CurrentSurfaceTexture::Lost => {
2903                    retries += 1;
2904                    if retries >= MAX_RETRIES {
2905                        log::warn!(
2906                            "surface lost persisted after {MAX_RETRIES} retries; skipping frame"
2907                        );
2908                        return;
2909                    }
2910                    log::warn!("surface lost; reconfiguring");
2911                    self.surface.configure(&self.device, &self.config);
2912                }
2913                wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
2914                    request_frame();
2915                    return;
2916                }
2917                wgpu::CurrentSurfaceTexture::Validation => {
2918                    retries += 1;
2919                    if retries >= MAX_RETRIES {
2920                        log::warn!(
2921                            "surface validation persisted after {MAX_RETRIES} retries; skipping frame"
2922                        );
2923                        return;
2924                    }
2925                    self.surface.configure(&self.device, &self.config);
2926                }
2927            }
2928        };
2929
2930        fn to_ndc(x: f32, y: f32, w: f32, h: f32, fb_w: f32, fb_h: f32) -> [f32; 4] {
2931            let x0 = (x / fb_w) * 2.0 - 1.0;
2932            let y0 = 1.0 - (y / fb_h) * 2.0;
2933            let x1 = ((x + w) / fb_w) * 2.0 - 1.0;
2934            let y1 = 1.0 - ((y + h) / fb_h) * 2.0;
2935            let min_x = x0.min(x1);
2936            let min_y = y0.min(y1);
2937            let w_ndc = (x1 - x0).abs();
2938            let h_ndc = (y1 - y0).abs();
2939            [min_x, min_y, w_ndc, h_ndc]
2940        }
2941
2942        /// Convert a local-space rect + transform to NDC center-based position+size and rotation.
2943        fn rect_to_instance_ndc(
2944            rect: repose_core::Rect,
2945            transform: &Transform,
2946            fb_w: f32,
2947            fb_h: f32,
2948        ) -> ([f32; 4], [f32; 2]) {
2949            let cx = rect.x + rect.w * 0.5;
2950            let cy = rect.y + rect.h * 0.5;
2951
2952            // Apply full transform to center
2953            let sx = cx * transform.scale_x;
2954            let sy = cy * transform.scale_y;
2955            let cos_a = transform.rotate.cos();
2956            let sin_a = transform.rotate.sin();
2957            let tx = sx * cos_a - sy * sin_a + transform.translate_x;
2958            let ty = sx * sin_a + sy * cos_a + transform.translate_y;
2959
2960            // NDC center
2961            let ndc_cx = (tx / fb_w) * 2.0 - 1.0;
2962            let ndc_cy = 1.0 - (ty / fb_h) * 2.0;
2963            // NDC size (after scale only, no rotation - rotation is done in shader)
2964            let ndc_w = (rect.w * transform.scale_x / fb_w) * 2.0;
2965            let ndc_h = (rect.h * transform.scale_y / fb_h) * 2.0;
2966
2967            ([ndc_cx, ndc_cy, ndc_w, ndc_h], [cos_a, sin_a])
2968        }
2969
2970        fn to_scissor(r: &repose_core::Rect, fb_w: u32, fb_h: u32) -> (u32, u32, u32, u32) {
2971            let mut x = r.x.floor() as i64;
2972            let mut y = r.y.floor() as i64;
2973            let fb_wi = fb_w as i64;
2974            let fb_hi = fb_h as i64;
2975            x = x.clamp(0, fb_wi.saturating_sub(1));
2976            y = y.clamp(0, fb_hi.saturating_sub(1));
2977            let w_req = r.w.ceil().max(1.0) as i64;
2978            let h_req = r.h.ceil().max(1.0) as i64;
2979            let w = (w_req).min(fb_wi - x).max(1);
2980            let h = (h_req).min(fb_hi - y).max(1);
2981            (x as u32, y as u32, w as u32, h as u32)
2982        }
2983
2984        let fb_w = self.config.width as f32;
2985        let fb_h = self.config.height as f32;
2986
2987        let globals = Globals {
2988            ndc_to_px: [fb_w * 0.5, fb_h * 0.5],
2989            _pad: [0.0, 0.0],
2990        };
2991        self.queue
2992            .write_buffer(&self.globals_buf, 0, bytemuck::bytes_of(&globals));
2993
2994        let mut passes: Vec<Pass> = Vec::with_capacity(1);
2995        let mut current_pass: Pass = Pass {
2996            target: PassTarget::Surface,
2997            initial_scissor: (0, 0, self.config.width, self.config.height),
2998            clear_color: Some([
2999                scene.clear_color.0 as f32 / 255.0,
3000                scene.clear_color.1 as f32 / 255.0,
3001                scene.clear_color.2 as f32 / 255.0,
3002                scene.clear_color.3 as f32 / 255.0,
3003            ]),
3004            cmds: Vec::with_capacity(scene.nodes.len()),
3005        };
3006        let mut target_stack: Vec<PassTarget> = Vec::new();
3007        let mut layer_alphas: Vec<(u32, f32, (u32, u32, u32, u32))> = Vec::new();
3008        let mut layer_blurs: Vec<(u32, f32, f32)> = Vec::new();
3009        let mut current_target_size: (f32, f32) = (fb_w, fb_h);
3010
3011        struct Batch {
3012            rects: Vec<RectInstance>,
3013            borders: Vec<BorderInstance>,
3014            ellipses: Vec<EllipseInstance>,
3015            e_borders: Vec<EllipseBorderInstance>,
3016            arcs: Vec<ArcInstance>,
3017            masks: Vec<GlyphInstance>,
3018            colors: Vec<GlyphInstance>,
3019            nv12s: Vec<Nv12Instance>,
3020        }
3021
3022        impl Batch {
3023            fn new() -> Self {
3024                Self {
3025                    rects: vec![],
3026                    borders: vec![],
3027                    ellipses: vec![],
3028                    e_borders: vec![],
3029                    arcs: vec![],
3030                    masks: vec![],
3031                    colors: vec![],
3032                    nv12s: vec![],
3033                }
3034            }
3035
3036            fn is_empty(&self) -> bool {
3037                self.rects.is_empty()
3038                    && self.borders.is_empty()
3039                    && self.ellipses.is_empty()
3040                    && self.e_borders.is_empty()
3041                    && self.arcs.is_empty()
3042                    && self.masks.is_empty()
3043                    && self.colors.is_empty()
3044                    && self.nv12s.is_empty()
3045            }
3046
3047            fn flush(
3048                &mut self,
3049                pipes: (
3050                    &mut InstancedPipe<RectInstance>,
3051                    &mut InstancedPipe<BorderInstance>,
3052                    &mut InstancedPipe<EllipseInstance>,
3053                    &mut InstancedPipe<EllipseBorderInstance>,
3054                    &mut InstancedPipe<ArcInstance>,
3055                ),
3056                glyph_pipes: (
3057                    &mut InstancedPipe<GlyphInstance>,
3058                    &mut InstancedPipe<GlyphInstance>,
3059                ),
3060                nv12_pipe: &mut InstancedPipe<Nv12Instance>,
3061                device: &wgpu::Device,
3062                queue: &wgpu::Queue,
3063                cmds: &mut Vec<Cmd>,
3064            ) {
3065                let (rects, borders, ellipses, e_borders, arcs) = pipes;
3066                let (masks, colors) = glyph_pipes;
3067
3068                macro_rules! flush_one {
3069                    ($buf:ident, $pipe:expr, $variant:ident) => {
3070                        if !self.$buf.is_empty() {
3071                            if let Some((off, cnt)) = $pipe.upload(device, queue, &self.$buf) {
3072                                cmds.push(Cmd::$variant { off, cnt });
3073                            }
3074                            self.$buf.clear();
3075                        }
3076                    };
3077                }
3078
3079                flush_one!(rects, rects, Rect);
3080                flush_one!(borders, borders, Border);
3081                flush_one!(ellipses, ellipses, Ellipse);
3082                flush_one!(e_borders, e_borders, EllipseBorder);
3083                flush_one!(arcs, arcs, Arc);
3084                flush_one!(masks, masks, GlyphsMask);
3085                flush_one!(colors, colors, GlyphsColor);
3086
3087                if !self.nv12s.is_empty() {
3088                    if let Some((off, cnt)) = nv12_pipe.upload(device, queue, &self.nv12s) {
3089                        let _ = (off, cnt);
3090                    }
3091                    self.nv12s.clear();
3092                }
3093            }
3094        }
3095
3096        self.rects.reset();
3097        self.borders.reset();
3098        self.ellipses.reset();
3099        self.ellipse_borders.reset();
3100        self.arcs.reset();
3101        self.glyph_mask.reset();
3102        self.glyph_color.reset();
3103        self.clip_ring.reset();
3104        self.blur_ring.reset();
3105        self.nv12.reset();
3106
3107        self.slug_ring.reset();
3108        let mut batch = Batch::new();
3109        let mut slug_verts_local: Vec<slug::TessVertex> = Vec::new();
3110        let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
3111        let mut scissor_stack: Vec<repose_core::Rect> = Vec::with_capacity(8);
3112        let root_clip_rect = repose_core::Rect {
3113            x: 0.0,
3114            y: 0.0,
3115            w: fb_w,
3116            h: fb_h,
3117        };
3118
3119        let mut current_prim: Option<&'static str> = None;
3120
3121        macro_rules! flush_if_prim_changed {
3122            ($prim:literal, $pipe:expr) => {
3123                if current_prim != Some($prim) {
3124                    flush_batch!();
3125                    current_prim = Some($prim);
3126                }
3127            };
3128        }
3129
3130        macro_rules! flush_batch {
3131            () => {
3132                if !batch.is_empty() {
3133                    batch.flush(
3134                        (
3135                            &mut self.rects,
3136                            &mut self.borders,
3137                            &mut self.ellipses,
3138                            &mut self.ellipse_borders,
3139                            &mut self.arcs,
3140                        ),
3141                        (&mut self.glyph_mask, &mut self.glyph_color),
3142                        &mut self.nv12,
3143                        &self.device,
3144                        &self.queue,
3145                        &mut current_pass.cmds,
3146                    )
3147                }
3148            };
3149        }
3150
3151        for node in &scene.nodes {
3152            let t_identity = Transform::identity();
3153            let current_transform = transform_stack.last().unwrap_or(&t_identity);
3154
3155            match node {
3156                SceneNode::Rect {
3157                    rect,
3158                    brush,
3159                    radius,
3160                } => {
3161                    flush_if_prim_changed!("rect", &self.rects);
3162                    let (ndc, sin_cos) = rect_to_instance_ndc(
3163                        *rect,
3164                        current_transform,
3165                        current_target_size.0,
3166                        current_target_size.1,
3167                    );
3168                    let (brush_type, color0, color1, grad_start, grad_end) =
3169                        brush_to_instance_fields(brush);
3170                    batch.rects.push(RectInstance {
3171                        xywh: ndc,
3172                        radii: *radius,
3173                        brush_type,
3174                        _pad: [0.0; 3],
3175                        color0,
3176                        color1,
3177                        grad_start,
3178                        grad_end,
3179                        sin_cos,
3180                    });
3181                }
3182                SceneNode::Border {
3183                    rect,
3184                    color,
3185                    width,
3186                    radius,
3187                } => {
3188                    flush_if_prim_changed!("border", &self.borders);
3189                    let (ndc, sin_cos) = rect_to_instance_ndc(
3190                        *rect,
3191                        current_transform,
3192                        current_target_size.0,
3193                        current_target_size.1,
3194                    );
3195                    batch.borders.push(BorderInstance {
3196                        xywh: ndc,
3197                        radii: *radius,
3198                        stroke: *width,
3199                        color: color.to_linear(),
3200                        sin_cos,
3201                    });
3202                }
3203                SceneNode::Ellipse { rect, brush } => {
3204                    flush_if_prim_changed!("ellipse", &self.ellipses);
3205                    let (ndc, sin_cos) = rect_to_instance_ndc(
3206                        *rect,
3207                        current_transform,
3208                        current_target_size.0,
3209                        current_target_size.1,
3210                    );
3211                    let color = brush_to_solid_color(brush);
3212                    batch.ellipses.push(EllipseInstance {
3213                        xywh: ndc,
3214                        color,
3215                        sin_cos,
3216                    });
3217                }
3218                SceneNode::EllipseBorder { rect, color, width } => {
3219                    flush_if_prim_changed!("ellipse_border", &self.ellipse_borders);
3220                    let (ndc, sin_cos) = rect_to_instance_ndc(
3221                        *rect,
3222                        current_transform,
3223                        current_target_size.0,
3224                        current_target_size.1,
3225                    );
3226                    let pad_px = *width * 0.5 + 2.0;
3227                    let pad = (pad_px / current_target_size.0) * 2.0;
3228                    batch.e_borders.push(EllipseBorderInstance {
3229                        xywh: ndc,
3230                        stroke: *width,
3231                        pad,
3232                        color: color.to_linear(),
3233                        sin_cos,
3234                    });
3235                }
3236                SceneNode::Arc {
3237                    rect,
3238                    start_angle,
3239                    sweep_angle,
3240                    stroke_width,
3241                    color,
3242                    cap,
3243                } => {
3244                    flush_if_prim_changed!("arc", &self.arcs);
3245                    let (ndc, sin_cos) = rect_to_instance_ndc(
3246                        *rect,
3247                        current_transform,
3248                        current_target_size.0,
3249                        current_target_size.1,
3250                    );
3251                    let pad_px = *stroke_width * 0.5 + 2.0;
3252                    let pad = (pad_px / current_target_size.0) * 2.0;
3253                    let cap_val = match cap {
3254                        StrokeCap::Butt => 0.0,
3255                        StrokeCap::Round => 1.0,
3256                        StrokeCap::Square => 2.0,
3257                    };
3258                    batch.arcs.push(ArcInstance {
3259                        xywh: ndc,
3260                        start_angle: *start_angle,
3261                        sweep_angle: *sweep_angle,
3262                        stroke: *stroke_width,
3263                        pad,
3264                        color: color.to_linear(),
3265                        sin_cos,
3266                        cap: cap_val,
3267                    });
3268                }
3269                SceneNode::Text {
3270                    rect,
3271                    text,
3272                    color,
3273                    size,
3274                    font_family,
3275                    text_align: _,
3276                    font_weight,
3277                    font_style,
3278                    text_decoration,
3279                    letter_spacing,
3280                    line_height: _,
3281                    extra_style,
3282                    url: _,
3283                    font_variation_settings,
3284                } => {
3285                    flush_batch!(); // flush any prior primitives
3286
3287                    let px = *size;
3288                    let lh_ratio = rect.h / px;
3289                    let fw = font_weight.0;
3290                    let fs = if *font_style == FontStyle::Italic {
3291                        1
3292                    } else {
3293                        0
3294                    };
3295                    let shaped = repose_text::shape_line(
3296                        text.as_ref(),
3297                        px,
3298                        lh_ratio,
3299                        *font_family,
3300                        fw,
3301                        fs,
3302                        *letter_spacing,
3303                        font_variation_settings.as_deref(),
3304                    );
3305                    let baseline_y = shaped.first().map(|g| rect.y + g.y);
3306
3307                    let cos_a = current_transform.rotate.cos();
3308                    let sin_a = current_transform.rotate.sin();
3309                    let has_rotation = current_transform.rotate != 0.0;
3310
3311                    // For rotated text, the pivot is the center of the text rect.
3312                    let pivot_x = rect.x + rect.w * 0.5;
3313                    let pivot_y = rect.y + rect.h * 0.5;
3314
3315                    // Helper: compute NDC for a glyph rect, handling rotation correctly.
3316                    let make_glyph_instance =
3317                        |gx: f32, gy: f32, gw: f32, gh: f32| -> ([f32; 4], [f32; 2]) {
3318                            if has_rotation {
3319                                let corners =
3320                                    [(gx, gy), (gx + gw, gy), (gx + gw, gy + gh), (gx, gy + gh)];
3321                                let mut min_x = f32::MAX;
3322                                let mut max_x = f32::MIN;
3323                                let mut min_y = f32::MAX;
3324                                let mut max_y = f32::MIN;
3325                                for &(x, y) in &corners {
3326                                    let dx = x - pivot_x;
3327                                    let dy = y - pivot_y;
3328                                    let rx = pivot_x + dx * cos_a - dy * sin_a;
3329                                    let ry = pivot_y + dx * sin_a + dy * cos_a;
3330                                    min_x = min_x.min(rx);
3331                                    max_x = max_x.max(rx);
3332                                    min_y = min_y.min(ry);
3333                                    max_y = max_y.max(ry);
3334                                }
3335                                let bb_w = max_x - min_x;
3336                                let bb_h = max_y - min_y;
3337                                let ndc_tl = to_ndc(
3338                                    min_x,
3339                                    min_y,
3340                                    bb_w,
3341                                    bb_h,
3342                                    current_target_size.0,
3343                                    current_target_size.1,
3344                                );
3345                                let ndc = [
3346                                    ndc_tl[0] + ndc_tl[2] * 0.5,
3347                                    ndc_tl[1] + ndc_tl[3] * 0.5,
3348                                    ndc_tl[2],
3349                                    ndc_tl[3],
3350                                ];
3351                                (ndc, [cos_a, sin_a])
3352                            } else {
3353                                rect_to_instance_ndc(
3354                                    repose_core::Rect {
3355                                        x: gx,
3356                                        y: gy,
3357                                        w: gw,
3358                                        h: gh,
3359                                    },
3360                                    current_transform,
3361                                    current_target_size.0,
3362                                    current_target_size.1,
3363                                )
3364                            }
3365                        };
3366
3367                    let baseline_shift_y: f32 = px * extra_style.baseline_shift.0;
3368
3369                    let (
3370                        is_stroke,
3371                        stroke_width,
3372                        stroke_cap,
3373                        stroke_join,
3374                        stroke_miter,
3375                        stroke_path_effect,
3376                    ) = match &extra_style.draw_style {
3377                        repose_core::DrawStyle::Stroke {
3378                            width,
3379                            cap,
3380                            join,
3381                            miter,
3382                            path_effect,
3383                        } => (true, *width, *cap, *join, *miter, path_effect.clone()),
3384                        _ => (
3385                            false,
3386                            0.0,
3387                            repose_core::StrokeCap::Butt,
3388                            repose_core::StrokeJoin::Miter,
3389                            4.0,
3390                            None,
3391                        ),
3392                    };
3393                    let stroke_tess_key = if is_stroke {
3394                        Some(slug::StrokeTessKey::new(
3395                            stroke_width,
3396                            stroke_cap,
3397                            stroke_join,
3398                            stroke_miter,
3399                            &stroke_path_effect,
3400                        ))
3401                    } else {
3402                        None
3403                    };
3404
3405                    for sg in shaped {
3406                        let gx = rect.x + sg.x + sg.bearing_x;
3407                        let gy = rect.y + sg.y - sg.bearing_y + baseline_shift_y;
3408
3409                        // Vector glyph path: tessellated geometry with MSAA.
3410                        if self.slug_enabled {
3411                            let ck = repose_text::lookup_cache_key(sg.key, sg.px);
3412                            if let Some(ref ck) = ck {
3413                                // Check if cached.
3414                                let need_tessellate = self.slug_cache.get(ck).map_or(true, |g| {
3415                                    if is_stroke {
3416                                        let key = stroke_tess_key.as_ref().unwrap();
3417                                        !g.stroke_variants.contains_key(key)
3418                                    } else {
3419                                        g.fill_vertices.is_none()
3420                                    }
3421                                });
3422                                if need_tessellate {
3423                                    if let Some((ck2, commands)) =
3424                                        repose_text::lookup_and_extract_outline(sg.key, sg.px)
3425                                    {
3426                                        let font_size_px = f32::from_bits(ck2.font_size_bits);
3427                                        if is_stroke {
3428                                            self.slug_cache.get_or_insert_stroke(
3429                                                ck2,
3430                                                font_size_px,
3431                                                &commands,
3432                                                stroke_width,
3433                                                stroke_cap,
3434                                                stroke_join,
3435                                                stroke_miter,
3436                                                &stroke_path_effect,
3437                                            );
3438                                        } else {
3439                                            self.slug_cache.get_or_insert(
3440                                                ck2,
3441                                                font_size_px,
3442                                                &commands,
3443                                            );
3444                                        }
3445                                    }
3446                                } else {
3447                                    self.slug_cache.touch(ck);
3448                                }
3449                            }
3450                            if let Some(entry) = ck.as_ref().and_then(|ck| self.slug_cache.get(ck))
3451                            {
3452                                let ox = rect.x + sg.x;
3453                                let oy = rect.y + sg.y + baseline_shift_y;
3454                                let scx = current_transform.scale_x;
3455                                let scy = current_transform.scale_y;
3456                                let ttx = current_transform.translate_x;
3457                                let tty = current_transform.translate_y;
3458
3459                                let tf = |x: f32, y: f32| -> (f32, f32) {
3460                                    if has_rotation {
3461                                        let dx = x - pivot_x;
3462                                        let dy = y - pivot_y;
3463                                        let rx = pivot_x + dx * cos_a - dy * sin_a;
3464                                        let ry = pivot_y + dx * sin_a + dy * cos_a;
3465                                        (rx, ry)
3466                                    } else {
3467                                        (x * scx + ttx, y * scy + tty)
3468                                    }
3469                                };
3470
3471                                let tw = current_target_size.0;
3472                                let th = current_target_size.1;
3473
3474                                let verts = if is_stroke {
3475                                    let key = stroke_tess_key.as_ref().unwrap();
3476                                    entry
3477                                        .stroke_variants
3478                                        .get(key)
3479                                        .map(|v| v.as_slice())
3480                                        .unwrap_or(&[])
3481                                } else {
3482                                    entry.fill_vertices.as_deref().unwrap_or(&[])
3483                                };
3484
3485                                for &v in verts {
3486                                    let (sx, sy) = tf(ox + v[0] * px, oy - v[1] * px);
3487                                    let ndc_x = sx / tw * 2.0 - 1.0;
3488                                    let ndc_y = -(sy / th) * 2.0 + 1.0;
3489                                    slug_verts_local.push(slug::TessVertex {
3490                                        ndc_pos: [ndc_x, ndc_y],
3491                                        color: color.to_linear(),
3492                                    });
3493                                }
3494
3495                                if is_stroke {
3496                                    // Stroke glyphs cannot use atlas fallback...
3497                                    continue;
3498                                }
3499                                continue;
3500                            }
3501                        }
3502
3503                        // Don't use atlas fallback for strokes too
3504                        if is_stroke {
3505                            continue;
3506                        }
3507
3508                        // Atlas fallback: color emoji + failed slug extraction
3509                        if let Some(info) = self.upload_glyph_color(sg.key, sg.px) {
3510                            let (ndc, sin_cos) = make_glyph_instance(gx, gy, info.w, info.h);
3511                            batch.colors.push(GlyphInstance {
3512                                xywh: ndc,
3513                                uv: [info.u0, info.v1, info.u1, info.v0],
3514                                color: color.to_linear(),
3515                                sin_cos,
3516                            });
3517                        } else if let Some(info) = self.upload_glyph_mask(sg.key, sg.px) {
3518                            let (ndc, sin_cos) = make_glyph_instance(gx, gy, info.w, info.h);
3519                            batch.masks.push(GlyphInstance {
3520                                xywh: ndc,
3521                                uv: [info.u0, info.v1, info.u1, info.v0],
3522                                color: color.to_linear(),
3523                                sin_cos,
3524                            });
3525                        }
3526                    }
3527
3528                    // Upload slug vertices if any
3529                    if !slug_verts_local.is_empty() {
3530                        let bytes = bytemuck::cast_slice(&slug_verts_local);
3531                        self.slug_ring.grow_to_fit(&self.device, bytes.len() as u64);
3532                        let (off, _) = self.slug_ring.alloc_write(&self.queue, bytes);
3533                        current_pass.cmds.push(Cmd::GlyphsVector {
3534                            off,
3535                            cnt: slug_verts_local.len() as u32,
3536                        });
3537                        slug_verts_local.clear();
3538                    }
3539
3540                    // Text decoration: underline / strikethrough
3541                    if (text_decoration.underline || text_decoration.strikethrough)
3542                        && let Some(baseline_y) = baseline_y
3543                    {
3544                        flush_batch!();
3545                        current_prim = Some("rect");
3546                        let deco_color = text_decoration.color.unwrap_or(*color);
3547                        let thickness = (px * 0.07).max(1.0);
3548
3549                        if text_decoration.underline {
3550                            let dy = baseline_y + px * 0.1;
3551                            let (ndc, sin_cos) = rect_to_instance_ndc(
3552                                repose_core::Rect {
3553                                    x: rect.x,
3554                                    y: dy,
3555                                    w: rect.w,
3556                                    h: thickness,
3557                                },
3558                                current_transform,
3559                                current_target_size.0,
3560                                current_target_size.1,
3561                            );
3562                            batch.rects.push(RectInstance {
3563                                xywh: ndc,
3564                                radii: [0.0; 4],
3565                                brush_type: 0,
3566                                _pad: [0.0; 3],
3567                                color0: deco_color.to_linear(),
3568                                color1: [0.0; 4],
3569                                grad_start: [0.0; 2],
3570                                grad_end: [0.0; 2],
3571                                sin_cos,
3572                            });
3573                        }
3574                        if text_decoration.strikethrough {
3575                            let sy = baseline_y - px * 0.3;
3576                            let (ndc, sin_cos) = rect_to_instance_ndc(
3577                                repose_core::Rect {
3578                                    x: rect.x,
3579                                    y: sy,
3580                                    w: rect.w,
3581                                    h: thickness,
3582                                },
3583                                current_transform,
3584                                current_target_size.0,
3585                                current_target_size.1,
3586                            );
3587                            batch.rects.push(RectInstance {
3588                                xywh: ndc,
3589                                radii: [0.0; 4],
3590                                brush_type: 0,
3591                                _pad: [0.0; 3],
3592                                color0: deco_color.to_linear(),
3593                                color1: [0.0; 4],
3594                                grad_start: [0.0; 2],
3595                                grad_end: [0.0; 2],
3596                                sin_cos,
3597                            });
3598                        }
3599                    }
3600                }
3601                SceneNode::Image {
3602                    rect,
3603                    handle,
3604                    tint,
3605                    fit,
3606                } => {
3607                    flush_batch!();
3608
3609                    // Update usage timestamp for eviction
3610                    let (img_w, img_h, is_nv12) = if let Some(t) = self.images.get_mut(handle) {
3611                        match t {
3612                            ImageTex::Rgba {
3613                                w,
3614                                h,
3615                                last_used_frame,
3616                                ..
3617                            } => {
3618                                *last_used_frame = self.frame_index;
3619                                (*w, *h, false)
3620                            }
3621                            ImageTex::Nv12 {
3622                                w,
3623                                h,
3624                                last_used_frame,
3625                                ..
3626                            } => {
3627                                *last_used_frame = self.frame_index;
3628                                (*w, *h, true)
3629                            }
3630                        }
3631                    } else {
3632                        log::warn!("Image handle {} not found", handle);
3633                        continue;
3634                    };
3635
3636                    let src_w = img_w as f32;
3637                    let src_h = img_h as f32;
3638                    let transformed = current_transform.apply_to_rect(*rect);
3639                    let dst_w = transformed.w.max(0.0);
3640                    let dst_h = transformed.h.max(0.0);
3641                    if dst_w <= 0.0 || dst_h <= 0.0 {
3642                        continue;
3643                    }
3644
3645                    let (xywh_ndc, uv_rect) = match fit {
3646                        repose_core::view::ImageFit::Contain => {
3647                            let scale = (dst_w / src_w).min(dst_h / src_h);
3648                            let w = src_w * scale;
3649                            let h = src_h * scale;
3650                            let x = transformed.x + (dst_w - w) * 0.5;
3651                            let y = transformed.y + (dst_h - h) * 0.5;
3652                            (
3653                                to_ndc(x, y, w, h, current_target_size.0, current_target_size.1),
3654                                [0.0, 1.0, 1.0, 0.0],
3655                            )
3656                        }
3657                        repose_core::view::ImageFit::Cover => {
3658                            let scale = (dst_w / src_w).max(dst_h / src_h);
3659                            let content_w = src_w * scale;
3660                            let content_h = src_h * scale;
3661                            let overflow_x = (content_w - dst_w) * 0.5;
3662                            let overflow_y = (content_h - dst_h) * 0.5;
3663                            let u0 = (overflow_x / content_w).clamp(0.0, 1.0);
3664                            let v0 = (overflow_y / content_h).clamp(0.0, 1.0);
3665                            let u1 = ((overflow_x + dst_w) / content_w).clamp(0.0, 1.0);
3666                            let v1 = ((overflow_y + dst_h) / content_h).clamp(0.0, 1.0);
3667                            (
3668                                to_ndc(
3669                                    transformed.x,
3670                                    transformed.y,
3671                                    dst_w,
3672                                    dst_h,
3673                                    current_target_size.0,
3674                                    current_target_size.1,
3675                                ),
3676                                [u0, 1.0 - v1, u1, 1.0 - v0],
3677                            )
3678                        }
3679                        repose_core::view::ImageFit::FitWidth => {
3680                            let scale = dst_w / src_w;
3681                            let w = dst_w;
3682                            let h = src_h * scale;
3683                            let y = transformed.y + (dst_h - h) * 0.5;
3684                            (
3685                                to_ndc(
3686                                    transformed.x,
3687                                    y,
3688                                    w,
3689                                    h,
3690                                    current_target_size.0,
3691                                    current_target_size.1,
3692                                ),
3693                                [0.0, 1.0, 1.0, 0.0],
3694                            )
3695                        }
3696                        repose_core::view::ImageFit::FitHeight => {
3697                            let scale = dst_h / src_h;
3698                            let w = src_w * scale;
3699                            let h = dst_h;
3700                            let x = transformed.x + (dst_w - w) * 0.5;
3701                            (
3702                                to_ndc(
3703                                    x,
3704                                    transformed.y,
3705                                    w,
3706                                    h,
3707                                    current_target_size.0,
3708                                    current_target_size.1,
3709                                ),
3710                                [0.0, 1.0, 1.0, 0.0],
3711                            )
3712                        }
3713                        _ => ([0.0; 4], [0.0; 4]),
3714                    };
3715
3716                    // Convert top-left based NDC to center-based for shader
3717                    let ndc_center = [
3718                        xywh_ndc[0] + xywh_ndc[2] * 0.5,
3719                        xywh_ndc[1] + xywh_ndc[3] * 0.5,
3720                        xywh_ndc[2],
3721                        xywh_ndc[3],
3722                    ];
3723
3724                    if is_nv12 {
3725                        let uv_x_offset = if let Some(ImageTex::Nv12 { w, color_info, .. }) =
3726                            self.images.get(handle)
3727                        {
3728                            match color_info.chroma_siting {
3729                                ChromaSiting::Center | ChromaSiting::TopLeft => 0.0,
3730                                ChromaSiting::Left => -1.0 / *w as f32,
3731                            }
3732                        } else {
3733                            0.0
3734                        };
3735
3736                        let inst = Nv12Instance {
3737                            xywh: ndc_center,
3738                            uv: uv_rect,
3739                            color: tint.to_linear(),
3740                            uv_x_offset,
3741                            sin_cos: [1.0, 0.0],
3742                            _pad: [0.0],
3743                        };
3744                        if let Some((off, _)) = self.nv12.upload(&self.device, &self.queue, &[inst])
3745                        {
3746                            current_pass.cmds.push(Cmd::ImageNv12 {
3747                                off,
3748                                cnt: 1,
3749                                handle: *handle,
3750                            });
3751                        }
3752                    } else {
3753                        // RGBA uses GlyphInstance struct (reused pipeline)
3754                        let inst = GlyphInstance {
3755                            xywh: ndc_center,
3756                            uv: uv_rect,
3757                            color: tint.to_linear(),
3758                            sin_cos: [1.0, 0.0],
3759                        };
3760                        if let Some((off, _)) =
3761                            self.glyph_color.upload(&self.device, &self.queue, &[inst])
3762                        {
3763                            current_pass.cmds.push(Cmd::ImageRgba {
3764                                off,
3765                                cnt: 1,
3766                                handle: *handle,
3767                            });
3768                        }
3769                    }
3770                }
3771                SceneNode::PushClip { rect, radius, op } => {
3772                    flush_batch!(); // flush content before entering clip
3773
3774                    let is_diff = matches!(op, repose_core::ClipOp::Difference);
3775
3776                    let t_identity = Transform::identity();
3777                    let current_transform = transform_stack.last().unwrap_or(&t_identity);
3778                    let transformed = current_transform.apply_to_rect(*rect);
3779
3780                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
3781                    let next_scissor = if is_diff {
3782                        top
3783                    } else {
3784                        intersect(top, transformed)
3785                    };
3786                    scissor_stack.push(next_scissor);
3787                    let scissor = to_scissor(
3788                        &next_scissor,
3789                        current_target_size.0 as u32,
3790                        current_target_size.1 as u32,
3791                    );
3792
3793                    let clip_ndc_tl = to_ndc(
3794                        transformed.x,
3795                        transformed.y,
3796                        transformed.w,
3797                        transformed.h,
3798                        current_target_size.0,
3799                        current_target_size.1,
3800                    );
3801                    let inst = ClipInstance {
3802                        xywh: [
3803                            clip_ndc_tl[0] + clip_ndc_tl[2] * 0.5,
3804                            clip_ndc_tl[1] + clip_ndc_tl[3] * 0.5,
3805                            clip_ndc_tl[2],
3806                            clip_ndc_tl[3],
3807                        ],
3808                        radii: *radius,
3809                        sin_cos: [1.0, 0.0],
3810                    };
3811                    let bytes = bytemuck::bytes_of(&inst);
3812                    self.clip_ring.grow_to_fit(&self.device, bytes.len() as u64);
3813                    let (off, _) = self.clip_ring.alloc_write(&self.queue, bytes);
3814
3815                    let rounded = radius.iter().any(|&r| r > 0.5);
3816
3817                    current_pass.cmds.push(Cmd::ClipPush {
3818                        off,
3819                        cnt: 1,
3820                        scissor,
3821                        difference: is_diff,
3822                        rounded,
3823                    });
3824                }
3825                SceneNode::PopClip => {
3826                    flush_batch!();
3827
3828                    if !scissor_stack.is_empty() {
3829                        scissor_stack.pop();
3830                    } else {
3831                        log::warn!("PopClip with empty stack");
3832                    }
3833
3834                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
3835                    let scissor = to_scissor(
3836                        &top,
3837                        current_target_size.0 as u32,
3838                        current_target_size.1 as u32,
3839                    );
3840                    current_pass.cmds.push(Cmd::ClipPop { scissor });
3841                }
3842                SceneNode::Shadow {
3843                    rect,
3844                    radius,
3845                    elevation: _,
3846                    color,
3847                } => {
3848                    flush_if_prim_changed!("rect", &self.rects);
3849                    let (ndc, sin_cos) = rect_to_instance_ndc(
3850                        *rect,
3851                        current_transform,
3852                        current_target_size.0,
3853                        current_target_size.1,
3854                    );
3855                    let (brush_type, color0, _color1, _grad_start, _grad_end) =
3856                        brush_to_instance_fields(&Brush::Solid(*color));
3857                    batch.rects.push(RectInstance {
3858                        xywh: ndc,
3859                        radii: *radius,
3860                        brush_type,
3861                        _pad: [0.0; 3],
3862                        color0,
3863                        color1: [0.0; 4],
3864                        grad_start: [0.0; 2],
3865                        grad_end: [0.0; 2],
3866                        sin_cos,
3867                    });
3868                }
3869                SceneNode::PushTransform { transform } => {
3870                    flush_batch!(); // flush before transform change
3871                    let combined = current_transform.combine(transform);
3872                    transform_stack.push(combined);
3873                }
3874                SceneNode::PopTransform => {
3875                    flush_batch!(); // flush before transform change
3876                    transform_stack.pop();
3877                }
3878                SceneNode::BeginLayer {
3879                    rect,
3880                    layer_id,
3881                    alpha,
3882                    blur_radius_x,
3883                    blur_radius_y,
3884                    rectangle_edge: _,
3885                } => {
3886                    flush_batch!();
3887                    let w = (rect.w.max(1.0)).ceil() as u32;
3888                    let h = (rect.h.max(1.0)).ceil() as u32;
3889                    // Close out the current pass, start a new one for the layer.
3890                    let prev_target = current_pass.target;
3891                    let prev_scissor = current_pass.initial_scissor;
3892                    let saved = std::mem::replace(
3893                        &mut current_pass,
3894                        Pass {
3895                            target: PassTarget::Layer(*layer_id),
3896                            initial_scissor: (0, 0, w, h),
3897                            clear_color: Some([0.0, 0.0, 0.0, 0.0]),
3898                            cmds: Vec::new(),
3899                        },
3900                    );
3901                    passes.push(saved);
3902                    target_stack.push(prev_target);
3903                    let _ = prev_scissor; // initial_scissor of resumed pass is restored at EndLayer
3904                    // Get or create the layer's offscreen texture now so that
3905                    // subsequent scissor ops / draws have a valid target.
3906                    self.get_or_create_layer(*layer_id, w, h, *rect);
3907                    current_target_size = (w as f32, h as f32);
3908                    layer_alphas.push((*layer_id, *alpha, current_pass.initial_scissor));
3909                    // Store blur info for post-processing after EndLayer
3910                    if *blur_radius_x > 0.0 || *blur_radius_y > 0.0 {
3911                        layer_blurs.push((*layer_id, *blur_radius_x, *blur_radius_y));
3912                    }
3913                }
3914                SceneNode::EndLayer { layer_id } => {
3915                    flush_batch!();
3916                    // Finish the layer's pass, start a new one on the previous target.
3917                    let saved = std::mem::replace(
3918                        &mut current_pass,
3919                        Pass {
3920                            target: target_stack.pop().unwrap_or(PassTarget::Surface),
3921                            initial_scissor: (0, 0, self.config.width, self.config.height),
3922                            clear_color: None, // LoadOp::Load - don't wipe earlier surface content
3923                            cmds: Vec::new(),
3924                        },
3925                    );
3926                    passes.push(saved);
3927                    current_target_size = (fb_w, fb_h);
3928                    // Issue a composite quad for the just-finished layer in the new pass.
3929                    if let Some((_, layer_alpha, _)) = layer_alphas
3930                        .iter()
3931                        .find(|(id, _, _)| id == layer_id)
3932                        .copied()
3933                    {
3934                        let layer = self.layer_pool.get(layer_id).expect("layer target");
3935                        let ndc_tl = to_ndc(
3936                            layer.rect_px.0,
3937                            layer.rect_px.1,
3938                            layer.rect_px.2,
3939                            layer.rect_px.3,
3940                            fb_w,
3941                            fb_h,
3942                        );
3943                        // Check if this layer needs content blur
3944                        let blur_px_val = layer_blurs
3945                            .iter()
3946                            .find(|(id, _, _)| id == layer_id)
3947                            .map(|(_, bx, by)| (*bx, *by));
3948                        if let Some((blur_x, blur_y)) =
3949                            blur_px_val.filter(|(bx, by)| *bx > 0.0 || *by > 0.0)
3950                        {
3951                            // Content blur: draw blurred version using the blur_content pipeline
3952                            let bw_uv = (blur_x * 1.5) / layer.width.max(1) as f32;
3953                            let bh_uv = (blur_y * 1.5) / layer.height.max(1) as f32;
3954                            let inst = BlurInstance {
3955                                xywh: [
3956                                    ndc_tl[0] + ndc_tl[2] * 0.5,
3957                                    ndc_tl[1] + ndc_tl[3] * 0.5,
3958                                    ndc_tl[2],
3959                                    ndc_tl[3],
3960                                ],
3961                                uv: [0.0, 0.0, 1.0, 1.0],
3962                                color: [1.0, 1.0, 1.0, layer_alpha],
3963                                blur_uv: [bw_uv, bh_uv],
3964                                sin_cos: [1.0, 0.0],
3965                            };
3966                            self.blur_ring.grow_to_fit(
3967                                &self.device,
3968                                std::mem::size_of::<BlurInstance>() as u64,
3969                            );
3970                            let bytes = bytemuck::bytes_of(&inst);
3971                            let (off, _) = self.blur_ring.alloc_write(&self.queue, bytes);
3972                            current_pass.cmds.push(Cmd::CompositeBlur {
3973                                off,
3974                                cnt: 1,
3975                                layer_id: *layer_id,
3976                            });
3977                        } else {
3978                            // Normal sharp composite
3979                            let inst = GlyphInstance {
3980                                xywh: [
3981                                    ndc_tl[0] + ndc_tl[2] * 0.5,
3982                                    ndc_tl[1] + ndc_tl[3] * 0.5,
3983                                    ndc_tl[2],
3984                                    ndc_tl[3],
3985                                ],
3986                                uv: [0.0, 1.0, 1.0, 0.0],
3987                                color: [1.0, 1.0, 1.0, layer_alpha],
3988                                sin_cos: [1.0, 0.0],
3989                            };
3990                            if let Some((off, cnt)) =
3991                                self.glyph_color.upload(&self.device, &self.queue, &[inst])
3992                            {
3993                                current_pass.cmds.push(Cmd::CompositeLayer {
3994                                    off,
3995                                    cnt,
3996                                    layer_id: *layer_id,
3997                                    alpha: layer_alpha,
3998                                });
3999                            }
4000                        }
4001                    }
4002                }
4003                SceneNode::CompositeShadow {
4004                    layer_id,
4005                    blur_px,
4006                    offset_px,
4007                    color,
4008                } => {
4009                    flush_batch!();
4010                    if let Some(layer) = self.layer_pool.get(layer_id).cloned() {
4011                        // Shadow rect = layer rect + offset.
4012                        let sx = layer.rect_px.0 + offset_px.0;
4013                        let sy = layer.rect_px.1 + offset_px.1;
4014                        let sw = layer.rect_px.2;
4015                        let sh = layer.rect_px.3;
4016                        // The blur in UV space is 1.5 * blur_px / texture_size
4017                        // (the 1.5 matches the 3x3 Gaussian span).
4018                        let bw_uv = (blur_px * 1.5) / layer.width.max(1) as f32;
4019                        let bh_uv = (blur_px * 1.5) / layer.height.max(1) as f32;
4020                        let ndc_tl = to_ndc(sx, sy, sw, sh, fb_w, fb_h);
4021                        let inst = BlurInstance {
4022                            xywh: [
4023                                ndc_tl[0] + ndc_tl[2] * 0.5,
4024                                ndc_tl[1] + ndc_tl[3] * 0.5,
4025                                ndc_tl[2],
4026                                ndc_tl[3],
4027                            ],
4028                            uv: [0.0, 0.0, 1.0, 1.0],
4029                            color: [
4030                                color.0 as f32 / 255.0,
4031                                color.1 as f32 / 255.0,
4032                                color.2 as f32 / 255.0,
4033                                color.3 as f32 / 255.0,
4034                            ],
4035                            blur_uv: [bw_uv, bh_uv],
4036                            sin_cos: [1.0, 0.0],
4037                        };
4038                        self.blur_ring
4039                            .grow_to_fit(&self.device, std::mem::size_of::<BlurInstance>() as u64);
4040                        let bytes = bytemuck::bytes_of(&inst);
4041                        let (off, _) = self.blur_ring.alloc_write(&self.queue, bytes);
4042                        current_pass.cmds.push(Cmd::CompositeShadow {
4043                            off,
4044                            cnt: 1,
4045                            layer_id: *layer_id,
4046                        });
4047                    }
4048                }
4049                _ => {}
4050            }
4051        }
4052
4053        flush_batch!();
4054
4055        // Push the final pass.
4056        passes.push(current_pass);
4057
4058        let mut encoder = self
4059            .device
4060            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4061                label: Some("frame encoder"),
4062            });
4063
4064        let bind_mask = self.atlas_bind_group_mask();
4065        let bind_color = self.atlas_bind_group_color();
4066        let mut clip_depth: u32 = 0;
4067
4068        for pass in std::mem::take(&mut passes) {
4069            let (color_view, resolve_target, depth_stencil_view, is_layer) = match pass.target {
4070                PassTarget::Surface => {
4071                    let swap_view = frame
4072                        .texture
4073                        .create_view(&wgpu::TextureViewDescriptor::default());
4074                    let use_ws = self.working_space && self.ws_view.is_some();
4075                    let (color, resolve) = if use_ws {
4076                        let ws_view = self.ws_view.as_ref().unwrap();
4077                        if let Some(msaa_view) = &self.msaa_view {
4078                            // MSAA resolves to working-space texture
4079                            (msaa_view.clone(), Some(ws_view.clone()))
4080                        } else {
4081                            // Direct render to working-space texture
4082                            (ws_view.clone(), None)
4083                        }
4084                    } else if let Some(msaa_view) = &self.msaa_view {
4085                        (msaa_view.clone(), Some(swap_view))
4086                    } else {
4087                        (swap_view, None)
4088                    };
4089                    (color, resolve, self.depth_stencil_view.clone(), false)
4090                }
4091                PassTarget::Layer(layer_id) => {
4092                    if let Some(lt) = self.layer_pool.get(&layer_id) {
4093                        (lt.view.clone(), None, lt.depth_stencil_view.clone(), true)
4094                    } else {
4095                        log::warn!("missing layer target {layer_id}");
4096                        continue;
4097                    }
4098                }
4099            };
4100
4101            if is_layer {
4102                clip_depth = 0;
4103            }
4104
4105            let pipes: &Pipelines = if is_layer {
4106                &self.layer_pipes
4107            } else {
4108                &self.surface_pipes
4109            };
4110
4111            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
4112                label: Some("pass"),
4113                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
4114                    view: &color_view,
4115                    resolve_target: resolve_target.as_ref(),
4116                    ops: wgpu::Operations {
4117                        load: match pass.clear_color {
4118                            Some(c) => wgpu::LoadOp::Clear(wgpu::Color {
4119                                r: c[0] as f64,
4120                                g: c[1] as f64,
4121                                b: c[2] as f64,
4122                                a: c[3] as f64,
4123                            }),
4124                            None => wgpu::LoadOp::Load,
4125                        },
4126                        store: wgpu::StoreOp::Store,
4127                    },
4128                    depth_slice: None,
4129                })],
4130                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
4131                    view: &depth_stencil_view,
4132                    depth_ops: None,
4133                    stencil_ops: Some(wgpu::Operations {
4134                        load: if is_layer || pass.clear_color.is_some() {
4135                            wgpu::LoadOp::Clear(0)
4136                        } else {
4137                            wgpu::LoadOp::Load
4138                        },
4139                        store: wgpu::StoreOp::Store,
4140                    }),
4141                }),
4142                timestamp_writes: None,
4143                occlusion_query_set: None,
4144                multiview_mask: None,
4145            });
4146
4147            rpass.set_bind_group(0, &self.globals_bind, &[]);
4148            rpass.set_stencil_reference(clip_depth);
4149            rpass.set_scissor_rect(
4150                pass.initial_scissor.0,
4151                pass.initial_scissor.1,
4152                pass.initial_scissor.2,
4153                pass.initial_scissor.3,
4154            );
4155
4156            macro_rules! draw_simple {
4157                ($pipeline:expr, $ring:expr, $inst:ty, $off:ident, $n:ident) => {{
4158                    rpass.set_pipeline($pipeline);
4159                    let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
4160                    rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
4161                    rpass.draw(0..6, 0..$n);
4162                }};
4163            }
4164
4165            macro_rules! draw_with_bind {
4166                ($pipeline:expr, $ring:expr, $inst:ty, $bind:expr, $off:ident, $n:ident) => {{
4167                    rpass.set_pipeline($pipeline);
4168                    rpass.set_bind_group(1, $bind, &[]);
4169                    let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
4170                    rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
4171                    rpass.draw(0..6, 0..$n);
4172                }};
4173            }
4174
4175            for cmd in pass.cmds {
4176                match cmd {
4177                    Cmd::ClipPush {
4178                        off,
4179                        cnt: n,
4180                        scissor,
4181                        difference,
4182                        rounded,
4183                    } => {
4184                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
4185                        rpass.set_stencil_reference(clip_depth);
4186
4187                        if difference {
4188                            rpass.set_pipeline(&pipes.clip_dec);
4189                        } else if self.msaa_samples > 1 && !is_layer && rounded {
4190                            rpass.set_pipeline(&pipes.clip_a2c);
4191                        } else {
4192                            rpass.set_pipeline(&pipes.clip_bin);
4193                        }
4194
4195                        let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
4196                        rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
4197                        rpass.draw(0..6, 0..n);
4198
4199                        if !difference {
4200                            clip_depth = (clip_depth + 1).min(255);
4201                            rpass.set_stencil_reference(clip_depth);
4202                        }
4203                    }
4204
4205                    Cmd::ClipPop { scissor } => {
4206                        clip_depth = clip_depth.saturating_sub(1);
4207                        rpass.set_stencil_reference(clip_depth);
4208                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
4209                    }
4210
4211                    Cmd::Rect { off, cnt: n } => {
4212                        draw_simple!(&pipes.rects, self.rects.ring, RectInstance, off, n);
4213                    }
4214
4215                    Cmd::Border { off, cnt: n } => {
4216                        draw_simple!(&pipes.borders, self.borders.ring, BorderInstance, off, n);
4217                    }
4218
4219                    Cmd::GlyphsMask { off, cnt: n } => {
4220                        draw_with_bind!(
4221                            &pipes.text_mask,
4222                            self.glyph_mask.ring,
4223                            GlyphInstance,
4224                            &bind_mask,
4225                            off,
4226                            n
4227                        );
4228                    }
4229
4230                    Cmd::GlyphsColor { off, cnt: n } => {
4231                        draw_with_bind!(
4232                            &pipes.text_color,
4233                            self.glyph_color.ring,
4234                            GlyphInstance,
4235                            &bind_color,
4236                            off,
4237                            n
4238                        );
4239                    }
4240
4241                    Cmd::GlyphsVector { off, cnt: n } => {
4242                        if let Some(ref slug_pipe) = pipes.slug.as_ref() {
4243                            rpass.set_pipeline(slug_pipe);
4244                            let bytes = (n as u64) * std::mem::size_of::<slug::TessVertex>() as u64;
4245                            rpass.set_vertex_buffer(0, self.slug_ring.buf.slice(off..off + bytes));
4246                            rpass.draw(0..n, 0..1);
4247                        }
4248                    }
4249
4250                    Cmd::ImageRgba {
4251                        off,
4252                        cnt: n,
4253                        handle,
4254                    } => {
4255                        if let Some(ImageTex::Rgba { bind, .. }) = self.images.get(&handle) {
4256                            draw_with_bind!(
4257                                &pipes.image_rgba,
4258                                self.glyph_color.ring,
4259                                GlyphInstance,
4260                                bind,
4261                                off,
4262                                n
4263                            );
4264                        }
4265                    }
4266
4267                    Cmd::ImageNv12 {
4268                        off,
4269                        cnt: n,
4270                        handle,
4271                    } => {
4272                        if let Some(ImageTex::Nv12 { bind, .. }) = self.images.get(&handle) {
4273                            draw_with_bind!(
4274                                &pipes.image_nv12,
4275                                self.nv12.ring,
4276                                Nv12Instance,
4277                                bind,
4278                                off,
4279                                n
4280                            );
4281                        }
4282                    }
4283
4284                    Cmd::Ellipse { off, cnt: n } => {
4285                        draw_simple!(&pipes.ellipses, self.ellipses.ring, EllipseInstance, off, n);
4286                    }
4287
4288                    Cmd::EllipseBorder { off, cnt: n } => {
4289                        draw_simple!(
4290                            &pipes.ellipse_borders,
4291                            self.ellipse_borders.ring,
4292                            EllipseBorderInstance,
4293                            off,
4294                            n
4295                        );
4296                    }
4297
4298                    Cmd::Arc { off, cnt: n } => {
4299                        draw_simple!(&pipes.arcs, self.arcs.ring, ArcInstance, off, n);
4300                    }
4301
4302                    Cmd::PushTransform(_) => {}
4303                    Cmd::PopTransform => {}
4304                    Cmd::CompositeLayer {
4305                        off,
4306                        cnt: n,
4307                        layer_id,
4308                        alpha: _,
4309                    } => {
4310                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
4311                            draw_with_bind!(
4312                                &pipes.image_rgba,
4313                                self.glyph_color.ring,
4314                                GlyphInstance,
4315                                &lt.bind,
4316                                off,
4317                                n
4318                            );
4319                        }
4320                    }
4321                    Cmd::CompositeShadow {
4322                        off,
4323                        cnt: n,
4324                        layer_id,
4325                    } => {
4326                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
4327                            draw_with_bind!(
4328                                &pipes.blur,
4329                                self.blur_ring,
4330                                BlurInstance,
4331                                &lt.bind,
4332                                off,
4333                                n
4334                            );
4335                        }
4336                    }
4337                    Cmd::CompositeBlur {
4338                        off,
4339                        cnt: n,
4340                        layer_id,
4341                    } => {
4342                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
4343                            draw_with_bind!(
4344                                &pipes.blur_content,
4345                                self.blur_ring,
4346                                BlurInstance,
4347                                &lt.bind,
4348                                off,
4349                                n
4350                            );
4351                        }
4352                    }
4353                }
4354            }
4355        }
4356
4357        // Display pass: linear working space → sRGB OETF → swapchain
4358        if self.working_space {
4359            if let (Some(_ws_view), Some(ws_bind), Some(display_pipeline)) =
4360                (&self.ws_view, &self.ws_bind, &self.display_pipeline)
4361            {
4362                let swap_view = frame
4363                    .texture
4364                    .create_view(&wgpu::TextureViewDescriptor::default());
4365                let mut display_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
4366                    label: Some("display transform"),
4367                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
4368                        view: &swap_view,
4369                        resolve_target: None,
4370                        ops: wgpu::Operations {
4371                            load: wgpu::LoadOp::Load,
4372                            store: wgpu::StoreOp::Store,
4373                        },
4374                        depth_slice: None,
4375                    })],
4376                    depth_stencil_attachment: None,
4377                    timestamp_writes: None,
4378                    occlusion_query_set: None,
4379                    multiview_mask: None,
4380                });
4381                display_pass.set_pipeline(display_pipeline);
4382                display_pass.set_bind_group(1, ws_bind, &[]);
4383                display_pass.draw(0..3, 0..1);
4384            }
4385        }
4386
4387        self.queue.submit(std::iter::once(encoder.finish()));
4388        if let Err(e) = catch_unwind(AssertUnwindSafe(|| self.queue.present(frame))) {
4389            log::warn!("queue.present panicked: {:?}", e);
4390        }
4391
4392        // Frame end maintenance: Evict unused images
4393        self.evict_unused_images();
4394    }
4395}
4396
4397fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> repose_core::Rect {
4398    let x0 = a.x.max(b.x);
4399    let y0 = a.y.max(b.y);
4400    let x1 = (a.x + a.w).min(b.x + b.w);
4401    let y1 = (a.y + a.h).min(b.y + b.h);
4402    repose_core::Rect {
4403        x: x0,
4404        y: y0,
4405        w: (x1 - x0).max(0.0),
4406        h: (y1 - y0).max(0.0),
4407    }
4408}