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