repose_render_wgpu/
lib.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::num::NonZeroU32;
4use std::sync::Arc;
5
6use ab_glyph::{Font, FontArc, Glyph, PxScale, ScaleFont, point};
7use cosmic_text;
8use fontdb::Database;
9use repose_core::{Color, GlyphRasterConfig, RenderBackend, Scene, SceneNode, Transform};
10use std::panic::{AssertUnwindSafe, catch_unwind};
11use wgpu::util::DeviceExt;
12
13#[derive(Clone)]
14struct UploadRing {
15    buf: wgpu::Buffer,
16    cap: u64,
17    head: u64,
18}
19impl UploadRing {
20    fn new(device: &wgpu::Device, label: &str, cap: u64) -> Self {
21        let buf = device.create_buffer(&wgpu::BufferDescriptor {
22            label: Some(label),
23            size: cap,
24            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
25            mapped_at_creation: false,
26        });
27        Self { buf, cap, head: 0 }
28    }
29    fn reset(&mut self) {
30        self.head = 0;
31    }
32    fn alloc_write(&mut self, queue: &wgpu::Queue, bytes: &[u8]) -> (u64, u64) {
33        let len = bytes.len() as u64;
34        let align = 4u64; // vertex buffer slice offset alignment
35        let start = (self.head + (align - 1)) & !(align - 1);
36        let end = start + len;
37        if end > self.cap {
38            // wrap and overwrite from start
39            self.head = 0;
40            let start = 0;
41            let end = len.min(self.cap);
42            queue.write_buffer(&self.buf, start, &bytes[0..end as usize]);
43            self.head = end;
44            (start, len.min(self.cap - start))
45        } else {
46            queue.write_buffer(&self.buf, start, bytes);
47            self.head = end;
48            (start, len)
49        }
50    }
51}
52
53pub struct WgpuBackend {
54    surface: wgpu::Surface<'static>,
55    device: wgpu::Device,
56    queue: wgpu::Queue,
57    config: wgpu::SurfaceConfiguration,
58
59    rect_pipeline: wgpu::RenderPipeline,
60    // rect_bind_layout: wgpu::BindGroupLayout,
61    border_pipeline: wgpu::RenderPipeline,
62    // border_bind_layout: wgpu::BindGroupLayout,
63    text_pipeline_mask: wgpu::RenderPipeline,
64    text_pipeline_color: wgpu::RenderPipeline,
65    text_bind_layout: wgpu::BindGroupLayout,
66
67    // Glyph atlas
68    atlas_mask: AtlasA8,
69    atlas_color: AtlasRGBA,
70
71    // per-frame upload rings
72    ring_rect: UploadRing,
73    ring_border: UploadRing,
74    ring_glyph_mask: UploadRing,
75    ring_glyph_color: UploadRing,
76}
77
78struct AtlasA8 {
79    tex: wgpu::Texture,
80    view: wgpu::TextureView,
81    sampler: wgpu::Sampler,
82    size: u32,
83    next_x: u32,
84    next_y: u32,
85    row_h: u32,
86    map: HashMap<(repose_text::GlyphKey, u32), GlyphInfo>,
87}
88
89struct AtlasRGBA {
90    tex: wgpu::Texture,
91    view: wgpu::TextureView,
92    sampler: wgpu::Sampler,
93    size: u32,
94    next_x: u32,
95    next_y: u32,
96    row_h: u32,
97    map: HashMap<(repose_text::GlyphKey, u32), GlyphInfo>,
98}
99
100#[derive(Clone, Copy)]
101struct GlyphInfo {
102    u0: f32,
103    v0: f32,
104    u1: f32,
105    v1: f32,
106    w: f32,
107    h: f32,
108    bearing_x: f32,
109    bearing_y: f32,
110    advance: f32,
111}
112
113#[repr(C)]
114#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
115struct RectInstance {
116    // xy in NDC, wh in NDC extents
117    xywh: [f32; 4],
118    // radius in NDC units
119    radius: f32,
120    // rgba (linear)
121    color: [f32; 4],
122}
123
124#[repr(C)]
125#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
126struct BorderInstance {
127    // outer rect in NDC
128    xywh: [f32; 4],
129    // outer radius in NDC
130    radius_outer: f32,
131    // stroke width in NDC
132    stroke: f32,
133    // rgba (linear)
134    color: [f32; 4],
135}
136
137#[repr(C)]
138#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
139struct GlyphInstance {
140    // xywh in NDC
141    xywh: [f32; 4],
142    // uv
143    uv: [f32; 4],
144    // color
145    color: [f32; 4],
146}
147
148impl WgpuBackend {
149    pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
150        // Instance/Surface (latest API with backend options from env)
151        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::from_env_or_default());
152        let surface = instance.create_surface(window.clone())?;
153
154        // Adapter/Device
155        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
156            power_preference: wgpu::PowerPreference::HighPerformance,
157            compatible_surface: Some(&surface),
158            force_fallback_adapter: false,
159        }))
160        .map_err(|_e| anyhow::anyhow!("No adapter"))?;
161
162        let (device, queue) =
163            pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
164                label: Some("repose-rs device"),
165                required_features: wgpu::Features::empty(),
166                required_limits: wgpu::Limits::default(),
167                experimental_features: wgpu::ExperimentalFeatures::disabled(),
168                memory_hints: wgpu::MemoryHints::default(),
169                trace: wgpu::Trace::Off,
170            }))?;
171
172        let size = window.inner_size();
173
174        let caps = surface.get_capabilities(&adapter);
175        let format = caps
176            .formats
177            .iter()
178            .copied()
179            .find(|f| f.is_srgb()) // pick sRGB if available
180            .unwrap_or(caps.formats[0]);
181        let present_mode = caps
182            .present_modes
183            .iter()
184            .copied()
185            .find(|m| *m == wgpu::PresentMode::Mailbox || *m == wgpu::PresentMode::Immediate)
186            .unwrap_or(wgpu::PresentMode::Fifo);
187        let alpha_mode = caps.alpha_modes[0];
188
189        let config = wgpu::SurfaceConfiguration {
190            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
191            format,
192            width: size.width.max(1),
193            height: size.height.max(1),
194            present_mode,
195            alpha_mode,
196            view_formats: vec![],
197            desired_maximum_frame_latency: 2,
198        };
199        surface.configure(&device, &config);
200
201        // Pipelines: Rects
202        let rect_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
203            label: Some("rect.wgsl"),
204            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/rect.wgsl"))),
205        });
206        let rect_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
207            label: Some("rect bind layout"),
208            entries: &[],
209        });
210        let rect_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
211            label: Some("rect pipeline layout"),
212            bind_group_layouts: &[], //&[&rect_bind_layout],
213            push_constant_ranges: &[],
214        });
215        let rect_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
216            label: Some("rect pipeline"),
217            layout: Some(&rect_pipeline_layout),
218            vertex: wgpu::VertexState {
219                module: &rect_shader,
220                entry_point: Some("vs_main"),
221                buffers: &[wgpu::VertexBufferLayout {
222                    array_stride: std::mem::size_of::<RectInstance>() as u64,
223                    step_mode: wgpu::VertexStepMode::Instance,
224                    attributes: &[
225                        // xywh: vec4<f32>
226                        wgpu::VertexAttribute {
227                            shader_location: 0,
228                            offset: 0,
229                            format: wgpu::VertexFormat::Float32x4,
230                        },
231                        // radius: f32
232                        wgpu::VertexAttribute {
233                            shader_location: 1,
234                            offset: 16,
235                            format: wgpu::VertexFormat::Float32,
236                        },
237                        // color: vec4<f32>
238                        wgpu::VertexAttribute {
239                            shader_location: 2,
240                            offset: 20,
241                            format: wgpu::VertexFormat::Float32x4,
242                        },
243                    ],
244                }],
245                compilation_options: wgpu::PipelineCompilationOptions::default(),
246            },
247            fragment: Some(wgpu::FragmentState {
248                module: &rect_shader,
249                entry_point: Some("fs_main"),
250                targets: &[Some(wgpu::ColorTargetState {
251                    format: config.format,
252                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
253                    write_mask: wgpu::ColorWrites::ALL,
254                })],
255                compilation_options: wgpu::PipelineCompilationOptions::default(),
256            }),
257            primitive: wgpu::PrimitiveState::default(),
258            depth_stencil: None,
259            multisample: wgpu::MultisampleState::default(),
260            multiview: None,
261            cache: None,
262        });
263
264        // Pipelines: Borders (SDF ring)
265        let border_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
266            label: Some("border.wgsl"),
267            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/border.wgsl"))),
268        });
269        let border_bind_layout =
270            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
271                label: Some("border bind layout"),
272                entries: &[],
273            });
274        let border_pipeline_layout =
275            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
276                label: Some("border pipeline layout"),
277                bind_group_layouts: &[], //&[&border_bind_layout],
278                push_constant_ranges: &[],
279            });
280        let border_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
281            label: Some("border pipeline"),
282            layout: Some(&border_pipeline_layout),
283            vertex: wgpu::VertexState {
284                module: &border_shader,
285                entry_point: Some("vs_main"),
286                buffers: &[wgpu::VertexBufferLayout {
287                    array_stride: std::mem::size_of::<BorderInstance>() as u64,
288                    step_mode: wgpu::VertexStepMode::Instance,
289                    attributes: &[
290                        // xywh
291                        wgpu::VertexAttribute {
292                            shader_location: 0,
293                            offset: 0,
294                            format: wgpu::VertexFormat::Float32x4,
295                        },
296                        // radius_outer
297                        wgpu::VertexAttribute {
298                            shader_location: 1,
299                            offset: 16,
300                            format: wgpu::VertexFormat::Float32,
301                        },
302                        // stroke
303                        wgpu::VertexAttribute {
304                            shader_location: 2,
305                            offset: 20,
306                            format: wgpu::VertexFormat::Float32,
307                        },
308                        // color
309                        wgpu::VertexAttribute {
310                            shader_location: 3,
311                            offset: 24,
312                            format: wgpu::VertexFormat::Float32x4,
313                        },
314                    ],
315                }],
316                compilation_options: wgpu::PipelineCompilationOptions::default(),
317            },
318            fragment: Some(wgpu::FragmentState {
319                module: &border_shader,
320                entry_point: Some("fs_main"),
321                targets: &[Some(wgpu::ColorTargetState {
322                    format: config.format,
323                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
324                    write_mask: wgpu::ColorWrites::ALL,
325                })],
326                compilation_options: wgpu::PipelineCompilationOptions::default(),
327            }),
328            primitive: wgpu::PrimitiveState::default(),
329            depth_stencil: None,
330            multisample: wgpu::MultisampleState::default(),
331            multiview: None,
332            cache: None,
333        });
334
335        // Pipelines: Text
336        let text_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
337            label: Some("text.wgsl"),
338            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/text.wgsl"))),
339        });
340        let text_color_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
341            label: Some("text_color.wgsl"),
342            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
343                "shaders/text_color.wgsl"
344            ))),
345        });
346        let text_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
347            label: Some("text bind layout"),
348            entries: &[
349                wgpu::BindGroupLayoutEntry {
350                    binding: 0,
351                    visibility: wgpu::ShaderStages::FRAGMENT,
352                    ty: wgpu::BindingType::Texture {
353                        multisampled: false,
354                        view_dimension: wgpu::TextureViewDimension::D2,
355                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
356                    },
357                    count: None,
358                },
359                wgpu::BindGroupLayoutEntry {
360                    binding: 1,
361                    visibility: wgpu::ShaderStages::FRAGMENT,
362                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
363                    count: None,
364                },
365            ],
366        });
367        let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
368            label: Some("text pipeline layout"),
369            bind_group_layouts: &[&text_bind_layout],
370            push_constant_ranges: &[],
371        });
372        let text_pipeline_mask = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
373            label: Some("text pipeline (mask)"),
374            layout: Some(&text_pipeline_layout),
375            vertex: wgpu::VertexState {
376                module: &text_mask_shader,
377                entry_point: Some("vs_main"),
378                buffers: &[wgpu::VertexBufferLayout {
379                    array_stride: std::mem::size_of::<GlyphInstance>() as u64,
380                    step_mode: wgpu::VertexStepMode::Instance,
381                    attributes: &[
382                        wgpu::VertexAttribute {
383                            shader_location: 0,
384                            offset: 0,
385                            format: wgpu::VertexFormat::Float32x4,
386                        },
387                        wgpu::VertexAttribute {
388                            shader_location: 1,
389                            offset: 16,
390                            format: wgpu::VertexFormat::Float32x4,
391                        },
392                        wgpu::VertexAttribute {
393                            shader_location: 2,
394                            offset: 32,
395                            format: wgpu::VertexFormat::Float32x4,
396                        },
397                    ],
398                }],
399                compilation_options: wgpu::PipelineCompilationOptions::default(),
400            },
401            fragment: Some(wgpu::FragmentState {
402                module: &text_mask_shader,
403                entry_point: Some("fs_main"),
404                targets: &[Some(wgpu::ColorTargetState {
405                    format: config.format,
406                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
407                    write_mask: wgpu::ColorWrites::ALL,
408                })],
409                compilation_options: wgpu::PipelineCompilationOptions::default(),
410            }),
411            primitive: wgpu::PrimitiveState::default(),
412            depth_stencil: None,
413            multisample: wgpu::MultisampleState::default(),
414            multiview: None,
415            cache: None,
416        });
417        let text_pipeline_color = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
418            label: Some("text pipeline (color)"),
419            layout: Some(&text_pipeline_layout),
420            vertex: wgpu::VertexState {
421                module: &text_color_shader,
422                entry_point: Some("vs_main"),
423                buffers: &[wgpu::VertexBufferLayout {
424                    array_stride: std::mem::size_of::<GlyphInstance>() as u64,
425                    step_mode: wgpu::VertexStepMode::Instance,
426                    attributes: &[
427                        wgpu::VertexAttribute {
428                            shader_location: 0,
429                            offset: 0,
430                            format: wgpu::VertexFormat::Float32x4,
431                        },
432                        wgpu::VertexAttribute {
433                            shader_location: 1,
434                            offset: 16,
435                            format: wgpu::VertexFormat::Float32x4,
436                        },
437                        wgpu::VertexAttribute {
438                            shader_location: 2,
439                            offset: 32,
440                            format: wgpu::VertexFormat::Float32x4,
441                        },
442                    ],
443                }],
444                compilation_options: wgpu::PipelineCompilationOptions::default(),
445            },
446            fragment: Some(wgpu::FragmentState {
447                module: &text_color_shader,
448                entry_point: Some("fs_main"),
449                targets: &[Some(wgpu::ColorTargetState {
450                    format: config.format,
451                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
452                    write_mask: wgpu::ColorWrites::ALL,
453                })],
454                compilation_options: wgpu::PipelineCompilationOptions::default(),
455            }),
456            primitive: wgpu::PrimitiveState::default(),
457            depth_stencil: None,
458            multisample: wgpu::MultisampleState::default(),
459            multiview: None,
460            cache: None,
461        });
462
463        // Atlases
464        let atlas_mask = Self::init_atlas_mask(&device)?;
465        let atlas_color = Self::init_atlas_color(&device)?;
466
467        // Upload rings (starts off small, grows in-place by recreating if needed — future work)
468        let ring_rect = UploadRing::new(&device, "ring rect", 1 << 20); // 1 MiB
469        let ring_border = UploadRing::new(&device, "ring border", 1 << 20);
470        let ring_glyph_mask = UploadRing::new(&device, "ring glyph mask", 1 << 20);
471        let ring_glyph_color = UploadRing::new(&device, "ring glyph color", 1 << 20);
472
473        Ok(Self {
474            surface,
475            device,
476            queue,
477            config,
478            rect_pipeline,
479            // rect_bind_layout,
480            border_pipeline,
481            // border_bind_layout,
482            text_pipeline_mask,
483            text_pipeline_color,
484            text_bind_layout,
485            atlas_mask,
486            atlas_color,
487            ring_rect,
488            ring_border,
489            ring_glyph_color,
490            ring_glyph_mask,
491        })
492    }
493
494    fn init_atlas_mask(device: &wgpu::Device) -> anyhow::Result<AtlasA8> {
495        let size = 1024u32;
496        let tex = device.create_texture(&wgpu::TextureDescriptor {
497            label: Some("glyph atlas A8"),
498            size: wgpu::Extent3d {
499                width: size,
500                height: size,
501                depth_or_array_layers: 1,
502            },
503            mip_level_count: 1,
504            sample_count: 1,
505            dimension: wgpu::TextureDimension::D2,
506            format: wgpu::TextureFormat::R8Unorm,
507            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
508            view_formats: &[],
509        });
510        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
511        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
512            label: Some("glyph atlas sampler A8"),
513            address_mode_u: wgpu::AddressMode::ClampToEdge,
514            address_mode_v: wgpu::AddressMode::ClampToEdge,
515            address_mode_w: wgpu::AddressMode::ClampToEdge,
516            mag_filter: wgpu::FilterMode::Linear,
517            min_filter: wgpu::FilterMode::Linear,
518            mipmap_filter: wgpu::FilterMode::Linear,
519            ..Default::default()
520        });
521
522        Ok(AtlasA8 {
523            tex,
524            view,
525            sampler,
526            size,
527            next_x: 1,
528            next_y: 1,
529            row_h: 0,
530            map: HashMap::new(),
531        })
532    }
533
534    fn init_atlas_color(device: &wgpu::Device) -> anyhow::Result<AtlasRGBA> {
535        let size = 1024u32;
536        let tex = device.create_texture(&wgpu::TextureDescriptor {
537            label: Some("glyph atlas RGBA"),
538            size: wgpu::Extent3d {
539                width: size,
540                height: size,
541                depth_or_array_layers: 1,
542            },
543            mip_level_count: 1,
544            sample_count: 1,
545            dimension: wgpu::TextureDimension::D2,
546            format: wgpu::TextureFormat::Rgba8UnormSrgb,
547            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
548            view_formats: &[],
549        });
550        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
551        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
552            label: Some("glyph atlas sampler RGBA"),
553            address_mode_u: wgpu::AddressMode::ClampToEdge,
554            address_mode_v: wgpu::AddressMode::ClampToEdge,
555            address_mode_w: wgpu::AddressMode::ClampToEdge,
556            mag_filter: wgpu::FilterMode::Linear,
557            min_filter: wgpu::FilterMode::Linear,
558            mipmap_filter: wgpu::FilterMode::Linear,
559            ..Default::default()
560        });
561        Ok(AtlasRGBA {
562            tex,
563            view,
564            sampler,
565            size,
566            next_x: 1,
567            next_y: 1,
568            row_h: 0,
569            map: HashMap::new(),
570        })
571    }
572
573    fn atlas_bind_group_mask(&self) -> wgpu::BindGroup {
574        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
575            label: Some("atlas bind"),
576            layout: &self.text_bind_layout,
577            entries: &[
578                wgpu::BindGroupEntry {
579                    binding: 0,
580                    resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
581                },
582                wgpu::BindGroupEntry {
583                    binding: 1,
584                    resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
585                },
586            ],
587        })
588    }
589    fn atlas_bind_group_color(&self) -> wgpu::BindGroup {
590        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
591            label: Some("atlas bind color"),
592            layout: &self.text_bind_layout,
593            entries: &[
594                wgpu::BindGroupEntry {
595                    binding: 0,
596                    resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
597                },
598                wgpu::BindGroupEntry {
599                    binding: 1,
600                    resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
601                },
602            ],
603        })
604    }
605
606    fn upload_glyph_mask(&mut self, key: repose_text::GlyphKey, px: u32) -> Option<GlyphInfo> {
607        let keyp = (key, px);
608        if let Some(info) = self.atlas_mask.map.get(&keyp) {
609            return Some(*info);
610        }
611
612        let gb = repose_text::rasterize(key, px as f32)?;
613        if gb.w == 0 || gb.h == 0 || gb.data.is_empty() {
614            return None; //Whitespace, but doesn't get inserted?
615        }
616        if !matches!(
617            gb.content,
618            cosmic_text::SwashContent::Mask | cosmic_text::SwashContent::SubpixelMask
619        ) {
620            return None; // handled by color path
621        }
622        let w = gb.w.max(1);
623        let h = gb.h.max(1);
624        // Packing
625        if self.atlas_mask.next_x + w + 1 >= self.atlas_mask.size {
626            self.atlas_mask.next_x = 1;
627            self.atlas_mask.next_y += self.atlas_mask.row_h + 1;
628            self.atlas_mask.row_h = 0;
629        }
630        if self.atlas_mask.next_y + h + 1 >= self.atlas_mask.size {
631            // atlas_mask full
632            return None;
633        }
634        let x = self.atlas_mask.next_x;
635        let y = self.atlas_mask.next_y;
636        self.atlas_mask.next_x += w + 1;
637        self.atlas_mask.row_h = self.atlas_mask.row_h.max(h + 1);
638
639        let buf = gb.data;
640
641        // Upload
642        let layout = wgpu::TexelCopyBufferLayout {
643            offset: 0,
644            bytes_per_row: Some(w),
645            rows_per_image: Some(h),
646        };
647        let size = wgpu::Extent3d {
648            width: w,
649            height: h,
650            depth_or_array_layers: 1,
651        };
652        self.queue.write_texture(
653            wgpu::TexelCopyTextureInfoBase {
654                texture: &self.atlas_mask.tex,
655                mip_level: 0,
656                origin: wgpu::Origin3d { x, y, z: 0 },
657                aspect: wgpu::TextureAspect::All,
658            },
659            &buf,
660            layout,
661            size,
662        );
663
664        let info = GlyphInfo {
665            u0: x as f32 / self.atlas_mask.size as f32,
666            v0: y as f32 / self.atlas_mask.size as f32,
667            u1: (x + w) as f32 / self.atlas_mask.size as f32,
668            v1: (y + h) as f32 / self.atlas_mask.size as f32,
669            w: w as f32,
670            h: h as f32,
671            bearing_x: 0.0, // not used from atlas_mask so take it via shaping
672            bearing_y: 0.0,
673            advance: 0.0,
674        };
675        self.atlas_mask.map.insert(keyp, info);
676        Some(info)
677    }
678    fn upload_glyph_color(&mut self, key: repose_text::GlyphKey, px: u32) -> Option<GlyphInfo> {
679        let keyp = (key, px);
680        if let Some(info) = self.atlas_color.map.get(&keyp) {
681            return Some(*info);
682        }
683        let gb = repose_text::rasterize(key, px as f32)?;
684        if !matches!(gb.content, cosmic_text::SwashContent::Color) {
685            return None;
686        }
687        let w = gb.w.max(1);
688        let h = gb.h.max(1);
689        if !self.alloc_space_color(w, h) {
690            self.grow_color_and_rebuild();
691        }
692        if !self.alloc_space_color(w, h) {
693            return None;
694        }
695        let x = self.atlas_color.next_x;
696        let y = self.atlas_color.next_y;
697        self.atlas_color.next_x += w + 1;
698        self.atlas_color.row_h = self.atlas_color.row_h.max(h + 1);
699
700        let layout = wgpu::TexelCopyBufferLayout {
701            offset: 0,
702            bytes_per_row: Some(w * 4),
703            rows_per_image: Some(h),
704        };
705        let size = wgpu::Extent3d {
706            width: w,
707            height: h,
708            depth_or_array_layers: 1,
709        };
710        self.queue.write_texture(
711            wgpu::TexelCopyTextureInfoBase {
712                texture: &self.atlas_color.tex,
713                mip_level: 0,
714                origin: wgpu::Origin3d { x, y, z: 0 },
715                aspect: wgpu::TextureAspect::All,
716            },
717            &gb.data,
718            layout,
719            size,
720        );
721        let info = GlyphInfo {
722            u0: x as f32 / self.atlas_color.size as f32,
723            v0: y as f32 / self.atlas_color.size as f32,
724            u1: (x + w) as f32 / self.atlas_color.size as f32,
725            v1: (y + h) as f32 / self.atlas_color.size as f32,
726            w: w as f32,
727            h: h as f32,
728            bearing_x: 0.0,
729            bearing_y: 0.0,
730            advance: 0.0,
731        };
732        self.atlas_color.map.insert(keyp, info);
733        Some(info)
734    }
735
736    // Atlas alloc/grow (A8)
737    fn alloc_space_mask(&mut self, w: u32, h: u32) -> bool {
738        if self.atlas_mask.next_x + w + 1 >= self.atlas_mask.size {
739            self.atlas_mask.next_x = 1;
740            self.atlas_mask.next_y += self.atlas_mask.row_h + 1;
741            self.atlas_mask.row_h = 0;
742        }
743        if self.atlas_mask.next_y + h + 1 >= self.atlas_mask.size {
744            return false;
745        }
746        true
747    }
748    fn grow_mask_and_rebuild(&mut self) {
749        let new_size = (self.atlas_mask.size * 2).min(4096);
750        if new_size == self.atlas_mask.size {
751            return;
752        }
753        // recreate texture
754        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
755            label: Some("glyph atlas A8 (grown)"),
756            size: wgpu::Extent3d {
757                width: new_size,
758                height: new_size,
759                depth_or_array_layers: 1,
760            },
761            mip_level_count: 1,
762            sample_count: 1,
763            dimension: wgpu::TextureDimension::D2,
764            format: wgpu::TextureFormat::R8Unorm,
765            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
766            view_formats: &[],
767        });
768        self.atlas_mask.tex = tex;
769        self.atlas_mask.view = self
770            .atlas_mask
771            .tex
772            .create_view(&wgpu::TextureViewDescriptor::default());
773        self.atlas_mask.size = new_size;
774        self.atlas_mask.next_x = 1;
775        self.atlas_mask.next_y = 1;
776        self.atlas_mask.row_h = 0;
777        // rebuild all keys
778        let keys: Vec<(repose_text::GlyphKey, u32)> = self.atlas_mask.map.keys().copied().collect();
779        self.atlas_mask.map.clear();
780        for (k, px) in keys {
781            let _ = self.upload_glyph_mask(k, px);
782        }
783    }
784    // Atlas alloc/grow (RGBA)
785    fn alloc_space_color(&mut self, w: u32, h: u32) -> bool {
786        if self.atlas_color.next_x + w + 1 >= self.atlas_color.size {
787            self.atlas_color.next_x = 1;
788            self.atlas_color.next_y += self.atlas_color.row_h + 1;
789            self.atlas_color.row_h = 0;
790        }
791        if self.atlas_color.next_y + h + 1 >= self.atlas_color.size {
792            return false;
793        }
794        true
795    }
796    fn grow_color_and_rebuild(&mut self) {
797        let new_size = (self.atlas_color.size * 2).min(4096);
798        if new_size == self.atlas_color.size {
799            return;
800        }
801        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
802            label: Some("glyph atlas RGBA (grown)"),
803            size: wgpu::Extent3d {
804                width: new_size,
805                height: new_size,
806                depth_or_array_layers: 1,
807            },
808            mip_level_count: 1,
809            sample_count: 1,
810            dimension: wgpu::TextureDimension::D2,
811            format: wgpu::TextureFormat::Rgba8UnormSrgb,
812            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
813            view_formats: &[],
814        });
815        self.atlas_color.tex = tex;
816        self.atlas_color.view = self
817            .atlas_color
818            .tex
819            .create_view(&wgpu::TextureViewDescriptor::default());
820        self.atlas_color.size = new_size;
821        self.atlas_color.next_x = 1;
822        self.atlas_color.next_y = 1;
823        self.atlas_color.row_h = 0;
824        let keys: Vec<(repose_text::GlyphKey, u32)> =
825            self.atlas_color.map.keys().copied().collect();
826        self.atlas_color.map.clear();
827        for (k, px) in keys {
828            let _ = self.upload_glyph_color(k, px);
829        }
830    }
831}
832
833impl RenderBackend for WgpuBackend {
834    fn configure_surface(&mut self, width: u32, height: u32) {
835        if width == 0 || height == 0 {
836            return;
837        }
838        self.config.width = width;
839        self.config.height = height;
840        self.surface.configure(&self.device, &self.config);
841    }
842
843    fn frame(&mut self, scene: &Scene, _glyph_cfg: GlyphRasterConfig) {
844        if self.config.width == 0 || self.config.height == 0 {
845            return;
846        }
847        let frame = loop {
848            match self.surface.get_current_texture() {
849                Ok(f) => break f,
850                Err(wgpu::SurfaceError::Lost) => {
851                    log::warn!("surface lost; reconfiguring");
852                    self.surface.configure(&self.device, &self.config);
853                }
854                Err(wgpu::SurfaceError::Outdated) => {
855                    log::warn!("surface outdated; reconfiguring");
856                    self.surface.configure(&self.device, &self.config);
857                }
858                Err(wgpu::SurfaceError::Timeout) => {
859                    log::warn!("surface timeout; retrying");
860                    continue;
861                }
862                Err(wgpu::SurfaceError::OutOfMemory) => {
863                    log::error!("surface OOM");
864                    return;
865                }
866                Err(wgpu::SurfaceError::Other) => {
867                    log::error!("Other error");
868                    return;
869                }
870            }
871        };
872        let view = frame
873            .texture
874            .create_view(&wgpu::TextureViewDescriptor::default());
875
876        // Helper: pixels -> NDC
877        fn to_ndc(x: f32, y: f32, w: f32, h: f32, fb_w: f32, fb_h: f32) -> [f32; 4] {
878            let x0 = (x / fb_w) * 2.0 - 1.0;
879            let y0 = 1.0 - (y / fb_h) * 2.0;
880            let x1 = ((x + w) / fb_w) * 2.0 - 1.0;
881            let y1 = 1.0 - ((y + h) / fb_h) * 2.0;
882            let min_x = x0.min(x1);
883            let min_y = y0.min(y1);
884            let w_ndc = (x1 - x0).abs();
885            let h_ndc = (y1 - y0).abs();
886            [min_x, min_y, w_ndc, h_ndc]
887        }
888        fn to_ndc_scalar(px: f32, fb_dim: f32) -> f32 {
889            (px / fb_dim) * 2.0
890        }
891        fn to_ndc_radius(r: f32, fb_w: f32, fb_h: f32) -> f32 {
892            let rx = to_ndc_scalar(r, fb_w);
893            let ry = to_ndc_scalar(r, fb_h);
894            rx.min(ry)
895        }
896        fn to_ndc_stroke(w: f32, fb_w: f32, fb_h: f32) -> f32 {
897            let sx = to_ndc_scalar(w, fb_w);
898            let sy = to_ndc_scalar(w, fb_h);
899            sx.min(sy)
900        }
901        fn to_scissor(r: &repose_core::Rect, fb_w: u32, fb_h: u32) -> (u32, u32, u32, u32) {
902            let x = r.x.max(0.0).floor() as u32;
903            let y = r.y.max(0.0).floor() as u32;
904            let w = ((r.w.max(0.0).ceil() as u32).min(fb_w.saturating_sub(x))).max(1);
905            let h = ((r.h.max(0.0).ceil() as u32).min(fb_h.saturating_sub(y))).max(1);
906            (x, y, w, h)
907        }
908
909        let fb_w = self.config.width as f32;
910        let fb_h = self.config.height as f32;
911
912        // Prebuild draw commands, batching per pipeline between clip boundaries
913        enum Cmd {
914            SetClipPush(repose_core::Rect),
915            SetClipPop,
916            Rect { off: u64, cnt: u32 },
917            Border { off: u64, cnt: u32 },
918            GlyphsMask { off: u64, cnt: u32 },
919            GlyphsColor { off: u64, cnt: u32 },
920            PushTransform(Transform),
921            PopTransform,
922        }
923        let mut cmds: Vec<Cmd> = Vec::with_capacity(scene.nodes.len());
924        struct Batch {
925            rects: Vec<RectInstance>,
926            borders: Vec<BorderInstance>,
927            masks: Vec<GlyphInstance>,
928            colors: Vec<GlyphInstance>,
929        }
930        impl Batch {
931            fn new() -> Self {
932                Self {
933                    rects: vec![],
934                    borders: vec![],
935                    masks: vec![],
936                    colors: vec![],
937                }
938            }
939
940            fn flush(
941                &mut self,
942                rings: (
943                    &mut UploadRing,
944                    &mut UploadRing,
945                    &mut UploadRing,
946                    &mut UploadRing,
947                ),
948                queue: &wgpu::Queue,
949                cmds: &mut Vec<Cmd>,
950            ) {
951                let (ring_rect, ring_border, ring_mask, ring_color) = rings;
952
953                if !self.rects.is_empty() {
954                    let bytes = bytemuck::cast_slice(&self.rects);
955                    let (off, wrote) = ring_rect.alloc_write(queue, bytes);
956                    debug_assert_eq!(wrote as usize, bytes.len());
957                    cmds.push(Cmd::Rect {
958                        off,
959                        cnt: self.rects.len() as u32,
960                    });
961                    self.rects.clear();
962                }
963                if !self.borders.is_empty() {
964                    let bytes = bytemuck::cast_slice(&self.borders);
965                    let (off, wrote) = ring_border.alloc_write(queue, bytes);
966                    debug_assert_eq!(wrote as usize, bytes.len());
967                    cmds.push(Cmd::Border {
968                        off,
969                        cnt: self.borders.len() as u32,
970                    });
971                    self.borders.clear();
972                }
973                if !self.masks.is_empty() {
974                    let bytes = bytemuck::cast_slice(&self.masks);
975                    let (off, wrote) = ring_mask.alloc_write(queue, bytes);
976                    debug_assert_eq!(wrote as usize, bytes.len());
977                    cmds.push(Cmd::GlyphsMask {
978                        off,
979                        cnt: self.masks.len() as u32,
980                    });
981                    self.masks.clear();
982                }
983                if !self.colors.is_empty() {
984                    let bytes = bytemuck::cast_slice(&self.colors);
985                    let (off, wrote) = ring_color.alloc_write(queue, bytes);
986                    debug_assert_eq!(wrote as usize, bytes.len());
987                    cmds.push(Cmd::GlyphsColor {
988                        off,
989                        cnt: self.colors.len() as u32,
990                    });
991                    self.colors.clear();
992                }
993            }
994        }
995        // per frame
996        self.ring_rect.reset();
997        self.ring_border.reset();
998        self.ring_glyph_mask.reset();
999        self.ring_glyph_color.reset();
1000        let mut batch = Batch::new();
1001
1002        let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
1003
1004        for node in &scene.nodes {
1005            let t_identity = Transform::identity();
1006            let current_transform = transform_stack.last().unwrap_or(&t_identity);
1007
1008            match node {
1009                SceneNode::Rect {
1010                    rect,
1011                    color,
1012                    radius,
1013                } => {
1014                    let transformed_rect = current_transform.apply_to_rect(*rect);
1015                    batch.rects.push(RectInstance {
1016                        xywh: to_ndc(
1017                            transformed_rect.x,
1018                            transformed_rect.y,
1019                            transformed_rect.w,
1020                            transformed_rect.h,
1021                            fb_w,
1022                            fb_h,
1023                        ),
1024                        radius: to_ndc_radius(*radius, fb_w, fb_h),
1025                        color: color.to_linear(),
1026                    });
1027                }
1028                SceneNode::Border {
1029                    rect,
1030                    color,
1031                    width,
1032                    radius,
1033                } => {
1034                    let transformed_rect = current_transform.apply_to_rect(*rect);
1035
1036                    batch.borders.push(BorderInstance {
1037                        xywh: to_ndc(
1038                            transformed_rect.x,
1039                            transformed_rect.y,
1040                            transformed_rect.w,
1041                            transformed_rect.h,
1042                            fb_w,
1043                            fb_h,
1044                        ),
1045                        radius_outer: to_ndc_radius(*radius, fb_w, fb_h),
1046                        stroke: to_ndc_stroke(*width, fb_w, fb_h),
1047                        color: color.to_linear(),
1048                    });
1049                }
1050                SceneNode::Text {
1051                    rect,
1052                    text,
1053                    color,
1054                    size,
1055                } => {
1056                    let px = (*size).clamp(8.0, 96.0);
1057                    let shaped = repose_text::shape_line(text, px);
1058                    for sg in shaped {
1059                        // Try color first; if not color, try mask
1060                        if let Some(info) = self.upload_glyph_color(sg.key, px as u32) {
1061                            let x = rect.x + sg.x + sg.bearing_x;
1062                            let y = rect.y + sg.y - sg.bearing_y;
1063                            batch.colors.push(GlyphInstance {
1064                                xywh: to_ndc(x, y, info.w, info.h, fb_w, fb_h),
1065                                uv: [info.u0, info.v1, info.u1, info.v0],
1066                                color: [1.0, 1.0, 1.0, 1.0], // do not tint color glyphs
1067                            });
1068                        } else if let Some(info) = self.upload_glyph_mask(sg.key, px as u32) {
1069                            let x = rect.x + sg.x + sg.bearing_x;
1070                            let y = rect.y + sg.y - sg.bearing_y;
1071                            batch.masks.push(GlyphInstance {
1072                                xywh: to_ndc(x, y, info.w, info.h, fb_w, fb_h),
1073                                uv: [info.u0, info.v1, info.u1, info.v0],
1074                                color: color.to_linear(),
1075                            });
1076                        }
1077                    }
1078                }
1079                SceneNode::PushClip { rect, .. } => {
1080                    batch.flush(
1081                        (
1082                            &mut self.ring_rect,
1083                            &mut self.ring_border,
1084                            &mut self.ring_glyph_mask,
1085                            &mut self.ring_glyph_color,
1086                        ),
1087                        &self.queue,
1088                        &mut cmds,
1089                    );
1090                    cmds.push(Cmd::SetClipPush(*rect));
1091                }
1092                SceneNode::PopClip => {
1093                    batch.flush(
1094                        (
1095                            &mut self.ring_rect,
1096                            &mut self.ring_border,
1097                            &mut self.ring_glyph_mask,
1098                            &mut self.ring_glyph_color,
1099                        ),
1100                        &self.queue,
1101                        &mut cmds,
1102                    );
1103                    cmds.push(Cmd::SetClipPop);
1104                }
1105                SceneNode::PushTransform { transform } => {
1106                    let combined = current_transform.combine(transform);
1107                    transform_stack.push(combined);
1108                }
1109                SceneNode::PopTransform => {
1110                    transform_stack.pop();
1111                }
1112            }
1113            // flush trailing batch
1114            batch.flush(
1115                (
1116                    &mut self.ring_rect,
1117                    &mut self.ring_border,
1118                    &mut self.ring_glyph_mask,
1119                    &mut self.ring_glyph_color,
1120                ),
1121                &self.queue,
1122                &mut cmds,
1123            );
1124        }
1125
1126        let mut encoder = self
1127            .device
1128            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1129                label: Some("frame encoder"),
1130            });
1131
1132        {
1133            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1134                label: Some("main pass"),
1135                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1136                    view: &view,
1137                    resolve_target: None,
1138                    ops: wgpu::Operations {
1139                        load: wgpu::LoadOp::Clear(wgpu::Color {
1140                            r: scene.clear_color.0 as f64 / 255.0,
1141                            g: scene.clear_color.1 as f64 / 255.0,
1142                            b: scene.clear_color.2 as f64 / 255.0,
1143                            a: scene.clear_color.3 as f64 / 255.0,
1144                        }),
1145                        store: wgpu::StoreOp::Store,
1146                    },
1147                    depth_slice: None,
1148                })],
1149                depth_stencil_attachment: None,
1150                timestamp_writes: None,
1151                occlusion_query_set: None,
1152            });
1153
1154            // initial full scissor
1155            rpass.set_scissor_rect(0, 0, self.config.width, self.config.height);
1156            let bind_mask = self.atlas_bind_group_mask();
1157            let bind_color = self.atlas_bind_group_color();
1158            let root_clip = repose_core::Rect {
1159                x: 0.0,
1160                y: 0.0,
1161                w: fb_w,
1162                h: fb_h,
1163            };
1164            let mut clip_stack: Vec<repose_core::Rect> = Vec::with_capacity(8);
1165
1166            for cmd in cmds {
1167                match cmd {
1168                    Cmd::SetClipPush(r) => {
1169                        let top = clip_stack.last().copied().unwrap_or(root_clip);
1170                        let next = intersect(top, r);
1171
1172                        // Validate clip rect
1173                        let next = repose_core::Rect {
1174                            x: next.x.max(0.0),
1175                            y: next.y.max(0.0),
1176                            w: next.w.max(1.0), // Minimum 1px to avoid GPU issues
1177                            h: next.h.max(1.0),
1178                        };
1179
1180                        clip_stack.push(next);
1181                        let (x, y, w, h) = to_scissor(&next, self.config.width, self.config.height);
1182                        rpass.set_scissor_rect(x, y, w, h);
1183                    }
1184                    Cmd::SetClipPop => {
1185                        if !clip_stack.is_empty() {
1186                            clip_stack.pop();
1187                        } else {
1188                            log::warn!("PopClip with empty stack");
1189                        }
1190
1191                        let top = clip_stack.last().copied().unwrap_or(root_clip);
1192                        let (x, y, w, h) = to_scissor(&top, self.config.width, self.config.height);
1193                        rpass.set_scissor_rect(x, y, w, h);
1194                    }
1195
1196                    Cmd::Rect { off, cnt: n } => {
1197                        rpass.set_pipeline(&self.rect_pipeline);
1198                        let bytes = (n as u64) * std::mem::size_of::<RectInstance>() as u64;
1199                        rpass.set_vertex_buffer(0, self.ring_rect.buf.slice(off..off + bytes));
1200                        rpass.draw(0..6, 0..n);
1201                    }
1202                    Cmd::Border { off, cnt: n } => {
1203                        rpass.set_pipeline(&self.border_pipeline);
1204                        let bytes = (n as u64) * std::mem::size_of::<BorderInstance>() as u64;
1205                        rpass.set_vertex_buffer(0, self.ring_border.buf.slice(off..off + bytes));
1206                        rpass.draw(0..6, 0..n);
1207                    }
1208                    Cmd::GlyphsMask { off, cnt: n } => {
1209                        rpass.set_pipeline(&self.text_pipeline_mask);
1210                        rpass.set_bind_group(0, &bind_mask, &[]);
1211                        let bytes = (n as u64) * std::mem::size_of::<GlyphInstance>() as u64;
1212                        rpass
1213                            .set_vertex_buffer(0, self.ring_glyph_mask.buf.slice(off..off + bytes));
1214                        rpass.draw(0..6, 0..n);
1215                    }
1216                    Cmd::GlyphsColor { off, cnt: n } => {
1217                        rpass.set_pipeline(&self.text_pipeline_color);
1218                        rpass.set_bind_group(0, &bind_color, &[]);
1219                        let bytes = (n as u64) * std::mem::size_of::<GlyphInstance>() as u64;
1220                        rpass.set_vertex_buffer(
1221                            0,
1222                            self.ring_glyph_color.buf.slice(off..off + bytes),
1223                        );
1224                        rpass.draw(0..6, 0..n);
1225                    }
1226                    Cmd::PushTransform(transform) => {}
1227                    Cmd::PopTransform => {}
1228                }
1229            }
1230        }
1231
1232        self.queue.submit(std::iter::once(encoder.finish()));
1233        if let Err(e) = catch_unwind(AssertUnwindSafe(|| frame.present())) {
1234            log::warn!("frame.present panicked: {:?}", e);
1235        }
1236    }
1237}
1238
1239fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> repose_core::Rect {
1240    let x0 = a.x.max(b.x);
1241    let y0 = a.y.max(b.y);
1242    let x1 = (a.x + a.w).min(b.x + b.w);
1243    let y1 = (a.y + a.h).min(b.y + b.h);
1244    repose_core::Rect {
1245        x: x0,
1246        y: y0,
1247        w: (x1 - x0).max(0.0),
1248        h: (y1 - y0).max(0.0),
1249    }
1250}