repose_render_wgpu/
lib.rs

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