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::render::{ImageId, RenderBackend};
23use truce_gui::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::to_physical_px(logical_w, f64::from(scale));
357 let height = truce_gui::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 = fontdue::Font::from_bytes(
588 truce_gui::font::JETBRAINS_MONO,
589 fontdue::FontSettings::default(),
590 )
591 .expect("failed to parse embedded font");
592
593 Some(Self {
594 device,
595 queue,
596 surface: Some(surface),
597 surface_config: Some(surface_config),
598 pipeline,
599 target_format: surface_format,
600 msaa_texture,
601 msaa_width: width,
602 msaa_height: height,
603 vertices: Vec::with_capacity(4096),
604 indices: Vec::with_capacity(8192),
605 batches: Vec::new(),
606 glyph_atlas: GlyphAtlas::new(),
607 font,
608 atlas_texture,
609 atlas_bind_group,
610 tex_bind_group_layout,
611 sampler,
612 images: Vec::new(),
613 viewport_buffer,
614 viewport_bind_group,
615 clear_color: None,
616 present_clear_default: wgpu::Color::BLACK,
617 width,
618 height,
619 scale,
620 })
621 }
622
623 #[cfg(target_os = "macos")]
633 pub unsafe fn from_metal_layer(
634 metal_layer: *mut c_void,
635 logical_w: u32,
636 logical_h: u32,
637 scale: f32,
638 ) -> Option<Self> {
639 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
640 desc.backends = wgpu::Backends::METAL;
641 let instance = wgpu::Instance::new(desc);
642
643 let surface = unsafe {
644 instance
645 .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(metal_layer))
646 }
647 .ok()?;
648
649 Self::from_surface(&instance, surface, logical_w, logical_h, scale)
650 }
651
652 #[cfg(not(target_os = "ios"))]
660 #[must_use]
661 pub unsafe fn from_window(
662 window: &baseview::Window,
663 logical_w: u32,
664 logical_h: u32,
665 scale: f32,
666 ) -> Option<Self> {
667 unsafe {
668 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
669 desc.backends = wgpu::Backends::PRIMARY;
670 let instance = wgpu::Instance::new(desc);
671
672 let surface = crate::platform::create_wgpu_surface(&instance, window)?;
673 Self::from_surface(&instance, surface, logical_w, logical_h, scale)
674 }
675 }
676
677 #[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
719 #[must_use]
720 pub fn new(
721 device: Arc<wgpu::Device>,
722 queue: Arc<wgpu::Queue>,
723 target_format: wgpu::TextureFormat,
724 max_logical_w: u32,
725 max_logical_h: u32,
726 scale: f32,
727 ) -> Option<Self> {
728 let scale = scale.max(0.0);
729 let width = truce_gui::to_physical_px(max_logical_w, f64::from(scale));
730 let height = truce_gui::to_physical_px(max_logical_h, f64::from(scale));
731
732 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
734 label: Some("truce-gpu-shader"),
735 source: wgpu::ShaderSource::Wgsl(SHADER_SRC.into()),
736 });
737
738 let matrix = ortho_matrix(width as f32, height as f32);
740 let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
741 label: Some("viewport"),
742 contents: bytemuck::cast_slice(&matrix),
743 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
744 });
745
746 let viewport_bind_group_layout =
747 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
748 label: Some("viewport-layout"),
749 entries: &[wgpu::BindGroupLayoutEntry {
750 binding: 0,
751 visibility: wgpu::ShaderStages::VERTEX,
752 ty: wgpu::BindingType::Buffer {
753 ty: wgpu::BufferBindingType::Uniform,
754 has_dynamic_offset: false,
755 min_binding_size: None,
756 },
757 count: None,
758 }],
759 });
760
761 let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
762 label: Some("viewport-bg"),
763 layout: &viewport_bind_group_layout,
764 entries: &[wgpu::BindGroupEntry {
765 binding: 0,
766 resource: viewport_buffer.as_entire_binding(),
767 }],
768 });
769
770 let atlas_texture = device.create_texture(&wgpu::TextureDescriptor {
772 label: Some("glyph-atlas"),
773 size: wgpu::Extent3d {
774 width: ATLAS_SIZE,
775 height: ATLAS_SIZE,
776 depth_or_array_layers: 1,
777 },
778 mip_level_count: 1,
779 sample_count: 1,
780 dimension: wgpu::TextureDimension::D2,
781 format: wgpu::TextureFormat::R8Unorm,
782 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
783 view_formats: &[],
784 });
785 let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
786 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
787 mag_filter: wgpu::FilterMode::Linear,
788 min_filter: wgpu::FilterMode::Linear,
789 ..Default::default()
790 });
791 let tex_bind_group_layout =
792 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
793 label: Some("tex-layout"),
794 entries: &[
795 wgpu::BindGroupLayoutEntry {
796 binding: 0,
797 visibility: wgpu::ShaderStages::FRAGMENT,
798 ty: wgpu::BindingType::Texture {
799 sample_type: wgpu::TextureSampleType::Float { filterable: true },
800 view_dimension: wgpu::TextureViewDimension::D2,
801 multisampled: false,
802 },
803 count: None,
804 },
805 wgpu::BindGroupLayoutEntry {
806 binding: 1,
807 visibility: wgpu::ShaderStages::FRAGMENT,
808 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
809 count: None,
810 },
811 ],
812 });
813 let atlas_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
814 label: Some("atlas-bg"),
815 layout: &tex_bind_group_layout,
816 entries: &[
817 wgpu::BindGroupEntry {
818 binding: 0,
819 resource: wgpu::BindingResource::TextureView(&atlas_view),
820 },
821 wgpu::BindGroupEntry {
822 binding: 1,
823 resource: wgpu::BindingResource::Sampler(&sampler),
824 },
825 ],
826 });
827
828 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
830 label: Some("truce-gpu-pipeline-layout"),
831 bind_group_layouts: &[
832 Some(&viewport_bind_group_layout),
833 Some(&tex_bind_group_layout),
834 ],
835 immediate_size: 0,
836 });
837
838 let vertex_layout = wgpu::VertexBufferLayout {
839 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
840 step_mode: wgpu::VertexStepMode::Vertex,
841 attributes: &[
842 wgpu::VertexAttribute {
843 offset: 0,
844 shader_location: 0,
845 format: wgpu::VertexFormat::Float32x2,
846 },
847 wgpu::VertexAttribute {
848 offset: 8,
849 shader_location: 1,
850 format: wgpu::VertexFormat::Float32x4,
851 },
852 wgpu::VertexAttribute {
853 offset: 24,
854 shader_location: 2,
855 format: wgpu::VertexFormat::Float32x2,
856 },
857 wgpu::VertexAttribute {
858 offset: 32,
859 shader_location: 3,
860 format: wgpu::VertexFormat::Float32,
861 },
862 ],
863 };
864
865 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
866 label: Some("truce-gpu-pipeline"),
867 layout: Some(&pipeline_layout),
868 vertex: wgpu::VertexState {
869 module: &shader,
870 entry_point: Some("vs_main"),
871 buffers: &[vertex_layout],
872 compilation_options: wgpu::PipelineCompilationOptions::default(),
873 },
874 fragment: Some(wgpu::FragmentState {
875 module: &shader,
876 entry_point: Some("fs_main"),
877 targets: &[Some(wgpu::ColorTargetState {
878 format: target_format,
879 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
880 write_mask: wgpu::ColorWrites::ALL,
881 })],
882 compilation_options: wgpu::PipelineCompilationOptions::default(),
883 }),
884 primitive: wgpu::PrimitiveState {
885 topology: wgpu::PrimitiveTopology::TriangleList,
886 ..Default::default()
887 },
888 depth_stencil: None,
889 multisample: wgpu::MultisampleState {
890 count: 4,
891 mask: !0,
892 alpha_to_coverage_enabled: false,
893 },
894 multiview_mask: None,
895 cache: None,
896 });
897
898 let msaa_texture = Self::create_msaa_view(&device, target_format, width, height);
900
901 let font = fontdue::Font::from_bytes(
902 truce_gui::font::JETBRAINS_MONO,
903 fontdue::FontSettings::default(),
904 )
905 .expect("failed to parse embedded font");
906
907 Some(Self {
908 device,
909 queue,
910 surface: None,
911 surface_config: None,
912 pipeline,
913 target_format,
914 msaa_texture,
915 msaa_width: width,
916 msaa_height: height,
917 vertices: Vec::with_capacity(4096),
918 indices: Vec::with_capacity(8192),
919 batches: Vec::new(),
920 glyph_atlas: GlyphAtlas::new(),
921 font,
922 atlas_texture,
923 atlas_bind_group,
924 tex_bind_group_layout,
925 sampler,
926 images: Vec::new(),
927 viewport_buffer,
928 viewport_bind_group,
929 clear_color: None,
930 present_clear_default: wgpu::Color::TRANSPARENT,
931 width,
932 height,
933 scale,
934 })
935 }
936
937 #[allow(clippy::cast_precision_loss)]
948 pub fn begin_frame(&mut self, logical_w: u32, logical_h: u32) {
949 let phys_w = truce_gui::to_physical_px(logical_w, f64::from(self.scale));
950 let phys_h = truce_gui::to_physical_px(logical_h, f64::from(self.scale));
951 self.vertices.clear();
952 self.indices.clear();
953 self.batches.clear();
954 self.clear_color = None;
955
956 if phys_w != self.width || phys_h != self.height {
957 self.width = phys_w;
958 self.height = phys_h;
959 let matrix = ortho_matrix(phys_w as f32, phys_h as f32);
960 self.queue
961 .write_buffer(&self.viewport_buffer, 0, bytemuck::cast_slice(&matrix));
962 }
963
964 if phys_w != self.msaa_width || phys_h != self.msaa_height {
965 self.msaa_texture =
966 Self::create_msaa_view(&self.device, self.target_format, phys_w, phys_h);
967 self.msaa_width = phys_w;
968 self.msaa_height = phys_h;
969 }
970 }
971
972 pub fn scale(&self) -> f32 {
977 self.scale
978 }
979
980 pub fn set_scale(&mut self, scale: f32) {
988 if scale.is_finite() && scale > 0.0 {
989 self.scale = scale;
990 }
991 }
992
993 pub fn finish(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
1002 self.flush_atlas();
1003
1004 if self.indices.is_empty() {
1005 self.clear_color = None;
1006 return;
1007 }
1008
1009 let vertex_buffer = self
1010 .device
1011 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1012 label: Some("vertices"),
1013 contents: bytemuck::cast_slice(&self.vertices),
1014 usage: wgpu::BufferUsages::VERTEX,
1015 });
1016
1017 let index_buffer = self
1018 .device
1019 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1020 label: Some("indices"),
1021 contents: bytemuck::cast_slice(&self.indices),
1022 usage: wgpu::BufferUsages::INDEX,
1023 });
1024
1025 let (load, store) = match self.clear_color {
1031 Some(c) => (wgpu::LoadOp::Clear(c), wgpu::StoreOp::Discard),
1032 None => (wgpu::LoadOp::Load, wgpu::StoreOp::Store),
1033 };
1034
1035 {
1036 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1037 label: Some("truce-gpu-frame"),
1038 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1039 view: &self.msaa_texture,
1040 resolve_target: Some(view),
1041 ops: wgpu::Operations { load, store },
1042 depth_slice: None,
1043 })],
1044 depth_stencil_attachment: None,
1045 timestamp_writes: None,
1046 occlusion_query_set: None,
1047 multiview_mask: None,
1048 });
1049
1050 pass.set_pipeline(&self.pipeline);
1051 pass.set_bind_group(0, &self.viewport_bind_group, &[]);
1052 pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1053 pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1054
1055 let total_indices = len_u32(self.indices.len());
1056 if self.batches.is_empty() {
1057 pass.set_bind_group(1, &self.atlas_bind_group, &[]);
1058 pass.draw_indexed(0..total_indices, 0, 0..1);
1059 } else {
1060 for i in 0..self.batches.len() {
1061 let b = self.batches[i];
1062 let end = self
1063 .batches
1064 .get(i + 1)
1065 .map_or(total_indices, |n| n.index_start);
1066 if end <= b.index_start {
1067 continue;
1068 }
1069 let bg = match b.image {
1070 None => &self.atlas_bind_group,
1071 Some(img_id) => {
1072 match self.images.get(img_id.0 as usize).and_then(|s| s.as_ref()) {
1073 Some(entry) => &entry.bind_group,
1074 None => continue,
1075 }
1076 }
1077 };
1078 pass.set_bind_group(1, bg, &[]);
1079 pass.draw_indexed(b.index_start..end, 0, 0..1);
1080 }
1081 }
1082 }
1083
1084 self.clear_color = None;
1085 }
1086
1087 fn create_msaa_view(
1088 device: &wgpu::Device,
1089 format: wgpu::TextureFormat,
1090 width: u32,
1091 height: u32,
1092 ) -> wgpu::TextureView {
1093 let tex = device.create_texture(&wgpu::TextureDescriptor {
1094 label: Some("msaa"),
1095 size: wgpu::Extent3d {
1096 width,
1097 height,
1098 depth_or_array_layers: 1,
1099 },
1100 mip_level_count: 1,
1101 sample_count: 4,
1102 dimension: wgpu::TextureDimension::D2,
1103 format,
1104 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1105 view_formats: &[],
1106 });
1107 tex.create_view(&wgpu::TextureViewDescriptor::default())
1108 }
1109
1110 fn create_msaa_texture(
1111 device: &wgpu::Device,
1112 config: &wgpu::SurfaceConfiguration,
1113 ) -> wgpu::TextureView {
1114 let tex = device.create_texture(&wgpu::TextureDescriptor {
1115 label: Some("msaa"),
1116 size: wgpu::Extent3d {
1117 width: config.width,
1118 height: config.height,
1119 depth_or_array_layers: 1,
1120 },
1121 mip_level_count: 1,
1122 sample_count: 4,
1123 dimension: wgpu::TextureDimension::D2,
1124 format: config.format,
1125 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1126 view_formats: &[],
1127 });
1128 tex.create_view(&wgpu::TextureViewDescriptor::default())
1129 }
1130
1131 #[allow(clippy::cast_precision_loss)]
1137 pub fn resize(&mut self, logical_w: u32, logical_h: u32) -> bool {
1138 let new_w = truce_gui::to_physical_px(logical_w, f64::from(self.scale));
1139 let new_h = truce_gui::to_physical_px(logical_h, f64::from(self.scale));
1140 if new_w == self.width && new_h == self.height {
1141 return false;
1142 }
1143 self.width = new_w;
1144 self.height = new_h;
1145
1146 if let Some(ref surface) = self.surface
1147 && let Some(ref mut config) = self.surface_config
1148 {
1149 config.width = new_w;
1150 config.height = new_h;
1151 surface.configure(&self.device, config);
1152 self.msaa_texture = Self::create_msaa_texture(&self.device, config);
1153 }
1154
1155 let matrix = ortho_matrix(new_w as f32, new_h as f32);
1157 self.queue
1158 .write_buffer(&self.viewport_buffer, 0, bytemuck::cast_slice(&matrix));
1159
1160 true
1161 }
1162
1163 fn color_arr(c: Color) -> [f32; 4] {
1166 [c.r, c.g, c.b, c.a]
1167 }
1168
1169 fn ensure_batch(&mut self, image: Option<ImageId>) {
1172 let needs_new = self.batches.last().is_none_or(|last| last.image != image);
1173 if needs_new {
1174 self.batches.push(DrawBatch {
1175 index_start: len_u32(self.indices.len()),
1176 image,
1177 });
1178 }
1179 }
1180
1181 fn push_quad(&mut self, v0: Vertex, v1: Vertex, v2: Vertex, v3: Vertex) {
1182 self.ensure_batch(None);
1183 let base = len_u32(self.vertices.len());
1184 self.vertices.extend_from_slice(&[v0, v1, v2, v3]);
1185 self.indices
1186 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
1187 }
1188
1189 fn fill_path(&mut self, path: &Path, color: [f32; 4]) {
1191 self.ensure_batch(None);
1192 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
1193 let mut tessellator = FillTessellator::new();
1194 let _ = tessellator.tessellate_path(
1195 path,
1196 &FillOptions::tolerance(0.5),
1197 &mut BuffersBuilder::new(&mut buffers, |vertex: FillVertex| {
1198 let p = vertex.position();
1199 Vertex::solid(p.x, p.y, color)
1200 }),
1201 );
1202 let base = len_u32(self.vertices.len());
1203 self.vertices.extend_from_slice(&buffers.vertices);
1204 self.indices
1205 .extend(buffers.indices.iter().map(|i| i + base));
1206 }
1207
1208 fn stroke_path(&mut self, path: &Path, color: [f32; 4], opts: &StrokeOptions) {
1210 self.ensure_batch(None);
1211 let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
1212 let mut tessellator = StrokeTessellator::new();
1213 let _ = tessellator.tessellate_path(
1214 path,
1215 opts,
1216 &mut BuffersBuilder::new(&mut buffers, |vertex: StrokeVertex| {
1217 let p = vertex.position();
1218 Vertex::solid(p.x, p.y, color)
1219 }),
1220 );
1221 let base = len_u32(self.vertices.len());
1222 self.vertices.extend_from_slice(&buffers.vertices);
1223 self.indices
1224 .extend(buffers.indices.iter().map(|i| i + base));
1225 }
1226
1227 fn flush_atlas(&mut self) {
1229 for (x, y, w, h, data) in self.glyph_atlas.pending.drain(..) {
1230 if w == 0 || h == 0 {
1231 continue;
1232 }
1233 self.queue.write_texture(
1234 wgpu::TexelCopyTextureInfo {
1235 texture: &self.atlas_texture,
1236 mip_level: 0,
1237 origin: wgpu::Origin3d { x, y, z: 0 },
1238 aspect: wgpu::TextureAspect::All,
1239 },
1240 &data,
1241 wgpu::TexelCopyBufferLayout {
1242 offset: 0,
1243 bytes_per_row: Some(w),
1244 rows_per_image: Some(h),
1245 },
1246 wgpu::Extent3d {
1247 width: w,
1248 height: h,
1249 depth_or_array_layers: 1,
1250 },
1251 );
1252 }
1253 }
1254}
1255
1256#[allow(clippy::many_single_char_names)]
1266impl RenderBackend for WgpuBackend {
1267 fn clear(&mut self, color: Color) {
1268 self.clear_color = Some(wgpu::Color {
1269 r: f64::from(color.r),
1270 g: f64::from(color.g),
1271 b: f64::from(color.b),
1272 a: f64::from(color.a),
1273 });
1274 self.vertices.clear();
1275 self.indices.clear();
1276 self.batches.clear();
1277 if self.glyph_atlas.overflow_pending {
1282 self.glyph_atlas.clear();
1283 }
1284 }
1285
1286 fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Color) {
1287 let s = self.scale;
1288 let c = Self::color_arr(color);
1289 self.push_quad(
1290 Vertex::solid(x * s, y * s, c),
1291 Vertex::solid((x + w) * s, y * s, c),
1292 Vertex::solid((x + w) * s, (y + h) * s, c),
1293 Vertex::solid(x * s, (y + h) * s, c),
1294 );
1295 }
1296
1297 fn fill_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color) {
1298 let s = self.scale;
1299 let c = Self::color_arr(color);
1300 let mut builder = Path::builder();
1301 builder.add_circle(
1302 point(cx * s, cy * s),
1303 radius * s,
1304 lyon_tessellation::path::Winding::Positive,
1305 );
1306 let path = builder.build();
1307 self.fill_path(&path, c);
1308 }
1309
1310 fn stroke_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color, width: f32) {
1311 let s = self.scale;
1312 let c = Self::color_arr(color);
1313 let mut builder = Path::builder();
1314 builder.add_circle(
1315 point(cx * s, cy * s),
1316 radius * s,
1317 lyon_tessellation::path::Winding::Positive,
1318 );
1319 let path = builder.build();
1320 let opts = StrokeOptions::tolerance(0.5).with_line_width(width * s);
1321 self.stroke_path(&path, c, &opts);
1322 }
1323
1324 #[allow(clippy::cast_precision_loss)]
1325 fn stroke_arc(
1326 &mut self,
1327 cx: f32,
1328 cy: f32,
1329 radius: f32,
1330 start_angle: f32,
1331 end_angle: f32,
1332 color: Color,
1333 width: f32,
1334 ) {
1335 let s = self.scale;
1336 let c = Self::color_arr(color);
1337 let segments = 64u32;
1338 let sweep = end_angle - start_angle;
1339 let step = sweep / segments as f32;
1340
1341 let mut builder = Path::builder();
1342 builder.begin(point(
1343 cx * s + radius * s * start_angle.cos(),
1344 cy * s + radius * s * start_angle.sin(),
1345 ));
1346 for i in 1..=segments {
1347 let angle = start_angle + step * i as f32;
1348 builder.line_to(point(
1349 cx * s + radius * s * angle.cos(),
1350 cy * s + radius * s * angle.sin(),
1351 ));
1352 }
1353 builder.end(false);
1354 let path = builder.build();
1355
1356 let opts = StrokeOptions::tolerance(0.5)
1357 .with_line_width(width * s)
1358 .with_line_cap(lyon_tessellation::LineCap::Round);
1359 self.stroke_path(&path, c, &opts);
1360 }
1361
1362 fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color, width: f32) {
1363 let s = self.scale;
1364 let c = Self::color_arr(color);
1365 let mut builder = Path::builder();
1366 builder.begin(point(x1 * s, y1 * s));
1367 builder.line_to(point(x2 * s, y2 * s));
1368 builder.end(false);
1369 let path = builder.build();
1370
1371 let opts = StrokeOptions::tolerance(0.5)
1372 .with_line_width(width * s)
1373 .with_line_cap(lyon_tessellation::LineCap::Round);
1374 self.stroke_path(&path, c, &opts);
1375 }
1376
1377 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1380 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: Color) {
1381 let s = self.scale;
1382 let phys_size = size * s;
1383 let c = Self::color_arr(color);
1384 let line_metrics = self.font.horizontal_line_metrics(phys_size);
1385 let ascent = line_metrics.map_or(phys_size * 0.8, |m| m.ascent);
1386
1387 let mut cursor_x = x * s;
1388
1389 let chars: Vec<char> = text.chars().collect();
1390 for &ch in &chars {
1391 self.glyph_atlas.ensure_glyph(&self.font, ch, phys_size);
1392 }
1393
1394 for &ch in &chars {
1400 let key = (ch, (phys_size * 10.0) as u32);
1401 let Some(g) = self.glyph_atlas.glyphs.get(&key) else {
1402 continue;
1403 };
1404 let (u0, v0, u1, v1, gw, gh, y_off, advance) = (
1405 g.u0, g.v0, g.u1, g.v1, g.width, g.height, g.y_offset, g.advance,
1406 );
1407 let gx = cursor_x;
1408 let gy = y * s + ascent - y_off - gh;
1409
1410 self.push_quad(
1411 Vertex::glyph(gx, gy, c, u0, v0),
1412 Vertex::glyph(gx + gw, gy, c, u1, v0),
1413 Vertex::glyph(gx + gw, gy + gh, c, u1, v1),
1414 Vertex::glyph(gx, gy + gh, c, u0, v1),
1415 );
1416
1417 cursor_x += advance;
1418 }
1419 }
1420
1421 fn text_width(&self, text: &str, size: f32) -> f32 {
1422 let phys_size = size * self.scale;
1423 truce_gui::font::text_width_fontdue(text, phys_size) / self.scale
1424 }
1425
1426 fn register_image(&mut self, rgba: &[u8], width: u32, height: u32) -> ImageId {
1427 let expected = (width as usize) * (height as usize) * 4;
1428 if width == 0 || height == 0 || rgba.len() < expected {
1429 return ImageId::INVALID;
1430 }
1431
1432 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1433 label: Some("image"),
1434 size: wgpu::Extent3d {
1435 width,
1436 height,
1437 depth_or_array_layers: 1,
1438 },
1439 mip_level_count: 1,
1440 sample_count: 1,
1441 dimension: wgpu::TextureDimension::D2,
1442 format: wgpu::TextureFormat::Rgba8Unorm,
1443 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1444 view_formats: &[],
1445 });
1446
1447 self.queue.write_texture(
1448 wgpu::TexelCopyTextureInfo {
1449 texture: &texture,
1450 mip_level: 0,
1451 origin: wgpu::Origin3d::ZERO,
1452 aspect: wgpu::TextureAspect::All,
1453 },
1454 &rgba[..expected],
1455 wgpu::TexelCopyBufferLayout {
1456 offset: 0,
1457 bytes_per_row: Some(width * 4),
1458 rows_per_image: Some(height),
1459 },
1460 wgpu::Extent3d {
1461 width,
1462 height,
1463 depth_or_array_layers: 1,
1464 },
1465 );
1466
1467 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1468 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1469 label: Some("image-bg"),
1470 layout: &self.tex_bind_group_layout,
1471 entries: &[
1472 wgpu::BindGroupEntry {
1473 binding: 0,
1474 resource: wgpu::BindingResource::TextureView(&view),
1475 },
1476 wgpu::BindGroupEntry {
1477 binding: 1,
1478 resource: wgpu::BindingResource::Sampler(&self.sampler),
1479 },
1480 ],
1481 });
1482
1483 let entry = ImageEntry {
1484 _texture: texture,
1485 bind_group,
1486 };
1487
1488 if let Some((idx, slot)) = self
1489 .images
1490 .iter_mut()
1491 .enumerate()
1492 .find(|(_, s)| s.is_none())
1493 {
1494 *slot = Some(entry);
1495 return ImageId(len_u32(idx));
1496 }
1497 let id = len_u32(self.images.len());
1498 self.images.push(Some(entry));
1499 ImageId(id)
1500 }
1501
1502 fn unregister_image(&mut self, id: ImageId) {
1503 if let Some(slot) = self.images.get_mut(id.0 as usize) {
1504 *slot = None;
1505 }
1506 }
1507
1508 fn draw_image(&mut self, id: ImageId, x: f32, y: f32, w: f32, h: f32) {
1509 if self
1510 .images
1511 .get(id.0 as usize)
1512 .and_then(|s| s.as_ref())
1513 .is_none()
1514 {
1515 return;
1516 }
1517 self.ensure_batch(Some(id));
1518
1519 let s = self.scale;
1520 let c = [1.0, 1.0, 1.0, 1.0];
1521 let base = len_u32(self.vertices.len());
1522 self.vertices.extend_from_slice(&[
1523 Vertex::image(x * s, y * s, c, 0.0, 0.0),
1524 Vertex::image((x + w) * s, y * s, c, 1.0, 0.0),
1525 Vertex::image((x + w) * s, (y + h) * s, c, 1.0, 1.0),
1526 Vertex::image(x * s, (y + h) * s, c, 0.0, 1.0),
1527 ]);
1528 self.indices
1529 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
1530 }
1531
1532 fn present(&mut self) {
1533 self.flush_atlas();
1535
1536 let Some(surface) = &self.surface else {
1537 return; };
1539
1540 let (wgpu::CurrentSurfaceTexture::Success(frame)
1541 | wgpu::CurrentSurfaceTexture::Suboptimal(frame)) = surface.get_current_texture()
1542 else {
1543 return;
1544 };
1545 let frame_view = frame
1546 .texture
1547 .create_view(&wgpu::TextureViewDescriptor::default());
1548
1549 if self.vertices.is_empty() {
1550 self.clear_only_pass(&frame_view);
1556 frame.present();
1557 return;
1558 }
1559
1560 self.render_pass(&frame_view);
1561 frame.present();
1562 }
1563}
1564
1565impl WgpuBackend {
1566 fn clear_only_pass(&mut self, resolve_target: &wgpu::TextureView) {
1572 let clear_color = self.clear_color.unwrap_or(self.present_clear_default);
1573 let mut encoder = self
1574 .device
1575 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1576 label: Some("clear-only"),
1577 });
1578 {
1579 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1580 label: Some("clear-only"),
1581 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1582 view: &self.msaa_texture,
1583 resolve_target: Some(resolve_target),
1584 ops: wgpu::Operations {
1585 load: wgpu::LoadOp::Clear(clear_color),
1586 store: wgpu::StoreOp::Discard,
1587 },
1588 depth_slice: None,
1589 })],
1590 depth_stencil_attachment: None,
1591 timestamp_writes: None,
1592 occlusion_query_set: None,
1593 multiview_mask: None,
1594 });
1595 }
1596 self.queue.submit(std::iter::once(encoder.finish()));
1597 }
1598
1599 fn render_pass(&mut self, resolve_target: &wgpu::TextureView) {
1601 let clear_color = self.clear_color.unwrap_or(self.present_clear_default);
1602 let vertex_buffer = self
1603 .device
1604 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1605 label: Some("vertices"),
1606 contents: bytemuck::cast_slice(&self.vertices),
1607 usage: wgpu::BufferUsages::VERTEX,
1608 });
1609
1610 let index_buffer = self
1611 .device
1612 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1613 label: Some("indices"),
1614 contents: bytemuck::cast_slice(&self.indices),
1615 usage: wgpu::BufferUsages::INDEX,
1616 });
1617
1618 let mut encoder = self
1619 .device
1620 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1621 label: Some("frame"),
1622 });
1623
1624 {
1625 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1626 label: Some("main"),
1627 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1628 view: &self.msaa_texture,
1629 resolve_target: Some(resolve_target),
1630 ops: wgpu::Operations {
1631 load: wgpu::LoadOp::Clear(clear_color),
1632 store: wgpu::StoreOp::Discard,
1633 },
1634 depth_slice: None,
1635 })],
1636 depth_stencil_attachment: None,
1637 timestamp_writes: None,
1638 occlusion_query_set: None,
1639 multiview_mask: None,
1640 });
1641
1642 pass.set_pipeline(&self.pipeline);
1643 pass.set_bind_group(0, &self.viewport_bind_group, &[]);
1644 pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1645 pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1646
1647 let total_indices = len_u32(self.indices.len());
1648 if self.batches.is_empty() {
1649 pass.set_bind_group(1, &self.atlas_bind_group, &[]);
1653 pass.draw_indexed(0..total_indices, 0, 0..1);
1654 } else {
1655 for i in 0..self.batches.len() {
1656 let b = self.batches[i];
1657 let end = self
1658 .batches
1659 .get(i + 1)
1660 .map_or(total_indices, |n| n.index_start);
1661 if end <= b.index_start {
1662 continue;
1663 }
1664 let bg = match b.image {
1665 None => &self.atlas_bind_group,
1666 Some(img_id) => {
1667 match self.images.get(img_id.0 as usize).and_then(|s| s.as_ref()) {
1668 Some(entry) => &entry.bind_group,
1669 None => continue,
1671 }
1672 }
1673 };
1674 pass.set_bind_group(1, bg, &[]);
1675 pass.draw_indexed(b.index_start..end, 0, 0..1);
1676 }
1677 }
1678 }
1679
1680 self.queue.submit(std::iter::once(encoder.finish()));
1681 }
1682
1683 #[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
1692 #[must_use]
1693 pub fn headless(width: u32, height: u32, scale: f32) -> Option<Self> {
1694 let phys_w = truce_gui::to_physical_px(width, f64::from(scale));
1695 let phys_h = truce_gui::to_physical_px(height, f64::from(scale));
1696
1697 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
1698 desc.backends = wgpu::Backends::PRIMARY;
1699 let instance = wgpu::Instance::new(desc);
1700
1701 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1711 power_preference: wgpu::PowerPreference::HighPerformance,
1712 compatible_surface: None,
1713 force_fallback_adapter: false,
1714 }))
1715 .ok()?;
1716
1717 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1718 label: Some("truce-gpu-headless"),
1719 required_features: wgpu::Features::empty(),
1720 required_limits: wgpu::Limits::downlevel_defaults(),
1721 experimental_features: wgpu::ExperimentalFeatures::default(),
1722 memory_hints: wgpu::MemoryHints::Performance,
1723 trace: wgpu::Trace::Off,
1724 }))
1725 .ok()?;
1726 let device = Arc::new(device);
1727 let queue = Arc::new(queue);
1728
1729 let texture_format = wgpu::TextureFormat::Rgba8Unorm;
1731
1732 let msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
1734 label: Some("msaa"),
1735 size: wgpu::Extent3d {
1736 width: phys_w,
1737 height: phys_h,
1738 depth_or_array_layers: 1,
1739 },
1740 mip_level_count: 1,
1741 sample_count: 4,
1742 dimension: wgpu::TextureDimension::D2,
1743 format: texture_format,
1744 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1745 view_formats: &[],
1746 });
1747 let msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
1748
1749 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1751 label: Some("truce-gpu-shader"),
1752 source: wgpu::ShaderSource::Wgsl(SHADER_SRC.into()),
1753 });
1754
1755 let matrix = ortho_matrix(phys_w as f32, phys_h as f32);
1757 let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1758 label: Some("viewport"),
1759 contents: bytemuck::cast_slice(&matrix),
1760 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1761 });
1762
1763 let viewport_bind_group_layout =
1764 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1765 label: Some("viewport-layout"),
1766 entries: &[wgpu::BindGroupLayoutEntry {
1767 binding: 0,
1768 visibility: wgpu::ShaderStages::VERTEX,
1769 ty: wgpu::BindingType::Buffer {
1770 ty: wgpu::BufferBindingType::Uniform,
1771 has_dynamic_offset: false,
1772 min_binding_size: None,
1773 },
1774 count: None,
1775 }],
1776 });
1777
1778 let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1779 label: Some("viewport-bg"),
1780 layout: &viewport_bind_group_layout,
1781 entries: &[wgpu::BindGroupEntry {
1782 binding: 0,
1783 resource: viewport_buffer.as_entire_binding(),
1784 }],
1785 });
1786
1787 let atlas_texture = device.create_texture(&wgpu::TextureDescriptor {
1789 label: Some("glyph-atlas"),
1790 size: wgpu::Extent3d {
1791 width: ATLAS_SIZE,
1792 height: ATLAS_SIZE,
1793 depth_or_array_layers: 1,
1794 },
1795 mip_level_count: 1,
1796 sample_count: 1,
1797 dimension: wgpu::TextureDimension::D2,
1798 format: wgpu::TextureFormat::R8Unorm,
1799 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1800 view_formats: &[],
1801 });
1802 let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
1803 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1804 mag_filter: wgpu::FilterMode::Linear,
1805 min_filter: wgpu::FilterMode::Linear,
1806 ..Default::default()
1807 });
1808 let tex_bind_group_layout =
1809 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1810 label: Some("tex-layout"),
1811 entries: &[
1812 wgpu::BindGroupLayoutEntry {
1813 binding: 0,
1814 visibility: wgpu::ShaderStages::FRAGMENT,
1815 ty: wgpu::BindingType::Texture {
1816 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1817 view_dimension: wgpu::TextureViewDimension::D2,
1818 multisampled: false,
1819 },
1820 count: None,
1821 },
1822 wgpu::BindGroupLayoutEntry {
1823 binding: 1,
1824 visibility: wgpu::ShaderStages::FRAGMENT,
1825 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1826 count: None,
1827 },
1828 ],
1829 });
1830 let atlas_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1831 label: Some("atlas-bg"),
1832 layout: &tex_bind_group_layout,
1833 entries: &[
1834 wgpu::BindGroupEntry {
1835 binding: 0,
1836 resource: wgpu::BindingResource::TextureView(&atlas_view),
1837 },
1838 wgpu::BindGroupEntry {
1839 binding: 1,
1840 resource: wgpu::BindingResource::Sampler(&sampler),
1841 },
1842 ],
1843 });
1844
1845 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1847 label: Some("truce-gpu-pipeline-layout"),
1848 bind_group_layouts: &[
1849 Some(&viewport_bind_group_layout),
1850 Some(&tex_bind_group_layout),
1851 ],
1852 immediate_size: 0,
1853 });
1854
1855 let vertex_layout = wgpu::VertexBufferLayout {
1856 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
1857 step_mode: wgpu::VertexStepMode::Vertex,
1858 attributes: &[
1859 wgpu::VertexAttribute {
1860 offset: 0,
1861 shader_location: 0,
1862 format: wgpu::VertexFormat::Float32x2,
1863 },
1864 wgpu::VertexAttribute {
1865 offset: 8,
1866 shader_location: 1,
1867 format: wgpu::VertexFormat::Float32x4,
1868 },
1869 wgpu::VertexAttribute {
1870 offset: 24,
1871 shader_location: 2,
1872 format: wgpu::VertexFormat::Float32x2,
1873 },
1874 wgpu::VertexAttribute {
1875 offset: 32,
1876 shader_location: 3,
1877 format: wgpu::VertexFormat::Float32,
1878 },
1879 ],
1880 };
1881
1882 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1883 label: Some("truce-gpu-pipeline"),
1884 layout: Some(&pipeline_layout),
1885 vertex: wgpu::VertexState {
1886 module: &shader,
1887 entry_point: Some("vs_main"),
1888 buffers: &[vertex_layout],
1889 compilation_options: wgpu::PipelineCompilationOptions::default(),
1890 },
1891 fragment: Some(wgpu::FragmentState {
1892 module: &shader,
1893 entry_point: Some("fs_main"),
1894 targets: &[Some(wgpu::ColorTargetState {
1895 format: texture_format,
1896 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1897 write_mask: wgpu::ColorWrites::ALL,
1898 })],
1899 compilation_options: wgpu::PipelineCompilationOptions::default(),
1900 }),
1901 primitive: wgpu::PrimitiveState {
1902 topology: wgpu::PrimitiveTopology::TriangleList,
1903 ..Default::default()
1904 },
1905 depth_stencil: None,
1906 multisample: wgpu::MultisampleState {
1907 count: 4,
1908 mask: !0,
1909 alpha_to_coverage_enabled: false,
1910 },
1911 multiview_mask: None,
1912 cache: None,
1913 });
1914
1915 let font = fontdue::Font::from_bytes(
1916 truce_gui::font::JETBRAINS_MONO,
1917 fontdue::FontSettings::default(),
1918 )
1919 .expect("failed to parse embedded font");
1920
1921 Some(Self {
1922 device,
1923 queue,
1924 surface: None,
1925 surface_config: None,
1926 pipeline,
1927 target_format: texture_format,
1928 msaa_texture: msaa_view,
1929 msaa_width: phys_w,
1930 msaa_height: phys_h,
1931 vertices: Vec::with_capacity(4096),
1932 indices: Vec::with_capacity(8192),
1933 batches: Vec::new(),
1934 glyph_atlas: GlyphAtlas::new(),
1935 font,
1936 atlas_texture,
1937 atlas_bind_group,
1938 tex_bind_group_layout,
1939 sampler,
1940 images: Vec::new(),
1941 viewport_buffer,
1942 viewport_bind_group,
1943 clear_color: None,
1944 present_clear_default: wgpu::Color::BLACK,
1945 width: phys_w,
1946 height: phys_h,
1947 scale,
1948 })
1949 }
1950
1951 pub fn read_pixels(&mut self) -> Vec<u8> {
1961 self.flush_atlas();
1962
1963 let w = self.width;
1964 let h = self.height;
1965 let format = wgpu::TextureFormat::Rgba8Unorm;
1966
1967 let target_texture = self.device.create_texture(&wgpu::TextureDescriptor {
1969 label: Some("offscreen"),
1970 size: wgpu::Extent3d {
1971 width: w,
1972 height: h,
1973 depth_or_array_layers: 1,
1974 },
1975 mip_level_count: 1,
1976 sample_count: 1,
1977 dimension: wgpu::TextureDimension::D2,
1978 format,
1979 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1980 view_formats: &[],
1981 });
1982 let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
1983
1984 if !self.vertices.is_empty() {
1986 self.render_pass(&target_view);
1987 }
1988
1989 let bytes_per_row = (w * 4 + 255) & !255; let readback_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1992 label: Some("readback"),
1993 size: u64::from(bytes_per_row * h),
1994 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1995 mapped_at_creation: false,
1996 });
1997
1998 let mut encoder = self
1999 .device
2000 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2001 label: Some("readback"),
2002 });
2003 encoder.copy_texture_to_buffer(
2004 wgpu::TexelCopyTextureInfo {
2005 texture: &target_texture,
2006 mip_level: 0,
2007 origin: wgpu::Origin3d::ZERO,
2008 aspect: wgpu::TextureAspect::All,
2009 },
2010 wgpu::TexelCopyBufferInfo {
2011 buffer: &readback_buf,
2012 layout: wgpu::TexelCopyBufferLayout {
2013 offset: 0,
2014 bytes_per_row: Some(bytes_per_row),
2015 rows_per_image: None,
2016 },
2017 },
2018 wgpu::Extent3d {
2019 width: w,
2020 height: h,
2021 depth_or_array_layers: 1,
2022 },
2023 );
2024 self.queue.submit(std::iter::once(encoder.finish()));
2025
2026 let buf_slice = readback_buf.slice(..);
2028 let (tx, rx) = std::sync::mpsc::channel();
2029 buf_slice.map_async(wgpu::MapMode::Read, move |result| {
2030 tx.send(result).unwrap();
2031 });
2032 let _ = self.device.poll(wgpu::PollType::Wait {
2033 submission_index: None,
2034 timeout: None,
2035 });
2036 rx.recv().unwrap().expect("buffer map failed");
2037
2038 let mapped = buf_slice.get_mapped_range();
2039 let mut pixels = Vec::with_capacity((w * h * 4) as usize);
2040 for row in 0..h {
2041 let start = (row * bytes_per_row) as usize;
2042 let end = start + (w * 4) as usize;
2043 pixels.extend_from_slice(&mapped[start..end]);
2044 }
2045 drop(mapped);
2046 readback_buf.unmap();
2047
2048 for px in pixels.chunks_exact_mut(4) {
2055 let a = px[3];
2056 if a == 0 || a == 255 {
2057 continue;
2058 }
2059 let a16 = u16::from(a);
2060 px[0] = ((u16::from(px[0]) * 255 + a16 / 2) / a16).min(255) as u8;
2062 px[1] = ((u16::from(px[1]) * 255 + a16 / 2) / a16).min(255) as u8;
2063 px[2] = ((u16::from(px[2]) * 255 + a16 / 2) / a16).min(255) as u8;
2064 }
2065
2066 pixels
2067 }
2068}
2069
2070#[cfg(test)]
2075mod tests {
2076 use super::*;
2077
2078 #[test]
2079 fn vertex_size() {
2080 let size = std::mem::size_of::<Vertex>();
2082 assert!(size > 0, "Vertex should have non-zero size: {size}");
2083 }
2084
2085 #[test]
2091 fn ortho_matrix_maps_origin() {
2092 let m = ortho_matrix(800.0, 600.0);
2093 let x = m[0][0] * 0.0 + m[3][0];
2094 let y = m[1][1] * 0.0 + m[3][1];
2095 assert!((x - (-1.0)).abs() < 1e-6);
2096 assert!((y - 1.0).abs() < 1e-6);
2097 }
2098
2099 #[test]
2100 fn ortho_matrix_maps_bottom_right() {
2101 let m = ortho_matrix(800.0, 600.0);
2102 let x = m[0][0] * 800.0 + m[3][0];
2103 let y = m[1][1] * 600.0 + m[3][1];
2104 assert!((x - 1.0).abs() < 1e-6);
2105 assert!((y - (-1.0)).abs() < 1e-6);
2106 }
2107
2108 #[test]
2109 fn glyph_atlas_shelf_packing() {
2110 let font = fontdue::Font::from_bytes(
2111 truce_gui::font::JETBRAINS_MONO,
2112 fontdue::FontSettings::default(),
2113 )
2114 .unwrap();
2115 let mut atlas = GlyphAtlas::new();
2116
2117 atlas.ensure_glyph(&font, 'A', 14.0);
2119 atlas.ensure_glyph(&font, 'B', 14.0);
2120 atlas.ensure_glyph(&font, 'C', 14.0);
2121
2122 assert_eq!(atlas.glyphs.len(), 3);
2123 assert!(!atlas.pending.is_empty());
2124
2125 atlas.ensure_glyph(&font, 'A', 14.0);
2127 assert_eq!(atlas.glyphs.len(), 3);
2128 }
2129
2130 #[test]
2131 fn lyon_fill_circle_produces_triangles() {
2132 let mut builder = Path::builder();
2133 builder.add_circle(
2134 point(50.0, 50.0),
2135 10.0,
2136 lyon_tessellation::path::Winding::Positive,
2137 );
2138 let path = builder.build();
2139 let mut buffers: VertexBuffers<[f32; 2], u32> = VertexBuffers::new();
2140 let mut tess = FillTessellator::new();
2141 tess.tessellate_path(
2142 &path,
2143 &FillOptions::tolerance(0.5),
2144 &mut BuffersBuilder::new(&mut buffers, |v: FillVertex| {
2145 let p = v.position();
2146 [p.x, p.y]
2147 }),
2148 )
2149 .unwrap();
2150 assert!(buffers.vertices.len() >= 3);
2151 assert!(buffers.indices.len() >= 3);
2152 }
2153
2154 #[test]
2159 #[allow(clippy::too_many_lines, clippy::many_single_char_names)]
2160 fn standalone_pipeline_renders() {
2161 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
2162 desc.backends = wgpu::Backends::PRIMARY;
2163 let instance = wgpu::Instance::new(desc);
2164 let Ok(adapter) =
2165 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
2166 power_preference: wgpu::PowerPreference::HighPerformance,
2167 compatible_surface: None,
2168 force_fallback_adapter: false,
2169 }))
2170 else {
2171 return; };
2173 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
2174 label: Some("standalone-test"),
2175 required_features: wgpu::Features::empty(),
2176 required_limits: wgpu::Limits::downlevel_defaults(),
2177 experimental_features: wgpu::ExperimentalFeatures::default(),
2178 memory_hints: wgpu::MemoryHints::Performance,
2179 trace: wgpu::Trace::Off,
2180 }))
2181 .expect("request_device");
2182 let device = Arc::new(device);
2183 let queue = Arc::new(queue);
2184
2185 let w = 64u32;
2186 let h = 48u32;
2187 let format = wgpu::TextureFormat::Rgba8Unorm;
2188 let mut backend =
2189 WgpuBackend::new(Arc::clone(&device), Arc::clone(&queue), format, w, h, 1.0)
2190 .expect("backend new");
2191
2192 let target = device.create_texture(&wgpu::TextureDescriptor {
2195 label: Some("standalone-target"),
2196 size: wgpu::Extent3d {
2197 width: w,
2198 height: h,
2199 depth_or_array_layers: 1,
2200 },
2201 mip_level_count: 1,
2202 sample_count: 1,
2203 dimension: wgpu::TextureDimension::D2,
2204 format,
2205 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2206 view_formats: &[],
2207 });
2208 let view = target.create_view(&wgpu::TextureViewDescriptor::default());
2209
2210 backend.begin_frame(w, h);
2211 backend.clear(Color::rgb(0.0, 0.0, 0.0));
2212 backend.fill_rect(8.0, 8.0, 16.0, 16.0, Color::rgb(0.0, 1.0, 0.0));
2213 backend.draw_text("x", 20.0, 20.0, 14.0, Color::rgb(1.0, 1.0, 1.0));
2214
2215 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2216 label: Some("standalone-enc"),
2217 });
2218 backend.finish(&mut encoder, &view);
2219
2220 let bytes_per_row = (w * 4 + 255) & !255;
2222 let readback = device.create_buffer(&wgpu::BufferDescriptor {
2223 label: Some("readback"),
2224 size: u64::from(bytes_per_row * h),
2225 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2226 mapped_at_creation: false,
2227 });
2228 encoder.copy_texture_to_buffer(
2229 wgpu::TexelCopyTextureInfo {
2230 texture: &target,
2231 mip_level: 0,
2232 origin: wgpu::Origin3d::ZERO,
2233 aspect: wgpu::TextureAspect::All,
2234 },
2235 wgpu::TexelCopyBufferInfo {
2236 buffer: &readback,
2237 layout: wgpu::TexelCopyBufferLayout {
2238 offset: 0,
2239 bytes_per_row: Some(bytes_per_row),
2240 rows_per_image: None,
2241 },
2242 },
2243 wgpu::Extent3d {
2244 width: w,
2245 height: h,
2246 depth_or_array_layers: 1,
2247 },
2248 );
2249 queue.submit(std::iter::once(encoder.finish()));
2250
2251 let slice = readback.slice(..);
2252 let (tx, rx) = std::sync::mpsc::channel();
2253 slice.map_async(wgpu::MapMode::Read, move |r| {
2254 tx.send(r).unwrap();
2255 });
2256 let _ = device.poll(wgpu::PollType::Wait {
2257 submission_index: None,
2258 timeout: None,
2259 });
2260 rx.recv().unwrap().unwrap();
2261 let mapped = slice.get_mapped_range();
2262
2263 let row_off = 16usize * bytes_per_row as usize;
2265 let px_off = row_off + 16 * 4;
2266 let r = mapped[px_off];
2267 let g = mapped[px_off + 1];
2268 let b = mapped[px_off + 2];
2269 assert!(g > 200, "green rect not rendered: got rgb=({r},{g},{b})");
2270 assert!(
2271 r < 50 && b < 50,
2272 "green rect leaked other channels: rgb=({r},{g},{b})"
2273 );
2274 }
2275}