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