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