Skip to main content

repose_render_wgpu/
lib.rs

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