Skip to main content

repose_render_wgpu/
lib.rs

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