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 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 msaa_view: Option<wgpu::TextureView>,
50 depth_view: Option<wgpu::TextureView>,
52 pub camera3d: Camera3D,
54 camera3d_buf: wgpu::Buffer,
55 camera3d_bg: wgpu::BindGroup,
56 meshes: MeshRegistry,
57 mesh_batcher: MeshBatcher,
58 #[cfg(feature = "vector")]
61 pub(crate) vector: Option<VectorPass>,
62 capture_copy_src: bool,
64 pending_screenshot: Option<PathBuf>,
66 adapter_info: wgpu::AdapterInfo,
67 supports_compute: bool,
68 unmet_requirements: Option<crate::DeviceRequirementsError>,
70 canvas: Option<Canvas>,
73 virtual_resolution: Option<(u32, u32)>,
74 blit: Blit,
75 post: PostChain,
76 window_scratch: Option<RenderTexture>,
78 start: web_time::Instant,
79 frame_count: u32,
80 #[cfg(feature = "indexed")]
81 indexed: Option<crate::indexed::IndexedState>,
82 #[cfg(feature = "indexed")]
84 disable_indexed_after_frame: bool,
85 frame_active: bool,
87 pub(crate) builtin_font: Option<BitmapFont>,
89 scale_mode: ScaleMode,
90 scale_filter: ScaleFilter,
91 border_color: Color,
92}
93
94impl Graphics {
95 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 idesc.backends = if cfg!(target_arch = "wasm32") {
115 wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL
116 } else {
117 wgpu::Backends::PRIMARY
118 };
119 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 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 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 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 self.sync_canvas();
386 }
387
388 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 if after != before || after.is_none() {
414 let (w, h) = self.size();
415 self.resize_render_targets(w, h);
416 }
417 }
418
419 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 pub fn set_virtual_resolution(&mut self, resolution: Option<(u32, u32)>) {
450 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 pub fn virtual_resolution(&self) -> Option<(u32, u32)> {
461 self.virtual_resolution
462 }
463
464 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 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 #[cfg(feature = "indexed")]
488 pub fn enable_indexed(&mut self, palette: &crate::Palette) {
489 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 #[cfg(feature = "indexed")]
501 pub fn disable_indexed(&mut self) {
502 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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 pub fn post_effect_enabled(&self, id: PostEffectId) -> Option<bool> {
590 self.post.is_enabled(id)
591 }
592
593 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 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 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 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 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 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 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 pub fn window_size(&self) -> (u32, u32) {
670 (self.surface_config.width, self.surface_config.height)
671 }
672
673 pub fn surface_format(&self) -> wgpu::TextureFormat {
676 render_format(&self.surface_config)
677 }
678
679 pub fn multisample(&self) -> wgpu::MultisampleState {
682 crate::target::multisample(self.sample_count)
683 }
684
685 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 pub fn adapter_info(&self) -> &wgpu::AdapterInfo {
701 &self.adapter_info
702 }
703
704 pub fn backend(&self) -> wgpu::Backend {
707 self.adapter_info.backend
708 }
709
710 pub fn supports_compute(&self) -> bool {
714 self.supports_compute
715 }
716
717 pub fn unmet_requirements(&self) -> Option<&crate::DeviceRequirementsError> {
722 self.unmet_requirements.as_ref()
723 }
724
725 #[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 pub fn measure_text(&mut self, s: &str, size_px: f32) -> [f32; 2] {
742 self.text.measure(s, size_px)
743 }
744
745 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 pub fn load_font_bytes(&mut self, bytes: Vec<u8>) {
757 self.text.load_font_bytes(bytes);
758 }
759
760 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 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 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 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 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 pub fn texture_wrap(&self, id: TextureId) -> Option<TextureWrap> {
854 self.textures.get(id).map(|t| t.wrap)
855 }
856
857 pub fn texture_size(&self, id: TextureId) -> Option<[u32; 2]> {
860 self.textures.get(id).map(|t| t.size)
861 }
862
863 pub fn create_mesh(&mut self, vertices: &[MeshVertex], indices: &[u16]) -> MeshId {
866 self.meshes.create(&self.device, vertices, indices)
867 }
868
869 pub fn draw_mesh(&mut self, mesh: MeshId, instance: MeshInstance) {
872 self.mesh_batcher.push(mesh, instance);
873 }
874
875 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 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 #[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 pub fn flush_pending(&mut self, frame: &mut Frame) {
988 self.flush_into(frame);
989 self.composite(frame);
990 }
991
992 fn composite(&mut self, frame: &mut Frame) {
995 if frame.composited {
996 return;
997 }
998 frame.composited = true;
999 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 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 #[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 #[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 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 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 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 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 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 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 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 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 #[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
1262fn 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
1307fn 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
1321pub(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}