Skip to main content

repose_render_wgpu/
lib.rs

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