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::bitmap_font::BitmapFont;
10use crate::camera::{Camera2D, CameraUniform};
11use crate::camera3d::{Camera, Camera3D};
12use crate::canvas::{self, Blit, Canvas, CanvasRect, RenderTexture, ScaleFilter, ScaleMode};
13use crate::color::Color;
14use crate::config::{self, GraphicsConfig};
15use crate::custom::CustomPass;
16use crate::frame::Frame;
17use crate::mesh::{MeshBatcher, MeshId, MeshInstance, MeshRegistry, MeshVertex};
18use crate::post::{self, PostChain, PostEffect, PostEffectId, PostStage, StageRun};
19use crate::primitives::PrimitiveBatcher;
20use crate::shapes::ShapeBatcher;
21use crate::sprite::SpriteBatcher;
22use crate::target::Targets;
23use crate::text::TextSystem;
24use crate::texture::{TextureId, TextureRegistry, TextureWrap};
25#[cfg(feature = "vector")]
26use crate::vector::VectorPass;
27
28pub struct Graphics {
29    pub(crate) device: wgpu::Device,
30    pub(crate) queue: wgpu::Queue,
31    surface: wgpu::Surface<'static>,
32    surface_config: wgpu::SurfaceConfiguration,
33    window: Arc<Window>,
34    pub camera: Camera2D,
35    camera_buf: wgpu::Buffer,
36    pub(crate) camera_bg: wgpu::BindGroup,
37    pub(crate) sprites: SpriteBatcher,
38    pub(crate) primitives: PrimitiveBatcher,
39    pub(crate) shapes: ShapeBatcher,
40    pub(crate) text: TextSystem,
41    pub(crate) textures: TextureRegistry,
42    /// Passes queued with `Painter::custom`, run at their layer.
43    pub(crate) custom: crate::custom::CustomPasses,
44    pub(crate) clear_color: [f32; 4],
45    texture_paths: HashMap<PathBuf, TextureId>,
46    sample_count: u32,
47    depth_format: Option<wgpu::TextureFormat>,
48    /// Multisampled color target (resolved to the surface) when `sample_count > 1`.
49    msaa_view: Option<wgpu::TextureView>,
50    /// Depth attachment when `depth_format` is set. Recreated on resize.
51    depth_view: Option<wgpu::TextureView>,
52    /// Perspective camera for 3D meshes. Set its fields to move the view.
53    pub camera3d: Camera3D,
54    camera3d_buf: wgpu::Buffer,
55    camera3d_bg: wgpu::BindGroup,
56    meshes: MeshRegistry,
57    mesh_batcher: MeshBatcher,
58    /// Vello vector backend; composites over the 2D layers each frame. `None` on adapters
59    /// without compute shaders (WebGL2), where vector drawing is skipped.
60    #[cfg(feature = "vector")]
61    pub(crate) vector: Option<VectorPass>,
62    /// Whether the surface supports `COPY_SRC` (required to read frames back for screenshots).
63    capture_copy_src: bool,
64    /// Path to write a screenshot of the next presented frame, set by `request_screenshot`.
65    pending_screenshot: Option<PathBuf>,
66    adapter_info: wgpu::AdapterInfo,
67    supports_compute: bool,
68    /// What the device was created without (`continue_without_requirements`).
69    unmet_requirements: Option<crate::DeviceRequirementsError>,
70    /// The offscreen scene the painter draws into: the virtual-resolution canvas, or a
71    /// window-sized one while post effects are registered. `None`: straight to the surface.
72    canvas: Option<Canvas>,
73    virtual_resolution: Option<(u32, u32)>,
74    blit: Blit,
75    post: PostChain,
76    /// Window-sized target the canvas is scaled into when window-stage effects run.
77    window_scratch: Option<RenderTexture>,
78    start: web_time::Instant,
79    frame_count: u32,
80    #[cfg(feature = "indexed")]
81    indexed: Option<crate::indexed::IndexedState>,
82    /// `disable_indexed` was called mid-frame; applied once the frame is presented.
83    #[cfg(feature = "indexed")]
84    disable_indexed_after_frame: bool,
85    /// Between `begin_frame` and `present`.
86    frame_active: bool,
87    /// The built-in pixel font, uploaded on first use.
88    pub(crate) builtin_font: Option<BitmapFont>,
89    scale_mode: ScaleMode,
90    scale_filter: ScaleFilter,
91    border_color: Color,
92}
93
94impl Graphics {
95    /// Create the surface and device for `window`. Fails with a
96    /// [`DeviceRequirementsError`](crate::DeviceRequirementsError) when the adapter lacks a
97    /// limit or feature from `config`.
98    pub async fn new(window: Arc<Window>, config: &GraphicsConfig) -> Result<Self> {
99        let GraphicsConfig {
100            vsync,
101            depth_format,
102            msaa_samples,
103            virtual_resolution,
104            scale_mode,
105            scale_filter,
106            border_color,
107            ..
108        } = *config;
109        let size = window.inner_size();
110        let (width, height) = (size.width.max(1), size.height.max(1));
111
112        let mut idesc = wgpu::InstanceDescriptor::new_without_display_handle();
113        // Web: WebGPU where the browser has it, WebGL2 otherwise. Native: Vulkan/Metal/DX12.
114        idesc.backends = if cfg!(target_arch = "wasm32") {
115            wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL
116        } else {
117            wgpu::Backends::PRIMARY
118        };
119        // A browser may expose `navigator.gpu` yet hand out no adapter (headless Chrome, blocked
120        // GPUs). This probes for a real WebGPU adapter and drops to WebGL2 when there is none;
121        // natively it is a plain `Instance::new`.
122        let instance = wgpu::util::new_instance_with_webgpu_detection(idesc).await;
123        let surface = instance
124            .create_surface(window.clone())
125            .context("create_surface")?;
126        let adapter = instance
127            .request_adapter(&wgpu::RequestAdapterOptions {
128                power_preference: wgpu::PowerPreference::HighPerformance,
129                compatible_surface: Some(&surface),
130                force_fallback_adapter: false,
131                apply_limit_buckets: false,
132            })
133            .await
134            .context("no compatible adapter")?;
135
136        let info = adapter.get_info();
137        let backend = info.backend;
138        log::info!("wgpu adapter: {:?} ({backend:?})", info.name);
139        let adapter_limits = adapter.limits();
140        let adapter_features = adapter.features();
141        let (required_limits, required_features) =
142            config::merged_requirements(config, backend, &adapter_limits, adapter_features, true);
143        let checked = config::check_requirements(
144            &info.name,
145            backend,
146            &adapter_limits,
147            adapter_features,
148            &required_limits,
149            required_features,
150        );
151        let (required_limits, required_features, unmet_requirements) = match checked {
152            Ok(()) => (required_limits, required_features, None),
153            Err(err) if config.continue_without_requirements => {
154                log::warn!("{err}; continuing without the game's required limits and features");
155                let (limits, features) = config::merged_requirements(
156                    config,
157                    backend,
158                    &adapter_limits,
159                    adapter_features,
160                    false,
161                );
162                (limits, features, Some(err))
163            }
164            Err(err) => return Err(err.into()),
165        };
166        let mut device_desc = wgpu::DeviceDescriptor {
167            label: Some("toolkit.device"),
168            required_features,
169            required_limits,
170            memory_hints: wgpu::MemoryHints::Performance,
171            ..Default::default()
172        };
173        if let Some(hook) = &config.device_hook {
174            hook(&adapter, &mut device_desc);
175        }
176        let (device, queue) = adapter
177            .request_device(&device_desc)
178            .await
179            .context("request_device")?;
180        let supports_compute = adapter
181            .get_downlevel_capabilities()
182            .flags
183            .contains(wgpu::DownlevelFlags::COMPUTE_SHADERS);
184
185        let caps = surface.get_capabilities(&adapter);
186        let surface_format = caps
187            .formats
188            .iter()
189            .copied()
190            .find(wgpu::TextureFormat::is_srgb)
191            .unwrap_or(caps.formats[0]);
192        // Always render through an sRGB view, so shaders write linear color and the hardware
193        // encodes it, the same on every platform. Surfaces without an sRGB format (WebGPU
194        // canvases) still allow the sRGB variant as a view format.
195        let format = surface_format.add_srgb_suffix();
196        let present_mode = if vsync {
197            wgpu::PresentMode::AutoVsync
198        } else {
199            wgpu::PresentMode::AutoNoVsync
200        };
201        // Add COPY_SRC when the surface supports it so frames can be read back for
202        // screenshots; fall back to render-only otherwise (screenshots become a no-op).
203        let capture_copy_src = caps.usages.contains(wgpu::TextureUsages::COPY_SRC);
204        let surface_usage = if capture_copy_src {
205            wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC
206        } else {
207            wgpu::TextureUsages::RENDER_ATTACHMENT
208        };
209        let surface_config = wgpu::SurfaceConfiguration {
210            usage: surface_usage,
211            format: surface_format,
212            width,
213            height,
214            present_mode,
215            desired_maximum_frame_latency: 2,
216            alpha_mode: caps.alpha_modes[0],
217            view_formats: if format == surface_format {
218                vec![]
219            } else {
220                vec![format]
221            },
222            color_space: wgpu::SurfaceColorSpace::Auto,
223        };
224        surface.configure(&device, &surface_config);
225
226        // With a virtual resolution everything the painter draws goes into the canvas, so the
227        // cameras, text, attachments and vector target are all sized to it.
228        let blit = Blit::new(&device, format);
229        let canvas = virtual_resolution.map(|(w, h)| Canvas {
230            tex: RenderTexture::new(&device, &blit, format, [w, h], "canvas"),
231            follows_window: false,
232        });
233        let [width, height] = canvas.as_ref().map_or([width, height], Canvas::size);
234
235        let camera = Camera2D::new(width as f32, height as f32);
236        let camera_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
237            label: Some("camera.buf"),
238            contents: bytemuck::bytes_of(&CameraUniform {
239                view_proj: camera.view_proj(),
240            }),
241            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
242        });
243        let camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
244            label: Some("camera.bgl"),
245            entries: &[wgpu::BindGroupLayoutEntry {
246                binding: 0,
247                visibility: wgpu::ShaderStages::VERTEX,
248                ty: wgpu::BindingType::Buffer {
249                    ty: wgpu::BufferBindingType::Uniform,
250                    has_dynamic_offset: false,
251                    min_binding_size: None,
252                },
253                count: None,
254            }],
255        });
256        let camera_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
257            label: Some("camera.bg"),
258            layout: &camera_bgl,
259            entries: &[wgpu::BindGroupEntry {
260                binding: 0,
261                resource: camera_buf.as_entire_binding(),
262            }],
263        });
264
265        let camera3d = Camera3D::new(width as f32 / height.max(1) as f32);
266        let camera3d_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
267            label: Some("camera3d.buf"),
268            contents: bytemuck::bytes_of(&CameraUniform {
269                view_proj: camera3d.view_proj(),
270            }),
271            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
272        });
273        let camera3d_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
274            label: Some("camera3d.bg"),
275            layout: &camera_bgl,
276            entries: &[wgpu::BindGroupEntry {
277                binding: 0,
278                resource: camera3d_buf.as_entire_binding(),
279            }],
280        });
281
282        let sample_count = msaa_samples.max(1);
283        let textures = TextureRegistry::new(&device, &queue);
284        let sprites = SpriteBatcher::new(
285            &device,
286            format,
287            &camera_bgl,
288            &textures.layout,
289            sample_count,
290            depth_format,
291        );
292        let primitives =
293            PrimitiveBatcher::new(&device, format, &camera_bgl, sample_count, depth_format);
294        let shapes = ShapeBatcher::new(
295            &device,
296            format,
297            &camera_bgl,
298            &textures.layout,
299            sample_count,
300            depth_format,
301        );
302        let text = TextSystem::new(
303            &device,
304            &queue,
305            format,
306            width,
307            height,
308            sample_count,
309            depth_format,
310        );
311        let (msaa_view, depth_view) =
312            make_attachments(&device, [width, height], format, sample_count, depth_format);
313        let meshes = MeshRegistry::new();
314        let mesh_batcher =
315            MeshBatcher::new(&device, format, &camera_bgl, sample_count, depth_format);
316        #[cfg(feature = "vector")]
317        let vector = if supports_compute {
318            Some(VectorPass::new(&device, format, width, height))
319        } else {
320            log::error!("vector graphics disabled: adapter has no compute shaders ({backend:?})");
321            None
322        };
323
324        Ok(Self {
325            device,
326            queue,
327            surface,
328            surface_config,
329            window,
330            camera,
331            camera_buf,
332            camera_bg,
333            sprites,
334            primitives,
335            shapes,
336            text,
337            textures,
338            custom: crate::custom::CustomPasses::default(),
339            clear_color: [0.0, 0.0, 0.0, 1.0],
340            texture_paths: HashMap::new(),
341            sample_count,
342            depth_format,
343            msaa_view,
344            depth_view,
345            camera3d,
346            camera3d_buf,
347            camera3d_bg,
348            meshes,
349            mesh_batcher,
350            #[cfg(feature = "vector")]
351            vector,
352            capture_copy_src,
353            pending_screenshot: None,
354            adapter_info: info,
355            supports_compute,
356            unmet_requirements,
357            canvas,
358            virtual_resolution,
359            blit,
360            post: PostChain::default(),
361            window_scratch: None,
362            start: web_time::Instant::now(),
363            frame_count: 0,
364            builtin_font: None,
365            #[cfg(feature = "indexed")]
366            indexed: None,
367            #[cfg(feature = "indexed")]
368            disable_indexed_after_frame: false,
369            frame_active: false,
370            scale_mode,
371            scale_filter,
372            border_color,
373        })
374    }
375
376    pub fn resize(&mut self, width: u32, height: u32) {
377        if width == 0 || height == 0 {
378            return;
379        }
380        self.surface_config.width = width;
381        self.surface_config.height = height;
382        self.surface.configure(&self.device, &self.surface_config);
383        // With a virtual resolution the drawing size stays put; only where the canvas lands
384        // in the window moves.
385        self.sync_canvas();
386    }
387
388    /// Make the offscreen canvas match the virtual resolution and post effects, and resize
389    /// the render targets if the drawing size changed.
390    fn sync_canvas(&mut self) {
391        let before = self.canvas.as_ref().map(Canvas::size);
392        let (w, h) = self.window_size();
393        let wanted = match self.virtual_resolution {
394            Some((cw, ch)) => Some(([cw.max(1), ch.max(1)], false)),
395            None if !self.post.is_empty() || self.is_indexed() => Some(([w, h], true)),
396            None => None,
397        };
398        let current = self.canvas.as_ref().map(|c| (c.size(), c.follows_window));
399        if current != wanted {
400            self.canvas = wanted.map(|(size, follows_window)| Canvas {
401                tex: RenderTexture::new(
402                    &self.device,
403                    &self.blit,
404                    self.surface_format(),
405                    size,
406                    "canvas",
407                ),
408                follows_window,
409            });
410        }
411        let after = self.canvas.as_ref().map(Canvas::size);
412        // Without a canvas the window size drives everything; resize on every call then.
413        if after != before || after.is_none() {
414            let (w, h) = self.size();
415            self.resize_render_targets(w, h);
416        }
417    }
418
419    /// Resize everything that draws at the render size (the canvas, else the window).
420    fn resize_render_targets(&mut self, width: u32, height: u32) {
421        let (msaa_view, depth_view) = make_attachments(
422            &self.device,
423            [width, height],
424            self.surface_format(),
425            self.sample_count,
426            self.depth_format,
427        );
428        self.msaa_view = msaa_view;
429        self.depth_view = depth_view;
430        self.camera.resize(width as f32, height as f32);
431        self.camera3d.resize(width as f32, height as f32);
432        self.text.resize(&self.queue, width, height);
433        #[cfg(feature = "vector")]
434        if let Some(vector) = self.vector.as_mut() {
435            vector.resize(&self.device, width, height);
436        }
437    }
438
439    /// Draw into a fixed-size canvas of `(width, height)` pixels that is scaled onto the
440    /// window, or (`None`) straight into the window again. Usually set once through
441    /// `AppConfig::virtual_resolution`; changing it at runtime reallocates the canvas.
442    /// See [`GraphicsConfig::virtual_resolution`].
443    ///
444    /// The toolkit's input follows right after the `update` that changed it: the next
445    /// `update` step and `render` already read `mouse_pos` and touches in the new canvas
446    /// pixels. With [`ScaleMode::Integer`], a canvas larger than the window is scaled down to
447    /// fit (fractionally) rather than cropped; use [`Self::set_scale_mode`] for per-resolution
448    /// choices, e.g. `Fit` for 800 x 600 on a 1080p screen.
449    pub fn set_virtual_resolution(&mut self, resolution: Option<(u32, u32)>) {
450        // A zero-sized canvas would give the cameras a zero viewport; use at least 1 x 1.
451        let resolution = resolution.map(|(w, h)| (w.max(1), h.max(1)));
452        if self.virtual_resolution == resolution {
453            return;
454        }
455        self.virtual_resolution = resolution;
456        self.sync_canvas();
457    }
458
459    /// The virtual resolution set by [`Self::set_virtual_resolution`], if any.
460    pub fn virtual_resolution(&self) -> Option<(u32, u32)> {
461        self.virtual_resolution
462    }
463
464    /// Run `effect` on every frame from now on, after the effects already in `stage`. See
465    /// [`PostStage`] for the two stages and [`ShaderEffect`](crate::ShaderEffect) for
466    /// single-shader effects. While any effect is registered, the scene is drawn into an
467    /// offscreen texture (window-sized without a virtual resolution).
468    pub fn add_post_effect(&mut self, stage: PostStage, effect: impl PostEffect) -> PostEffectId {
469        let id = self.post.add(stage, Box::new(effect));
470        self.sync_canvas();
471        id
472    }
473
474    /// Whether the canvas holds palette indices (feature `indexed`); always `false` without it.
475    pub fn is_indexed(&self) -> bool {
476        #[cfg(feature = "indexed")]
477        return self.indexed.is_some();
478        #[cfg(not(feature = "indexed"))]
479        false
480    }
481
482    /// Switch to indexed colour with `palette` in row 0: from the next frame on, the painter
483    /// draws palette indices ([`Color::index`], textures from
484    /// [`Self::load_indexed_texture`]) and the end of the frame looks them up, before post
485    /// effects and the upscale. Works with or without a virtual resolution. Keep the
486    /// [`PaletteCycle`](crate::PaletteCycle)s and rows, reset by [`Self::disable_indexed`].
487    #[cfg(feature = "indexed")]
488    pub fn enable_indexed(&mut self, palette: &crate::Palette) {
489        // The latest call wins: cancel a `disable_indexed` deferred earlier in this frame.
490        self.disable_indexed_after_frame = false;
491        let format = self.surface_format();
492        let state = self.indexed.get_or_insert_with(|| {
493            crate::indexed::IndexedState::new(&self.device, &self.queue, format)
494        });
495        state.set_palette(&self.queue, 0, palette);
496        self.sync_canvas();
497    }
498
499    /// Back to direct RGBA drawing; drops palettes and cycles.
500    #[cfg(feature = "indexed")]
501    pub fn disable_indexed(&mut self) {
502        // During a frame, the canvas already holds indices: keep the palette until that frame
503        // is resolved and presented, then switch.
504        if self.frame_active {
505            self.disable_indexed_after_frame = true;
506            return;
507        }
508        self.indexed = None;
509        self.sync_canvas();
510    }
511
512    /// Fill palette row `row` (`0..PALETTE_ROWS`). Row 0 shows unless
513    /// [`Self::set_palette_blend`] picks others. Takes effect this frame; ignored when not
514    /// indexed.
515    #[cfg(feature = "indexed")]
516    pub fn set_palette(&mut self, row: u32, palette: &crate::Palette) {
517        if let Some(ix) = self.indexed.as_mut() {
518            ix.set_palette(&self.queue, row, palette);
519        }
520    }
521
522    /// Show row `row_a` crossfaded towards `row_b` by `t` (`0` = all `a`, `1` = all `b`), e.g.
523    /// a day-night fade or a damage flash. Default: row 0, no blend.
524    #[cfg(feature = "indexed")]
525    pub fn set_palette_blend(&mut self, row_a: u32, row_b: u32, t: f32) {
526        if let Some(ix) = self.indexed.as_mut() {
527            ix.set_blend(row_a, row_b, t);
528        }
529    }
530
531    /// Start rotating a range of palette entries; `None` when not indexed, when the range is
532    /// empty or runs past index 255, or when
533    /// [`MAX_PALETTE_CYCLES`](crate::MAX_PALETTE_CYCLES) are already running.
534    #[cfg(feature = "indexed")]
535    pub fn add_palette_cycle(
536        &mut self,
537        cycle: crate::PaletteCycle,
538    ) -> Option<crate::PaletteCycleId> {
539        self.indexed.as_mut()?.add_cycle(cycle)
540    }
541
542    /// Stop a palette cycle; returns whether it existed.
543    #[cfg(feature = "indexed")]
544    pub fn remove_palette_cycle(&mut self, id: crate::PaletteCycleId) -> bool {
545        self.indexed.as_mut().is_some_and(|ix| ix.remove_cycle(id))
546    }
547
548    /// Upload an [`IndexedImage`](crate::IndexedImage) as a texture of indices for an indexed
549    /// canvas: draw it like any sprite (untinted) and its pixels come out in the canvas
550    /// palette's colours. Transparent indices (`tRNS` alpha 0) stay transparent.
551    #[cfg(feature = "indexed")]
552    pub fn create_indexed_texture(
553        &mut self,
554        image: &crate::IndexedImage,
555        label: Option<&str>,
556    ) -> TextureId {
557        self.create_texture_rgba(image.width, image.height, &image.index_texels(), label)
558    }
559
560    /// Decode an indexed PNG and upload its indices ([`Self::create_indexed_texture`]);
561    /// returns the texture and the PNG's own palette, e.g. for
562    /// [`Self::enable_indexed`].
563    #[cfg(feature = "indexed")]
564    pub fn load_indexed_texture(
565        &mut self,
566        png: &[u8],
567        label: Option<&str>,
568    ) -> Result<(TextureId, crate::Palette)> {
569        let image = crate::IndexedImage::from_png(png)?;
570        let id = self.create_indexed_texture(&image, label);
571        Ok((id, image.palette))
572    }
573
574    /// Remove an effect; returns whether it existed.
575    pub fn remove_post_effect(&mut self, id: PostEffectId) -> bool {
576        let removed = self.post.remove(id);
577        self.sync_canvas();
578        removed
579    }
580
581    /// Switch an effect off (skipped, keeps its place) or back on. Unknown ids are ignored.
582    pub fn set_post_effect_enabled(&mut self, id: PostEffectId, enabled: bool) {
583        if !self.post.set_enabled(id, enabled) {
584            log::warn!("set_post_effect_enabled: unknown {id:?}");
585        }
586    }
587
588    /// Whether an effect is enabled; `None` for an unknown id.
589    pub fn post_effect_enabled(&self, id: PostEffectId) -> Option<bool> {
590        self.post.is_enabled(id)
591    }
592
593    /// The effect behind `id` as its concrete type, to change its settings, e.g.
594    /// `gfx.post_effect_mut::<ShaderEffect>(id).unwrap().set_params(&p)`. `None` for an
595    /// unknown id or another type.
596    pub fn post_effect_mut<T: PostEffect>(&mut self, id: PostEffectId) -> Option<&mut T> {
597        self.post.get_mut(id)?.downcast_mut::<T>()
598    }
599
600    /// How the canvas is scaled onto the window. Takes effect on the next frame.
601    pub fn set_scale_mode(&mut self, mode: ScaleMode) {
602        self.scale_mode = mode;
603    }
604
605    pub fn scale_mode(&self) -> ScaleMode {
606        self.scale_mode
607    }
608
609    /// Nearest (crisp, default) or linear filtering when scaling the canvas.
610    pub fn set_scale_filter(&mut self, filter: ScaleFilter) {
611        self.scale_filter = filter;
612    }
613
614    pub fn scale_filter(&self) -> ScaleFilter {
615        self.scale_filter
616    }
617
618    /// Colour of the window area around the canvas (letterbox / pillarbox bars).
619    pub fn set_border_color(&mut self, color: impl Into<Color>) {
620        self.border_color = color.into();
621    }
622
623    pub fn border_color(&self) -> Color {
624        self.border_color
625    }
626
627    /// Where the canvas lands in the window, in physical pixels, or `None` without a virtual
628    /// resolution. Use [`CanvasRect::window_to_canvas`] to map window positions; the toolkit's
629    /// `Input` already reports the mouse and touches in canvas pixels.
630    pub fn canvas_rect(&self) -> Option<CanvasRect> {
631        let (w, h) = self.virtual_resolution?;
632        Some(canvas::layout(
633            [self.surface_config.width, self.surface_config.height],
634            [w, h],
635            self.scale_mode,
636        ))
637    }
638
639    /// The offscreen texture the painter draws into, when there is one (a virtual resolution
640    /// is set, post effects are registered or indexed colour is on), for games that record
641    /// their own passes into it (in [`Self::surface_format`], at [`Self::size`]). Draw after
642    /// the painter has flushed (it clears the canvas) and before [`Self::flush_pending`] or
643    /// the end of the frame, which scale it onto the window. To draw under painter content,
644    /// use [`Painter::custom`](crate::Painter::custom) instead.
645    ///
646    /// With indexed colour on ([`Self::is_indexed`]) it holds palette indices, not colours:
647    /// write them with `index_out` from `INDEX_WGSL` (feature `indexed`). An RGBA pass would
648    /// write meaningless indices; run colour effects as a [`PostEffect`] instead, which sees
649    /// the resolved colours.
650    pub fn canvas_view(&self) -> Option<&wgpu::TextureView> {
651        self.canvas.as_ref().map(|c| &c.tex.view)
652    }
653
654    pub fn window(&self) -> &Arc<Window> {
655        &self.window
656    }
657
658    /// The size the painter draws at, in pixels: the virtual resolution when one is set,
659    /// else the window's size. Lay out the game with this.
660    pub fn size(&self) -> (u32, u32) {
661        match &self.canvas {
662            Some(c) => (c.size()[0], c.size()[1]),
663            None => self.window_size(),
664        }
665    }
666
667    /// The window's (surface's) size in physical pixels, whether or not a virtual resolution
668    /// is set. Overlays drawn onto the window itself (egui) use this.
669    pub fn window_size(&self) -> (u32, u32) {
670        (self.surface_config.width, self.surface_config.height)
671    }
672
673    /// The color format the frame is rendered in: always an sRGB format, possibly a view of a
674    /// non-sRGB surface. Build render pipelines that draw into a [`Frame`] with this.
675    pub fn surface_format(&self) -> wgpu::TextureFormat {
676        render_format(&self.surface_config)
677    }
678
679    /// Multisample state matching the painter's render target (`AppConfig::msaa_samples`),
680    /// for pipelines that draw in a [`Painter::custom`](crate::Painter::custom) pass.
681    pub fn multisample(&self) -> wgpu::MultisampleState {
682        crate::target::multisample(self.sample_count)
683    }
684
685    /// Depth-stencil state matching the painter's render target, for pipelines that draw in
686    /// a [`Painter::custom`](crate::Painter::custom) pass: `None` without a depth buffer, else
687    /// one that neither tests nor writes depth.
688    pub fn depth_stencil(&self) -> Option<wgpu::DepthStencilState> {
689        self.depth_format.map(crate::target::no_write_depth)
690    }
691
692    pub fn device(&self) -> &wgpu::Device {
693        &self.device
694    }
695    pub fn queue(&self) -> &wgpu::Queue {
696        &self.queue
697    }
698
699    /// The GPU adapter the device was created on: its name, vendor and backend.
700    pub fn adapter_info(&self) -> &wgpu::AdapterInfo {
701        &self.adapter_info
702    }
703
704    /// The graphics backend in use, e.g. `Vulkan`, `Metal`, `BrowserWebGpu` or `Gl`
705    /// (WebGL2 on the web).
706    pub fn backend(&self) -> wgpu::Backend {
707        self.adapter_info.backend
708    }
709
710    /// Whether compute shaders are available. `false` on WebGL2, the web fallback when the
711    /// browser has no WebGPU; check this in `Game::init` to show a "WebGPU required" message
712    /// instead of failing. The device's limits and features are on [`Self::device`].
713    pub fn supports_compute(&self) -> bool {
714        self.supports_compute
715    }
716
717    /// The required limits and features the device was created without, when
718    /// `continue_without_requirements` is set and the adapter couldn't provide them; `None`
719    /// when everything was met. Check it in `Game::init` to show a "this browser/GPU is not
720    /// supported" screen through the painter instead of failing to start.
721    pub fn unmet_requirements(&self) -> Option<&crate::DeviceRequirementsError> {
722        self.unmet_requirements.as_ref()
723    }
724
725    /// Load a PNG from disk and remember its path for [`Graphics::refresh_textures`]. Not
726    /// available on the web; use [`Graphics::load_texture_bytes`] (or `ctx.load_texture`,
727    /// which picks the right one per platform).
728    #[cfg(not(target_arch = "wasm32"))]
729    pub fn load_texture(&mut self, path: impl AsRef<Path>) -> Result<TextureId> {
730        let path = path.as_ref();
731        let id = self.textures.load_file(&self.device, &self.queue, path)?;
732        let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
733        self.texture_paths.insert(canon, id);
734        Ok(id)
735    }
736
737    /// Size `[width, height]` in pixels that `s` takes up drawn at `size_px` (the widest line,
738    /// and the height of all lines), for aligning text or sizing a box to fit. Uses the same
739    /// layout as drawing, so the result matches `Painter::text` exactly; measuring and then
740    /// drawing the same string shapes it once.
741    pub fn measure_text(&mut self, s: &str, size_px: f32) -> [f32; 2] {
742        self.text.measure(s, size_px)
743    }
744
745    /// The built-in 5 × 7 pixel font (6 × 8 cells, printable ASCII), uploaded on first use.
746    /// [`Painter::pixel_text`](crate::Painter::pixel_text) draws with it; use this to
747    /// [`measure`](BitmapFont::measure) text.
748    pub fn builtin_font(&mut self) -> &BitmapFont {
749        if self.builtin_font.is_none() {
750            self.builtin_font = Some(BitmapFont::builtin(self));
751        }
752        self.builtin_font.as_ref().expect("just created")
753    }
754
755    /// Register an extra font face (TTF/OTF bytes) as a glyph fallback for text rendering.
756    pub fn load_font_bytes(&mut self, bytes: Vec<u8>) {
757        self.text.load_font_bytes(bytes);
758    }
759
760    /// Decode a PNG held in memory and upload it. `label` names the texture in GPU debuggers
761    /// and error messages.
762    pub fn load_texture_bytes(&mut self, bytes: &[u8], label: Option<&str>) -> Result<TextureId> {
763        self.textures
764            .load_bytes(&self.device, &self.queue, bytes, label)
765    }
766
767    /// Re-decode any texture whose backing file is in `changed_paths` and update its bind
768    /// group in place. Returns the number of textures actually reloaded.
769    pub fn refresh_textures<I, P>(&mut self, changed_paths: I) -> usize
770    where
771        I: IntoIterator<Item = P>,
772        P: AsRef<Path>,
773    {
774        let mut n = 0;
775        for p in changed_paths {
776            let canon = p
777                .as_ref()
778                .canonicalize()
779                .unwrap_or_else(|_| p.as_ref().to_path_buf());
780            if let Some(&id) = self.texture_paths.get(&canon) {
781                match self.textures.reload(&self.device, &self.queue, id, &canon) {
782                    Ok(()) => n += 1,
783                    Err(e) => log::warn!("reload {} failed: {e:?}", canon.display()),
784                }
785            }
786        }
787        n
788    }
789
790    pub fn create_texture_rgba(
791        &mut self,
792        width: u32,
793        height: u32,
794        rgba: &[u8],
795        label: Option<&str>,
796    ) -> TextureId {
797        self.textures
798            .create_from_rgba(&self.device, &self.queue, width, height, rgba, label)
799    }
800
801    /// Overwrite the pixels of an existing texture without reallocating it, for content that
802    /// changes every frame (a software-rendered framebuffer, a video frame). `rgba` must hold
803    /// `width * height * 4` bytes for the texture's size; create the texture once with
804    /// [`Self::create_texture_rgba`]. Unknown ids and size mismatches are logged and ignored.
805    pub fn update_texture_rgba(&mut self, id: TextureId, rgba: &[u8]) {
806        if !self.textures.write_rgba(&self.queue, id, rgba) {
807            log::warn!("update_texture_rgba: unknown or unwritable {id:?}, or wrong data size");
808        }
809    }
810
811    /// Make a texture the game created itself drawable like any other [`TextureId`]: the
812    /// output of a compute shader, a render target, a video frame decoded on the GPU. The
813    /// registry takes ownership of `texture` and samples it through `view` (nearest filtering,
814    /// [`TextureWrap::Clamp`] until changed with [`Self::set_texture_wrap`]).
815    ///
816    /// `texture` needs [`wgpu::TextureUsages::TEXTURE_BINDING`] and `view` a filterable float
817    /// format with a 2D dimension. The sprite shader reads `view` as linear color, so use an
818    /// sRGB view (e.g. `Rgba8UnormSrgb`) for content authored in sRGB like PNG pixels. To
819    /// write such a texture from a compute shader, create it as `Rgba8Unorm` with
820    /// `STORAGE_BINDING` and `view_formats: &[Rgba8UnormSrgb]`, store through an `Rgba8Unorm`
821    /// view and register an `Rgba8UnormSrgb` one created with `usage:
822    /// Some(TEXTURE_BINDING)` (an sRGB view can't carry `STORAGE_BINDING`).
823    /// [`Self::update_texture_rgba`] only writes textures with `COPY_DST` and 4 bytes per
824    /// texel; others are logged and skipped.
825    pub fn register_texture(
826        &mut self,
827        texture: wgpu::Texture,
828        view: wgpu::TextureView,
829        label: Option<&str>,
830    ) -> TextureId {
831        self.textures.register(&self.device, texture, view, label)
832    }
833
834    pub fn white_texture(&self) -> TextureId {
835        self.textures.white()
836    }
837
838    /// Choose what `id` shows for UVs outside `0..1`: [`TextureWrap::Clamp`] (the default for
839    /// every texture) stretches the edge texels, [`TextureWrap::Repeat`] and
840    /// [`TextureWrap::MirrorRepeat`] tile. Set `Repeat` on a texture to tile it across one
841    /// quad (`Painter::quad_textured` with UVs up to `[4.0, 10.0]`, a sprite with
842    /// [`crate::SpriteInstance::with_uv`] beyond `1.0`) instead of drawing a quad per tile.
843    /// Keep atlases on `Clamp`: their regions sample right up to the edge, and with wrapping
844    /// a UV of exactly `1.0` bleeds in the opposite edge. The mode survives hot-reloads.
845    /// Unknown ids are ignored.
846    pub fn set_texture_wrap(&mut self, id: TextureId, wrap: TextureWrap) {
847        if !self.textures.set_wrap(&self.device, id, wrap) {
848            log::warn!("set_texture_wrap: unknown {id:?}");
849        }
850    }
851
852    /// The [`TextureWrap`] mode of `id`, or `None` for an unknown id.
853    pub fn texture_wrap(&self, id: TextureId) -> Option<TextureWrap> {
854        self.textures.get(id).map(|t| t.wrap)
855    }
856
857    /// Pixel size `[width, height]` of a loaded texture, or `None` for an unknown id. Follows
858    /// hot-reloads, so it reports the size of the current contents.
859    pub fn texture_size(&self, id: TextureId) -> Option<[u32; 2]> {
860        self.textures.get(id).map(|t| t.size)
861    }
862
863    /// Upload a static mesh (positions + normals, indexed) and return its handle. Meshes are
864    /// drawn depth-tested via [`Graphics::draw_mesh`] using the perspective [`Self::camera3d`].
865    pub fn create_mesh(&mut self, vertices: &[MeshVertex], indices: &[u16]) -> MeshId {
866        self.meshes.create(&self.device, vertices, indices)
867    }
868
869    /// Queue one instance of `mesh` for this frame. Meshes render before the 2D layers, so
870    /// 2D sprites, primitives and text draw on top of them.
871    pub fn draw_mesh(&mut self, mesh: MeshId, instance: MeshInstance) {
872        self.mesh_batcher.push(mesh, instance);
873    }
874
875    /// Save a PNG of the next presented frame to `path`. The read-back stalls that frame, so
876    /// this is for debug/dev use. A no-op (with a warning) if the surface lacks `COPY_SRC`,
877    /// and on the web, where there is neither a blocking read-back nor a filesystem.
878    pub fn request_screenshot(&mut self, path: impl Into<PathBuf>) {
879        if cfg!(target_arch = "wasm32") {
880            log::warn!("screenshot unsupported on the web");
881            return;
882        }
883        if !self.capture_copy_src {
884            log::warn!("screenshot unsupported: surface does not allow COPY_SRC read-back");
885            return;
886        }
887        self.pending_screenshot = Some(path.into());
888    }
889
890    pub fn begin_frame(&mut self) -> Result<Frame> {
891        self.frame_count = self.frame_count.wrapping_add(1);
892        self.queue.write_buffer(
893            &self.camera_buf,
894            0,
895            bytemuck::bytes_of(&CameraUniform {
896                view_proj: self.camera.view_proj(),
897            }),
898        );
899        self.queue.write_buffer(
900            &self.camera3d_buf,
901            0,
902            bytemuck::bytes_of(&CameraUniform {
903                view_proj: self.camera3d.view_proj(),
904            }),
905        );
906
907        let surface_texture = match self.surface.get_current_texture() {
908            wgpu::CurrentSurfaceTexture::Success(t)
909            | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
910            other => anyhow::bail!("surface unavailable: {other:?}"),
911        };
912        let view = surface_texture
913            .texture
914            .create_view(&wgpu::TextureViewDescriptor {
915                format: Some(render_format(&self.surface_config)),
916                ..Default::default()
917            });
918        let encoder = self
919            .device
920            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
921                label: Some("frame.encoder"),
922            });
923        self.frame_active = true;
924        Ok(Frame {
925            encoder: Some(encoder),
926            view,
927            surface_texture: Some(surface_texture),
928            canvas: self.canvas.as_ref().map(|c| c.tex.view.clone()),
929            canvas_size: self.canvas.as_ref().map_or([0, 0], Canvas::size),
930            canvas_virtual: self.virtual_resolution.is_some(),
931            clear_color: self.clear_color,
932            flushed: false,
933            composited: false,
934            indexed: self.is_indexed(),
935        })
936    }
937
938    pub fn present(&mut self, mut frame: Frame) {
939        if !frame.flushed {
940            self.flush_into(&mut frame);
941        }
942        self.composite(&mut frame);
943
944        if let Some(encoder) = frame.encoder.take() {
945            self.queue.submit(std::iter::once(encoder.finish()));
946        }
947
948        // Capture the just-submitted surface before presenting (the queue runs the read-back
949        // copy after the render submit, so it sees the finished frame).
950        let shot_path = self.pending_screenshot.take();
951        if let (Some(path), Some(st)) = (&shot_path, frame.surface_texture.as_ref()) {
952            self.save_screenshot(&st.texture, path);
953        }
954
955        if let Some(st) = frame.surface_texture.take() {
956            self.window.pre_present_notify();
957            self.queue.present(st);
958        }
959        self.frame_active = false;
960        #[cfg(feature = "indexed")]
961        if std::mem::take(&mut self.disable_indexed_after_frame) {
962            self.disable_indexed();
963        }
964    }
965
966    #[cfg(target_arch = "wasm32")]
967    fn save_screenshot(&self, _texture: &wgpu::Texture, _path: &Path) {}
968
969    /// Read the surface texture back to the CPU and save it. Synchronous (blocks on a poll),
970    /// so only used for the occasional debug screenshot. `miniscreenshot-wgpu` handles the
971    /// staging copy, row-padding strip, and BGRA->RGBA conversion.
972    #[cfg(not(target_arch = "wasm32"))]
973    fn save_screenshot(&self, texture: &wgpu::Texture, path: &Path) {
974        match miniscreenshot_wgpu::capture(&self.device, &self.queue, texture) {
975            Ok(shot) => match shot.save(path) {
976                Ok(()) => log::info!("saved screenshot {}", path.display()),
977                Err(e) => log::warn!("screenshot save failed: {e}"),
978            },
979            Err(e) => log::warn!("screenshot capture failed: {e}"),
980        }
981    }
982
983    /// Flush all queued 2D layers (sprites + shapes + primitives + text) into the current
984    /// `frame` and, with a virtual resolution, scale the canvas onto the window. Call it
985    /// before stacking passes that draw onto the window itself (e.g. egui); after it, the
986    /// window view holds the finished scene. The end of the frame does the same if needed.
987    pub fn flush_pending(&mut self, frame: &mut Frame) {
988        self.flush_into(frame);
989        self.composite(frame);
990    }
991
992    /// Run the post effects and scale the canvas onto the window, once per frame; nothing
993    /// when the frame was drawn straight into the window.
994    fn composite(&mut self, frame: &mut Frame) {
995        if frame.composited {
996            return;
997        }
998        frame.composited = true;
999        // A canvas created mid-frame (an effect added during `render`) wasn't drawn into.
1000        if frame.canvas.is_none() {
1001            return;
1002        }
1003        let window = [self.surface_config.width, self.surface_config.height];
1004        let rect = self.canvas_rect().unwrap_or(CanvasRect {
1005            origin: [0.0, 0.0],
1006            size: [window[0] as f32, window[1] as f32],
1007        });
1008        let format = self.surface_format();
1009        let b = self.border_color;
1010        let border = wgpu::Color {
1011            r: srgb_to_linear(b.r),
1012            g: srgb_to_linear(b.g),
1013            b: srgb_to_linear(b.b),
1014            a: f64::from(b.a),
1015        };
1016        let Some(encoder) = frame.encoder.as_mut() else {
1017            return;
1018        };
1019        // The canvas was removed or replaced during the frame (the last effect removed, the
1020        // virtual resolution changed): show what was drawn into the frame's canvas, without
1021        // effects, rather than an empty frame.
1022        let canvas = self
1023            .canvas
1024            .as_ref()
1025            .filter(|c| frame.canvas.as_ref() == Some(&c.tex.view));
1026        let Some(canvas) = canvas else {
1027            if let Some(view) = &frame.canvas {
1028                let rect = if frame.canvas_virtual {
1029                    canvas::layout(window, frame.canvas_size, self.scale_mode)
1030                } else {
1031                    CanvasRect {
1032                        origin: [0.0, 0.0],
1033                        size: [window[0] as f32, window[1] as f32],
1034                    }
1035                };
1036                let groups = self.blit.bind_groups(&self.device, view);
1037                self.blit.draw(
1038                    encoder,
1039                    &groups[self.scale_filter.index()],
1040                    &frame.view,
1041                    rect,
1042                    border,
1043                );
1044            }
1045            return;
1046        };
1047        let run = StageRun {
1048            device: &self.device,
1049            queue: &self.queue,
1050            blit: &self.blit,
1051            format,
1052            globals: post::globals(
1053                canvas.size(),
1054                window,
1055                rect,
1056                self.start.elapsed().as_secs_f32(),
1057                self.frame_count,
1058            ),
1059            indices: None,
1060            textures: &self.textures,
1061        };
1062
1063        // Indexed canvas: indices become colour before any effect sees the scene.
1064        #[cfg(feature = "indexed")]
1065        if let Some(ix) = self.indexed.as_mut() {
1066            ix.resolve(
1067                &run,
1068                encoder,
1069                &canvas.tex.view,
1070                canvas.size(),
1071                self.start.elapsed().as_secs_f64(),
1072            );
1073        }
1074        #[cfg(feature = "indexed")]
1075        let base = self
1076            .indexed
1077            .as_ref()
1078            .and_then(|ix| ix.resolved.as_ref())
1079            .unwrap_or(&canvas.tex);
1080        #[cfg(not(feature = "indexed"))]
1081        let base = &canvas.tex;
1082        // Effects in both stages can read the indices behind the resolved colours.
1083        #[cfg(feature = "indexed")]
1084        let run = StageRun {
1085            indices: self.indexed.is_some().then_some(&canvas.tex.view),
1086            ..run
1087        };
1088
1089        let post = &mut self.post;
1090        let canvas_out = post.run(PostStage::Canvas, &run, encoder, &base.view, canvas.size());
1091        let scene = match canvas_out {
1092            Some(out) => post.output(PostStage::Canvas, out).unwrap_or(base),
1093            None => base,
1094        };
1095
1096        if !post.active(PostStage::Window) {
1097            self.blit.draw(
1098                encoder,
1099                &scene.blit_bind_groups[self.scale_filter.index()],
1100                &frame.view,
1101                rect,
1102                border,
1103            );
1104            return;
1105        }
1106
1107        // Window stage: it needs the upscaled frame, border included, as a texture. A
1108        // window-sized canvas already is that.
1109        let scene_view = scene.view.clone();
1110        let window_in = if canvas.follows_window {
1111            scene_view
1112        } else {
1113            if self.window_scratch.as_ref().map(|t| t.size) != Some(window) {
1114                self.window_scratch = Some(RenderTexture::new(
1115                    &self.device,
1116                    &self.blit,
1117                    format,
1118                    window,
1119                    "post.window.input",
1120                ));
1121            }
1122            let scratch = self.window_scratch.as_ref().expect("just created");
1123            self.blit.draw(
1124                encoder,
1125                &scene.blit_bind_groups[self.scale_filter.index()],
1126                &scratch.view,
1127                rect,
1128                border,
1129            );
1130            scratch.view.clone()
1131        };
1132        let full = CanvasRect {
1133            origin: [0.0, 0.0],
1134            size: [window[0] as f32, window[1] as f32],
1135        };
1136        if let Some(out) = post.run(PostStage::Window, &run, encoder, &window_in, window)
1137            && let Some(result) = post.output(PostStage::Window, out)
1138        {
1139            self.blit.draw(
1140                encoder,
1141                &result.blit_bind_groups[ScaleFilter::Nearest.index()],
1142                &frame.view,
1143                full,
1144                wgpu::Color::BLACK,
1145            );
1146        }
1147    }
1148
1149    pub(crate) fn flush_into(&mut self, frame: &mut Frame) {
1150        if frame.flushed {
1151            return;
1152        }
1153        let Some(encoder) = frame.encoder.as_mut() else {
1154            return;
1155        };
1156        // Interleave sprites, filled shapes, circles and layered text by layer so a circle on
1157        // layer -1 draws under a sprite on layer 0; within a layer, sprites draw first, then
1158        // shapes, then circles, then text. Unlayered text (`Painter::text`) draws on top.
1159        let mut layers = std::collections::BTreeSet::new();
1160        self.sprites.collect_layers(&mut layers);
1161        self.shapes.collect_layers(&mut layers);
1162        self.primitives.collect_layers(&mut layers);
1163        self.text.collect_layers(&mut layers);
1164        self.custom.collect_layers(&mut layers);
1165
1166        // Upload each batcher's instances exactly once before drawing: `queue.write_buffer`
1167        // is not part of the encoder command stream, so writing per layer pass would clobber
1168        // earlier layers' instance data.
1169        self.sprites.upload(&self.device, &self.queue);
1170        self.shapes.upload(&self.device, &self.queue);
1171        self.primitives.upload(&self.device, &self.queue);
1172        self.text.prepare(&self.device, &self.queue);
1173
1174        // Draws go to the canvas when there is one, else to the surface. When multisampling,
1175        // they target the MSAA texture and resolve to that; otherwise they target it directly.
1176        let target = frame.canvas.as_ref().unwrap_or(&frame.view);
1177        let (color, resolve) = match self.msaa_view.as_ref() {
1178            Some(msaa) => (msaa, Some(target)),
1179            None => (target, None),
1180        };
1181        let targets = Targets {
1182            color,
1183            resolve,
1184            depth: self.depth_view.as_ref(),
1185        };
1186
1187        // Clear color (and depth) once up front; every layer pass then loads onto it.
1188        let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1189            label: Some("clear.pass"),
1190            color_attachments: &[Some(targets.color_attachment(wgpu::LoadOp::Clear(
1191                wgpu::Color {
1192                    // sRGB like every painter color; the clear value is linear.
1193                    r: srgb_to_linear(frame.clear_color[0]),
1194                    g: srgb_to_linear(frame.clear_color[1]),
1195                    b: srgb_to_linear(frame.clear_color[2]),
1196                    a: frame.clear_color[3] as f64,
1197                },
1198            )))],
1199            depth_stencil_attachment: targets.depth_attachment(wgpu::LoadOp::Clear(1.0)),
1200            occlusion_query_set: None,
1201            timestamp_writes: None,
1202            multiview_mask: None,
1203        });
1204
1205        // 3D meshes draw first, depth-tested, so the 2D layers (which do not write depth)
1206        // composite on top of them.
1207        self.mesh_batcher.draw(
1208            &self.device,
1209            &self.queue,
1210            &self.meshes,
1211            encoder,
1212            &targets,
1213            &self.camera3d_bg,
1214        );
1215
1216        let size = frame.canvas.as_ref().map_or(
1217            [self.surface_config.width, self.surface_config.height],
1218            |_| frame.canvas_size,
1219        );
1220        for &layer in &layers {
1221            // Custom passes first: within a layer they draw under the painter's own content.
1222            for f in self.custom.take_layer(layer) {
1223                f(&mut CustomPass {
1224                    device: &self.device,
1225                    queue: &self.queue,
1226                    encoder,
1227                    targets,
1228                    format: self.surface_format(),
1229                    sample_count: self.sample_count,
1230                    depth_format: self.depth_format,
1231                    size,
1232                    indexed: frame.indexed,
1233                });
1234            }
1235            self.sprites
1236                .draw_layer(layer, encoder, &targets, &self.camera_bg, &self.textures);
1237            self.shapes
1238                .draw_layer(layer, encoder, &targets, &self.camera_bg, &self.textures);
1239            self.primitives
1240                .draw_layer(layer, encoder, &targets, &self.camera_bg);
1241            self.text.draw_layer(Some(layer), encoder, &targets);
1242        }
1243
1244        self.text.draw_layer(None, encoder, &targets);
1245        self.text.finish();
1246
1247        // Vector content composites on top of the 2D layers, directly onto the target.
1248        #[cfg(feature = "vector")]
1249        if let Some(vector) = self.vector.as_mut() {
1250            vector.render_and_composite(&self.device, &self.queue, encoder, target);
1251        }
1252
1253        self.sprites.clear();
1254        self.shapes.clear();
1255        self.primitives.clear();
1256        self.mesh_batcher.clear();
1257        self.custom.clear();
1258        frame.flushed = true;
1259    }
1260}
1261
1262/// Allocate the MSAA color target (when `sample_count > 1`) and depth target (when a depth
1263/// format is set), both `size` pixels. Returns `(msaa_view, depth_view)`.
1264fn make_attachments(
1265    device: &wgpu::Device,
1266    size: [u32; 2],
1267    color_format: wgpu::TextureFormat,
1268    sample_count: u32,
1269    depth_format: Option<wgpu::TextureFormat>,
1270) -> (Option<wgpu::TextureView>, Option<wgpu::TextureView>) {
1271    let size = wgpu::Extent3d {
1272        width: size[0].max(1),
1273        height: size[1].max(1),
1274        depth_or_array_layers: 1,
1275    };
1276    let msaa_view = (sample_count > 1).then(|| {
1277        device
1278            .create_texture(&wgpu::TextureDescriptor {
1279                label: Some("msaa.color"),
1280                size,
1281                mip_level_count: 1,
1282                sample_count,
1283                dimension: wgpu::TextureDimension::D2,
1284                format: color_format,
1285                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1286                view_formats: &[],
1287            })
1288            .create_view(&wgpu::TextureViewDescriptor::default())
1289    });
1290    let depth_view = depth_format.map(|format| {
1291        device
1292            .create_texture(&wgpu::TextureDescriptor {
1293                label: Some("depth"),
1294                size,
1295                mip_level_count: 1,
1296                sample_count,
1297                dimension: wgpu::TextureDimension::D2,
1298                format,
1299                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1300                view_formats: &[],
1301            })
1302            .create_view(&wgpu::TextureViewDescriptor::default())
1303    });
1304    (msaa_view, depth_view)
1305}
1306
1307/// The sRGB format frames are rendered in: the surface's own if it is sRGB, otherwise the
1308/// sRGB view format `Graphics::new` added.
1309fn render_format(config: &wgpu::SurfaceConfiguration) -> wgpu::TextureFormat {
1310    if config.format.is_srgb() {
1311        return config.format;
1312    }
1313    config
1314        .view_formats
1315        .iter()
1316        .copied()
1317        .find(wgpu::TextureFormat::is_srgb)
1318        .unwrap_or(config.format)
1319}
1320
1321/// Decode one sRGB color component to linear, as the shaders do for painter colors.
1322pub(crate) fn srgb_to_linear(c: f32) -> f64 {
1323    let c = f64::from(c);
1324    if c <= 0.04045 {
1325        c / 12.92
1326    } else {
1327        ((c + 0.055) / 1.055).powf(2.4)
1328    }
1329}