1use std::collections::HashMap;
8#[cfg(target_os = "macos")]
9use std::ffi::c_void;
10use std::sync::Arc;
11
12use bytemuck::{Pod, Zeroable};
13use lyon_tessellation::geom::point;
14use lyon_tessellation::path::Path;
15use lyon_tessellation::{
16 BuffersBuilder, FillOptions, FillTessellator, FillVertex, StrokeOptions, StrokeTessellator,
17 StrokeVertex, VertexBuffers,
18};
19use wgpu::util::DeviceExt;
20
21use truce_core::cast::len_u32;
22use truce_gui_types::render::{ImageId, RenderBackend};
23use truce_gui_types::theme::Color;
24
25#[repr(C)]
30#[derive(Copy, Clone, Pod, Zeroable)]
31struct Vertex {
32 position: [f32; 2],
33 color: [f32; 4],
34 uv: [f32; 2],
35 tex_mode: f32,
38 _pad: f32,
39}
40
41impl Vertex {
42 fn solid(x: f32, y: f32, color: [f32; 4]) -> Self {
43 Self {
44 position: [x, y],
45 color,
46 uv: [0.0, 0.0],
47 tex_mode: 0.0,
48 _pad: 0.0,
49 }
50 }
51
52 fn glyph(x: f32, y: f32, color: [f32; 4], u: f32, v: f32) -> Self {
53 Self {
54 position: [x, y],
55 color,
56 uv: [u, v],
57 tex_mode: 1.0,
58 _pad: 0.0,
59 }
60 }
61
62 fn image(x: f32, y: f32, color: [f32; 4], u: f32, v: f32) -> Self {
63 Self {
64 position: [x, y],
65 color,
66 uv: [u, v],
67 tex_mode: 2.0,
68 _pad: 0.0,
69 }
70 }
71}
72
73const ATLAS_SIZE: u32 = 512;
78
79struct GlyphUV {
80 u0: f32,
81 v0: f32,
82 u1: f32,
83 v1: f32,
84 advance: f32,
85 width: f32,
86 height: f32,
87 y_offset: f32,
88}
89
90struct GlyphAtlas {
91 shelf_y: u32,
93 shelf_h: u32,
94 cursor_x: u32,
95 glyphs: HashMap<(char, u32), GlyphUV>,
97 pending: Vec<(u32, u32, u32, u32, Vec<u8>)>,
99 overflow_pending: bool,
104}
105
106impl GlyphAtlas {
107 fn new() -> Self {
108 Self {
109 shelf_y: 0,
110 shelf_h: 0,
111 cursor_x: 0,
112 glyphs: HashMap::new(),
113 pending: Vec::new(),
114 overflow_pending: false,
115 }
116 }
117
118 fn clear(&mut self) {
119 self.shelf_y = 0;
120 self.shelf_h = 0;
121 self.cursor_x = 0;
122 self.glyphs.clear();
123 self.overflow_pending = false;
124 }
125
126 #[allow(
134 clippy::cast_possible_truncation,
135 clippy::cast_sign_loss,
136 clippy::cast_precision_loss
137 )]
138 fn ensure_glyph(&mut self, font: &truce_font::raster::FontRaster, ch: char, size: f32) {
139 let key = (ch, (size * 10.0) as u32);
140 if self.glyphs.contains_key(&key) {
141 return;
142 }
143 let glyph = font.rasterize(ch, size);
144 let (gw, gh) = (glyph.width, glyph.height);
145
146 if self.cursor_x + gw > ATLAS_SIZE {
148 self.shelf_y += self.shelf_h;
149 self.shelf_h = 0;
150 self.cursor_x = 0;
151 }
152 if self.shelf_y + gh > ATLAS_SIZE {
153 self.overflow_pending = true;
159 return;
160 }
161
162 let x = self.cursor_x;
163 let y = self.shelf_y;
164 self.cursor_x += gw;
165 self.shelf_h = self.shelf_h.max(gh);
166
167 let u0 = x as f32 / ATLAS_SIZE as f32;
168 let v0 = y as f32 / ATLAS_SIZE as f32;
169 let u1 = (x + gw) as f32 / ATLAS_SIZE as f32;
170 let v1 = (y + gh) as f32 / ATLAS_SIZE as f32;
171
172 self.pending.push((x, y, gw, gh, glyph.coverage));
173
174 self.glyphs.insert(
175 key,
176 GlyphUV {
177 u0,
178 v0,
179 u1,
180 v1,
181 advance: glyph.advance,
182 width: gw as f32,
183 height: gh as f32,
184 y_offset: glyph.y_min,
185 },
186 );
187 }
188}
189
190const SHADER_SRC: &str = r"
195struct Viewport {
196 transform: mat4x4<f32>,
197};
198@group(0) @binding(0) var<uniform> viewport: Viewport;
199
200// At group 1 slot 0 we bind either the R8 glyph atlas (tex_mode == 1.0)
201// or an RGBA image (tex_mode == 2.0). For solid draws (tex_mode == 0.0)
202// the texture is not sampled; any compatible binding works.
203@group(1) @binding(0) var main_tex: texture_2d<f32>;
204@group(1) @binding(1) var main_samp: sampler;
205
206struct VsIn {
207 @location(0) position: vec2<f32>,
208 @location(1) color: vec4<f32>,
209 @location(2) uv: vec2<f32>,
210 @location(3) tex_mode: f32,
211};
212
213struct VsOut {
214 @builtin(position) clip_pos: vec4<f32>,
215 @location(0) color: vec4<f32>,
216 @location(1) uv: vec2<f32>,
217 @location(2) tex_mode: f32,
218};
219
220@vertex
221fn vs_main(in: VsIn) -> VsOut {
222 var out: VsOut;
223 out.clip_pos = viewport.transform * vec4<f32>(in.position, 0.0, 1.0);
224 out.color = in.color;
225 out.uv = in.uv;
226 out.tex_mode = in.tex_mode;
227 return out;
228}
229
230@fragment
231fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
232 let tex = textureSample(main_tex, main_samp, in.uv);
233 if (in.tex_mode > 1.5) {
234 // Image: RGBA texture tinted by vertex color. Both sides are
235 // treated as premultiplied; output is premultiplied.
236 return tex * in.color;
237 }
238 // Glyph (tex_mode == 1) uses .r as coverage; solid (tex_mode == 0)
239 // bypasses the sample. mix(1.0, tex.r, tex_mode) handles both.
240 let alpha = mix(1.0, tex.r, in.tex_mode);
241 return vec4<f32>(in.color.rgb * in.color.a * alpha, in.color.a * alpha);
242}
243";
244
245struct ImageEntry {
252 _texture: wgpu::Texture,
253 bind_group: wgpu::BindGroup,
254}
255
256#[derive(Clone, Copy)]
262struct DrawBatch {
263 index_start: u32,
264 image: Option<ImageId>,
265}
266
267pub struct WgpuBackend {
273 device: Arc<wgpu::Device>,
274 queue: Arc<wgpu::Queue>,
275 surface: Option<wgpu::Surface<'static>>,
280 surface_config: Option<wgpu::SurfaceConfiguration>,
281 #[cfg(not(target_os = "ios"))]
288 pump: Option<crate::pump::PumpClient>,
289 pipeline: wgpu::RenderPipeline,
290 target_format: wgpu::TextureFormat,
293 msaa_texture: wgpu::TextureView,
294 msaa_width: u32,
297 msaa_height: u32,
298 last_acquire_wait: std::time::Duration,
303 vertices: Vec<Vertex>,
304 indices: Vec<u32>,
305 batches: Vec<DrawBatch>,
309 glyph_atlas: GlyphAtlas,
310 font: truce_font::raster::FontRaster,
311 atlas_texture: wgpu::Texture,
312 atlas_bind_group: wgpu::BindGroup,
313 tex_bind_group_layout: wgpu::BindGroupLayout,
316 sampler: wgpu::Sampler,
318 images: Vec<Option<ImageEntry>>,
320 viewport_buffer: wgpu::Buffer,
321 viewport_bind_group: wgpu::BindGroup,
322 clear_color: Option<wgpu::Color>,
328 present_clear_default: wgpu::Color,
334 width: u32,
335 height: u32,
336 scale: f32,
338}
339
340fn ortho_matrix(w: f32, h: f32) -> [[f32; 4]; 4] {
341 [
342 [2.0 / w, 0.0, 0.0, 0.0],
343 [0.0, -2.0 / h, 0.0, 0.0],
344 [0.0, 0.0, 1.0, 0.0],
345 [-1.0, 1.0, 0.0, 1.0],
346 ]
347}
348
349impl WgpuBackend {
350 pub fn from_surface(
361 instance: &wgpu::Instance,
362 surface: wgpu::Surface<'static>,
363 logical_w: u32,
364 logical_h: u32,
365 scale: f32,
366 ) -> Option<Self> {
367 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
368 power_preference: wgpu::PowerPreference::HighPerformance,
369 compatible_surface: Some(&surface),
370 force_fallback_adapter: false,
371 }))
372 .ok()?;
373 let (mut backend, device, config) =
374 Self::pump_init(&adapter, &surface, logical_w, logical_h, scale)?;
375 surface.configure(&device, &config);
376 backend.surface = Some(surface);
377 Some(backend)
378 }
379
380 #[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
391 fn from_parts(
392 device: Arc<wgpu::Device>,
393 queue: Arc<wgpu::Queue>,
394 surface: Option<wgpu::Surface<'static>>,
395 surface_config: wgpu::SurfaceConfiguration,
396 width: u32,
397 height: u32,
398 scale: f32,
399 ) -> Self {
400 let surface_format = surface_config.format;
401
402 let msaa_texture = Self::create_msaa_texture(&device, &surface_config);
404
405 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
407 label: Some("truce-gpu-shader"),
408 source: wgpu::ShaderSource::Wgsl(SHADER_SRC.into()),
409 });
410
411 let matrix = ortho_matrix(width as f32, height as f32);
413 let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
414 label: Some("viewport"),
415 contents: bytemuck::cast_slice(&matrix),
416 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
417 });
418
419 let viewport_bind_group_layout =
420 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
421 label: Some("viewport-layout"),
422 entries: &[wgpu::BindGroupLayoutEntry {
423 binding: 0,
424 visibility: wgpu::ShaderStages::VERTEX,
425 ty: wgpu::BindingType::Buffer {
426 ty: wgpu::BufferBindingType::Uniform,
427 has_dynamic_offset: false,
428 min_binding_size: None,
429 },
430 count: None,
431 }],
432 });
433
434 let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
435 label: Some("viewport-bg"),
436 layout: &viewport_bind_group_layout,
437 entries: &[wgpu::BindGroupEntry {
438 binding: 0,
439 resource: viewport_buffer.as_entire_binding(),
440 }],
441 });
442
443 let atlas_texture = device.create_texture(&wgpu::TextureDescriptor {
445 label: Some("glyph-atlas"),
446 size: wgpu::Extent3d {
447 width: ATLAS_SIZE,
448 height: ATLAS_SIZE,
449 depth_or_array_layers: 1,
450 },
451 mip_level_count: 1,
452 sample_count: 1,
453 dimension: wgpu::TextureDimension::D2,
454 format: wgpu::TextureFormat::R8Unorm,
455 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
456 view_formats: &[],
457 });
458
459 let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
460 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
461 mag_filter: wgpu::FilterMode::Linear,
462 min_filter: wgpu::FilterMode::Linear,
463 ..Default::default()
464 });
465
466 let tex_bind_group_layout =
467 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
468 label: Some("tex-layout"),
469 entries: &[
470 wgpu::BindGroupLayoutEntry {
471 binding: 0,
472 visibility: wgpu::ShaderStages::FRAGMENT,
473 ty: wgpu::BindingType::Texture {
474 sample_type: wgpu::TextureSampleType::Float { filterable: true },
475 view_dimension: wgpu::TextureViewDimension::D2,
476 multisampled: false,
477 },
478 count: None,
479 },
480 wgpu::BindGroupLayoutEntry {
481 binding: 1,
482 visibility: wgpu::ShaderStages::FRAGMENT,
483 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
484 count: None,
485 },
486 ],
487 });
488
489 let atlas_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
490 label: Some("atlas-bg"),
491 layout: &tex_bind_group_layout,
492 entries: &[
493 wgpu::BindGroupEntry {
494 binding: 0,
495 resource: wgpu::BindingResource::TextureView(&atlas_view),
496 },
497 wgpu::BindGroupEntry {
498 binding: 1,
499 resource: wgpu::BindingResource::Sampler(&sampler),
500 },
501 ],
502 });
503
504 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
506 label: Some("truce-gpu-pipeline-layout"),
507 bind_group_layouts: &[
508 Some(&viewport_bind_group_layout),
509 Some(&tex_bind_group_layout),
510 ],
511 immediate_size: 0,
512 });
513
514 let vertex_layout = wgpu::VertexBufferLayout {
515 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
516 step_mode: wgpu::VertexStepMode::Vertex,
517 attributes: &[
518 wgpu::VertexAttribute {
520 offset: 0,
521 shader_location: 0,
522 format: wgpu::VertexFormat::Float32x2,
523 },
524 wgpu::VertexAttribute {
526 offset: 8,
527 shader_location: 1,
528 format: wgpu::VertexFormat::Float32x4,
529 },
530 wgpu::VertexAttribute {
532 offset: 24,
533 shader_location: 2,
534 format: wgpu::VertexFormat::Float32x2,
535 },
536 wgpu::VertexAttribute {
538 offset: 32,
539 shader_location: 3,
540 format: wgpu::VertexFormat::Float32,
541 },
542 ],
543 };
544
545 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
546 label: Some("truce-gpu-pipeline"),
547 layout: Some(&pipeline_layout),
548 vertex: wgpu::VertexState {
549 module: &shader,
550 entry_point: Some("vs_main"),
551 buffers: &[vertex_layout],
552 compilation_options: wgpu::PipelineCompilationOptions::default(),
553 },
554 fragment: Some(wgpu::FragmentState {
555 module: &shader,
556 entry_point: Some("fs_main"),
557 targets: &[Some(wgpu::ColorTargetState {
558 format: surface_format,
559 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
560 write_mask: wgpu::ColorWrites::ALL,
561 })],
562 compilation_options: wgpu::PipelineCompilationOptions::default(),
563 }),
564 primitive: wgpu::PrimitiveState {
565 topology: wgpu::PrimitiveTopology::TriangleList,
566 strip_index_format: None,
567 front_face: wgpu::FrontFace::Ccw,
568 cull_mode: None,
569 unclipped_depth: false,
570 polygon_mode: wgpu::PolygonMode::Fill,
571 conservative: false,
572 },
573 depth_stencil: None,
574 multisample: wgpu::MultisampleState {
575 count: 4,
576 mask: !0,
577 alpha_to_coverage_enabled: false,
578 },
579 multiview_mask: None,
580 cache: None,
581 });
582
583 let font = truce_font::raster::FontRaster::new();
585
586 Self {
587 device,
588 queue,
589 surface,
590 surface_config: Some(surface_config),
591 #[cfg(not(target_os = "ios"))]
592 pump: None,
593 pipeline,
594 target_format: surface_format,
595 msaa_texture,
596 msaa_width: width,
597 msaa_height: height,
598 last_acquire_wait: std::time::Duration::ZERO,
599 vertices: Vec::with_capacity(4096),
600 indices: Vec::with_capacity(8192),
601 batches: Vec::new(),
602 glyph_atlas: GlyphAtlas::new(),
603 font,
604 atlas_texture,
605 atlas_bind_group,
606 tex_bind_group_layout,
607 sampler,
608 images: Vec::new(),
609 viewport_buffer,
610 viewport_bind_group,
611 clear_color: None,
612 present_clear_default: wgpu::Color::BLACK,
613 width,
614 height,
615 scale,
616 }
617 }
618
619 pub fn pump_init(
629 adapter: &wgpu::Adapter,
630 surface: &wgpu::Surface<'static>,
631 logical_w: u32,
632 logical_h: u32,
633 scale: f32,
634 ) -> Option<(Self, wgpu::Device, wgpu::SurfaceConfiguration)> {
635 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
643 label: Some("truce-gpu"),
644 required_features: wgpu::Features::empty(),
645 required_limits: adapter.limits(),
646 experimental_features: wgpu::ExperimentalFeatures::default(),
647 memory_hints: wgpu::MemoryHints::Performance,
648 trace: wgpu::Trace::Off,
649 }))
650 .ok()?;
651 let plain_device = device.clone();
652 let device = Arc::new(device);
653 let queue = Arc::new(queue);
654
655 let max_dim = device.limits().max_texture_dimension_2d.max(1);
656 let width = truce_gui_types::to_physical_px(logical_w, f64::from(scale)).clamp(1, max_dim);
657 let height = truce_gui_types::to_physical_px(logical_h, f64::from(scale)).clamp(1, max_dim);
658
659 let surface_caps = surface.get_capabilities(adapter);
666 let surface_format = surface_caps
667 .formats
668 .iter()
669 .find(|f| **f == wgpu::TextureFormat::Rgba8Unorm)
670 .or_else(|| surface_caps.formats.iter().find(|f| !f.is_srgb()))
671 .copied()
672 .unwrap_or(surface_caps.formats[0]);
673
674 let surface_config = wgpu::SurfaceConfiguration {
675 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
676 format: surface_format,
677 width,
678 height,
679 #[cfg(target_os = "windows")]
685 present_mode: wgpu::PresentMode::AutoNoVsync,
686 #[cfg(not(target_os = "windows"))]
687 present_mode: wgpu::PresentMode::AutoVsync,
688 desired_maximum_frame_latency: 2,
689 alpha_mode: wgpu::CompositeAlphaMode::Auto,
690 view_formats: vec![],
691 };
692
693 let backend = Self::from_parts(
694 device,
695 queue,
696 None,
697 surface_config.clone(),
698 width,
699 height,
700 scale,
701 );
702 Some((backend, plain_device, surface_config))
703 }
704
705 #[cfg(not(target_os = "ios"))]
708 pub fn set_pump(&mut self, client: crate::pump::PumpClient) {
709 self.pump = Some(client);
710 }
711
712 #[cfg(target_os = "macos")]
722 pub unsafe fn from_metal_layer(
723 metal_layer: *mut c_void,
724 logical_w: u32,
725 logical_h: u32,
726 scale: f32,
727 ) -> Option<Self> {
728 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
729 desc.backends = wgpu::Backends::METAL;
730 let instance = wgpu::Instance::new(desc);
731
732 let surface = unsafe {
733 instance
734 .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(metal_layer))
735 }
736 .ok()?;
737
738 Self::from_surface(&instance, surface, logical_w, logical_h, scale)
739 }
740
741 #[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
784 #[must_use]
785 pub fn new(
786 device: Arc<wgpu::Device>,
787 queue: Arc<wgpu::Queue>,
788 target_format: wgpu::TextureFormat,
789 max_logical_w: u32,
790 max_logical_h: u32,
791 scale: f32,
792 ) -> Option<Self> {
793 let scale = scale.max(0.0);
794 let width = truce_gui_types::to_physical_px(max_logical_w, f64::from(scale));
795 let height = truce_gui_types::to_physical_px(max_logical_h, f64::from(scale));
796
797 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
799 label: Some("truce-gpu-shader"),
800 source: wgpu::ShaderSource::Wgsl(SHADER_SRC.into()),
801 });
802
803 let matrix = ortho_matrix(width as f32, height as f32);
805 let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
806 label: Some("viewport"),
807 contents: bytemuck::cast_slice(&matrix),
808 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
809 });
810
811 let viewport_bind_group_layout =
812 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
813 label: Some("viewport-layout"),
814 entries: &[wgpu::BindGroupLayoutEntry {
815 binding: 0,
816 visibility: wgpu::ShaderStages::VERTEX,
817 ty: wgpu::BindingType::Buffer {
818 ty: wgpu::BufferBindingType::Uniform,
819 has_dynamic_offset: false,
820 min_binding_size: None,
821 },
822 count: None,
823 }],
824 });
825
826 let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
827 label: Some("viewport-bg"),
828 layout: &viewport_bind_group_layout,
829 entries: &[wgpu::BindGroupEntry {
830 binding: 0,
831 resource: viewport_buffer.as_entire_binding(),
832 }],
833 });
834
835 let atlas_texture = device.create_texture(&wgpu::TextureDescriptor {
837 label: Some("glyph-atlas"),
838 size: wgpu::Extent3d {
839 width: ATLAS_SIZE,
840 height: ATLAS_SIZE,
841 depth_or_array_layers: 1,
842 },
843 mip_level_count: 1,
844 sample_count: 1,
845 dimension: wgpu::TextureDimension::D2,
846 format: wgpu::TextureFormat::R8Unorm,
847 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
848 view_formats: &[],
849 });
850 let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
851 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
852 mag_filter: wgpu::FilterMode::Linear,
853 min_filter: wgpu::FilterMode::Linear,
854 ..Default::default()
855 });
856 let tex_bind_group_layout =
857 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
858 label: Some("tex-layout"),
859 entries: &[
860 wgpu::BindGroupLayoutEntry {
861 binding: 0,
862 visibility: wgpu::ShaderStages::FRAGMENT,
863 ty: wgpu::BindingType::Texture {
864 sample_type: wgpu::TextureSampleType::Float { filterable: true },
865 view_dimension: wgpu::TextureViewDimension::D2,
866 multisampled: false,
867 },
868 count: None,
869 },
870 wgpu::BindGroupLayoutEntry {
871 binding: 1,
872 visibility: wgpu::ShaderStages::FRAGMENT,
873 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
874 count: None,
875 },
876 ],
877 });
878 let atlas_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
879 label: Some("atlas-bg"),
880 layout: &tex_bind_group_layout,
881 entries: &[
882 wgpu::BindGroupEntry {
883 binding: 0,
884 resource: wgpu::BindingResource::TextureView(&atlas_view),
885 },
886 wgpu::BindGroupEntry {
887 binding: 1,
888 resource: wgpu::BindingResource::Sampler(&sampler),
889 },
890 ],
891 });
892
893 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
895 label: Some("truce-gpu-pipeline-layout"),
896 bind_group_layouts: &[
897 Some(&viewport_bind_group_layout),
898 Some(&tex_bind_group_layout),
899 ],
900 immediate_size: 0,
901 });
902
903 let vertex_layout = wgpu::VertexBufferLayout {
904 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
905 step_mode: wgpu::VertexStepMode::Vertex,
906 attributes: &[
907 wgpu::VertexAttribute {
908 offset: 0,
909 shader_location: 0,
910 format: wgpu::VertexFormat::Float32x2,
911 },
912 wgpu::VertexAttribute {
913 offset: 8,
914 shader_location: 1,
915 format: wgpu::VertexFormat::Float32x4,
916 },
917 wgpu::VertexAttribute {
918 offset: 24,
919 shader_location: 2,
920 format: wgpu::VertexFormat::Float32x2,
921 },
922 wgpu::VertexAttribute {
923 offset: 32,
924 shader_location: 3,
925 format: wgpu::VertexFormat::Float32,
926 },
927 ],
928 };
929
930 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
931 label: Some("truce-gpu-pipeline"),
932 layout: Some(&pipeline_layout),
933 vertex: wgpu::VertexState {
934 module: &shader,
935 entry_point: Some("vs_main"),
936 buffers: &[vertex_layout],
937 compilation_options: wgpu::PipelineCompilationOptions::default(),
938 },
939 fragment: Some(wgpu::FragmentState {
940 module: &shader,
941 entry_point: Some("fs_main"),
942 targets: &[Some(wgpu::ColorTargetState {
943 format: target_format,
944 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
945 write_mask: wgpu::ColorWrites::ALL,
946 })],
947 compilation_options: wgpu::PipelineCompilationOptions::default(),
948 }),
949 primitive: wgpu::PrimitiveState {
950 topology: wgpu::PrimitiveTopology::TriangleList,
951 ..Default::default()
952 },
953 depth_stencil: None,
954 multisample: wgpu::MultisampleState {
955 count: 4,
956 mask: !0,
957 alpha_to_coverage_enabled: false,
958 },
959 multiview_mask: None,
960 cache: None,
961 });
962
963 let msaa_texture = Self::create_msaa_view(&device, target_format, width, height);
965
966 let font = truce_font::raster::FontRaster::new();
967
968 Some(Self {
969 device,
970 queue,
971 surface: None,
972 surface_config: None,
973 #[cfg(not(target_os = "ios"))]
974 pump: None,
975 pipeline,
976 target_format,
977 msaa_texture,
978 msaa_width: width,
979 msaa_height: height,
980 last_acquire_wait: std::time::Duration::ZERO,
981 vertices: Vec::with_capacity(4096),
982 indices: Vec::with_capacity(8192),
983 batches: Vec::new(),
984 glyph_atlas: GlyphAtlas::new(),
985 font,
986 atlas_texture,
987 atlas_bind_group,
988 tex_bind_group_layout,
989 sampler,
990 images: Vec::new(),
991 viewport_buffer,
992 viewport_bind_group,
993 clear_color: None,
994 present_clear_default: wgpu::Color::TRANSPARENT,
995 width,
996 height,
997 scale,
998 })
999 }
1000
1001 #[allow(clippy::cast_precision_loss)]
1012 pub fn begin_frame(&mut self, logical_w: u32, logical_h: u32) {
1013 let phys_w = truce_gui_types::to_physical_px(logical_w, f64::from(self.scale));
1014 let phys_h = truce_gui_types::to_physical_px(logical_h, f64::from(self.scale));
1015 self.vertices.clear();
1016 self.indices.clear();
1017 self.batches.clear();
1018 self.clear_color = None;
1019
1020 if phys_w != self.width || phys_h != self.height {
1021 self.width = phys_w;
1022 self.height = phys_h;
1023 let matrix = ortho_matrix(phys_w as f32, phys_h as f32);
1024 self.queue
1025 .write_buffer(&self.viewport_buffer, 0, bytemuck::cast_slice(&matrix));
1026 }
1027
1028 if phys_w != self.msaa_width || phys_h != self.msaa_height {
1029 self.msaa_texture =
1030 Self::create_msaa_view(&self.device, self.target_format, phys_w, phys_h);
1031 self.msaa_width = phys_w;
1032 self.msaa_height = phys_h;
1033 }
1034 }
1035
1036 pub fn scale(&self) -> f32 {
1041 self.scale
1042 }
1043
1044 pub fn set_scale(&mut self, scale: f32) {
1052 if scale.is_finite() && scale > 0.0 {
1053 self.scale = scale;
1054 }
1055 }
1056
1057 pub fn finish(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
1066 self.flush_atlas();
1067
1068 if self.indices.is_empty() {
1069 self.clear_color = None;
1070 return;
1071 }
1072
1073 let vertex_buffer = self
1074 .device
1075 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1076 label: Some("vertices"),
1077 contents: bytemuck::cast_slice(&self.vertices),
1078 usage: wgpu::BufferUsages::VERTEX,
1079 });
1080
1081 let index_buffer = self
1082 .device
1083 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1084 label: Some("indices"),
1085 contents: bytemuck::cast_slice(&self.indices),
1086 usage: wgpu::BufferUsages::INDEX,
1087 });
1088
1089 let (load, store) = match self.clear_color {
1095 Some(c) => (wgpu::LoadOp::Clear(c), wgpu::StoreOp::Discard),
1096 None => (wgpu::LoadOp::Load, wgpu::StoreOp::Store),
1097 };
1098
1099 {
1100 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1101 label: Some("truce-gpu-frame"),
1102 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1103 view: &self.msaa_texture,
1104 resolve_target: Some(view),
1105 ops: wgpu::Operations { load, store },
1106 depth_slice: None,
1107 })],
1108 depth_stencil_attachment: None,
1109 timestamp_writes: None,
1110 occlusion_query_set: None,
1111 multiview_mask: None,
1112 });
1113
1114 pass.set_pipeline(&self.pipeline);
1115 pass.set_bind_group(0, &self.viewport_bind_group, &[]);
1116 pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1117 pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1118
1119 let total_indices = len_u32(self.indices.len());
1120 if self.batches.is_empty() {
1121 pass.set_bind_group(1, &self.atlas_bind_group, &[]);
1122 pass.draw_indexed(0..total_indices, 0, 0..1);
1123 } else {
1124 for i in 0..self.batches.len() {
1125 let b = self.batches[i];
1126 let end = self
1127 .batches
1128 .get(i + 1)
1129 .map_or(total_indices, |n| n.index_start);
1130 if end <= b.index_start {
1131 continue;
1132 }
1133 let bg = match b.image {
1134 None => &self.atlas_bind_group,
1135 Some(img_id) => {
1136 match self.images.get(img_id.0 as usize).and_then(|s| s.as_ref()) {
1137 Some(entry) => &entry.bind_group,
1138 None => continue,
1139 }
1140 }
1141 };
1142 pass.set_bind_group(1, bg, &[]);
1143 pass.draw_indexed(b.index_start..end, 0, 0..1);
1144 }
1145 }
1146 }
1147
1148 self.clear_color = None;
1149 }
1150
1151 fn create_msaa_view(
1152 device: &wgpu::Device,
1153 format: wgpu::TextureFormat,
1154 width: u32,
1155 height: u32,
1156 ) -> wgpu::TextureView {
1157 let tex = device.create_texture(&wgpu::TextureDescriptor {
1158 label: Some("msaa"),
1159 size: wgpu::Extent3d {
1160 width,
1161 height,
1162 depth_or_array_layers: 1,
1163 },
1164 mip_level_count: 1,
1165 sample_count: 4,
1166 dimension: wgpu::TextureDimension::D2,
1167 format,
1168 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1169 view_formats: &[],
1170 });
1171 tex.create_view(&wgpu::TextureViewDescriptor::default())
1172 }
1173
1174 fn create_msaa_texture(
1175 device: &wgpu::Device,
1176 config: &wgpu::SurfaceConfiguration,
1177 ) -> wgpu::TextureView {
1178 let tex = device.create_texture(&wgpu::TextureDescriptor {
1179 label: Some("msaa"),
1180 size: wgpu::Extent3d {
1181 width: config.width,
1182 height: config.height,
1183 depth_or_array_layers: 1,
1184 },
1185 mip_level_count: 1,
1186 sample_count: 4,
1187 dimension: wgpu::TextureDimension::D2,
1188 format: config.format,
1189 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1190 view_formats: &[],
1191 });
1192 tex.create_view(&wgpu::TextureViewDescriptor::default())
1193 }
1194
1195 #[must_use]
1198 pub fn acquire_wait(&self) -> std::time::Duration {
1199 self.last_acquire_wait
1200 }
1201
1202 #[allow(clippy::cast_precision_loss)]
1208 pub fn resize(&mut self, logical_w: u32, logical_h: u32) -> bool {
1209 let max_dim = self.device.limits().max_texture_dimension_2d.max(1);
1214 let new_w =
1215 truce_gui_types::to_physical_px(logical_w, f64::from(self.scale)).clamp(1, max_dim);
1216 let new_h =
1217 truce_gui_types::to_physical_px(logical_h, f64::from(self.scale)).clamp(1, max_dim);
1218 if new_w == self.width && new_h == self.height {
1219 return false;
1220 }
1221 self.width = new_w;
1222 self.height = new_h;
1223
1224 if let Some(ref surface) = self.surface
1225 && let Some(ref mut config) = self.surface_config
1226 {
1227 config.width = new_w;
1228 config.height = new_h;
1229 surface.configure(&self.device, config);
1230 self.msaa_texture = Self::create_msaa_texture(&self.device, config);
1231 }
1232 #[cfg(not(target_os = "ios"))]
1235 if self.surface.is_none()
1236 && let Some(ref pump) = self.pump
1237 && let Some(ref mut config) = self.surface_config
1238 {
1239 config.width = new_w;
1240 config.height = new_h;
1241 pump.resize(new_w, new_h);
1242 self.msaa_texture = Self::create_msaa_texture(&self.device, config);
1243 }
1244
1245 let matrix = ortho_matrix(new_w as f32, new_h as f32);
1247 self.queue
1248 .write_buffer(&self.viewport_buffer, 0, bytemuck::cast_slice(&matrix));
1249
1250 true
1251 }
1252
1253 fn color_arr(c: Color) -> [f32; 4] {
1256 [c.r, c.g, c.b, c.a]
1257 }
1258
1259 fn ensure_batch(&mut self, image: Option<ImageId>) {
1262 let needs_new = self.batches.last().is_none_or(|last| last.image != image);
1263 if needs_new {
1264 self.batches.push(DrawBatch {
1265 index_start: len_u32(self.indices.len()),
1266 image,
1267 });
1268 }
1269 }
1270
1271 fn push_quad(&mut self, v0: Vertex, v1: Vertex, v2: Vertex, v3: Vertex) {
1272 self.ensure_batch(None);
1273 let base = len_u32(self.vertices.len());
1274 self.vertices.extend_from_slice(&[v0, v1, v2, v3]);
1275 self.indices
1276 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
1277 }
1278
1279 fn fill_path(&mut self, path: &Path, color: [f32; 4]) {
1281 self.ensure_batch(None);
1282 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
1283 let mut tessellator = FillTessellator::new();
1284 let _ = tessellator.tessellate_path(
1285 path,
1286 &FillOptions::tolerance(0.5),
1287 &mut BuffersBuilder::new(&mut buffers, |vertex: FillVertex| {
1288 let p = vertex.position();
1289 Vertex::solid(p.x, p.y, color)
1290 }),
1291 );
1292 let base = len_u32(self.vertices.len());
1293 self.vertices.extend_from_slice(&buffers.vertices);
1294 self.indices
1295 .extend(buffers.indices.iter().map(|i| i + base));
1296 }
1297
1298 fn stroke_path(&mut self, path: &Path, color: [f32; 4], opts: &StrokeOptions) {
1300 self.ensure_batch(None);
1301 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
1302 let mut tessellator = StrokeTessellator::new();
1303 let _ = tessellator.tessellate_path(
1304 path,
1305 opts,
1306 &mut BuffersBuilder::new(&mut buffers, |vertex: StrokeVertex| {
1307 let p = vertex.position();
1308 Vertex::solid(p.x, p.y, color)
1309 }),
1310 );
1311 let base = len_u32(self.vertices.len());
1312 self.vertices.extend_from_slice(&buffers.vertices);
1313 self.indices
1314 .extend(buffers.indices.iter().map(|i| i + base));
1315 }
1316
1317 fn flush_atlas(&mut self) {
1319 for (x, y, w, h, data) in self.glyph_atlas.pending.drain(..) {
1320 if w == 0 || h == 0 {
1321 continue;
1322 }
1323 self.queue.write_texture(
1324 wgpu::TexelCopyTextureInfo {
1325 texture: &self.atlas_texture,
1326 mip_level: 0,
1327 origin: wgpu::Origin3d { x, y, z: 0 },
1328 aspect: wgpu::TextureAspect::All,
1329 },
1330 &data,
1331 wgpu::TexelCopyBufferLayout {
1332 offset: 0,
1333 bytes_per_row: Some(w),
1334 rows_per_image: Some(h),
1335 },
1336 wgpu::Extent3d {
1337 width: w,
1338 height: h,
1339 depth_or_array_layers: 1,
1340 },
1341 );
1342 }
1343 }
1344}
1345
1346#[allow(clippy::many_single_char_names)]
1356impl RenderBackend for WgpuBackend {
1357 fn clear(&mut self, color: Color) {
1358 self.clear_color = Some(wgpu::Color {
1359 r: f64::from(color.r),
1360 g: f64::from(color.g),
1361 b: f64::from(color.b),
1362 a: f64::from(color.a),
1363 });
1364 self.vertices.clear();
1365 self.indices.clear();
1366 self.batches.clear();
1367 if self.glyph_atlas.overflow_pending {
1372 self.glyph_atlas.clear();
1373 }
1374 }
1375
1376 fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Color) {
1377 let s = self.scale;
1378 let c = Self::color_arr(color);
1379 self.push_quad(
1380 Vertex::solid(x * s, y * s, c),
1381 Vertex::solid((x + w) * s, y * s, c),
1382 Vertex::solid((x + w) * s, (y + h) * s, c),
1383 Vertex::solid(x * s, (y + h) * s, c),
1384 );
1385 }
1386
1387 fn fill_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color) {
1388 let s = self.scale;
1389 let c = Self::color_arr(color);
1390 let mut builder = Path::builder();
1391 builder.add_circle(
1392 point(cx * s, cy * s),
1393 radius * s,
1394 lyon_tessellation::path::Winding::Positive,
1395 );
1396 let path = builder.build();
1397 self.fill_path(&path, c);
1398 }
1399
1400 fn stroke_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color, width: f32) {
1401 let s = self.scale;
1402 let c = Self::color_arr(color);
1403 let mut builder = Path::builder();
1404 builder.add_circle(
1405 point(cx * s, cy * s),
1406 radius * s,
1407 lyon_tessellation::path::Winding::Positive,
1408 );
1409 let path = builder.build();
1410 let opts = StrokeOptions::tolerance(0.5).with_line_width(width * s);
1411 self.stroke_path(&path, c, &opts);
1412 }
1413
1414 #[allow(clippy::cast_precision_loss)]
1415 fn stroke_arc(
1416 &mut self,
1417 cx: f32,
1418 cy: f32,
1419 radius: f32,
1420 start_angle: f32,
1421 end_angle: f32,
1422 color: Color,
1423 width: f32,
1424 ) {
1425 let s = self.scale;
1426 let c = Self::color_arr(color);
1427 let segments = 64u32;
1428 let sweep = end_angle - start_angle;
1429 let step = sweep / segments as f32;
1430
1431 let mut builder = Path::builder();
1432 builder.begin(point(
1433 cx * s + radius * s * start_angle.cos(),
1434 cy * s + radius * s * start_angle.sin(),
1435 ));
1436 for i in 1..=segments {
1437 let angle = start_angle + step * i as f32;
1438 builder.line_to(point(
1439 cx * s + radius * s * angle.cos(),
1440 cy * s + radius * s * angle.sin(),
1441 ));
1442 }
1443 builder.end(false);
1444 let path = builder.build();
1445
1446 let opts = StrokeOptions::tolerance(0.5)
1447 .with_line_width(width * s)
1448 .with_line_cap(lyon_tessellation::LineCap::Round);
1449 self.stroke_path(&path, c, &opts);
1450 }
1451
1452 fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color, width: f32) {
1453 let s = self.scale;
1454 let c = Self::color_arr(color);
1455 let mut builder = Path::builder();
1456 builder.begin(point(x1 * s, y1 * s));
1457 builder.line_to(point(x2 * s, y2 * s));
1458 builder.end(false);
1459 let path = builder.build();
1460
1461 let opts = StrokeOptions::tolerance(0.5)
1462 .with_line_width(width * s)
1463 .with_line_cap(lyon_tessellation::LineCap::Round);
1464 self.stroke_path(&path, c, &opts);
1465 }
1466
1467 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1470 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: Color) {
1471 let s = self.scale;
1472 let phys_size = size * s;
1473 let c = Self::color_arr(color);
1474 let ascent = self.font.ascent(phys_size);
1475
1476 let mut cursor_x = x * s;
1477
1478 let chars: Vec<char> = text.chars().collect();
1479 for &ch in &chars {
1480 self.glyph_atlas.ensure_glyph(&self.font, ch, phys_size);
1481 }
1482
1483 for &ch in &chars {
1489 let key = (ch, (phys_size * 10.0) as u32);
1490 let Some(g) = self.glyph_atlas.glyphs.get(&key) else {
1491 continue;
1492 };
1493 let (u0, v0, u1, v1, gw, gh, y_off, advance) = (
1494 g.u0, g.v0, g.u1, g.v1, g.width, g.height, g.y_offset, g.advance,
1495 );
1496 let gx = cursor_x.round();
1506 let gy = (y * s + ascent - y_off - gh).round();
1507
1508 self.push_quad(
1509 Vertex::glyph(gx, gy, c, u0, v0),
1510 Vertex::glyph(gx + gw, gy, c, u1, v0),
1511 Vertex::glyph(gx + gw, gy + gh, c, u1, v1),
1512 Vertex::glyph(gx, gy + gh, c, u0, v1),
1513 );
1514
1515 cursor_x += advance;
1516 }
1517 }
1518
1519 fn text_width(&self, text: &str, size: f32) -> f32 {
1520 let phys_size = size * self.scale;
1521 let phys: f32 = text
1524 .chars()
1525 .map(|ch| self.font.advance(ch, phys_size))
1526 .sum();
1527 phys / self.scale
1528 }
1529
1530 fn register_image(&mut self, rgba: &[u8], width: u32, height: u32) -> ImageId {
1531 let expected = (width as usize) * (height as usize) * 4;
1532 if width == 0 || height == 0 || rgba.len() < expected {
1533 return ImageId::INVALID;
1534 }
1535
1536 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1537 label: Some("image"),
1538 size: wgpu::Extent3d {
1539 width,
1540 height,
1541 depth_or_array_layers: 1,
1542 },
1543 mip_level_count: 1,
1544 sample_count: 1,
1545 dimension: wgpu::TextureDimension::D2,
1546 format: wgpu::TextureFormat::Rgba8Unorm,
1547 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1548 view_formats: &[],
1549 });
1550
1551 self.queue.write_texture(
1552 wgpu::TexelCopyTextureInfo {
1553 texture: &texture,
1554 mip_level: 0,
1555 origin: wgpu::Origin3d::ZERO,
1556 aspect: wgpu::TextureAspect::All,
1557 },
1558 &rgba[..expected],
1559 wgpu::TexelCopyBufferLayout {
1560 offset: 0,
1561 bytes_per_row: Some(width * 4),
1562 rows_per_image: Some(height),
1563 },
1564 wgpu::Extent3d {
1565 width,
1566 height,
1567 depth_or_array_layers: 1,
1568 },
1569 );
1570
1571 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1572 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1573 label: Some("image-bg"),
1574 layout: &self.tex_bind_group_layout,
1575 entries: &[
1576 wgpu::BindGroupEntry {
1577 binding: 0,
1578 resource: wgpu::BindingResource::TextureView(&view),
1579 },
1580 wgpu::BindGroupEntry {
1581 binding: 1,
1582 resource: wgpu::BindingResource::Sampler(&self.sampler),
1583 },
1584 ],
1585 });
1586
1587 let entry = ImageEntry {
1588 _texture: texture,
1589 bind_group,
1590 };
1591
1592 if let Some((idx, slot)) = self
1593 .images
1594 .iter_mut()
1595 .enumerate()
1596 .find(|(_, s)| s.is_none())
1597 {
1598 *slot = Some(entry);
1599 return ImageId(len_u32(idx));
1600 }
1601 let id = len_u32(self.images.len());
1602 self.images.push(Some(entry));
1603 ImageId(id)
1604 }
1605
1606 fn unregister_image(&mut self, id: ImageId) {
1607 if let Some(slot) = self.images.get_mut(id.0 as usize) {
1608 *slot = None;
1609 }
1610 }
1611
1612 fn draw_image(&mut self, id: ImageId, x: f32, y: f32, w: f32, h: f32) {
1613 if self
1614 .images
1615 .get(id.0 as usize)
1616 .and_then(|s| s.as_ref())
1617 .is_none()
1618 {
1619 return;
1620 }
1621 self.ensure_batch(Some(id));
1622
1623 let s = self.scale;
1624 let c = [1.0, 1.0, 1.0, 1.0];
1625 let base = len_u32(self.vertices.len());
1626 self.vertices.extend_from_slice(&[
1627 Vertex::image(x * s, y * s, c, 0.0, 0.0),
1628 Vertex::image((x + w) * s, y * s, c, 1.0, 0.0),
1629 Vertex::image((x + w) * s, (y + h) * s, c, 1.0, 1.0),
1630 Vertex::image(x * s, (y + h) * s, c, 0.0, 1.0),
1631 ]);
1632 self.indices
1633 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
1634 }
1635
1636 fn present(&mut self) {
1637 #[cfg(not(target_os = "ios"))]
1646 if let Some(pump) = self.pump.clone() {
1647 let frame = pump.try_take_frame();
1648 self.last_acquire_wait = pump.last_acquire_wait();
1649 let Some(frame) = frame else {
1650 return;
1651 };
1652 let expected = self.surface_config.as_ref().map(|c| (c.width, c.height));
1655 if expected != Some((frame.texture.width(), frame.texture.height())) {
1656 pump.discard(frame);
1657 return;
1658 }
1659 self.flush_atlas();
1662 let frame_view = frame
1663 .texture
1664 .create_view(&wgpu::TextureViewDescriptor::default());
1665 if self.vertices.is_empty() {
1666 self.clear_only_pass(&frame_view);
1667 } else {
1668 self.render_pass(&frame_view);
1669 }
1670 pump.present(frame);
1671 return;
1672 }
1673
1674 self.flush_atlas();
1676
1677 let Some(surface) = &self.surface else {
1678 return; };
1680
1681 let acquire_start = std::time::Instant::now();
1690 let mut acquired = None;
1691 let mut transient_skip = false;
1692 for _ in 0..2 {
1693 match surface.get_current_texture() {
1694 wgpu::CurrentSurfaceTexture::Success(frame)
1695 | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
1696 acquired = Some(frame);
1697 break;
1698 }
1699 wgpu::CurrentSurfaceTexture::Outdated
1700 | wgpu::CurrentSurfaceTexture::Lost
1701 | wgpu::CurrentSurfaceTexture::Validation => {
1702 if let Some(config) = &self.surface_config {
1703 surface.configure(&self.device, config);
1704 }
1705 }
1706 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
1707 transient_skip = true;
1708 break;
1709 }
1710 }
1711 }
1712 self.last_acquire_wait = acquire_start.elapsed();
1713 if transient_skip {
1714 return;
1715 }
1716 let Some(frame) = acquired else {
1717 return;
1718 };
1719 let frame_view = frame
1720 .texture
1721 .create_view(&wgpu::TextureViewDescriptor::default());
1722
1723 if self.vertices.is_empty() {
1724 self.clear_only_pass(&frame_view);
1730 frame.present();
1731 return;
1732 }
1733
1734 self.render_pass(&frame_view);
1735 frame.present();
1736 }
1737}
1738
1739impl WgpuBackend {
1740 fn clear_only_pass(&mut self, resolve_target: &wgpu::TextureView) {
1746 let clear_color = self.clear_color.unwrap_or(self.present_clear_default);
1747 let mut encoder = self
1748 .device
1749 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1750 label: Some("clear-only"),
1751 });
1752 {
1753 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1754 label: Some("clear-only"),
1755 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1756 view: &self.msaa_texture,
1757 resolve_target: Some(resolve_target),
1758 ops: wgpu::Operations {
1759 load: wgpu::LoadOp::Clear(clear_color),
1760 store: wgpu::StoreOp::Discard,
1761 },
1762 depth_slice: None,
1763 })],
1764 depth_stencil_attachment: None,
1765 timestamp_writes: None,
1766 occlusion_query_set: None,
1767 multiview_mask: None,
1768 });
1769 }
1770 self.queue.submit(std::iter::once(encoder.finish()));
1771 }
1772
1773 fn render_pass(&mut self, resolve_target: &wgpu::TextureView) {
1775 let clear_color = self.clear_color.unwrap_or(self.present_clear_default);
1776 let vertex_buffer = self
1777 .device
1778 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1779 label: Some("vertices"),
1780 contents: bytemuck::cast_slice(&self.vertices),
1781 usage: wgpu::BufferUsages::VERTEX,
1782 });
1783
1784 let index_buffer = self
1785 .device
1786 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1787 label: Some("indices"),
1788 contents: bytemuck::cast_slice(&self.indices),
1789 usage: wgpu::BufferUsages::INDEX,
1790 });
1791
1792 let mut encoder = self
1793 .device
1794 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1795 label: Some("frame"),
1796 });
1797
1798 {
1799 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1800 label: Some("main"),
1801 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1802 view: &self.msaa_texture,
1803 resolve_target: Some(resolve_target),
1804 ops: wgpu::Operations {
1805 load: wgpu::LoadOp::Clear(clear_color),
1806 store: wgpu::StoreOp::Discard,
1807 },
1808 depth_slice: None,
1809 })],
1810 depth_stencil_attachment: None,
1811 timestamp_writes: None,
1812 occlusion_query_set: None,
1813 multiview_mask: None,
1814 });
1815
1816 pass.set_pipeline(&self.pipeline);
1817 pass.set_bind_group(0, &self.viewport_bind_group, &[]);
1818 pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1819 pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1820
1821 let total_indices = len_u32(self.indices.len());
1822 if self.batches.is_empty() {
1823 pass.set_bind_group(1, &self.atlas_bind_group, &[]);
1827 pass.draw_indexed(0..total_indices, 0, 0..1);
1828 } else {
1829 for i in 0..self.batches.len() {
1830 let b = self.batches[i];
1831 let end = self
1832 .batches
1833 .get(i + 1)
1834 .map_or(total_indices, |n| n.index_start);
1835 if end <= b.index_start {
1836 continue;
1837 }
1838 let bg = match b.image {
1839 None => &self.atlas_bind_group,
1840 Some(img_id) => {
1841 match self.images.get(img_id.0 as usize).and_then(|s| s.as_ref()) {
1842 Some(entry) => &entry.bind_group,
1843 None => continue,
1845 }
1846 }
1847 };
1848 pass.set_bind_group(1, bg, &[]);
1849 pass.draw_indexed(b.index_start..end, 0, 0..1);
1850 }
1851 }
1852 }
1853
1854 self.queue.submit(std::iter::once(encoder.finish()));
1855 }
1856
1857 #[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
1866 #[must_use]
1867 pub fn headless(width: u32, height: u32, scale: f32) -> Option<Self> {
1868 let phys_w = truce_gui_types::to_physical_px(width, f64::from(scale));
1869 let phys_h = truce_gui_types::to_physical_px(height, f64::from(scale));
1870
1871 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
1872 desc.backends = wgpu::Backends::PRIMARY;
1873 let instance = wgpu::Instance::new(desc);
1874
1875 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1885 power_preference: wgpu::PowerPreference::HighPerformance,
1886 compatible_surface: None,
1887 force_fallback_adapter: false,
1888 }))
1889 .ok()?;
1890
1891 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1892 label: Some("truce-gpu-headless"),
1893 required_features: wgpu::Features::empty(),
1894 required_limits: adapter.limits(),
1895 experimental_features: wgpu::ExperimentalFeatures::default(),
1896 memory_hints: wgpu::MemoryHints::Performance,
1897 trace: wgpu::Trace::Off,
1898 }))
1899 .ok()?;
1900 let device = Arc::new(device);
1901 let queue = Arc::new(queue);
1902
1903 let texture_format = wgpu::TextureFormat::Rgba8Unorm;
1905
1906 let msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
1908 label: Some("msaa"),
1909 size: wgpu::Extent3d {
1910 width: phys_w,
1911 height: phys_h,
1912 depth_or_array_layers: 1,
1913 },
1914 mip_level_count: 1,
1915 sample_count: 4,
1916 dimension: wgpu::TextureDimension::D2,
1917 format: texture_format,
1918 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1919 view_formats: &[],
1920 });
1921 let msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
1922
1923 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1925 label: Some("truce-gpu-shader"),
1926 source: wgpu::ShaderSource::Wgsl(SHADER_SRC.into()),
1927 });
1928
1929 let matrix = ortho_matrix(phys_w as f32, phys_h as f32);
1931 let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1932 label: Some("viewport"),
1933 contents: bytemuck::cast_slice(&matrix),
1934 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1935 });
1936
1937 let viewport_bind_group_layout =
1938 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1939 label: Some("viewport-layout"),
1940 entries: &[wgpu::BindGroupLayoutEntry {
1941 binding: 0,
1942 visibility: wgpu::ShaderStages::VERTEX,
1943 ty: wgpu::BindingType::Buffer {
1944 ty: wgpu::BufferBindingType::Uniform,
1945 has_dynamic_offset: false,
1946 min_binding_size: None,
1947 },
1948 count: None,
1949 }],
1950 });
1951
1952 let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1953 label: Some("viewport-bg"),
1954 layout: &viewport_bind_group_layout,
1955 entries: &[wgpu::BindGroupEntry {
1956 binding: 0,
1957 resource: viewport_buffer.as_entire_binding(),
1958 }],
1959 });
1960
1961 let atlas_texture = device.create_texture(&wgpu::TextureDescriptor {
1963 label: Some("glyph-atlas"),
1964 size: wgpu::Extent3d {
1965 width: ATLAS_SIZE,
1966 height: ATLAS_SIZE,
1967 depth_or_array_layers: 1,
1968 },
1969 mip_level_count: 1,
1970 sample_count: 1,
1971 dimension: wgpu::TextureDimension::D2,
1972 format: wgpu::TextureFormat::R8Unorm,
1973 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1974 view_formats: &[],
1975 });
1976 let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
1977 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1978 mag_filter: wgpu::FilterMode::Linear,
1979 min_filter: wgpu::FilterMode::Linear,
1980 ..Default::default()
1981 });
1982 let tex_bind_group_layout =
1983 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1984 label: Some("tex-layout"),
1985 entries: &[
1986 wgpu::BindGroupLayoutEntry {
1987 binding: 0,
1988 visibility: wgpu::ShaderStages::FRAGMENT,
1989 ty: wgpu::BindingType::Texture {
1990 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1991 view_dimension: wgpu::TextureViewDimension::D2,
1992 multisampled: false,
1993 },
1994 count: None,
1995 },
1996 wgpu::BindGroupLayoutEntry {
1997 binding: 1,
1998 visibility: wgpu::ShaderStages::FRAGMENT,
1999 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2000 count: None,
2001 },
2002 ],
2003 });
2004 let atlas_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2005 label: Some("atlas-bg"),
2006 layout: &tex_bind_group_layout,
2007 entries: &[
2008 wgpu::BindGroupEntry {
2009 binding: 0,
2010 resource: wgpu::BindingResource::TextureView(&atlas_view),
2011 },
2012 wgpu::BindGroupEntry {
2013 binding: 1,
2014 resource: wgpu::BindingResource::Sampler(&sampler),
2015 },
2016 ],
2017 });
2018
2019 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2021 label: Some("truce-gpu-pipeline-layout"),
2022 bind_group_layouts: &[
2023 Some(&viewport_bind_group_layout),
2024 Some(&tex_bind_group_layout),
2025 ],
2026 immediate_size: 0,
2027 });
2028
2029 let vertex_layout = wgpu::VertexBufferLayout {
2030 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
2031 step_mode: wgpu::VertexStepMode::Vertex,
2032 attributes: &[
2033 wgpu::VertexAttribute {
2034 offset: 0,
2035 shader_location: 0,
2036 format: wgpu::VertexFormat::Float32x2,
2037 },
2038 wgpu::VertexAttribute {
2039 offset: 8,
2040 shader_location: 1,
2041 format: wgpu::VertexFormat::Float32x4,
2042 },
2043 wgpu::VertexAttribute {
2044 offset: 24,
2045 shader_location: 2,
2046 format: wgpu::VertexFormat::Float32x2,
2047 },
2048 wgpu::VertexAttribute {
2049 offset: 32,
2050 shader_location: 3,
2051 format: wgpu::VertexFormat::Float32,
2052 },
2053 ],
2054 };
2055
2056 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2057 label: Some("truce-gpu-pipeline"),
2058 layout: Some(&pipeline_layout),
2059 vertex: wgpu::VertexState {
2060 module: &shader,
2061 entry_point: Some("vs_main"),
2062 buffers: &[vertex_layout],
2063 compilation_options: wgpu::PipelineCompilationOptions::default(),
2064 },
2065 fragment: Some(wgpu::FragmentState {
2066 module: &shader,
2067 entry_point: Some("fs_main"),
2068 targets: &[Some(wgpu::ColorTargetState {
2069 format: texture_format,
2070 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
2071 write_mask: wgpu::ColorWrites::ALL,
2072 })],
2073 compilation_options: wgpu::PipelineCompilationOptions::default(),
2074 }),
2075 primitive: wgpu::PrimitiveState {
2076 topology: wgpu::PrimitiveTopology::TriangleList,
2077 ..Default::default()
2078 },
2079 depth_stencil: None,
2080 multisample: wgpu::MultisampleState {
2081 count: 4,
2082 mask: !0,
2083 alpha_to_coverage_enabled: false,
2084 },
2085 multiview_mask: None,
2086 cache: None,
2087 });
2088
2089 let font = truce_font::raster::FontRaster::new();
2090
2091 Some(Self {
2092 device,
2093 queue,
2094 surface: None,
2095 surface_config: None,
2096 #[cfg(not(target_os = "ios"))]
2097 pump: None,
2098 pipeline,
2099 target_format: texture_format,
2100 msaa_texture: msaa_view,
2101 msaa_width: phys_w,
2102 msaa_height: phys_h,
2103 vertices: Vec::with_capacity(4096),
2104 indices: Vec::with_capacity(8192),
2105 batches: Vec::new(),
2106 glyph_atlas: GlyphAtlas::new(),
2107 font,
2108 atlas_texture,
2109 atlas_bind_group,
2110 tex_bind_group_layout,
2111 sampler,
2112 images: Vec::new(),
2113 viewport_buffer,
2114 viewport_bind_group,
2115 clear_color: None,
2116 present_clear_default: wgpu::Color::BLACK,
2117 width: phys_w,
2118 height: phys_h,
2119 scale,
2120 last_acquire_wait: std::time::Duration::ZERO,
2121 })
2122 }
2123
2124 pub fn read_pixels(&mut self) -> Vec<u8> {
2134 self.flush_atlas();
2135
2136 let w = self.width;
2137 let h = self.height;
2138 let format = wgpu::TextureFormat::Rgba8Unorm;
2139
2140 let target_texture = self.device.create_texture(&wgpu::TextureDescriptor {
2142 label: Some("offscreen"),
2143 size: wgpu::Extent3d {
2144 width: w,
2145 height: h,
2146 depth_or_array_layers: 1,
2147 },
2148 mip_level_count: 1,
2149 sample_count: 1,
2150 dimension: wgpu::TextureDimension::D2,
2151 format,
2152 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2153 view_formats: &[],
2154 });
2155 let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
2156
2157 if !self.vertices.is_empty() {
2159 self.render_pass(&target_view);
2160 }
2161
2162 let bytes_per_row = (w * 4 + 255) & !255; let readback_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
2165 label: Some("readback"),
2166 size: u64::from(bytes_per_row * h),
2167 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2168 mapped_at_creation: false,
2169 });
2170
2171 let mut encoder = self
2172 .device
2173 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2174 label: Some("readback"),
2175 });
2176 encoder.copy_texture_to_buffer(
2177 wgpu::TexelCopyTextureInfo {
2178 texture: &target_texture,
2179 mip_level: 0,
2180 origin: wgpu::Origin3d::ZERO,
2181 aspect: wgpu::TextureAspect::All,
2182 },
2183 wgpu::TexelCopyBufferInfo {
2184 buffer: &readback_buf,
2185 layout: wgpu::TexelCopyBufferLayout {
2186 offset: 0,
2187 bytes_per_row: Some(bytes_per_row),
2188 rows_per_image: None,
2189 },
2190 },
2191 wgpu::Extent3d {
2192 width: w,
2193 height: h,
2194 depth_or_array_layers: 1,
2195 },
2196 );
2197 self.queue.submit(std::iter::once(encoder.finish()));
2198
2199 let buf_slice = readback_buf.slice(..);
2201 let (tx, rx) = std::sync::mpsc::channel();
2202 buf_slice.map_async(wgpu::MapMode::Read, move |result| {
2203 tx.send(result).unwrap();
2204 });
2205 let _ = self.device.poll(wgpu::PollType::Wait {
2206 submission_index: None,
2207 timeout: None,
2208 });
2209 rx.recv().unwrap().expect("buffer map failed");
2210
2211 let mapped = buf_slice.get_mapped_range();
2212 let mut pixels = Vec::with_capacity((w * h * 4) as usize);
2213 for row in 0..h {
2214 let start = (row * bytes_per_row) as usize;
2215 let end = start + (w * 4) as usize;
2216 pixels.extend_from_slice(&mapped[start..end]);
2217 }
2218 drop(mapped);
2219 readback_buf.unmap();
2220
2221 for px in pixels.chunks_exact_mut(4) {
2228 let a = px[3];
2229 if a == 0 || a == 255 {
2230 continue;
2231 }
2232 let a16 = u16::from(a);
2233 px[0] = ((u16::from(px[0]) * 255 + a16 / 2) / a16).min(255) as u8;
2235 px[1] = ((u16::from(px[1]) * 255 + a16 / 2) / a16).min(255) as u8;
2236 px[2] = ((u16::from(px[2]) * 255 + a16 / 2) / a16).min(255) as u8;
2237 }
2238
2239 pixels
2240 }
2241}
2242
2243#[cfg(test)]
2248mod tests {
2249 use super::*;
2250
2251 #[test]
2252 fn vertex_size() {
2253 let size = std::mem::size_of::<Vertex>();
2255 assert!(size > 0, "Vertex should have non-zero size: {size}");
2256 }
2257
2258 #[test]
2264 fn ortho_matrix_maps_origin() {
2265 let m = ortho_matrix(800.0, 600.0);
2266 let x = m[0][0] * 0.0 + m[3][0];
2267 let y = m[1][1] * 0.0 + m[3][1];
2268 assert!((x - (-1.0)).abs() < 1e-6);
2269 assert!((y - 1.0).abs() < 1e-6);
2270 }
2271
2272 #[test]
2273 fn ortho_matrix_maps_bottom_right() {
2274 let m = ortho_matrix(800.0, 600.0);
2275 let x = m[0][0] * 800.0 + m[3][0];
2276 let y = m[1][1] * 600.0 + m[3][1];
2277 assert!((x - 1.0).abs() < 1e-6);
2278 assert!((y - (-1.0)).abs() < 1e-6);
2279 }
2280
2281 #[test]
2282 fn glyph_atlas_shelf_packing() {
2283 let font = truce_font::raster::FontRaster::new();
2284 let mut atlas = GlyphAtlas::new();
2285
2286 atlas.ensure_glyph(&font, 'A', 14.0);
2288 atlas.ensure_glyph(&font, 'B', 14.0);
2289 atlas.ensure_glyph(&font, 'C', 14.0);
2290
2291 assert_eq!(atlas.glyphs.len(), 3);
2292 assert!(!atlas.pending.is_empty());
2293
2294 atlas.ensure_glyph(&font, 'A', 14.0);
2296 assert_eq!(atlas.glyphs.len(), 3);
2297 }
2298
2299 #[test]
2300 fn lyon_fill_circle_produces_triangles() {
2301 let mut builder = Path::builder();
2302 builder.add_circle(
2303 point(50.0, 50.0),
2304 10.0,
2305 lyon_tessellation::path::Winding::Positive,
2306 );
2307 let path = builder.build();
2308 let mut buffers: VertexBuffers<[f32; 2], u32> = VertexBuffers::new();
2309 let mut tess = FillTessellator::new();
2310 tess.tessellate_path(
2311 &path,
2312 &FillOptions::tolerance(0.5),
2313 &mut BuffersBuilder::new(&mut buffers, |v: FillVertex| {
2314 let p = v.position();
2315 [p.x, p.y]
2316 }),
2317 )
2318 .unwrap();
2319 assert!(buffers.vertices.len() >= 3);
2320 assert!(buffers.indices.len() >= 3);
2321 }
2322
2323 #[test]
2328 #[allow(clippy::too_many_lines, clippy::many_single_char_names)]
2329 fn standalone_pipeline_renders() {
2330 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
2331 desc.backends = wgpu::Backends::PRIMARY;
2332 let instance = wgpu::Instance::new(desc);
2333 let Ok(adapter) =
2334 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
2335 power_preference: wgpu::PowerPreference::HighPerformance,
2336 compatible_surface: None,
2337 force_fallback_adapter: false,
2338 }))
2339 else {
2340 return; };
2342 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
2343 label: Some("standalone-test"),
2344 required_features: wgpu::Features::empty(),
2345 required_limits: adapter.limits(),
2346 experimental_features: wgpu::ExperimentalFeatures::default(),
2347 memory_hints: wgpu::MemoryHints::Performance,
2348 trace: wgpu::Trace::Off,
2349 }))
2350 .expect("request_device");
2351 let device = Arc::new(device);
2352 let queue = Arc::new(queue);
2353
2354 let w = 64u32;
2355 let h = 48u32;
2356 let format = wgpu::TextureFormat::Rgba8Unorm;
2357 let mut backend =
2358 WgpuBackend::new(Arc::clone(&device), Arc::clone(&queue), format, w, h, 1.0)
2359 .expect("backend new");
2360
2361 let target = device.create_texture(&wgpu::TextureDescriptor {
2364 label: Some("standalone-target"),
2365 size: wgpu::Extent3d {
2366 width: w,
2367 height: h,
2368 depth_or_array_layers: 1,
2369 },
2370 mip_level_count: 1,
2371 sample_count: 1,
2372 dimension: wgpu::TextureDimension::D2,
2373 format,
2374 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2375 view_formats: &[],
2376 });
2377 let view = target.create_view(&wgpu::TextureViewDescriptor::default());
2378
2379 backend.begin_frame(w, h);
2380 backend.clear(Color::rgb(0.0, 0.0, 0.0));
2381 backend.fill_rect(8.0, 8.0, 16.0, 16.0, Color::rgb(0.0, 1.0, 0.0));
2382 backend.draw_text("x", 20.0, 20.0, 14.0, Color::rgb(1.0, 1.0, 1.0));
2383
2384 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2385 label: Some("standalone-enc"),
2386 });
2387 backend.finish(&mut encoder, &view);
2388
2389 let bytes_per_row = (w * 4 + 255) & !255;
2391 let readback = device.create_buffer(&wgpu::BufferDescriptor {
2392 label: Some("readback"),
2393 size: u64::from(bytes_per_row * h),
2394 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2395 mapped_at_creation: false,
2396 });
2397 encoder.copy_texture_to_buffer(
2398 wgpu::TexelCopyTextureInfo {
2399 texture: &target,
2400 mip_level: 0,
2401 origin: wgpu::Origin3d::ZERO,
2402 aspect: wgpu::TextureAspect::All,
2403 },
2404 wgpu::TexelCopyBufferInfo {
2405 buffer: &readback,
2406 layout: wgpu::TexelCopyBufferLayout {
2407 offset: 0,
2408 bytes_per_row: Some(bytes_per_row),
2409 rows_per_image: None,
2410 },
2411 },
2412 wgpu::Extent3d {
2413 width: w,
2414 height: h,
2415 depth_or_array_layers: 1,
2416 },
2417 );
2418 queue.submit(std::iter::once(encoder.finish()));
2419
2420 let slice = readback.slice(..);
2421 let (tx, rx) = std::sync::mpsc::channel();
2422 slice.map_async(wgpu::MapMode::Read, move |r| {
2423 tx.send(r).unwrap();
2424 });
2425 let _ = device.poll(wgpu::PollType::Wait {
2426 submission_index: None,
2427 timeout: None,
2428 });
2429 rx.recv().unwrap().unwrap();
2430 let mapped = slice.get_mapped_range();
2431
2432 let row_off = 16usize * bytes_per_row as usize;
2434 let px_off = row_off + 16 * 4;
2435 let r = mapped[px_off];
2436 let g = mapped[px_off + 1];
2437 let b = mapped[px_off + 2];
2438 assert!(g > 200, "green rect not rendered: got rgb=({r},{g},{b})");
2439 assert!(
2440 r < 50 && b < 50,
2441 "green rect leaked other channels: rgb=({r},{g},{b})"
2442 );
2443 }
2444}