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 white_texture(&self) -> TextureId {
395 self.textures.white()
396 }
397
398 pub fn set_texture_wrap(&mut self, id: TextureId, wrap: TextureWrap) {
407 if !self.textures.set_wrap(&self.device, id, wrap) {
408 log::warn!("set_texture_wrap: unknown {id:?}");
409 }
410 }
411
412 pub fn texture_wrap(&self, id: TextureId) -> Option<TextureWrap> {
414 self.textures.get(id).map(|t| t.wrap)
415 }
416
417 pub fn texture_size(&self, id: TextureId) -> Option<[u32; 2]> {
420 self.textures.get(id).map(|t| t.size)
421 }
422
423 pub fn create_mesh(&mut self, vertices: &[MeshVertex], indices: &[u16]) -> MeshId {
426 self.meshes.create(&self.device, vertices, indices)
427 }
428
429 pub fn draw_mesh(&mut self, mesh: MeshId, instance: MeshInstance) {
432 self.mesh_batcher.push(mesh, instance);
433 }
434
435 pub fn request_screenshot(&mut self, path: impl Into<PathBuf>) {
439 if cfg!(target_arch = "wasm32") {
440 log::warn!("screenshot unsupported on the web");
441 return;
442 }
443 if !self.capture_copy_src {
444 log::warn!("screenshot unsupported: surface does not allow COPY_SRC read-back");
445 return;
446 }
447 self.pending_screenshot = Some(path.into());
448 }
449
450 pub fn begin_frame(&mut self) -> Result<Frame> {
451 self.queue.write_buffer(
452 &self.camera_buf,
453 0,
454 bytemuck::bytes_of(&CameraUniform {
455 view_proj: self.camera.view_proj(),
456 }),
457 );
458 self.queue.write_buffer(
459 &self.camera3d_buf,
460 0,
461 bytemuck::bytes_of(&CameraUniform {
462 view_proj: self.camera3d.view_proj(),
463 }),
464 );
465
466 let surface_texture = match self.surface.get_current_texture() {
467 wgpu::CurrentSurfaceTexture::Success(t)
468 | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
469 other => anyhow::bail!("surface unavailable: {other:?}"),
470 };
471 let view = surface_texture
472 .texture
473 .create_view(&wgpu::TextureViewDescriptor {
474 format: Some(render_format(&self.surface_config)),
475 ..Default::default()
476 });
477 let encoder = self
478 .device
479 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
480 label: Some("frame.encoder"),
481 });
482 Ok(Frame {
483 encoder: Some(encoder),
484 view,
485 surface_texture: Some(surface_texture),
486 clear_color: self.clear_color,
487 flushed: false,
488 })
489 }
490
491 pub fn present(&mut self, mut frame: Frame) {
492 if !frame.flushed {
493 self.flush_into(&mut frame);
494 }
495
496 if let Some(encoder) = frame.encoder.take() {
497 self.queue.submit(std::iter::once(encoder.finish()));
498 }
499
500 let shot_path = self.pending_screenshot.take();
503 if let (Some(path), Some(st)) = (&shot_path, frame.surface_texture.as_ref()) {
504 self.save_screenshot(&st.texture, path);
505 }
506
507 if let Some(st) = frame.surface_texture.take() {
508 self.window.pre_present_notify();
509 self.queue.present(st);
510 }
511 }
512
513 #[cfg(target_arch = "wasm32")]
514 fn save_screenshot(&self, _texture: &wgpu::Texture, _path: &Path) {}
515
516 #[cfg(not(target_arch = "wasm32"))]
520 fn save_screenshot(&self, texture: &wgpu::Texture, path: &Path) {
521 match miniscreenshot_wgpu::capture(&self.device, &self.queue, texture) {
522 Ok(shot) => match shot.save(path) {
523 Ok(()) => log::info!("saved screenshot {}", path.display()),
524 Err(e) => log::warn!("screenshot save failed: {e}"),
525 },
526 Err(e) => log::warn!("screenshot capture failed: {e}"),
527 }
528 }
529
530 pub fn flush_pending(&mut self, frame: &mut Frame) {
534 self.flush_into(frame);
535 }
536
537 pub(crate) fn flush_into(&mut self, frame: &mut Frame) {
538 if frame.flushed {
539 return;
540 }
541 let Some(encoder) = frame.encoder.as_mut() else {
542 return;
543 };
544 let mut layers = std::collections::BTreeSet::new();
548 self.sprites.collect_layers(&mut layers);
549 self.shapes.collect_layers(&mut layers);
550 self.primitives.collect_layers(&mut layers);
551 self.text.collect_layers(&mut layers);
552
553 self.sprites.upload(&self.device, &self.queue);
557 self.shapes.upload(&self.device, &self.queue);
558 self.primitives.upload(&self.device, &self.queue);
559 self.text.prepare(&self.device, &self.queue);
560
561 let (color, resolve) = match self.msaa_view.as_ref() {
564 Some(msaa) => (msaa, Some(&frame.view)),
565 None => (&frame.view, None),
566 };
567 let targets = Targets {
568 color,
569 resolve,
570 depth: self.depth_view.as_ref(),
571 };
572
573 let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
575 label: Some("clear.pass"),
576 color_attachments: &[Some(targets.color_attachment(wgpu::LoadOp::Clear(
577 wgpu::Color {
578 r: srgb_to_linear(frame.clear_color[0]),
580 g: srgb_to_linear(frame.clear_color[1]),
581 b: srgb_to_linear(frame.clear_color[2]),
582 a: frame.clear_color[3] as f64,
583 },
584 )))],
585 depth_stencil_attachment: targets.depth_attachment(wgpu::LoadOp::Clear(1.0)),
586 occlusion_query_set: None,
587 timestamp_writes: None,
588 multiview_mask: None,
589 });
590
591 self.mesh_batcher.draw(
594 &self.device,
595 &self.queue,
596 &self.meshes,
597 encoder,
598 &targets,
599 &self.camera3d_bg,
600 );
601
602 for &layer in &layers {
603 self.sprites
604 .draw_layer(layer, encoder, &targets, &self.camera_bg, &self.textures);
605 self.shapes
606 .draw_layer(layer, encoder, &targets, &self.camera_bg, &self.textures);
607 self.primitives
608 .draw_layer(layer, encoder, &targets, &self.camera_bg);
609 self.text.draw_layer(Some(layer), encoder, &targets);
610 }
611
612 self.text.draw_layer(None, encoder, &targets);
613 self.text.finish();
614
615 #[cfg(feature = "vector")]
617 if let Some(vector) = self.vector.as_mut() {
618 vector.render_and_composite(&self.device, &self.queue, encoder, &frame.view);
619 }
620
621 self.sprites.clear();
622 self.shapes.clear();
623 self.primitives.clear();
624 self.mesh_batcher.clear();
625 frame.flushed = true;
626 }
627}
628
629fn make_attachments(
632 device: &wgpu::Device,
633 config: &wgpu::SurfaceConfiguration,
634 sample_count: u32,
635 depth_format: Option<wgpu::TextureFormat>,
636) -> (Option<wgpu::TextureView>, Option<wgpu::TextureView>) {
637 let size = wgpu::Extent3d {
638 width: config.width,
639 height: config.height,
640 depth_or_array_layers: 1,
641 };
642 let msaa_view = (sample_count > 1).then(|| {
643 device
644 .create_texture(&wgpu::TextureDescriptor {
645 label: Some("msaa.color"),
646 size,
647 mip_level_count: 1,
648 sample_count,
649 dimension: wgpu::TextureDimension::D2,
650 format: render_format(config),
651 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
652 view_formats: &[],
653 })
654 .create_view(&wgpu::TextureViewDescriptor::default())
655 });
656 let depth_view = depth_format.map(|format| {
657 device
658 .create_texture(&wgpu::TextureDescriptor {
659 label: Some("depth"),
660 size,
661 mip_level_count: 1,
662 sample_count,
663 dimension: wgpu::TextureDimension::D2,
664 format,
665 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
666 view_formats: &[],
667 })
668 .create_view(&wgpu::TextureViewDescriptor::default())
669 });
670 (msaa_view, depth_view)
671}
672
673fn render_format(config: &wgpu::SurfaceConfiguration) -> wgpu::TextureFormat {
676 if config.format.is_srgb() {
677 return config.format;
678 }
679 config
680 .view_formats
681 .iter()
682 .copied()
683 .find(wgpu::TextureFormat::is_srgb)
684 .unwrap_or(config.format)
685}
686
687fn srgb_to_linear(c: f32) -> f64 {
689 let c = f64::from(c);
690 if c <= 0.04045 {
691 c / 12.92
692 } else {
693 ((c + 0.055) / 1.055).powf(2.4)
694 }
695}