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