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