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};
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 format = caps
119 .formats
120 .iter()
121 .copied()
122 .find(wgpu::TextureFormat::is_srgb)
123 .unwrap_or(caps.formats[0]);
124 let present_mode = if vsync {
125 wgpu::PresentMode::AutoVsync
126 } else {
127 wgpu::PresentMode::AutoNoVsync
128 };
129 let capture_copy_src = caps.usages.contains(wgpu::TextureUsages::COPY_SRC);
132 let surface_usage = if capture_copy_src {
133 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC
134 } else {
135 wgpu::TextureUsages::RENDER_ATTACHMENT
136 };
137 let surface_config = wgpu::SurfaceConfiguration {
138 usage: surface_usage,
139 format,
140 width,
141 height,
142 present_mode,
143 desired_maximum_frame_latency: 2,
144 alpha_mode: caps.alpha_modes[0],
145 view_formats: vec![],
146 color_space: wgpu::SurfaceColorSpace::Auto,
147 };
148 surface.configure(&device, &surface_config);
149
150 let camera = Camera2D::new(width as f32, height as f32);
151 let camera_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
152 label: Some("camera.buf"),
153 contents: bytemuck::bytes_of(&CameraUniform {
154 view_proj: camera.view_proj(),
155 }),
156 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
157 });
158 let camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
159 label: Some("camera.bgl"),
160 entries: &[wgpu::BindGroupLayoutEntry {
161 binding: 0,
162 visibility: wgpu::ShaderStages::VERTEX,
163 ty: wgpu::BindingType::Buffer {
164 ty: wgpu::BufferBindingType::Uniform,
165 has_dynamic_offset: false,
166 min_binding_size: None,
167 },
168 count: None,
169 }],
170 });
171 let camera_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
172 label: Some("camera.bg"),
173 layout: &camera_bgl,
174 entries: &[wgpu::BindGroupEntry {
175 binding: 0,
176 resource: camera_buf.as_entire_binding(),
177 }],
178 });
179
180 let camera3d = Camera3D::new(width as f32 / height.max(1) as f32);
181 let camera3d_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
182 label: Some("camera3d.buf"),
183 contents: bytemuck::bytes_of(&CameraUniform {
184 view_proj: camera3d.view_proj(),
185 }),
186 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
187 });
188 let camera3d_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
189 label: Some("camera3d.bg"),
190 layout: &camera_bgl,
191 entries: &[wgpu::BindGroupEntry {
192 binding: 0,
193 resource: camera3d_buf.as_entire_binding(),
194 }],
195 });
196
197 let sample_count = msaa_samples.max(1);
198 let textures = TextureRegistry::new(&device, &queue);
199 let sprites = SpriteBatcher::new(
200 &device,
201 format,
202 &camera_bgl,
203 &textures.layout,
204 sample_count,
205 depth_format,
206 );
207 let primitives =
208 PrimitiveBatcher::new(&device, format, &camera_bgl, sample_count, depth_format);
209 let shapes = ShapeBatcher::new(
210 &device,
211 format,
212 &camera_bgl,
213 &textures.layout,
214 sample_count,
215 depth_format,
216 );
217 let text = TextSystem::new(
218 &device,
219 &queue,
220 format,
221 width,
222 height,
223 sample_count,
224 depth_format,
225 );
226 let (msaa_view, depth_view) =
227 make_attachments(&device, &surface_config, sample_count, depth_format);
228 let meshes = MeshRegistry::new();
229 let mesh_batcher =
230 MeshBatcher::new(&device, format, &camera_bgl, sample_count, depth_format);
231 #[cfg(feature = "vector")]
232 let vector = if adapter
233 .get_downlevel_capabilities()
234 .flags
235 .contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
236 {
237 Some(VectorPass::new(&device, format, width, height))
238 } else {
239 log::error!("vector graphics disabled: adapter has no compute shaders ({backend:?})");
240 None
241 };
242
243 Ok(Self {
244 device,
245 queue,
246 surface,
247 surface_config,
248 window,
249 camera,
250 camera_buf,
251 camera_bg,
252 sprites,
253 primitives,
254 shapes,
255 text,
256 textures,
257 clear_color: [0.0, 0.0, 0.0, 1.0],
258 texture_paths: HashMap::new(),
259 sample_count,
260 depth_format,
261 msaa_view,
262 depth_view,
263 camera3d,
264 camera3d_buf,
265 camera3d_bg,
266 meshes,
267 mesh_batcher,
268 #[cfg(feature = "vector")]
269 vector,
270 capture_copy_src,
271 pending_screenshot: None,
272 })
273 }
274
275 pub fn resize(&mut self, width: u32, height: u32) {
276 if width == 0 || height == 0 {
277 return;
278 }
279 self.surface_config.width = width;
280 self.surface_config.height = height;
281 self.surface.configure(&self.device, &self.surface_config);
282 let (msaa_view, depth_view) = make_attachments(
283 &self.device,
284 &self.surface_config,
285 self.sample_count,
286 self.depth_format,
287 );
288 self.msaa_view = msaa_view;
289 self.depth_view = depth_view;
290 self.camera.resize(width as f32, height as f32);
291 self.camera3d.resize(width as f32, height as f32);
292 self.text.resize(&self.queue, width, height);
293 #[cfg(feature = "vector")]
294 if let Some(vector) = self.vector.as_mut() {
295 vector.resize(&self.device, width, height);
296 }
297 }
298
299 pub fn window(&self) -> &Arc<Window> {
300 &self.window
301 }
302
303 pub fn size(&self) -> (u32, u32) {
304 (self.surface_config.width, self.surface_config.height)
305 }
306
307 pub fn surface_format(&self) -> wgpu::TextureFormat {
308 self.surface_config.format
309 }
310
311 pub fn device(&self) -> &wgpu::Device {
312 &self.device
313 }
314 pub fn queue(&self) -> &wgpu::Queue {
315 &self.queue
316 }
317
318 #[cfg(not(target_arch = "wasm32"))]
322 pub fn load_texture(&mut self, path: impl AsRef<Path>) -> Result<TextureId> {
323 let path = path.as_ref();
324 let id = self.textures.load_file(&self.device, &self.queue, path)?;
325 let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
326 self.texture_paths.insert(canon, id);
327 Ok(id)
328 }
329
330 pub fn load_font_bytes(&mut self, bytes: Vec<u8>) {
332 self.text.load_font_bytes(bytes);
333 }
334
335 pub fn load_texture_bytes(&mut self, bytes: &[u8], label: Option<&str>) -> Result<TextureId> {
338 self.textures
339 .load_bytes(&self.device, &self.queue, bytes, label)
340 }
341
342 pub fn refresh_textures<I, P>(&mut self, changed_paths: I) -> usize
345 where
346 I: IntoIterator<Item = P>,
347 P: AsRef<Path>,
348 {
349 let mut n = 0;
350 for p in changed_paths {
351 let canon = p
352 .as_ref()
353 .canonicalize()
354 .unwrap_or_else(|_| p.as_ref().to_path_buf());
355 if let Some(&id) = self.texture_paths.get(&canon) {
356 match self.textures.reload(&self.device, &self.queue, id, &canon) {
357 Ok(()) => n += 1,
358 Err(e) => log::warn!("reload {} failed: {e:?}", canon.display()),
359 }
360 }
361 }
362 n
363 }
364
365 pub fn create_texture_rgba(
366 &mut self,
367 width: u32,
368 height: u32,
369 rgba: &[u8],
370 label: Option<&str>,
371 ) -> TextureId {
372 self.textures
373 .create_from_rgba(&self.device, &self.queue, width, height, rgba, label)
374 }
375
376 pub fn white_texture(&self) -> TextureId {
377 self.textures.white()
378 }
379
380 pub fn texture_size(&self, id: TextureId) -> Option<[u32; 2]> {
383 self.textures.get(id).map(|t| t.size)
384 }
385
386 pub fn create_mesh(&mut self, vertices: &[MeshVertex], indices: &[u16]) -> MeshId {
389 self.meshes.create(&self.device, vertices, indices)
390 }
391
392 pub fn draw_mesh(&mut self, mesh: MeshId, instance: MeshInstance) {
395 self.mesh_batcher.push(mesh, instance);
396 }
397
398 pub fn request_screenshot(&mut self, path: impl Into<PathBuf>) {
402 if cfg!(target_arch = "wasm32") {
403 log::warn!("screenshot unsupported on the web");
404 return;
405 }
406 if !self.capture_copy_src {
407 log::warn!("screenshot unsupported: surface does not allow COPY_SRC read-back");
408 return;
409 }
410 self.pending_screenshot = Some(path.into());
411 }
412
413 pub fn begin_frame(&mut self) -> Result<Frame> {
414 self.queue.write_buffer(
415 &self.camera_buf,
416 0,
417 bytemuck::bytes_of(&CameraUniform {
418 view_proj: self.camera.view_proj(),
419 }),
420 );
421 self.queue.write_buffer(
422 &self.camera3d_buf,
423 0,
424 bytemuck::bytes_of(&CameraUniform {
425 view_proj: self.camera3d.view_proj(),
426 }),
427 );
428
429 let surface_texture = match self.surface.get_current_texture() {
430 wgpu::CurrentSurfaceTexture::Success(t)
431 | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
432 other => anyhow::bail!("surface unavailable: {other:?}"),
433 };
434 let view = surface_texture
435 .texture
436 .create_view(&wgpu::TextureViewDescriptor::default());
437 let encoder = self
438 .device
439 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
440 label: Some("frame.encoder"),
441 });
442 Ok(Frame {
443 encoder: Some(encoder),
444 view,
445 surface_texture: Some(surface_texture),
446 clear_color: self.clear_color,
447 flushed: false,
448 })
449 }
450
451 pub fn present(&mut self, mut frame: Frame) {
452 if !frame.flushed {
453 self.flush_into(&mut frame);
454 }
455
456 if let Some(encoder) = frame.encoder.take() {
457 self.queue.submit(std::iter::once(encoder.finish()));
458 }
459
460 let shot_path = self.pending_screenshot.take();
463 if let (Some(path), Some(st)) = (&shot_path, frame.surface_texture.as_ref()) {
464 self.save_screenshot(&st.texture, path);
465 }
466
467 if let Some(st) = frame.surface_texture.take() {
468 self.window.pre_present_notify();
469 self.queue.present(st);
470 }
471 }
472
473 #[cfg(target_arch = "wasm32")]
474 fn save_screenshot(&self, _texture: &wgpu::Texture, _path: &Path) {}
475
476 #[cfg(not(target_arch = "wasm32"))]
480 fn save_screenshot(&self, texture: &wgpu::Texture, path: &Path) {
481 match miniscreenshot_wgpu::capture(&self.device, &self.queue, texture) {
482 Ok(shot) => match shot.save(path) {
483 Ok(()) => log::info!("saved screenshot {}", path.display()),
484 Err(e) => log::warn!("screenshot save failed: {e}"),
485 },
486 Err(e) => log::warn!("screenshot capture failed: {e}"),
487 }
488 }
489
490 pub fn flush_pending(&mut self, frame: &mut Frame) {
494 self.flush_into(frame);
495 }
496
497 pub(crate) fn flush_into(&mut self, frame: &mut Frame) {
498 if frame.flushed {
499 return;
500 }
501 let Some(encoder) = frame.encoder.as_mut() else {
502 return;
503 };
504 let mut layers = std::collections::BTreeSet::new();
509 self.sprites.collect_layers(&mut layers);
510 self.shapes.collect_layers(&mut layers);
511 self.primitives.collect_layers(&mut layers);
512
513 self.sprites.upload(&self.device, &self.queue);
517 self.shapes.upload(&self.device, &self.queue);
518 self.primitives.upload(&self.device, &self.queue);
519
520 let (color, resolve) = match self.msaa_view.as_ref() {
523 Some(msaa) => (msaa, Some(&frame.view)),
524 None => (&frame.view, None),
525 };
526 let targets = Targets {
527 color,
528 resolve,
529 depth: self.depth_view.as_ref(),
530 };
531
532 let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
534 label: Some("clear.pass"),
535 color_attachments: &[Some(targets.color_attachment(wgpu::LoadOp::Clear(
536 wgpu::Color {
537 r: frame.clear_color[0] as f64,
538 g: frame.clear_color[1] as f64,
539 b: frame.clear_color[2] as f64,
540 a: frame.clear_color[3] as f64,
541 },
542 )))],
543 depth_stencil_attachment: targets.depth_attachment(wgpu::LoadOp::Clear(1.0)),
544 occlusion_query_set: None,
545 timestamp_writes: None,
546 multiview_mask: None,
547 });
548
549 self.mesh_batcher.draw(
552 &self.device,
553 &self.queue,
554 &self.meshes,
555 encoder,
556 &targets,
557 &self.camera3d_bg,
558 );
559
560 for &layer in &layers {
561 self.sprites
562 .draw_layer(layer, encoder, &targets, &self.camera_bg, &self.textures);
563 self.shapes
564 .draw_layer(layer, encoder, &targets, &self.camera_bg, &self.textures);
565 self.primitives
566 .draw_layer(layer, encoder, &targets, &self.camera_bg);
567 }
568
569 self.text
570 .flush(&self.device, &self.queue, encoder, &targets);
571
572 #[cfg(feature = "vector")]
574 if let Some(vector) = self.vector.as_mut() {
575 vector.render_and_composite(&self.device, &self.queue, encoder, &frame.view);
576 }
577
578 self.sprites.clear();
579 self.shapes.clear();
580 self.primitives.clear();
581 self.mesh_batcher.clear();
582 frame.flushed = true;
583 }
584}
585
586fn make_attachments(
589 device: &wgpu::Device,
590 config: &wgpu::SurfaceConfiguration,
591 sample_count: u32,
592 depth_format: Option<wgpu::TextureFormat>,
593) -> (Option<wgpu::TextureView>, Option<wgpu::TextureView>) {
594 let size = wgpu::Extent3d {
595 width: config.width,
596 height: config.height,
597 depth_or_array_layers: 1,
598 };
599 let msaa_view = (sample_count > 1).then(|| {
600 device
601 .create_texture(&wgpu::TextureDescriptor {
602 label: Some("msaa.color"),
603 size,
604 mip_level_count: 1,
605 sample_count,
606 dimension: wgpu::TextureDimension::D2,
607 format: config.format,
608 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
609 view_formats: &[],
610 })
611 .create_view(&wgpu::TextureViewDescriptor::default())
612 });
613 let depth_view = depth_format.map(|format| {
614 device
615 .create_texture(&wgpu::TextureDescriptor {
616 label: Some("depth"),
617 size,
618 mip_level_count: 1,
619 sample_count,
620 dimension: wgpu::TextureDimension::D2,
621 format,
622 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
623 view_formats: &[],
624 })
625 .create_view(&wgpu::TextureViewDescriptor::default())
626 });
627 (msaa_view, depth_view)
628}