1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use anyhow::{Context, Result};
6use wgpu::util::DeviceExt;
7use winit::window::Window;
8
9use crate::camera::{Camera2D, CameraUniform};
10use crate::camera3d::{Camera, Camera3D};
11use crate::frame::Frame;
12use crate::mesh::{MeshBatcher, MeshId, MeshInstance, MeshRegistry, MeshVertex};
13use crate::primitives::PrimitiveBatcher;
14use crate::shapes::ShapeBatcher;
15use crate::sprite::SpriteBatcher;
16use crate::target::Targets;
17use crate::text::TextSystem;
18use crate::texture::{TextureId, TextureRegistry, TextureWrap};
19#[cfg(feature = "vector")]
20use crate::vector::VectorPass;
21
22pub struct Graphics {
23 pub(crate) device: wgpu::Device,
24 pub(crate) queue: wgpu::Queue,
25 surface: wgpu::Surface<'static>,
26 surface_config: wgpu::SurfaceConfiguration,
27 window: Arc<Window>,
28 pub camera: Camera2D,
29 camera_buf: wgpu::Buffer,
30 pub(crate) camera_bg: wgpu::BindGroup,
31 pub(crate) sprites: SpriteBatcher,
32 pub(crate) primitives: PrimitiveBatcher,
33 pub(crate) shapes: ShapeBatcher,
34 pub(crate) text: TextSystem,
35 pub(crate) textures: TextureRegistry,
36 pub(crate) clear_color: [f32; 4],
37 texture_paths: HashMap<PathBuf, TextureId>,
38 sample_count: u32,
39 depth_format: Option<wgpu::TextureFormat>,
40 msaa_view: Option<wgpu::TextureView>,
42 depth_view: Option<wgpu::TextureView>,
44 pub camera3d: Camera3D,
46 camera3d_buf: wgpu::Buffer,
47 camera3d_bg: wgpu::BindGroup,
48 meshes: MeshRegistry,
49 mesh_batcher: MeshBatcher,
50 #[cfg(feature = "vector")]
53 pub(crate) vector: Option<VectorPass>,
54 capture_copy_src: bool,
56 pending_screenshot: Option<PathBuf>,
58}
59
60impl Graphics {
61 pub async fn new(
62 window: Arc<Window>,
63 vsync: bool,
64 depth_format: Option<wgpu::TextureFormat>,
65 msaa_samples: u32,
66 ) -> Result<Self> {
67 let size = window.inner_size();
68 let (width, height) = (size.width.max(1), size.height.max(1));
69
70 let mut idesc = wgpu::InstanceDescriptor::new_without_display_handle();
71 idesc.backends = if cfg!(target_arch = "wasm32") {
73 wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL
74 } else {
75 wgpu::Backends::PRIMARY
76 };
77 let instance = wgpu::util::new_instance_with_webgpu_detection(idesc).await;
81 let surface = instance
82 .create_surface(window.clone())
83 .context("create_surface")?;
84 let adapter = instance
85 .request_adapter(&wgpu::RequestAdapterOptions {
86 power_preference: wgpu::PowerPreference::HighPerformance,
87 compatible_surface: Some(&surface),
88 force_fallback_adapter: false,
89 apply_limit_buckets: false,
90 })
91 .await
92 .context("no compatible adapter")?;
93
94 let backend = adapter.get_info().backend;
95 log::info!("wgpu adapter: {:?} ({backend:?})", adapter.get_info().name);
96 let required_limits = if backend == wgpu::Backend::Gl {
99 wgpu::Limits::downlevel_webgl2_defaults()
100 } else if cfg!(feature = "vector") {
101 wgpu::Limits::default()
102 } else {
103 wgpu::Limits::downlevel_defaults()
104 }
105 .using_resolution(adapter.limits());
106 let (device, queue) = adapter
107 .request_device(&wgpu::DeviceDescriptor {
108 label: Some("toolkit.device"),
109 required_features: wgpu::Features::empty(),
110 required_limits,
111 memory_hints: wgpu::MemoryHints::Performance,
112 ..Default::default()
113 })
114 .await
115 .context("request_device")?;
116
117 let caps = surface.get_capabilities(&adapter);
118 let surface_format = caps
119 .formats
120 .iter()
121 .copied()
122 .find(wgpu::TextureFormat::is_srgb)
123 .unwrap_or(caps.formats[0]);
124 let format = surface_format.add_srgb_suffix();
128 let present_mode = if vsync {
129 wgpu::PresentMode::AutoVsync
130 } else {
131 wgpu::PresentMode::AutoNoVsync
132 };
133 let capture_copy_src = caps.usages.contains(wgpu::TextureUsages::COPY_SRC);
136 let surface_usage = if capture_copy_src {
137 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC
138 } else {
139 wgpu::TextureUsages::RENDER_ATTACHMENT
140 };
141 let surface_config = wgpu::SurfaceConfiguration {
142 usage: surface_usage,
143 format: surface_format,
144 width,
145 height,
146 present_mode,
147 desired_maximum_frame_latency: 2,
148 alpha_mode: caps.alpha_modes[0],
149 view_formats: if format == surface_format {
150 vec![]
151 } else {
152 vec![format]
153 },
154 color_space: wgpu::SurfaceColorSpace::Auto,
155 };
156 surface.configure(&device, &surface_config);
157
158 let camera = Camera2D::new(width as f32, height as f32);
159 let camera_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
160 label: Some("camera.buf"),
161 contents: bytemuck::bytes_of(&CameraUniform {
162 view_proj: camera.view_proj(),
163 }),
164 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
165 });
166 let camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
167 label: Some("camera.bgl"),
168 entries: &[wgpu::BindGroupLayoutEntry {
169 binding: 0,
170 visibility: wgpu::ShaderStages::VERTEX,
171 ty: wgpu::BindingType::Buffer {
172 ty: wgpu::BufferBindingType::Uniform,
173 has_dynamic_offset: false,
174 min_binding_size: None,
175 },
176 count: None,
177 }],
178 });
179 let camera_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
180 label: Some("camera.bg"),
181 layout: &camera_bgl,
182 entries: &[wgpu::BindGroupEntry {
183 binding: 0,
184 resource: camera_buf.as_entire_binding(),
185 }],
186 });
187
188 let camera3d = Camera3D::new(width as f32 / height.max(1) as f32);
189 let camera3d_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
190 label: Some("camera3d.buf"),
191 contents: bytemuck::bytes_of(&CameraUniform {
192 view_proj: camera3d.view_proj(),
193 }),
194 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
195 });
196 let camera3d_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
197 label: Some("camera3d.bg"),
198 layout: &camera_bgl,
199 entries: &[wgpu::BindGroupEntry {
200 binding: 0,
201 resource: camera3d_buf.as_entire_binding(),
202 }],
203 });
204
205 let sample_count = msaa_samples.max(1);
206 let textures = TextureRegistry::new(&device, &queue);
207 let sprites = SpriteBatcher::new(
208 &device,
209 format,
210 &camera_bgl,
211 &textures.layout,
212 sample_count,
213 depth_format,
214 );
215 let primitives =
216 PrimitiveBatcher::new(&device, format, &camera_bgl, sample_count, depth_format);
217 let shapes = ShapeBatcher::new(
218 &device,
219 format,
220 &camera_bgl,
221 &textures.layout,
222 sample_count,
223 depth_format,
224 );
225 let text = TextSystem::new(
226 &device,
227 &queue,
228 format,
229 width,
230 height,
231 sample_count,
232 depth_format,
233 );
234 let (msaa_view, depth_view) =
235 make_attachments(&device, &surface_config, sample_count, depth_format);
236 let meshes = MeshRegistry::new();
237 let mesh_batcher =
238 MeshBatcher::new(&device, format, &camera_bgl, sample_count, depth_format);
239 #[cfg(feature = "vector")]
240 let vector = if adapter
241 .get_downlevel_capabilities()
242 .flags
243 .contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
244 {
245 Some(VectorPass::new(&device, format, width, height))
246 } else {
247 log::error!("vector graphics disabled: adapter has no compute shaders ({backend:?})");
248 None
249 };
250
251 Ok(Self {
252 device,
253 queue,
254 surface,
255 surface_config,
256 window,
257 camera,
258 camera_buf,
259 camera_bg,
260 sprites,
261 primitives,
262 shapes,
263 text,
264 textures,
265 clear_color: [0.0, 0.0, 0.0, 1.0],
266 texture_paths: HashMap::new(),
267 sample_count,
268 depth_format,
269 msaa_view,
270 depth_view,
271 camera3d,
272 camera3d_buf,
273 camera3d_bg,
274 meshes,
275 mesh_batcher,
276 #[cfg(feature = "vector")]
277 vector,
278 capture_copy_src,
279 pending_screenshot: None,
280 })
281 }
282
283 pub fn resize(&mut self, width: u32, height: u32) {
284 if width == 0 || height == 0 {
285 return;
286 }
287 self.surface_config.width = width;
288 self.surface_config.height = height;
289 self.surface.configure(&self.device, &self.surface_config);
290 let (msaa_view, depth_view) = make_attachments(
291 &self.device,
292 &self.surface_config,
293 self.sample_count,
294 self.depth_format,
295 );
296 self.msaa_view = msaa_view;
297 self.depth_view = depth_view;
298 self.camera.resize(width as f32, height as f32);
299 self.camera3d.resize(width as f32, height as f32);
300 self.text.resize(&self.queue, width, height);
301 #[cfg(feature = "vector")]
302 if let Some(vector) = self.vector.as_mut() {
303 vector.resize(&self.device, width, height);
304 }
305 }
306
307 pub fn window(&self) -> &Arc<Window> {
308 &self.window
309 }
310
311 pub fn size(&self) -> (u32, u32) {
312 (self.surface_config.width, self.surface_config.height)
313 }
314
315 pub fn surface_format(&self) -> wgpu::TextureFormat {
318 render_format(&self.surface_config)
319 }
320
321 pub fn device(&self) -> &wgpu::Device {
322 &self.device
323 }
324 pub fn queue(&self) -> &wgpu::Queue {
325 &self.queue
326 }
327
328 #[cfg(not(target_arch = "wasm32"))]
332 pub fn load_texture(&mut self, path: impl AsRef<Path>) -> Result<TextureId> {
333 let path = path.as_ref();
334 let id = self.textures.load_file(&self.device, &self.queue, path)?;
335 let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
336 self.texture_paths.insert(canon, id);
337 Ok(id)
338 }
339
340 pub fn measure_text(&mut self, s: &str, size_px: f32) -> [f32; 2] {
345 self.text.measure(s, size_px)
346 }
347
348 pub fn load_font_bytes(&mut self, bytes: Vec<u8>) {
350 self.text.load_font_bytes(bytes);
351 }
352
353 pub fn load_texture_bytes(&mut self, bytes: &[u8], label: Option<&str>) -> Result<TextureId> {
356 self.textures
357 .load_bytes(&self.device, &self.queue, bytes, label)
358 }
359
360 pub fn refresh_textures<I, P>(&mut self, changed_paths: I) -> usize
363 where
364 I: IntoIterator<Item = P>,
365 P: AsRef<Path>,
366 {
367 let mut n = 0;
368 for p in changed_paths {
369 let canon = p
370 .as_ref()
371 .canonicalize()
372 .unwrap_or_else(|_| p.as_ref().to_path_buf());
373 if let Some(&id) = self.texture_paths.get(&canon) {
374 match self.textures.reload(&self.device, &self.queue, id, &canon) {
375 Ok(()) => n += 1,
376 Err(e) => log::warn!("reload {} failed: {e:?}", canon.display()),
377 }
378 }
379 }
380 n
381 }
382
383 pub fn create_texture_rgba(
384 &mut self,
385 width: u32,
386 height: u32,
387 rgba: &[u8],
388 label: Option<&str>,
389 ) -> TextureId {
390 self.textures
391 .create_from_rgba(&self.device, &self.queue, width, height, rgba, label)
392 }
393
394 pub fn update_texture_rgba(&mut self, id: TextureId, rgba: &[u8]) {
399 if !self.textures.write_rgba(&self.queue, id, rgba) {
400 log::warn!("update_texture_rgba: unknown {id:?} or wrong data size");
401 }
402 }
403
404 pub fn white_texture(&self) -> TextureId {
405 self.textures.white()
406 }
407
408 pub fn set_texture_wrap(&mut self, id: TextureId, wrap: TextureWrap) {
417 if !self.textures.set_wrap(&self.device, id, wrap) {
418 log::warn!("set_texture_wrap: unknown {id:?}");
419 }
420 }
421
422 pub fn texture_wrap(&self, id: TextureId) -> Option<TextureWrap> {
424 self.textures.get(id).map(|t| t.wrap)
425 }
426
427 pub fn texture_size(&self, id: TextureId) -> Option<[u32; 2]> {
430 self.textures.get(id).map(|t| t.size)
431 }
432
433 pub fn create_mesh(&mut self, vertices: &[MeshVertex], indices: &[u16]) -> MeshId {
436 self.meshes.create(&self.device, vertices, indices)
437 }
438
439 pub fn draw_mesh(&mut self, mesh: MeshId, instance: MeshInstance) {
442 self.mesh_batcher.push(mesh, instance);
443 }
444
445 pub fn request_screenshot(&mut self, path: impl Into<PathBuf>) {
449 if cfg!(target_arch = "wasm32") {
450 log::warn!("screenshot unsupported on the web");
451 return;
452 }
453 if !self.capture_copy_src {
454 log::warn!("screenshot unsupported: surface does not allow COPY_SRC read-back");
455 return;
456 }
457 self.pending_screenshot = Some(path.into());
458 }
459
460 pub fn begin_frame(&mut self) -> Result<Frame> {
461 self.queue.write_buffer(
462 &self.camera_buf,
463 0,
464 bytemuck::bytes_of(&CameraUniform {
465 view_proj: self.camera.view_proj(),
466 }),
467 );
468 self.queue.write_buffer(
469 &self.camera3d_buf,
470 0,
471 bytemuck::bytes_of(&CameraUniform {
472 view_proj: self.camera3d.view_proj(),
473 }),
474 );
475
476 let surface_texture = match self.surface.get_current_texture() {
477 wgpu::CurrentSurfaceTexture::Success(t)
478 | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
479 other => anyhow::bail!("surface unavailable: {other:?}"),
480 };
481 let view = surface_texture
482 .texture
483 .create_view(&wgpu::TextureViewDescriptor {
484 format: Some(render_format(&self.surface_config)),
485 ..Default::default()
486 });
487 let encoder = self
488 .device
489 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
490 label: Some("frame.encoder"),
491 });
492 Ok(Frame {
493 encoder: Some(encoder),
494 view,
495 surface_texture: Some(surface_texture),
496 clear_color: self.clear_color,
497 flushed: false,
498 })
499 }
500
501 pub fn present(&mut self, mut frame: Frame) {
502 if !frame.flushed {
503 self.flush_into(&mut frame);
504 }
505
506 if let Some(encoder) = frame.encoder.take() {
507 self.queue.submit(std::iter::once(encoder.finish()));
508 }
509
510 let shot_path = self.pending_screenshot.take();
513 if let (Some(path), Some(st)) = (&shot_path, frame.surface_texture.as_ref()) {
514 self.save_screenshot(&st.texture, path);
515 }
516
517 if let Some(st) = frame.surface_texture.take() {
518 self.window.pre_present_notify();
519 self.queue.present(st);
520 }
521 }
522
523 #[cfg(target_arch = "wasm32")]
524 fn save_screenshot(&self, _texture: &wgpu::Texture, _path: &Path) {}
525
526 #[cfg(not(target_arch = "wasm32"))]
530 fn save_screenshot(&self, texture: &wgpu::Texture, path: &Path) {
531 match miniscreenshot_wgpu::capture(&self.device, &self.queue, texture) {
532 Ok(shot) => match shot.save(path) {
533 Ok(()) => log::info!("saved screenshot {}", path.display()),
534 Err(e) => log::warn!("screenshot save failed: {e}"),
535 },
536 Err(e) => log::warn!("screenshot capture failed: {e}"),
537 }
538 }
539
540 pub fn flush_pending(&mut self, frame: &mut Frame) {
544 self.flush_into(frame);
545 }
546
547 pub(crate) fn flush_into(&mut self, frame: &mut Frame) {
548 if frame.flushed {
549 return;
550 }
551 let Some(encoder) = frame.encoder.as_mut() else {
552 return;
553 };
554 let mut layers = std::collections::BTreeSet::new();
558 self.sprites.collect_layers(&mut layers);
559 self.shapes.collect_layers(&mut layers);
560 self.primitives.collect_layers(&mut layers);
561 self.text.collect_layers(&mut layers);
562
563 self.sprites.upload(&self.device, &self.queue);
567 self.shapes.upload(&self.device, &self.queue);
568 self.primitives.upload(&self.device, &self.queue);
569 self.text.prepare(&self.device, &self.queue);
570
571 let (color, resolve) = match self.msaa_view.as_ref() {
574 Some(msaa) => (msaa, Some(&frame.view)),
575 None => (&frame.view, None),
576 };
577 let targets = Targets {
578 color,
579 resolve,
580 depth: self.depth_view.as_ref(),
581 };
582
583 let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
585 label: Some("clear.pass"),
586 color_attachments: &[Some(targets.color_attachment(wgpu::LoadOp::Clear(
587 wgpu::Color {
588 r: srgb_to_linear(frame.clear_color[0]),
590 g: srgb_to_linear(frame.clear_color[1]),
591 b: srgb_to_linear(frame.clear_color[2]),
592 a: frame.clear_color[3] as f64,
593 },
594 )))],
595 depth_stencil_attachment: targets.depth_attachment(wgpu::LoadOp::Clear(1.0)),
596 occlusion_query_set: None,
597 timestamp_writes: None,
598 multiview_mask: None,
599 });
600
601 self.mesh_batcher.draw(
604 &self.device,
605 &self.queue,
606 &self.meshes,
607 encoder,
608 &targets,
609 &self.camera3d_bg,
610 );
611
612 for &layer in &layers {
613 self.sprites
614 .draw_layer(layer, encoder, &targets, &self.camera_bg, &self.textures);
615 self.shapes
616 .draw_layer(layer, encoder, &targets, &self.camera_bg, &self.textures);
617 self.primitives
618 .draw_layer(layer, encoder, &targets, &self.camera_bg);
619 self.text.draw_layer(Some(layer), encoder, &targets);
620 }
621
622 self.text.draw_layer(None, encoder, &targets);
623 self.text.finish();
624
625 #[cfg(feature = "vector")]
627 if let Some(vector) = self.vector.as_mut() {
628 vector.render_and_composite(&self.device, &self.queue, encoder, &frame.view);
629 }
630
631 self.sprites.clear();
632 self.shapes.clear();
633 self.primitives.clear();
634 self.mesh_batcher.clear();
635 frame.flushed = true;
636 }
637}
638
639fn make_attachments(
642 device: &wgpu::Device,
643 config: &wgpu::SurfaceConfiguration,
644 sample_count: u32,
645 depth_format: Option<wgpu::TextureFormat>,
646) -> (Option<wgpu::TextureView>, Option<wgpu::TextureView>) {
647 let size = wgpu::Extent3d {
648 width: config.width,
649 height: config.height,
650 depth_or_array_layers: 1,
651 };
652 let msaa_view = (sample_count > 1).then(|| {
653 device
654 .create_texture(&wgpu::TextureDescriptor {
655 label: Some("msaa.color"),
656 size,
657 mip_level_count: 1,
658 sample_count,
659 dimension: wgpu::TextureDimension::D2,
660 format: render_format(config),
661 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
662 view_formats: &[],
663 })
664 .create_view(&wgpu::TextureViewDescriptor::default())
665 });
666 let depth_view = depth_format.map(|format| {
667 device
668 .create_texture(&wgpu::TextureDescriptor {
669 label: Some("depth"),
670 size,
671 mip_level_count: 1,
672 sample_count,
673 dimension: wgpu::TextureDimension::D2,
674 format,
675 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
676 view_formats: &[],
677 })
678 .create_view(&wgpu::TextureViewDescriptor::default())
679 });
680 (msaa_view, depth_view)
681}
682
683fn render_format(config: &wgpu::SurfaceConfiguration) -> wgpu::TextureFormat {
686 if config.format.is_srgb() {
687 return config.format;
688 }
689 config
690 .view_formats
691 .iter()
692 .copied()
693 .find(wgpu::TextureFormat::is_srgb)
694 .unwrap_or(config.format)
695}
696
697fn srgb_to_linear(c: f32) -> f64 {
699 let c = f64::from(c);
700 if c <= 0.04045 {
701 c / 12.92
702 } else {
703 ((c + 0.055) / 1.055).powf(2.4)
704 }
705}