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