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