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, 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    /// 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 surface_format = caps
119            .formats
120            .iter()
121            .copied()
122            .find(wgpu::TextureFormat::is_srgb)
123            .unwrap_or(caps.formats[0]);
124        // Always render through an sRGB view, so shaders write linear color and the hardware
125        // encodes it, the same on every platform. Surfaces without an sRGB format (WebGPU
126        // canvases) still allow the sRGB variant as a view format.
127        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        // Add COPY_SRC when the surface supports it so frames can be read back for
134        // screenshots; fall back to render-only otherwise (screenshots become a no-op).
135        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    /// The color format the frame is rendered in: always an sRGB format, possibly a view of a
316    /// non-sRGB surface. Build render pipelines that draw into a [`Frame`] with this.
317    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    /// Load a PNG from disk and remember its path for [`Graphics::refresh_textures`]. Not
329    /// available on the web; use [`Graphics::load_texture_bytes`] (or `ctx.load_texture`,
330    /// which picks the right one per platform).
331    #[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    /// Size `[width, height]` in pixels that `s` takes up drawn at `size_px` (the widest line,
341    /// and the height of all lines), for aligning text or sizing a box to fit. Uses the same
342    /// layout as drawing, so the result matches `Painter::text` exactly; measuring and then
343    /// drawing the same string shapes it once.
344    pub fn measure_text(&mut self, s: &str, size_px: f32) -> [f32; 2] {
345        self.text.measure(s, size_px)
346    }
347
348    /// Register an extra font face (TTF/OTF bytes) as a glyph fallback for text rendering.
349    pub fn load_font_bytes(&mut self, bytes: Vec<u8>) {
350        self.text.load_font_bytes(bytes);
351    }
352
353    /// Decode a PNG held in memory and upload it. `label` names the texture in GPU debuggers
354    /// and error messages.
355    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    /// Re-decode any texture whose backing file is in `changed_paths` and update its bind
361    /// group in place. Returns the number of textures actually reloaded.
362    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    /// Choose what `id` shows for UVs outside `0..1`: [`TextureWrap::Clamp`] (the default for
399    /// every texture) stretches the edge texels, [`TextureWrap::Repeat`] and
400    /// [`TextureWrap::MirrorRepeat`] tile. Set `Repeat` on a texture to tile it across one
401    /// quad (`Painter::quad_textured` with UVs up to `[4.0, 10.0]`, a sprite with
402    /// [`crate::SpriteInstance::with_uv`] beyond `1.0`) instead of drawing a quad per tile.
403    /// Keep atlases on `Clamp`: their regions sample right up to the edge, and with wrapping
404    /// a UV of exactly `1.0` bleeds in the opposite edge. The mode survives hot-reloads.
405    /// Unknown ids are ignored.
406    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    /// The [`TextureWrap`] mode of `id`, or `None` for an unknown id.
413    pub fn texture_wrap(&self, id: TextureId) -> Option<TextureWrap> {
414        self.textures.get(id).map(|t| t.wrap)
415    }
416
417    /// Pixel size `[width, height]` of a loaded texture, or `None` for an unknown id. Follows
418    /// hot-reloads, so it reports the size of the current contents.
419    pub fn texture_size(&self, id: TextureId) -> Option<[u32; 2]> {
420        self.textures.get(id).map(|t| t.size)
421    }
422
423    /// Upload a static mesh (positions + normals, indexed) and return its handle. Meshes are
424    /// drawn depth-tested via [`Graphics::draw_mesh`] using the perspective [`Self::camera3d`].
425    pub fn create_mesh(&mut self, vertices: &[MeshVertex], indices: &[u16]) -> MeshId {
426        self.meshes.create(&self.device, vertices, indices)
427    }
428
429    /// Queue one instance of `mesh` for this frame. Meshes render before the 2D layers, so
430    /// 2D sprites, primitives and text draw on top of them.
431    pub fn draw_mesh(&mut self, mesh: MeshId, instance: MeshInstance) {
432        self.mesh_batcher.push(mesh, instance);
433    }
434
435    /// Save a PNG of the next presented frame to `path`. The read-back stalls that frame, so
436    /// this is for debug/dev use. A no-op (with a warning) if the surface lacks `COPY_SRC`,
437    /// and on the web, where there is neither a blocking read-back nor a filesystem.
438    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        // Capture the just-submitted surface before presenting (the queue runs the read-back
501        // copy after the render submit, so it sees the finished frame).
502        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    /// Read the surface texture back to the CPU and save it. Synchronous (blocks on a poll),
517    /// so only used for the occasional debug screenshot. `miniscreenshot-wgpu` handles the
518    /// staging copy, row-padding strip, and BGRA->RGBA conversion.
519    #[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    /// Flush all queued 2D layers (sprites + shapes + primitives + text) into the current `frame`.
531    /// Called automatically when a [`crate::Painter`] is dropped; call manually before
532    /// stacking other passes (e.g. egui) when no painter was created.
533    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        // Interleave sprites, filled shapes, circles and layered text by layer so a circle on
545        // layer -1 draws under a sprite on layer 0; within a layer, sprites draw first, then
546        // shapes, then circles, then text. Unlayered text (`Painter::text`) draws on top.
547        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        // Upload each batcher's instances exactly once before drawing: `queue.write_buffer`
554        // is not part of the encoder command stream, so writing per layer pass would clobber
555        // earlier layers' instance data.
556        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        // When multisampling, draws target the MSAA texture and resolve to the surface;
562        // otherwise they target the surface directly.
563        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        // Clear color (and depth) once up front; every layer pass then loads onto it.
574        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                    // sRGB like every painter color; the clear value is linear.
579                    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        // 3D meshes draw first, depth-tested, so the 2D layers (which do not write depth)
592        // composite on top of them.
593        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        // Vector content composites on top of the 2D layers, directly onto the surface.
616        #[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
629/// Allocate the MSAA color target (when `sample_count > 1`) and depth target (when a depth
630/// format is set), both sized to the surface. Returns `(msaa_view, depth_view)`.
631fn 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
673/// The sRGB format frames are rendered in: the surface's own if it is sRGB, otherwise the
674/// sRGB view format `Graphics::new` added.
675fn 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
687/// Decode one sRGB color component to linear, as the shaders do for painter colors.
688fn 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}