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; let start = (self.head + (align - 1)) & !(align - 1);
53 let end = start + len;
54 if end > self.cap {
55 self.head = 0;
57 let start = 0;
58 let end = len.min(self.cap);
59 queue.write_buffer(&self.buf, start, &bytes[0..end as usize]);
60 self.head = end;
61 (start, len.min(self.cap - start))
62 } else {
63 queue.write_buffer(&self.buf, start, bytes);
64 self.head = end;
65 (start, len)
66 }
67 }
68}
69
70pub struct WgpuBackend {
71 surface: wgpu::Surface<'static>,
72 device: wgpu::Device,
73 queue: wgpu::Queue,
74 config: wgpu::SurfaceConfiguration,
75
76 rect_pipeline: wgpu::RenderPipeline,
77 border_pipeline: wgpu::RenderPipeline,
79 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 atlas_mask: AtlasA8,
88 atlas_color: AtlasRGBA,
89
90 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 xywh: [f32; 4],
149 radius: f32,
151 color: [f32; 4],
153}
154
155#[repr(C)]
156#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
157struct BorderInstance {
158 xywh: [f32; 4],
160 radius: f32,
162 stroke: f32,
164 color: [f32; 4],
166}
167
168#[repr(C)]
169#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
170struct EllipseInstance {
171 xywh: [f32; 4],
173 color: [f32; 4],
175}
176
177#[repr(C)]
178#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
179struct EllipseBorderInstance {
180 xywh: [f32; 4],
182 stroke: f32,
184 color: [f32; 4],
186}
187
188#[repr(C)]
189#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
190struct GlyphInstance {
191 xywh: [f32; 4],
193 uv: [f32; 4],
195 color: [f32; 4],
197}
198
199impl WgpuBackend {
200 pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
201 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::from_env_or_default());
203 let surface = instance.create_surface(window.clone())?;
204
205 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()) .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 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: &[], 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 wgpu::VertexAttribute {
278 shader_location: 0,
279 offset: 0,
280 format: wgpu::VertexFormat::Float32x4,
281 },
282 wgpu::VertexAttribute {
284 shader_location: 1,
285 offset: 16,
286 format: wgpu::VertexFormat::Float32,
287 },
288 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 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: &[], 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 wgpu::VertexAttribute {
343 shader_location: 0,
344 offset: 0,
345 format: wgpu::VertexFormat::Float32x4,
346 },
347 wgpu::VertexAttribute {
349 shader_location: 1,
350 offset: 16,
351 format: wgpu::VertexFormat::Float32,
352 },
353 wgpu::VertexAttribute {
355 shader_location: 2,
356 offset: 20,
357 format: wgpu::VertexFormat::Float32,
358 },
359 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 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 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 let atlas_mask = Self::init_atlas_mask(&device)?;
627 let atlas_color = Self::init_atlas_color(&device)?;
628
629 let ring_rect = UploadRing::new(&device, "ring rect", 1 << 20); 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 border_pipeline,
645 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 let img = image::load_from_memory(data).expect("decode image");
667 let rgba = img.to_rgba8();
668 let (w, h) = rgba.dimensions();
669 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, 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; }
851 if !matches!(
852 gb.content,
853 cosmic_text::SwashContent::Mask | cosmic_text::SwashContent::SubpixelMask
854 ) {
855 return None; }
857 let w = gb.w.max(1);
858 let h = gb.h.max(1);
859 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 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, 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 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 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 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 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 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 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 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 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 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 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], });
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 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 let overflow_x = (content_w - dst_w) * 0.5;
1428 let overflow_y = (content_h - dst_h) * 0.5;
1429 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 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 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 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}