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