Skip to main content

game_toolkit_gfx/
graphics.rs

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    /// Multisampled color target (resolved to the surface) when `sample_count > 1`.
41    msaa_view: Option<wgpu::TextureView>,
42    /// Depth attachment when `depth_format` is set. Recreated on resize.
43    depth_view: Option<wgpu::TextureView>,
44    /// Perspective camera for 3D meshes. Set its fields to move the view.
45    pub camera3d: Camera3D,
46    camera3d_buf: wgpu::Buffer,
47    camera3d_bg: wgpu::BindGroup,
48    meshes: MeshRegistry,
49    mesh_batcher: MeshBatcher,
50    /// Vello vector backend; composites over the 2D layers each frame. `None` on adapters
51    /// without compute shaders (WebGL2), where vector drawing is skipped.
52    #[cfg(feature = "vector")]
53    pub(crate) vector: Option<VectorPass>,
54    /// Whether the surface supports `COPY_SRC` (required to read frames back for screenshots).
55    capture_copy_src: bool,
56    /// Path to write a screenshot of the next presented frame, set by `request_screenshot`.
57    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        // Web: WebGPU where the browser has it, WebGL2 otherwise. Native: Vulkan/Metal/DX12.
72        idesc.backends = if cfg!(target_arch = "wasm32") {
73            wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL
74        } else {
75            wgpu::Backends::PRIMARY
76        };
77        // A browser may expose `navigator.gpu` yet hand out no adapter (headless Chrome, blocked
78        // GPUs). This probes for a real WebGPU adapter and drops to WebGL2 when there is none;
79        // natively it is a plain `Instance::new`.
80        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        // WebGL2 can't meet even the downlevel limits (and has no compute, so no vello).
97        // Otherwise vello needs the standard (non-downlevel) limits; bump them when it's on.
98        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        // Add COPY_SRC when the surface supports it so frames can be read back for
130        // screenshots; fall back to render-only otherwise (screenshots become a no-op).
131        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    /// Load a PNG from disk and remember its path for [`Graphics::refresh_textures`]. Not
319    /// available on the web; use [`Graphics::load_texture_bytes`] (or `ctx.load_texture`,
320    /// which picks the right one per platform).
321    #[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    /// Register an extra font face (TTF/OTF bytes) as a glyph fallback for text rendering.
331    pub fn load_font_bytes(&mut self, bytes: Vec<u8>) {
332        self.text.load_font_bytes(bytes);
333    }
334
335    /// Decode a PNG held in memory and upload it. `label` names the texture in GPU debuggers
336    /// and error messages.
337    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    /// Re-decode any texture whose backing file is in `changed_paths` and update its bind
343    /// group in place. Returns the number of textures actually reloaded.
344    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    /// Pixel size `[width, height]` of a loaded texture, or `None` for an unknown id. Follows
381    /// hot-reloads, so it reports the size of the current contents.
382    pub fn texture_size(&self, id: TextureId) -> Option<[u32; 2]> {
383        self.textures.get(id).map(|t| t.size)
384    }
385
386    /// Upload a static mesh (positions + normals, indexed) and return its handle. Meshes are
387    /// drawn depth-tested via [`Graphics::draw_mesh`] using the perspective [`Self::camera3d`].
388    pub fn create_mesh(&mut self, vertices: &[MeshVertex], indices: &[u16]) -> MeshId {
389        self.meshes.create(&self.device, vertices, indices)
390    }
391
392    /// Queue one instance of `mesh` for this frame. Meshes render before the 2D layers, so
393    /// 2D sprites, primitives and text draw on top of them.
394    pub fn draw_mesh(&mut self, mesh: MeshId, instance: MeshInstance) {
395        self.mesh_batcher.push(mesh, instance);
396    }
397
398    /// Save a PNG of the next presented frame to `path`. The read-back stalls that frame, so
399    /// this is for debug/dev use. A no-op (with a warning) if the surface lacks `COPY_SRC`,
400    /// and on the web, where there is neither a blocking read-back nor a filesystem.
401    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        // Capture the just-submitted surface before presenting (the queue runs the read-back
461        // copy after the render submit, so it sees the finished frame).
462        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    /// Read the surface texture back to the CPU and save it. Synchronous (blocks on a poll),
477    /// so only used for the occasional debug screenshot. `miniscreenshot-wgpu` handles the
478    /// staging copy, row-padding strip, and BGRA->RGBA conversion.
479    #[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    /// Flush all queued 2D layers (sprites + shapes + primitives + text) into the current `frame`.
491    /// Called automatically when a [`crate::Painter`] is dropped; call manually before
492    /// stacking other passes (e.g. egui) when no painter was created.
493    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        // Interleave sprites, filled shapes and circles by layer so a circle on layer -1
505        // draws under a sprite on layer 0; within a layer, sprites draw first, then shapes,
506        // then circles. Text renders once on top (glyphon prepares a single vertex buffer per
507        // call, so it is not layered).
508        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        // Upload each batcher's instances exactly once before drawing: `queue.write_buffer`
514        // is not part of the encoder command stream, so writing per layer pass would clobber
515        // earlier layers' instance data.
516        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        // When multisampling, draws target the MSAA texture and resolve to the surface;
521        // otherwise they target the surface directly.
522        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        // Clear color (and depth) once up front; every layer pass then loads onto it.
533        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        // 3D meshes draw first, depth-tested, so the 2D layers (which do not write depth)
550        // composite on top of them.
551        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        // Vector content composites on top of the 2D layers, directly onto the surface.
573        #[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
586/// Allocate the MSAA color target (when `sample_count > 1`) and depth target (when a depth
587/// format is set), both sized to the surface. Returns `(msaa_view, depth_view)`.
588fn 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}