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::sprite::SpriteBatcher;
15use crate::target::Targets;
16use crate::text::TextSystem;
17use crate::texture::{TextureId, TextureRegistry};
18#[cfg(feature = "vector")]
19use crate::vector::VectorPass;
20
21pub struct Graphics {
22 pub(crate) device: wgpu::Device,
23 pub(crate) queue: wgpu::Queue,
24 surface: wgpu::Surface<'static>,
25 surface_config: wgpu::SurfaceConfiguration,
26 window: Arc<Window>,
27 pub camera: Camera2D,
28 camera_buf: wgpu::Buffer,
29 pub(crate) camera_bg: wgpu::BindGroup,
30 pub(crate) sprites: SpriteBatcher,
31 pub(crate) primitives: PrimitiveBatcher,
32 pub(crate) text: TextSystem,
33 pub(crate) textures: TextureRegistry,
34 pub(crate) clear_color: [f32; 4],
35 texture_paths: HashMap<PathBuf, TextureId>,
36 sample_count: u32,
37 depth_format: Option<wgpu::TextureFormat>,
38 msaa_view: Option<wgpu::TextureView>,
40 depth_view: Option<wgpu::TextureView>,
42 pub camera3d: Camera3D,
44 camera3d_buf: wgpu::Buffer,
45 camera3d_bg: wgpu::BindGroup,
46 meshes: MeshRegistry,
47 mesh_batcher: MeshBatcher,
48 #[cfg(feature = "vector")]
51 pub(crate) vector: Option<VectorPass>,
52 capture_copy_src: bool,
54 pending_screenshot: Option<PathBuf>,
56}
57
58impl Graphics {
59 pub async fn new(
60 window: Arc<Window>,
61 vsync: bool,
62 depth_format: Option<wgpu::TextureFormat>,
63 msaa_samples: u32,
64 ) -> Result<Self> {
65 let size = window.inner_size();
66 let (width, height) = (size.width.max(1), size.height.max(1));
67
68 let mut idesc = wgpu::InstanceDescriptor::new_without_display_handle();
69 idesc.backends = if cfg!(target_arch = "wasm32") {
71 wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL
72 } else {
73 wgpu::Backends::PRIMARY
74 };
75 let instance = wgpu::util::new_instance_with_webgpu_detection(idesc).await;
79 let surface = instance
80 .create_surface(window.clone())
81 .context("create_surface")?;
82 let adapter = instance
83 .request_adapter(&wgpu::RequestAdapterOptions {
84 power_preference: wgpu::PowerPreference::HighPerformance,
85 compatible_surface: Some(&surface),
86 force_fallback_adapter: false,
87 apply_limit_buckets: false,
88 })
89 .await
90 .context("no compatible adapter")?;
91
92 let backend = adapter.get_info().backend;
93 log::info!("wgpu adapter: {:?} ({backend:?})", adapter.get_info().name);
94 let required_limits = if backend == wgpu::Backend::Gl {
97 wgpu::Limits::downlevel_webgl2_defaults()
98 } else if cfg!(feature = "vector") {
99 wgpu::Limits::default()
100 } else {
101 wgpu::Limits::downlevel_defaults()
102 }
103 .using_resolution(adapter.limits());
104 let (device, queue) = adapter
105 .request_device(&wgpu::DeviceDescriptor {
106 label: Some("toolkit.device"),
107 required_features: wgpu::Features::empty(),
108 required_limits,
109 memory_hints: wgpu::MemoryHints::Performance,
110 ..Default::default()
111 })
112 .await
113 .context("request_device")?;
114
115 let caps = surface.get_capabilities(&adapter);
116 let format = caps
117 .formats
118 .iter()
119 .copied()
120 .find(wgpu::TextureFormat::is_srgb)
121 .unwrap_or(caps.formats[0]);
122 let present_mode = if vsync {
123 wgpu::PresentMode::AutoVsync
124 } else {
125 wgpu::PresentMode::AutoNoVsync
126 };
127 let capture_copy_src = caps.usages.contains(wgpu::TextureUsages::COPY_SRC);
130 let surface_usage = if capture_copy_src {
131 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC
132 } else {
133 wgpu::TextureUsages::RENDER_ATTACHMENT
134 };
135 let surface_config = wgpu::SurfaceConfiguration {
136 usage: surface_usage,
137 format,
138 width,
139 height,
140 present_mode,
141 desired_maximum_frame_latency: 2,
142 alpha_mode: caps.alpha_modes[0],
143 view_formats: vec![],
144 color_space: wgpu::SurfaceColorSpace::Auto,
145 };
146 surface.configure(&device, &surface_config);
147
148 let camera = Camera2D::new(width as f32, height as f32);
149 let camera_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
150 label: Some("camera.buf"),
151 contents: bytemuck::bytes_of(&CameraUniform {
152 view_proj: camera.view_proj(),
153 }),
154 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
155 });
156 let camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
157 label: Some("camera.bgl"),
158 entries: &[wgpu::BindGroupLayoutEntry {
159 binding: 0,
160 visibility: wgpu::ShaderStages::VERTEX,
161 ty: wgpu::BindingType::Buffer {
162 ty: wgpu::BufferBindingType::Uniform,
163 has_dynamic_offset: false,
164 min_binding_size: None,
165 },
166 count: None,
167 }],
168 });
169 let camera_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
170 label: Some("camera.bg"),
171 layout: &camera_bgl,
172 entries: &[wgpu::BindGroupEntry {
173 binding: 0,
174 resource: camera_buf.as_entire_binding(),
175 }],
176 });
177
178 let camera3d = Camera3D::new(width as f32 / height.max(1) as f32);
179 let camera3d_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
180 label: Some("camera3d.buf"),
181 contents: bytemuck::bytes_of(&CameraUniform {
182 view_proj: camera3d.view_proj(),
183 }),
184 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
185 });
186 let camera3d_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
187 label: Some("camera3d.bg"),
188 layout: &camera_bgl,
189 entries: &[wgpu::BindGroupEntry {
190 binding: 0,
191 resource: camera3d_buf.as_entire_binding(),
192 }],
193 });
194
195 let sample_count = msaa_samples.max(1);
196 let textures = TextureRegistry::new(&device, &queue);
197 let sprites = SpriteBatcher::new(
198 &device,
199 format,
200 &camera_bgl,
201 &textures.layout,
202 sample_count,
203 depth_format,
204 );
205 let primitives =
206 PrimitiveBatcher::new(&device, format, &camera_bgl, sample_count, depth_format);
207 let text = TextSystem::new(
208 &device,
209 &queue,
210 format,
211 width,
212 height,
213 sample_count,
214 depth_format,
215 );
216 let (msaa_view, depth_view) =
217 make_attachments(&device, &surface_config, sample_count, depth_format);
218 let meshes = MeshRegistry::new();
219 let mesh_batcher =
220 MeshBatcher::new(&device, format, &camera_bgl, sample_count, depth_format);
221 #[cfg(feature = "vector")]
222 let vector = if adapter
223 .get_downlevel_capabilities()
224 .flags
225 .contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
226 {
227 Some(VectorPass::new(&device, format, width, height))
228 } else {
229 log::error!("vector graphics disabled: adapter has no compute shaders ({backend:?})");
230 None
231 };
232
233 Ok(Self {
234 device,
235 queue,
236 surface,
237 surface_config,
238 window,
239 camera,
240 camera_buf,
241 camera_bg,
242 sprites,
243 primitives,
244 text,
245 textures,
246 clear_color: [0.0, 0.0, 0.0, 1.0],
247 texture_paths: HashMap::new(),
248 sample_count,
249 depth_format,
250 msaa_view,
251 depth_view,
252 camera3d,
253 camera3d_buf,
254 camera3d_bg,
255 meshes,
256 mesh_batcher,
257 #[cfg(feature = "vector")]
258 vector,
259 capture_copy_src,
260 pending_screenshot: None,
261 })
262 }
263
264 pub fn resize(&mut self, width: u32, height: u32) {
265 if width == 0 || height == 0 {
266 return;
267 }
268 self.surface_config.width = width;
269 self.surface_config.height = height;
270 self.surface.configure(&self.device, &self.surface_config);
271 let (msaa_view, depth_view) = make_attachments(
272 &self.device,
273 &self.surface_config,
274 self.sample_count,
275 self.depth_format,
276 );
277 self.msaa_view = msaa_view;
278 self.depth_view = depth_view;
279 self.camera.resize(width as f32, height as f32);
280 self.camera3d.resize(width as f32, height as f32);
281 self.text.resize(&self.queue, width, height);
282 #[cfg(feature = "vector")]
283 if let Some(vector) = self.vector.as_mut() {
284 vector.resize(&self.device, width, height);
285 }
286 }
287
288 pub fn window(&self) -> &Arc<Window> {
289 &self.window
290 }
291
292 pub fn size(&self) -> (u32, u32) {
293 (self.surface_config.width, self.surface_config.height)
294 }
295
296 pub fn surface_format(&self) -> wgpu::TextureFormat {
297 self.surface_config.format
298 }
299
300 pub fn device(&self) -> &wgpu::Device {
301 &self.device
302 }
303 pub fn queue(&self) -> &wgpu::Queue {
304 &self.queue
305 }
306
307 #[cfg(not(target_arch = "wasm32"))]
311 pub fn load_texture(&mut self, path: impl AsRef<Path>) -> Result<TextureId> {
312 let path = path.as_ref();
313 let id = self.textures.load_file(&self.device, &self.queue, path)?;
314 let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
315 self.texture_paths.insert(canon, id);
316 Ok(id)
317 }
318
319 pub fn load_font_bytes(&mut self, bytes: Vec<u8>) {
321 self.text.load_font_bytes(bytes);
322 }
323
324 pub fn load_texture_bytes(&mut self, bytes: &[u8], label: Option<&str>) -> Result<TextureId> {
327 self.textures
328 .load_bytes(&self.device, &self.queue, bytes, label)
329 }
330
331 pub fn refresh_textures<I, P>(&mut self, changed_paths: I) -> usize
334 where
335 I: IntoIterator<Item = P>,
336 P: AsRef<Path>,
337 {
338 let mut n = 0;
339 for p in changed_paths {
340 let canon = p
341 .as_ref()
342 .canonicalize()
343 .unwrap_or_else(|_| p.as_ref().to_path_buf());
344 if let Some(&id) = self.texture_paths.get(&canon) {
345 match self.textures.reload(&self.device, &self.queue, id, &canon) {
346 Ok(()) => n += 1,
347 Err(e) => log::warn!("reload {} failed: {e:?}", canon.display()),
348 }
349 }
350 }
351 n
352 }
353
354 pub fn create_texture_rgba(
355 &mut self,
356 width: u32,
357 height: u32,
358 rgba: &[u8],
359 label: Option<&str>,
360 ) -> TextureId {
361 self.textures
362 .create_from_rgba(&self.device, &self.queue, width, height, rgba, label)
363 }
364
365 pub fn white_texture(&self) -> TextureId {
366 self.textures.white()
367 }
368
369 pub fn create_mesh(&mut self, vertices: &[MeshVertex], indices: &[u16]) -> MeshId {
372 self.meshes.create(&self.device, vertices, indices)
373 }
374
375 pub fn draw_mesh(&mut self, mesh: MeshId, instance: MeshInstance) {
378 self.mesh_batcher.push(mesh, instance);
379 }
380
381 pub fn request_screenshot(&mut self, path: impl Into<PathBuf>) {
385 if cfg!(target_arch = "wasm32") {
386 log::warn!("screenshot unsupported on the web");
387 return;
388 }
389 if !self.capture_copy_src {
390 log::warn!("screenshot unsupported: surface does not allow COPY_SRC read-back");
391 return;
392 }
393 self.pending_screenshot = Some(path.into());
394 }
395
396 pub fn begin_frame(&mut self) -> Result<Frame> {
397 self.queue.write_buffer(
398 &self.camera_buf,
399 0,
400 bytemuck::bytes_of(&CameraUniform {
401 view_proj: self.camera.view_proj(),
402 }),
403 );
404 self.queue.write_buffer(
405 &self.camera3d_buf,
406 0,
407 bytemuck::bytes_of(&CameraUniform {
408 view_proj: self.camera3d.view_proj(),
409 }),
410 );
411
412 let surface_texture = match self.surface.get_current_texture() {
413 wgpu::CurrentSurfaceTexture::Success(t)
414 | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
415 other => anyhow::bail!("surface unavailable: {other:?}"),
416 };
417 let view = surface_texture
418 .texture
419 .create_view(&wgpu::TextureViewDescriptor::default());
420 let encoder = self
421 .device
422 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
423 label: Some("frame.encoder"),
424 });
425 Ok(Frame {
426 encoder: Some(encoder),
427 view,
428 surface_texture: Some(surface_texture),
429 clear_color: self.clear_color,
430 flushed: false,
431 })
432 }
433
434 pub fn present(&mut self, mut frame: Frame) {
435 if !frame.flushed {
436 self.flush_into(&mut frame);
437 }
438
439 if let Some(encoder) = frame.encoder.take() {
440 self.queue.submit(std::iter::once(encoder.finish()));
441 }
442
443 let shot_path = self.pending_screenshot.take();
446 if let (Some(path), Some(st)) = (&shot_path, frame.surface_texture.as_ref()) {
447 self.save_screenshot(&st.texture, path);
448 }
449
450 if let Some(st) = frame.surface_texture.take() {
451 self.window.pre_present_notify();
452 self.queue.present(st);
453 }
454 }
455
456 #[cfg(target_arch = "wasm32")]
457 fn save_screenshot(&self, _texture: &wgpu::Texture, _path: &Path) {}
458
459 #[cfg(not(target_arch = "wasm32"))]
463 fn save_screenshot(&self, texture: &wgpu::Texture, path: &Path) {
464 match miniscreenshot_wgpu::capture(&self.device, &self.queue, texture) {
465 Ok(shot) => match shot.save(path) {
466 Ok(()) => log::info!("saved screenshot {}", path.display()),
467 Err(e) => log::warn!("screenshot save failed: {e}"),
468 },
469 Err(e) => log::warn!("screenshot capture failed: {e}"),
470 }
471 }
472
473 pub fn flush_pending(&mut self, frame: &mut Frame) {
477 self.flush_into(frame);
478 }
479
480 pub(crate) fn flush_into(&mut self, frame: &mut Frame) {
481 if frame.flushed {
482 return;
483 }
484 let Some(encoder) = frame.encoder.as_mut() else {
485 return;
486 };
487 let mut layers = std::collections::BTreeSet::new();
491 self.sprites.collect_layers(&mut layers);
492 self.primitives.collect_layers(&mut layers);
493
494 self.sprites.upload(&self.device, &self.queue);
498 self.primitives.upload(&self.device, &self.queue);
499
500 let (color, resolve) = match self.msaa_view.as_ref() {
503 Some(msaa) => (msaa, Some(&frame.view)),
504 None => (&frame.view, None),
505 };
506 let targets = Targets {
507 color,
508 resolve,
509 depth: self.depth_view.as_ref(),
510 };
511
512 let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
514 label: Some("clear.pass"),
515 color_attachments: &[Some(targets.color_attachment(wgpu::LoadOp::Clear(
516 wgpu::Color {
517 r: frame.clear_color[0] as f64,
518 g: frame.clear_color[1] as f64,
519 b: frame.clear_color[2] as f64,
520 a: frame.clear_color[3] as f64,
521 },
522 )))],
523 depth_stencil_attachment: targets.depth_attachment(wgpu::LoadOp::Clear(1.0)),
524 occlusion_query_set: None,
525 timestamp_writes: None,
526 multiview_mask: None,
527 });
528
529 self.mesh_batcher.draw(
532 &self.device,
533 &self.queue,
534 &self.meshes,
535 encoder,
536 &targets,
537 &self.camera3d_bg,
538 );
539
540 for &layer in &layers {
541 self.sprites
542 .draw_layer(layer, encoder, &targets, &self.camera_bg, &self.textures);
543 self.primitives
544 .draw_layer(layer, encoder, &targets, &self.camera_bg);
545 }
546
547 self.text
548 .flush(&self.device, &self.queue, encoder, &targets);
549
550 #[cfg(feature = "vector")]
552 if let Some(vector) = self.vector.as_mut() {
553 vector.render_and_composite(&self.device, &self.queue, encoder, &frame.view);
554 }
555
556 self.sprites.clear();
557 self.primitives.clear();
558 self.mesh_batcher.clear();
559 frame.flushed = true;
560 }
561}
562
563fn make_attachments(
566 device: &wgpu::Device,
567 config: &wgpu::SurfaceConfiguration,
568 sample_count: u32,
569 depth_format: Option<wgpu::TextureFormat>,
570) -> (Option<wgpu::TextureView>, Option<wgpu::TextureView>) {
571 let size = wgpu::Extent3d {
572 width: config.width,
573 height: config.height,
574 depth_or_array_layers: 1,
575 };
576 let msaa_view = (sample_count > 1).then(|| {
577 device
578 .create_texture(&wgpu::TextureDescriptor {
579 label: Some("msaa.color"),
580 size,
581 mip_level_count: 1,
582 sample_count,
583 dimension: wgpu::TextureDimension::D2,
584 format: config.format,
585 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
586 view_formats: &[],
587 })
588 .create_view(&wgpu::TextureViewDescriptor::default())
589 });
590 let depth_view = depth_format.map(|format| {
591 device
592 .create_texture(&wgpu::TextureDescriptor {
593 label: Some("depth"),
594 size,
595 mip_level_count: 1,
596 sample_count,
597 dimension: wgpu::TextureDimension::D2,
598 format,
599 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
600 view_formats: &[],
601 })
602 .create_view(&wgpu::TextureViewDescriptor::default())
603 });
604 (msaa_view, depth_view)
605}