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