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}
17
18impl UploadRing {
19 fn new(device: &wgpu::Device, label: &str, cap: u64) -> Self {
20 let buf = device.create_buffer(&wgpu::BufferDescriptor {
21 label: Some(label),
22 size: cap,
23 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
24 mapped_at_creation: false,
25 });
26 Self { buf, cap, head: 0 }
27 }
28
29 fn reset(&mut self) {
30 self.head = 0;
31 }
32
33 fn grow_to_fit(&mut self, device: &wgpu::Device, needed: u64) {
34 if needed <= self.cap {
35 return;
36 }
37 let new_cap = needed.next_power_of_two();
38 self.buf = device.create_buffer(&wgpu::BufferDescriptor {
39 label: Some("upload ring (grown)"),
40 size: new_cap,
41 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
42 mapped_at_creation: false,
43 });
44 self.cap = new_cap;
45 self.head = 0;
46 }
47
48 fn alloc_write(&mut self, queue: &wgpu::Queue, bytes: &[u8]) -> (u64, u64) {
49 let len = bytes.len() as u64;
50 let start = (self.head + 3) & !3; let end = start + len;
52 assert!(end <= self.cap, "ring overflow - call grow_to_fit first");
53 queue.write_buffer(&self.buf, start, bytes);
54 self.head = end;
55 (start, len)
56 }
57}
58
59struct InstancedPipe<I: bytemuck::Pod> {
60 pipeline: wgpu::RenderPipeline,
61 ring: UploadRing,
62 stride: u64,
63 _marker: std::marker::PhantomData<I>,
64}
65
66impl<I: bytemuck::Pod> InstancedPipe<I> {
67 fn new(pipeline: wgpu::RenderPipeline, ring: UploadRing) -> Self {
68 Self {
69 pipeline,
70 ring,
71 stride: std::mem::size_of::<I>() as u64,
72 _marker: std::marker::PhantomData,
73 }
74 }
75
76 fn upload(
77 &mut self,
78 device: &wgpu::Device,
79 queue: &wgpu::Queue,
80 data: &[I],
81 ) -> Option<(u64, u32)> {
82 if data.is_empty() {
83 return None;
84 }
85 let bytes = bytemuck::cast_slice(data);
86 self.ring.grow_to_fit(device, bytes.len() as u64);
87 let (off, wrote) = self.ring.alloc_write(queue, bytes);
88 debug_assert_eq!(wrote as usize, bytes.len());
89 Some((off, data.len() as u32))
90 }
91
92 fn bind<'a>(&'a self, rpass: &mut wgpu::RenderPass<'a>, off: u64, cnt: u32) {
93 let bytes = (cnt as u64) * self.stride;
94 rpass.set_pipeline(&self.pipeline);
95 rpass.set_vertex_buffer(0, self.ring.buf.slice(off..off + bytes));
96 rpass.draw(0..6, 0..cnt);
97 }
98
99 fn reset(&mut self) {
100 self.ring.reset();
101 }
102}
103
104#[repr(C)]
105#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
106struct Globals {
107 ndc_to_px: [f32; 2],
108 _pad: [f32; 2],
109}
110
111pub struct WgpuBackend {
112 surface: wgpu::Surface<'static>,
113 device: wgpu::Device,
114 queue: wgpu::Queue,
115 config: wgpu::SurfaceConfiguration,
116
117 rects: InstancedPipe<RectInstance>,
119 borders: InstancedPipe<BorderInstance>,
120 ellipses: InstancedPipe<EllipseInstance>,
121 ellipse_borders: InstancedPipe<EllipseBorderInstance>,
122 glyph_mask: InstancedPipe<GlyphInstance>,
123 glyph_color: InstancedPipe<GlyphInstance>,
124
125 image_pipeline_rgba: wgpu::RenderPipeline,
127 image_pipeline_nv12: wgpu::RenderPipeline,
128 image_bind_layout_rgba: wgpu::BindGroupLayout,
129 image_bind_layout_nv12: wgpu::BindGroupLayout,
130 image_sampler: wgpu::Sampler,
131
132 text_bind_layout: wgpu::BindGroupLayout,
133
134 clip_pipeline_a2c: wgpu::RenderPipeline,
136 clip_pipeline_bin: wgpu::RenderPipeline,
137 clip_ring: UploadRing,
138
139 nv12: InstancedPipe<Nv12Instance>,
141
142 msaa_samples: u32,
143
144 depth_stencil_tex: wgpu::Texture,
146 depth_stencil_view: wgpu::TextureView,
147
148 msaa_tex: Option<wgpu::Texture>,
150 msaa_view: Option<wgpu::TextureView>,
151
152 globals_layout: wgpu::BindGroupLayout,
153 globals_buf: wgpu::Buffer,
154 globals_bind: wgpu::BindGroup,
155
156 atlas_mask: AtlasA8,
158 atlas_color: AtlasRGBA,
159
160 next_image_handle: u64,
162 images: HashMap<u64, ImageTex>,
163
164 frame_index: u64,
166 image_bytes_total: u64,
167 image_evict_after_frames: u64,
168 image_budget_bytes: u64,
169}
170
171enum ImageTex {
172 Rgba {
173 tex: wgpu::Texture,
174 view: wgpu::TextureView,
175 bind: wgpu::BindGroup,
176 w: u32,
177 h: u32,
178 format: wgpu::TextureFormat,
179 last_used_frame: u64,
180 bytes: u64,
181 },
182 Nv12 {
183 tex_y: wgpu::Texture,
184 view_y: wgpu::TextureView,
185 tex_uv: wgpu::Texture,
186 view_uv: wgpu::TextureView,
187 bind: wgpu::BindGroup,
188 w: u32,
189 h: u32,
190 full_range: bool,
191 last_used_frame: u64,
192 bytes: u64,
193 },
194}
195
196struct AtlasA8 {
197 tex: wgpu::Texture,
198 view: wgpu::TextureView,
199 sampler: wgpu::Sampler,
200 size: u32,
201 next_x: u32,
202 next_y: u32,
203 row_h: u32,
204 map: HashMap<(repose_text::GlyphKey, u32), GlyphInfo>,
205}
206
207struct AtlasRGBA {
208 tex: wgpu::Texture,
209 view: wgpu::TextureView,
210 sampler: wgpu::Sampler,
211 size: u32,
212 next_x: u32,
213 next_y: u32,
214 row_h: u32,
215 map: HashMap<(repose_text::GlyphKey, u32), GlyphInfo>,
216}
217
218#[derive(Clone, Copy)]
219struct GlyphInfo {
220 u0: f32,
221 v0: f32,
222 u1: f32,
223 v1: f32,
224 w: f32,
225 h: f32,
226 bearing_x: f32,
227 bearing_y: f32,
228 advance: f32,
229}
230
231#[repr(C)]
232#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
233struct RectInstance {
234 xywh: [f32; 4],
235 radius: f32,
236 brush_type: u32,
237 color0: [f32; 4],
238 color1: [f32; 4],
239 grad_start: [f32; 2],
240 grad_end: [f32; 2],
241}
242
243#[repr(C)]
244#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
245struct BorderInstance {
246 xywh: [f32; 4],
247 radius: f32,
248 stroke: f32,
249 color: [f32; 4],
250}
251
252#[repr(C)]
253#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
254struct EllipseInstance {
255 xywh: [f32; 4],
256 color: [f32; 4],
257}
258
259#[repr(C)]
260#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
261struct EllipseBorderInstance {
262 xywh: [f32; 4],
263 stroke: f32,
264 color: [f32; 4],
265}
266
267#[repr(C)]
268#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
269struct GlyphInstance {
270 xywh: [f32; 4],
271 uv: [f32; 4],
272 color: [f32; 4],
273}
274
275#[repr(C)]
276#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
277struct Nv12Instance {
278 xywh: [f32; 4],
279 uv: [f32; 4],
280 color: [f32; 4], full_range: f32,
282 _pad: [f32; 3],
283}
284
285#[repr(C)]
286#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
287struct ClipInstance {
288 xywh: [f32; 4],
289 radius: f32,
290 _pad: [f32; 3],
291}
292
293fn swash_to_a8_coverage(content: cosmic_text::SwashContent, data: &[u8]) -> Option<Vec<u8>> {
294 match content {
295 cosmic_text::SwashContent::Mask => Some(data.to_vec()),
296 cosmic_text::SwashContent::SubpixelMask => {
297 let mut out = Vec::with_capacity(data.len() / 4);
298 for px in data.chunks_exact(4) {
299 let r = px[0];
300 let g = px[1];
301 let b = px[2];
302 out.push(r.max(g).max(b));
303 }
304 Some(out)
305 }
306 cosmic_text::SwashContent::Color => None,
307 }
308}
309
310impl WgpuBackend {
311 pub async fn new_async(window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
312 let instance: Instance;
313
314 if cfg!(target_arch = "wasm32") {
315 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
316 desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
317 instance = wgpu::util::new_instance_with_webgpu_detection(desc).await;
318 } else {
319 instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
320 };
321
322 let surface = instance.create_surface(window.clone())?;
323
324 let adapter = instance
325 .request_adapter(&wgpu::RequestAdapterOptions {
326 power_preference: wgpu::PowerPreference::HighPerformance,
327 compatible_surface: Some(&surface),
328 force_fallback_adapter: false,
329 })
330 .await
331 .map_err(|e| anyhow::anyhow!("No suitable adapter: {e:?}"))?;
332
333 let limits = if cfg!(target_arch = "wasm32") {
334 wgpu::Limits::downlevel_webgl2_defaults()
335 } else {
336 wgpu::Limits::default()
337 };
338
339 let (device, queue) = adapter
340 .request_device(&wgpu::DeviceDescriptor {
341 label: Some("repose-rs device"),
342 required_features: wgpu::Features::empty(),
343 required_limits: limits,
344 experimental_features: wgpu::ExperimentalFeatures::disabled(),
345 memory_hints: wgpu::MemoryHints::default(),
346 trace: wgpu::Trace::Off,
347 })
348 .await
349 .map_err(|e| anyhow::anyhow!("request_device failed: {e:?}"))?;
350
351 let size = window.inner_size();
352
353 let caps = surface.get_capabilities(&adapter);
354 let format = caps
355 .formats
356 .iter()
357 .copied()
358 .find(|f| f.is_srgb())
359 .unwrap_or(caps.formats[0]);
360 let present_mode = caps
361 .present_modes
362 .iter()
363 .copied()
364 .find(|m| *m == wgpu::PresentMode::Mailbox || *m == wgpu::PresentMode::Immediate)
365 .unwrap_or(wgpu::PresentMode::Fifo);
366 let alpha_mode = caps.alpha_modes[0];
367
368 let config = wgpu::SurfaceConfiguration {
369 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
370 format,
371 width: size.width.max(1),
372 height: size.height.max(1),
373 present_mode,
374 alpha_mode,
375 view_formats: vec![],
376 desired_maximum_frame_latency: 2,
377 };
378 surface.configure(&device, &config);
379
380 let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
381 label: Some("globals layout"),
382 entries: &[wgpu::BindGroupLayoutEntry {
383 binding: 0,
384 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
385 ty: wgpu::BindingType::Buffer {
386 ty: wgpu::BufferBindingType::Uniform,
387 has_dynamic_offset: false,
388 min_binding_size: None,
389 },
390 count: None,
391 }],
392 });
393
394 let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
395 label: Some("globals buf"),
396 size: std::mem::size_of::<Globals>() as u64,
397 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
398 mapped_at_creation: false,
399 });
400
401 let globals_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
402 label: Some("globals bind"),
403 layout: &globals_layout,
404 entries: &[wgpu::BindGroupEntry {
405 binding: 0,
406 resource: globals_buf.as_entire_binding(),
407 }],
408 });
409
410 let fmt_features = adapter.get_texture_format_features(format);
412 let msaa_samples = if fmt_features.flags.sample_count_supported(4)
413 && fmt_features
414 .flags
415 .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE)
416 {
417 4
418 } else {
419 1
420 };
421
422 let ds_format = wgpu::TextureFormat::Depth24PlusStencil8;
423
424 let stencil_for_content = wgpu::DepthStencilState {
425 format: ds_format,
426 depth_write_enabled: Some(false),
427 depth_compare: Some(wgpu::CompareFunction::Always),
428 stencil: wgpu::StencilState {
429 front: wgpu::StencilFaceState {
430 compare: wgpu::CompareFunction::LessEqual,
431 fail_op: wgpu::StencilOperation::Keep,
432 depth_fail_op: wgpu::StencilOperation::Keep,
433 pass_op: wgpu::StencilOperation::Keep,
434 },
435 back: wgpu::StencilFaceState {
436 compare: wgpu::CompareFunction::LessEqual,
437 fail_op: wgpu::StencilOperation::Keep,
438 depth_fail_op: wgpu::StencilOperation::Keep,
439 pass_op: wgpu::StencilOperation::Keep,
440 },
441 read_mask: 0xFF,
442 write_mask: 0x00,
443 },
444 bias: wgpu::DepthBiasState::default(),
445 };
446
447 let stencil_for_clip_inc = wgpu::DepthStencilState {
448 format: ds_format,
449 depth_write_enabled: Some(false),
450 depth_compare: Some(wgpu::CompareFunction::Always),
451 stencil: wgpu::StencilState {
452 front: wgpu::StencilFaceState {
453 compare: wgpu::CompareFunction::Equal,
454 fail_op: wgpu::StencilOperation::Keep,
455 depth_fail_op: wgpu::StencilOperation::Keep,
456 pass_op: wgpu::StencilOperation::IncrementClamp,
457 },
458 back: wgpu::StencilFaceState {
459 compare: wgpu::CompareFunction::Equal,
460 fail_op: wgpu::StencilOperation::Keep,
461 depth_fail_op: wgpu::StencilOperation::Keep,
462 pass_op: wgpu::StencilOperation::IncrementClamp,
463 },
464 read_mask: 0xFF,
465 write_mask: 0xFF,
466 },
467 bias: wgpu::DepthBiasState::default(),
468 };
469
470 let multisample_state = wgpu::MultisampleState {
471 count: msaa_samples,
472 mask: !0,
473 alpha_to_coverage_enabled: false,
474 };
475
476 let rect_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
480 label: Some("rect.wgsl"),
481 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/rect.wgsl"))),
482 });
483 let rect_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
484 label: Some("rect pipeline layout"),
485 bind_group_layouts: &[Some(&globals_layout)],
486 immediate_size: 0,
487 });
488 let rect_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
489 label: Some("rect pipeline"),
490 layout: Some(&rect_pipeline_layout),
491 vertex: wgpu::VertexState {
492 module: &rect_shader,
493 entry_point: Some("vs_main"),
494 buffers: &[wgpu::VertexBufferLayout {
495 array_stride: std::mem::size_of::<RectInstance>() as u64,
496 step_mode: wgpu::VertexStepMode::Instance,
497 attributes: &[
498 wgpu::VertexAttribute {
499 shader_location: 0,
500 offset: 0,
501 format: wgpu::VertexFormat::Float32x4,
502 },
503 wgpu::VertexAttribute {
504 shader_location: 1,
505 offset: 16,
506 format: wgpu::VertexFormat::Float32,
507 },
508 wgpu::VertexAttribute {
509 shader_location: 2,
510 offset: 20,
511 format: wgpu::VertexFormat::Uint32,
512 },
513 wgpu::VertexAttribute {
514 shader_location: 3,
515 offset: 24,
516 format: wgpu::VertexFormat::Float32x4,
517 },
518 wgpu::VertexAttribute {
519 shader_location: 4,
520 offset: 40,
521 format: wgpu::VertexFormat::Float32x4,
522 },
523 wgpu::VertexAttribute {
524 shader_location: 5,
525 offset: 56,
526 format: wgpu::VertexFormat::Float32x2,
527 },
528 wgpu::VertexAttribute {
529 shader_location: 6,
530 offset: 64,
531 format: wgpu::VertexFormat::Float32x2,
532 },
533 ],
534 }],
535 compilation_options: wgpu::PipelineCompilationOptions::default(),
536 },
537 fragment: Some(wgpu::FragmentState {
538 module: &rect_shader,
539 entry_point: Some("fs_main"),
540 targets: &[Some(wgpu::ColorTargetState {
541 format: config.format,
542 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
543 write_mask: wgpu::ColorWrites::ALL,
544 })],
545 compilation_options: wgpu::PipelineCompilationOptions::default(),
546 }),
547 primitive: wgpu::PrimitiveState::default(),
548 depth_stencil: Some(stencil_for_content.clone()),
549 multisample: multisample_state,
550 multiview_mask: None,
551 cache: None,
552 });
553
554 let border_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
556 label: Some("border.wgsl"),
557 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/border.wgsl"))),
558 });
559 let border_pipeline_layout =
560 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
561 label: Some("border pipeline layout"),
562 bind_group_layouts: &[Some(&globals_layout)],
563 immediate_size: 0,
564 });
565 let border_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
566 label: Some("border pipeline"),
567 layout: Some(&border_pipeline_layout),
568 vertex: wgpu::VertexState {
569 module: &border_shader,
570 entry_point: Some("vs_main"),
571 buffers: &[wgpu::VertexBufferLayout {
572 array_stride: std::mem::size_of::<BorderInstance>() as u64,
573 step_mode: wgpu::VertexStepMode::Instance,
574 attributes: &[
575 wgpu::VertexAttribute {
576 shader_location: 0,
577 offset: 0,
578 format: wgpu::VertexFormat::Float32x4,
579 },
580 wgpu::VertexAttribute {
581 shader_location: 1,
582 offset: 16,
583 format: wgpu::VertexFormat::Float32,
584 },
585 wgpu::VertexAttribute {
586 shader_location: 2,
587 offset: 20,
588 format: wgpu::VertexFormat::Float32,
589 },
590 wgpu::VertexAttribute {
591 shader_location: 3,
592 offset: 24,
593 format: wgpu::VertexFormat::Float32x4,
594 },
595 ],
596 }],
597 compilation_options: wgpu::PipelineCompilationOptions::default(),
598 },
599 fragment: Some(wgpu::FragmentState {
600 module: &border_shader,
601 entry_point: Some("fs_main"),
602 targets: &[Some(wgpu::ColorTargetState {
603 format: config.format,
604 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
605 write_mask: wgpu::ColorWrites::ALL,
606 })],
607 compilation_options: wgpu::PipelineCompilationOptions::default(),
608 }),
609 primitive: wgpu::PrimitiveState::default(),
610 depth_stencil: Some(stencil_for_content.clone()),
611 multisample: multisample_state,
612 multiview_mask: None,
613 cache: None,
614 });
615
616 let ellipse_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
618 label: Some("ellipse.wgsl"),
619 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/ellipse.wgsl"))),
620 });
621 let ellipse_pipeline_layout =
622 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
623 label: Some("ellipse pipeline layout"),
624 bind_group_layouts: &[Some(&globals_layout)],
625 immediate_size: 0,
626 });
627 let ellipse_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
628 label: Some("ellipse pipeline"),
629 layout: Some(&ellipse_pipeline_layout),
630 vertex: wgpu::VertexState {
631 module: &ellipse_shader,
632 entry_point: Some("vs_main"),
633 buffers: &[wgpu::VertexBufferLayout {
634 array_stride: std::mem::size_of::<EllipseInstance>() as u64,
635 step_mode: wgpu::VertexStepMode::Instance,
636 attributes: &[
637 wgpu::VertexAttribute {
638 shader_location: 0,
639 offset: 0,
640 format: wgpu::VertexFormat::Float32x4,
641 },
642 wgpu::VertexAttribute {
643 shader_location: 1,
644 offset: 16,
645 format: wgpu::VertexFormat::Float32x4,
646 },
647 ],
648 }],
649 compilation_options: wgpu::PipelineCompilationOptions::default(),
650 },
651 fragment: Some(wgpu::FragmentState {
652 module: &ellipse_shader,
653 entry_point: Some("fs_main"),
654 targets: &[Some(wgpu::ColorTargetState {
655 format: config.format,
656 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
657 write_mask: wgpu::ColorWrites::ALL,
658 })],
659 compilation_options: wgpu::PipelineCompilationOptions::default(),
660 }),
661 primitive: wgpu::PrimitiveState::default(),
662 depth_stencil: Some(stencil_for_content.clone()),
663 multisample: multisample_state,
664 multiview_mask: None,
665 cache: None,
666 });
667
668 let ellipse_border_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
670 label: Some("ellipse_border.wgsl"),
671 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
672 "shaders/ellipse_border.wgsl"
673 ))),
674 });
675 let ellipse_border_layout =
676 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
677 label: Some("ellipse border layout"),
678 bind_group_layouts: &[Some(&globals_layout)],
679 immediate_size: 0,
680 });
681 let ellipse_border_pipeline =
682 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
683 label: Some("ellipse border pipeline"),
684 layout: Some(&ellipse_border_layout),
685 vertex: wgpu::VertexState {
686 module: &ellipse_border_shader,
687 entry_point: Some("vs_main"),
688 buffers: &[wgpu::VertexBufferLayout {
689 array_stride: std::mem::size_of::<EllipseBorderInstance>() as u64,
690 step_mode: wgpu::VertexStepMode::Instance,
691 attributes: &[
692 wgpu::VertexAttribute {
693 shader_location: 0,
694 offset: 0,
695 format: wgpu::VertexFormat::Float32x4,
696 },
697 wgpu::VertexAttribute {
698 shader_location: 1,
699 offset: 16,
700 format: wgpu::VertexFormat::Float32,
701 },
702 wgpu::VertexAttribute {
703 shader_location: 2,
704 offset: 20,
705 format: wgpu::VertexFormat::Float32x4,
706 },
707 ],
708 }],
709 compilation_options: wgpu::PipelineCompilationOptions::default(),
710 },
711 fragment: Some(wgpu::FragmentState {
712 module: &ellipse_border_shader,
713 entry_point: Some("fs_main"),
714 targets: &[Some(wgpu::ColorTargetState {
715 format: config.format,
716 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
717 write_mask: wgpu::ColorWrites::ALL,
718 })],
719 compilation_options: wgpu::PipelineCompilationOptions::default(),
720 }),
721 primitive: wgpu::PrimitiveState::default(),
722 depth_stencil: Some(stencil_for_content.clone()),
723 multisample: multisample_state,
724 multiview_mask: None,
725 cache: None,
726 });
727
728 let text_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
731 label: Some("text.wgsl"),
732 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/text.wgsl"))),
733 });
734 let text_color_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
735 label: Some("text_color.wgsl"),
736 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
737 "shaders/text_color.wgsl"
738 ))),
739 });
740
741 let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
743 label: Some("image/text sampler"),
744 address_mode_u: wgpu::AddressMode::ClampToEdge,
745 address_mode_v: wgpu::AddressMode::ClampToEdge,
746 mag_filter: wgpu::FilterMode::Linear,
747 min_filter: wgpu::FilterMode::Linear,
748 mipmap_filter: wgpu::MipmapFilterMode::Linear,
749 ..Default::default()
750 });
751
752 let text_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
754 label: Some("text/rgba bind layout"),
755 entries: &[
756 wgpu::BindGroupLayoutEntry {
757 binding: 0,
758 visibility: wgpu::ShaderStages::FRAGMENT,
759 ty: wgpu::BindingType::Texture {
760 multisampled: false,
761 view_dimension: wgpu::TextureViewDimension::D2,
762 sample_type: wgpu::TextureSampleType::Float { filterable: true },
763 },
764 count: None,
765 },
766 wgpu::BindGroupLayoutEntry {
767 binding: 1,
768 visibility: wgpu::ShaderStages::FRAGMENT,
769 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
770 count: None,
771 },
772 ],
773 });
774 let image_bind_layout_rgba = text_bind_layout.clone();
776
777 let image_bind_layout_nv12 =
779 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
780 label: Some("image bind layout nv12"),
781 entries: &[
782 wgpu::BindGroupLayoutEntry {
784 binding: 0,
785 visibility: wgpu::ShaderStages::FRAGMENT,
786 ty: wgpu::BindingType::Texture {
787 multisampled: false,
788 view_dimension: wgpu::TextureViewDimension::D2,
789 sample_type: wgpu::TextureSampleType::Float { filterable: true },
790 },
791 count: None,
792 },
793 wgpu::BindGroupLayoutEntry {
795 binding: 1,
796 visibility: wgpu::ShaderStages::FRAGMENT,
797 ty: wgpu::BindingType::Texture {
798 multisampled: false,
799 view_dimension: wgpu::TextureViewDimension::D2,
800 sample_type: wgpu::TextureSampleType::Float { filterable: true },
801 },
802 count: None,
803 },
804 wgpu::BindGroupLayoutEntry {
806 binding: 2,
807 visibility: wgpu::ShaderStages::FRAGMENT,
808 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
809 count: None,
810 },
811 ],
812 });
813
814 let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
815 label: Some("text pipeline layout"),
816 bind_group_layouts: &[Some(&globals_layout), Some(&text_bind_layout)],
817 immediate_size: 0,
818 });
819
820 let text_pipeline_mask = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
821 label: Some("text pipeline (mask)"),
822 layout: Some(&text_pipeline_layout),
823 vertex: wgpu::VertexState {
824 module: &text_mask_shader,
825 entry_point: Some("vs_main"),
826 buffers: &[wgpu::VertexBufferLayout {
827 array_stride: std::mem::size_of::<GlyphInstance>() as u64,
828 step_mode: wgpu::VertexStepMode::Instance,
829 attributes: &[
830 wgpu::VertexAttribute {
831 shader_location: 0,
832 offset: 0,
833 format: wgpu::VertexFormat::Float32x4,
834 },
835 wgpu::VertexAttribute {
836 shader_location: 1,
837 offset: 16,
838 format: wgpu::VertexFormat::Float32x4,
839 },
840 wgpu::VertexAttribute {
841 shader_location: 2,
842 offset: 32,
843 format: wgpu::VertexFormat::Float32x4,
844 },
845 ],
846 }],
847 compilation_options: wgpu::PipelineCompilationOptions::default(),
848 },
849 fragment: Some(wgpu::FragmentState {
850 module: &text_mask_shader,
851 entry_point: Some("fs_main"),
852 targets: &[Some(wgpu::ColorTargetState {
853 format: config.format,
854 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
855 write_mask: wgpu::ColorWrites::ALL,
856 })],
857 compilation_options: wgpu::PipelineCompilationOptions::default(),
858 }),
859 primitive: wgpu::PrimitiveState::default(),
860 depth_stencil: Some(stencil_for_content.clone()),
861 multisample: multisample_state,
862 multiview_mask: None,
863 cache: None,
864 });
865
866 let text_pipeline_color = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
867 label: Some("text pipeline (color)"),
868 layout: Some(&text_pipeline_layout),
869 vertex: wgpu::VertexState {
870 module: &text_color_shader,
871 entry_point: Some("vs_main"),
872 buffers: &[wgpu::VertexBufferLayout {
873 array_stride: std::mem::size_of::<GlyphInstance>() as u64,
874 step_mode: wgpu::VertexStepMode::Instance,
875 attributes: &[
876 wgpu::VertexAttribute {
877 shader_location: 0,
878 offset: 0,
879 format: wgpu::VertexFormat::Float32x4,
880 },
881 wgpu::VertexAttribute {
882 shader_location: 1,
883 offset: 16,
884 format: wgpu::VertexFormat::Float32x4,
885 },
886 wgpu::VertexAttribute {
887 shader_location: 2,
888 offset: 32,
889 format: wgpu::VertexFormat::Float32x4,
890 },
891 ],
892 }],
893 compilation_options: wgpu::PipelineCompilationOptions::default(),
894 },
895 fragment: Some(wgpu::FragmentState {
896 module: &text_color_shader,
897 entry_point: Some("fs_main"),
898 targets: &[Some(wgpu::ColorTargetState {
899 format: config.format,
900 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
901 write_mask: wgpu::ColorWrites::ALL,
902 })],
903 compilation_options: wgpu::PipelineCompilationOptions::default(),
904 }),
905 primitive: wgpu::PrimitiveState::default(),
906 depth_stencil: Some(stencil_for_content.clone()),
907 multisample: multisample_state,
908 multiview_mask: None,
909 cache: None,
910 });
911
912 let image_pipeline_rgba = text_pipeline_color.clone(); let image_nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
917 label: Some("image_nv12.wgsl"),
918 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
919 "shaders/image_nv12.wgsl"
920 ))),
921 });
922
923 let image_nv12_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
924 label: Some("image nv12 pipeline layout"),
925 bind_group_layouts: &[Some(&globals_layout), Some(&image_bind_layout_nv12)],
926 immediate_size: 0,
927 });
928
929 let image_pipeline_nv12 = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
930 label: Some("image nv12 pipeline"),
931 layout: Some(&image_nv12_layout),
932 vertex: wgpu::VertexState {
933 module: &image_nv12_shader,
934 entry_point: Some("vs_main"),
935 buffers: &[wgpu::VertexBufferLayout {
936 array_stride: std::mem::size_of::<Nv12Instance>() as u64,
937 step_mode: wgpu::VertexStepMode::Instance,
938 attributes: &[
939 wgpu::VertexAttribute {
940 shader_location: 0,
941 offset: 0,
942 format: wgpu::VertexFormat::Float32x4,
943 }, wgpu::VertexAttribute {
945 shader_location: 1,
946 offset: 16,
947 format: wgpu::VertexFormat::Float32x4,
948 }, wgpu::VertexAttribute {
950 shader_location: 2,
951 offset: 32,
952 format: wgpu::VertexFormat::Float32x4,
953 }, wgpu::VertexAttribute {
955 shader_location: 3,
956 offset: 48,
957 format: wgpu::VertexFormat::Float32,
958 }, ],
960 }],
961 compilation_options: wgpu::PipelineCompilationOptions::default(),
962 },
963 fragment: Some(wgpu::FragmentState {
964 module: &image_nv12_shader,
965 entry_point: Some("fs_main"),
966 targets: &[Some(wgpu::ColorTargetState {
967 format: config.format,
968 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
969 write_mask: wgpu::ColorWrites::ALL,
970 })],
971 compilation_options: wgpu::PipelineCompilationOptions::default(),
972 }),
973 primitive: wgpu::PrimitiveState::default(),
974 depth_stencil: Some(stencil_for_content.clone()),
975 multisample: multisample_state,
976 multiview_mask: None,
977 cache: None,
978 });
979
980 let clip_shader_a2c = device.create_shader_module(wgpu::ShaderModuleDescriptor {
983 label: Some("clip_round_rect_a2c.wgsl"),
984 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
985 "shaders/clip_round_rect_a2c.wgsl"
986 ))),
987 });
988 let clip_shader_bin = device.create_shader_module(wgpu::ShaderModuleDescriptor {
989 label: Some("clip_round_rect_bin.wgsl"),
990 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
991 "shaders/clip_round_rect_bin.wgsl"
992 ))),
993 });
994
995 let clip_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
996 label: Some("clip pipeline layout"),
997 bind_group_layouts: &[Some(&globals_layout)],
998 immediate_size: 0,
999 });
1000
1001 let clip_vertex_layout = wgpu::VertexBufferLayout {
1002 array_stride: std::mem::size_of::<ClipInstance>() as u64,
1003 step_mode: wgpu::VertexStepMode::Instance,
1004 attributes: &[
1005 wgpu::VertexAttribute {
1006 shader_location: 0,
1007 offset: 0,
1008 format: wgpu::VertexFormat::Float32x4,
1009 },
1010 wgpu::VertexAttribute {
1011 shader_location: 1,
1012 offset: 16,
1013 format: wgpu::VertexFormat::Float32,
1014 },
1015 ],
1016 };
1017
1018 let clip_color_target = wgpu::ColorTargetState {
1019 format: config.format,
1020 blend: None,
1021 write_mask: wgpu::ColorWrites::empty(),
1022 };
1023
1024 let clip_pipeline_a2c = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1025 label: Some("clip pipeline (a2c)"),
1026 layout: Some(&clip_pipeline_layout),
1027 vertex: wgpu::VertexState {
1028 module: &clip_shader_a2c,
1029 entry_point: Some("vs_main"),
1030 buffers: &[clip_vertex_layout.clone()],
1031 compilation_options: wgpu::PipelineCompilationOptions::default(),
1032 },
1033 fragment: Some(wgpu::FragmentState {
1034 module: &clip_shader_a2c,
1035 entry_point: Some("fs_main"),
1036 targets: &[Some(clip_color_target.clone())],
1037 compilation_options: wgpu::PipelineCompilationOptions::default(),
1038 }),
1039 primitive: wgpu::PrimitiveState::default(),
1040 depth_stencil: Some(stencil_for_clip_inc.clone()),
1041 multisample: wgpu::MultisampleState {
1042 count: msaa_samples,
1043 mask: !0,
1044 alpha_to_coverage_enabled: msaa_samples > 1,
1045 },
1046 multiview_mask: None,
1047 cache: None,
1048 });
1049
1050 let clip_pipeline_bin = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1051 label: Some("clip pipeline (bin)"),
1052 layout: Some(&clip_pipeline_layout),
1053 vertex: wgpu::VertexState {
1054 module: &clip_shader_bin,
1055 entry_point: Some("vs_main"),
1056 buffers: &[clip_vertex_layout],
1057 compilation_options: wgpu::PipelineCompilationOptions::default(),
1058 },
1059 fragment: Some(wgpu::FragmentState {
1060 module: &clip_shader_bin,
1061 entry_point: Some("fs_main"),
1062 targets: &[Some(clip_color_target)],
1063 compilation_options: wgpu::PipelineCompilationOptions::default(),
1064 }),
1065 primitive: wgpu::PrimitiveState::default(),
1066 depth_stencil: Some(stencil_for_clip_inc),
1067 multisample: wgpu::MultisampleState {
1068 count: msaa_samples,
1069 mask: !0,
1070 alpha_to_coverage_enabled: false,
1071 },
1072 multiview_mask: None,
1073 cache: None,
1074 });
1075
1076 let atlas_mask = Self::init_atlas_mask(&device)?;
1078 let atlas_color = Self::init_atlas_color(&device)?;
1079
1080 let ring_rect = UploadRing::new(&device, "ring rect", 1 << 20);
1082 let ring_border = UploadRing::new(&device, "ring border", 1 << 20);
1083 let ring_ellipse = UploadRing::new(&device, "ring ellipse", 1 << 20);
1084 let ring_ellipse_border = UploadRing::new(&device, "ring ellipse border", 1 << 20);
1085 let ring_glyph_mask = UploadRing::new(&device, "ring glyph mask", 1 << 20);
1086 let ring_glyph_color = UploadRing::new(&device, "ring glyph color", 1 << 20);
1087 let ring_clip = UploadRing::new(&device, "ring clip", 1 << 16);
1088 let ring_nv12 = UploadRing::new(&device, "ring nv12", 1 << 20);
1089
1090 let depth_stencil_tex = device.create_texture(&wgpu::TextureDescriptor {
1092 label: Some("temp ds"),
1093 size: wgpu::Extent3d {
1094 width: 1,
1095 height: 1,
1096 depth_or_array_layers: 1,
1097 },
1098 mip_level_count: 1,
1099 sample_count: 1,
1100 dimension: wgpu::TextureDimension::D2,
1101 format: wgpu::TextureFormat::Depth24PlusStencil8,
1102 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1103 view_formats: &[],
1104 });
1105 let depth_stencil_view =
1106 depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
1107
1108 let mut backend = Self {
1109 surface,
1110 device,
1111 queue,
1112 config,
1113
1114 rects: InstancedPipe::new(rect_pipeline, ring_rect),
1115 borders: InstancedPipe::new(border_pipeline, ring_border),
1116 ellipses: InstancedPipe::new(ellipse_pipeline, ring_ellipse),
1117 ellipse_borders: InstancedPipe::new(ellipse_border_pipeline, ring_ellipse_border),
1118 glyph_mask: InstancedPipe::new(text_pipeline_mask, ring_glyph_mask),
1119 glyph_color: InstancedPipe::new(text_pipeline_color, ring_glyph_color),
1120
1121 text_bind_layout,
1122
1123 image_pipeline_rgba,
1124 image_pipeline_nv12: image_pipeline_nv12.clone(),
1125 image_bind_layout_rgba,
1126 image_bind_layout_nv12,
1127 image_sampler,
1128
1129 clip_pipeline_a2c,
1130 clip_pipeline_bin,
1131 clip_ring: ring_clip,
1132
1133 nv12: InstancedPipe::new(image_pipeline_nv12, ring_nv12),
1134
1135 msaa_samples,
1136 depth_stencil_tex,
1137 depth_stencil_view,
1138 msaa_tex: None,
1139 msaa_view: None,
1140 globals_bind,
1141 globals_buf,
1142 globals_layout,
1143
1144 atlas_mask,
1145 atlas_color,
1146
1147 next_image_handle: 1,
1148 images: HashMap::new(),
1149
1150 frame_index: 0,
1151 image_bytes_total: 0,
1152 image_evict_after_frames: 600, image_budget_bytes: 512 * 1024 * 1024, };
1155
1156 backend.recreate_msaa_and_depth_stencil();
1157 Ok(backend)
1158 }
1159
1160 #[cfg(not(target_arch = "wasm32"))]
1161 pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
1162 pollster::block_on(Self::new_async(window))
1163 }
1164
1165 #[cfg(target_arch = "wasm32")]
1166 pub fn new(_window: Arc<winit::window::Window>) -> anyhow::Result<Self> {
1167 anyhow::bail!("Use WgpuBackend::new_async(window).await on wasm32")
1168 }
1169
1170 pub fn set_image_from_bytes(
1173 &mut self,
1174 handle: u64,
1175 data: &[u8],
1176 srgb: bool,
1177 ) -> anyhow::Result<()> {
1178 let img = image::load_from_memory(data)?;
1179 let rgba = img.to_rgba8();
1180 let (w, h) = rgba.dimensions();
1181 self.set_image_rgba8(handle, w, h, &rgba, srgb)
1182 }
1183
1184 pub fn set_image_rgba8(
1185 &mut self,
1186 handle: u64,
1187 w: u32,
1188 h: u32,
1189 rgba: &[u8],
1190 srgb: bool,
1191 ) -> anyhow::Result<()> {
1192 let expected = (w as usize) * (h as usize) * 4;
1193 if rgba.len() < expected {
1194 return Err(anyhow::anyhow!(
1195 "RGBA buffer too small: {} < {}",
1196 rgba.len(),
1197 expected
1198 ));
1199 }
1200
1201 let format = if srgb {
1202 wgpu::TextureFormat::Rgba8UnormSrgb
1203 } else {
1204 wgpu::TextureFormat::Rgba8Unorm
1205 };
1206
1207 let needs_recreate = match self.images.get(&handle) {
1208 Some(ImageTex::Rgba {
1209 w: cw,
1210 h: ch,
1211 format: cf,
1212 ..
1213 }) => *cw != w || *ch != h || *cf != format,
1214 _ => true,
1215 };
1216
1217 if needs_recreate {
1218 self.remove_image(handle);
1220
1221 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1222 label: Some("user image rgba"),
1223 size: wgpu::Extent3d {
1224 width: w,
1225 height: h,
1226 depth_or_array_layers: 1,
1227 },
1228 mip_level_count: 1,
1229 sample_count: 1,
1230 dimension: wgpu::TextureDimension::D2,
1231 format,
1232 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1233 view_formats: &[],
1234 });
1235 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1236
1237 let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1238 label: Some("image bind rgba"),
1239 layout: &self.image_bind_layout_rgba,
1240 entries: &[
1241 wgpu::BindGroupEntry {
1242 binding: 0,
1243 resource: wgpu::BindingResource::TextureView(&view),
1244 },
1245 wgpu::BindGroupEntry {
1246 binding: 1,
1247 resource: wgpu::BindingResource::Sampler(&self.image_sampler),
1248 },
1249 ],
1250 });
1251
1252 let bytes = (w as u64) * (h as u64) * 4;
1253 self.image_bytes_total += bytes;
1254
1255 self.images.insert(
1256 handle,
1257 ImageTex::Rgba {
1258 tex,
1259 view,
1260 bind,
1261 w,
1262 h,
1263 format,
1264 last_used_frame: self.frame_index,
1265 bytes,
1266 },
1267 );
1268 }
1269
1270 let tex = match self.images.get(&handle) {
1271 Some(ImageTex::Rgba { tex, .. }) => tex,
1272 _ => unreachable!(),
1273 };
1274
1275 self.queue.write_texture(
1276 wgpu::TexelCopyTextureInfo {
1277 texture: tex,
1278 mip_level: 0,
1279 origin: wgpu::Origin3d::ZERO,
1280 aspect: wgpu::TextureAspect::All,
1281 },
1282 &rgba[..expected],
1283 wgpu::TexelCopyBufferLayout {
1284 offset: 0,
1285 bytes_per_row: Some(4 * w),
1286 rows_per_image: Some(h),
1287 },
1288 wgpu::Extent3d {
1289 width: w,
1290 height: h,
1291 depth_or_array_layers: 1,
1292 },
1293 );
1294
1295 self.evict_budget_excess();
1297
1298 Ok(())
1299 }
1300
1301 pub fn set_image_nv12(
1302 &mut self,
1303 handle: u64,
1304 w: u32,
1305 h: u32,
1306 y: &[u8],
1307 uv: &[u8],
1308 full_range: bool,
1309 ) -> anyhow::Result<()> {
1310 let y_expected = (w as usize) * (h as usize);
1311 let uv_w = (w / 2).max(1);
1312 let uv_h = (h / 2).max(1);
1313 let uv_expected = (uv_w as usize) * (uv_h as usize) * 2;
1314
1315 if y.len() < y_expected {
1316 return Err(anyhow::anyhow!("Y plane too small"));
1317 }
1318 if uv.len() < uv_expected {
1319 return Err(anyhow::anyhow!("UV plane too small"));
1320 }
1321
1322 let needs_recreate = match self.images.get(&handle) {
1323 Some(ImageTex::Nv12 { w: ww, h: hh, .. }) => *ww != w || *hh != h,
1324 _ => true,
1325 };
1326
1327 if needs_recreate {
1328 self.remove_image(handle);
1329
1330 let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
1331 label: Some("nv12 Y"),
1332 size: wgpu::Extent3d {
1333 width: w,
1334 height: h,
1335 depth_or_array_layers: 1,
1336 },
1337 mip_level_count: 1,
1338 sample_count: 1,
1339 dimension: wgpu::TextureDimension::D2,
1340 format: wgpu::TextureFormat::R8Unorm,
1341 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1342 view_formats: &[],
1343 });
1344 let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
1345
1346 let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
1347 label: Some("nv12 UV"),
1348 size: wgpu::Extent3d {
1349 width: uv_w,
1350 height: uv_h,
1351 depth_or_array_layers: 1,
1352 },
1353 mip_level_count: 1,
1354 sample_count: 1,
1355 dimension: wgpu::TextureDimension::D2,
1356 format: wgpu::TextureFormat::Rg8Unorm,
1357 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1358 view_formats: &[],
1359 });
1360 let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
1361
1362 let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1363 label: Some("nv12 bind"),
1364 layout: &self.image_bind_layout_nv12,
1365 entries: &[
1366 wgpu::BindGroupEntry {
1367 binding: 0,
1368 resource: wgpu::BindingResource::TextureView(&view_y),
1369 },
1370 wgpu::BindGroupEntry {
1371 binding: 1,
1372 resource: wgpu::BindingResource::TextureView(&view_uv),
1373 },
1374 wgpu::BindGroupEntry {
1375 binding: 2,
1376 resource: wgpu::BindingResource::Sampler(&self.image_sampler),
1377 },
1378 ],
1379 });
1380
1381 let bytes = (w as u64) * (h as u64) + (uv_w as u64) * (uv_h as u64) * 2;
1382 self.image_bytes_total += bytes;
1383
1384 self.images.insert(
1385 handle,
1386 ImageTex::Nv12 {
1387 tex_y,
1388 view_y,
1389 tex_uv,
1390 view_uv,
1391 bind,
1392 w,
1393 h,
1394 full_range,
1395 last_used_frame: self.frame_index,
1396 bytes,
1397 },
1398 );
1399 }
1400
1401 let (tex_y, tex_uv, _bind) = match self.images.get(&handle) {
1402 Some(ImageTex::Nv12 {
1403 tex_y,
1404 tex_uv,
1405 bind,
1406 ..
1407 }) => (tex_y, tex_uv, bind),
1408 _ => return Err(anyhow::anyhow!("Handle is not NV12")),
1409 };
1410
1411 self.queue.write_texture(
1412 wgpu::TexelCopyTextureInfo {
1413 texture: tex_y,
1414 mip_level: 0,
1415 origin: wgpu::Origin3d::ZERO,
1416 aspect: wgpu::TextureAspect::All,
1417 },
1418 &y[..y_expected],
1419 wgpu::TexelCopyBufferLayout {
1420 offset: 0,
1421 bytes_per_row: Some(w),
1422 rows_per_image: Some(h),
1423 },
1424 wgpu::Extent3d {
1425 width: w,
1426 height: h,
1427 depth_or_array_layers: 1,
1428 },
1429 );
1430
1431 self.queue.write_texture(
1432 wgpu::TexelCopyTextureInfo {
1433 texture: tex_uv,
1434 mip_level: 0,
1435 origin: wgpu::Origin3d::ZERO,
1436 aspect: wgpu::TextureAspect::All,
1437 },
1438 &uv[..uv_expected],
1439 wgpu::TexelCopyBufferLayout {
1440 offset: 0,
1441 bytes_per_row: Some(2 * uv_w),
1442 rows_per_image: Some(uv_h),
1443 },
1444 wgpu::Extent3d {
1445 width: uv_w,
1446 height: uv_h,
1447 depth_or_array_layers: 1,
1448 },
1449 );
1450
1451 self.evict_budget_excess();
1452 Ok(())
1453 }
1454
1455 pub fn remove_image(&mut self, handle: u64) {
1456 if let Some(img) = self.images.remove(&handle) {
1457 let b = match img {
1458 ImageTex::Rgba { bytes, .. } => bytes,
1459 ImageTex::Nv12 { bytes, .. } => bytes,
1460 };
1461 self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
1462 }
1463 }
1464
1465 pub fn register_image_from_bytes(&mut self, data: &[u8], srgb: bool) -> u64 {
1467 let handle = self.next_image_handle;
1468 self.next_image_handle += 1;
1469 if let Err(e) = self.set_image_from_bytes(handle, data, srgb) {
1470 log::error!("Failed to register image: {e}");
1471 }
1472 handle
1473 }
1474
1475 fn evict_unused_images(&mut self) {
1476 let now = self.frame_index;
1477 let evict_after = self.image_evict_after_frames;
1478
1479 let mut to_remove = Vec::new();
1481 for (h, t) in self.images.iter() {
1482 let last = match t {
1483 ImageTex::Rgba {
1484 last_used_frame, ..
1485 } => *last_used_frame,
1486 ImageTex::Nv12 {
1487 last_used_frame, ..
1488 } => *last_used_frame,
1489 };
1490 if now.saturating_sub(last) > evict_after {
1491 to_remove.push(*h);
1492 }
1493 }
1494 for h in to_remove {
1495 self.remove_image(h);
1496 }
1497
1498 self.evict_budget_excess();
1499 }
1500
1501 fn evict_budget_excess(&mut self) {
1502 if self.image_bytes_total <= self.image_budget_bytes {
1503 return;
1504 }
1505 let mut candidates: Vec<(u64, u64, u64)> = self
1507 .images
1508 .iter()
1509 .map(|(h, t)| {
1510 let (last, bytes) = match t {
1511 ImageTex::Rgba {
1512 last_used_frame,
1513 bytes,
1514 ..
1515 } => (*last_used_frame, *bytes),
1516 ImageTex::Nv12 {
1517 last_used_frame,
1518 bytes,
1519 ..
1520 } => (*last_used_frame, *bytes),
1521 };
1522 (*h, last, bytes)
1523 })
1524 .collect();
1525
1526 candidates.sort_by_key(|k| k.1);
1528
1529 let now = self.frame_index;
1530 for (h, last, _bytes) in candidates {
1531 if self.image_bytes_total <= self.image_budget_bytes {
1532 break;
1533 }
1534 if last == now {
1536 continue;
1537 }
1538 self.remove_image(h);
1539 }
1540 }
1541
1542 fn recreate_msaa_and_depth_stencil(&mut self) {
1543 if self.msaa_samples > 1 {
1544 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1545 label: Some("msaa color"),
1546 size: wgpu::Extent3d {
1547 width: self.config.width.max(1),
1548 height: self.config.height.max(1),
1549 depth_or_array_layers: 1,
1550 },
1551 mip_level_count: 1,
1552 sample_count: self.msaa_samples,
1553 dimension: wgpu::TextureDimension::D2,
1554 format: self.config.format,
1555 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1556 view_formats: &[],
1557 });
1558 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1559 self.msaa_tex = Some(tex);
1560 self.msaa_view = Some(view);
1561 } else {
1562 self.msaa_tex = None;
1563 self.msaa_view = None;
1564 }
1565
1566 self.depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
1567 label: Some("depth-stencil (stencil clips)"),
1568 size: wgpu::Extent3d {
1569 width: self.config.width.max(1),
1570 height: self.config.height.max(1),
1571 depth_or_array_layers: 1,
1572 },
1573 mip_level_count: 1,
1574 sample_count: self.msaa_samples,
1575 dimension: wgpu::TextureDimension::D2,
1576 format: wgpu::TextureFormat::Depth24PlusStencil8,
1577 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1578 view_formats: &[],
1579 });
1580 self.depth_stencil_view = self
1581 .depth_stencil_tex
1582 .create_view(&wgpu::TextureViewDescriptor::default());
1583 }
1584
1585 fn init_atlas_mask(device: &wgpu::Device) -> anyhow::Result<AtlasA8> {
1586 let size = 1024u32;
1587 let tex = device.create_texture(&wgpu::TextureDescriptor {
1588 label: Some("glyph atlas A8"),
1589 size: wgpu::Extent3d {
1590 width: size,
1591 height: size,
1592 depth_or_array_layers: 1,
1593 },
1594 mip_level_count: 1,
1595 sample_count: 1,
1596 dimension: wgpu::TextureDimension::D2,
1597 format: wgpu::TextureFormat::R8Unorm,
1598 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1599 view_formats: &[],
1600 });
1601 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1602 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1603 label: Some("glyph atlas sampler A8"),
1604 address_mode_u: wgpu::AddressMode::ClampToEdge,
1605 address_mode_v: wgpu::AddressMode::ClampToEdge,
1606 address_mode_w: wgpu::AddressMode::ClampToEdge,
1607 mag_filter: wgpu::FilterMode::Linear,
1608 min_filter: wgpu::FilterMode::Linear,
1609 mipmap_filter: wgpu::MipmapFilterMode::Linear,
1610 ..Default::default()
1611 });
1612
1613 Ok(AtlasA8 {
1614 tex,
1615 view,
1616 sampler,
1617 size,
1618 next_x: 1,
1619 next_y: 1,
1620 row_h: 0,
1621 map: HashMap::new(),
1622 })
1623 }
1624
1625 fn init_atlas_color(device: &wgpu::Device) -> anyhow::Result<AtlasRGBA> {
1626 let size = 1024u32;
1627 let tex = device.create_texture(&wgpu::TextureDescriptor {
1628 label: Some("glyph atlas RGBA"),
1629 size: wgpu::Extent3d {
1630 width: size,
1631 height: size,
1632 depth_or_array_layers: 1,
1633 },
1634 mip_level_count: 1,
1635 sample_count: 1,
1636 dimension: wgpu::TextureDimension::D2,
1637 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1638 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1639 view_formats: &[],
1640 });
1641 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
1642 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1643 label: Some("glyph atlas sampler RGBA"),
1644 address_mode_u: wgpu::AddressMode::ClampToEdge,
1645 address_mode_v: wgpu::AddressMode::ClampToEdge,
1646 address_mode_w: wgpu::AddressMode::ClampToEdge,
1647 mag_filter: wgpu::FilterMode::Linear,
1648 min_filter: wgpu::FilterMode::Linear,
1649 mipmap_filter: wgpu::MipmapFilterMode::Linear,
1650 ..Default::default()
1651 });
1652 Ok(AtlasRGBA {
1653 tex,
1654 view,
1655 sampler,
1656 size,
1657 next_x: 1,
1658 next_y: 1,
1659 row_h: 0,
1660 map: HashMap::new(),
1661 })
1662 }
1663
1664 fn atlas_bind_group_mask(&self) -> wgpu::BindGroup {
1665 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1666 label: Some("atlas bind"),
1667 layout: &self.text_bind_layout,
1668 entries: &[
1669 wgpu::BindGroupEntry {
1670 binding: 0,
1671 resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
1672 },
1673 wgpu::BindGroupEntry {
1674 binding: 1,
1675 resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
1676 },
1677 ],
1678 })
1679 }
1680
1681 fn atlas_bind_group_color(&self) -> wgpu::BindGroup {
1682 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1683 label: Some("atlas bind color"),
1684 layout: &self.text_bind_layout,
1685 entries: &[
1686 wgpu::BindGroupEntry {
1687 binding: 0,
1688 resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
1689 },
1690 wgpu::BindGroupEntry {
1691 binding: 1,
1692 resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
1693 },
1694 ],
1695 })
1696 }
1697
1698 fn upload_glyph_mask(&mut self, key: repose_text::GlyphKey, px: u32) -> Option<GlyphInfo> {
1699 let keyp = (key, px);
1700 if let Some(info) = self.atlas_mask.map.get(&keyp) {
1701 return Some(*info);
1702 }
1703
1704 let gb = repose_text::rasterize(key, px as f32)?;
1705 if gb.w == 0 || gb.h == 0 || gb.data.is_empty() {
1706 return None;
1707 }
1708
1709 let coverage = match swash_to_a8_coverage(gb.content, &gb.data) {
1710 Some(c) => c,
1711 None => return None,
1712 };
1713
1714 let w = gb.w.max(1);
1715 let h = gb.h.max(1);
1716
1717 if !self.alloc_space_mask(w, h) {
1718 self.grow_mask_and_rebuild();
1719 }
1720 if !self.alloc_space_mask(w, h) {
1721 return None;
1722 }
1723 let x = self.atlas_mask.next_x;
1724 let y = self.atlas_mask.next_y;
1725 self.atlas_mask.next_x += w + 1;
1726 self.atlas_mask.row_h = self.atlas_mask.row_h.max(h + 1);
1727
1728 let layout = wgpu::TexelCopyBufferLayout {
1729 offset: 0,
1730 bytes_per_row: Some(w),
1731 rows_per_image: Some(h),
1732 };
1733 let size = wgpu::Extent3d {
1734 width: w,
1735 height: h,
1736 depth_or_array_layers: 1,
1737 };
1738 self.queue.write_texture(
1739 wgpu::TexelCopyTextureInfoBase {
1740 texture: &self.atlas_mask.tex,
1741 mip_level: 0,
1742 origin: wgpu::Origin3d { x, y, z: 0 },
1743 aspect: wgpu::TextureAspect::All,
1744 },
1745 &coverage,
1746 layout,
1747 size,
1748 );
1749
1750 let info = GlyphInfo {
1751 u0: x as f32 / self.atlas_mask.size as f32,
1752 v0: y as f32 / self.atlas_mask.size as f32,
1753 u1: (x + w) as f32 / self.atlas_mask.size as f32,
1754 v1: (y + h) as f32 / self.atlas_mask.size as f32,
1755 w: w as f32,
1756 h: h as f32,
1757 bearing_x: 0.0,
1758 bearing_y: 0.0,
1759 advance: 0.0,
1760 };
1761 self.atlas_mask.map.insert(keyp, info);
1762 Some(info)
1763 }
1764
1765 fn upload_glyph_color(&mut self, key: repose_text::GlyphKey, px: u32) -> Option<GlyphInfo> {
1766 let keyp = (key, px);
1767 if let Some(info) = self.atlas_color.map.get(&keyp) {
1768 return Some(*info);
1769 }
1770 let gb = repose_text::rasterize(key, px as f32)?;
1771 if !matches!(gb.content, cosmic_text::SwashContent::Color) {
1772 return None;
1773 }
1774 let w = gb.w.max(1);
1775 let h = gb.h.max(1);
1776 if !self.alloc_space_color(w, h) {
1777 self.grow_color_and_rebuild();
1778 }
1779 if !self.alloc_space_color(w, h) {
1780 return None;
1781 }
1782 let x = self.atlas_color.next_x;
1783 let y = self.atlas_color.next_y;
1784 self.atlas_color.next_x += w + 1;
1785 self.atlas_color.row_h = self.atlas_color.row_h.max(h + 1);
1786
1787 let layout = wgpu::TexelCopyBufferLayout {
1788 offset: 0,
1789 bytes_per_row: Some(w * 4),
1790 rows_per_image: Some(h),
1791 };
1792 let size = wgpu::Extent3d {
1793 width: w,
1794 height: h,
1795 depth_or_array_layers: 1,
1796 };
1797 self.queue.write_texture(
1798 wgpu::TexelCopyTextureInfoBase {
1799 texture: &self.atlas_color.tex,
1800 mip_level: 0,
1801 origin: wgpu::Origin3d { x, y, z: 0 },
1802 aspect: wgpu::TextureAspect::All,
1803 },
1804 &gb.data,
1805 layout,
1806 size,
1807 );
1808 let info = GlyphInfo {
1809 u0: x as f32 / self.atlas_color.size as f32,
1810 v0: y as f32 / self.atlas_color.size as f32,
1811 u1: (x + w) as f32 / self.atlas_color.size as f32,
1812 v1: (y + h) as f32 / self.atlas_color.size as f32,
1813 w: w as f32,
1814 h: h as f32,
1815 bearing_x: 0.0,
1816 bearing_y: 0.0,
1817 advance: 0.0,
1818 };
1819 self.atlas_color.map.insert(keyp, info);
1820 Some(info)
1821 }
1822
1823 fn alloc_space_mask(&mut self, w: u32, h: u32) -> bool {
1824 if self.atlas_mask.next_x + w + 1 >= self.atlas_mask.size {
1825 self.atlas_mask.next_x = 1;
1826 self.atlas_mask.next_y += self.atlas_mask.row_h + 1;
1827 self.atlas_mask.row_h = 0;
1828 }
1829 if self.atlas_mask.next_y + h + 1 >= self.atlas_mask.size {
1830 return false;
1831 }
1832 true
1833 }
1834
1835 fn grow_mask_and_rebuild(&mut self) {
1836 let new_size = (self.atlas_mask.size * 2).min(4096);
1837 if new_size == self.atlas_mask.size {
1838 return;
1839 }
1840 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1841 label: Some("glyph atlas A8 (grown)"),
1842 size: wgpu::Extent3d {
1843 width: new_size,
1844 height: new_size,
1845 depth_or_array_layers: 1,
1846 },
1847 mip_level_count: 1,
1848 sample_count: 1,
1849 dimension: wgpu::TextureDimension::D2,
1850 format: wgpu::TextureFormat::R8Unorm,
1851 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1852 view_formats: &[],
1853 });
1854 self.atlas_mask.tex = tex;
1855 self.atlas_mask.view = self
1856 .atlas_mask
1857 .tex
1858 .create_view(&wgpu::TextureViewDescriptor::default());
1859 self.atlas_mask.size = new_size;
1860 self.atlas_mask.next_x = 1;
1861 self.atlas_mask.next_y = 1;
1862 self.atlas_mask.row_h = 0;
1863 let keys: Vec<(repose_text::GlyphKey, u32)> = self.atlas_mask.map.keys().copied().collect();
1864 self.atlas_mask.map.clear();
1865 for (k, px) in keys {
1866 let _ = self.upload_glyph_mask(k, px);
1867 }
1868 }
1869
1870 fn alloc_space_color(&mut self, w: u32, h: u32) -> bool {
1871 if self.atlas_color.next_x + w + 1 >= self.atlas_color.size {
1872 self.atlas_color.next_x = 1;
1873 self.atlas_color.next_y += self.atlas_color.row_h + 1;
1874 self.atlas_color.row_h = 0;
1875 }
1876 if self.atlas_color.next_y + h + 1 >= self.atlas_color.size {
1877 return false;
1878 }
1879 true
1880 }
1881
1882 fn grow_color_and_rebuild(&mut self) {
1883 let new_size = (self.atlas_color.size * 2).min(4096);
1884 if new_size == self.atlas_color.size {
1885 return;
1886 }
1887 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
1888 label: Some("glyph atlas RGBA (grown)"),
1889 size: wgpu::Extent3d {
1890 width: new_size,
1891 height: new_size,
1892 depth_or_array_layers: 1,
1893 },
1894 mip_level_count: 1,
1895 sample_count: 1,
1896 dimension: wgpu::TextureDimension::D2,
1897 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1898 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1899 view_formats: &[],
1900 });
1901 self.atlas_color.tex = tex;
1902 self.atlas_color.view = self
1903 .atlas_color
1904 .tex
1905 .create_view(&wgpu::TextureViewDescriptor::default());
1906 self.atlas_color.size = new_size;
1907 self.atlas_color.next_x = 1;
1908 self.atlas_color.next_y = 1;
1909 self.atlas_color.row_h = 0;
1910 let keys: Vec<(repose_text::GlyphKey, u32)> =
1911 self.atlas_color.map.keys().copied().collect();
1912 self.atlas_color.map.clear();
1913 for (k, px) in keys {
1914 let _ = self.upload_glyph_color(k, px);
1915 }
1916 }
1917}
1918
1919fn brush_to_instance_fields(brush: &Brush) -> (u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2]) {
1920 match brush {
1921 Brush::Solid(c) => (
1922 0u32,
1923 c.to_linear(),
1924 [0.0, 0.0, 0.0, 0.0],
1925 [0.0, 0.0],
1926 [0.0, 1.0],
1927 ),
1928 Brush::Linear {
1929 start,
1930 end,
1931 start_color,
1932 end_color,
1933 } => (
1934 1u32,
1935 start_color.to_linear(),
1936 end_color.to_linear(),
1937 [start.x, start.y],
1938 [end.x, end.y],
1939 ),
1940 }
1941}
1942
1943fn brush_to_solid_color(brush: &Brush) -> [f32; 4] {
1944 match brush {
1945 Brush::Solid(c) => c.to_linear(),
1946 Brush::Linear { start_color, .. } => start_color.to_linear(),
1947 }
1948}
1949
1950impl RenderBackend for WgpuBackend {
1951 fn configure_surface(&mut self, width: u32, height: u32) {
1952 if width == 0 || height == 0 {
1953 return;
1954 }
1955 self.config.width = width;
1956 self.config.height = height;
1957 self.surface.configure(&self.device, &self.config);
1958 self.recreate_msaa_and_depth_stencil();
1959 }
1960
1961 fn frame(&mut self, scene: &Scene, _glyph_cfg: GlyphRasterConfig) {
1962 self.frame_index = self.frame_index.wrapping_add(1);
1964
1965 if self.config.width == 0 || self.config.height == 0 {
1966 return;
1967 }
1968 let frame = loop {
1969 match self.surface.get_current_texture() {
1970 wgpu::CurrentSurfaceTexture::Success(f) => break f,
1971 wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
1972 log::warn!("suboptimal surface; reconfiguring");
1973 self.surface.configure(&self.device, &self.config);
1974 break f;
1975 }
1976 wgpu::CurrentSurfaceTexture::Outdated => {
1977 log::warn!("surface outdated; reconfiguring");
1978 self.surface.configure(&self.device, &self.config);
1979 }
1980 wgpu::CurrentSurfaceTexture::Lost => {
1981 log::warn!("surface lost; reconfiguring");
1982 self.surface.configure(&self.device, &self.config);
1983 }
1984 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
1985 return;
1987 }
1988 wgpu::CurrentSurfaceTexture::Validation => {
1989 log::error!("surface validation error");
1990 return;
1991 }
1992 }
1993 };
1994
1995 fn to_ndc(x: f32, y: f32, w: f32, h: f32, fb_w: f32, fb_h: f32) -> [f32; 4] {
1996 let x0 = (x / fb_w) * 2.0 - 1.0;
1997 let y0 = 1.0 - (y / fb_h) * 2.0;
1998 let x1 = ((x + w) / fb_w) * 2.0 - 1.0;
1999 let y1 = 1.0 - ((y + h) / fb_h) * 2.0;
2000 let min_x = x0.min(x1);
2001 let min_y = y0.min(y1);
2002 let w_ndc = (x1 - x0).abs();
2003 let h_ndc = (y1 - y0).abs();
2004 [min_x, min_y, w_ndc, h_ndc]
2005 }
2006
2007 fn to_scissor(r: &repose_core::Rect, fb_w: u32, fb_h: u32) -> (u32, u32, u32, u32) {
2008 let mut x = r.x.floor() as i64;
2009 let mut y = r.y.floor() as i64;
2010 let fb_wi = fb_w as i64;
2011 let fb_hi = fb_h as i64;
2012 x = x.clamp(0, fb_wi.saturating_sub(1));
2013 y = y.clamp(0, fb_hi.saturating_sub(1));
2014 let w_req = r.w.ceil().max(1.0) as i64;
2015 let h_req = r.h.ceil().max(1.0) as i64;
2016 let w = (w_req).min(fb_wi - x).max(1);
2017 let h = (h_req).min(fb_hi - y).max(1);
2018 (x as u32, y as u32, w as u32, h as u32)
2019 }
2020
2021 let fb_w = self.config.width as f32;
2022 let fb_h = self.config.height as f32;
2023
2024 let globals = Globals {
2025 ndc_to_px: [fb_w * 0.5, fb_h * 0.5],
2026 _pad: [0.0, 0.0],
2027 };
2028 self.queue
2029 .write_buffer(&self.globals_buf, 0, bytemuck::bytes_of(&globals));
2030
2031 enum Cmd {
2032 ClipPush {
2033 off: u64,
2034 cnt: u32,
2035 scissor: (u32, u32, u32, u32),
2036 },
2037 ClipPop {
2038 scissor: (u32, u32, u32, u32),
2039 },
2040 Rect {
2041 off: u64,
2042 cnt: u32,
2043 },
2044 Border {
2045 off: u64,
2046 cnt: u32,
2047 },
2048 Ellipse {
2049 off: u64,
2050 cnt: u32,
2051 },
2052 EllipseBorder {
2053 off: u64,
2054 cnt: u32,
2055 },
2056 GlyphsMask {
2057 off: u64,
2058 cnt: u32,
2059 },
2060 GlyphsColor {
2061 off: u64,
2062 cnt: u32,
2063 },
2064 ImageRgba {
2065 off: u64,
2066 cnt: u32,
2067 handle: u64,
2068 },
2069 ImageNv12 {
2070 off: u64,
2071 cnt: u32,
2072 handle: u64,
2073 },
2074 PushTransform(Transform),
2075 PopTransform,
2076 }
2077
2078 let mut cmds: Vec<Cmd> = Vec::with_capacity(scene.nodes.len());
2079
2080 struct Batch {
2081 rects: Vec<RectInstance>,
2082 borders: Vec<BorderInstance>,
2083 ellipses: Vec<EllipseInstance>,
2084 e_borders: Vec<EllipseBorderInstance>,
2085 masks: Vec<GlyphInstance>,
2086 colors: Vec<GlyphInstance>,
2087 nv12s: Vec<Nv12Instance>,
2088 }
2089
2090 impl Batch {
2091 fn new() -> Self {
2092 Self {
2093 rects: vec![],
2094 borders: vec![],
2095 ellipses: vec![],
2096 e_borders: vec![],
2097 masks: vec![],
2098 colors: vec![],
2099 nv12s: vec![],
2100 }
2101 }
2102
2103 fn is_empty(&self) -> bool {
2104 self.rects.is_empty()
2105 && self.borders.is_empty()
2106 && self.ellipses.is_empty()
2107 && self.e_borders.is_empty()
2108 && self.masks.is_empty()
2109 && self.colors.is_empty()
2110 && self.nv12s.is_empty()
2111 }
2112
2113 fn flush(
2114 &mut self,
2115 pipes: (
2116 &mut InstancedPipe<RectInstance>,
2117 &mut InstancedPipe<BorderInstance>,
2118 &mut InstancedPipe<EllipseInstance>,
2119 &mut InstancedPipe<EllipseBorderInstance>,
2120 ),
2121 glyph_pipes: (
2122 &mut InstancedPipe<GlyphInstance>,
2123 &mut InstancedPipe<GlyphInstance>,
2124 ),
2125 nv12_pipe: &mut InstancedPipe<Nv12Instance>,
2126 device: &wgpu::Device,
2127 queue: &wgpu::Queue,
2128 cmds: &mut Vec<Cmd>,
2129 ) {
2130 let (rects, borders, ellipses, e_borders) = pipes;
2131 let (masks, colors) = glyph_pipes;
2132
2133 if !self.rects.is_empty() {
2134 if let Some((off, cnt)) = rects.upload(device, queue, &self.rects) {
2135 cmds.push(Cmd::Rect { off, cnt });
2136 }
2137 self.rects.clear();
2138 }
2139 if !self.borders.is_empty() {
2140 if let Some((off, cnt)) = borders.upload(device, queue, &self.borders) {
2141 cmds.push(Cmd::Border { off, cnt });
2142 }
2143 self.borders.clear();
2144 }
2145 if !self.ellipses.is_empty() {
2146 if let Some((off, cnt)) = ellipses.upload(device, queue, &self.ellipses) {
2147 cmds.push(Cmd::Ellipse { off, cnt });
2148 }
2149 self.ellipses.clear();
2150 }
2151 if !self.e_borders.is_empty() {
2152 if let Some((off, cnt)) = e_borders.upload(device, queue, &self.e_borders) {
2153 cmds.push(Cmd::EllipseBorder { off, cnt });
2154 }
2155 self.e_borders.clear();
2156 }
2157 if !self.masks.is_empty() {
2158 if let Some((off, cnt)) = masks.upload(device, queue, &self.masks) {
2159 cmds.push(Cmd::GlyphsMask { off, cnt });
2160 }
2161 self.masks.clear();
2162 }
2163 if !self.colors.is_empty() {
2164 if let Some((off, cnt)) = colors.upload(device, queue, &self.colors) {
2165 cmds.push(Cmd::GlyphsColor { off, cnt });
2166 }
2167 self.colors.clear();
2168 }
2169 if !self.nv12s.is_empty() {
2170 if let Some((off, cnt)) = nv12_pipe.upload(device, queue, &self.nv12s) {
2171 let _ = (off, cnt);
2173 }
2174 self.nv12s.clear();
2175 }
2176 }
2177 }
2178
2179 self.rects.reset();
2180 self.borders.reset();
2181 self.ellipses.reset();
2182 self.ellipse_borders.reset();
2183 self.glyph_mask.reset();
2184 self.glyph_color.reset();
2185 self.clip_ring.reset();
2186 self.nv12.reset();
2187
2188 let mut batch = Batch::new();
2189 let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
2190 let mut scissor_stack: Vec<repose_core::Rect> = Vec::with_capacity(8);
2191 let root_clip_rect = repose_core::Rect {
2192 x: 0.0,
2193 y: 0.0,
2194 w: fb_w,
2195 h: fb_h,
2196 };
2197
2198 let mut current_prim: Option<&'static str> = None;
2199
2200 macro_rules! flush_if_prim_changed {
2201 ($prim:literal, $pipe:expr) => {
2202 if current_prim != Some($prim) {
2203 flush_batch!();
2204 current_prim = Some($prim);
2205 }
2206 };
2207 }
2208
2209 macro_rules! flush_batch {
2210 () => {
2211 if !batch.is_empty() {
2212 batch.flush(
2213 (
2214 &mut self.rects,
2215 &mut self.borders,
2216 &mut self.ellipses,
2217 &mut self.ellipse_borders,
2218 ),
2219 (&mut self.glyph_mask, &mut self.glyph_color),
2220 &mut self.nv12,
2221 &self.device,
2222 &self.queue,
2223 &mut cmds,
2224 )
2225 }
2226 current_prim = None;
2227 };
2228 }
2229
2230 for node in &scene.nodes {
2231 let t_identity = Transform::identity();
2232 let current_transform = transform_stack.last().unwrap_or(&t_identity);
2233
2234 match node {
2235 SceneNode::Rect {
2236 rect,
2237 brush,
2238 radius,
2239 } => {
2240 flush_if_prim_changed!("rect", &self.rects);
2241 let transformed_rect = current_transform.apply_to_rect(*rect);
2242 let (brush_type, color0, color1, grad_start, grad_end) =
2243 brush_to_instance_fields(brush);
2244 batch.rects.push(RectInstance {
2245 xywh: to_ndc(
2246 transformed_rect.x,
2247 transformed_rect.y,
2248 transformed_rect.w,
2249 transformed_rect.h,
2250 fb_w,
2251 fb_h,
2252 ),
2253 radius: *radius,
2254 brush_type,
2255 color0,
2256 color1,
2257 grad_start,
2258 grad_end,
2259 });
2260 }
2261 SceneNode::Border {
2262 rect,
2263 color,
2264 width,
2265 radius,
2266 } => {
2267 flush_if_prim_changed!("border", &self.borders);
2268 let transformed_rect = current_transform.apply_to_rect(*rect);
2269 batch.borders.push(BorderInstance {
2270 xywh: to_ndc(
2271 transformed_rect.x,
2272 transformed_rect.y,
2273 transformed_rect.w,
2274 transformed_rect.h,
2275 fb_w,
2276 fb_h,
2277 ),
2278 radius: *radius,
2279 stroke: *width,
2280 color: color.to_linear(),
2281 });
2282 }
2283 SceneNode::Ellipse { rect, brush } => {
2284 flush_if_prim_changed!("ellipse", &self.ellipses);
2285 let transformed = current_transform.apply_to_rect(*rect);
2286 let color = brush_to_solid_color(brush);
2287 batch.ellipses.push(EllipseInstance {
2288 xywh: to_ndc(
2289 transformed.x,
2290 transformed.y,
2291 transformed.w,
2292 transformed.h,
2293 fb_w,
2294 fb_h,
2295 ),
2296 color,
2297 });
2298 }
2299 SceneNode::EllipseBorder { rect, color, width } => {
2300 flush_if_prim_changed!("ellipse_border", &self.ellipse_borders);
2301 let transformed = current_transform.apply_to_rect(*rect);
2302 batch.e_borders.push(EllipseBorderInstance {
2303 xywh: to_ndc(
2304 transformed.x,
2305 transformed.y,
2306 transformed.w,
2307 transformed.h,
2308 fb_w,
2309 fb_h,
2310 ),
2311 stroke: *width,
2312 color: color.to_linear(),
2313 });
2314 }
2315 SceneNode::Text {
2316 rect,
2317 text,
2318 color,
2319 size,
2320 font_family,
2321 } => {
2322 flush_batch!(); let px = (*size).clamp(8.0, 96.0);
2325 let shaped = repose_text::shape_line(text.as_ref(), px, *font_family);
2326 let transformed_rect = current_transform.apply_to_rect(*rect);
2327
2328 for sg in shaped {
2329 if let Some(info) = self.upload_glyph_color(sg.key, px as u32) {
2330 let x = transformed_rect.x + sg.x + sg.bearing_x;
2331 let y = transformed_rect.y + sg.y - sg.bearing_y;
2332 batch.colors.push(GlyphInstance {
2333 xywh: to_ndc(x, y, info.w, info.h, fb_w, fb_h),
2334 uv: [info.u0, info.v1, info.u1, info.v0],
2335 color: color.to_linear(),
2336 });
2337 } else if let Some(info) = self.upload_glyph_mask(sg.key, px as u32) {
2338 let x = transformed_rect.x + sg.x + sg.bearing_x;
2339 let y = transformed_rect.y + sg.y - sg.bearing_y;
2340 batch.masks.push(GlyphInstance {
2341 xywh: to_ndc(x, y, info.w, info.h, fb_w, fb_h),
2342 uv: [info.u0, info.v1, info.u1, info.v0],
2343 color: color.to_linear(),
2344 });
2345 }
2346 }
2347 }
2349 SceneNode::Image {
2350 rect,
2351 handle,
2352 tint,
2353 fit,
2354 } => {
2355 flush_batch!();
2356
2357 let (img_w, img_h, is_nv12) = if let Some(t) = self.images.get_mut(handle) {
2359 match t {
2360 ImageTex::Rgba {
2361 w,
2362 h,
2363 last_used_frame,
2364 ..
2365 } => {
2366 *last_used_frame = self.frame_index;
2367 (*w, *h, false)
2368 }
2369 ImageTex::Nv12 {
2370 w,
2371 h,
2372 last_used_frame,
2373 ..
2374 } => {
2375 *last_used_frame = self.frame_index;
2376 (*w, *h, true)
2377 }
2378 }
2379 } else {
2380 log::warn!("Image handle {} not found", handle);
2381 continue;
2382 };
2383
2384 let src_w = img_w as f32;
2385 let src_h = img_h as f32;
2386 let dst_w = rect.w.max(0.0);
2387 let dst_h = rect.h.max(0.0);
2388 if dst_w <= 0.0 || dst_h <= 0.0 {
2389 continue;
2390 }
2391
2392 let (xywh_ndc, uv_rect) = match fit {
2393 repose_core::view::ImageFit::Contain => {
2394 let scale = (dst_w / src_w).min(dst_h / src_h);
2395 let w = src_w * scale;
2396 let h = src_h * scale;
2397 let x = rect.x + (dst_w - w) * 0.5;
2398 let y = rect.y + (dst_h - h) * 0.5;
2399 (to_ndc(x, y, w, h, fb_w, fb_h), [0.0, 1.0, 1.0, 0.0])
2400 }
2401 repose_core::view::ImageFit::Cover => {
2402 let scale = (dst_w / src_w).max(dst_h / src_h);
2403 let content_w = src_w * scale;
2404 let content_h = src_h * scale;
2405 let overflow_x = (content_w - dst_w) * 0.5;
2406 let overflow_y = (content_h - dst_h) * 0.5;
2407 let u0 = (overflow_x / content_w).clamp(0.0, 1.0);
2408 let v0 = (overflow_y / content_h).clamp(0.0, 1.0);
2409 let u1 = ((overflow_x + dst_w) / content_w).clamp(0.0, 1.0);
2410 let v1 = ((overflow_y + dst_h) / content_h).clamp(0.0, 1.0);
2411 (
2412 to_ndc(rect.x, rect.y, dst_w, dst_h, fb_w, fb_h),
2413 [u0, 1.0 - v1, u1, 1.0 - v0],
2414 )
2415 }
2416 repose_core::view::ImageFit::FitWidth => {
2417 let scale = dst_w / src_w;
2418 let w = dst_w;
2419 let h = src_h * scale;
2420 let y = rect.y + (dst_h - h) * 0.5;
2421 (to_ndc(rect.x, y, w, h, fb_w, fb_h), [0.0, 1.0, 1.0, 0.0])
2422 }
2423 repose_core::view::ImageFit::FitHeight => {
2424 let scale = dst_h / src_h;
2425 let w = src_w * scale;
2426 let h = dst_h;
2427 let x = rect.x + (dst_w - w) * 0.5;
2428 (to_ndc(x, rect.y, w, h, fb_w, fb_h), [0.0, 1.0, 1.0, 0.0])
2429 }
2430 };
2431
2432 if is_nv12 {
2433 let full_range = if let Some(ImageTex::Nv12 { full_range, .. }) =
2434 self.images.get(handle)
2435 {
2436 if *full_range { 1.0 } else { 0.0 }
2437 } else {
2438 0.0
2439 };
2440
2441 let inst = Nv12Instance {
2442 xywh: xywh_ndc,
2443 uv: uv_rect,
2444 color: tint.to_linear(),
2445 full_range,
2446 _pad: [0.0; 3],
2447 };
2448 if let Some((off, _)) = self.nv12.upload(&self.device, &self.queue, &[inst])
2449 {
2450 cmds.push(Cmd::ImageNv12 {
2451 off,
2452 cnt: 1,
2453 handle: *handle,
2454 });
2455 }
2456 } else {
2457 let inst = GlyphInstance {
2459 xywh: xywh_ndc,
2460 uv: uv_rect,
2461 color: tint.to_linear(),
2462 };
2463 if let Some((off, _)) =
2464 self.glyph_color.upload(&self.device, &self.queue, &[inst])
2465 {
2466 cmds.push(Cmd::ImageRgba {
2467 off,
2468 cnt: 1,
2469 handle: *handle,
2470 });
2471 }
2472 }
2473 }
2474 SceneNode::PushClip { rect, radius } => {
2475 flush_batch!(); let t_identity = Transform::identity();
2478 let current_transform = transform_stack.last().unwrap_or(&t_identity);
2479 let transformed = current_transform.apply_to_rect(*rect);
2480
2481 let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
2482 let next_scissor = intersect(top, transformed);
2483 scissor_stack.push(next_scissor);
2484 let scissor = to_scissor(&next_scissor, self.config.width, self.config.height);
2485
2486 let inst = ClipInstance {
2487 xywh: to_ndc(
2488 transformed.x,
2489 transformed.y,
2490 transformed.w,
2491 transformed.h,
2492 fb_w,
2493 fb_h,
2494 ),
2495 radius: *radius,
2496 _pad: [0.0; 3],
2497 };
2498 let bytes = bytemuck::bytes_of(&inst);
2499 self.clip_ring.grow_to_fit(&self.device, bytes.len() as u64);
2500 let (off, _) = self.clip_ring.alloc_write(&self.queue, bytes);
2501
2502 cmds.push(Cmd::ClipPush {
2503 off,
2504 cnt: 1,
2505 scissor,
2506 });
2507 }
2508 SceneNode::PopClip => {
2509 flush_batch!();
2510
2511 if !scissor_stack.is_empty() {
2512 scissor_stack.pop();
2513 } else {
2514 log::warn!("PopClip with empty stack");
2515 }
2516
2517 let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
2518 let scissor = to_scissor(&top, self.config.width, self.config.height);
2519 cmds.push(Cmd::ClipPop { scissor });
2520 }
2521 SceneNode::Shadow {
2522 rect,
2523 radius,
2524 elevation: _,
2525 color,
2526 } => {
2527 flush_if_prim_changed!("rect", &self.rects);
2528 let transformed_rect = current_transform.apply_to_rect(*rect);
2529 let (brush_type, color0, _color1, _grad_start, _grad_end) =
2530 brush_to_instance_fields(&Brush::Solid(*color));
2531 batch.rects.push(RectInstance {
2532 xywh: to_ndc(
2533 transformed_rect.x,
2534 transformed_rect.y,
2535 transformed_rect.w,
2536 transformed_rect.h,
2537 fb_w,
2538 fb_h,
2539 ),
2540 radius: *radius,
2541 brush_type,
2542 color0,
2543 color1: [0.0; 4],
2544 grad_start: [0.0; 2],
2545 grad_end: [0.0; 2],
2546 });
2547 }
2548 SceneNode::PushTransform { transform } => {
2549 flush_batch!(); let combined = current_transform.combine(transform);
2551 if transform.rotate != 0.0 {
2552 ROT_WARN_ONCE.call_once(|| {
2553 log::warn!(
2554 "Transform rotation is not supported for Rect/Text/Image; rotation will be ignored."
2555 );
2556 });
2557 }
2558 transform_stack.push(combined);
2559 }
2560 SceneNode::PopTransform => {
2561 flush_batch!(); transform_stack.pop();
2563 }
2564 }
2565 }
2566
2567 flush_batch!();
2568
2569 let mut encoder = self
2570 .device
2571 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2572 label: Some("frame encoder"),
2573 });
2574
2575 {
2576 let swap_view = frame
2577 .texture
2578 .create_view(&wgpu::TextureViewDescriptor::default());
2579
2580 let (color_view, resolve_target) = if let Some(msaa_view) = &self.msaa_view {
2581 (msaa_view, Some(&swap_view))
2582 } else {
2583 (&swap_view, None)
2584 };
2585
2586 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2587 label: Some("main pass"),
2588 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2589 view: color_view,
2590 resolve_target,
2591 ops: wgpu::Operations {
2592 load: wgpu::LoadOp::Clear(wgpu::Color {
2593 r: scene.clear_color.0 as f64 / 255.0,
2594 g: scene.clear_color.1 as f64 / 255.0,
2595 b: scene.clear_color.2 as f64 / 255.0,
2596 a: scene.clear_color.3 as f64 / 255.0,
2597 }),
2598 store: wgpu::StoreOp::Store,
2599 },
2600 depth_slice: None,
2601 })],
2602 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2603 view: &self.depth_stencil_view,
2604 depth_ops: None,
2605 stencil_ops: Some(wgpu::Operations {
2606 load: wgpu::LoadOp::Clear(0),
2607 store: wgpu::StoreOp::Store,
2608 }),
2609 }),
2610 timestamp_writes: None,
2611 occlusion_query_set: None,
2612 multiview_mask: None,
2613 });
2614
2615 let mut clip_depth: u32 = 0;
2616 rpass.set_bind_group(0, &self.globals_bind, &[]);
2617 rpass.set_stencil_reference(clip_depth);
2618 rpass.set_scissor_rect(0, 0, self.config.width, self.config.height);
2619
2620 let bind_mask = self.atlas_bind_group_mask();
2621 let bind_color = self.atlas_bind_group_color();
2622
2623 for cmd in cmds {
2624 match cmd {
2625 Cmd::ClipPush {
2626 off,
2627 cnt: n,
2628 scissor,
2629 } => {
2630 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
2631 rpass.set_stencil_reference(clip_depth);
2632
2633 if self.msaa_samples > 1 {
2634 rpass.set_pipeline(&self.clip_pipeline_a2c);
2635 } else {
2636 rpass.set_pipeline(&self.clip_pipeline_bin);
2637 }
2638
2639 let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
2640 rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
2641 rpass.draw(0..6, 0..n);
2642
2643 clip_depth = (clip_depth + 1).min(255);
2644 rpass.set_stencil_reference(clip_depth);
2645 }
2646
2647 Cmd::ClipPop { scissor } => {
2648 clip_depth = clip_depth.saturating_sub(1);
2649 rpass.set_stencil_reference(clip_depth);
2650 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
2651 }
2652
2653 Cmd::Rect { off, cnt: n } => {
2654 rpass.set_pipeline(&self.rects.pipeline);
2655 let bytes = (n as u64) * std::mem::size_of::<RectInstance>() as u64;
2656 rpass.set_vertex_buffer(0, self.rects.ring.buf.slice(off..off + bytes));
2657 rpass.draw(0..6, 0..n);
2658 }
2659
2660 Cmd::Border { off, cnt: n } => {
2661 rpass.set_pipeline(&self.borders.pipeline);
2662 let bytes = (n as u64) * std::mem::size_of::<BorderInstance>() as u64;
2663 rpass.set_vertex_buffer(0, self.borders.ring.buf.slice(off..off + bytes));
2664 rpass.draw(0..6, 0..n);
2665 }
2666
2667 Cmd::GlyphsMask { off, cnt: n } => {
2668 rpass.set_pipeline(&self.glyph_mask.pipeline);
2669 rpass.set_bind_group(1, &bind_mask, &[]);
2670 let bytes = (n as u64) * std::mem::size_of::<GlyphInstance>() as u64;
2671 rpass
2672 .set_vertex_buffer(0, self.glyph_mask.ring.buf.slice(off..off + bytes));
2673 rpass.draw(0..6, 0..n);
2674 }
2675
2676 Cmd::GlyphsColor { off, cnt: n } => {
2677 rpass.set_pipeline(&self.glyph_color.pipeline);
2678 rpass.set_bind_group(1, &bind_color, &[]);
2679 let bytes = (n as u64) * std::mem::size_of::<GlyphInstance>() as u64;
2680 rpass.set_vertex_buffer(
2681 0,
2682 self.glyph_color.ring.buf.slice(off..off + bytes),
2683 );
2684 rpass.draw(0..6, 0..n);
2685 }
2686
2687 Cmd::ImageRgba {
2688 off,
2689 cnt: n,
2690 handle,
2691 } => {
2692 if let Some(ImageTex::Rgba { bind, .. }) = self.images.get(&handle) {
2693 rpass.set_pipeline(&self.image_pipeline_rgba);
2694 rpass.set_bind_group(1, bind, &[]);
2695 let bytes = (n as u64) * std::mem::size_of::<GlyphInstance>() as u64;
2696 rpass.set_vertex_buffer(
2697 0,
2698 self.glyph_color.ring.buf.slice(off..off + bytes),
2699 );
2700 rpass.draw(0..6, 0..n);
2701 }
2702 }
2703
2704 Cmd::ImageNv12 {
2705 off,
2706 cnt: n,
2707 handle,
2708 } => {
2709 if let Some(ImageTex::Nv12 { bind, .. }) = self.images.get(&handle) {
2710 rpass.set_pipeline(&self.nv12.pipeline);
2711 rpass.set_bind_group(1, bind, &[]);
2712 let bytes = (n as u64) * std::mem::size_of::<Nv12Instance>() as u64;
2713 rpass.set_vertex_buffer(0, self.nv12.ring.buf.slice(off..off + bytes));
2714 rpass.draw(0..6, 0..n);
2715 }
2716 }
2717
2718 Cmd::Ellipse { off, cnt: n } => {
2719 rpass.set_pipeline(&self.ellipses.pipeline);
2720 let bytes = (n as u64) * std::mem::size_of::<EllipseInstance>() as u64;
2721 rpass.set_vertex_buffer(0, self.ellipses.ring.buf.slice(off..off + bytes));
2722 rpass.draw(0..6, 0..n);
2723 }
2724
2725 Cmd::EllipseBorder { off, cnt: n } => {
2726 rpass.set_pipeline(&self.ellipse_borders.pipeline);
2727 let bytes =
2728 (n as u64) * std::mem::size_of::<EllipseBorderInstance>() as u64;
2729 rpass.set_vertex_buffer(
2730 0,
2731 self.ellipse_borders.ring.buf.slice(off..off + bytes),
2732 );
2733 rpass.draw(0..6, 0..n);
2734 }
2735
2736 Cmd::PushTransform(_) => {}
2737 Cmd::PopTransform => {}
2738 }
2739 }
2740 }
2741
2742 self.queue.submit(std::iter::once(encoder.finish()));
2743 if let Err(e) = catch_unwind(AssertUnwindSafe(|| frame.present())) {
2744 log::warn!("frame.present panicked: {:?}", e);
2745 }
2746
2747 self.evict_unused_images();
2749 }
2750}
2751
2752fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> repose_core::Rect {
2753 let x0 = a.x.max(b.x);
2754 let y0 = a.y.max(b.y);
2755 let x1 = (a.x + a.w).min(b.x + b.w);
2756 let y1 = (a.y + a.h).min(b.y + b.h);
2757 repose_core::Rect {
2758 x: x0,
2759 y: y0,
2760 w: (x1 - x0).max(0.0),
2761 h: (y1 - y0).max(0.0),
2762 }
2763}