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