Skip to main content

repose_render_wgpu/
lib.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use std::{borrow::Cow, sync::Once};
4
5use repose_core::{Brush, GlyphRasterConfig, RenderBackend, Scene, SceneNode, Transform};
6use std::panic::{AssertUnwindSafe, catch_unwind};
7use wgpu::Instance;
8
9static ROT_WARN_ONCE: Once = Once::new();
10
11#[derive(Clone)]
12struct UploadRing {
13    buf: wgpu::Buffer,
14    cap: u64,
15    head: u64,
16}
17
18impl UploadRing {
19    fn new(device: &wgpu::Device, label: &str, cap: u64) -> Self {
20        let buf = device.create_buffer(&wgpu::BufferDescriptor {
21            label: Some(label),
22            size: cap,
23            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
24            mapped_at_creation: false,
25        });
26        Self { buf, cap, head: 0 }
27    }
28
29    fn reset(&mut self) {
30        self.head = 0;
31    }
32
33    fn grow_to_fit(&mut self, device: &wgpu::Device, needed: u64) {
34        if needed <= self.cap {
35            return;
36        }
37        let new_cap = needed.next_power_of_two();
38        self.buf = device.create_buffer(&wgpu::BufferDescriptor {
39            label: Some("upload ring (grown)"),
40            size: new_cap,
41            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
42            mapped_at_creation: false,
43        });
44        self.cap = new_cap;
45        self.head = 0;
46    }
47
48    fn alloc_write(&mut self, queue: &wgpu::Queue, bytes: &[u8]) -> (u64, u64) {
49        let len = bytes.len() as u64;
50        let start = (self.head + 3) & !3; // align to 4
51        let end = start + len;
52        debug_assert!(end <= self.cap, "ring overflow - call grow_to_fit first");
53        queue.write_buffer(&self.buf, start, bytes);
54        self.head = end;
55        (start, len)
56    }
57}
58
59struct InstancedPipe<I: bytemuck::Pod> {
60    pipeline: wgpu::RenderPipeline,
61    ring: UploadRing,
62    stride: u64,
63    _marker: std::marker::PhantomData<I>,
64}
65
66impl<I: bytemuck::Pod> InstancedPipe<I> {
67    fn new(pipeline: wgpu::RenderPipeline, ring: UploadRing) -> Self {
68        Self {
69            pipeline,
70            ring,
71            stride: std::mem::size_of::<I>() as u64,
72            _marker: std::marker::PhantomData,
73        }
74    }
75
76    fn upload(
77        &mut self,
78        device: &wgpu::Device,
79        queue: &wgpu::Queue,
80        data: &[I],
81    ) -> Option<(u64, u32)> {
82        if data.is_empty() {
83            return None;
84        }
85        let bytes = bytemuck::cast_slice(data);
86        self.ring.grow_to_fit(device, bytes.len() as u64);
87        let (off, wrote) = self.ring.alloc_write(queue, bytes);
88        debug_assert_eq!(wrote as usize, bytes.len());
89        Some((off, data.len() as u32))
90    }
91
92    fn bind<'a>(&'a self, rpass: &mut wgpu::RenderPass<'a>, off: u64, cnt: u32) {
93        let bytes = (cnt as u64) * self.stride;
94        rpass.set_pipeline(&self.pipeline);
95        rpass.set_vertex_buffer(0, self.ring.buf.slice(off..off + bytes));
96        rpass.draw(0..6, 0..cnt);
97    }
98
99    fn reset(&mut self) {
100        self.ring.reset();
101    }
102}
103
104#[repr(C)]
105#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
106struct Globals {
107    ndc_to_px: [f32; 2],
108    _pad: [f32; 2],
109}
110
111pub struct WgpuBackend {
112    surface: wgpu::Surface<'static>,
113    device: wgpu::Device,
114    queue: wgpu::Queue,
115    config: wgpu::SurfaceConfiguration,
116
117    // Instanced draw pipes
118    rects: InstancedPipe<RectInstance>,
119    borders: InstancedPipe<BorderInstance>,
120    ellipses: InstancedPipe<EllipseInstance>,
121    ellipse_borders: InstancedPipe<EllipseBorderInstance>,
122    glyph_mask: InstancedPipe<GlyphInstance>,
123    glyph_color: InstancedPipe<GlyphInstance>,
124
125    // Not instanced
126    image_pipeline_rgba: wgpu::RenderPipeline,
127    image_pipeline_nv12: wgpu::RenderPipeline,
128    image_bind_layout_rgba: wgpu::BindGroupLayout,
129    image_bind_layout_nv12: wgpu::BindGroupLayout,
130    image_sampler: wgpu::Sampler,
131
132    text_bind_layout: wgpu::BindGroupLayout,
133
134    // Stencil clip pipelines
135    clip_pipeline_a2c: wgpu::RenderPipeline,
136    clip_pipeline_bin: wgpu::RenderPipeline,
137    clip_ring: UploadRing,
138
139    // Instanced
140    nv12: InstancedPipe<Nv12Instance>,
141
142    msaa_samples: u32,
143
144    // Depth-stencil target
145    depth_stencil_tex: wgpu::Texture,
146    depth_stencil_view: wgpu::TextureView,
147
148    // Optional MSAA color target
149    msaa_tex: Option<wgpu::Texture>,
150    msaa_view: Option<wgpu::TextureView>,
151
152    globals_layout: wgpu::BindGroupLayout,
153    globals_buf: wgpu::Buffer,
154    globals_bind: wgpu::BindGroup,
155
156    // Glyph atlas
157    atlas_mask: AtlasA8,
158    atlas_color: AtlasRGBA,
159
160    // Image management
161    next_image_handle: u64,
162    images: HashMap<u64, ImageTex>,
163
164    // Eviction stats
165    frame_index: u64,
166    image_bytes_total: u64,
167    image_evict_after_frames: u64,
168    image_budget_bytes: u64,
169}
170
171enum ImageTex {
172    Rgba {
173        tex: wgpu::Texture,
174        view: wgpu::TextureView,
175        bind: wgpu::BindGroup,
176        w: u32,
177        h: u32,
178        format: wgpu::TextureFormat,
179        last_used_frame: u64,
180        bytes: u64,
181    },
182    Nv12 {
183        tex_y: wgpu::Texture,
184        view_y: wgpu::TextureView,
185        tex_uv: wgpu::Texture,
186        view_uv: wgpu::TextureView,
187        bind: wgpu::BindGroup,
188        w: u32,
189        h: u32,
190        full_range: bool,
191        last_used_frame: u64,
192        bytes: u64,
193    },
194}
195
196struct AtlasA8 {
197    tex: wgpu::Texture,
198    view: wgpu::TextureView,
199    sampler: wgpu::Sampler,
200    size: u32,
201    next_x: u32,
202    next_y: u32,
203    row_h: u32,
204    map: HashMap<(repose_text::GlyphKey, u32), GlyphInfo>,
205}
206
207struct AtlasRGBA {
208    tex: wgpu::Texture,
209    view: wgpu::TextureView,
210    sampler: wgpu::Sampler,
211    size: u32,
212    next_x: u32,
213    next_y: u32,
214    row_h: u32,
215    map: HashMap<(repose_text::GlyphKey, u32), GlyphInfo>,
216}
217
218#[derive(Clone, Copy)]
219struct GlyphInfo {
220    u0: f32,
221    v0: f32,
222    u1: f32,
223    v1: f32,
224    w: f32,
225    h: f32,
226    bearing_x: f32,
227    bearing_y: f32,
228    advance: f32,
229}
230
231#[repr(C)]
232#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
233struct RectInstance {
234    xywh: [f32; 4],
235    radius: f32,
236    brush_type: u32,
237    color0: [f32; 4],
238    color1: [f32; 4],
239    grad_start: [f32; 2],
240    grad_end: [f32; 2],
241}
242
243#[repr(C)]
244#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
245struct BorderInstance {
246    xywh: [f32; 4],
247    radius: f32,
248    stroke: f32,
249    color: [f32; 4],
250}
251
252#[repr(C)]
253#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
254struct EllipseInstance {
255    xywh: [f32; 4],
256    color: [f32; 4],
257}
258
259#[repr(C)]
260#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
261struct EllipseBorderInstance {
262    xywh: [f32; 4],
263    stroke: f32,
264    color: [f32; 4],
265}
266
267#[repr(C)]
268#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
269struct GlyphInstance {
270    xywh: [f32; 4],
271    uv: [f32; 4],
272    color: [f32; 4],
273}
274
275#[repr(C)]
276#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
277struct Nv12Instance {
278    xywh: [f32; 4],
279    uv: [f32; 4],
280    color: [f32; 4], // tint
281    full_range: f32,
282    _pad: [f32; 3],
283}
284
285#[repr(C)]
286#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
287struct ClipInstance {
288    xywh: [f32; 4],
289    radius: f32,
290    _pad: [f32; 3],
291}
292
293fn swash_to_a8_coverage(content: cosmic_text::SwashContent, data: &[u8]) -> Option<Vec<u8>> {
294    match content {
295        cosmic_text::SwashContent::Mask => Some(data.to_vec()),
296        cosmic_text::SwashContent::SubpixelMask => {
297            let mut out = Vec::with_capacity(data.len() / 4);
298            for px in data.chunks_exact(4) {
299                let r = px[0];
300                let g = px[1];
301                let b = px[2];
302                out.push(r.max(g).max(b));
303            }
304            Some(out)
305        }
306        cosmic_text::SwashContent::Color => None,
307    }
308}
309
310impl WgpuBackend {
311    pub async fn new_async(window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
312        let instance: Instance;
313
314        if cfg!(target_arch = "wasm32") {
315            let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
316            desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
317            instance = wgpu::util::new_instance_with_webgpu_detection(desc).await;
318        } else {
319            instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
320        };
321
322        let surface = instance.create_surface(window.clone())?;
323
324        let adapter = instance
325            .request_adapter(&wgpu::RequestAdapterOptions {
326                power_preference: wgpu::PowerPreference::HighPerformance,
327                compatible_surface: Some(&surface),
328                force_fallback_adapter: false,
329            })
330            .await
331            .map_err(|e| anyhow::anyhow!("No suitable adapter: {e:?}"))?;
332
333        let limits = if cfg!(target_arch = "wasm32") {
334            wgpu::Limits::downlevel_webgl2_defaults()
335        } else {
336            wgpu::Limits::default()
337        };
338
339        let (device, queue) = adapter
340            .request_device(&wgpu::DeviceDescriptor {
341                label: Some("repose-rs device"),
342                required_features: wgpu::Features::empty(),
343                required_limits: limits,
344                experimental_features: wgpu::ExperimentalFeatures::disabled(),
345                memory_hints: wgpu::MemoryHints::default(),
346                trace: wgpu::Trace::Off,
347            })
348            .await
349            .map_err(|e| anyhow::anyhow!("request_device failed: {e:?}"))?;
350
351        let size = window.inner_size();
352
353        let caps = surface.get_capabilities(&adapter);
354        let format = caps
355            .formats
356            .iter()
357            .copied()
358            .find(|f| f.is_srgb())
359            .unwrap_or(caps.formats[0]);
360        let present_mode = caps
361            .present_modes
362            .iter()
363            .copied()
364            .find(|m| *m == wgpu::PresentMode::Mailbox || *m == wgpu::PresentMode::Immediate)
365            .unwrap_or(wgpu::PresentMode::Fifo);
366        let alpha_mode = caps.alpha_modes[0];
367
368        let config = wgpu::SurfaceConfiguration {
369            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
370            format,
371            width: size.width.max(1),
372            height: size.height.max(1),
373            present_mode,
374            alpha_mode,
375            view_formats: vec![],
376            desired_maximum_frame_latency: 2,
377        };
378        surface.configure(&device, &config);
379
380        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
381            label: Some("globals layout"),
382            entries: &[wgpu::BindGroupLayoutEntry {
383                binding: 0,
384                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
385                ty: wgpu::BindingType::Buffer {
386                    ty: wgpu::BufferBindingType::Uniform,
387                    has_dynamic_offset: false,
388                    min_binding_size: None,
389                },
390                count: None,
391            }],
392        });
393
394        let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
395            label: Some("globals buf"),
396            size: std::mem::size_of::<Globals>() as u64,
397            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
398            mapped_at_creation: false,
399        });
400
401        let globals_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
402            label: Some("globals bind"),
403            layout: &globals_layout,
404            entries: &[wgpu::BindGroupEntry {
405                binding: 0,
406                resource: globals_buf.as_entire_binding(),
407            }],
408        });
409
410        // Pick MSAA sample count
411        let fmt_features = adapter.get_texture_format_features(format);
412        let msaa_samples = if fmt_features.flags.sample_count_supported(4)
413            && fmt_features
414                .flags
415                .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE)
416        {
417            4
418        } else {
419            1
420        };
421
422        let ds_format = wgpu::TextureFormat::Depth24PlusStencil8;
423
424        let stencil_for_content = wgpu::DepthStencilState {
425            format: ds_format,
426            depth_write_enabled: Some(false),
427            depth_compare: Some(wgpu::CompareFunction::Always),
428            stencil: wgpu::StencilState {
429                front: wgpu::StencilFaceState {
430                    compare: wgpu::CompareFunction::LessEqual,
431                    fail_op: wgpu::StencilOperation::Keep,
432                    depth_fail_op: wgpu::StencilOperation::Keep,
433                    pass_op: wgpu::StencilOperation::Keep,
434                },
435                back: wgpu::StencilFaceState {
436                    compare: wgpu::CompareFunction::LessEqual,
437                    fail_op: wgpu::StencilOperation::Keep,
438                    depth_fail_op: wgpu::StencilOperation::Keep,
439                    pass_op: wgpu::StencilOperation::Keep,
440                },
441                read_mask: 0xFF,
442                write_mask: 0x00,
443            },
444            bias: wgpu::DepthBiasState::default(),
445        };
446
447        let stencil_for_clip_inc = wgpu::DepthStencilState {
448            format: ds_format,
449            depth_write_enabled: Some(false),
450            depth_compare: Some(wgpu::CompareFunction::Always),
451            stencil: wgpu::StencilState {
452                front: wgpu::StencilFaceState {
453                    compare: wgpu::CompareFunction::Equal,
454                    fail_op: wgpu::StencilOperation::Keep,
455                    depth_fail_op: wgpu::StencilOperation::Keep,
456                    pass_op: wgpu::StencilOperation::IncrementClamp,
457                },
458                back: wgpu::StencilFaceState {
459                    compare: wgpu::CompareFunction::Equal,
460                    fail_op: wgpu::StencilOperation::Keep,
461                    depth_fail_op: wgpu::StencilOperation::Keep,
462                    pass_op: wgpu::StencilOperation::IncrementClamp,
463                },
464                read_mask: 0xFF,
465                write_mask: 0xFF,
466            },
467            bias: wgpu::DepthBiasState::default(),
468        };
469
470        let multisample_state = wgpu::MultisampleState {
471            count: msaa_samples,
472            mask: !0,
473            alpha_to_coverage_enabled: false,
474        };
475
476        // PIPELINES
477
478        // Rect
479        let rect_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
480            label: Some("rect.wgsl"),
481            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/rect.wgsl"))),
482        });
483        let rect_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
484            label: Some("rect pipeline layout"),
485            bind_group_layouts: &[Some(&globals_layout)],
486            immediate_size: 0,
487        });
488        let rect_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
489            label: Some("rect pipeline"),
490            layout: Some(&rect_pipeline_layout),
491            vertex: wgpu::VertexState {
492                module: &rect_shader,
493                entry_point: Some("vs_main"),
494                buffers: &[wgpu::VertexBufferLayout {
495                    array_stride: std::mem::size_of::<RectInstance>() as u64,
496                    step_mode: wgpu::VertexStepMode::Instance,
497                    attributes: &[
498                        wgpu::VertexAttribute {
499                            shader_location: 0,
500                            offset: 0,
501                            format: wgpu::VertexFormat::Float32x4,
502                        },
503                        wgpu::VertexAttribute {
504                            shader_location: 1,
505                            offset: 16,
506                            format: wgpu::VertexFormat::Float32,
507                        },
508                        wgpu::VertexAttribute {
509                            shader_location: 2,
510                            offset: 20,
511                            format: wgpu::VertexFormat::Uint32,
512                        },
513                        wgpu::VertexAttribute {
514                            shader_location: 3,
515                            offset: 24,
516                            format: wgpu::VertexFormat::Float32x4,
517                        },
518                        wgpu::VertexAttribute {
519                            shader_location: 4,
520                            offset: 40,
521                            format: wgpu::VertexFormat::Float32x4,
522                        },
523                        wgpu::VertexAttribute {
524                            shader_location: 5,
525                            offset: 56,
526                            format: wgpu::VertexFormat::Float32x2,
527                        },
528                        wgpu::VertexAttribute {
529                            shader_location: 6,
530                            offset: 64,
531                            format: wgpu::VertexFormat::Float32x2,
532                        },
533                    ],
534                }],
535                compilation_options: wgpu::PipelineCompilationOptions::default(),
536            },
537            fragment: Some(wgpu::FragmentState {
538                module: &rect_shader,
539                entry_point: Some("fs_main"),
540                targets: &[Some(wgpu::ColorTargetState {
541                    format: config.format,
542                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
543                    write_mask: wgpu::ColorWrites::ALL,
544                })],
545                compilation_options: wgpu::PipelineCompilationOptions::default(),
546            }),
547            primitive: wgpu::PrimitiveState::default(),
548            depth_stencil: Some(stencil_for_content.clone()),
549            multisample: multisample_state,
550            multiview_mask: None,
551            cache: None,
552        });
553
554        // Border
555        let border_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
556            label: Some("border.wgsl"),
557            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/border.wgsl"))),
558        });
559        let border_pipeline_layout =
560            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
561                label: Some("border pipeline layout"),
562                bind_group_layouts: &[Some(&globals_layout)],
563                immediate_size: 0,
564            });
565        let border_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
566            label: Some("border pipeline"),
567            layout: Some(&border_pipeline_layout),
568            vertex: wgpu::VertexState {
569                module: &border_shader,
570                entry_point: Some("vs_main"),
571                buffers: &[wgpu::VertexBufferLayout {
572                    array_stride: std::mem::size_of::<BorderInstance>() as u64,
573                    step_mode: wgpu::VertexStepMode::Instance,
574                    attributes: &[
575                        wgpu::VertexAttribute {
576                            shader_location: 0,
577                            offset: 0,
578                            format: wgpu::VertexFormat::Float32x4,
579                        },
580                        wgpu::VertexAttribute {
581                            shader_location: 1,
582                            offset: 16,
583                            format: wgpu::VertexFormat::Float32,
584                        },
585                        wgpu::VertexAttribute {
586                            shader_location: 2,
587                            offset: 20,
588                            format: wgpu::VertexFormat::Float32,
589                        },
590                        wgpu::VertexAttribute {
591                            shader_location: 3,
592                            offset: 24,
593                            format: wgpu::VertexFormat::Float32x4,
594                        },
595                    ],
596                }],
597                compilation_options: wgpu::PipelineCompilationOptions::default(),
598            },
599            fragment: Some(wgpu::FragmentState {
600                module: &border_shader,
601                entry_point: Some("fs_main"),
602                targets: &[Some(wgpu::ColorTargetState {
603                    format: config.format,
604                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
605                    write_mask: wgpu::ColorWrites::ALL,
606                })],
607                compilation_options: wgpu::PipelineCompilationOptions::default(),
608            }),
609            primitive: wgpu::PrimitiveState::default(),
610            depth_stencil: Some(stencil_for_content.clone()),
611            multisample: multisample_state,
612            multiview_mask: None,
613            cache: None,
614        });
615
616        // Ellipse
617        let ellipse_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
618            label: Some("ellipse.wgsl"),
619            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/ellipse.wgsl"))),
620        });
621        let ellipse_pipeline_layout =
622            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
623                label: Some("ellipse pipeline layout"),
624                bind_group_layouts: &[Some(&globals_layout)],
625                immediate_size: 0,
626            });
627        let ellipse_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
628            label: Some("ellipse pipeline"),
629            layout: Some(&ellipse_pipeline_layout),
630            vertex: wgpu::VertexState {
631                module: &ellipse_shader,
632                entry_point: Some("vs_main"),
633                buffers: &[wgpu::VertexBufferLayout {
634                    array_stride: std::mem::size_of::<EllipseInstance>() as u64,
635                    step_mode: wgpu::VertexStepMode::Instance,
636                    attributes: &[
637                        wgpu::VertexAttribute {
638                            shader_location: 0,
639                            offset: 0,
640                            format: wgpu::VertexFormat::Float32x4,
641                        },
642                        wgpu::VertexAttribute {
643                            shader_location: 1,
644                            offset: 16,
645                            format: wgpu::VertexFormat::Float32x4,
646                        },
647                    ],
648                }],
649                compilation_options: wgpu::PipelineCompilationOptions::default(),
650            },
651            fragment: Some(wgpu::FragmentState {
652                module: &ellipse_shader,
653                entry_point: Some("fs_main"),
654                targets: &[Some(wgpu::ColorTargetState {
655                    format: config.format,
656                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
657                    write_mask: wgpu::ColorWrites::ALL,
658                })],
659                compilation_options: wgpu::PipelineCompilationOptions::default(),
660            }),
661            primitive: wgpu::PrimitiveState::default(),
662            depth_stencil: Some(stencil_for_content.clone()),
663            multisample: multisample_state,
664            multiview_mask: None,
665            cache: None,
666        });
667
668        // Ellipse Border
669        let ellipse_border_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
670            label: Some("ellipse_border.wgsl"),
671            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
672                "shaders/ellipse_border.wgsl"
673            ))),
674        });
675        let ellipse_border_layout =
676            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
677                label: Some("ellipse border layout"),
678                bind_group_layouts: &[Some(&globals_layout)],
679                immediate_size: 0,
680            });
681        let ellipse_border_pipeline =
682            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
683                label: Some("ellipse border pipeline"),
684                layout: Some(&ellipse_border_layout),
685                vertex: wgpu::VertexState {
686                    module: &ellipse_border_shader,
687                    entry_point: Some("vs_main"),
688                    buffers: &[wgpu::VertexBufferLayout {
689                        array_stride: std::mem::size_of::<EllipseBorderInstance>() as u64,
690                        step_mode: wgpu::VertexStepMode::Instance,
691                        attributes: &[
692                            wgpu::VertexAttribute {
693                                shader_location: 0,
694                                offset: 0,
695                                format: wgpu::VertexFormat::Float32x4,
696                            },
697                            wgpu::VertexAttribute {
698                                shader_location: 1,
699                                offset: 16,
700                                format: wgpu::VertexFormat::Float32,
701                            },
702                            wgpu::VertexAttribute {
703                                shader_location: 2,
704                                offset: 20,
705                                format: wgpu::VertexFormat::Float32x4,
706                            },
707                        ],
708                    }],
709                    compilation_options: wgpu::PipelineCompilationOptions::default(),
710                },
711                fragment: Some(wgpu::FragmentState {
712                    module: &ellipse_border_shader,
713                    entry_point: Some("fs_main"),
714                    targets: &[Some(wgpu::ColorTargetState {
715                        format: config.format,
716                        blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
717                        write_mask: wgpu::ColorWrites::ALL,
718                    })],
719                    compilation_options: wgpu::PipelineCompilationOptions::default(),
720                }),
721                primitive: wgpu::PrimitiveState::default(),
722                depth_stencil: Some(stencil_for_content.clone()),
723                multisample: multisample_state,
724                multiview_mask: None,
725                cache: None,
726            });
727
728        // TEXT & IMAGES
729
730        let text_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
731            label: Some("text.wgsl"),
732            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/text.wgsl"))),
733        });
734        let text_color_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
735            label: Some("text_color.wgsl"),
736            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
737                "shaders/text_color.wgsl"
738            ))),
739        });
740
741        // Single shared sampler for images/text
742        let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
743            label: Some("image/text sampler"),
744            address_mode_u: wgpu::AddressMode::ClampToEdge,
745            address_mode_v: wgpu::AddressMode::ClampToEdge,
746            mag_filter: wgpu::FilterMode::Linear,
747            min_filter: wgpu::FilterMode::Linear,
748            mipmap_filter: wgpu::MipmapFilterMode::Linear,
749            ..Default::default()
750        });
751
752        // Layout for Text / RGBA Images (Texture + Sampler)
753        let text_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
754            label: Some("text/rgba bind layout"),
755            entries: &[
756                wgpu::BindGroupLayoutEntry {
757                    binding: 0,
758                    visibility: wgpu::ShaderStages::FRAGMENT,
759                    ty: wgpu::BindingType::Texture {
760                        multisampled: false,
761                        view_dimension: wgpu::TextureViewDimension::D2,
762                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
763                    },
764                    count: None,
765                },
766                wgpu::BindGroupLayoutEntry {
767                    binding: 1,
768                    visibility: wgpu::ShaderStages::FRAGMENT,
769                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
770                    count: None,
771                },
772            ],
773        });
774        // We reuse this for RGBA images for simplicity, or create a distinct one
775        let image_bind_layout_rgba = text_bind_layout.clone();
776
777        // Layout for NV12 Images (TextureY + TextureUV + Sampler)
778        let image_bind_layout_nv12 =
779            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
780                label: Some("image bind layout nv12"),
781                entries: &[
782                    // Y plane
783                    wgpu::BindGroupLayoutEntry {
784                        binding: 0,
785                        visibility: wgpu::ShaderStages::FRAGMENT,
786                        ty: wgpu::BindingType::Texture {
787                            multisampled: false,
788                            view_dimension: wgpu::TextureViewDimension::D2,
789                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
790                        },
791                        count: None,
792                    },
793                    // UV plane
794                    wgpu::BindGroupLayoutEntry {
795                        binding: 1,
796                        visibility: wgpu::ShaderStages::FRAGMENT,
797                        ty: wgpu::BindingType::Texture {
798                            multisampled: false,
799                            view_dimension: wgpu::TextureViewDimension::D2,
800                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
801                        },
802                        count: None,
803                    },
804                    // Sampler
805                    wgpu::BindGroupLayoutEntry {
806                        binding: 2,
807                        visibility: wgpu::ShaderStages::FRAGMENT,
808                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
809                        count: None,
810                    },
811                ],
812            });
813
814        let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
815            label: Some("text pipeline layout"),
816            bind_group_layouts: &[Some(&globals_layout), Some(&text_bind_layout)],
817            immediate_size: 0,
818        });
819
820        let text_pipeline_mask = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
821            label: Some("text pipeline (mask)"),
822            layout: Some(&text_pipeline_layout),
823            vertex: wgpu::VertexState {
824                module: &text_mask_shader,
825                entry_point: Some("vs_main"),
826                buffers: &[wgpu::VertexBufferLayout {
827                    array_stride: std::mem::size_of::<GlyphInstance>() as u64,
828                    step_mode: wgpu::VertexStepMode::Instance,
829                    attributes: &[
830                        wgpu::VertexAttribute {
831                            shader_location: 0,
832                            offset: 0,
833                            format: wgpu::VertexFormat::Float32x4,
834                        },
835                        wgpu::VertexAttribute {
836                            shader_location: 1,
837                            offset: 16,
838                            format: wgpu::VertexFormat::Float32x4,
839                        },
840                        wgpu::VertexAttribute {
841                            shader_location: 2,
842                            offset: 32,
843                            format: wgpu::VertexFormat::Float32x4,
844                        },
845                    ],
846                }],
847                compilation_options: wgpu::PipelineCompilationOptions::default(),
848            },
849            fragment: Some(wgpu::FragmentState {
850                module: &text_mask_shader,
851                entry_point: Some("fs_main"),
852                targets: &[Some(wgpu::ColorTargetState {
853                    format: config.format,
854                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
855                    write_mask: wgpu::ColorWrites::ALL,
856                })],
857                compilation_options: wgpu::PipelineCompilationOptions::default(),
858            }),
859            primitive: wgpu::PrimitiveState::default(),
860            depth_stencil: Some(stencil_for_content.clone()),
861            multisample: multisample_state,
862            multiview_mask: None,
863            cache: None,
864        });
865
866        let text_pipeline_color = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
867            label: Some("text pipeline (color)"),
868            layout: Some(&text_pipeline_layout),
869            vertex: wgpu::VertexState {
870                module: &text_color_shader,
871                entry_point: Some("vs_main"),
872                buffers: &[wgpu::VertexBufferLayout {
873                    array_stride: std::mem::size_of::<GlyphInstance>() as u64,
874                    step_mode: wgpu::VertexStepMode::Instance,
875                    attributes: &[
876                        wgpu::VertexAttribute {
877                            shader_location: 0,
878                            offset: 0,
879                            format: wgpu::VertexFormat::Float32x4,
880                        },
881                        wgpu::VertexAttribute {
882                            shader_location: 1,
883                            offset: 16,
884                            format: wgpu::VertexFormat::Float32x4,
885                        },
886                        wgpu::VertexAttribute {
887                            shader_location: 2,
888                            offset: 32,
889                            format: wgpu::VertexFormat::Float32x4,
890                        },
891                    ],
892                }],
893                compilation_options: wgpu::PipelineCompilationOptions::default(),
894            },
895            fragment: Some(wgpu::FragmentState {
896                module: &text_color_shader,
897                entry_point: Some("fs_main"),
898                targets: &[Some(wgpu::ColorTargetState {
899                    format: config.format,
900                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
901                    write_mask: wgpu::ColorWrites::ALL,
902                })],
903                compilation_options: wgpu::PipelineCompilationOptions::default(),
904            }),
905            primitive: wgpu::PrimitiveState::default(),
906            depth_stencil: Some(stencil_for_content.clone()),
907            multisample: multisample_state,
908            multiview_mask: None,
909            cache: None,
910        });
911
912        // Reuse text color pipeline for RGBA images (same vertex struct and bindings)
913        let image_pipeline_rgba = text_pipeline_color.clone(); // In real wgpu handle clone is cheap
914
915        // NV12 Image Pipeline
916        let image_nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
917            label: Some("image_nv12.wgsl"),
918            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
919                "shaders/image_nv12.wgsl"
920            ))),
921        });
922
923        let image_nv12_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
924            label: Some("image nv12 pipeline layout"),
925            bind_group_layouts: &[Some(&globals_layout), Some(&image_bind_layout_nv12)],
926            immediate_size: 0,
927        });
928
929        let image_pipeline_nv12 = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
930            label: Some("image nv12 pipeline"),
931            layout: Some(&image_nv12_layout),
932            vertex: wgpu::VertexState {
933                module: &image_nv12_shader,
934                entry_point: Some("vs_main"),
935                buffers: &[wgpu::VertexBufferLayout {
936                    array_stride: std::mem::size_of::<Nv12Instance>() as u64,
937                    step_mode: wgpu::VertexStepMode::Instance,
938                    attributes: &[
939                        wgpu::VertexAttribute {
940                            shader_location: 0,
941                            offset: 0,
942                            format: wgpu::VertexFormat::Float32x4,
943                        }, // xywh
944                        wgpu::VertexAttribute {
945                            shader_location: 1,
946                            offset: 16,
947                            format: wgpu::VertexFormat::Float32x4,
948                        }, // uv
949                        wgpu::VertexAttribute {
950                            shader_location: 2,
951                            offset: 32,
952                            format: wgpu::VertexFormat::Float32x4,
953                        }, // tint
954                        wgpu::VertexAttribute {
955                            shader_location: 3,
956                            offset: 48,
957                            format: wgpu::VertexFormat::Float32,
958                        }, // full_range
959                    ],
960                }],
961                compilation_options: wgpu::PipelineCompilationOptions::default(),
962            },
963            fragment: Some(wgpu::FragmentState {
964                module: &image_nv12_shader,
965                entry_point: Some("fs_main"),
966                targets: &[Some(wgpu::ColorTargetState {
967                    format: config.format,
968                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
969                    write_mask: wgpu::ColorWrites::ALL,
970                })],
971                compilation_options: wgpu::PipelineCompilationOptions::default(),
972            }),
973            primitive: wgpu::PrimitiveState::default(),
974            depth_stencil: Some(stencil_for_content.clone()),
975            multisample: multisample_state,
976            multiview_mask: None,
977            cache: None,
978        });
979
980        // CLIPPING
981
982        let clip_shader_a2c = device.create_shader_module(wgpu::ShaderModuleDescriptor {
983            label: Some("clip_round_rect_a2c.wgsl"),
984            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
985                "shaders/clip_round_rect_a2c.wgsl"
986            ))),
987        });
988        let clip_shader_bin = device.create_shader_module(wgpu::ShaderModuleDescriptor {
989            label: Some("clip_round_rect_bin.wgsl"),
990            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
991                "shaders/clip_round_rect_bin.wgsl"
992            ))),
993        });
994
995        let clip_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
996            label: Some("clip pipeline layout"),
997            bind_group_layouts: &[Some(&globals_layout)],
998            immediate_size: 0,
999        });
1000
1001        let clip_vertex_layout = wgpu::VertexBufferLayout {
1002            array_stride: std::mem::size_of::<ClipInstance>() as u64,
1003            step_mode: wgpu::VertexStepMode::Instance,
1004            attributes: &[
1005                wgpu::VertexAttribute {
1006                    shader_location: 0,
1007                    offset: 0,
1008                    format: wgpu::VertexFormat::Float32x4,
1009                },
1010                wgpu::VertexAttribute {
1011                    shader_location: 1,
1012                    offset: 16,
1013                    format: wgpu::VertexFormat::Float32,
1014                },
1015            ],
1016        };
1017
1018        let clip_color_target = wgpu::ColorTargetState {
1019            format: config.format,
1020            blend: None,
1021            write_mask: wgpu::ColorWrites::empty(),
1022        };
1023
1024        let clip_pipeline_a2c = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1025            label: Some("clip pipeline (a2c)"),
1026            layout: Some(&clip_pipeline_layout),
1027            vertex: wgpu::VertexState {
1028                module: &clip_shader_a2c,
1029                entry_point: Some("vs_main"),
1030                buffers: &[clip_vertex_layout.clone()],
1031                compilation_options: wgpu::PipelineCompilationOptions::default(),
1032            },
1033            fragment: Some(wgpu::FragmentState {
1034                module: &clip_shader_a2c,
1035                entry_point: Some("fs_main"),
1036                targets: &[Some(clip_color_target.clone())],
1037                compilation_options: wgpu::PipelineCompilationOptions::default(),
1038            }),
1039            primitive: wgpu::PrimitiveState::default(),
1040            depth_stencil: Some(stencil_for_clip_inc.clone()),
1041            multisample: wgpu::MultisampleState {
1042                count: msaa_samples,
1043                mask: !0,
1044                alpha_to_coverage_enabled: msaa_samples > 1,
1045            },
1046            multiview_mask: None,
1047            cache: None,
1048        });
1049
1050        let clip_pipeline_bin = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1051            label: Some("clip pipeline (bin)"),
1052            layout: Some(&clip_pipeline_layout),
1053            vertex: wgpu::VertexState {
1054                module: &clip_shader_bin,
1055                entry_point: Some("vs_main"),
1056                buffers: &[clip_vertex_layout],
1057                compilation_options: wgpu::PipelineCompilationOptions::default(),
1058            },
1059            fragment: Some(wgpu::FragmentState {
1060                module: &clip_shader_bin,
1061                entry_point: Some("fs_main"),
1062                targets: &[Some(clip_color_target)],
1063                compilation_options: wgpu::PipelineCompilationOptions::default(),
1064            }),
1065            primitive: wgpu::PrimitiveState::default(),
1066            depth_stencil: Some(stencil_for_clip_inc),
1067            multisample: wgpu::MultisampleState {
1068                count: msaa_samples,
1069                mask: !0,
1070                alpha_to_coverage_enabled: false,
1071            },
1072            multiview_mask: None,
1073            cache: None,
1074        });
1075
1076        // Atlases
1077        let atlas_mask = Self::init_atlas_mask(&device)?;
1078        let atlas_color = Self::init_atlas_color(&device)?;
1079
1080        // Upload rings
1081        let ring_rect = UploadRing::new(&device, "ring rect", 1 << 20);
1082        let ring_border = UploadRing::new(&device, "ring border", 1 << 20);
1083        let ring_ellipse = UploadRing::new(&device, "ring ellipse", 1 << 20);
1084        let ring_ellipse_border = UploadRing::new(&device, "ring ellipse border", 1 << 20);
1085        let ring_glyph_mask = UploadRing::new(&device, "ring glyph mask", 1 << 20);
1086        let ring_glyph_color = UploadRing::new(&device, "ring glyph color", 1 << 20);
1087        let ring_clip = UploadRing::new(&device, "ring clip", 1 << 16);
1088        let ring_nv12 = UploadRing::new(&device, "ring nv12", 1 << 20);
1089
1090        // Placeholder textures
1091        let depth_stencil_tex = device.create_texture(&wgpu::TextureDescriptor {
1092            label: Some("temp ds"),
1093            size: wgpu::Extent3d {
1094                width: 1,
1095                height: 1,
1096                depth_or_array_layers: 1,
1097            },
1098            mip_level_count: 1,
1099            sample_count: 1,
1100            dimension: wgpu::TextureDimension::D2,
1101            format: wgpu::TextureFormat::Depth24PlusStencil8,
1102            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1103            view_formats: &[],
1104        });
1105        let depth_stencil_view =
1106            depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
1107
1108        let mut backend = Self {
1109            surface,
1110            device,
1111            queue,
1112            config,
1113
1114            rects: InstancedPipe::new(rect_pipeline, ring_rect),
1115            borders: InstancedPipe::new(border_pipeline, ring_border),
1116            ellipses: InstancedPipe::new(ellipse_pipeline, ring_ellipse),
1117            ellipse_borders: InstancedPipe::new(ellipse_border_pipeline, ring_ellipse_border),
1118            glyph_mask: InstancedPipe::new(text_pipeline_mask, ring_glyph_mask),
1119            glyph_color: InstancedPipe::new(text_pipeline_color, ring_glyph_color),
1120
1121            text_bind_layout,
1122
1123            image_pipeline_rgba,
1124            image_pipeline_nv12: image_pipeline_nv12.clone(),
1125            image_bind_layout_rgba,
1126            image_bind_layout_nv12,
1127            image_sampler,
1128
1129            clip_pipeline_a2c,
1130            clip_pipeline_bin,
1131            clip_ring: ring_clip,
1132
1133            nv12: InstancedPipe::new(image_pipeline_nv12, ring_nv12),
1134
1135            msaa_samples,
1136            depth_stencil_tex,
1137            depth_stencil_view,
1138            msaa_tex: None,
1139            msaa_view: None,
1140            globals_bind,
1141            globals_buf,
1142            globals_layout,
1143
1144            atlas_mask,
1145            atlas_color,
1146
1147            next_image_handle: 1,
1148            images: HashMap::new(),
1149
1150            frame_index: 0,
1151            image_bytes_total: 0,
1152            image_evict_after_frames: 600,         // ~10s @ 60fps
1153            image_budget_bytes: 512 * 1024 * 1024, // 512 MB
1154        };
1155
1156        backend.recreate_msaa_and_depth_stencil();
1157        Ok(backend)
1158    }
1159
1160    #[cfg(not(target_arch = "wasm32"))]
1161    pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
1162        pollster::block_on(Self::new_async(window))
1163    }
1164
1165    #[cfg(target_arch = "wasm32")]
1166    pub fn new(_window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
1167        anyhow::bail!("Use WgpuBackend::new_async(window).await on wasm32")
1168    }
1169
1170    // Image API
1171
1172    pub fn set_image_from_bytes(
1173        &mut self,
1174        handle: u64,
1175        data: &[u8],
1176        srgb: bool,
1177    ) -> anyhow::Result<()> {
1178        let img = image::load_from_memory(data)?;
1179        let rgba = img.to_rgba8();
1180        let (w, h) = rgba.dimensions();
1181        self.set_image_rgba8(handle, w, h, &rgba, srgb)
1182    }
1183
1184    pub fn set_image_rgba8(
1185        &mut self,
1186        handle: u64,
1187        w: u32,
1188        h: u32,
1189        rgba: &[u8],
1190        srgb: bool,
1191    ) -> anyhow::Result<()> {
1192        let expected = (w as usize) * (h as usize) * 4;
1193        if rgba.len() < expected {
1194            return Err(anyhow::anyhow!(
1195                "RGBA buffer too small: {} < {}",
1196                rgba.len(),
1197                expected
1198            ));
1199        }
1200
1201        let format = if srgb {
1202            wgpu::TextureFormat::Rgba8UnormSrgb
1203        } else {
1204            wgpu::TextureFormat::Rgba8Unorm
1205        };
1206
1207        let needs_recreate = match self.images.get(&handle) {
1208            Some(ImageTex::Rgba {
1209                w: cw,
1210                h: ch,
1211                format: cf,
1212                ..
1213            }) => *cw != w || *ch != h || *cf != format,
1214            _ => true,
1215        };
1216
1217        if needs_recreate {
1218            // Remove old to track budget correctly
1219            self.remove_image(handle);
1220
1221            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1222                label: Some("user image rgba"),
1223                size: wgpu::Extent3d {
1224                    width: w,
1225                    height: h,
1226                    depth_or_array_layers: 1,
1227                },
1228                mip_level_count: 1,
1229                sample_count: 1,
1230                dimension: wgpu::TextureDimension::D2,
1231                format,
1232                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1233                view_formats: &[],
1234            });
1235            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1236
1237            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1238                label: Some("image bind rgba"),
1239                layout: &self.image_bind_layout_rgba,
1240                entries: &[
1241                    wgpu::BindGroupEntry {
1242                        binding: 0,
1243                        resource: wgpu::BindingResource::TextureView(&view),
1244                    },
1245                    wgpu::BindGroupEntry {
1246                        binding: 1,
1247                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
1248                    },
1249                ],
1250            });
1251
1252            let bytes = (w as u64) * (h as u64) * 4;
1253            self.image_bytes_total += bytes;
1254
1255            self.images.insert(
1256                handle,
1257                ImageTex::Rgba {
1258                    tex,
1259                    view,
1260                    bind,
1261                    w,
1262                    h,
1263                    format,
1264                    last_used_frame: self.frame_index,
1265                    bytes,
1266                },
1267            );
1268        }
1269
1270        let tex = match self.images.get(&handle) {
1271            Some(ImageTex::Rgba { tex, .. }) => tex,
1272            _ => unreachable!(),
1273        };
1274
1275        self.queue.write_texture(
1276            wgpu::TexelCopyTextureInfo {
1277                texture: tex,
1278                mip_level: 0,
1279                origin: wgpu::Origin3d::ZERO,
1280                aspect: wgpu::TextureAspect::All,
1281            },
1282            &rgba[..expected],
1283            wgpu::TexelCopyBufferLayout {
1284                offset: 0,
1285                bytes_per_row: Some(4 * w),
1286                rows_per_image: Some(h),
1287            },
1288            wgpu::Extent3d {
1289                width: w,
1290                height: h,
1291                depth_or_array_layers: 1,
1292            },
1293        );
1294
1295        // Ensure budget limits
1296        self.evict_budget_excess();
1297
1298        Ok(())
1299    }
1300
1301    pub fn set_image_nv12(
1302        &mut self,
1303        handle: u64,
1304        w: u32,
1305        h: u32,
1306        y: &[u8],
1307        uv: &[u8],
1308        full_range: bool,
1309    ) -> anyhow::Result<()> {
1310        let y_expected = (w as usize) * (h as usize);
1311        let uv_w = (w / 2).max(1);
1312        let uv_h = (h / 2).max(1);
1313        let uv_expected = (uv_w as usize) * (uv_h as usize) * 2;
1314
1315        if y.len() < y_expected {
1316            return Err(anyhow::anyhow!("Y plane too small"));
1317        }
1318        if uv.len() < uv_expected {
1319            return Err(anyhow::anyhow!("UV plane too small"));
1320        }
1321
1322        let needs_recreate = match self.images.get(&handle) {
1323            Some(ImageTex::Nv12 { w: ww, h: hh, .. }) => *ww != w || *hh != h,
1324            _ => true,
1325        };
1326
1327        if needs_recreate {
1328            self.remove_image(handle);
1329
1330            let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
1331                label: Some("nv12 Y"),
1332                size: wgpu::Extent3d {
1333                    width: w,
1334                    height: h,
1335                    depth_or_array_layers: 1,
1336                },
1337                mip_level_count: 1,
1338                sample_count: 1,
1339                dimension: wgpu::TextureDimension::D2,
1340                format: wgpu::TextureFormat::R8Unorm,
1341                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1342                view_formats: &[],
1343            });
1344            let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
1345
1346            let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
1347                label: Some("nv12 UV"),
1348                size: wgpu::Extent3d {
1349                    width: uv_w,
1350                    height: uv_h,
1351                    depth_or_array_layers: 1,
1352                },
1353                mip_level_count: 1,
1354                sample_count: 1,
1355                dimension: wgpu::TextureDimension::D2,
1356                format: wgpu::TextureFormat::Rg8Unorm,
1357                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1358                view_formats: &[],
1359            });
1360            let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
1361
1362            let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1363                label: Some("nv12 bind"),
1364                layout: &self.image_bind_layout_nv12,
1365                entries: &[
1366                    wgpu::BindGroupEntry {
1367                        binding: 0,
1368                        resource: wgpu::BindingResource::TextureView(&view_y),
1369                    },
1370                    wgpu::BindGroupEntry {
1371                        binding: 1,
1372                        resource: wgpu::BindingResource::TextureView(&view_uv),
1373                    },
1374                    wgpu::BindGroupEntry {
1375                        binding: 2,
1376                        resource: wgpu::BindingResource::Sampler(&self.image_sampler),
1377                    },
1378                ],
1379            });
1380
1381            let bytes = (w as u64) * (h as u64) + (uv_w as u64) * (uv_h as u64) * 2;
1382            self.image_bytes_total += bytes;
1383
1384            self.images.insert(
1385                handle,
1386                ImageTex::Nv12 {
1387                    tex_y,
1388                    view_y,
1389                    tex_uv,
1390                    view_uv,
1391                    bind,
1392                    w,
1393                    h,
1394                    full_range,
1395                    last_used_frame: self.frame_index,
1396                    bytes,
1397                },
1398            );
1399        }
1400
1401        let (tex_y, tex_uv, _bind) = match self.images.get(&handle) {
1402            Some(ImageTex::Nv12 {
1403                tex_y,
1404                tex_uv,
1405                bind,
1406                ..
1407            }) => (tex_y, tex_uv, bind),
1408            _ => return Err(anyhow::anyhow!("Handle is not NV12")),
1409        };
1410
1411        self.queue.write_texture(
1412            wgpu::TexelCopyTextureInfo {
1413                texture: tex_y,
1414                mip_level: 0,
1415                origin: wgpu::Origin3d::ZERO,
1416                aspect: wgpu::TextureAspect::All,
1417            },
1418            &y[..y_expected],
1419            wgpu::TexelCopyBufferLayout {
1420                offset: 0,
1421                bytes_per_row: Some(w),
1422                rows_per_image: Some(h),
1423            },
1424            wgpu::Extent3d {
1425                width: w,
1426                height: h,
1427                depth_or_array_layers: 1,
1428            },
1429        );
1430
1431        self.queue.write_texture(
1432            wgpu::TexelCopyTextureInfo {
1433                texture: tex_uv,
1434                mip_level: 0,
1435                origin: wgpu::Origin3d::ZERO,
1436                aspect: wgpu::TextureAspect::All,
1437            },
1438            &uv[..uv_expected],
1439            wgpu::TexelCopyBufferLayout {
1440                offset: 0,
1441                bytes_per_row: Some(2 * uv_w),
1442                rows_per_image: Some(uv_h),
1443            },
1444            wgpu::Extent3d {
1445                width: uv_w,
1446                height: uv_h,
1447                depth_or_array_layers: 1,
1448            },
1449        );
1450
1451        self.evict_budget_excess();
1452        Ok(())
1453    }
1454
1455    pub fn remove_image(&mut self, handle: u64) {
1456        if let Some(img) = self.images.remove(&handle) {
1457            let b = match img {
1458                ImageTex::Rgba { bytes, .. } => bytes,
1459                ImageTex::Nv12 { bytes, .. } => bytes,
1460            };
1461            self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
1462        }
1463    }
1464
1465    // Legacy support from Step 1 instructions (temporary until platform render logic is fully swapped)
1466    pub fn register_image_from_bytes(&mut self, data: &[u8], srgb: bool) -> u64 {
1467        let handle = self.next_image_handle;
1468        self.next_image_handle += 1;
1469        if let Err(e) = self.set_image_from_bytes(handle, data, srgb) {
1470            log::error!("Failed to register image: {e}");
1471        }
1472        handle
1473    }
1474
1475    fn evict_unused_images(&mut self) {
1476        let now = self.frame_index;
1477        let evict_after = self.image_evict_after_frames;
1478
1479        // Time based eviction
1480        let mut to_remove = Vec::new();
1481        for (h, t) in self.images.iter() {
1482            let last = match t {
1483                ImageTex::Rgba {
1484                    last_used_frame, ..
1485                } => *last_used_frame,
1486                ImageTex::Nv12 {
1487                    last_used_frame, ..
1488                } => *last_used_frame,
1489            };
1490            if now.saturating_sub(last) > evict_after {
1491                to_remove.push(*h);
1492            }
1493        }
1494        for h in to_remove {
1495            self.remove_image(h);
1496        }
1497
1498        self.evict_budget_excess();
1499    }
1500
1501    fn evict_budget_excess(&mut self) {
1502        if self.image_bytes_total <= self.image_budget_bytes {
1503            return;
1504        }
1505        // Collect (handle, last_used, bytes)
1506        let mut candidates: Vec<(u64, u64, u64)> = self
1507            .images
1508            .iter()
1509            .map(|(h, t)| {
1510                let (last, bytes) = match t {
1511                    ImageTex::Rgba {
1512                        last_used_frame,
1513                        bytes,
1514                        ..
1515                    } => (*last_used_frame, *bytes),
1516                    ImageTex::Nv12 {
1517                        last_used_frame,
1518                        bytes,
1519                        ..
1520                    } => (*last_used_frame, *bytes),
1521                };
1522                (*h, last, bytes)
1523            })
1524            .collect();
1525
1526        // Sort by last_used ascending (LRU first)
1527        candidates.sort_by_key(|k| k.1);
1528
1529        let now = self.frame_index;
1530        for (h, last, _bytes) in candidates {
1531            if self.image_bytes_total <= self.image_budget_bytes {
1532                break;
1533            }
1534            // Don't evict something used this frame
1535            if last == now {
1536                continue;
1537            }
1538            self.remove_image(h);
1539        }
1540    }
1541
1542    fn recreate_msaa_and_depth_stencil(&mut self) {
1543        if self.msaa_samples > 1 {
1544            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1545                label: Some("msaa color"),
1546                size: wgpu::Extent3d {
1547                    width: self.config.width.max(1),
1548                    height: self.config.height.max(1),
1549                    depth_or_array_layers: 1,
1550                },
1551                mip_level_count: 1,
1552                sample_count: self.msaa_samples,
1553                dimension: wgpu::TextureDimension::D2,
1554                format: self.config.format,
1555                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1556                view_formats: &[],
1557            });
1558            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1559            self.msaa_tex = Some(tex);
1560            self.msaa_view = Some(view);
1561        } else {
1562            self.msaa_tex = None;
1563            self.msaa_view = None;
1564        }
1565
1566        self.depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
1567            label: Some("depth-stencil (stencil clips)"),
1568            size: wgpu::Extent3d {
1569                width: self.config.width.max(1),
1570                height: self.config.height.max(1),
1571                depth_or_array_layers: 1,
1572            },
1573            mip_level_count: 1,
1574            sample_count: self.msaa_samples,
1575            dimension: wgpu::TextureDimension::D2,
1576            format: wgpu::TextureFormat::Depth24PlusStencil8,
1577            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1578            view_formats: &[],
1579        });
1580        self.depth_stencil_view = self
1581            .depth_stencil_tex
1582            .create_view(&wgpu::TextureViewDescriptor::default());
1583    }
1584
1585    fn init_atlas_mask(device: &wgpu::Device) -> anyhow::Result<AtlasA8> {
1586        let size = 1024u32;
1587        let tex = device.create_texture(&wgpu::TextureDescriptor {
1588            label: Some("glyph atlas A8"),
1589            size: wgpu::Extent3d {
1590                width: size,
1591                height: size,
1592                depth_or_array_layers: 1,
1593            },
1594            mip_level_count: 1,
1595            sample_count: 1,
1596            dimension: wgpu::TextureDimension::D2,
1597            format: wgpu::TextureFormat::R8Unorm,
1598            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1599            view_formats: &[],
1600        });
1601        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1602        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1603            label: Some("glyph atlas sampler A8"),
1604            address_mode_u: wgpu::AddressMode::ClampToEdge,
1605            address_mode_v: wgpu::AddressMode::ClampToEdge,
1606            address_mode_w: wgpu::AddressMode::ClampToEdge,
1607            mag_filter: wgpu::FilterMode::Linear,
1608            min_filter: wgpu::FilterMode::Linear,
1609            mipmap_filter: wgpu::MipmapFilterMode::Linear,
1610            ..Default::default()
1611        });
1612
1613        Ok(AtlasA8 {
1614            tex,
1615            view,
1616            sampler,
1617            size,
1618            next_x: 1,
1619            next_y: 1,
1620            row_h: 0,
1621            map: HashMap::new(),
1622        })
1623    }
1624
1625    fn init_atlas_color(device: &wgpu::Device) -> anyhow::Result<AtlasRGBA> {
1626        let size = 1024u32;
1627        let tex = device.create_texture(&wgpu::TextureDescriptor {
1628            label: Some("glyph atlas RGBA"),
1629            size: wgpu::Extent3d {
1630                width: size,
1631                height: size,
1632                depth_or_array_layers: 1,
1633            },
1634            mip_level_count: 1,
1635            sample_count: 1,
1636            dimension: wgpu::TextureDimension::D2,
1637            format: wgpu::TextureFormat::Rgba8UnormSrgb,
1638            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1639            view_formats: &[],
1640        });
1641        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1642        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1643            label: Some("glyph atlas sampler RGBA"),
1644            address_mode_u: wgpu::AddressMode::ClampToEdge,
1645            address_mode_v: wgpu::AddressMode::ClampToEdge,
1646            address_mode_w: wgpu::AddressMode::ClampToEdge,
1647            mag_filter: wgpu::FilterMode::Linear,
1648            min_filter: wgpu::FilterMode::Linear,
1649            mipmap_filter: wgpu::MipmapFilterMode::Linear,
1650            ..Default::default()
1651        });
1652        Ok(AtlasRGBA {
1653            tex,
1654            view,
1655            sampler,
1656            size,
1657            next_x: 1,
1658            next_y: 1,
1659            row_h: 0,
1660            map: HashMap::new(),
1661        })
1662    }
1663
1664    fn atlas_bind_group_mask(&self) -> wgpu::BindGroup {
1665        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1666            label: Some("atlas bind"),
1667            layout: &self.text_bind_layout,
1668            entries: &[
1669                wgpu::BindGroupEntry {
1670                    binding: 0,
1671                    resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
1672                },
1673                wgpu::BindGroupEntry {
1674                    binding: 1,
1675                    resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
1676                },
1677            ],
1678        })
1679    }
1680
1681    fn atlas_bind_group_color(&self) -> wgpu::BindGroup {
1682        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1683            label: Some("atlas bind color"),
1684            layout: &self.text_bind_layout,
1685            entries: &[
1686                wgpu::BindGroupEntry {
1687                    binding: 0,
1688                    resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
1689                },
1690                wgpu::BindGroupEntry {
1691                    binding: 1,
1692                    resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
1693                },
1694            ],
1695        })
1696    }
1697
1698    fn upload_glyph_mask(&mut self, key: repose_text::GlyphKey, px: u32) -> Option<GlyphInfo> {
1699        let keyp = (key, px);
1700        if let Some(info) = self.atlas_mask.map.get(&keyp) {
1701            return Some(*info);
1702        }
1703
1704        let gb = repose_text::rasterize(key, px as f32)?;
1705        if gb.w == 0 || gb.h == 0 || gb.data.is_empty() {
1706            return None;
1707        }
1708
1709        let coverage = match swash_to_a8_coverage(gb.content, &gb.data) {
1710            Some(c) => c,
1711            None => return None,
1712        };
1713
1714        let w = gb.w.max(1);
1715        let h = gb.h.max(1);
1716
1717        if !self.alloc_space_mask(w, h) {
1718            self.grow_mask_and_rebuild();
1719        }
1720        if !self.alloc_space_mask(w, h) {
1721            return None;
1722        }
1723        let x = self.atlas_mask.next_x;
1724        let y = self.atlas_mask.next_y;
1725        self.atlas_mask.next_x += w + 1;
1726        self.atlas_mask.row_h = self.atlas_mask.row_h.max(h + 1);
1727
1728        let layout = wgpu::TexelCopyBufferLayout {
1729            offset: 0,
1730            bytes_per_row: Some(w),
1731            rows_per_image: Some(h),
1732        };
1733        let size = wgpu::Extent3d {
1734            width: w,
1735            height: h,
1736            depth_or_array_layers: 1,
1737        };
1738        self.queue.write_texture(
1739            wgpu::TexelCopyTextureInfoBase {
1740                texture: &self.atlas_mask.tex,
1741                mip_level: 0,
1742                origin: wgpu::Origin3d { x, y, z: 0 },
1743                aspect: wgpu::TextureAspect::All,
1744            },
1745            &coverage,
1746            layout,
1747            size,
1748        );
1749
1750        let info = GlyphInfo {
1751            u0: x as f32 / self.atlas_mask.size as f32,
1752            v0: y as f32 / self.atlas_mask.size as f32,
1753            u1: (x + w) as f32 / self.atlas_mask.size as f32,
1754            v1: (y + h) as f32 / self.atlas_mask.size as f32,
1755            w: w as f32,
1756            h: h as f32,
1757            bearing_x: 0.0,
1758            bearing_y: 0.0,
1759            advance: 0.0,
1760        };
1761        self.atlas_mask.map.insert(keyp, info);
1762        Some(info)
1763    }
1764
1765    fn upload_glyph_color(&mut self, key: repose_text::GlyphKey, px: u32) -> Option<GlyphInfo> {
1766        let keyp = (key, px);
1767        if let Some(info) = self.atlas_color.map.get(&keyp) {
1768            return Some(*info);
1769        }
1770        let gb = repose_text::rasterize(key, px as f32)?;
1771        if !matches!(gb.content, cosmic_text::SwashContent::Color) {
1772            return None;
1773        }
1774        let w = gb.w.max(1);
1775        let h = gb.h.max(1);
1776        if !self.alloc_space_color(w, h) {
1777            self.grow_color_and_rebuild();
1778        }
1779        if !self.alloc_space_color(w, h) {
1780            return None;
1781        }
1782        let x = self.atlas_color.next_x;
1783        let y = self.atlas_color.next_y;
1784        self.atlas_color.next_x += w + 1;
1785        self.atlas_color.row_h = self.atlas_color.row_h.max(h + 1);
1786
1787        let layout = wgpu::TexelCopyBufferLayout {
1788            offset: 0,
1789            bytes_per_row: Some(w * 4),
1790            rows_per_image: Some(h),
1791        };
1792        let size = wgpu::Extent3d {
1793            width: w,
1794            height: h,
1795            depth_or_array_layers: 1,
1796        };
1797        self.queue.write_texture(
1798            wgpu::TexelCopyTextureInfoBase {
1799                texture: &self.atlas_color.tex,
1800                mip_level: 0,
1801                origin: wgpu::Origin3d { x, y, z: 0 },
1802                aspect: wgpu::TextureAspect::All,
1803            },
1804            &gb.data,
1805            layout,
1806            size,
1807        );
1808        let info = GlyphInfo {
1809            u0: x as f32 / self.atlas_color.size as f32,
1810            v0: y as f32 / self.atlas_color.size as f32,
1811            u1: (x + w) as f32 / self.atlas_color.size as f32,
1812            v1: (y + h) as f32 / self.atlas_color.size as f32,
1813            w: w as f32,
1814            h: h as f32,
1815            bearing_x: 0.0,
1816            bearing_y: 0.0,
1817            advance: 0.0,
1818        };
1819        self.atlas_color.map.insert(keyp, info);
1820        Some(info)
1821    }
1822
1823    fn alloc_space_mask(&mut self, w: u32, h: u32) -> bool {
1824        if self.atlas_mask.next_x + w + 1 >= self.atlas_mask.size {
1825            self.atlas_mask.next_x = 1;
1826            self.atlas_mask.next_y += self.atlas_mask.row_h + 1;
1827            self.atlas_mask.row_h = 0;
1828        }
1829        if self.atlas_mask.next_y + h + 1 >= self.atlas_mask.size {
1830            return false;
1831        }
1832        true
1833    }
1834
1835    fn grow_mask_and_rebuild(&mut self) {
1836        let new_size = (self.atlas_mask.size * 2).min(4096);
1837        if new_size == self.atlas_mask.size {
1838            return;
1839        }
1840        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1841            label: Some("glyph atlas A8 (grown)"),
1842            size: wgpu::Extent3d {
1843                width: new_size,
1844                height: new_size,
1845                depth_or_array_layers: 1,
1846            },
1847            mip_level_count: 1,
1848            sample_count: 1,
1849            dimension: wgpu::TextureDimension::D2,
1850            format: wgpu::TextureFormat::R8Unorm,
1851            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1852            view_formats: &[],
1853        });
1854        self.atlas_mask.tex = tex;
1855        self.atlas_mask.view = self
1856            .atlas_mask
1857            .tex
1858            .create_view(&wgpu::TextureViewDescriptor::default());
1859        self.atlas_mask.size = new_size;
1860        self.atlas_mask.next_x = 1;
1861        self.atlas_mask.next_y = 1;
1862        self.atlas_mask.row_h = 0;
1863        let keys: Vec<(repose_text::GlyphKey, u32)> = self.atlas_mask.map.keys().copied().collect();
1864        self.atlas_mask.map.clear();
1865        for (k, px) in keys {
1866            let _ = self.upload_glyph_mask(k, px);
1867        }
1868    }
1869
1870    fn alloc_space_color(&mut self, w: u32, h: u32) -> bool {
1871        if self.atlas_color.next_x + w + 1 >= self.atlas_color.size {
1872            self.atlas_color.next_x = 1;
1873            self.atlas_color.next_y += self.atlas_color.row_h + 1;
1874            self.atlas_color.row_h = 0;
1875        }
1876        if self.atlas_color.next_y + h + 1 >= self.atlas_color.size {
1877            return false;
1878        }
1879        true
1880    }
1881
1882    fn grow_color_and_rebuild(&mut self) {
1883        let new_size = (self.atlas_color.size * 2).min(4096);
1884        if new_size == self.atlas_color.size {
1885            return;
1886        }
1887        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1888            label: Some("glyph atlas RGBA (grown)"),
1889            size: wgpu::Extent3d {
1890                width: new_size,
1891                height: new_size,
1892                depth_or_array_layers: 1,
1893            },
1894            mip_level_count: 1,
1895            sample_count: 1,
1896            dimension: wgpu::TextureDimension::D2,
1897            format: wgpu::TextureFormat::Rgba8UnormSrgb,
1898            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1899            view_formats: &[],
1900        });
1901        self.atlas_color.tex = tex;
1902        self.atlas_color.view = self
1903            .atlas_color
1904            .tex
1905            .create_view(&wgpu::TextureViewDescriptor::default());
1906        self.atlas_color.size = new_size;
1907        self.atlas_color.next_x = 1;
1908        self.atlas_color.next_y = 1;
1909        self.atlas_color.row_h = 0;
1910        let keys: Vec<(repose_text::GlyphKey, u32)> =
1911            self.atlas_color.map.keys().copied().collect();
1912        self.atlas_color.map.clear();
1913        for (k, px) in keys {
1914            let _ = self.upload_glyph_color(k, px);
1915        }
1916    }
1917}
1918
1919fn brush_to_instance_fields(brush: &Brush) -> (u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2]) {
1920    match brush {
1921        Brush::Solid(c) => (
1922            0u32,
1923            c.to_linear(),
1924            [0.0, 0.0, 0.0, 0.0],
1925            [0.0, 0.0],
1926            [0.0, 1.0],
1927        ),
1928        Brush::Linear {
1929            start,
1930            end,
1931            start_color,
1932            end_color,
1933        } => (
1934            1u32,
1935            start_color.to_linear(),
1936            end_color.to_linear(),
1937            [start.x, start.y],
1938            [end.x, end.y],
1939        ),
1940    }
1941}
1942
1943fn brush_to_solid_color(brush: &Brush) -> [f32; 4] {
1944    match brush {
1945        Brush::Solid(c) => c.to_linear(),
1946        Brush::Linear { start_color, .. } => start_color.to_linear(),
1947    }
1948}
1949
1950impl RenderBackend for WgpuBackend {
1951    fn configure_surface(&mut self, width: u32, height: u32) {
1952        if width == 0 || height == 0 {
1953            return;
1954        }
1955        self.config.width = width;
1956        self.config.height = height;
1957        self.surface.configure(&self.device, &self.config);
1958        self.recreate_msaa_and_depth_stencil();
1959    }
1960
1961    fn frame(&mut self, scene: &Scene, _glyph_cfg: GlyphRasterConfig) {
1962        // Frame start maintenance
1963        self.frame_index = self.frame_index.wrapping_add(1);
1964
1965        if self.config.width == 0 || self.config.height == 0 {
1966            return;
1967        }
1968        let frame = loop {
1969            match self.surface.get_current_texture() {
1970                wgpu::CurrentSurfaceTexture::Success(f) => break f,
1971                wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
1972                    log::warn!("suboptimal surface; reconfiguring");
1973                    self.surface.configure(&self.device, &self.config);
1974                    break f;
1975                }
1976                wgpu::CurrentSurfaceTexture::Outdated => {
1977                    log::warn!("surface outdated; reconfiguring");
1978                    self.surface.configure(&self.device, &self.config);
1979                }
1980                wgpu::CurrentSurfaceTexture::Lost => {
1981                    log::warn!("surface lost; reconfiguring");
1982                    self.surface.configure(&self.device, &self.config);
1983                }
1984                wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
1985                    // Skip frame if surface is not ready
1986                    return;
1987                }
1988                wgpu::CurrentSurfaceTexture::Validation => {
1989                    log::error!("surface validation error");
1990                    return;
1991                }
1992            }
1993        };
1994
1995        fn to_ndc(x: f32, y: f32, w: f32, h: f32, fb_w: f32, fb_h: f32) -> [f32; 4] {
1996            let x0 = (x / fb_w) * 2.0 - 1.0;
1997            let y0 = 1.0 - (y / fb_h) * 2.0;
1998            let x1 = ((x + w) / fb_w) * 2.0 - 1.0;
1999            let y1 = 1.0 - ((y + h) / fb_h) * 2.0;
2000            let min_x = x0.min(x1);
2001            let min_y = y0.min(y1);
2002            let w_ndc = (x1 - x0).abs();
2003            let h_ndc = (y1 - y0).abs();
2004            [min_x, min_y, w_ndc, h_ndc]
2005        }
2006
2007        fn to_scissor(r: &repose_core::Rect, fb_w: u32, fb_h: u32) -> (u32, u32, u32, u32) {
2008            let mut x = r.x.floor() as i64;
2009            let mut y = r.y.floor() as i64;
2010            let fb_wi = fb_w as i64;
2011            let fb_hi = fb_h as i64;
2012            x = x.clamp(0, fb_wi.saturating_sub(1));
2013            y = y.clamp(0, fb_hi.saturating_sub(1));
2014            let w_req = r.w.ceil().max(1.0) as i64;
2015            let h_req = r.h.ceil().max(1.0) as i64;
2016            let w = (w_req).min(fb_wi - x).max(1);
2017            let h = (h_req).min(fb_hi - y).max(1);
2018            (x as u32, y as u32, w as u32, h as u32)
2019        }
2020
2021        let fb_w = self.config.width as f32;
2022        let fb_h = self.config.height as f32;
2023
2024        let globals = Globals {
2025            ndc_to_px: [fb_w * 0.5, fb_h * 0.5],
2026            _pad: [0.0, 0.0],
2027        };
2028        self.queue
2029            .write_buffer(&self.globals_buf, 0, bytemuck::bytes_of(&globals));
2030
2031        enum Cmd {
2032            ClipPush {
2033                off: u64,
2034                cnt: u32,
2035                scissor: (u32, u32, u32, u32),
2036            },
2037            ClipPop {
2038                scissor: (u32, u32, u32, u32),
2039            },
2040            Rect {
2041                off: u64,
2042                cnt: u32,
2043            },
2044            Border {
2045                off: u64,
2046                cnt: u32,
2047            },
2048            Ellipse {
2049                off: u64,
2050                cnt: u32,
2051            },
2052            EllipseBorder {
2053                off: u64,
2054                cnt: u32,
2055            },
2056            GlyphsMask {
2057                off: u64,
2058                cnt: u32,
2059            },
2060            GlyphsColor {
2061                off: u64,
2062                cnt: u32,
2063            },
2064            ImageRgba {
2065                off: u64,
2066                cnt: u32,
2067                handle: u64,
2068            },
2069            ImageNv12 {
2070                off: u64,
2071                cnt: u32,
2072                handle: u64,
2073            },
2074            PushTransform(Transform),
2075            PopTransform,
2076        }
2077
2078        let mut cmds: Vec<Cmd> = Vec::with_capacity(scene.nodes.len());
2079
2080        struct Batch {
2081            rects: Vec<RectInstance>,
2082            borders: Vec<BorderInstance>,
2083            ellipses: Vec<EllipseInstance>,
2084            e_borders: Vec<EllipseBorderInstance>,
2085            masks: Vec<GlyphInstance>,
2086            colors: Vec<GlyphInstance>,
2087            nv12s: Vec<Nv12Instance>,
2088        }
2089
2090        impl Batch {
2091            fn new() -> Self {
2092                Self {
2093                    rects: vec![],
2094                    borders: vec![],
2095                    ellipses: vec![],
2096                    e_borders: vec![],
2097                    masks: vec![],
2098                    colors: vec![],
2099                    nv12s: vec![],
2100                }
2101            }
2102
2103            fn is_empty(&self) -> bool {
2104                self.rects.is_empty()
2105                    && self.borders.is_empty()
2106                    && self.ellipses.is_empty()
2107                    && self.e_borders.is_empty()
2108                    && self.masks.is_empty()
2109                    && self.colors.is_empty()
2110                    && self.nv12s.is_empty()
2111            }
2112
2113            fn flush(
2114                &mut self,
2115                pipes: (
2116                    &mut InstancedPipe<RectInstance>,
2117                    &mut InstancedPipe<BorderInstance>,
2118                    &mut InstancedPipe<EllipseInstance>,
2119                    &mut InstancedPipe<EllipseBorderInstance>,
2120                ),
2121                glyph_pipes: (
2122                    &mut InstancedPipe<GlyphInstance>,
2123                    &mut InstancedPipe<GlyphInstance>,
2124                ),
2125                nv12_pipe: &mut InstancedPipe<Nv12Instance>,
2126                device: &wgpu::Device,
2127                queue: &wgpu::Queue,
2128                cmds: &mut Vec<Cmd>,
2129            ) {
2130                let (rects, borders, ellipses, e_borders) = pipes;
2131                let (masks, colors) = glyph_pipes;
2132
2133                if !self.rects.is_empty() {
2134                    if let Some((off, cnt)) = rects.upload(device, queue, &self.rects) {
2135                        cmds.push(Cmd::Rect { off, cnt });
2136                    }
2137                    self.rects.clear();
2138                }
2139                if !self.borders.is_empty() {
2140                    if let Some((off, cnt)) = borders.upload(device, queue, &self.borders) {
2141                        cmds.push(Cmd::Border { off, cnt });
2142                    }
2143                    self.borders.clear();
2144                }
2145                if !self.ellipses.is_empty() {
2146                    if let Some((off, cnt)) = ellipses.upload(device, queue, &self.ellipses) {
2147                        cmds.push(Cmd::Ellipse { off, cnt });
2148                    }
2149                    self.ellipses.clear();
2150                }
2151                if !self.e_borders.is_empty() {
2152                    if let Some((off, cnt)) = e_borders.upload(device, queue, &self.e_borders) {
2153                        cmds.push(Cmd::EllipseBorder { off, cnt });
2154                    }
2155                    self.e_borders.clear();
2156                }
2157                if !self.masks.is_empty() {
2158                    if let Some((off, cnt)) = masks.upload(device, queue, &self.masks) {
2159                        cmds.push(Cmd::GlyphsMask { off, cnt });
2160                    }
2161                    self.masks.clear();
2162                }
2163                if !self.colors.is_empty() {
2164                    if let Some((off, cnt)) = colors.upload(device, queue, &self.colors) {
2165                        cmds.push(Cmd::GlyphsColor { off, cnt });
2166                    }
2167                    self.colors.clear();
2168                }
2169                if !self.nv12s.is_empty() {
2170                    if let Some((off, cnt)) = nv12_pipe.upload(device, queue, &self.nv12s) {
2171                        // NV12 instances are unused via this batch path currently
2172                        let _ = (off, cnt);
2173                    }
2174                    self.nv12s.clear();
2175                }
2176            }
2177        }
2178
2179        self.rects.reset();
2180        self.borders.reset();
2181        self.ellipses.reset();
2182        self.ellipse_borders.reset();
2183        self.glyph_mask.reset();
2184        self.glyph_color.reset();
2185        self.clip_ring.reset();
2186        self.nv12.reset();
2187
2188        let mut batch = Batch::new();
2189        let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
2190        let mut scissor_stack: Vec<repose_core::Rect> = Vec::with_capacity(8);
2191        let root_clip_rect = repose_core::Rect {
2192            x: 0.0,
2193            y: 0.0,
2194            w: fb_w,
2195            h: fb_h,
2196        };
2197
2198        let mut current_prim: Option<&'static str> = None;
2199
2200        macro_rules! flush_if_prim_changed {
2201            ($prim:literal, $pipe:expr) => {
2202                if current_prim != Some($prim) {
2203                    flush_batch!();
2204                    current_prim = Some($prim);
2205                }
2206            };
2207        }
2208
2209        macro_rules! flush_batch {
2210            () => {
2211                if !batch.is_empty() {
2212                    batch.flush(
2213                        (
2214                            &mut self.rects,
2215                            &mut self.borders,
2216                            &mut self.ellipses,
2217                            &mut self.ellipse_borders,
2218                        ),
2219                        (&mut self.glyph_mask, &mut self.glyph_color),
2220                        &mut self.nv12,
2221                        &self.device,
2222                        &self.queue,
2223                        &mut cmds,
2224                    )
2225                }
2226                current_prim = None;
2227            };
2228        }
2229
2230        for node in &scene.nodes {
2231            let t_identity = Transform::identity();
2232            let current_transform = transform_stack.last().unwrap_or(&t_identity);
2233
2234            match node {
2235                SceneNode::Rect {
2236                    rect,
2237                    brush,
2238                    radius,
2239                } => {
2240                    flush_if_prim_changed!("rect", &self.rects);
2241                    let transformed_rect = current_transform.apply_to_rect(*rect);
2242                    let (brush_type, color0, color1, grad_start, grad_end) =
2243                        brush_to_instance_fields(brush);
2244                    batch.rects.push(RectInstance {
2245                        xywh: to_ndc(
2246                            transformed_rect.x,
2247                            transformed_rect.y,
2248                            transformed_rect.w,
2249                            transformed_rect.h,
2250                            fb_w,
2251                            fb_h,
2252                        ),
2253                        radius: *radius,
2254                        brush_type,
2255                        color0,
2256                        color1,
2257                        grad_start,
2258                        grad_end,
2259                    });
2260                }
2261                SceneNode::Border {
2262                    rect,
2263                    color,
2264                    width,
2265                    radius,
2266                } => {
2267                    flush_if_prim_changed!("border", &self.borders);
2268                    let transformed_rect = current_transform.apply_to_rect(*rect);
2269                    batch.borders.push(BorderInstance {
2270                        xywh: to_ndc(
2271                            transformed_rect.x,
2272                            transformed_rect.y,
2273                            transformed_rect.w,
2274                            transformed_rect.h,
2275                            fb_w,
2276                            fb_h,
2277                        ),
2278                        radius: *radius,
2279                        stroke: *width,
2280                        color: color.to_linear(),
2281                    });
2282                }
2283                SceneNode::Ellipse { rect, brush } => {
2284                    flush_if_prim_changed!("ellipse", &self.ellipses);
2285                    let transformed = current_transform.apply_to_rect(*rect);
2286                    let color = brush_to_solid_color(brush);
2287                    batch.ellipses.push(EllipseInstance {
2288                        xywh: to_ndc(
2289                            transformed.x,
2290                            transformed.y,
2291                            transformed.w,
2292                            transformed.h,
2293                            fb_w,
2294                            fb_h,
2295                        ),
2296                        color,
2297                    });
2298                }
2299                SceneNode::EllipseBorder { rect, color, width } => {
2300                    flush_if_prim_changed!("ellipse_border", &self.ellipse_borders);
2301                    let transformed = current_transform.apply_to_rect(*rect);
2302                    batch.e_borders.push(EllipseBorderInstance {
2303                        xywh: to_ndc(
2304                            transformed.x,
2305                            transformed.y,
2306                            transformed.w,
2307                            transformed.h,
2308                            fb_w,
2309                            fb_h,
2310                        ),
2311                        stroke: *width,
2312                        color: color.to_linear(),
2313                    });
2314                }
2315                SceneNode::Text {
2316                    rect,
2317                    text,
2318                    color,
2319                    size,
2320                } => {
2321                    flush_batch!(); // flush any prior primitives
2322
2323                    let px = (*size).clamp(8.0, 96.0);
2324                    let shaped = repose_text::shape_line(text.as_ref(), px);
2325                    let transformed_rect = current_transform.apply_to_rect(*rect);
2326
2327                    for sg in shaped {
2328                        if let Some(info) = self.upload_glyph_color(sg.key, px as u32) {
2329                            let x = transformed_rect.x + sg.x + sg.bearing_x;
2330                            let y = transformed_rect.y + sg.y - sg.bearing_y;
2331                            batch.colors.push(GlyphInstance {
2332                                xywh: to_ndc(x, y, info.w, info.h, fb_w, fb_h),
2333                                uv: [info.u0, info.v1, info.u1, info.v0],
2334                                color: color.to_linear(),
2335                            });
2336                        } else if let Some(info) = self.upload_glyph_mask(sg.key, px as u32) {
2337                            let x = transformed_rect.x + sg.x + sg.bearing_x;
2338                            let y = transformed_rect.y + sg.y - sg.bearing_y;
2339                            batch.masks.push(GlyphInstance {
2340                                xywh: to_ndc(x, y, info.w, info.h, fb_w, fb_h),
2341                                uv: [info.u0, info.v1, info.u1, info.v0],
2342                                color: color.to_linear(),
2343                            });
2344                        }
2345                    }
2346                    // Don't flush here - let next primitive trigger flush
2347                }
2348                SceneNode::Image {
2349                    rect,
2350                    handle,
2351                    tint,
2352                    fit,
2353                } => {
2354                    flush_batch!();
2355
2356                    // Update usage timestamp for eviction
2357                    let (img_w, img_h, is_nv12) = if let Some(t) = self.images.get_mut(handle) {
2358                        match t {
2359                            ImageTex::Rgba {
2360                                w,
2361                                h,
2362                                last_used_frame,
2363                                ..
2364                            } => {
2365                                *last_used_frame = self.frame_index;
2366                                (*w, *h, false)
2367                            }
2368                            ImageTex::Nv12 {
2369                                w,
2370                                h,
2371                                last_used_frame,
2372                                ..
2373                            } => {
2374                                *last_used_frame = self.frame_index;
2375                                (*w, *h, true)
2376                            }
2377                        }
2378                    } else {
2379                        log::warn!("Image handle {} not found", handle);
2380                        continue;
2381                    };
2382
2383                    let src_w = img_w as f32;
2384                    let src_h = img_h as f32;
2385                    let dst_w = rect.w.max(0.0);
2386                    let dst_h = rect.h.max(0.0);
2387                    if dst_w <= 0.0 || dst_h <= 0.0 {
2388                        continue;
2389                    }
2390
2391                    let (xywh_ndc, uv_rect) = match fit {
2392                        repose_core::view::ImageFit::Contain => {
2393                            let scale = (dst_w / src_w).min(dst_h / src_h);
2394                            let w = src_w * scale;
2395                            let h = src_h * scale;
2396                            let x = rect.x + (dst_w - w) * 0.5;
2397                            let y = rect.y + (dst_h - h) * 0.5;
2398                            (to_ndc(x, y, w, h, fb_w, fb_h), [0.0, 1.0, 1.0, 0.0])
2399                        }
2400                        repose_core::view::ImageFit::Cover => {
2401                            let scale = (dst_w / src_w).max(dst_h / src_h);
2402                            let content_w = src_w * scale;
2403                            let content_h = src_h * scale;
2404                            let overflow_x = (content_w - dst_w) * 0.5;
2405                            let overflow_y = (content_h - dst_h) * 0.5;
2406                            let u0 = (overflow_x / content_w).clamp(0.0, 1.0);
2407                            let v0 = (overflow_y / content_h).clamp(0.0, 1.0);
2408                            let u1 = ((overflow_x + dst_w) / content_w).clamp(0.0, 1.0);
2409                            let v1 = ((overflow_y + dst_h) / content_h).clamp(0.0, 1.0);
2410                            (
2411                                to_ndc(rect.x, rect.y, dst_w, dst_h, fb_w, fb_h),
2412                                [u0, 1.0 - v1, u1, 1.0 - v0],
2413                            )
2414                        }
2415                        repose_core::view::ImageFit::FitWidth => {
2416                            let scale = dst_w / src_w;
2417                            let w = dst_w;
2418                            let h = src_h * scale;
2419                            let y = rect.y + (dst_h - h) * 0.5;
2420                            (to_ndc(rect.x, y, w, h, fb_w, fb_h), [0.0, 1.0, 1.0, 0.0])
2421                        }
2422                        repose_core::view::ImageFit::FitHeight => {
2423                            let scale = dst_h / src_h;
2424                            let w = src_w * scale;
2425                            let h = dst_h;
2426                            let x = rect.x + (dst_w - w) * 0.5;
2427                            (to_ndc(x, rect.y, w, h, fb_w, fb_h), [0.0, 1.0, 1.0, 0.0])
2428                        }
2429                    };
2430
2431                    if is_nv12 {
2432                        let full_range = if let Some(ImageTex::Nv12 { full_range, .. }) =
2433                            self.images.get(handle)
2434                        {
2435                            if *full_range { 1.0 } else { 0.0 }
2436                        } else {
2437                            0.0
2438                        };
2439
2440                        let inst = Nv12Instance {
2441                            xywh: xywh_ndc,
2442                            uv: uv_rect,
2443                            color: tint.to_linear(),
2444                            full_range,
2445                            _pad: [0.0; 3],
2446                        };
2447                        if let Some((off, _)) = self.nv12.upload(&self.device, &self.queue, &[inst])
2448                        {
2449                            cmds.push(Cmd::ImageNv12 {
2450                                off,
2451                                cnt: 1,
2452                                handle: *handle,
2453                            });
2454                        }
2455                    } else {
2456                        // RGBA uses GlyphInstance struct (reused pipeline)
2457                        let inst = GlyphInstance {
2458                            xywh: xywh_ndc,
2459                            uv: uv_rect,
2460                            color: tint.to_linear(),
2461                        };
2462                        if let Some((off, _)) =
2463                            self.glyph_color.upload(&self.device, &self.queue, &[inst])
2464                        {
2465                            cmds.push(Cmd::ImageRgba {
2466                                off,
2467                                cnt: 1,
2468                                handle: *handle,
2469                            });
2470                        }
2471                    }
2472                }
2473                SceneNode::PushClip { rect, radius } => {
2474                    flush_batch!(); // flush content before entering clip
2475
2476                    let t_identity = Transform::identity();
2477                    let current_transform = transform_stack.last().unwrap_or(&t_identity);
2478                    let transformed = current_transform.apply_to_rect(*rect);
2479
2480                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
2481                    let next_scissor = intersect(top, transformed);
2482                    scissor_stack.push(next_scissor);
2483                    let scissor = to_scissor(&next_scissor, self.config.width, self.config.height);
2484
2485                    let inst = ClipInstance {
2486                        xywh: to_ndc(
2487                            transformed.x,
2488                            transformed.y,
2489                            transformed.w,
2490                            transformed.h,
2491                            fb_w,
2492                            fb_h,
2493                        ),
2494                        radius: *radius,
2495                        _pad: [0.0; 3],
2496                    };
2497                    let bytes = bytemuck::bytes_of(&inst);
2498                    self.clip_ring.grow_to_fit(&self.device, bytes.len() as u64);
2499                    let (off, _) = self.clip_ring.alloc_write(&self.queue, bytes);
2500
2501                    cmds.push(Cmd::ClipPush {
2502                        off,
2503                        cnt: 1,
2504                        scissor,
2505                    });
2506                }
2507                SceneNode::PopClip => {
2508                    flush_batch!();
2509
2510                    if !scissor_stack.is_empty() {
2511                        scissor_stack.pop();
2512                    } else {
2513                        log::warn!("PopClip with empty stack");
2514                    }
2515
2516                    let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
2517                    let scissor = to_scissor(&top, self.config.width, self.config.height);
2518                    cmds.push(Cmd::ClipPop { scissor });
2519                }
2520                SceneNode::PushTransform { transform } => {
2521                    flush_batch!(); // flush before transform change
2522                    let combined = current_transform.combine(transform);
2523                    if transform.rotate != 0.0 {
2524                        ROT_WARN_ONCE.call_once(|| {
2525                            log::warn!(
2526                                "Transform rotation is not supported for Rect/Text/Image; rotation will be ignored."
2527                            );
2528                        });
2529                    }
2530                    transform_stack.push(combined);
2531                }
2532                SceneNode::PopTransform => {
2533                    flush_batch!(); // flush before transform change
2534                    transform_stack.pop();
2535                }
2536            }
2537        }
2538
2539        flush_batch!();
2540
2541        let mut encoder = self
2542            .device
2543            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2544                label: Some("frame encoder"),
2545            });
2546
2547        {
2548            let swap_view = frame
2549                .texture
2550                .create_view(&wgpu::TextureViewDescriptor::default());
2551
2552            let (color_view, resolve_target) = if let Some(msaa_view) = &self.msaa_view {
2553                (msaa_view, Some(&swap_view))
2554            } else {
2555                (&swap_view, None)
2556            };
2557
2558            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2559                label: Some("main pass"),
2560                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2561                    view: color_view,
2562                    resolve_target,
2563                    ops: wgpu::Operations {
2564                        load: wgpu::LoadOp::Clear(wgpu::Color {
2565                            r: scene.clear_color.0 as f64 / 255.0,
2566                            g: scene.clear_color.1 as f64 / 255.0,
2567                            b: scene.clear_color.2 as f64 / 255.0,
2568                            a: scene.clear_color.3 as f64 / 255.0,
2569                        }),
2570                        store: wgpu::StoreOp::Store,
2571                    },
2572                    depth_slice: None,
2573                })],
2574                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2575                    view: &self.depth_stencil_view,
2576                    depth_ops: None,
2577                    stencil_ops: Some(wgpu::Operations {
2578                        load: wgpu::LoadOp::Clear(0),
2579                        store: wgpu::StoreOp::Store,
2580                    }),
2581                }),
2582                timestamp_writes: None,
2583                occlusion_query_set: None,
2584                multiview_mask: None,
2585            });
2586
2587            let mut clip_depth: u32 = 0;
2588            rpass.set_bind_group(0, &self.globals_bind, &[]);
2589            rpass.set_stencil_reference(clip_depth);
2590            rpass.set_scissor_rect(0, 0, self.config.width, self.config.height);
2591
2592            let bind_mask = self.atlas_bind_group_mask();
2593            let bind_color = self.atlas_bind_group_color();
2594
2595            for cmd in cmds {
2596                match cmd {
2597                    Cmd::ClipPush {
2598                        off,
2599                        cnt: n,
2600                        scissor,
2601                    } => {
2602                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
2603                        rpass.set_stencil_reference(clip_depth);
2604
2605                        if self.msaa_samples > 1 {
2606                            rpass.set_pipeline(&self.clip_pipeline_a2c);
2607                        } else {
2608                            rpass.set_pipeline(&self.clip_pipeline_bin);
2609                        }
2610
2611                        let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
2612                        rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
2613                        rpass.draw(0..6, 0..n);
2614
2615                        clip_depth = (clip_depth + 1).min(255);
2616                        rpass.set_stencil_reference(clip_depth);
2617                    }
2618
2619                    Cmd::ClipPop { scissor } => {
2620                        clip_depth = clip_depth.saturating_sub(1);
2621                        rpass.set_stencil_reference(clip_depth);
2622                        rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
2623                    }
2624
2625                    Cmd::Rect { off, cnt: n } => {
2626                        rpass.set_pipeline(&self.rects.pipeline);
2627                        let bytes = (n as u64) * std::mem::size_of::<RectInstance>() as u64;
2628                        rpass.set_vertex_buffer(0, self.rects.ring.buf.slice(off..off + bytes));
2629                        rpass.draw(0..6, 0..n);
2630                    }
2631
2632                    Cmd::Border { off, cnt: n } => {
2633                        rpass.set_pipeline(&self.borders.pipeline);
2634                        let bytes = (n as u64) * std::mem::size_of::<BorderInstance>() as u64;
2635                        rpass.set_vertex_buffer(0, self.borders.ring.buf.slice(off..off + bytes));
2636                        rpass.draw(0..6, 0..n);
2637                    }
2638
2639                    Cmd::GlyphsMask { off, cnt: n } => {
2640                        rpass.set_pipeline(&self.glyph_mask.pipeline);
2641                        rpass.set_bind_group(1, &bind_mask, &[]);
2642                        let bytes = (n as u64) * std::mem::size_of::<GlyphInstance>() as u64;
2643                        rpass
2644                            .set_vertex_buffer(0, self.glyph_mask.ring.buf.slice(off..off + bytes));
2645                        rpass.draw(0..6, 0..n);
2646                    }
2647
2648                    Cmd::GlyphsColor { off, cnt: n } => {
2649                        rpass.set_pipeline(&self.glyph_color.pipeline);
2650                        rpass.set_bind_group(1, &bind_color, &[]);
2651                        let bytes = (n as u64) * std::mem::size_of::<GlyphInstance>() as u64;
2652                        rpass.set_vertex_buffer(
2653                            0,
2654                            self.glyph_color.ring.buf.slice(off..off + bytes),
2655                        );
2656                        rpass.draw(0..6, 0..n);
2657                    }
2658
2659                    Cmd::ImageRgba {
2660                        off,
2661                        cnt: n,
2662                        handle,
2663                    } => {
2664                        if let Some(ImageTex::Rgba { bind, .. }) = self.images.get(&handle) {
2665                            rpass.set_pipeline(&self.image_pipeline_rgba);
2666                            rpass.set_bind_group(1, bind, &[]);
2667                            let bytes = (n as u64) * std::mem::size_of::<GlyphInstance>() as u64;
2668                            rpass.set_vertex_buffer(
2669                                0,
2670                                self.glyph_color.ring.buf.slice(off..off + bytes),
2671                            );
2672                            rpass.draw(0..6, 0..n);
2673                        }
2674                    }
2675
2676                    Cmd::ImageNv12 {
2677                        off,
2678                        cnt: n,
2679                        handle,
2680                    } => {
2681                        if let Some(ImageTex::Nv12 { bind, .. }) = self.images.get(&handle) {
2682                            rpass.set_pipeline(&self.nv12.pipeline);
2683                            rpass.set_bind_group(1, bind, &[]);
2684                            let bytes = (n as u64) * std::mem::size_of::<Nv12Instance>() as u64;
2685                            rpass.set_vertex_buffer(0, self.nv12.ring.buf.slice(off..off + bytes));
2686                            rpass.draw(0..6, 0..n);
2687                        }
2688                    }
2689
2690                    Cmd::Ellipse { off, cnt: n } => {
2691                        rpass.set_pipeline(&self.ellipses.pipeline);
2692                        let bytes = (n as u64) * std::mem::size_of::<EllipseInstance>() as u64;
2693                        rpass.set_vertex_buffer(0, self.ellipses.ring.buf.slice(off..off + bytes));
2694                        rpass.draw(0..6, 0..n);
2695                    }
2696
2697                    Cmd::EllipseBorder { off, cnt: n } => {
2698                        rpass.set_pipeline(&self.ellipse_borders.pipeline);
2699                        let bytes =
2700                            (n as u64) * std::mem::size_of::<EllipseBorderInstance>() as u64;
2701                        rpass.set_vertex_buffer(
2702                            0,
2703                            self.ellipse_borders.ring.buf.slice(off..off + bytes),
2704                        );
2705                        rpass.draw(0..6, 0..n);
2706                    }
2707
2708                    Cmd::PushTransform(_) => {}
2709                    Cmd::PopTransform => {}
2710                }
2711            }
2712        }
2713
2714        self.queue.submit(std::iter::once(encoder.finish()));
2715        if let Err(e) = catch_unwind(AssertUnwindSafe(|| frame.present())) {
2716            log::warn!("frame.present panicked: {:?}", e);
2717        }
2718
2719        // Frame end maintenance: Evict unused images
2720        self.evict_unused_images();
2721    }
2722}
2723
2724fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> repose_core::Rect {
2725    let x0 = a.x.max(b.x);
2726    let y0 = a.y.max(b.y);
2727    let x1 = (a.x + a.w).min(b.x + b.w);
2728    let y1 = (a.y + a.h).min(b.y + b.h);
2729    repose_core::Rect {
2730        x: x0,
2731        y: y0,
2732        w: (x1 - x0).max(0.0),
2733        h: (y1 - y0).max(0.0),
2734    }
2735}