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, 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::Float32,
298            },
299            wgpu::VertexAttribute {
300                shader_location: 2,
301                offset: 20,
302                format: wgpu::VertexFormat::Uint32,
303            },
304            wgpu::VertexAttribute {
305                shader_location: 3,
306                offset: 24,
307                format: wgpu::VertexFormat::Float32x4,
308            },
309            wgpu::VertexAttribute {
310                shader_location: 4,
311                offset: 40,
312                format: wgpu::VertexFormat::Float32x4,
313            },
314            wgpu::VertexAttribute {
315                shader_location: 5,
316                offset: 56,
317                format: wgpu::VertexFormat::Float32x2,
318            },
319            wgpu::VertexAttribute {
320                shader_location: 6,
321                offset: 64,
322                format: wgpu::VertexFormat::Float32x2,
323            },
324            wgpu::VertexAttribute {
325                shader_location: 7,
326                offset: 72,
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::Float32,
340            },
341            wgpu::VertexAttribute {
342                shader_location: 2,
343                offset: 20,
344                format: wgpu::VertexFormat::Float32,
345            },
346            wgpu::VertexAttribute {
347                shader_location: 3,
348                offset: 24,
349                format: wgpu::VertexFormat::Float32x4,
350            },
351            wgpu::VertexAttribute {
352                shader_location: 4,
353                offset: 40,
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    radius: f32,
927    brush_type: u32,
928    color0: [f32; 4],
929    color1: [f32; 4],
930    grad_start: [f32; 2],
931    grad_end: [f32; 2],
932    sin_cos: [f32; 2],
933}
934
935#[repr(C)]
936#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
937struct BorderInstance {
938    xywh: [f32; 4],
939    radius: f32,
940    stroke: f32,
941    color: [f32; 4],
942    sin_cos: [f32; 2],
943}
944
945#[repr(C)]
946#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
947struct EllipseInstance {
948    xywh: [f32; 4],
949    color: [f32; 4],
950    sin_cos: [f32; 2],
951}
952
953#[repr(C)]
954#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
955struct EllipseBorderInstance {
956    xywh: [f32; 4],
957    stroke: f32,
958    pad: f32,
959    color: [f32; 4],
960    sin_cos: [f32; 2],
961}
962
963#[repr(C)]
964#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
965struct ArcInstance {
966    xywh: [f32; 4],
967    start_angle: f32,
968    sweep_angle: f32,
969    stroke: f32,
970    pad: f32,
971    color: [f32; 4],
972    sin_cos: [f32; 2],
973    cap: f32, // 0=Butt, 1=Round, 2=Square
974}
975
976#[repr(C)]
977#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
978struct GlyphInstance {
979    xywh: [f32; 4],
980    uv: [f32; 4],
981    color: [f32; 4],
982    sin_cos: [f32; 2],
983}
984
985#[repr(C)]
986#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
987struct BlurInstance {
988    xywh: [f32; 4],
989    uv: [f32; 4],
990    color: [f32; 4],
991    blur_uv: [f32; 2],
992    sin_cos: [f32; 2],
993}
994
995#[repr(C)]
996#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
997struct Nv12Instance {
998    xywh: [f32; 4],
999    uv: [f32; 4],
1000    color: [f32; 4], // tint
1001    full_range: f32,
1002    sin_cos: [f32; 2],
1003    _pad: [f32; 1],
1004}
1005
1006#[repr(C)]
1007#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1008struct ClipInstance {
1009    xywh: [f32; 4],
1010    radius: f32,
1011    sin_cos: [f32; 2],
1012}
1013
1014fn swash_to_a8_coverage(content: cosmic_text::SwashContent, data: &[u8]) -> Option<Vec<u8>> {
1015    match content {
1016        cosmic_text::SwashContent::Mask => Some(data.to_vec()),
1017        cosmic_text::SwashContent::SubpixelMask => {
1018            let mut out = Vec::with_capacity(data.len() / 4);
1019            for px in data.chunks_exact(4) {
1020                let r = px[0];
1021                let g = px[1];
1022                let b = px[2];
1023                out.push(r.max(g).max(b));
1024            }
1025            Some(out)
1026        }
1027        cosmic_text::SwashContent::Color => None,
1028    }
1029}
1030
1031impl WgpuBackend {
1032    pub async fn new_async(window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
1033        let instance: Instance;
1034
1035        if cfg!(target_arch = "wasm32") {
1036            let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
1037            desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
1038            instance = wgpu::util::new_instance_with_webgpu_detection(desc).await;
1039        } else {
1040            instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
1041        };
1042
1043        let surface = instance.create_surface(window.clone())?;
1044
1045        let adapter = instance
1046            .request_adapter(&wgpu::RequestAdapterOptions {
1047                power_preference: wgpu::PowerPreference::HighPerformance,
1048                compatible_surface: Some(&surface),
1049                force_fallback_adapter: false,
1050            })
1051            .await
1052            .map_err(|e| anyhow::anyhow!("No suitable adapter: {e:?}"))?;
1053
1054        let limits = adapter.limits();
1055
1056        let (device, queue) = adapter
1057            .request_device(&wgpu::DeviceDescriptor {
1058                label: Some("repose-rs device"),
1059                required_features: wgpu::Features::empty(),
1060                required_limits: limits,
1061                experimental_features: wgpu::ExperimentalFeatures::disabled(),
1062                memory_hints: wgpu::MemoryHints::default(),
1063                trace: wgpu::Trace::Off,
1064            })
1065            .await
1066            .map_err(|e| anyhow::anyhow!("request_device failed: {e:?}"))?;
1067
1068        let size = window.inner_size();
1069
1070        let caps = surface.get_capabilities(&adapter);
1071        let format = caps
1072            .formats
1073            .iter()
1074            .copied()
1075            .find(|f| f.is_srgb())
1076            .unwrap_or(caps.formats[0]);
1077        let present_mode = caps
1078            .present_modes
1079            .iter()
1080            .copied()
1081            .find(|m| *m == wgpu::PresentMode::Mailbox || *m == wgpu::PresentMode::Immediate)
1082            .unwrap_or(wgpu::PresentMode::Fifo);
1083        let alpha_mode = caps.alpha_modes[0];
1084
1085        let config = wgpu::SurfaceConfiguration {
1086            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1087            format,
1088            width: size.width.max(1),
1089            height: size.height.max(1),
1090            present_mode,
1091            alpha_mode,
1092            view_formats: vec![],
1093            desired_maximum_frame_latency: 2,
1094        };
1095        surface.configure(&device, &config);
1096
1097        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1098            label: Some("globals layout"),
1099            entries: &[wgpu::BindGroupLayoutEntry {
1100                binding: 0,
1101                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1102                ty: wgpu::BindingType::Buffer {
1103                    ty: wgpu::BufferBindingType::Uniform,
1104                    has_dynamic_offset: false,
1105                    min_binding_size: None,
1106                },
1107                count: None,
1108            }],
1109        });
1110
1111        let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
1112            label: Some("globals buf"),
1113            size: std::mem::size_of::<Globals>() as u64,
1114            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1115            mapped_at_creation: false,
1116        });
1117
1118        let globals_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1119            label: Some("globals bind"),
1120            layout: &globals_layout,
1121            entries: &[wgpu::BindGroupEntry {
1122                binding: 0,
1123                resource: globals_buf.as_entire_binding(),
1124            }],
1125        });
1126
1127        // Pick MSAA sample count
1128        let fmt_features = adapter.get_texture_format_features(format);
1129        let msaa_samples = if fmt_features.flags.sample_count_supported(4)
1130            && fmt_features
1131                .flags
1132                .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE)
1133        {
1134            4
1135        } else {
1136            1
1137        };
1138
1139        let ds_format = wgpu::TextureFormat::Depth24PlusStencil8;
1140
1141        let stencil_for_content = wgpu::DepthStencilState {
1142            format: ds_format,
1143            depth_write_enabled: Some(false),
1144            depth_compare: Some(wgpu::CompareFunction::Always),
1145            stencil: wgpu::StencilState {
1146                front: wgpu::StencilFaceState {
1147                    compare: wgpu::CompareFunction::LessEqual,
1148                    fail_op: wgpu::StencilOperation::Keep,
1149                    depth_fail_op: wgpu::StencilOperation::Keep,
1150                    pass_op: wgpu::StencilOperation::Keep,
1151                },
1152                back: wgpu::StencilFaceState {
1153                    compare: wgpu::CompareFunction::LessEqual,
1154                    fail_op: wgpu::StencilOperation::Keep,
1155                    depth_fail_op: wgpu::StencilOperation::Keep,
1156                    pass_op: wgpu::StencilOperation::Keep,
1157                },
1158                read_mask: 0xFF,
1159                write_mask: 0x00,
1160            },
1161            bias: wgpu::DepthBiasState::default(),
1162        };
1163
1164        let stencil_for_clip_inc = wgpu::DepthStencilState {
1165            format: ds_format,
1166            depth_write_enabled: Some(false),
1167            depth_compare: Some(wgpu::CompareFunction::Always),
1168            stencil: wgpu::StencilState {
1169                front: wgpu::StencilFaceState {
1170                    compare: wgpu::CompareFunction::Equal,
1171                    fail_op: wgpu::StencilOperation::Keep,
1172                    depth_fail_op: wgpu::StencilOperation::Keep,
1173                    pass_op: wgpu::StencilOperation::IncrementClamp,
1174                },
1175                back: wgpu::StencilFaceState {
1176                    compare: wgpu::CompareFunction::Equal,
1177                    fail_op: wgpu::StencilOperation::Keep,
1178                    depth_fail_op: wgpu::StencilOperation::Keep,
1179                    pass_op: wgpu::StencilOperation::IncrementClamp,
1180                },
1181                read_mask: 0xFF,
1182                write_mask: 0xFF,
1183            },
1184            bias: wgpu::DepthBiasState::default(),
1185        };
1186
1187        let _multisample_state = wgpu::MultisampleState {
1188            count: msaa_samples,
1189            mask: !0,
1190            alpha_to_coverage_enabled: false,
1191        };
1192
1193        // PIPELINES
1194
1195        // Single shared sampler for images/text
1196        let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1197            label: Some("image/text sampler"),
1198            address_mode_u: wgpu::AddressMode::ClampToEdge,
1199            address_mode_v: wgpu::AddressMode::ClampToEdge,
1200            mag_filter: wgpu::FilterMode::Linear,
1201            min_filter: wgpu::FilterMode::Linear,
1202            mipmap_filter: wgpu::MipmapFilterMode::Linear,
1203            ..Default::default()
1204        });
1205
1206        // Layout for Text / RGBA Images (Texture + Sampler)
1207        let text_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1208            label: Some("text/rgba bind layout"),
1209            entries: &[
1210                wgpu::BindGroupLayoutEntry {
1211                    binding: 0,
1212                    visibility: wgpu::ShaderStages::FRAGMENT,
1213                    ty: wgpu::BindingType::Texture {
1214                        multisampled: false,
1215                        view_dimension: wgpu::TextureViewDimension::D2,
1216                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
1217                    },
1218                    count: None,
1219                },
1220                wgpu::BindGroupLayoutEntry {
1221                    binding: 1,
1222                    visibility: wgpu::ShaderStages::FRAGMENT,
1223                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1224                    count: None,
1225                },
1226            ],
1227        });
1228        // We reuse this for RGBA images for simplicity, or create a distinct one
1229        let image_bind_layout_rgba = text_bind_layout.clone();
1230
1231        // Layout for NV12 Images (TextureY + TextureUV + Sampler)
1232        let image_bind_layout_nv12 =
1233            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1234                label: Some("image bind layout nv12"),
1235                entries: &[
1236                    // Y plane
1237                    wgpu::BindGroupLayoutEntry {
1238                        binding: 0,
1239                        visibility: wgpu::ShaderStages::FRAGMENT,
1240                        ty: wgpu::BindingType::Texture {
1241                            multisampled: false,
1242                            view_dimension: wgpu::TextureViewDimension::D2,
1243                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
1244                        },
1245                        count: None,
1246                    },
1247                    // UV plane
1248                    wgpu::BindGroupLayoutEntry {
1249                        binding: 1,
1250                        visibility: wgpu::ShaderStages::FRAGMENT,
1251                        ty: wgpu::BindingType::Texture {
1252                            multisampled: false,
1253                            view_dimension: wgpu::TextureViewDimension::D2,
1254                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
1255                        },
1256                        count: None,
1257                    },
1258                    // Sampler
1259                    wgpu::BindGroupLayoutEntry {
1260                        binding: 2,
1261                        visibility: wgpu::ShaderStages::FRAGMENT,
1262                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1263                        count: None,
1264                    },
1265                ],
1266            });
1267
1268        // Clipping layout
1269        let clip_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1270            label: Some("clip pipeline layout"),
1271            bind_group_layouts: &[Some(&globals_layout)],
1272            immediate_size: 0,
1273        });
1274        let clip_vertex_layout = wgpu::VertexBufferLayout {
1275            array_stride: std::mem::size_of::<ClipInstance>() as u64,
1276            step_mode: wgpu::VertexStepMode::Instance,
1277            attributes: &[
1278                wgpu::VertexAttribute {
1279                    shader_location: 0,
1280                    offset: 0,
1281                    format: wgpu::VertexFormat::Float32x4,
1282                },
1283                wgpu::VertexAttribute {
1284                    shader_location: 1,
1285                    offset: 16,
1286                    format: wgpu::VertexFormat::Float32,
1287                },
1288                wgpu::VertexAttribute {
1289                    shader_location: 2,
1290                    offset: 20,
1291                    format: wgpu::VertexFormat::Float32x2,
1292                },
1293            ],
1294        };
1295        let clip_color_target = wgpu::ColorTargetState {
1296            format: config.format,
1297            blend: None,
1298            write_mask: wgpu::ColorWrites::empty(),
1299        };
1300
1301        // Two sets of pipelines: one for the MSAA surface pass, one for layer
1302        // render-to-texture passes (sample_count = 1).
1303        let surface_pipes = Pipelines::create(
1304            &device,
1305            config.format,
1306            msaa_samples,
1307            &globals_layout,
1308            &text_bind_layout,
1309            &image_bind_layout_nv12,
1310            &clip_pipeline_layout,
1311            &stencil_for_content,
1312            &stencil_for_clip_inc,
1313            &clip_color_target,
1314            &clip_vertex_layout,
1315        );
1316        let layer_pipes = Pipelines::create(
1317            &device,
1318            config.format,
1319            1,
1320            &globals_layout,
1321            &text_bind_layout,
1322            &image_bind_layout_nv12,
1323            &clip_pipeline_layout,
1324            &stencil_for_content,
1325            &stencil_for_clip_inc,
1326            &clip_color_target,
1327            &clip_vertex_layout,
1328        );
1329
1330        // Vector glyph rendering always available with tessellation+MSAA approach.
1331        let slug_enabled = true;
1332
1333        // Blur composite ring (for graphics-layer drop shadows)
1334        let blur_ring = UploadRing::new(&device, "blur ring", 1024 * 1024);
1335
1336        // Atlases
1337        let atlas_mask = Self::init_atlas_mask(&device)?;
1338        let atlas_color = Self::init_atlas_color(&device)?;
1339
1340        // Upload rings
1341        let ring_rect = UploadRing::new(&device, "ring rect", 1 << 20);
1342        let ring_border = UploadRing::new(&device, "ring border", 1 << 20);
1343        let ring_ellipse = UploadRing::new(&device, "ring ellipse", 1 << 20);
1344        let ring_ellipse_border = UploadRing::new(&device, "ring ellipse border", 1 << 20);
1345        let ring_arc = UploadRing::new(&device, "ring arc", 1 << 20);
1346        let ring_glyph_mask = UploadRing::new(&device, "ring glyph mask", 1 << 20);
1347        let ring_glyph_color = UploadRing::new(&device, "ring glyph color", 1 << 20);
1348        let ring_slug = UploadRing::new(&device, "ring slug", 1 << 22);
1349        let ring_clip = UploadRing::new(&device, "ring clip", 1 << 16);
1350        let ring_nv12 = UploadRing::new(&device, "ring nv12", 1 << 20);
1351
1352        // Placeholder textures
1353        let depth_stencil_tex = device.create_texture(&wgpu::TextureDescriptor {
1354            label: Some("temp ds"),
1355            size: wgpu::Extent3d {
1356                width: 1,
1357                height: 1,
1358                depth_or_array_layers: 1,
1359            },
1360            mip_level_count: 1,
1361            sample_count: 1,
1362            dimension: wgpu::TextureDimension::D2,
1363            format: wgpu::TextureFormat::Depth24PlusStencil8,
1364            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1365            view_formats: &[],
1366        });
1367        let depth_stencil_view =
1368            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
1369
1370        let mut backend = Self {
1371            surface,
1372            device,
1373            queue,
1374            config,
1375
1376            surface_pipes,
1377            layer_pipes,
1378
1379            rects: InstancedPipe::new(ring_rect),
1380            borders: InstancedPipe::new(ring_border),
1381            ellipses: InstancedPipe::new(ring_ellipse),
1382            ellipse_borders: InstancedPipe::new(ring_ellipse_border),
1383            arcs: InstancedPipe::new(ring_arc),
1384            glyph_mask: InstancedPipe::new(ring_glyph_mask),
1385            glyph_color: InstancedPipe::new(ring_glyph_color),
1386
1387            text_bind_layout,
1388
1389            image_bind_layout_rgba,
1390            image_bind_layout_nv12,
1391            image_sampler,
1392
1393            blur_ring,
1394
1395            slug_enabled,
1396            slug_ring: ring_slug,
1397            slug_cache: slug::GlyphSlugCache::new(),
1398
1399            clip_ring: ring_clip,
1400
1401            nv12: InstancedPipe::new(ring_nv12),
1402
1403            msaa_samples,
1404            depth_stencil_tex,
1405            depth_stencil_view,
1406            msaa_tex: None,
1407            msaa_view: None,
1408            globals_bind,
1409            globals_buf,
1410            globals_layout,
1411
1412            atlas_mask,
1413            atlas_color,
1414
1415            next_image_handle: 1,
1416            images: HashMap::new(),
1417
1418            frame_index: 0,
1419            image_bytes_total: 0,
1420            image_evict_after_frames: 600,         // ~10s @ 60fps
1421            image_budget_bytes: 512 * 1024 * 1024, // 512 MB
1422            layer_pool: HashMap::new(),
1423        };
1424
1425        backend.recreate_msaa_and_depth_stencil();
1426        Ok(backend)
1427    }
1428
1429    #[cfg(not(target_arch = "wasm32"))]
1430    pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
1431        pollster::block_on(Self::new_async(window))
1432    }
1433
1434    #[cfg(target_arch = "wasm32")]
1435    pub fn new(_window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
1436        anyhow::bail!("Use WgpuBackend::new_async(window).await on wasm32")
1437    }
1438
1439    // Image API
1440
1441    pub fn set_image_from_bytes(
1442        &mut self,
1443        handle: u64,
1444        data: &[u8],
1445        srgb: bool,
1446    ) -> anyhow::Result<()> {
1447        let img = image::load_from_memory(data)?;
1448        let rgba = img.to_rgba8();
1449        let (w, h) = rgba.dimensions();
1450        self.set_image_rgba8(handle, w, h, &rgba, srgb)
1451    }
1452
1453    pub fn set_image_rgba8(
1454        &mut self,
1455        handle: u64,
1456        w: u32,
1457        h: u32,
1458        rgba: &[u8],
1459        srgb: bool,
1460    ) -> anyhow::Result<()> {
1461        let expected = (w as usize) * (h as usize) * 4;
1462        if rgba.len() < expected {
1463            return Err(anyhow::anyhow!(
1464                "RGBA buffer too small: {} < {}",
1465                rgba.len(),
1466                expected
1467            ));
1468        }
1469
1470        let format = if srgb {
1471            wgpu::TextureFormat::Rgba8UnormSrgb
1472        } else {
1473            wgpu::TextureFormat::Rgba8Unorm
1474        };
1475
1476        let needs_recreate = match self.images.get(&handle) {
1477            Some(ImageTex::Rgba {
1478                w: cw,
1479                h: ch,
1480                format: cf,
1481                ..
1482            }) => *cw != w || *ch != h || *cf != format,
1483            _ => true,
1484        };
1485
1486        if needs_recreate {
1487            // Remove old to track budget correctly
1488            self.remove_image(handle);
1489
1490            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1491                label: Some("user image rgba"),
1492                size: wgpu::Extent3d {
1493                    width: w,
1494                    height: h,
1495                    depth_or_array_layers: 1,
1496                },
1497                mip_level_count: 1,
1498                sample_count: 1,
1499                dimension: wgpu::TextureDimension::D2,
1500                format,
1501                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1502                view_formats: &[],
1503            });
1504            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1505
1506            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1507                label: Some("image bind rgba"),
1508                layout: &self.image_bind_layout_rgba,
1509                entries: &[
1510                    wgpu::BindGroupEntry {
1511                        binding: 0,
1512                        resource: wgpu::BindingResource::TextureView(&view),
1513                    },
1514                    wgpu::BindGroupEntry {
1515                        binding: 1,
1516                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
1517                    },
1518                ],
1519            });
1520
1521            let bytes = (w as u64) * (h as u64) * 4;
1522            self.image_bytes_total += bytes;
1523
1524            self.images.insert(
1525                handle,
1526                ImageTex::Rgba {
1527                    tex,
1528                    view,
1529                    bind,
1530                    w,
1531                    h,
1532                    format,
1533                    last_used_frame: self.frame_index,
1534                    bytes,
1535                },
1536            );
1537        }
1538
1539        let tex = match self.images.get(&handle) {
1540            Some(ImageTex::Rgba { tex, .. }) => tex,
1541            _ => unreachable!(),
1542        };
1543
1544        self.queue.write_texture(
1545            wgpu::TexelCopyTextureInfo {
1546                texture: tex,
1547                mip_level: 0,
1548                origin: wgpu::Origin3d::ZERO,
1549                aspect: wgpu::TextureAspect::All,
1550            },
1551            &rgba[..expected],
1552            wgpu::TexelCopyBufferLayout {
1553                offset: 0,
1554                bytes_per_row: Some(4 * w),
1555                rows_per_image: Some(h),
1556            },
1557            wgpu::Extent3d {
1558                width: w,
1559                height: h,
1560                depth_or_array_layers: 1,
1561            },
1562        );
1563
1564        // Ensure budget limits
1565        self.evict_budget_excess();
1566
1567        Ok(())
1568    }
1569
1570    pub fn set_image_nv12(
1571        &mut self,
1572        handle: u64,
1573        w: u32,
1574        h: u32,
1575        y: &[u8],
1576        uv: &[u8],
1577        full_range: bool,
1578    ) -> anyhow::Result<()> {
1579        let y_expected = (w as usize) * (h as usize);
1580        let uv_w = (w / 2).max(1);
1581        let uv_h = (h / 2).max(1);
1582        let uv_expected = (uv_w as usize) * (uv_h as usize) * 2;
1583
1584        if y.len() < y_expected {
1585            return Err(anyhow::anyhow!("Y plane too small"));
1586        }
1587        if uv.len() < uv_expected {
1588            return Err(anyhow::anyhow!("UV plane too small"));
1589        }
1590
1591        let needs_recreate = match self.images.get(&handle) {
1592            Some(ImageTex::Nv12 { w: ww, h: hh, .. }) => *ww != w || *hh != h,
1593            _ => true,
1594        };
1595
1596        if needs_recreate {
1597            self.remove_image(handle);
1598
1599            let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
1600                label: Some("nv12 Y"),
1601                size: wgpu::Extent3d {
1602                    width: w,
1603                    height: h,
1604                    depth_or_array_layers: 1,
1605                },
1606                mip_level_count: 1,
1607                sample_count: 1,
1608                dimension: wgpu::TextureDimension::D2,
1609                format: wgpu::TextureFormat::R8Unorm,
1610                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1611                view_formats: &[],
1612            });
1613            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
1614
1615            let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
1616                label: Some("nv12 UV"),
1617                size: wgpu::Extent3d {
1618                    width: uv_w,
1619                    height: uv_h,
1620                    depth_or_array_layers: 1,
1621                },
1622                mip_level_count: 1,
1623                sample_count: 1,
1624                dimension: wgpu::TextureDimension::D2,
1625                format: wgpu::TextureFormat::Rg8Unorm,
1626                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1627                view_formats: &[],
1628            });
1629            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
1630
1631            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1632                label: Some("nv12 bind"),
1633                layout: &self.image_bind_layout_nv12,
1634                entries: &[
1635                    wgpu::BindGroupEntry {
1636                        binding: 0,
1637                        resource: wgpu::BindingResource::TextureView(&view_y),
1638                    },
1639                    wgpu::BindGroupEntry {
1640                        binding: 1,
1641                        resource: wgpu::BindingResource::TextureView(&view_uv),
1642                    },
1643                    wgpu::BindGroupEntry {
1644                        binding: 2,
1645                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
1646                    },
1647                ],
1648            });
1649
1650            let bytes = (w as u64) * (h as u64) + (uv_w as u64) * (uv_h as u64) * 2;
1651            self.image_bytes_total += bytes;
1652
1653            self.images.insert(
1654                handle,
1655                ImageTex::Nv12 {
1656                    tex_y,
1657                    view_y,
1658                    tex_uv,
1659                    view_uv,
1660                    bind,
1661                    w,
1662                    h,
1663                    full_range,
1664                    last_used_frame: self.frame_index,
1665                    bytes,
1666                },
1667            );
1668        }
1669
1670        let (tex_y, tex_uv, _bind) = match self.images.get(&handle) {
1671            Some(ImageTex::Nv12 {
1672                tex_y,
1673                tex_uv,
1674                bind,
1675                ..
1676            }) => (tex_y, tex_uv, bind),
1677            _ => return Err(anyhow::anyhow!("Handle is not NV12")),
1678        };
1679
1680        self.queue.write_texture(
1681            wgpu::TexelCopyTextureInfo {
1682                texture: tex_y,
1683                mip_level: 0,
1684                origin: wgpu::Origin3d::ZERO,
1685                aspect: wgpu::TextureAspect::All,
1686            },
1687            &y[..y_expected],
1688            wgpu::TexelCopyBufferLayout {
1689                offset: 0,
1690                bytes_per_row: Some(w),
1691                rows_per_image: Some(h),
1692            },
1693            wgpu::Extent3d {
1694                width: w,
1695                height: h,
1696                depth_or_array_layers: 1,
1697            },
1698        );
1699
1700        self.queue.write_texture(
1701            wgpu::TexelCopyTextureInfo {
1702                texture: tex_uv,
1703                mip_level: 0,
1704                origin: wgpu::Origin3d::ZERO,
1705                aspect: wgpu::TextureAspect::All,
1706            },
1707            &uv[..uv_expected],
1708            wgpu::TexelCopyBufferLayout {
1709                offset: 0,
1710                bytes_per_row: Some(2 * uv_w),
1711                rows_per_image: Some(uv_h),
1712            },
1713            wgpu::Extent3d {
1714                width: uv_w,
1715                height: uv_h,
1716                depth_or_array_layers: 1,
1717            },
1718        );
1719
1720        self.evict_budget_excess();
1721        Ok(())
1722    }
1723
1724    pub fn remove_image(&mut self, handle: u64) {
1725        if let Some(img) = self.images.remove(&handle) {
1726            let b = match img {
1727                ImageTex::Rgba { bytes, .. } => bytes,
1728                ImageTex::Nv12 { bytes, .. } => bytes,
1729            };
1730            self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
1731        }
1732    }
1733
1734    // Legacy support from Step 1 instructions (temporary until platform render logic is fully swapped)
1735    pub fn register_image_from_bytes(&mut self, data: &[u8], srgb: bool) -> u64 {
1736        let handle = self.next_image_handle;
1737        self.next_image_handle += 1;
1738        if let Err(e) = self.set_image_from_bytes(handle, data, srgb) {
1739            log::error!("Failed to register image: {e}");
1740        }
1741        handle
1742    }
1743
1744    fn evict_unused_images(&mut self) {
1745        let now = self.frame_index;
1746        let evict_after = self.image_evict_after_frames;
1747
1748        // Time based eviction
1749        let mut to_remove = Vec::new();
1750        for (h, t) in self.images.iter() {
1751            let last = match t {
1752                ImageTex::Rgba {
1753                    last_used_frame, ..
1754                } => *last_used_frame,
1755                ImageTex::Nv12 {
1756                    last_used_frame, ..
1757                } => *last_used_frame,
1758            };
1759            if now.saturating_sub(last) > evict_after {
1760                to_remove.push(*h);
1761            }
1762        }
1763        for h in to_remove {
1764            self.remove_image(h);
1765        }
1766
1767        self.evict_budget_excess();
1768    }
1769
1770    fn evict_budget_excess(&mut self) {
1771        if self.image_bytes_total <= self.image_budget_bytes {
1772            return;
1773        }
1774        // Collect (handle, last_used, bytes)
1775        let mut candidates: Vec<(u64, u64, u64)> = self
1776            .images
1777            .iter()
1778            .map(|(h, t)| {
1779                let (last, bytes) = match t {
1780                    ImageTex::Rgba {
1781                        last_used_frame,
1782                        bytes,
1783                        ..
1784                    } => (*last_used_frame, *bytes),
1785                    ImageTex::Nv12 {
1786                        last_used_frame,
1787                        bytes,
1788                        ..
1789                    } => (*last_used_frame, *bytes),
1790                };
1791                (*h, last, bytes)
1792            })
1793            .collect();
1794
1795        // Sort by last_used ascending (LRU first)
1796        candidates.sort_by_key(|k| k.1);
1797
1798        let now = self.frame_index;
1799        for (h, last, _bytes) in candidates {
1800            if self.image_bytes_total <= self.image_budget_bytes {
1801                break;
1802            }
1803            // Don't evict something used this frame
1804            if last == now {
1805                continue;
1806            }
1807            self.remove_image(h);
1808        }
1809    }
1810
1811    fn recreate_msaa_and_depth_stencil(&mut self) {
1812        if self.msaa_samples > 1 {
1813            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1814                label: Some("msaa color"),
1815                size: wgpu::Extent3d {
1816                    width: self.config.width.max(1),
1817                    height: self.config.height.max(1),
1818                    depth_or_array_layers: 1,
1819                },
1820                mip_level_count: 1,
1821                sample_count: self.msaa_samples,
1822                dimension: wgpu::TextureDimension::D2,
1823                format: self.config.format,
1824                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1825                view_formats: &[],
1826            });
1827            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1828            self.msaa_tex = Some(tex);
1829            self.msaa_view = Some(view);
1830        } else {
1831            self.msaa_tex = None;
1832            self.msaa_view = None;
1833        }
1834
1835        self.depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
1836            label: Some("depth-stencil (stencil clips)"),
1837            size: wgpu::Extent3d {
1838                width: self.config.width.max(1),
1839                height: self.config.height.max(1),
1840                depth_or_array_layers: 1,
1841            },
1842            mip_level_count: 1,
1843            sample_count: self.msaa_samples,
1844            dimension: wgpu::TextureDimension::D2,
1845            format: wgpu::TextureFormat::Depth24PlusStencil8,
1846            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1847            view_formats: &[],
1848        });
1849        self.depth_stencil_view = self
1850            .depth_stencil_tex
1851            .create_view(&wgpu::TextureViewDescriptor::default());
1852    }
1853
1854    fn init_atlas_mask(device: &wgpu::Device) -> anyhow::Result<AtlasA8> {
1855        let size = 1024u32;
1856        let tex = device.create_texture(&wgpu::TextureDescriptor {
1857            label: Some("glyph atlas A8"),
1858            size: wgpu::Extent3d {
1859                width: size,
1860                height: size,
1861                depth_or_array_layers: 1,
1862            },
1863            mip_level_count: 1,
1864            sample_count: 1,
1865            dimension: wgpu::TextureDimension::D2,
1866            format: wgpu::TextureFormat::R8Unorm,
1867            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1868            view_formats: &[],
1869        });
1870        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1871        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1872            label: Some("glyph atlas sampler A8"),
1873            address_mode_u: wgpu::AddressMode::ClampToEdge,
1874            address_mode_v: wgpu::AddressMode::ClampToEdge,
1875            address_mode_w: wgpu::AddressMode::ClampToEdge,
1876            mag_filter: wgpu::FilterMode::Linear,
1877            min_filter: wgpu::FilterMode::Linear,
1878            mipmap_filter: wgpu::MipmapFilterMode::Linear,
1879            ..Default::default()
1880        });
1881
1882        Ok(AtlasA8 {
1883            tex,
1884            view,
1885            sampler,
1886            size,
1887            next_x: 1,
1888            next_y: 1,
1889            row_h: 0,
1890            map: HashMap::new(),
1891        })
1892    }
1893
1894    fn init_atlas_color(device: &wgpu::Device) -> anyhow::Result<AtlasRGBA> {
1895        let size = 1024u32;
1896        let tex = device.create_texture(&wgpu::TextureDescriptor {
1897            label: Some("glyph atlas RGBA"),
1898            size: wgpu::Extent3d {
1899                width: size,
1900                height: size,
1901                depth_or_array_layers: 1,
1902            },
1903            mip_level_count: 1,
1904            sample_count: 1,
1905            dimension: wgpu::TextureDimension::D2,
1906            format: wgpu::TextureFormat::Rgba8UnormSrgb,
1907            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1908            view_formats: &[],
1909        });
1910        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1911        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1912            label: Some("glyph atlas sampler RGBA"),
1913            address_mode_u: wgpu::AddressMode::ClampToEdge,
1914            address_mode_v: wgpu::AddressMode::ClampToEdge,
1915            address_mode_w: wgpu::AddressMode::ClampToEdge,
1916            mag_filter: wgpu::FilterMode::Linear,
1917            min_filter: wgpu::FilterMode::Linear,
1918            mipmap_filter: wgpu::MipmapFilterMode::Linear,
1919            ..Default::default()
1920        });
1921        Ok(AtlasRGBA {
1922            tex,
1923            view,
1924            sampler,
1925            size,
1926            next_x: 1,
1927            next_y: 1,
1928            row_h: 0,
1929            map: HashMap::new(),
1930        })
1931    }
1932
1933    fn get_or_create_layer(
1934        &mut self,
1935        layer_id: u32,
1936        width: u32,
1937        height: u32,
1938        rect: repose_core::Rect,
1939    ) {
1940        let needs_alloc = match self.layer_pool.get(&layer_id) {
1941            Some(lt) => lt.width != width || lt.height != height,
1942            None => true,
1943        };
1944        if !needs_alloc {
1945            return;
1946        }
1947        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1948            label: Some("graphics layer"),
1949            size: wgpu::Extent3d {
1950                width: width.max(1),
1951                height: height.max(1),
1952                depth_or_array_layers: 1,
1953            },
1954            mip_level_count: 1,
1955            sample_count: 1,
1956            dimension: wgpu::TextureDimension::D2,
1957            format: self.config.format,
1958            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
1959            view_formats: &[],
1960        });
1961        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1962        let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1963            label: Some("layer bind"),
1964            layout: &self.image_bind_layout_rgba,
1965            entries: &[
1966                wgpu::BindGroupEntry {
1967                    binding: 0,
1968                    resource: wgpu::BindingResource::TextureView(&view),
1969                },
1970                wgpu::BindGroupEntry {
1971                    binding: 1,
1972                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
1973                },
1974            ],
1975        });
1976        let depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
1977            label: Some("graphics layer depth-stencil"),
1978            size: wgpu::Extent3d {
1979                width: width.max(1),
1980                height: height.max(1),
1981                depth_or_array_layers: 1,
1982            },
1983            mip_level_count: 1,
1984            sample_count: 1,
1985            dimension: wgpu::TextureDimension::D2,
1986            format: wgpu::TextureFormat::Depth24PlusStencil8,
1987            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1988            view_formats: &[],
1989        });
1990        let depth_stencil_view =
1991            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
1992        self.layer_pool.insert(
1993            layer_id,
1994            LayerTarget {
1995                texture: tex,
1996                view,
1997                bind,
1998                depth_stencil_tex,
1999                depth_stencil_view,
2000                width,
2001                height,
2002                rect_px: (rect.x, rect.y, rect.w, rect.h),
2003            },
2004        );
2005    }
2006
2007    fn atlas_bind_group_mask(&self) -> wgpu::BindGroup {
2008        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2009            label: Some("atlas bind"),
2010            layout: &self.text_bind_layout,
2011            entries: &[
2012                wgpu::BindGroupEntry {
2013                    binding: 0,
2014                    resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
2015                },
2016                wgpu::BindGroupEntry {
2017                    binding: 1,
2018                    resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
2019                },
2020            ],
2021        })
2022    }
2023
2024    fn atlas_bind_group_color(&self) -> wgpu::BindGroup {
2025        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2026            label: Some("atlas bind color"),
2027            layout: &self.text_bind_layout,
2028            entries: &[
2029                wgpu::BindGroupEntry {
2030                    binding: 0,
2031                    resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
2032                },
2033                wgpu::BindGroupEntry {
2034                    binding: 1,
2035                    resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
2036                },
2037            ],
2038        })
2039    }
2040
2041    fn upload_glyph_mask(&mut self, key: repose_text::GlyphKey, px: u32) -> Option<GlyphInfo> {
2042        let keyp = (key, px);
2043        if let Some(info) = self.atlas_mask.map.get(&keyp) {
2044            return Some(*info);
2045        }
2046
2047        let gb = repose_text::rasterize(key, px as f32)?;
2048        if gb.w == 0 || gb.h == 0 || gb.data.is_empty() {
2049            return None;
2050        }
2051
2052        let coverage = swash_to_a8_coverage(gb.content, &gb.data)?;
2053
2054        let w = gb.w.max(1);
2055        let h = gb.h.max(1);
2056
2057        if !self.alloc_space_mask(w, h) {
2058            self.grow_mask_and_rebuild();
2059        }
2060        if !self.alloc_space_mask(w, h) {
2061            return None;
2062        }
2063        let x = self.atlas_mask.next_x;
2064        let y = self.atlas_mask.next_y;
2065        self.atlas_mask.next_x += w + 1;
2066        self.atlas_mask.row_h = self.atlas_mask.row_h.max(h + 1);
2067
2068        let layout = wgpu::TexelCopyBufferLayout {
2069            offset: 0,
2070            bytes_per_row: Some(w),
2071            rows_per_image: Some(h),
2072        };
2073        let size = wgpu::Extent3d {
2074            width: w,
2075            height: h,
2076            depth_or_array_layers: 1,
2077        };
2078        self.queue.write_texture(
2079            wgpu::TexelCopyTextureInfoBase {
2080                texture: &self.atlas_mask.tex,
2081                mip_level: 0,
2082                origin: wgpu::Origin3d { x, y, z: 0 },
2083                aspect: wgpu::TextureAspect::All,
2084            },
2085            &coverage,
2086            layout,
2087            size,
2088        );
2089
2090        let info = GlyphInfo {
2091            u0: x as f32 / self.atlas_mask.size as f32,
2092            v0: y as f32 / self.atlas_mask.size as f32,
2093            u1: (x + w) as f32 / self.atlas_mask.size as f32,
2094            v1: (y + h) as f32 / self.atlas_mask.size as f32,
2095            w: w as f32,
2096            h: h as f32,
2097            bearing_x: 0.0,
2098            bearing_y: 0.0,
2099            advance: 0.0,
2100        };
2101        self.atlas_mask.map.insert(keyp, info);
2102        Some(info)
2103    }
2104
2105    fn upload_glyph_color(&mut self, key: repose_text::GlyphKey, px: u32) -> Option<GlyphInfo> {
2106        let keyp = (key, px);
2107        if let Some(info) = self.atlas_color.map.get(&keyp) {
2108            return Some(*info);
2109        }
2110        let gb = repose_text::rasterize(key, px as f32)?;
2111        if !matches!(gb.content, cosmic_text::SwashContent::Color) {
2112            return None;
2113        }
2114        let w = gb.w.max(1);
2115        let h = gb.h.max(1);
2116        if !self.alloc_space_color(w, h) {
2117            self.grow_color_and_rebuild();
2118        }
2119        if !self.alloc_space_color(w, h) {
2120            return None;
2121        }
2122        let x = self.atlas_color.next_x;
2123        let y = self.atlas_color.next_y;
2124        self.atlas_color.next_x += w + 1;
2125        self.atlas_color.row_h = self.atlas_color.row_h.max(h + 1);
2126
2127        let layout = wgpu::TexelCopyBufferLayout {
2128            offset: 0,
2129            bytes_per_row: Some(w * 4),
2130            rows_per_image: Some(h),
2131        };
2132        let size = wgpu::Extent3d {
2133            width: w,
2134            height: h,
2135            depth_or_array_layers: 1,
2136        };
2137        self.queue.write_texture(
2138            wgpu::TexelCopyTextureInfoBase {
2139                texture: &self.atlas_color.tex,
2140                mip_level: 0,
2141                origin: wgpu::Origin3d { x, y, z: 0 },
2142                aspect: wgpu::TextureAspect::All,
2143            },
2144            &gb.data,
2145            layout,
2146            size,
2147        );
2148        let info = GlyphInfo {
2149            u0: x as f32 / self.atlas_color.size as f32,
2150            v0: y as f32 / self.atlas_color.size as f32,
2151            u1: (x + w) as f32 / self.atlas_color.size as f32,
2152            v1: (y + h) as f32 / self.atlas_color.size as f32,
2153            w: w as f32,
2154            h: h as f32,
2155            bearing_x: 0.0,
2156            bearing_y: 0.0,
2157            advance: 0.0,
2158        };
2159        self.atlas_color.map.insert(keyp, info);
2160        Some(info)
2161    }
2162
2163    fn alloc_space_mask(&mut self, w: u32, h: u32) -> bool {
2164        if self.atlas_mask.next_x + w + 1 >= self.atlas_mask.size {
2165            self.atlas_mask.next_x = 1;
2166            self.atlas_mask.next_y += self.atlas_mask.row_h + 1;
2167            self.atlas_mask.row_h = 0;
2168        }
2169        if self.atlas_mask.next_y + h + 1 >= self.atlas_mask.size {
2170            return false;
2171        }
2172        true
2173    }
2174
2175    fn grow_mask_and_rebuild(&mut self) {
2176        let new_size = (self.atlas_mask.size * 2).min(4096);
2177        if new_size == self.atlas_mask.size {
2178            return;
2179        }
2180        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
2181            label: Some("glyph atlas A8 (grown)"),
2182            size: wgpu::Extent3d {
2183                width: new_size,
2184                height: new_size,
2185                depth_or_array_layers: 1,
2186            },
2187            mip_level_count: 1,
2188            sample_count: 1,
2189            dimension: wgpu::TextureDimension::D2,
2190            format: wgpu::TextureFormat::R8Unorm,
2191            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2192            view_formats: &[],
2193        });
2194        self.atlas_mask.tex = tex;
2195        self.atlas_mask.view = self
2196            .atlas_mask
2197            .tex
2198            .create_view(&wgpu::TextureViewDescriptor::default());
2199        self.atlas_mask.size = new_size;
2200        self.atlas_mask.next_x = 1;
2201        self.atlas_mask.next_y = 1;
2202        self.atlas_mask.row_h = 0;
2203        let keys: Vec<(repose_text::GlyphKey, u32)> = self.atlas_mask.map.keys().copied().collect();
2204        self.atlas_mask.map.clear();
2205        for (k, px) in keys {
2206            let _ = self.upload_glyph_mask(k, px);
2207        }
2208    }
2209
2210    fn alloc_space_color(&mut self, w: u32, h: u32) -> bool {
2211        if self.atlas_color.next_x + w + 1 >= self.atlas_color.size {
2212            self.atlas_color.next_x = 1;
2213            self.atlas_color.next_y += self.atlas_color.row_h + 1;
2214            self.atlas_color.row_h = 0;
2215        }
2216        if self.atlas_color.next_y + h + 1 >= self.atlas_color.size {
2217            return false;
2218        }
2219        true
2220    }
2221
2222    fn grow_color_and_rebuild(&mut self) {
2223        let new_size = (self.atlas_color.size * 2).min(4096);
2224        if new_size == self.atlas_color.size {
2225            return;
2226        }
2227        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
2228            label: Some("glyph atlas RGBA (grown)"),
2229            size: wgpu::Extent3d {
2230                width: new_size,
2231                height: new_size,
2232                depth_or_array_layers: 1,
2233            },
2234            mip_level_count: 1,
2235            sample_count: 1,
2236            dimension: wgpu::TextureDimension::D2,
2237            format: wgpu::TextureFormat::Rgba8UnormSrgb,
2238            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2239            view_formats: &[],
2240        });
2241        self.atlas_color.tex = tex;
2242        self.atlas_color.view = self
2243            .atlas_color
2244            .tex
2245            .create_view(&wgpu::TextureViewDescriptor::default());
2246        self.atlas_color.size = new_size;
2247        self.atlas_color.next_x = 1;
2248        self.atlas_color.next_y = 1;
2249        self.atlas_color.row_h = 0;
2250        let keys: Vec<(repose_text::GlyphKey, u32)> =
2251            self.atlas_color.map.keys().copied().collect();
2252        self.atlas_color.map.clear();
2253        for (k, px) in keys {
2254            let _ = self.upload_glyph_color(k, px);
2255        }
2256    }
2257}
2258
2259fn brush_to_instance_fields(brush: &Brush) -> (u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2]) {
2260    match brush {
2261        Brush::Solid(c) => (
2262            0u32,
2263            c.to_linear(),
2264            [0.0, 0.0, 0.0, 0.0],
2265            [0.0, 0.0],
2266            [0.0, 1.0],
2267        ),
2268        Brush::Linear {
2269            start,
2270            end,
2271            start_color,
2272            end_color,
2273        } => (
2274            1u32,
2275            start_color.to_linear(),
2276            end_color.to_linear(),
2277            [start.x, start.y],
2278            [end.x, end.y],
2279        ),
2280        _ => (0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
2281    }
2282}
2283
2284fn brush_to_solid_color(brush: &Brush) -> [f32; 4] {
2285    match brush {
2286        Brush::Solid(c) => c.to_linear(),
2287        Brush::Linear { start_color, .. } => start_color.to_linear(),
2288        _ => [0.0; 4],
2289    }
2290}
2291
2292impl RenderBackend for WgpuBackend {
2293    fn configure_surface(&mut self, width: u32, height: u32) {
2294        if width == 0 || height == 0 {
2295            return;
2296        }
2297        self.config.width = width;
2298        self.config.height = height;
2299        self.surface.configure(&self.device, &self.config);
2300        self.recreate_msaa_and_depth_stencil();
2301    }
2302
2303    fn frame(&mut self, scene: &Scene, _glyph_cfg: GlyphRasterConfig) {
2304        // Frame start maintenance
2305        self.frame_index = self.frame_index.wrapping_add(1);
2306        self.slug_cache.next_frame();
2307
2308        if self.config.width == 0 || self.config.height == 0 {
2309            return;
2310        }
2311        let mut retries = 0u32;
2312        const MAX_RETRIES: u32 = 4;
2313        let frame = loop {
2314            match self.surface.get_current_texture() {
2315                wgpu::CurrentSurfaceTexture::Success(f) => break f,
2316                wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
2317                    log::warn!("suboptimal surface; reconfiguring");
2318                    self.surface.configure(&self.device, &self.config);
2319                    break f;
2320                }
2321                wgpu::CurrentSurfaceTexture::Outdated => {
2322                    retries += 1;
2323                    if retries >= MAX_RETRIES {
2324                        log::warn!(
2325                            "surface outdated persisted after {MAX_RETRIES} retries; skipping frame"
2326                        );
2327                        return;
2328                    }
2329                    log::warn!("surface outdated; reconfiguring");
2330                    self.surface.configure(&self.device, &self.config);
2331                }
2332                wgpu::CurrentSurfaceTexture::Lost => {
2333                    retries += 1;
2334                    if retries >= MAX_RETRIES {
2335                        log::warn!(
2336                            "surface lost persisted after {MAX_RETRIES} retries; skipping frame"
2337                        );
2338                        return;
2339                    }
2340                    log::warn!("surface lost; reconfiguring");
2341                    self.surface.configure(&self.device, &self.config);
2342                }
2343                wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
2344                    request_frame();
2345                    return;
2346                }
2347                wgpu::CurrentSurfaceTexture::Validation => {
2348                    retries += 1;
2349                    if retries >= MAX_RETRIES {
2350                        log::warn!(
2351                            "surface validation persisted after {MAX_RETRIES} retries; skipping frame"
2352                        );
2353                        return;
2354                    }
2355                    self.surface.configure(&self.device, &self.config);
2356                }
2357            }
2358        };
2359
2360        fn to_ndc(x: f32, y: f32, w: f32, h: f32, fb_w: f32, fb_h: f32) -> [f32; 4] {
2361            let x0 = (x / fb_w) * 2.0 - 1.0;
2362            let y0 = 1.0 - (y / fb_h) * 2.0;
2363            let x1 = ((x + w) / fb_w) * 2.0 - 1.0;
2364            let y1 = 1.0 - ((y + h) / fb_h) * 2.0;
2365            let min_x = x0.min(x1);
2366            let min_y = y0.min(y1);
2367            let w_ndc = (x1 - x0).abs();
2368            let h_ndc = (y1 - y0).abs();
2369            [min_x, min_y, w_ndc, h_ndc]
2370        }
2371
2372        /// Convert a local-space rect + transform to NDC center-based position+size and rotation.
2373        fn rect_to_instance_ndc(
2374            rect: repose_core::Rect,
2375            transform: &Transform,
2376            fb_w: f32,
2377            fb_h: f32,
2378        ) -> ([f32; 4], [f32; 2]) {
2379            let cx = rect.x + rect.w * 0.5;
2380            let cy = rect.y + rect.h * 0.5;
2381
2382            // Apply full transform to center
2383            let sx = cx * transform.scale_x;
2384            let sy = cy * transform.scale_y;
2385            let cos_a = transform.rotate.cos();
2386            let sin_a = transform.rotate.sin();
2387            let tx = sx * cos_a - sy * sin_a + transform.translate_x;
2388            let ty = sx * sin_a + sy * cos_a + transform.translate_y;
2389
2390            // NDC center
2391            let ndc_cx = (tx / fb_w) * 2.0 - 1.0;
2392            let ndc_cy = 1.0 - (ty / fb_h) * 2.0;
2393            // NDC size (after scale only, no rotation — rotation is done in shader)
2394            let ndc_w = (rect.w * transform.scale_x / fb_w) * 2.0;
2395            let ndc_h = (rect.h * transform.scale_y / fb_h) * 2.0;
2396
2397            ([ndc_cx, ndc_cy, ndc_w, ndc_h], [cos_a, sin_a])
2398        }
2399
2400        fn to_scissor(r: &repose_core::Rect, fb_w: u32, fb_h: u32) -> (u32, u32, u32, u32) {
2401            let mut x = r.x.floor() as i64;
2402            let mut y = r.y.floor() as i64;
2403            let fb_wi = fb_w as i64;
2404            let fb_hi = fb_h as i64;
2405            x = x.clamp(0, fb_wi.saturating_sub(1));
2406            y = y.clamp(0, fb_hi.saturating_sub(1));
2407            let w_req = r.w.ceil().max(1.0) as i64;
2408            let h_req = r.h.ceil().max(1.0) as i64;
2409            let w = (w_req).min(fb_wi - x).max(1);
2410            let h = (h_req).min(fb_hi - y).max(1);
2411            (x as u32, y as u32, w as u32, h as u32)
2412        }
2413
2414        let fb_w = self.config.width as f32;
2415        let fb_h = self.config.height as f32;
2416
2417        let globals = Globals {
2418            ndc_to_px: [fb_w * 0.5, fb_h * 0.5],
2419            _pad: [0.0, 0.0],
2420        };
2421        self.queue
2422            .write_buffer(&self.globals_buf, 0, bytemuck::bytes_of(&globals));
2423
2424        let mut passes: Vec<Pass> = Vec::with_capacity(1);
2425        let mut current_pass: Pass = Pass {
2426            target: PassTarget::Surface,
2427            initial_scissor: (0, 0, self.config.width, self.config.height),
2428            clear_color: Some([
2429                scene.clear_color.0 as f32 / 255.0,
2430                scene.clear_color.1 as f32 / 255.0,
2431                scene.clear_color.2 as f32 / 255.0,
2432                scene.clear_color.3 as f32 / 255.0,
2433            ]),
2434            cmds: Vec::with_capacity(scene.nodes.len()),
2435        };
2436        let mut target_stack: Vec<PassTarget> = Vec::new();
2437        let mut layer_alphas: Vec<(u32, f32, (u32, u32, u32, u32))> = Vec::new();
2438        let mut current_target_size: (f32, f32) = (fb_w, fb_h);
2439
2440        struct Batch {
2441            rects: Vec<RectInstance>,
2442            borders: Vec<BorderInstance>,
2443            ellipses: Vec<EllipseInstance>,
2444            e_borders: Vec<EllipseBorderInstance>,
2445            arcs: Vec<ArcInstance>,
2446            masks: Vec<GlyphInstance>,
2447            colors: Vec<GlyphInstance>,
2448            nv12s: Vec<Nv12Instance>,
2449        }
2450
2451        impl Batch {
2452            fn new() -> Self {
2453                Self {
2454                    rects: vec![],
2455                    borders: vec![],
2456                    ellipses: vec![],
2457                    e_borders: vec![],
2458                    arcs: vec![],
2459                    masks: vec![],
2460                    colors: vec![],
2461                    nv12s: vec![],
2462                }
2463            }
2464
2465            fn is_empty(&self) -> bool {
2466                self.rects.is_empty()
2467                    && self.borders.is_empty()
2468                    && self.ellipses.is_empty()
2469                    && self.e_borders.is_empty()
2470                    && self.arcs.is_empty()
2471                    && self.masks.is_empty()
2472                    && self.colors.is_empty()
2473                    && self.nv12s.is_empty()
2474            }
2475
2476            fn flush(
2477                &mut self,
2478                pipes: (
2479                    &mut InstancedPipe<RectInstance>,
2480                    &mut InstancedPipe<BorderInstance>,
2481                    &mut InstancedPipe<EllipseInstance>,
2482                    &mut InstancedPipe<EllipseBorderInstance>,
2483                    &mut InstancedPipe<ArcInstance>,
2484                ),
2485                glyph_pipes: (
2486                    &mut InstancedPipe<GlyphInstance>,
2487                    &mut InstancedPipe<GlyphInstance>,
2488                ),
2489                nv12_pipe: &mut InstancedPipe<Nv12Instance>,
2490                device: &wgpu::Device,
2491                queue: &wgpu::Queue,
2492                cmds: &mut Vec<Cmd>,
2493            ) {
2494                let (rects, borders, ellipses, e_borders, arcs) = pipes;
2495                let (masks, colors) = glyph_pipes;
2496
2497                macro_rules! flush_one {
2498                    ($buf:ident, $pipe:expr, $variant:ident) => {
2499                        if !self.$buf.is_empty() {
2500                            if let Some((off, cnt)) = $pipe.upload(device, queue, &self.$buf) {
2501                                cmds.push(Cmd::$variant { off, cnt });
2502                            }
2503                            self.$buf.clear();
2504                        }
2505                    };
2506                }
2507
2508                flush_one!(rects, rects, Rect);
2509                flush_one!(borders, borders, Border);
2510                flush_one!(ellipses, ellipses, Ellipse);
2511                flush_one!(e_borders, e_borders, EllipseBorder);
2512                flush_one!(arcs, arcs, Arc);
2513                flush_one!(masks, masks, GlyphsMask);
2514                flush_one!(colors, colors, GlyphsColor);
2515
2516                if !self.nv12s.is_empty() {
2517                    if let Some((off, cnt)) = nv12_pipe.upload(device, queue, &self.nv12s) {
2518                        let _ = (off, cnt);
2519                    }
2520                    self.nv12s.clear();
2521                }
2522            }
2523        }
2524
2525        self.rects.reset();
2526        self.borders.reset();
2527        self.ellipses.reset();
2528        self.ellipse_borders.reset();
2529        self.arcs.reset();
2530        self.glyph_mask.reset();
2531        self.glyph_color.reset();
2532        self.clip_ring.reset();
2533        self.blur_ring.reset();
2534        self.nv12.reset();
2535
2536        self.slug_ring.reset();
2537        let mut batch = Batch::new();
2538        let mut slug_verts_local: Vec<slug::TessVertex> = Vec::new();
2539        let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
2540        let mut scissor_stack: Vec<repose_core::Rect> = Vec::with_capacity(8);
2541        let root_clip_rect = repose_core::Rect {
2542            x: 0.0,
2543            y: 0.0,
2544            w: fb_w,
2545            h: fb_h,
2546        };
2547
2548        let mut current_prim: Option<&'static str> = None;
2549
2550        macro_rules! flush_if_prim_changed {
2551            ($prim:literal, $pipe:expr) => {
2552                if current_prim != Some($prim) {
2553                    flush_batch!();
2554                    current_prim = Some($prim);
2555                }
2556            };
2557        }
2558
2559        macro_rules! flush_batch {
2560            () => {
2561                if !batch.is_empty() {
2562                    batch.flush(
2563                        (
2564                            &mut self.rects,
2565                            &mut self.borders,
2566                            &mut self.ellipses,
2567                            &mut self.ellipse_borders,
2568                            &mut self.arcs,
2569                        ),
2570                        (&mut self.glyph_mask, &mut self.glyph_color),
2571                        &mut self.nv12,
2572                        &self.device,
2573                        &self.queue,
2574                        &mut current_pass.cmds,
2575                    )
2576                }
2577            };
2578        }
2579
2580        for node in &scene.nodes {
2581            let t_identity = Transform::identity();
2582            let current_transform = transform_stack.last().unwrap_or(&t_identity);
2583
2584            match node {
2585                SceneNode::Rect {
2586                    rect,
2587                    brush,
2588                    radius,
2589                } => {
2590                    flush_if_prim_changed!("rect", &self.rects);
2591                    let (ndc, sin_cos) = rect_to_instance_ndc(
2592                        *rect,
2593                        current_transform,
2594                        current_target_size.0,
2595                        current_target_size.1,
2596                    );
2597                    let (brush_type, color0, color1, grad_start, grad_end) =
2598                        brush_to_instance_fields(brush);
2599                    batch.rects.push(RectInstance {
2600                        xywh: ndc,
2601                        radius: *radius,
2602                        brush_type,
2603                        color0,
2604                        color1,
2605                        grad_start,
2606                        grad_end,
2607                        sin_cos,
2608                    });
2609                }
2610                SceneNode::Border {
2611                    rect,
2612                    color,
2613                    width,
2614                    radius,
2615                } => {
2616                    flush_if_prim_changed!("border", &self.borders);
2617                    let (ndc, sin_cos) = rect_to_instance_ndc(
2618                        *rect,
2619                        current_transform,
2620                        current_target_size.0,
2621                        current_target_size.1,
2622                    );
2623                    batch.borders.push(BorderInstance {
2624                        xywh: ndc,
2625                        radius: *radius,
2626                        stroke: *width,
2627                        color: color.to_linear(),
2628                        sin_cos,
2629                    });
2630                }
2631                SceneNode::Ellipse { rect, brush } => {
2632                    flush_if_prim_changed!("ellipse", &self.ellipses);
2633                    let (ndc, sin_cos) = rect_to_instance_ndc(
2634                        *rect,
2635                        current_transform,
2636                        current_target_size.0,
2637                        current_target_size.1,
2638                    );
2639                    let color = brush_to_solid_color(brush);
2640                    batch.ellipses.push(EllipseInstance {
2641                        xywh: ndc,
2642                        color,
2643                        sin_cos,
2644                    });
2645                }
2646                SceneNode::EllipseBorder { rect, color, width } => {
2647                    flush_if_prim_changed!("ellipse_border", &self.ellipse_borders);
2648                    let (ndc, sin_cos) = rect_to_instance_ndc(
2649                        *rect,
2650                        current_transform,
2651                        current_target_size.0,
2652                        current_target_size.1,
2653                    );
2654                    let pad_px = *width * 0.5 + 2.0;
2655                    let pad = (pad_px / current_target_size.0) * 2.0;
2656                    batch.e_borders.push(EllipseBorderInstance {
2657                        xywh: ndc,
2658                        stroke: *width,
2659                        pad,
2660                        color: color.to_linear(),
2661                        sin_cos,
2662                    });
2663                }
2664                SceneNode::Arc {
2665                    rect,
2666                    start_angle,
2667                    sweep_angle,
2668                    stroke_width,
2669                    color,
2670                    cap,
2671                } => {
2672                    flush_if_prim_changed!("arc", &self.arcs);
2673                    let (ndc, sin_cos) = rect_to_instance_ndc(
2674                        *rect,
2675                        current_transform,
2676                        current_target_size.0,
2677                        current_target_size.1,
2678                    );
2679                    let pad_px = *stroke_width * 0.5 + 2.0;
2680                    let pad = (pad_px / current_target_size.0) * 2.0;
2681                    let cap_val = match cap {
2682                        StrokeCap::Butt => 0.0,
2683                        StrokeCap::Round => 1.0,
2684                        StrokeCap::Square => 2.0,
2685                    };
2686                    batch.arcs.push(ArcInstance {
2687                        xywh: ndc,
2688                        start_angle: *start_angle,
2689                        sweep_angle: *sweep_angle,
2690                        stroke: *stroke_width,
2691                        pad,
2692                        color: color.to_linear(),
2693                        sin_cos,
2694                        cap: cap_val,
2695                    });
2696                }
2697                SceneNode::Text {
2698                    rect,
2699                    text,
2700                    color,
2701                    size,
2702                    font_family,
2703                } => {
2704                    flush_batch!(); // flush any prior primitives
2705
2706                    let px = (*size).clamp(8.0, 96.0);
2707                    let shaped = repose_text::shape_line(text.as_ref(), px, *font_family);
2708
2709                    let cos_a = current_transform.rotate.cos();
2710                    let sin_a = current_transform.rotate.sin();
2711                    let has_rotation = current_transform.rotate != 0.0;
2712
2713                    // For rotated text, the pivot is the center of the text rect.
2714                    let pivot_x = rect.x + rect.w * 0.5;
2715                    let pivot_y = rect.y + rect.h * 0.5;
2716
2717                    // Helper: compute NDC for a glyph rect, handling rotation correctly.
2718                    let make_glyph_instance =
2719                        |gx: f32, gy: f32, gw: f32, gh: f32| -> ([f32; 4], [f32; 2]) {
2720                            if has_rotation {
2721                                let corners =
2722                                    [(gx, gy), (gx + gw, gy), (gx + gw, gy + gh), (gx, gy + gh)];
2723                                let mut min_x = f32::MAX;
2724                                let mut max_x = f32::MIN;
2725                                let mut min_y = f32::MAX;
2726                                let mut max_y = f32::MIN;
2727                                for &(x, y) in &corners {
2728                                    let dx = x - pivot_x;
2729                                    let dy = y - pivot_y;
2730                                    let rx = pivot_x + dx * cos_a - dy * sin_a;
2731                                    let ry = pivot_y + dx * sin_a + dy * cos_a;
2732                                    min_x = min_x.min(rx);
2733                                    max_x = max_x.max(rx);
2734                                    min_y = min_y.min(ry);
2735                                    max_y = max_y.max(ry);
2736                                }
2737                                let bb_w = max_x - min_x;
2738                                let bb_h = max_y - min_y;
2739                                let ndc_tl = to_ndc(
2740                                    min_x,
2741                                    min_y,
2742                                    bb_w,
2743                                    bb_h,
2744                                    current_target_size.0,
2745                                    current_target_size.1,
2746                                );
2747                                let ndc = [
2748                                    ndc_tl[0] + ndc_tl[2] * 0.5,
2749                                    ndc_tl[1] + ndc_tl[3] * 0.5,
2750                                    ndc_tl[2],
2751                                    ndc_tl[3],
2752                                ];
2753                                (ndc, [cos_a, sin_a])
2754                            } else {
2755                                rect_to_instance_ndc(
2756                                    repose_core::Rect {
2757                                        x: gx,
2758                                        y: gy,
2759                                        w: gw,
2760                                        h: gh,
2761                                    },
2762                                    current_transform,
2763                                    current_target_size.0,
2764                                    current_target_size.1,
2765                                )
2766                            }
2767                        };
2768
2769                    for sg in shaped {
2770                        let gx = rect.x + sg.x + sg.bearing_x;
2771                        let gy = rect.y + sg.y - sg.bearing_y;
2772
2773                        // Vector glyph path: tessellated geometry with MSAA.
2774                        if self.slug_enabled {
2775                            let ck = repose_text::lookup_cache_key(sg.key);
2776                            if let Some(ref ck) = ck {
2777                                if !self.slug_cache.contains(ck) {
2778                                    if let Some((ck2, commands)) =
2779                                        repose_text::lookup_and_extract_outline(sg.key)
2780                                    {
2781                                        let font_size_px = f32::from_bits(ck2.font_size_bits);
2782                                        self.slug_cache.get_or_insert(ck2, font_size_px, &commands);
2783                                    }
2784                                }
2785                            }
2786                            if let Some(ref ck) = ck {
2787                                self.slug_cache.touch(ck);
2788                            }
2789                            if let Some(entry) = ck.as_ref().and_then(|ck| self.slug_cache.get(ck))
2790                            {
2791                                let ox = rect.x + sg.x;
2792                                let oy = rect.y + sg.y;
2793                                let scx = current_transform.scale_x;
2794                                let scy = current_transform.scale_y;
2795                                let ttx = current_transform.translate_x;
2796                                let tty = current_transform.translate_y;
2797
2798                                let tf = |x: f32, y: f32| -> (f32, f32) {
2799                                    if has_rotation {
2800                                        let dx = x - pivot_x;
2801                                        let dy = y - pivot_y;
2802                                        let rx = pivot_x + dx * cos_a - dy * sin_a;
2803                                        let ry = pivot_y + dx * sin_a + dy * cos_a;
2804                                        (rx, ry)
2805                                    } else {
2806                                        (x * scx + ttx, y * scy + tty)
2807                                    }
2808                                };
2809
2810                                let tw = current_target_size.0;
2811                                let th = current_target_size.1;
2812
2813                                for &v in &entry.vertices {
2814                                    let (sx, sy) = tf(ox + v[0] * px, oy - v[1] * px);
2815                                    let ndc_x = sx / tw * 2.0 - 1.0;
2816                                    let ndc_y = -(sy / th) * 2.0 + 1.0;
2817                                    slug_verts_local.push(slug::TessVertex {
2818                                        ndc_pos: [ndc_x, ndc_y],
2819                                        color: color.to_linear(),
2820                                    });
2821                                }
2822                                continue;
2823                            }
2824                        }
2825
2826                        // Atlas fallback: color emoji + failed slug extraction
2827                        if let Some(info) = self.upload_glyph_color(sg.key, px as u32) {
2828                            let (ndc, sin_cos) = make_glyph_instance(gx, gy, info.w, info.h);
2829                            batch.colors.push(GlyphInstance {
2830                                xywh: ndc,
2831                                uv: [info.u0, info.v1, info.u1, info.v0],
2832                                color: color.to_linear(),
2833                                sin_cos,
2834                            });
2835                        } else if let Some(info) = self.upload_glyph_mask(sg.key, px as u32) {
2836                            let (ndc, sin_cos) = make_glyph_instance(gx, gy, info.w, info.h);
2837                            batch.masks.push(GlyphInstance {
2838                                xywh: ndc,
2839                                uv: [info.u0, info.v1, info.u1, info.v0],
2840                                color: color.to_linear(),
2841                                sin_cos,
2842                            });
2843                        }
2844                    }
2845
2846                    // Upload slug vertices if any
2847                    if !slug_verts_local.is_empty() {
2848                        let bytes = bytemuck::cast_slice(&slug_verts_local);
2849                        self.slug_ring.grow_to_fit(&self.device, bytes.len() as u64);
2850                        let (off, _) = self.slug_ring.alloc_write(&self.queue, bytes);
2851                        current_pass.cmds.push(Cmd::GlyphsVector {
2852                            off,
2853                            cnt: slug_verts_local.len() as u32,
2854                        });
2855                        slug_verts_local.clear();
2856                    }
2857                }
2858                SceneNode::Image {
2859                    rect,
2860                    handle,
2861                    tint,
2862                    fit,
2863                } => {
2864                    flush_batch!();
2865
2866                    // Update usage timestamp for eviction
2867                    let (img_w, img_h, is_nv12) = if let Some(t) = self.images.get_mut(handle) {
2868                        match t {
2869                            ImageTex::Rgba {
2870                                w,
2871                                h,
2872                                last_used_frame,
2873                                ..
2874                            } => {
2875                                *last_used_frame = self.frame_index;
2876                                (*w, *h, false)
2877                            }
2878                            ImageTex::Nv12 {
2879                                w,
2880                                h,
2881                                last_used_frame,
2882                                ..
2883                            } => {
2884                                *last_used_frame = self.frame_index;
2885                                (*w, *h, true)
2886                            }
2887                        }
2888                    } else {
2889                        log::warn!("Image handle {} not found", handle);
2890                        continue;
2891                    };
2892
2893                    let src_w = img_w as f32;
2894                    let src_h = img_h as f32;
2895                    let transformed = current_transform.apply_to_rect(*rect);
2896                    let dst_w = transformed.w.max(0.0);
2897                    let dst_h = transformed.h.max(0.0);
2898                    if dst_w <= 0.0 || dst_h <= 0.0 {
2899                        continue;
2900                    }
2901
2902                    let (xywh_ndc, uv_rect) = match fit {
2903                        repose_core::view::ImageFit::Contain => {
2904                            let scale = (dst_w / src_w).min(dst_h / src_h);
2905                            let w = src_w * scale;
2906                            let h = src_h * scale;
2907                            let x = transformed.x + (dst_w - w) * 0.5;
2908                            let y = transformed.y + (dst_h - h) * 0.5;
2909                            (
2910                                to_ndc(x, y, w, h, current_target_size.0, current_target_size.1),
2911                                [0.0, 1.0, 1.0, 0.0],
2912                            )
2913                        }
2914                        repose_core::view::ImageFit::Cover => {
2915                            let scale = (dst_w / src_w).max(dst_h / src_h);
2916                            let content_w = src_w * scale;
2917                            let content_h = src_h * scale;
2918                            let overflow_x = (content_w - dst_w) * 0.5;
2919                            let overflow_y = (content_h - dst_h) * 0.5;
2920                            let u0 = (overflow_x / content_w).clamp(0.0, 1.0);
2921                            let v0 = (overflow_y / content_h).clamp(0.0, 1.0);
2922                            let u1 = ((overflow_x + dst_w) / content_w).clamp(0.0, 1.0);
2923                            let v1 = ((overflow_y + dst_h) / content_h).clamp(0.0, 1.0);
2924                            (
2925                                to_ndc(
2926                                    transformed.x,
2927                                    transformed.y,
2928                                    dst_w,
2929                                    dst_h,
2930                                    current_target_size.0,
2931                                    current_target_size.1,
2932                                ),
2933                                [u0, 1.0 - v1, u1, 1.0 - v0],
2934                            )
2935                        }
2936                        repose_core::view::ImageFit::FitWidth => {
2937                            let scale = dst_w / src_w;
2938                            let w = dst_w;
2939                            let h = src_h * scale;
2940                            let y = transformed.y + (dst_h - h) * 0.5;
2941                            (
2942                                to_ndc(
2943                                    transformed.x,
2944                                    y,
2945                                    w,
2946                                    h,
2947                                    current_target_size.0,
2948                                    current_target_size.1,
2949                                ),
2950                                [0.0, 1.0, 1.0, 0.0],
2951                            )
2952                        }
2953                        repose_core::view::ImageFit::FitHeight => {
2954                            let scale = dst_h / src_h;
2955                            let w = src_w * scale;
2956                            let h = dst_h;
2957                            let x = transformed.x + (dst_w - w) * 0.5;
2958                            (
2959                                to_ndc(
2960                                    x,
2961                                    transformed.y,
2962                                    w,
2963                                    h,
2964                                    current_target_size.0,
2965                                    current_target_size.1,
2966                                ),
2967                                [0.0, 1.0, 1.0, 0.0],
2968                            )
2969                        }
2970                        _ => ([0.0; 4], [0.0; 4]),
2971                    };
2972
2973                    // Convert top-left based NDC to center-based for shader
2974                    let ndc_center = [
2975                        xywh_ndc[0] + xywh_ndc[2] * 0.5,
2976                        xywh_ndc[1] + xywh_ndc[3] * 0.5,
2977                        xywh_ndc[2],
2978                        xywh_ndc[3],
2979                    ];
2980
2981                    if is_nv12 {
2982                        let full_range = if let Some(ImageTex::Nv12 { full_range, .. }) =
2983                            self.images.get(handle)
2984                        {
2985                            if *full_range { 1.0 } else { 0.0 }
2986                        } else {
2987                            0.0
2988                        };
2989
2990                        let inst = Nv12Instance {
2991                            xywh: ndc_center,
2992                            uv: uv_rect,
2993                            color: tint.to_linear(),
2994                            full_range,
2995                            sin_cos: [1.0, 0.0],
2996                            _pad: [0.0],
2997                        };
2998                        if let Some((off, _)) = self.nv12.upload(&self.device, &self.queue, &[inst])
2999                        {
3000                            current_pass.cmds.push(Cmd::ImageNv12 {
3001                                off,
3002                                cnt: 1,
3003                                handle: *handle,
3004                            });
3005                        }
3006                    } else {
3007                        // RGBA uses GlyphInstance struct (reused pipeline)
3008                        let inst = GlyphInstance {
3009                            xywh: ndc_center,
3010                            uv: uv_rect,
3011                            color: tint.to_linear(),
3012                            sin_cos: [1.0, 0.0],
3013                        };
3014                        if let Some((off, _)) =
3015                            self.glyph_color.upload(&self.device, &self.queue, &[inst])
3016                        {
3017                            current_pass.cmds.push(Cmd::ImageRgba {
3018                                off,
3019                                cnt: 1,
3020                                handle: *handle,
3021                            });
3022                        }
3023                    }
3024                }
3025                SceneNode::PushClip { rect, radius } => {
3026                    flush_batch!(); // flush content before entering clip
3027
3028                    let t_identity = Transform::identity();
3029                    let current_transform = transform_stack.last().unwrap_or(&t_identity);
3030                    let transformed = current_transform.apply_to_rect(*rect);
3031
3032                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
3033                    let next_scissor = intersect(top, transformed);
3034                    scissor_stack.push(next_scissor);
3035                    let scissor = to_scissor(
3036                        &next_scissor,
3037                        current_target_size.0 as u32,
3038                        current_target_size.1 as u32,
3039                    );
3040
3041                    let clip_ndc_tl = to_ndc(
3042                        transformed.x,
3043                        transformed.y,
3044                        transformed.w,
3045                        transformed.h,
3046                        current_target_size.0,
3047                        current_target_size.1,
3048                    );
3049                    let inst = ClipInstance {
3050                        xywh: [
3051                            clip_ndc_tl[0] + clip_ndc_tl[2] * 0.5,
3052                            clip_ndc_tl[1] + clip_ndc_tl[3] * 0.5,
3053                            clip_ndc_tl[2],
3054                            clip_ndc_tl[3],
3055                        ],
3056                        radius: *radius,
3057                        sin_cos: [1.0, 0.0],
3058                    };
3059                    let bytes = bytemuck::bytes_of(&inst);
3060                    self.clip_ring.grow_to_fit(&self.device, bytes.len() as u64);
3061                    let (off, _) = self.clip_ring.alloc_write(&self.queue, bytes);
3062
3063                    current_pass.cmds.push(Cmd::ClipPush {
3064                        off,
3065                        cnt: 1,
3066                        scissor,
3067                    });
3068                }
3069                SceneNode::PopClip => {
3070                    flush_batch!();
3071
3072                    if !scissor_stack.is_empty() {
3073                        scissor_stack.pop();
3074                    } else {
3075                        log::warn!("PopClip with empty stack");
3076                    }
3077
3078                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
3079                    let scissor = to_scissor(
3080                        &top,
3081                        current_target_size.0 as u32,
3082                        current_target_size.1 as u32,
3083                    );
3084                    current_pass.cmds.push(Cmd::ClipPop { scissor });
3085                }
3086                SceneNode::Shadow {
3087                    rect,
3088                    radius,
3089                    elevation: _,
3090                    color,
3091                } => {
3092                    flush_if_prim_changed!("rect", &self.rects);
3093                    let (ndc, sin_cos) = rect_to_instance_ndc(
3094                        *rect,
3095                        current_transform,
3096                        current_target_size.0,
3097                        current_target_size.1,
3098                    );
3099                    let (brush_type, color0, _color1, _grad_start, _grad_end) =
3100                        brush_to_instance_fields(&Brush::Solid(*color));
3101                    batch.rects.push(RectInstance {
3102                        xywh: ndc,
3103                        radius: *radius,
3104                        brush_type,
3105                        color0,
3106                        color1: [0.0; 4],
3107                        grad_start: [0.0; 2],
3108                        grad_end: [0.0; 2],
3109                        sin_cos,
3110                    });
3111                }
3112                SceneNode::PushTransform { transform } => {
3113                    flush_batch!(); // flush before transform change
3114                    let combined = current_transform.combine(transform);
3115                    transform_stack.push(combined);
3116                }
3117                SceneNode::PopTransform => {
3118                    flush_batch!(); // flush before transform change
3119                    transform_stack.pop();
3120                }
3121                SceneNode::BeginLayer {
3122                    rect,
3123                    layer_id,
3124                    alpha,
3125                } => {
3126                    flush_batch!();
3127                    let w = (rect.w.max(1.0)).ceil() as u32;
3128                    let h = (rect.h.max(1.0)).ceil() as u32;
3129                    // Close out the current pass, start a new one for the layer.
3130                    let prev_target = current_pass.target;
3131                    let prev_scissor = current_pass.initial_scissor;
3132                    let saved = std::mem::replace(
3133                        &mut current_pass,
3134                        Pass {
3135                            target: PassTarget::Layer(*layer_id),
3136                            initial_scissor: (0, 0, w, h),
3137                            clear_color: Some([0.0, 0.0, 0.0, 0.0]),
3138                            cmds: Vec::new(),
3139                        },
3140                    );
3141                    passes.push(saved);
3142                    target_stack.push(prev_target);
3143                    let _ = prev_scissor; // initial_scissor of resumed pass is restored at EndLayer
3144                    // Get or create the layer's offscreen texture now so that
3145                    // subsequent scissor ops / draws have a valid target.
3146                    self.get_or_create_layer(*layer_id, w, h, *rect);
3147                    current_target_size = (w as f32, h as f32);
3148                    layer_alphas.push((*layer_id, *alpha, current_pass.initial_scissor));
3149                }
3150                SceneNode::EndLayer { layer_id } => {
3151                    flush_batch!();
3152                    // Finish the layer's pass, start a new one on the previous target.
3153                    let saved = std::mem::replace(
3154                        &mut current_pass,
3155                        Pass {
3156                            target: target_stack.pop().unwrap_or(PassTarget::Surface),
3157                            initial_scissor: (0, 0, self.config.width, self.config.height),
3158                            clear_color: None, // LoadOp::Load - don't wipe earlier surface content
3159                            cmds: Vec::new(),
3160                        },
3161                    );
3162                    passes.push(saved);
3163                    current_target_size = (fb_w, fb_h);
3164                    // Issue a composite quad for the just-finished layer in the new pass.
3165                    if let Some((_, layer_alpha, _)) = layer_alphas
3166                        .iter()
3167                        .find(|(id, _, _)| id == layer_id)
3168                        .copied()
3169                    {
3170                        let layer = self.layer_pool.get(layer_id).expect("layer target");
3171                        let ndc_tl = to_ndc(
3172                            layer.rect_px.0,
3173                            layer.rect_px.1,
3174                            layer.rect_px.2,
3175                            layer.rect_px.3,
3176                            fb_w,
3177                            fb_h,
3178                        );
3179                        let inst = GlyphInstance {
3180                            xywh: [
3181                                ndc_tl[0] + ndc_tl[2] * 0.5,
3182                                ndc_tl[1] + ndc_tl[3] * 0.5,
3183                                ndc_tl[2],
3184                                ndc_tl[3],
3185                            ],
3186                            uv: [0.0, 1.0, 1.0, 0.0],
3187                            color: [1.0, 1.0, 1.0, layer_alpha],
3188                            sin_cos: [1.0, 0.0],
3189                        };
3190                        if let Some((off, cnt)) =
3191                            self.glyph_color.upload(&self.device, &self.queue, &[inst])
3192                        {
3193                            current_pass.cmds.push(Cmd::CompositeLayer {
3194                                off,
3195                                cnt,
3196                                layer_id: *layer_id,
3197                                alpha: layer_alpha,
3198                            });
3199                        }
3200                    }
3201                }
3202                SceneNode::CompositeShadow {
3203                    layer_id,
3204                    blur_px,
3205                    offset_px,
3206                    color,
3207                } => {
3208                    flush_batch!();
3209                    if let Some(layer) = self.layer_pool.get(layer_id).cloned() {
3210                        // Shadow rect = layer rect + offset.
3211                        let sx = layer.rect_px.0 + offset_px.0;
3212                        let sy = layer.rect_px.1 + offset_px.1;
3213                        let sw = layer.rect_px.2;
3214                        let sh = layer.rect_px.3;
3215                        // The blur in UV space is 1.5 * blur_px / texture_size
3216                        // (the 1.5 matches the 3x3 Gaussian span).
3217                        let bw_uv = (blur_px * 1.5) / layer.width.max(1) as f32;
3218                        let bh_uv = (blur_px * 1.5) / layer.height.max(1) as f32;
3219                        let ndc_tl = to_ndc(sx, sy, sw, sh, fb_w, fb_h);
3220                        let inst = BlurInstance {
3221                            xywh: [
3222                                ndc_tl[0] + ndc_tl[2] * 0.5,
3223                                ndc_tl[1] + ndc_tl[3] * 0.5,
3224                                ndc_tl[2],
3225                                ndc_tl[3],
3226                            ],
3227                            uv: [0.0, 0.0, 1.0, 1.0],
3228                            color: [
3229                                color.0 as f32 / 255.0,
3230                                color.1 as f32 / 255.0,
3231                                color.2 as f32 / 255.0,
3232                                color.3 as f32 / 255.0,
3233                            ],
3234                            blur_uv: [bw_uv, bh_uv],
3235                            sin_cos: [1.0, 0.0],
3236                        };
3237                        self.blur_ring
3238                            .grow_to_fit(&self.device, std::mem::size_of::<BlurInstance>() as u64);
3239                        let bytes = bytemuck::bytes_of(&inst);
3240                        let (off, _) = self.blur_ring.alloc_write(&self.queue, bytes);
3241                        current_pass.cmds.push(Cmd::CompositeShadow {
3242                            off,
3243                            cnt: 1,
3244                            layer_id: *layer_id,
3245                        });
3246                    }
3247                }
3248                _ => {}
3249            }
3250        }
3251
3252        flush_batch!();
3253
3254        // Push the final pass.
3255        passes.push(current_pass);
3256
3257        let mut encoder = self
3258            .device
3259            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
3260                label: Some("frame encoder"),
3261            });
3262
3263        let bind_mask = self.atlas_bind_group_mask();
3264        let bind_color = self.atlas_bind_group_color();
3265        let mut clip_depth: u32 = 0;
3266
3267        for pass in std::mem::take(&mut passes) {
3268            let (color_view, resolve_target, depth_stencil_view, is_layer) = match pass.target {
3269                PassTarget::Surface => {
3270                    let swap_view = frame
3271                        .texture
3272                        .create_view(&wgpu::TextureViewDescriptor::default());
3273                    let (color, resolve) = if let Some(msaa_view) = &self.msaa_view {
3274                        (msaa_view.clone(), Some(swap_view))
3275                    } else {
3276                        (swap_view, None)
3277                    };
3278                    (color, resolve, self.depth_stencil_view.clone(), false)
3279                }
3280                PassTarget::Layer(layer_id) => {
3281                    if let Some(lt) = self.layer_pool.get(&layer_id) {
3282                        (lt.view.clone(), None, lt.depth_stencil_view.clone(), true)
3283                    } else {
3284                        log::warn!("missing layer target {layer_id}");
3285                        continue;
3286                    }
3287                }
3288            };
3289
3290            if is_layer {
3291                clip_depth = 0;
3292            }
3293
3294            let pipes: &Pipelines = if is_layer {
3295                &self.layer_pipes
3296            } else {
3297                &self.surface_pipes
3298            };
3299
3300            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3301                label: Some("pass"),
3302                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3303                    view: &color_view,
3304                    resolve_target: resolve_target.as_ref(),
3305                    ops: wgpu::Operations {
3306                        load: match pass.clear_color {
3307                            Some(c) => wgpu::LoadOp::Clear(wgpu::Color {
3308                                r: c[0] as f64,
3309                                g: c[1] as f64,
3310                                b: c[2] as f64,
3311                                a: c[3] as f64,
3312                            }),
3313                            None => wgpu::LoadOp::Load,
3314                        },
3315                        store: wgpu::StoreOp::Store,
3316                    },
3317                    depth_slice: None,
3318                })],
3319                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
3320                    view: &depth_stencil_view,
3321                    depth_ops: None,
3322                    stencil_ops: Some(wgpu::Operations {
3323                        load: if is_layer || pass.clear_color.is_some() {
3324                            wgpu::LoadOp::Clear(0)
3325                        } else {
3326                            wgpu::LoadOp::Load
3327                        },
3328                        store: wgpu::StoreOp::Store,
3329                    }),
3330                }),
3331                timestamp_writes: None,
3332                occlusion_query_set: None,
3333                multiview_mask: None,
3334            });
3335
3336            rpass.set_bind_group(0, &self.globals_bind, &[]);
3337            rpass.set_stencil_reference(clip_depth);
3338            rpass.set_scissor_rect(
3339                pass.initial_scissor.0,
3340                pass.initial_scissor.1,
3341                pass.initial_scissor.2,
3342                pass.initial_scissor.3,
3343            );
3344
3345            macro_rules! draw_simple {
3346                ($pipeline:expr, $ring:expr, $inst:ty, $off:ident, $n:ident) => {{
3347                    rpass.set_pipeline($pipeline);
3348                    let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
3349                    rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
3350                    rpass.draw(0..6, 0..$n);
3351                }};
3352            }
3353
3354            macro_rules! draw_with_bind {
3355                ($pipeline:expr, $ring:expr, $inst:ty, $bind:expr, $off:ident, $n:ident) => {{
3356                    rpass.set_pipeline($pipeline);
3357                    rpass.set_bind_group(1, $bind, &[]);
3358                    let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
3359                    rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
3360                    rpass.draw(0..6, 0..$n);
3361                }};
3362            }
3363
3364            for cmd in pass.cmds {
3365                match cmd {
3366                    Cmd::ClipPush {
3367                        off,
3368                        cnt: n,
3369                        scissor,
3370                    } => {
3371                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
3372                        rpass.set_stencil_reference(clip_depth);
3373
3374                        if self.msaa_samples > 1 && !is_layer {
3375                            rpass.set_pipeline(&pipes.clip_a2c);
3376                        } else {
3377                            rpass.set_pipeline(&pipes.clip_bin);
3378                        }
3379
3380                        let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
3381                        rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
3382                        rpass.draw(0..6, 0..n);
3383
3384                        clip_depth = (clip_depth + 1).min(255);
3385                        rpass.set_stencil_reference(clip_depth);
3386                    }
3387
3388                    Cmd::ClipPop { scissor } => {
3389                        clip_depth = clip_depth.saturating_sub(1);
3390                        rpass.set_stencil_reference(clip_depth);
3391                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
3392                    }
3393
3394                    Cmd::Rect { off, cnt: n } => {
3395                        draw_simple!(&pipes.rects, self.rects.ring, RectInstance, off, n);
3396                    }
3397
3398                    Cmd::Border { off, cnt: n } => {
3399                        draw_simple!(&pipes.borders, self.borders.ring, BorderInstance, off, n);
3400                    }
3401
3402                    Cmd::GlyphsMask { off, cnt: n } => {
3403                        draw_with_bind!(
3404                            &pipes.text_mask,
3405                            self.glyph_mask.ring,
3406                            GlyphInstance,
3407                            &bind_mask,
3408                            off,
3409                            n
3410                        );
3411                    }
3412
3413                    Cmd::GlyphsColor { off, cnt: n } => {
3414                        draw_with_bind!(
3415                            &pipes.text_color,
3416                            self.glyph_color.ring,
3417                            GlyphInstance,
3418                            &bind_color,
3419                            off,
3420                            n
3421                        );
3422                    }
3423
3424                    Cmd::GlyphsVector { off, cnt: n } => {
3425                        if let Some(ref slug_pipe) = pipes.slug.as_ref() {
3426                            rpass.set_pipeline(slug_pipe);
3427                            let bytes = (n as u64) * std::mem::size_of::<slug::TessVertex>() as u64;
3428                            rpass.set_vertex_buffer(0, self.slug_ring.buf.slice(off..off + bytes));
3429                            rpass.draw(0..n, 0..1);
3430                        }
3431                    }
3432
3433                    Cmd::ImageRgba {
3434                        off,
3435                        cnt: n,
3436                        handle,
3437                    } => {
3438                        if let Some(ImageTex::Rgba { bind, .. }) = self.images.get(&handle) {
3439                            draw_with_bind!(
3440                                &pipes.image_rgba,
3441                                self.glyph_color.ring,
3442                                GlyphInstance,
3443                                bind,
3444                                off,
3445                                n
3446                            );
3447                        }
3448                    }
3449
3450                    Cmd::ImageNv12 {
3451                        off,
3452                        cnt: n,
3453                        handle,
3454                    } => {
3455                        if let Some(ImageTex::Nv12 { bind, .. }) = self.images.get(&handle) {
3456                            draw_with_bind!(
3457                                &pipes.image_nv12,
3458                                self.nv12.ring,
3459                                Nv12Instance,
3460                                bind,
3461                                off,
3462                                n
3463                            );
3464                        }
3465                    }
3466
3467                    Cmd::Ellipse { off, cnt: n } => {
3468                        draw_simple!(&pipes.ellipses, self.ellipses.ring, EllipseInstance, off, n);
3469                    }
3470
3471                    Cmd::EllipseBorder { off, cnt: n } => {
3472                        draw_simple!(
3473                            &pipes.ellipse_borders,
3474                            self.ellipse_borders.ring,
3475                            EllipseBorderInstance,
3476                            off,
3477                            n
3478                        );
3479                    }
3480
3481                    Cmd::Arc { off, cnt: n } => {
3482                        draw_simple!(&pipes.arcs, self.arcs.ring, ArcInstance, off, n);
3483                    }
3484
3485                    Cmd::PushTransform(_) => {}
3486                    Cmd::PopTransform => {}
3487                    Cmd::CompositeLayer {
3488                        off,
3489                        cnt: n,
3490                        layer_id,
3491                        alpha: _,
3492                    } => {
3493                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
3494                            draw_with_bind!(
3495                                &pipes.image_rgba,
3496                                self.glyph_color.ring,
3497                                GlyphInstance,
3498                                &lt.bind,
3499                                off,
3500                                n
3501                            );
3502                        }
3503                    }
3504                    Cmd::CompositeShadow {
3505                        off,
3506                        cnt: n,
3507                        layer_id,
3508                    } => {
3509                        if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
3510                            draw_with_bind!(
3511                                &pipes.blur,
3512                                self.blur_ring,
3513                                BlurInstance,
3514                                &lt.bind,
3515                                off,
3516                                n
3517                            );
3518                        }
3519                    }
3520                }
3521            }
3522        }
3523
3524        self.queue.submit(std::iter::once(encoder.finish()));
3525        if let Err(e) = catch_unwind(AssertUnwindSafe(|| frame.present())) {
3526            log::warn!("frame.present panicked: {:?}", e);
3527        }
3528
3529        // Frame end maintenance: Evict unused images
3530        self.evict_unused_images();
3531    }
3532}
3533
3534fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> repose_core::Rect {
3535    let x0 = a.x.max(b.x);
3536    let y0 = a.y.max(b.y);
3537    let x1 = (a.x + a.w).min(b.x + b.w);
3538    let y1 = (a.y + a.h).min(b.y + b.h);
3539    repose_core::Rect {
3540        x: x0,
3541        y: y0,
3542        w: (x1 - x0).max(0.0),
3543        h: (y1 - y0).max(0.0),
3544    }
3545}