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