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