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>>,
280 surface_config: Option<wgpu::SurfaceConfiguration>,
281 pipeline: wgpu::RenderPipeline,
282 target_format: wgpu::TextureFormat,
285 msaa_texture: wgpu::TextureView,
286 msaa_width: u32,
289 msaa_height: u32,
290 vertices: Vec<Vertex>,
291 indices: Vec<u32>,
292 batches: Vec<DrawBatch>,
296 glyph_atlas: GlyphAtlas,
297 font: fontdue::Font,
298 atlas_texture: wgpu::Texture,
299 atlas_bind_group: wgpu::BindGroup,
300 tex_bind_group_layout: wgpu::BindGroupLayout,
303 sampler: wgpu::Sampler,
305 images: Vec<Option<ImageEntry>>,
307 viewport_buffer: wgpu::Buffer,
308 viewport_bind_group: wgpu::BindGroup,
309 clear_color: Option<wgpu::Color>,
315 present_clear_default: wgpu::Color,
321 width: u32,
322 height: u32,
323 scale: f32,
325}
326
327fn ortho_matrix(w: f32, h: f32) -> [[f32; 4]; 4] {
328 [
329 [2.0 / w, 0.0, 0.0, 0.0],
330 [0.0, -2.0 / h, 0.0, 0.0],
331 [0.0, 0.0, 1.0, 0.0],
332 [-1.0, 1.0, 0.0, 1.0],
333 ]
334}
335
336impl WgpuBackend {
337 #[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
349 pub fn from_surface(
350 instance: &wgpu::Instance,
351 surface: wgpu::Surface<'static>,
352 logical_w: u32,
353 logical_h: u32,
354 scale: f32,
355 ) -> Option<Self> {
356 let width = truce_gui_types::to_physical_px(logical_w, f64::from(scale));
357 let height = truce_gui_types::to_physical_px(logical_h, f64::from(scale));
358
359 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
360 power_preference: wgpu::PowerPreference::HighPerformance,
361 compatible_surface: Some(&surface),
362 force_fallback_adapter: false,
363 }))
364 .ok()?;
365
366 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
367 label: Some("truce-gpu"),
368 required_features: wgpu::Features::empty(),
369 required_limits: wgpu::Limits::downlevel_defaults(),
370 experimental_features: wgpu::ExperimentalFeatures::default(),
371 memory_hints: wgpu::MemoryHints::Performance,
372 trace: wgpu::Trace::Off,
373 }))
374 .ok()?;
375 let device = Arc::new(device);
376 let queue = Arc::new(queue);
377
378 let surface_caps = surface.get_capabilities(&adapter);
385 let surface_format = surface_caps
386 .formats
387 .iter()
388 .find(|f| **f == wgpu::TextureFormat::Rgba8Unorm)
389 .or_else(|| surface_caps.formats.iter().find(|f| !f.is_srgb()))
390 .copied()
391 .unwrap_or(surface_caps.formats[0]);
392
393 let surface_config = wgpu::SurfaceConfiguration {
394 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
395 format: surface_format,
396 width,
397 height,
398 present_mode: wgpu::PresentMode::AutoVsync,
399 desired_maximum_frame_latency: 2,
400 alpha_mode: wgpu::CompositeAlphaMode::Auto,
401 view_formats: vec![],
402 };
403 surface.configure(&device, &surface_config);
404
405 let msaa_texture = Self::create_msaa_texture(&device, &surface_config);
407
408 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
410 label: Some("truce-gpu-shader"),
411 source: wgpu::ShaderSource::Wgsl(SHADER_SRC.into()),
412 });
413
414 let matrix = ortho_matrix(width as f32, height as f32);
416 let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
417 label: Some("viewport"),
418 contents: bytemuck::cast_slice(&matrix),
419 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
420 });
421
422 let viewport_bind_group_layout =
423 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
424 label: Some("viewport-layout"),
425 entries: &[wgpu::BindGroupLayoutEntry {
426 binding: 0,
427 visibility: wgpu::ShaderStages::VERTEX,
428 ty: wgpu::BindingType::Buffer {
429 ty: wgpu::BufferBindingType::Uniform,
430 has_dynamic_offset: false,
431 min_binding_size: None,
432 },
433 count: None,
434 }],
435 });
436
437 let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
438 label: Some("viewport-bg"),
439 layout: &viewport_bind_group_layout,
440 entries: &[wgpu::BindGroupEntry {
441 binding: 0,
442 resource: viewport_buffer.as_entire_binding(),
443 }],
444 });
445
446 let atlas_texture = device.create_texture(&wgpu::TextureDescriptor {
448 label: Some("glyph-atlas"),
449 size: wgpu::Extent3d {
450 width: ATLAS_SIZE,
451 height: ATLAS_SIZE,
452 depth_or_array_layers: 1,
453 },
454 mip_level_count: 1,
455 sample_count: 1,
456 dimension: wgpu::TextureDimension::D2,
457 format: wgpu::TextureFormat::R8Unorm,
458 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
459 view_formats: &[],
460 });
461
462 let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
463 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
464 mag_filter: wgpu::FilterMode::Linear,
465 min_filter: wgpu::FilterMode::Linear,
466 ..Default::default()
467 });
468
469 let tex_bind_group_layout =
470 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
471 label: Some("tex-layout"),
472 entries: &[
473 wgpu::BindGroupLayoutEntry {
474 binding: 0,
475 visibility: wgpu::ShaderStages::FRAGMENT,
476 ty: wgpu::BindingType::Texture {
477 sample_type: wgpu::TextureSampleType::Float { filterable: true },
478 view_dimension: wgpu::TextureViewDimension::D2,
479 multisampled: false,
480 },
481 count: None,
482 },
483 wgpu::BindGroupLayoutEntry {
484 binding: 1,
485 visibility: wgpu::ShaderStages::FRAGMENT,
486 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
487 count: None,
488 },
489 ],
490 });
491
492 let atlas_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
493 label: Some("atlas-bg"),
494 layout: &tex_bind_group_layout,
495 entries: &[
496 wgpu::BindGroupEntry {
497 binding: 0,
498 resource: wgpu::BindingResource::TextureView(&atlas_view),
499 },
500 wgpu::BindGroupEntry {
501 binding: 1,
502 resource: wgpu::BindingResource::Sampler(&sampler),
503 },
504 ],
505 });
506
507 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
509 label: Some("truce-gpu-pipeline-layout"),
510 bind_group_layouts: &[
511 Some(&viewport_bind_group_layout),
512 Some(&tex_bind_group_layout),
513 ],
514 immediate_size: 0,
515 });
516
517 let vertex_layout = wgpu::VertexBufferLayout {
518 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
519 step_mode: wgpu::VertexStepMode::Vertex,
520 attributes: &[
521 wgpu::VertexAttribute {
523 offset: 0,
524 shader_location: 0,
525 format: wgpu::VertexFormat::Float32x2,
526 },
527 wgpu::VertexAttribute {
529 offset: 8,
530 shader_location: 1,
531 format: wgpu::VertexFormat::Float32x4,
532 },
533 wgpu::VertexAttribute {
535 offset: 24,
536 shader_location: 2,
537 format: wgpu::VertexFormat::Float32x2,
538 },
539 wgpu::VertexAttribute {
541 offset: 32,
542 shader_location: 3,
543 format: wgpu::VertexFormat::Float32,
544 },
545 ],
546 };
547
548 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
549 label: Some("truce-gpu-pipeline"),
550 layout: Some(&pipeline_layout),
551 vertex: wgpu::VertexState {
552 module: &shader,
553 entry_point: Some("vs_main"),
554 buffers: &[vertex_layout],
555 compilation_options: wgpu::PipelineCompilationOptions::default(),
556 },
557 fragment: Some(wgpu::FragmentState {
558 module: &shader,
559 entry_point: Some("fs_main"),
560 targets: &[Some(wgpu::ColorTargetState {
561 format: surface_format,
562 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
563 write_mask: wgpu::ColorWrites::ALL,
564 })],
565 compilation_options: wgpu::PipelineCompilationOptions::default(),
566 }),
567 primitive: wgpu::PrimitiveState {
568 topology: wgpu::PrimitiveTopology::TriangleList,
569 strip_index_format: None,
570 front_face: wgpu::FrontFace::Ccw,
571 cull_mode: None,
572 unclipped_depth: false,
573 polygon_mode: wgpu::PolygonMode::Fill,
574 conservative: false,
575 },
576 depth_stencil: None,
577 multisample: wgpu::MultisampleState {
578 count: 4,
579 mask: !0,
580 alpha_to_coverage_enabled: false,
581 },
582 multiview_mask: None,
583 cache: None,
584 });
585
586 let font =
588 fontdue::Font::from_bytes(truce_font::JETBRAINS_MONO, fontdue::FontSettings::default())
589 .expect("failed to parse embedded font");
590
591 Some(Self {
592 device,
593 queue,
594 surface: Some(surface),
595 surface_config: Some(surface_config),
596 pipeline,
597 target_format: surface_format,
598 msaa_texture,
599 msaa_width: width,
600 msaa_height: height,
601 vertices: Vec::with_capacity(4096),
602 indices: Vec::with_capacity(8192),
603 batches: Vec::new(),
604 glyph_atlas: GlyphAtlas::new(),
605 font,
606 atlas_texture,
607 atlas_bind_group,
608 tex_bind_group_layout,
609 sampler,
610 images: Vec::new(),
611 viewport_buffer,
612 viewport_bind_group,
613 clear_color: None,
614 present_clear_default: wgpu::Color::BLACK,
615 width,
616 height,
617 scale,
618 })
619 }
620
621 #[cfg(target_os = "macos")]
631 pub unsafe fn from_metal_layer(
632 metal_layer: *mut c_void,
633 logical_w: u32,
634 logical_h: u32,
635 scale: f32,
636 ) -> Option<Self> {
637 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
638 desc.backends = wgpu::Backends::METAL;
639 let instance = wgpu::Instance::new(desc);
640
641 let surface = unsafe {
642 instance
643 .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(metal_layer))
644 }
645 .ok()?;
646
647 Self::from_surface(&instance, surface, logical_w, logical_h, scale)
648 }
649
650 #[cfg(not(target_os = "ios"))]
658 #[must_use]
659 pub unsafe fn from_window(
660 window: &baseview::Window,
661 logical_w: u32,
662 logical_h: u32,
663 scale: f32,
664 ) -> Option<Self> {
665 unsafe {
666 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
667 desc.backends = wgpu::Backends::PRIMARY;
668 let instance = wgpu::Instance::new(desc);
669
670 let surface = crate::platform::create_wgpu_surface(&instance, window)?;
671 Self::from_surface(&instance, surface, logical_w, logical_h, scale)
672 }
673 }
674
675 #[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
717 #[must_use]
718 pub fn new(
719 device: Arc<wgpu::Device>,
720 queue: Arc<wgpu::Queue>,
721 target_format: wgpu::TextureFormat,
722 max_logical_w: u32,
723 max_logical_h: u32,
724 scale: f32,
725 ) -> Option<Self> {
726 let scale = scale.max(0.0);
727 let width = truce_gui_types::to_physical_px(max_logical_w, f64::from(scale));
728 let height = truce_gui_types::to_physical_px(max_logical_h, f64::from(scale));
729
730 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
732 label: Some("truce-gpu-shader"),
733 source: wgpu::ShaderSource::Wgsl(SHADER_SRC.into()),
734 });
735
736 let matrix = ortho_matrix(width as f32, height as f32);
738 let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
739 label: Some("viewport"),
740 contents: bytemuck::cast_slice(&matrix),
741 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
742 });
743
744 let viewport_bind_group_layout =
745 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
746 label: Some("viewport-layout"),
747 entries: &[wgpu::BindGroupLayoutEntry {
748 binding: 0,
749 visibility: wgpu::ShaderStages::VERTEX,
750 ty: wgpu::BindingType::Buffer {
751 ty: wgpu::BufferBindingType::Uniform,
752 has_dynamic_offset: false,
753 min_binding_size: None,
754 },
755 count: None,
756 }],
757 });
758
759 let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
760 label: Some("viewport-bg"),
761 layout: &viewport_bind_group_layout,
762 entries: &[wgpu::BindGroupEntry {
763 binding: 0,
764 resource: viewport_buffer.as_entire_binding(),
765 }],
766 });
767
768 let atlas_texture = device.create_texture(&wgpu::TextureDescriptor {
770 label: Some("glyph-atlas"),
771 size: wgpu::Extent3d {
772 width: ATLAS_SIZE,
773 height: ATLAS_SIZE,
774 depth_or_array_layers: 1,
775 },
776 mip_level_count: 1,
777 sample_count: 1,
778 dimension: wgpu::TextureDimension::D2,
779 format: wgpu::TextureFormat::R8Unorm,
780 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
781 view_formats: &[],
782 });
783 let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
784 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
785 mag_filter: wgpu::FilterMode::Linear,
786 min_filter: wgpu::FilterMode::Linear,
787 ..Default::default()
788 });
789 let tex_bind_group_layout =
790 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
791 label: Some("tex-layout"),
792 entries: &[
793 wgpu::BindGroupLayoutEntry {
794 binding: 0,
795 visibility: wgpu::ShaderStages::FRAGMENT,
796 ty: wgpu::BindingType::Texture {
797 sample_type: wgpu::TextureSampleType::Float { filterable: true },
798 view_dimension: wgpu::TextureViewDimension::D2,
799 multisampled: false,
800 },
801 count: None,
802 },
803 wgpu::BindGroupLayoutEntry {
804 binding: 1,
805 visibility: wgpu::ShaderStages::FRAGMENT,
806 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
807 count: None,
808 },
809 ],
810 });
811 let atlas_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
812 label: Some("atlas-bg"),
813 layout: &tex_bind_group_layout,
814 entries: &[
815 wgpu::BindGroupEntry {
816 binding: 0,
817 resource: wgpu::BindingResource::TextureView(&atlas_view),
818 },
819 wgpu::BindGroupEntry {
820 binding: 1,
821 resource: wgpu::BindingResource::Sampler(&sampler),
822 },
823 ],
824 });
825
826 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
828 label: Some("truce-gpu-pipeline-layout"),
829 bind_group_layouts: &[
830 Some(&viewport_bind_group_layout),
831 Some(&tex_bind_group_layout),
832 ],
833 immediate_size: 0,
834 });
835
836 let vertex_layout = wgpu::VertexBufferLayout {
837 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
838 step_mode: wgpu::VertexStepMode::Vertex,
839 attributes: &[
840 wgpu::VertexAttribute {
841 offset: 0,
842 shader_location: 0,
843 format: wgpu::VertexFormat::Float32x2,
844 },
845 wgpu::VertexAttribute {
846 offset: 8,
847 shader_location: 1,
848 format: wgpu::VertexFormat::Float32x4,
849 },
850 wgpu::VertexAttribute {
851 offset: 24,
852 shader_location: 2,
853 format: wgpu::VertexFormat::Float32x2,
854 },
855 wgpu::VertexAttribute {
856 offset: 32,
857 shader_location: 3,
858 format: wgpu::VertexFormat::Float32,
859 },
860 ],
861 };
862
863 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
864 label: Some("truce-gpu-pipeline"),
865 layout: Some(&pipeline_layout),
866 vertex: wgpu::VertexState {
867 module: &shader,
868 entry_point: Some("vs_main"),
869 buffers: &[vertex_layout],
870 compilation_options: wgpu::PipelineCompilationOptions::default(),
871 },
872 fragment: Some(wgpu::FragmentState {
873 module: &shader,
874 entry_point: Some("fs_main"),
875 targets: &[Some(wgpu::ColorTargetState {
876 format: target_format,
877 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
878 write_mask: wgpu::ColorWrites::ALL,
879 })],
880 compilation_options: wgpu::PipelineCompilationOptions::default(),
881 }),
882 primitive: wgpu::PrimitiveState {
883 topology: wgpu::PrimitiveTopology::TriangleList,
884 ..Default::default()
885 },
886 depth_stencil: None,
887 multisample: wgpu::MultisampleState {
888 count: 4,
889 mask: !0,
890 alpha_to_coverage_enabled: false,
891 },
892 multiview_mask: None,
893 cache: None,
894 });
895
896 let msaa_texture = Self::create_msaa_view(&device, target_format, width, height);
898
899 let font =
900 fontdue::Font::from_bytes(truce_font::JETBRAINS_MONO, fontdue::FontSettings::default())
901 .expect("failed to parse embedded font");
902
903 Some(Self {
904 device,
905 queue,
906 surface: None,
907 surface_config: None,
908 pipeline,
909 target_format,
910 msaa_texture,
911 msaa_width: width,
912 msaa_height: height,
913 vertices: Vec::with_capacity(4096),
914 indices: Vec::with_capacity(8192),
915 batches: Vec::new(),
916 glyph_atlas: GlyphAtlas::new(),
917 font,
918 atlas_texture,
919 atlas_bind_group,
920 tex_bind_group_layout,
921 sampler,
922 images: Vec::new(),
923 viewport_buffer,
924 viewport_bind_group,
925 clear_color: None,
926 present_clear_default: wgpu::Color::TRANSPARENT,
927 width,
928 height,
929 scale,
930 })
931 }
932
933 #[allow(clippy::cast_precision_loss)]
944 pub fn begin_frame(&mut self, logical_w: u32, logical_h: u32) {
945 let phys_w = truce_gui_types::to_physical_px(logical_w, f64::from(self.scale));
946 let phys_h = truce_gui_types::to_physical_px(logical_h, f64::from(self.scale));
947 self.vertices.clear();
948 self.indices.clear();
949 self.batches.clear();
950 self.clear_color = None;
951
952 if phys_w != self.width || phys_h != self.height {
953 self.width = phys_w;
954 self.height = phys_h;
955 let matrix = ortho_matrix(phys_w as f32, phys_h as f32);
956 self.queue
957 .write_buffer(&self.viewport_buffer, 0, bytemuck::cast_slice(&matrix));
958 }
959
960 if phys_w != self.msaa_width || phys_h != self.msaa_height {
961 self.msaa_texture =
962 Self::create_msaa_view(&self.device, self.target_format, phys_w, phys_h);
963 self.msaa_width = phys_w;
964 self.msaa_height = phys_h;
965 }
966 }
967
968 pub fn scale(&self) -> f32 {
973 self.scale
974 }
975
976 pub fn set_scale(&mut self, scale: f32) {
984 if scale.is_finite() && scale > 0.0 {
985 self.scale = scale;
986 }
987 }
988
989 pub fn finish(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
998 self.flush_atlas();
999
1000 if self.indices.is_empty() {
1001 self.clear_color = None;
1002 return;
1003 }
1004
1005 let vertex_buffer = self
1006 .device
1007 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1008 label: Some("vertices"),
1009 contents: bytemuck::cast_slice(&self.vertices),
1010 usage: wgpu::BufferUsages::VERTEX,
1011 });
1012
1013 let index_buffer = self
1014 .device
1015 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1016 label: Some("indices"),
1017 contents: bytemuck::cast_slice(&self.indices),
1018 usage: wgpu::BufferUsages::INDEX,
1019 });
1020
1021 let (load, store) = match self.clear_color {
1027 Some(c) => (wgpu::LoadOp::Clear(c), wgpu::StoreOp::Discard),
1028 None => (wgpu::LoadOp::Load, wgpu::StoreOp::Store),
1029 };
1030
1031 {
1032 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1033 label: Some("truce-gpu-frame"),
1034 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1035 view: &self.msaa_texture,
1036 resolve_target: Some(view),
1037 ops: wgpu::Operations { load, store },
1038 depth_slice: None,
1039 })],
1040 depth_stencil_attachment: None,
1041 timestamp_writes: None,
1042 occlusion_query_set: None,
1043 multiview_mask: None,
1044 });
1045
1046 pass.set_pipeline(&self.pipeline);
1047 pass.set_bind_group(0, &self.viewport_bind_group, &[]);
1048 pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1049 pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1050
1051 let total_indices = len_u32(self.indices.len());
1052 if self.batches.is_empty() {
1053 pass.set_bind_group(1, &self.atlas_bind_group, &[]);
1054 pass.draw_indexed(0..total_indices, 0, 0..1);
1055 } else {
1056 for i in 0..self.batches.len() {
1057 let b = self.batches[i];
1058 let end = self
1059 .batches
1060 .get(i + 1)
1061 .map_or(total_indices, |n| n.index_start);
1062 if end <= b.index_start {
1063 continue;
1064 }
1065 let bg = match b.image {
1066 None => &self.atlas_bind_group,
1067 Some(img_id) => {
1068 match self.images.get(img_id.0 as usize).and_then(|s| s.as_ref()) {
1069 Some(entry) => &entry.bind_group,
1070 None => continue,
1071 }
1072 }
1073 };
1074 pass.set_bind_group(1, bg, &[]);
1075 pass.draw_indexed(b.index_start..end, 0, 0..1);
1076 }
1077 }
1078 }
1079
1080 self.clear_color = None;
1081 }
1082
1083 fn create_msaa_view(
1084 device: &wgpu::Device,
1085 format: wgpu::TextureFormat,
1086 width: u32,
1087 height: u32,
1088 ) -> wgpu::TextureView {
1089 let tex = device.create_texture(&wgpu::TextureDescriptor {
1090 label: Some("msaa"),
1091 size: wgpu::Extent3d {
1092 width,
1093 height,
1094 depth_or_array_layers: 1,
1095 },
1096 mip_level_count: 1,
1097 sample_count: 4,
1098 dimension: wgpu::TextureDimension::D2,
1099 format,
1100 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1101 view_formats: &[],
1102 });
1103 tex.create_view(&wgpu::TextureViewDescriptor::default())
1104 }
1105
1106 fn create_msaa_texture(
1107 device: &wgpu::Device,
1108 config: &wgpu::SurfaceConfiguration,
1109 ) -> wgpu::TextureView {
1110 let tex = device.create_texture(&wgpu::TextureDescriptor {
1111 label: Some("msaa"),
1112 size: wgpu::Extent3d {
1113 width: config.width,
1114 height: config.height,
1115 depth_or_array_layers: 1,
1116 },
1117 mip_level_count: 1,
1118 sample_count: 4,
1119 dimension: wgpu::TextureDimension::D2,
1120 format: config.format,
1121 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1122 view_formats: &[],
1123 });
1124 tex.create_view(&wgpu::TextureViewDescriptor::default())
1125 }
1126
1127 #[allow(clippy::cast_precision_loss)]
1133 pub fn resize(&mut self, logical_w: u32, logical_h: u32) -> bool {
1134 let new_w = truce_gui_types::to_physical_px(logical_w, f64::from(self.scale));
1135 let new_h = truce_gui_types::to_physical_px(logical_h, f64::from(self.scale));
1136 if new_w == self.width && new_h == self.height {
1137 return false;
1138 }
1139 self.width = new_w;
1140 self.height = new_h;
1141
1142 if let Some(ref surface) = self.surface
1143 && let Some(ref mut config) = self.surface_config
1144 {
1145 config.width = new_w;
1146 config.height = new_h;
1147 surface.configure(&self.device, config);
1148 self.msaa_texture = Self::create_msaa_texture(&self.device, config);
1149 }
1150
1151 let matrix = ortho_matrix(new_w as f32, new_h as f32);
1153 self.queue
1154 .write_buffer(&self.viewport_buffer, 0, bytemuck::cast_slice(&matrix));
1155
1156 true
1157 }
1158
1159 fn color_arr(c: Color) -> [f32; 4] {
1162 [c.r, c.g, c.b, c.a]
1163 }
1164
1165 fn ensure_batch(&mut self, image: Option<ImageId>) {
1168 let needs_new = self.batches.last().is_none_or(|last| last.image != image);
1169 if needs_new {
1170 self.batches.push(DrawBatch {
1171 index_start: len_u32(self.indices.len()),
1172 image,
1173 });
1174 }
1175 }
1176
1177 fn push_quad(&mut self, v0: Vertex, v1: Vertex, v2: Vertex, v3: Vertex) {
1178 self.ensure_batch(None);
1179 let base = len_u32(self.vertices.len());
1180 self.vertices.extend_from_slice(&[v0, v1, v2, v3]);
1181 self.indices
1182 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
1183 }
1184
1185 fn fill_path(&mut self, path: &Path, color: [f32; 4]) {
1187 self.ensure_batch(None);
1188 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
1189 let mut tessellator = FillTessellator::new();
1190 let _ = tessellator.tessellate_path(
1191 path,
1192 &FillOptions::tolerance(0.5),
1193 &mut BuffersBuilder::new(&mut buffers, |vertex: FillVertex| {
1194 let p = vertex.position();
1195 Vertex::solid(p.x, p.y, color)
1196 }),
1197 );
1198 let base = len_u32(self.vertices.len());
1199 self.vertices.extend_from_slice(&buffers.vertices);
1200 self.indices
1201 .extend(buffers.indices.iter().map(|i| i + base));
1202 }
1203
1204 fn stroke_path(&mut self, path: &Path, color: [f32; 4], opts: &StrokeOptions) {
1206 self.ensure_batch(None);
1207 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
1208 let mut tessellator = StrokeTessellator::new();
1209 let _ = tessellator.tessellate_path(
1210 path,
1211 opts,
1212 &mut BuffersBuilder::new(&mut buffers, |vertex: StrokeVertex| {
1213 let p = vertex.position();
1214 Vertex::solid(p.x, p.y, color)
1215 }),
1216 );
1217 let base = len_u32(self.vertices.len());
1218 self.vertices.extend_from_slice(&buffers.vertices);
1219 self.indices
1220 .extend(buffers.indices.iter().map(|i| i + base));
1221 }
1222
1223 fn flush_atlas(&mut self) {
1225 for (x, y, w, h, data) in self.glyph_atlas.pending.drain(..) {
1226 if w == 0 || h == 0 {
1227 continue;
1228 }
1229 self.queue.write_texture(
1230 wgpu::TexelCopyTextureInfo {
1231 texture: &self.atlas_texture,
1232 mip_level: 0,
1233 origin: wgpu::Origin3d { x, y, z: 0 },
1234 aspect: wgpu::TextureAspect::All,
1235 },
1236 &data,
1237 wgpu::TexelCopyBufferLayout {
1238 offset: 0,
1239 bytes_per_row: Some(w),
1240 rows_per_image: Some(h),
1241 },
1242 wgpu::Extent3d {
1243 width: w,
1244 height: h,
1245 depth_or_array_layers: 1,
1246 },
1247 );
1248 }
1249 }
1250}
1251
1252#[allow(clippy::many_single_char_names)]
1262impl RenderBackend for WgpuBackend {
1263 fn clear(&mut self, color: Color) {
1264 self.clear_color = Some(wgpu::Color {
1265 r: f64::from(color.r),
1266 g: f64::from(color.g),
1267 b: f64::from(color.b),
1268 a: f64::from(color.a),
1269 });
1270 self.vertices.clear();
1271 self.indices.clear();
1272 self.batches.clear();
1273 if self.glyph_atlas.overflow_pending {
1278 self.glyph_atlas.clear();
1279 }
1280 }
1281
1282 fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Color) {
1283 let s = self.scale;
1284 let c = Self::color_arr(color);
1285 self.push_quad(
1286 Vertex::solid(x * s, y * s, c),
1287 Vertex::solid((x + w) * s, y * s, c),
1288 Vertex::solid((x + w) * s, (y + h) * s, c),
1289 Vertex::solid(x * s, (y + h) * s, c),
1290 );
1291 }
1292
1293 fn fill_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color) {
1294 let s = self.scale;
1295 let c = Self::color_arr(color);
1296 let mut builder = Path::builder();
1297 builder.add_circle(
1298 point(cx * s, cy * s),
1299 radius * s,
1300 lyon_tessellation::path::Winding::Positive,
1301 );
1302 let path = builder.build();
1303 self.fill_path(&path, c);
1304 }
1305
1306 fn stroke_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color, width: f32) {
1307 let s = self.scale;
1308 let c = Self::color_arr(color);
1309 let mut builder = Path::builder();
1310 builder.add_circle(
1311 point(cx * s, cy * s),
1312 radius * s,
1313 lyon_tessellation::path::Winding::Positive,
1314 );
1315 let path = builder.build();
1316 let opts = StrokeOptions::tolerance(0.5).with_line_width(width * s);
1317 self.stroke_path(&path, c, &opts);
1318 }
1319
1320 #[allow(clippy::cast_precision_loss)]
1321 fn stroke_arc(
1322 &mut self,
1323 cx: f32,
1324 cy: f32,
1325 radius: f32,
1326 start_angle: f32,
1327 end_angle: f32,
1328 color: Color,
1329 width: f32,
1330 ) {
1331 let s = self.scale;
1332 let c = Self::color_arr(color);
1333 let segments = 64u32;
1334 let sweep = end_angle - start_angle;
1335 let step = sweep / segments as f32;
1336
1337 let mut builder = Path::builder();
1338 builder.begin(point(
1339 cx * s + radius * s * start_angle.cos(),
1340 cy * s + radius * s * start_angle.sin(),
1341 ));
1342 for i in 1..=segments {
1343 let angle = start_angle + step * i as f32;
1344 builder.line_to(point(
1345 cx * s + radius * s * angle.cos(),
1346 cy * s + radius * s * angle.sin(),
1347 ));
1348 }
1349 builder.end(false);
1350 let path = builder.build();
1351
1352 let opts = StrokeOptions::tolerance(0.5)
1353 .with_line_width(width * s)
1354 .with_line_cap(lyon_tessellation::LineCap::Round);
1355 self.stroke_path(&path, c, &opts);
1356 }
1357
1358 fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color, width: f32) {
1359 let s = self.scale;
1360 let c = Self::color_arr(color);
1361 let mut builder = Path::builder();
1362 builder.begin(point(x1 * s, y1 * s));
1363 builder.line_to(point(x2 * s, y2 * s));
1364 builder.end(false);
1365 let path = builder.build();
1366
1367 let opts = StrokeOptions::tolerance(0.5)
1368 .with_line_width(width * s)
1369 .with_line_cap(lyon_tessellation::LineCap::Round);
1370 self.stroke_path(&path, c, &opts);
1371 }
1372
1373 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1376 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: Color) {
1377 let s = self.scale;
1378 let phys_size = size * s;
1379 let c = Self::color_arr(color);
1380 let line_metrics = self.font.horizontal_line_metrics(phys_size);
1381 let ascent = line_metrics.map_or(phys_size * 0.8, |m| m.ascent);
1382
1383 let mut cursor_x = x * s;
1384
1385 let chars: Vec<char> = text.chars().collect();
1386 for &ch in &chars {
1387 self.glyph_atlas.ensure_glyph(&self.font, ch, phys_size);
1388 }
1389
1390 for &ch in &chars {
1396 let key = (ch, (phys_size * 10.0) as u32);
1397 let Some(g) = self.glyph_atlas.glyphs.get(&key) else {
1398 continue;
1399 };
1400 let (u0, v0, u1, v1, gw, gh, y_off, advance) = (
1401 g.u0, g.v0, g.u1, g.v1, g.width, g.height, g.y_offset, g.advance,
1402 );
1403 let gx = cursor_x.round();
1413 let gy = (y * s + ascent - y_off - gh).round();
1414
1415 self.push_quad(
1416 Vertex::glyph(gx, gy, c, u0, v0),
1417 Vertex::glyph(gx + gw, gy, c, u1, v0),
1418 Vertex::glyph(gx + gw, gy + gh, c, u1, v1),
1419 Vertex::glyph(gx, gy + gh, c, u0, v1),
1420 );
1421
1422 cursor_x += advance;
1423 }
1424 }
1425
1426 fn text_width(&self, text: &str, size: f32) -> f32 {
1427 let phys_size = size * self.scale;
1428 let phys: f32 = text
1433 .chars()
1434 .map(|ch| self.font.metrics(ch, phys_size).advance_width)
1435 .sum();
1436 phys / self.scale
1437 }
1438
1439 fn register_image(&mut self, rgba: &[u8], width: u32, height: u32) -> ImageId {
1440 let expected = (width as usize) * (height as usize) * 4;
1441 if width == 0 || height == 0 || rgba.len() < expected {
1442 return ImageId::INVALID;
1443 }
1444
1445 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1446 label: Some("image"),
1447 size: wgpu::Extent3d {
1448 width,
1449 height,
1450 depth_or_array_layers: 1,
1451 },
1452 mip_level_count: 1,
1453 sample_count: 1,
1454 dimension: wgpu::TextureDimension::D2,
1455 format: wgpu::TextureFormat::Rgba8Unorm,
1456 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1457 view_formats: &[],
1458 });
1459
1460 self.queue.write_texture(
1461 wgpu::TexelCopyTextureInfo {
1462 texture: &texture,
1463 mip_level: 0,
1464 origin: wgpu::Origin3d::ZERO,
1465 aspect: wgpu::TextureAspect::All,
1466 },
1467 &rgba[..expected],
1468 wgpu::TexelCopyBufferLayout {
1469 offset: 0,
1470 bytes_per_row: Some(width * 4),
1471 rows_per_image: Some(height),
1472 },
1473 wgpu::Extent3d {
1474 width,
1475 height,
1476 depth_or_array_layers: 1,
1477 },
1478 );
1479
1480 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1481 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1482 label: Some("image-bg"),
1483 layout: &self.tex_bind_group_layout,
1484 entries: &[
1485 wgpu::BindGroupEntry {
1486 binding: 0,
1487 resource: wgpu::BindingResource::TextureView(&view),
1488 },
1489 wgpu::BindGroupEntry {
1490 binding: 1,
1491 resource: wgpu::BindingResource::Sampler(&self.sampler),
1492 },
1493 ],
1494 });
1495
1496 let entry = ImageEntry {
1497 _texture: texture,
1498 bind_group,
1499 };
1500
1501 if let Some((idx, slot)) = self
1502 .images
1503 .iter_mut()
1504 .enumerate()
1505 .find(|(_, s)| s.is_none())
1506 {
1507 *slot = Some(entry);
1508 return ImageId(len_u32(idx));
1509 }
1510 let id = len_u32(self.images.len());
1511 self.images.push(Some(entry));
1512 ImageId(id)
1513 }
1514
1515 fn unregister_image(&mut self, id: ImageId) {
1516 if let Some(slot) = self.images.get_mut(id.0 as usize) {
1517 *slot = None;
1518 }
1519 }
1520
1521 fn draw_image(&mut self, id: ImageId, x: f32, y: f32, w: f32, h: f32) {
1522 if self
1523 .images
1524 .get(id.0 as usize)
1525 .and_then(|s| s.as_ref())
1526 .is_none()
1527 {
1528 return;
1529 }
1530 self.ensure_batch(Some(id));
1531
1532 let s = self.scale;
1533 let c = [1.0, 1.0, 1.0, 1.0];
1534 let base = len_u32(self.vertices.len());
1535 self.vertices.extend_from_slice(&[
1536 Vertex::image(x * s, y * s, c, 0.0, 0.0),
1537 Vertex::image((x + w) * s, y * s, c, 1.0, 0.0),
1538 Vertex::image((x + w) * s, (y + h) * s, c, 1.0, 1.0),
1539 Vertex::image(x * s, (y + h) * s, c, 0.0, 1.0),
1540 ]);
1541 self.indices
1542 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
1543 }
1544
1545 fn present(&mut self) {
1546 self.flush_atlas();
1548
1549 let Some(surface) = &self.surface else {
1550 return; };
1552
1553 let (wgpu::CurrentSurfaceTexture::Success(frame)
1554 | wgpu::CurrentSurfaceTexture::Suboptimal(frame)) = surface.get_current_texture()
1555 else {
1556 return;
1557 };
1558 let frame_view = frame
1559 .texture
1560 .create_view(&wgpu::TextureViewDescriptor::default());
1561
1562 if self.vertices.is_empty() {
1563 self.clear_only_pass(&frame_view);
1569 frame.present();
1570 return;
1571 }
1572
1573 self.render_pass(&frame_view);
1574 frame.present();
1575 }
1576}
1577
1578impl WgpuBackend {
1579 fn clear_only_pass(&mut self, resolve_target: &wgpu::TextureView) {
1585 let clear_color = self.clear_color.unwrap_or(self.present_clear_default);
1586 let mut encoder = self
1587 .device
1588 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1589 label: Some("clear-only"),
1590 });
1591 {
1592 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1593 label: Some("clear-only"),
1594 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1595 view: &self.msaa_texture,
1596 resolve_target: Some(resolve_target),
1597 ops: wgpu::Operations {
1598 load: wgpu::LoadOp::Clear(clear_color),
1599 store: wgpu::StoreOp::Discard,
1600 },
1601 depth_slice: None,
1602 })],
1603 depth_stencil_attachment: None,
1604 timestamp_writes: None,
1605 occlusion_query_set: None,
1606 multiview_mask: None,
1607 });
1608 }
1609 self.queue.submit(std::iter::once(encoder.finish()));
1610 }
1611
1612 fn render_pass(&mut self, resolve_target: &wgpu::TextureView) {
1614 let clear_color = self.clear_color.unwrap_or(self.present_clear_default);
1615 let vertex_buffer = self
1616 .device
1617 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1618 label: Some("vertices"),
1619 contents: bytemuck::cast_slice(&self.vertices),
1620 usage: wgpu::BufferUsages::VERTEX,
1621 });
1622
1623 let index_buffer = self
1624 .device
1625 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1626 label: Some("indices"),
1627 contents: bytemuck::cast_slice(&self.indices),
1628 usage: wgpu::BufferUsages::INDEX,
1629 });
1630
1631 let mut encoder = self
1632 .device
1633 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1634 label: Some("frame"),
1635 });
1636
1637 {
1638 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1639 label: Some("main"),
1640 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1641 view: &self.msaa_texture,
1642 resolve_target: Some(resolve_target),
1643 ops: wgpu::Operations {
1644 load: wgpu::LoadOp::Clear(clear_color),
1645 store: wgpu::StoreOp::Discard,
1646 },
1647 depth_slice: None,
1648 })],
1649 depth_stencil_attachment: None,
1650 timestamp_writes: None,
1651 occlusion_query_set: None,
1652 multiview_mask: None,
1653 });
1654
1655 pass.set_pipeline(&self.pipeline);
1656 pass.set_bind_group(0, &self.viewport_bind_group, &[]);
1657 pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1658 pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1659
1660 let total_indices = len_u32(self.indices.len());
1661 if self.batches.is_empty() {
1662 pass.set_bind_group(1, &self.atlas_bind_group, &[]);
1666 pass.draw_indexed(0..total_indices, 0, 0..1);
1667 } else {
1668 for i in 0..self.batches.len() {
1669 let b = self.batches[i];
1670 let end = self
1671 .batches
1672 .get(i + 1)
1673 .map_or(total_indices, |n| n.index_start);
1674 if end <= b.index_start {
1675 continue;
1676 }
1677 let bg = match b.image {
1678 None => &self.atlas_bind_group,
1679 Some(img_id) => {
1680 match self.images.get(img_id.0 as usize).and_then(|s| s.as_ref()) {
1681 Some(entry) => &entry.bind_group,
1682 None => continue,
1684 }
1685 }
1686 };
1687 pass.set_bind_group(1, bg, &[]);
1688 pass.draw_indexed(b.index_start..end, 0, 0..1);
1689 }
1690 }
1691 }
1692
1693 self.queue.submit(std::iter::once(encoder.finish()));
1694 }
1695
1696 #[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
1705 #[must_use]
1706 pub fn headless(width: u32, height: u32, scale: f32) -> Option<Self> {
1707 let phys_w = truce_gui_types::to_physical_px(width, f64::from(scale));
1708 let phys_h = truce_gui_types::to_physical_px(height, f64::from(scale));
1709
1710 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
1711 desc.backends = wgpu::Backends::PRIMARY;
1712 let instance = wgpu::Instance::new(desc);
1713
1714 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1724 power_preference: wgpu::PowerPreference::HighPerformance,
1725 compatible_surface: None,
1726 force_fallback_adapter: false,
1727 }))
1728 .ok()?;
1729
1730 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1731 label: Some("truce-gpu-headless"),
1732 required_features: wgpu::Features::empty(),
1733 required_limits: wgpu::Limits::downlevel_defaults(),
1734 experimental_features: wgpu::ExperimentalFeatures::default(),
1735 memory_hints: wgpu::MemoryHints::Performance,
1736 trace: wgpu::Trace::Off,
1737 }))
1738 .ok()?;
1739 let device = Arc::new(device);
1740 let queue = Arc::new(queue);
1741
1742 let texture_format = wgpu::TextureFormat::Rgba8Unorm;
1744
1745 let msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
1747 label: Some("msaa"),
1748 size: wgpu::Extent3d {
1749 width: phys_w,
1750 height: phys_h,
1751 depth_or_array_layers: 1,
1752 },
1753 mip_level_count: 1,
1754 sample_count: 4,
1755 dimension: wgpu::TextureDimension::D2,
1756 format: texture_format,
1757 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1758 view_formats: &[],
1759 });
1760 let msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
1761
1762 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1764 label: Some("truce-gpu-shader"),
1765 source: wgpu::ShaderSource::Wgsl(SHADER_SRC.into()),
1766 });
1767
1768 let matrix = ortho_matrix(phys_w as f32, phys_h as f32);
1770 let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1771 label: Some("viewport"),
1772 contents: bytemuck::cast_slice(&matrix),
1773 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1774 });
1775
1776 let viewport_bind_group_layout =
1777 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1778 label: Some("viewport-layout"),
1779 entries: &[wgpu::BindGroupLayoutEntry {
1780 binding: 0,
1781 visibility: wgpu::ShaderStages::VERTEX,
1782 ty: wgpu::BindingType::Buffer {
1783 ty: wgpu::BufferBindingType::Uniform,
1784 has_dynamic_offset: false,
1785 min_binding_size: None,
1786 },
1787 count: None,
1788 }],
1789 });
1790
1791 let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1792 label: Some("viewport-bg"),
1793 layout: &viewport_bind_group_layout,
1794 entries: &[wgpu::BindGroupEntry {
1795 binding: 0,
1796 resource: viewport_buffer.as_entire_binding(),
1797 }],
1798 });
1799
1800 let atlas_texture = device.create_texture(&wgpu::TextureDescriptor {
1802 label: Some("glyph-atlas"),
1803 size: wgpu::Extent3d {
1804 width: ATLAS_SIZE,
1805 height: ATLAS_SIZE,
1806 depth_or_array_layers: 1,
1807 },
1808 mip_level_count: 1,
1809 sample_count: 1,
1810 dimension: wgpu::TextureDimension::D2,
1811 format: wgpu::TextureFormat::R8Unorm,
1812 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1813 view_formats: &[],
1814 });
1815 let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
1816 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1817 mag_filter: wgpu::FilterMode::Linear,
1818 min_filter: wgpu::FilterMode::Linear,
1819 ..Default::default()
1820 });
1821 let tex_bind_group_layout =
1822 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1823 label: Some("tex-layout"),
1824 entries: &[
1825 wgpu::BindGroupLayoutEntry {
1826 binding: 0,
1827 visibility: wgpu::ShaderStages::FRAGMENT,
1828 ty: wgpu::BindingType::Texture {
1829 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1830 view_dimension: wgpu::TextureViewDimension::D2,
1831 multisampled: false,
1832 },
1833 count: None,
1834 },
1835 wgpu::BindGroupLayoutEntry {
1836 binding: 1,
1837 visibility: wgpu::ShaderStages::FRAGMENT,
1838 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1839 count: None,
1840 },
1841 ],
1842 });
1843 let atlas_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1844 label: Some("atlas-bg"),
1845 layout: &tex_bind_group_layout,
1846 entries: &[
1847 wgpu::BindGroupEntry {
1848 binding: 0,
1849 resource: wgpu::BindingResource::TextureView(&atlas_view),
1850 },
1851 wgpu::BindGroupEntry {
1852 binding: 1,
1853 resource: wgpu::BindingResource::Sampler(&sampler),
1854 },
1855 ],
1856 });
1857
1858 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1860 label: Some("truce-gpu-pipeline-layout"),
1861 bind_group_layouts: &[
1862 Some(&viewport_bind_group_layout),
1863 Some(&tex_bind_group_layout),
1864 ],
1865 immediate_size: 0,
1866 });
1867
1868 let vertex_layout = wgpu::VertexBufferLayout {
1869 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
1870 step_mode: wgpu::VertexStepMode::Vertex,
1871 attributes: &[
1872 wgpu::VertexAttribute {
1873 offset: 0,
1874 shader_location: 0,
1875 format: wgpu::VertexFormat::Float32x2,
1876 },
1877 wgpu::VertexAttribute {
1878 offset: 8,
1879 shader_location: 1,
1880 format: wgpu::VertexFormat::Float32x4,
1881 },
1882 wgpu::VertexAttribute {
1883 offset: 24,
1884 shader_location: 2,
1885 format: wgpu::VertexFormat::Float32x2,
1886 },
1887 wgpu::VertexAttribute {
1888 offset: 32,
1889 shader_location: 3,
1890 format: wgpu::VertexFormat::Float32,
1891 },
1892 ],
1893 };
1894
1895 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1896 label: Some("truce-gpu-pipeline"),
1897 layout: Some(&pipeline_layout),
1898 vertex: wgpu::VertexState {
1899 module: &shader,
1900 entry_point: Some("vs_main"),
1901 buffers: &[vertex_layout],
1902 compilation_options: wgpu::PipelineCompilationOptions::default(),
1903 },
1904 fragment: Some(wgpu::FragmentState {
1905 module: &shader,
1906 entry_point: Some("fs_main"),
1907 targets: &[Some(wgpu::ColorTargetState {
1908 format: texture_format,
1909 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1910 write_mask: wgpu::ColorWrites::ALL,
1911 })],
1912 compilation_options: wgpu::PipelineCompilationOptions::default(),
1913 }),
1914 primitive: wgpu::PrimitiveState {
1915 topology: wgpu::PrimitiveTopology::TriangleList,
1916 ..Default::default()
1917 },
1918 depth_stencil: None,
1919 multisample: wgpu::MultisampleState {
1920 count: 4,
1921 mask: !0,
1922 alpha_to_coverage_enabled: false,
1923 },
1924 multiview_mask: None,
1925 cache: None,
1926 });
1927
1928 let font =
1929 fontdue::Font::from_bytes(truce_font::JETBRAINS_MONO, fontdue::FontSettings::default())
1930 .expect("failed to parse embedded font");
1931
1932 Some(Self {
1933 device,
1934 queue,
1935 surface: None,
1936 surface_config: None,
1937 pipeline,
1938 target_format: texture_format,
1939 msaa_texture: msaa_view,
1940 msaa_width: phys_w,
1941 msaa_height: phys_h,
1942 vertices: Vec::with_capacity(4096),
1943 indices: Vec::with_capacity(8192),
1944 batches: Vec::new(),
1945 glyph_atlas: GlyphAtlas::new(),
1946 font,
1947 atlas_texture,
1948 atlas_bind_group,
1949 tex_bind_group_layout,
1950 sampler,
1951 images: Vec::new(),
1952 viewport_buffer,
1953 viewport_bind_group,
1954 clear_color: None,
1955 present_clear_default: wgpu::Color::BLACK,
1956 width: phys_w,
1957 height: phys_h,
1958 scale,
1959 })
1960 }
1961
1962 pub fn read_pixels(&mut self) -> Vec<u8> {
1972 self.flush_atlas();
1973
1974 let w = self.width;
1975 let h = self.height;
1976 let format = wgpu::TextureFormat::Rgba8Unorm;
1977
1978 let target_texture = self.device.create_texture(&wgpu::TextureDescriptor {
1980 label: Some("offscreen"),
1981 size: wgpu::Extent3d {
1982 width: w,
1983 height: h,
1984 depth_or_array_layers: 1,
1985 },
1986 mip_level_count: 1,
1987 sample_count: 1,
1988 dimension: wgpu::TextureDimension::D2,
1989 format,
1990 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1991 view_formats: &[],
1992 });
1993 let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
1994
1995 if !self.vertices.is_empty() {
1997 self.render_pass(&target_view);
1998 }
1999
2000 let bytes_per_row = (w * 4 + 255) & !255; let readback_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
2003 label: Some("readback"),
2004 size: u64::from(bytes_per_row * h),
2005 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2006 mapped_at_creation: false,
2007 });
2008
2009 let mut encoder = self
2010 .device
2011 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2012 label: Some("readback"),
2013 });
2014 encoder.copy_texture_to_buffer(
2015 wgpu::TexelCopyTextureInfo {
2016 texture: &target_texture,
2017 mip_level: 0,
2018 origin: wgpu::Origin3d::ZERO,
2019 aspect: wgpu::TextureAspect::All,
2020 },
2021 wgpu::TexelCopyBufferInfo {
2022 buffer: &readback_buf,
2023 layout: wgpu::TexelCopyBufferLayout {
2024 offset: 0,
2025 bytes_per_row: Some(bytes_per_row),
2026 rows_per_image: None,
2027 },
2028 },
2029 wgpu::Extent3d {
2030 width: w,
2031 height: h,
2032 depth_or_array_layers: 1,
2033 },
2034 );
2035 self.queue.submit(std::iter::once(encoder.finish()));
2036
2037 let buf_slice = readback_buf.slice(..);
2039 let (tx, rx) = std::sync::mpsc::channel();
2040 buf_slice.map_async(wgpu::MapMode::Read, move |result| {
2041 tx.send(result).unwrap();
2042 });
2043 let _ = self.device.poll(wgpu::PollType::Wait {
2044 submission_index: None,
2045 timeout: None,
2046 });
2047 rx.recv().unwrap().expect("buffer map failed");
2048
2049 let mapped = buf_slice.get_mapped_range();
2050 let mut pixels = Vec::with_capacity((w * h * 4) as usize);
2051 for row in 0..h {
2052 let start = (row * bytes_per_row) as usize;
2053 let end = start + (w * 4) as usize;
2054 pixels.extend_from_slice(&mapped[start..end]);
2055 }
2056 drop(mapped);
2057 readback_buf.unmap();
2058
2059 for px in pixels.chunks_exact_mut(4) {
2066 let a = px[3];
2067 if a == 0 || a == 255 {
2068 continue;
2069 }
2070 let a16 = u16::from(a);
2071 px[0] = ((u16::from(px[0]) * 255 + a16 / 2) / a16).min(255) as u8;
2073 px[1] = ((u16::from(px[1]) * 255 + a16 / 2) / a16).min(255) as u8;
2074 px[2] = ((u16::from(px[2]) * 255 + a16 / 2) / a16).min(255) as u8;
2075 }
2076
2077 pixels
2078 }
2079}
2080
2081#[cfg(test)]
2086mod tests {
2087 use super::*;
2088
2089 #[test]
2090 fn vertex_size() {
2091 let size = std::mem::size_of::<Vertex>();
2093 assert!(size > 0, "Vertex should have non-zero size: {size}");
2094 }
2095
2096 #[test]
2102 fn ortho_matrix_maps_origin() {
2103 let m = ortho_matrix(800.0, 600.0);
2104 let x = m[0][0] * 0.0 + m[3][0];
2105 let y = m[1][1] * 0.0 + m[3][1];
2106 assert!((x - (-1.0)).abs() < 1e-6);
2107 assert!((y - 1.0).abs() < 1e-6);
2108 }
2109
2110 #[test]
2111 fn ortho_matrix_maps_bottom_right() {
2112 let m = ortho_matrix(800.0, 600.0);
2113 let x = m[0][0] * 800.0 + m[3][0];
2114 let y = m[1][1] * 600.0 + m[3][1];
2115 assert!((x - 1.0).abs() < 1e-6);
2116 assert!((y - (-1.0)).abs() < 1e-6);
2117 }
2118
2119 #[test]
2120 fn glyph_atlas_shelf_packing() {
2121 let font =
2122 fontdue::Font::from_bytes(truce_font::JETBRAINS_MONO, fontdue::FontSettings::default())
2123 .unwrap();
2124 let mut atlas = GlyphAtlas::new();
2125
2126 atlas.ensure_glyph(&font, 'A', 14.0);
2128 atlas.ensure_glyph(&font, 'B', 14.0);
2129 atlas.ensure_glyph(&font, 'C', 14.0);
2130
2131 assert_eq!(atlas.glyphs.len(), 3);
2132 assert!(!atlas.pending.is_empty());
2133
2134 atlas.ensure_glyph(&font, 'A', 14.0);
2136 assert_eq!(atlas.glyphs.len(), 3);
2137 }
2138
2139 #[test]
2140 fn lyon_fill_circle_produces_triangles() {
2141 let mut builder = Path::builder();
2142 builder.add_circle(
2143 point(50.0, 50.0),
2144 10.0,
2145 lyon_tessellation::path::Winding::Positive,
2146 );
2147 let path = builder.build();
2148 let mut buffers: VertexBuffers<[f32; 2], u32> = VertexBuffers::new();
2149 let mut tess = FillTessellator::new();
2150 tess.tessellate_path(
2151 &path,
2152 &FillOptions::tolerance(0.5),
2153 &mut BuffersBuilder::new(&mut buffers, |v: FillVertex| {
2154 let p = v.position();
2155 [p.x, p.y]
2156 }),
2157 )
2158 .unwrap();
2159 assert!(buffers.vertices.len() >= 3);
2160 assert!(buffers.indices.len() >= 3);
2161 }
2162
2163 #[test]
2168 #[allow(clippy::too_many_lines, clippy::many_single_char_names)]
2169 fn standalone_pipeline_renders() {
2170 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
2171 desc.backends = wgpu::Backends::PRIMARY;
2172 let instance = wgpu::Instance::new(desc);
2173 let Ok(adapter) =
2174 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
2175 power_preference: wgpu::PowerPreference::HighPerformance,
2176 compatible_surface: None,
2177 force_fallback_adapter: false,
2178 }))
2179 else {
2180 return; };
2182 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
2183 label: Some("standalone-test"),
2184 required_features: wgpu::Features::empty(),
2185 required_limits: wgpu::Limits::downlevel_defaults(),
2186 experimental_features: wgpu::ExperimentalFeatures::default(),
2187 memory_hints: wgpu::MemoryHints::Performance,
2188 trace: wgpu::Trace::Off,
2189 }))
2190 .expect("request_device");
2191 let device = Arc::new(device);
2192 let queue = Arc::new(queue);
2193
2194 let w = 64u32;
2195 let h = 48u32;
2196 let format = wgpu::TextureFormat::Rgba8Unorm;
2197 let mut backend =
2198 WgpuBackend::new(Arc::clone(&device), Arc::clone(&queue), format, w, h, 1.0)
2199 .expect("backend new");
2200
2201 let target = device.create_texture(&wgpu::TextureDescriptor {
2204 label: Some("standalone-target"),
2205 size: wgpu::Extent3d {
2206 width: w,
2207 height: h,
2208 depth_or_array_layers: 1,
2209 },
2210 mip_level_count: 1,
2211 sample_count: 1,
2212 dimension: wgpu::TextureDimension::D2,
2213 format,
2214 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2215 view_formats: &[],
2216 });
2217 let view = target.create_view(&wgpu::TextureViewDescriptor::default());
2218
2219 backend.begin_frame(w, h);
2220 backend.clear(Color::rgb(0.0, 0.0, 0.0));
2221 backend.fill_rect(8.0, 8.0, 16.0, 16.0, Color::rgb(0.0, 1.0, 0.0));
2222 backend.draw_text("x", 20.0, 20.0, 14.0, Color::rgb(1.0, 1.0, 1.0));
2223
2224 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2225 label: Some("standalone-enc"),
2226 });
2227 backend.finish(&mut encoder, &view);
2228
2229 let bytes_per_row = (w * 4 + 255) & !255;
2231 let readback = device.create_buffer(&wgpu::BufferDescriptor {
2232 label: Some("readback"),
2233 size: u64::from(bytes_per_row * h),
2234 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2235 mapped_at_creation: false,
2236 });
2237 encoder.copy_texture_to_buffer(
2238 wgpu::TexelCopyTextureInfo {
2239 texture: &target,
2240 mip_level: 0,
2241 origin: wgpu::Origin3d::ZERO,
2242 aspect: wgpu::TextureAspect::All,
2243 },
2244 wgpu::TexelCopyBufferInfo {
2245 buffer: &readback,
2246 layout: wgpu::TexelCopyBufferLayout {
2247 offset: 0,
2248 bytes_per_row: Some(bytes_per_row),
2249 rows_per_image: None,
2250 },
2251 },
2252 wgpu::Extent3d {
2253 width: w,
2254 height: h,
2255 depth_or_array_layers: 1,
2256 },
2257 );
2258 queue.submit(std::iter::once(encoder.finish()));
2259
2260 let slice = readback.slice(..);
2261 let (tx, rx) = std::sync::mpsc::channel();
2262 slice.map_async(wgpu::MapMode::Read, move |r| {
2263 tx.send(r).unwrap();
2264 });
2265 let _ = device.poll(wgpu::PollType::Wait {
2266 submission_index: None,
2267 timeout: None,
2268 });
2269 rx.recv().unwrap().unwrap();
2270 let mapped = slice.get_mapped_range();
2271
2272 let row_off = 16usize * bytes_per_row as usize;
2274 let px_off = row_off + 16 * 4;
2275 let r = mapped[px_off];
2276 let g = mapped[px_off + 1];
2277 let b = mapped[px_off + 2];
2278 assert!(g > 200, "green rect not rendered: got rgb=({r},{g},{b})");
2279 assert!(
2280 r < 50 && b < 50,
2281 "green rect leaked other channels: rgb=({r},{g},{b})"
2282 );
2283 }
2284}