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