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::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    /// Multisampled color target (resolved to the surface) when `sample_count > 1`.
39    msaa_view: Option<wgpu::TextureView>,
40    /// Depth attachment when `depth_format` is set. Recreated on resize.
41    depth_view: Option<wgpu::TextureView>,
42    /// Perspective camera for 3D meshes. Set its fields to move the view.
43    pub camera3d: Camera3D,
44    camera3d_buf: wgpu::Buffer,
45    camera3d_bg: wgpu::BindGroup,
46    meshes: MeshRegistry,
47    mesh_batcher: MeshBatcher,
48    /// Vello vector backend; composites over the 2D layers each frame.
49    #[cfg(feature = "vector")]
50    pub(crate) vector: VectorPass,
51    /// Whether the surface supports `COPY_SRC` (required to read frames back for screenshots).
52    capture_copy_src: bool,
53    /// Path to write a screenshot of the next presented frame, set by `request_screenshot`.
54    pending_screenshot: Option<PathBuf>,
55}
56
57impl Graphics {
58    pub async fn new(
59        window: Arc<Window>,
60        vsync: bool,
61        depth_format: Option<wgpu::TextureFormat>,
62        msaa_samples: u32,
63    ) -> Result<Self> {
64        let size = window.inner_size();
65        let (width, height) = (size.width.max(1), size.height.max(1));
66
67        let mut idesc = wgpu::InstanceDescriptor::new_without_display_handle();
68        idesc.backends = wgpu::Backends::PRIMARY;
69        let instance = wgpu::Instance::new(idesc);
70        let surface = instance
71            .create_surface(window.clone())
72            .context("create_surface")?;
73        let adapter = instance
74            .request_adapter(&wgpu::RequestAdapterOptions {
75                power_preference: wgpu::PowerPreference::HighPerformance,
76                compatible_surface: Some(&surface),
77                force_fallback_adapter: false,
78            })
79            .await
80            .context("no compatible adapter")?;
81
82        // Vello needs the standard (non-downlevel) limits; bump them when the feature is on.
83        let required_limits = if cfg!(feature = "vector") {
84            wgpu::Limits::default().using_resolution(adapter.limits())
85        } else {
86            wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits())
87        };
88        let (device, queue) = adapter
89            .request_device(&wgpu::DeviceDescriptor {
90                label: Some("toolkit.device"),
91                required_features: wgpu::Features::empty(),
92                required_limits,
93                memory_hints: wgpu::MemoryHints::Performance,
94                ..Default::default()
95            })
96            .await
97            .context("request_device")?;
98
99        let caps = surface.get_capabilities(&adapter);
100        let format = caps
101            .formats
102            .iter()
103            .copied()
104            .find(|f| f.is_srgb())
105            .unwrap_or(caps.formats[0]);
106        let present_mode = if vsync {
107            wgpu::PresentMode::AutoVsync
108        } else {
109            wgpu::PresentMode::AutoNoVsync
110        };
111        // Add COPY_SRC when the surface supports it so frames can be read back for
112        // screenshots; fall back to render-only otherwise (screenshots become a no-op).
113        let capture_copy_src = caps.usages.contains(wgpu::TextureUsages::COPY_SRC);
114        let surface_usage = if capture_copy_src {
115            wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC
116        } else {
117            wgpu::TextureUsages::RENDER_ATTACHMENT
118        };
119        let surface_config = wgpu::SurfaceConfiguration {
120            usage: surface_usage,
121            format,
122            width,
123            height,
124            present_mode,
125            desired_maximum_frame_latency: 2,
126            alpha_mode: caps.alpha_modes[0],
127            view_formats: vec![],
128        };
129        surface.configure(&device, &surface_config);
130
131        let camera = Camera2D::new(width as f32, height as f32);
132        let camera_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
133            label: Some("camera.buf"),
134            contents: bytemuck::bytes_of(&CameraUniform {
135                view_proj: camera.view_proj(),
136            }),
137            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
138        });
139        let camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
140            label: Some("camera.bgl"),
141            entries: &[wgpu::BindGroupLayoutEntry {
142                binding: 0,
143                visibility: wgpu::ShaderStages::VERTEX,
144                ty: wgpu::BindingType::Buffer {
145                    ty: wgpu::BufferBindingType::Uniform,
146                    has_dynamic_offset: false,
147                    min_binding_size: None,
148                },
149                count: None,
150            }],
151        });
152        let camera_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
153            label: Some("camera.bg"),
154            layout: &camera_bgl,
155            entries: &[wgpu::BindGroupEntry {
156                binding: 0,
157                resource: camera_buf.as_entire_binding(),
158            }],
159        });
160
161        let camera3d = Camera3D::new(width as f32 / height.max(1) as f32);
162        let camera3d_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
163            label: Some("camera3d.buf"),
164            contents: bytemuck::bytes_of(&CameraUniform {
165                view_proj: camera3d.view_proj(),
166            }),
167            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
168        });
169        let camera3d_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
170            label: Some("camera3d.bg"),
171            layout: &camera_bgl,
172            entries: &[wgpu::BindGroupEntry {
173                binding: 0,
174                resource: camera3d_buf.as_entire_binding(),
175            }],
176        });
177
178        let sample_count = msaa_samples.max(1);
179        let textures = TextureRegistry::new(&device, &queue);
180        let sprites = SpriteBatcher::new(
181            &device,
182            format,
183            &camera_bgl,
184            &textures.layout,
185            sample_count,
186            depth_format,
187        );
188        let primitives =
189            PrimitiveBatcher::new(&device, format, &camera_bgl, sample_count, depth_format);
190        let text = TextSystem::new(
191            &device,
192            &queue,
193            format,
194            width,
195            height,
196            sample_count,
197            depth_format,
198        );
199        let (msaa_view, depth_view) =
200            make_attachments(&device, &surface_config, sample_count, depth_format);
201        let meshes = MeshRegistry::new();
202        let mesh_batcher =
203            MeshBatcher::new(&device, format, &camera_bgl, sample_count, depth_format);
204        #[cfg(feature = "vector")]
205        let vector = VectorPass::new(&device, format, width, height);
206
207        Ok(Self {
208            device,
209            queue,
210            surface,
211            surface_config,
212            window,
213            camera,
214            camera_buf,
215            camera_bg,
216            sprites,
217            primitives,
218            text,
219            textures,
220            clear_color: [0.0, 0.0, 0.0, 1.0],
221            texture_paths: HashMap::new(),
222            sample_count,
223            depth_format,
224            msaa_view,
225            depth_view,
226            camera3d,
227            camera3d_buf,
228            camera3d_bg,
229            meshes,
230            mesh_batcher,
231            #[cfg(feature = "vector")]
232            vector,
233            capture_copy_src,
234            pending_screenshot: None,
235        })
236    }
237
238    pub fn resize(&mut self, width: u32, height: u32) {
239        if width == 0 || height == 0 {
240            return;
241        }
242        self.surface_config.width = width;
243        self.surface_config.height = height;
244        self.surface.configure(&self.device, &self.surface_config);
245        let (msaa_view, depth_view) = make_attachments(
246            &self.device,
247            &self.surface_config,
248            self.sample_count,
249            self.depth_format,
250        );
251        self.msaa_view = msaa_view;
252        self.depth_view = depth_view;
253        self.camera.resize(width as f32, height as f32);
254        self.camera3d.resize(width as f32, height as f32);
255        self.text.resize(&self.queue, width, height);
256        #[cfg(feature = "vector")]
257        self.vector.resize(&self.device, width, height);
258    }
259
260    pub fn window(&self) -> &Arc<Window> {
261        &self.window
262    }
263
264    pub fn size(&self) -> (u32, u32) {
265        (self.surface_config.width, self.surface_config.height)
266    }
267
268    pub fn surface_format(&self) -> wgpu::TextureFormat {
269        self.surface_config.format
270    }
271
272    pub fn device(&self) -> &wgpu::Device {
273        &self.device
274    }
275    pub fn queue(&self) -> &wgpu::Queue {
276        &self.queue
277    }
278
279    pub fn load_texture(&mut self, path: impl AsRef<Path>) -> Result<TextureId> {
280        let path = path.as_ref();
281        let id = self.textures.load_file(&self.device, &self.queue, path)?;
282        let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
283        self.texture_paths.insert(canon, id);
284        Ok(id)
285    }
286
287    /// Re-decode any texture whose backing file is in `changed_paths` and update its bind
288    /// group in place. Returns the number of textures actually reloaded.
289    pub fn refresh_textures<I, P>(&mut self, changed_paths: I) -> usize
290    where
291        I: IntoIterator<Item = P>,
292        P: AsRef<Path>,
293    {
294        let mut n = 0;
295        for p in changed_paths {
296            let canon = p
297                .as_ref()
298                .canonicalize()
299                .unwrap_or_else(|_| p.as_ref().to_path_buf());
300            if let Some(&id) = self.texture_paths.get(&canon) {
301                match self.textures.reload(&self.device, &self.queue, id, &canon) {
302                    Ok(()) => n += 1,
303                    Err(e) => log::warn!("reload {} failed: {e:?}", canon.display()),
304                }
305            }
306        }
307        n
308    }
309
310    pub fn create_texture_rgba(
311        &mut self,
312        width: u32,
313        height: u32,
314        rgba: &[u8],
315        label: Option<&str>,
316    ) -> TextureId {
317        self.textures
318            .create_from_rgba(&self.device, &self.queue, width, height, rgba, label)
319    }
320
321    pub fn white_texture(&self) -> TextureId {
322        self.textures.white()
323    }
324
325    /// Upload a static mesh (positions + normals, indexed) and return its handle. Meshes are
326    /// drawn depth-tested via [`Graphics::draw_mesh`] using the perspective [`Self::camera3d`].
327    pub fn create_mesh(&mut self, vertices: &[MeshVertex], indices: &[u16]) -> MeshId {
328        self.meshes.create(&self.device, vertices, indices)
329    }
330
331    /// Queue one instance of `mesh` for this frame. Meshes render before the 2D layers, so
332    /// 2D sprites, primitives and text draw on top of them.
333    pub fn draw_mesh(&mut self, mesh: MeshId, instance: MeshInstance) {
334        self.mesh_batcher.push(mesh, instance);
335    }
336
337    /// Save a PNG of the next presented frame to `path`. The read-back stalls that frame, so
338    /// this is for debug/dev use. A no-op (with a warning) if the surface lacks `COPY_SRC`.
339    pub fn request_screenshot(&mut self, path: impl Into<PathBuf>) {
340        if !self.capture_copy_src {
341            log::warn!("screenshot unsupported: surface does not allow COPY_SRC read-back");
342            return;
343        }
344        self.pending_screenshot = Some(path.into());
345    }
346
347    pub fn begin_frame(&mut self) -> Result<Frame> {
348        self.queue.write_buffer(
349            &self.camera_buf,
350            0,
351            bytemuck::bytes_of(&CameraUniform {
352                view_proj: self.camera.view_proj(),
353            }),
354        );
355        self.queue.write_buffer(
356            &self.camera3d_buf,
357            0,
358            bytemuck::bytes_of(&CameraUniform {
359                view_proj: self.camera3d.view_proj(),
360            }),
361        );
362
363        let surface_texture = match self.surface.get_current_texture() {
364            wgpu::CurrentSurfaceTexture::Success(t)
365            | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
366            other => anyhow::bail!("surface unavailable: {other:?}"),
367        };
368        let view = surface_texture
369            .texture
370            .create_view(&wgpu::TextureViewDescriptor::default());
371        let encoder = self
372            .device
373            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
374                label: Some("frame.encoder"),
375            });
376        Ok(Frame {
377            encoder: Some(encoder),
378            view,
379            surface_texture: Some(surface_texture),
380            clear_color: self.clear_color,
381            flushed: false,
382        })
383    }
384
385    pub fn present(&mut self, mut frame: Frame) {
386        if !frame.flushed {
387            self.flush_into(&mut frame);
388        }
389
390        if let Some(encoder) = frame.encoder.take() {
391            self.queue.submit(std::iter::once(encoder.finish()));
392        }
393
394        // Capture the just-submitted surface before presenting (the queue runs the read-back
395        // copy after the render submit, so it sees the finished frame).
396        let shot_path = self.pending_screenshot.take();
397        if let (Some(path), Some(st)) = (&shot_path, frame.surface_texture.as_ref()) {
398            self.save_screenshot(&st.texture, path);
399        }
400
401        if let Some(st) = frame.surface_texture.take() {
402            self.window.pre_present_notify();
403            st.present();
404        }
405    }
406
407    /// Read the surface texture back to the CPU and save it. Synchronous (blocks on a poll),
408    /// so only used for the occasional debug screenshot. `miniscreenshot-wgpu` handles the
409    /// staging copy, row-padding strip, and BGRA->RGBA conversion.
410    fn save_screenshot(&self, texture: &wgpu::Texture, path: &Path) {
411        match miniscreenshot_wgpu::capture(&self.device, &self.queue, texture) {
412            Ok(shot) => match shot.save(path) {
413                Ok(()) => log::info!("saved screenshot {}", path.display()),
414                Err(e) => log::warn!("screenshot save failed: {e}"),
415            },
416            Err(e) => log::warn!("screenshot capture failed: {e}"),
417        }
418    }
419
420    /// Flush all queued 2D layers (sprites + primitives + text) into the current `frame`.
421    /// Called automatically when a [`crate::Painter`] is dropped; call manually before
422    /// stacking other passes (e.g. egui) when no painter was created.
423    pub fn flush_pending(&mut self, frame: &mut Frame) {
424        self.flush_into(frame);
425    }
426
427    pub(crate) fn flush_into(&mut self, frame: &mut Frame) {
428        if frame.flushed {
429            return;
430        }
431        let Some(encoder) = frame.encoder.as_mut() else {
432            return;
433        };
434        // Interleave sprites and circles by layer so a circle on layer -1 draws under a
435        // sprite on layer 0; within a layer, sprites draw under circles. Text renders once
436        // on top (glyphon prepares a single vertex buffer per call, so it is not layered).
437        let mut layers = std::collections::BTreeSet::new();
438        self.sprites.collect_layers(&mut layers);
439        self.primitives.collect_layers(&mut layers);
440
441        // Upload each batcher's instances exactly once before drawing: `queue.write_buffer`
442        // is not part of the encoder command stream, so writing per layer pass would clobber
443        // earlier layers' instance data.
444        self.sprites.upload(&self.device, &self.queue);
445        self.primitives.upload(&self.device, &self.queue);
446
447        // When multisampling, draws target the MSAA texture and resolve to the surface;
448        // otherwise they target the surface directly.
449        let (color, resolve) = match self.msaa_view.as_ref() {
450            Some(msaa) => (msaa, Some(&frame.view)),
451            None => (&frame.view, None),
452        };
453        let targets = Targets {
454            color,
455            resolve,
456            depth: self.depth_view.as_ref(),
457        };
458
459        // Clear color (and depth) once up front; every layer pass then loads onto it.
460        let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
461            label: Some("clear.pass"),
462            color_attachments: &[Some(targets.color_attachment(wgpu::LoadOp::Clear(
463                wgpu::Color {
464                    r: frame.clear_color[0] as f64,
465                    g: frame.clear_color[1] as f64,
466                    b: frame.clear_color[2] as f64,
467                    a: frame.clear_color[3] as f64,
468                },
469            )))],
470            depth_stencil_attachment: targets.depth_attachment(wgpu::LoadOp::Clear(1.0)),
471            occlusion_query_set: None,
472            timestamp_writes: None,
473            multiview_mask: None,
474        });
475
476        // 3D meshes draw first, depth-tested, so the 2D layers (which do not write depth)
477        // composite on top of them.
478        self.mesh_batcher.draw(
479            &self.device,
480            &self.queue,
481            &self.meshes,
482            encoder,
483            &targets,
484            &self.camera3d_bg,
485        );
486
487        for &layer in &layers {
488            self.sprites
489                .draw_layer(layer, encoder, &targets, &self.camera_bg, &self.textures);
490            self.primitives
491                .draw_layer(layer, encoder, &targets, &self.camera_bg);
492        }
493
494        self.text
495            .flush(&self.device, &self.queue, encoder, &targets);
496
497        // Vector content composites on top of the 2D layers, directly onto the surface.
498        #[cfg(feature = "vector")]
499        self.vector
500            .render_and_composite(&self.device, &self.queue, encoder, &frame.view);
501
502        self.sprites.clear();
503        self.primitives.clear();
504        self.mesh_batcher.clear();
505        frame.flushed = true;
506    }
507}
508
509/// Allocate the MSAA color target (when `sample_count > 1`) and depth target (when a depth
510/// format is set), both sized to the surface. Returns `(msaa_view, depth_view)`.
511fn make_attachments(
512    device: &wgpu::Device,
513    config: &wgpu::SurfaceConfiguration,
514    sample_count: u32,
515    depth_format: Option<wgpu::TextureFormat>,
516) -> (Option<wgpu::TextureView>, Option<wgpu::TextureView>) {
517    let size = wgpu::Extent3d {
518        width: config.width,
519        height: config.height,
520        depth_or_array_layers: 1,
521    };
522    let msaa_view = (sample_count > 1).then(|| {
523        device
524            .create_texture(&wgpu::TextureDescriptor {
525                label: Some("msaa.color"),
526                size,
527                mip_level_count: 1,
528                sample_count,
529                dimension: wgpu::TextureDimension::D2,
530                format: config.format,
531                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
532                view_formats: &[],
533            })
534            .create_view(&wgpu::TextureViewDescriptor::default())
535    });
536    let depth_view = depth_format.map(|format| {
537        device
538            .create_texture(&wgpu::TextureDescriptor {
539                label: Some("depth"),
540                size,
541                mip_level_count: 1,
542                sample_count,
543                dimension: wgpu::TextureDimension::D2,
544                format,
545                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
546                view_formats: &[],
547            })
548            .create_view(&wgpu::TextureViewDescriptor::default())
549    });
550    (msaa_view, depth_view)
551}