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