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