Skip to main content

repose_render_wgpu/
lib.rs

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