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