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