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