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