diff --git a/Cargo.lock b/Cargo.lock index 3abe17ef9d6b..7551a9313714 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,7 +443,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.0.1", + "futures-lite 2.1.0", "parking", "polling 3.3.0", "rustix 0.38.24", @@ -1944,14 +1944,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", - "windows-sys 0.48.0", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", ] [[package]] @@ -2142,9 +2142,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb" +checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" dependencies = [ "futures-core", "pin-project-lite", @@ -2856,9 +2856,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kqueue" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ "kqueue-sys", "libc", @@ -2866,9 +2866,9 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", @@ -3456,20 +3456,21 @@ dependencies = [ [[package]] name = "notify" -version = "6.0.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d9ba6c734de18ca27c8cef5cd7058aa4ac9f63596131e4c7e41e579319032a2" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.1", "crossbeam-channel", "filetime", "fsevent-sys", "inotify", "kqueue", "libc", + "log", "mio", "walkdir", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -3717,9 +3718,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" @@ -4676,6 +4677,8 @@ dependencies = [ "arrow2", "bitflags 2.4.1", "bytemuck", + "cfg-if", + "cfg_aliases", "clean-path", "crossbeam", "document-features", @@ -4704,6 +4707,7 @@ dependencies = [ "type-map", "unindent", "walkdir", + "wasm-bindgen-futures", "wgpu", "wgpu-core", ] @@ -5270,6 +5274,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -7307,6 +7320,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -7337,6 +7359,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -7349,6 +7386,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -7361,6 +7404,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -7373,6 +7422,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -7385,6 +7440,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -7397,6 +7458,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -7409,6 +7476,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -7421,6 +7494,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winit" version = "0.28.7" diff --git a/Cargo.toml b/Cargo.toml index a7a0bc8f1b00..2ee2ed9b327b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ bytemuck = { version = "1.11", features = ["extern_crate_alloc"] } camino = "1.1" cargo_metadata = "0.18" cargo-run-wasm = "0.3.2" +cfg_aliases = "0.1" cfg-if = "1.0" clang-format = "0.3" clap = "4.0" diff --git a/crates/re_renderer/Cargo.toml b/crates/re_renderer/Cargo.toml index bc0e35c75ebe..b8d8fe5a4413 100644 --- a/crates/re_renderer/Cargo.toml +++ b/crates/re_renderer/Cargo.toml @@ -39,7 +39,7 @@ import-gltf = ["dep:gltf"] serde = ["dep:serde"] ## Render using webgl instead of webgpu on wasm builds. -webgl = ["wgpu/webgl"] +webgl = ["wgpu/webgl", "dep:wgpu-core"] [dependencies] re_error.workspace = true @@ -50,6 +50,7 @@ ahash.workspace = true anyhow.workspace = true bitflags.workspace = true bytemuck.workspace = true +cfg-if.workspace = true clean-path.workspace = true document-features.workspace = true ecolor = { workspace = true, features = ["bytemuck"] } @@ -67,13 +68,13 @@ static_assertions.workspace = true thiserror.workspace = true type-map.workspace = true wgpu.workspace = true -wgpu-core.workspace = true # optional arrow2 = { workspace = true, optional = true } gltf = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"], optional = true } tobj = { workspace = true, optional = true } +wgpu-core = { workspace = true, optional = true } # Needed for error handling when wgpu-core implemented backend is used. # native [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -81,18 +82,21 @@ crossbeam.workspace = true notify.workspace = true wgpu-core.workspace = true -# For examples: +# webgpu +[target.'cfg(all(target_arch = "wasm32", not(features = "webgl")))'.dependencies] +wasm-bindgen-futures.workspace = true + [dev-dependencies] unindent.workspace = true # For build.rs: [build-dependencies] - # Rerun re_build_tools.workspace = true # External anyhow.workspace = true +cfg_aliases.workspace = true clean-path.workspace = true pathdiff.workspace = true walkdir.workspace = true diff --git a/crates/re_renderer/build.rs b/crates/re_renderer/build.rs index ed43154880c6..012be5fbd2a3 100644 --- a/crates/re_renderer/build.rs +++ b/crates/re_renderer/build.rs @@ -119,6 +119,24 @@ fn should_run() -> bool { } fn main() { + // TODO(andreas): Create an upstream PR `cfg_aliases` to fix this. + // Workaround for CARGO_CFG_DEBUG_ASSERTIONS not being set as expected. + // `cfg_aliases` relies on this. + if std::env::var("PROFILE") == Ok("debug".to_owned()) { + std::env::set_var("CARGO_CFG_DEBUG_ASSERTIONS", "1"); + } + + #[allow(clippy::str_to_string)] + // TODO(andreas): Create an upstream PR to `cfg_aliases` fix this. + { + cfg_aliases::cfg_aliases! { + native: { not(target_arch = "wasm32") }, + webgl: { all(not(native), feature = "webgl") }, + webgpu: { all(not(webgl), not(native)) }, + load_shaders_from_disk: { all(native, debug_assertions) } // Shader reloading is only supported on native-debug currently. + } + } + if !should_run() { return; } diff --git a/crates/re_renderer/src/config.rs b/crates/re_renderer/src/config.rs index a8d264252669..25e75d47b113 100644 --- a/crates/re_renderer/src/config.rs +++ b/crates/re_renderer/src/config.rs @@ -153,10 +153,7 @@ pub struct RenderContextConfig { /// /// Other backend might work as well, but lack of support isn't regarded as a bug. pub fn supported_backends() -> wgpu::Backends { - if cfg!(target_arch = "wasm32") { - // Web - WebGL is used automatically when wgpu is compiled with `webgl` feature. - wgpu::Backends::GL | wgpu::Backends::BROWSER_WEBGPU - } else { + if cfg!(native) { // Native. // Only use Vulkan & Metal unless explicitly told so since this reduces surfaces and thus surprises. // @@ -167,5 +164,8 @@ pub fn supported_backends() -> wgpu::Backends { // For changing the backend we use standard wgpu env var, i.e. WGPU_BACKEND. wgpu::util::backend_bits_from_env() .unwrap_or(wgpu::Backends::VULKAN | wgpu::Backends::METAL) + } else { + // Web - WebGL is used automatically when wgpu is compiled with `webgl` feature. + wgpu::Backends::GL | wgpu::Backends::BROWSER_WEBGPU } } diff --git a/crates/re_renderer/src/context.rs b/crates/re_renderer/src/context.rs index 6a8cc4f13231..31781a316a6c 100644 --- a/crates/re_renderer/src/context.rs +++ b/crates/re_renderer/src/context.rs @@ -1,4 +1,7 @@ -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; use parking_lot::{MappedRwLockReadGuard, Mutex, RwLock, RwLockReadGuard}; use type_map::concurrent::{self, TypeMap}; @@ -6,6 +9,7 @@ use type_map::concurrent::{self, TypeMap}; use crate::{ allocator::{CpuWriteGpuReadBelt, GpuReadbackBelt}, config::{DeviceTier, RenderContextConfig}, + error_handling::{ErrorTracker, WgpuErrorScope}, global_bindings::GlobalBindings, renderer::Renderer, resource_managers::{MeshManager, TextureManager2D}, @@ -13,6 +17,9 @@ use crate::{ FileServer, RecommendedFileResolver, }; +/// Frame idx used before starting the first frame. +const STARTUP_FRAME_IDX: u64 = u64::MAX; + /// Any resource involving wgpu rendering which can be re-used across different scenes. /// I.e. render pipelines, resource pools, etc. pub struct RenderContext { @@ -27,8 +34,6 @@ pub struct RenderContext { renderers: RwLock, pub(crate) resolver: RecommendedFileResolver, - #[cfg(all(not(target_arch = "wasm32"), debug_assertions))] // native debug build - pub(crate) err_tracker: std::sync::Arc, pub mesh_manager: RwLock, pub texture_manager_2d: TextureManager2D, @@ -43,6 +48,18 @@ pub struct RenderContext { pub active_frame: ActiveFrameContext, + /// Frame index used for [`wgpu::Device::on_uncaptured_error`] callbacks. + /// + /// Today, when using wgpu-core (== native & webgl) this is equal to the current [`ActiveFrameContext::frame_index`] + /// since the content timeline is in sync with the device timeline, + /// meaning everything done on [`wgpu::Device`] happens right away. + /// On WebGPU however, the `content timeline`` may be arbitrarily behind the `device timeline`! + /// See . + frame_index_for_uncaptured_errors: Arc, + + /// Error tracker used for `top_level_error_scope` and [`wgpu::Device::on_uncaptured_error`]. + top_level_error_tracker: Arc, + pub gpu_resources: WgpuResourcePools, // Last due to drop order. } @@ -106,6 +123,32 @@ impl RenderContext { ) -> Self { re_tracing::profile_function!(); + let frame_index_for_uncaptured_errors = Arc::new(AtomicU64::new(STARTUP_FRAME_IDX)); + + // Make sure to catch all errors, never crash, and deduplicate reported errors. + // `on_uncaptured_error` is a last-resort handler which we should never hit, + // since there should always be an open error scope. + // + // Note that this handler may not be called for all errors! + // (as of writing, wgpu-core will always call it when there's no open error scope, but Dawn doesn't!) + // Therefore, it is important to always have a `WgpuErrorScope` open! + // See https://www.w3.org/TR/webgpu/#telemetry + let top_level_error_tracker = { + let err_tracker = Arc::new(ErrorTracker::default()); + device.on_uncaptured_error({ + let err_tracker = Arc::clone(&err_tracker); + let frame_index_for_uncaptured_errors = frame_index_for_uncaptured_errors.clone(); + Box::new(move |err| { + err_tracker.handle_error( + err, + frame_index_for_uncaptured_errors.load(Ordering::Acquire), + ); + }) + }); + err_tracker + }; + let top_level_error_scope = Some(WgpuErrorScope::start(&device)); + log_adapter_info(&adapter.get_info()); let mut gpu_resources = WgpuResourcePools::default(); @@ -145,19 +188,6 @@ impl RenderContext { adapter.get_downlevel_capabilities(), ); - // In debug builds, make sure to catch all errors, never crash, and try to - // always let the user find a way to return a poisoned pipeline back into a - // sane state. - #[cfg(all(not(target_arch = "wasm32"), debug_assertions))] // native debug build - let err_tracker = { - let err_tracker = std::sync::Arc::new(crate::error_tracker::ErrorTracker::default()); - device.on_uncaptured_error({ - let err_tracker = std::sync::Arc::clone(&err_tracker); - Box::new(move |err| err_tracker.handle_error(err)) - }); - err_tracker - }; - let resolver = crate::new_recommended_file_resolver(); let mesh_manager = RwLock::new(MeshManager::new()); let texture_manager_2d = @@ -166,7 +196,8 @@ impl RenderContext { let active_frame = ActiveFrameContext { before_view_builder_encoder: Mutex::new(FrameGlobalCommandEncoder::new(&device)), pinned_render_pipelines: None, - frame_index: 0, + frame_index: STARTUP_FRAME_IDX, + top_level_error_scope, }; // Register shader workarounds for the current device. @@ -184,32 +215,31 @@ impl RenderContext { )); } + let cpu_write_gpu_read_belt = Mutex::new(CpuWriteGpuReadBelt::new( + Self::CPU_WRITE_GPU_READ_BELT_DEFAULT_CHUNK_SIZE.unwrap(), + )); + let gpu_readback_belt = Mutex::new(GpuReadbackBelt::new( + Self::GPU_READBACK_BELT_DEFAULT_CHUNK_SIZE.unwrap(), + )); + RenderContext { device, queue, - config, global_bindings, - renderers: RwLock::new(Renderers { renderers: TypeMap::new(), }), - - gpu_resources, - + resolver, + top_level_error_tracker, mesh_manager, texture_manager_2d, - cpu_write_gpu_read_belt: Mutex::new(CpuWriteGpuReadBelt::new(Self::CPU_WRITE_GPU_READ_BELT_DEFAULT_CHUNK_SIZE.unwrap())), - gpu_readback_belt: Mutex::new(GpuReadbackBelt::new(Self::GPU_READBACK_BELT_DEFAULT_CHUNK_SIZE.unwrap())), - - resolver, - - #[cfg(all(not(target_arch = "wasm32"), debug_assertions))] // native debug build - err_tracker, - + cpu_write_gpu_read_belt, + gpu_readback_belt, inflight_queue_submissions: Vec::new(), - active_frame, + frame_index_for_uncaptured_errors, + gpu_resources, } } @@ -269,7 +299,10 @@ impl RenderContext { .0 .is_some() { - assert!(self.active_frame.frame_index == 0, "There was still a command encoder from the previous frame at the beginning of the current. Did you forget to call RenderContext::before_submit?"); + if self.active_frame.frame_index != STARTUP_FRAME_IDX { + re_log::error!("There was still a command encoder from the previous frame at the beginning of the current. +This means, either a call to RenderContext::before_submit was omitted, or the previous frame was unexpectedly cancelled."); + } self.before_submit(); } @@ -286,20 +319,36 @@ impl RenderContext { .return_resources(moved_render_pipelines); } + // Close previous' frame error scope. + if let Some(top_level_error_scope) = self.active_frame.top_level_error_scope.take() { + let frame_index_for_uncaptured_errors = self.frame_index_for_uncaptured_errors.clone(); + self.top_level_error_tracker.handle_error_future( + top_level_error_scope.end(), + self.active_frame.frame_index, + move |err_tracker, frame_index| { + // Update last completed frame index. + // + // Note that this means that the device timeline has now finished this frame as well! + // Reminder: On WebGPU the device timeline may be arbitrarily behind the content timeline! + // See . + frame_index_for_uncaptured_errors.store(frame_index, Ordering::Release); + err_tracker.on_device_timeline_frame_finished(frame_index); + + // TODO(#4507): Once we support creating more error handlers, + // we need to tell all of them here that the frame has finished. + }, + ); + } + // New active frame! self.active_frame = ActiveFrameContext { before_view_builder_encoder: Mutex::new(FrameGlobalCommandEncoder::new(&self.device)), - frame_index: self.active_frame.frame_index + 1, pinned_render_pipelines: None, + frame_index: self.active_frame.frame_index.wrapping_add(1), + top_level_error_scope: Some(WgpuErrorScope::start(&self.device)), }; let frame_index = self.active_frame.frame_index; - // Tick the error tracker so that it knows when to reset! - // Note that we're ticking on begin_frame rather than raw frames, which - // makes a world of difference when we're in a poisoned state. - #[cfg(all(not(target_arch = "wasm32"), debug_assertions))] // native debug build - self.err_tracker.tick(); - // The set of files on disk that were modified in any way since last frame, // ignoring deletions. // Always an empty set in release builds. @@ -443,7 +492,22 @@ pub struct ActiveFrameContext { pub pinned_render_pipelines: Option, /// Index of this frame. Is incremented for every render frame. + /// + /// Keep in mind that all operations on WebGPU are asynchronous: + /// This counter is part of the `content timeline` and may be arbitrarily + /// behind both of the `device timeline` and `queue timeline`. + /// See frame_index: u64, + + /// Top level device error scope, created at startup and closed & reopened on every frame. + /// + /// According to documentation, not all errors may be caught by [`wgpu::Device::on_uncaptured_error`]. + /// + /// Therefore, we should make sure that we _always_ have an error scope open! + /// Additionally, we use this to update [`RenderContext::frame_index_for_uncaptured_errors`]. + /// + /// The only time this is allowed to be `None` is during shutdown and when closing an old and opening a new scope. + top_level_error_scope: Option, } fn log_adapter_info(info: &wgpu::AdapterInfo) { diff --git a/crates/re_renderer/src/error_handling/error_tracker.rs b/crates/re_renderer/src/error_handling/error_tracker.rs new file mode 100644 index 000000000000..26e3b0e04372 --- /dev/null +++ b/crates/re_renderer/src/error_handling/error_tracker.rs @@ -0,0 +1,150 @@ +use ahash::HashMap; +use parking_lot::Mutex; + +use super::handle_async_error; + +#[cfg(not(webgpu))] +use super::wgpu_core_error::WrappedContextError; + +#[cfg(webgpu)] +#[derive(Hash, PartialEq, Eq, Debug)] +struct WrappedContextError(pub String); + +pub struct ErrorEntry { + /// Frame index for frame on which this error was last logged. + last_occurred_frame_index: u64, + + /// Description of the error. + // TODO(#4507): Expecting to need this once we use this in space views. Also very useful for debugging. + #[allow(dead_code)] + description: String, +} + +/// Keeps track of wgpu errors and de-duplicates messages across frames. +/// +/// On native & webgl, what accounts for as an error duplicate is a heuristic based on wgpu-core error type. +/// +/// Used to avoid spamming the user with repeating errors. +/// [`crate::RenderContext`] maintains a "top level" error tracker for all otherwise unhandled errors. +/// +/// TODO(#4507): Users should be able to create their own scopes feeding into separate trackers. +#[derive(Default)] +pub struct ErrorTracker { + pub errors: Mutex>, +} + +impl ErrorTracker { + /// Called by the renderer context when the last error scope of a frame has finished. + /// + /// Error scopes live on the device timeline, which may be arbitrarily delayed compared to the content timeline. + /// See . + /// Do *not* call this with the content pipeline's frame index! + pub fn on_device_timeline_frame_finished(&self, device_timeline_frame_index: u64) { + let mut errors = self.errors.lock(); + errors.retain(|_error, entry| { + // If the error was not logged on the just concluded frame, remove it. + device_timeline_frame_index == entry.last_occurred_frame_index + }); + } + + /// Handles an async error, calling [`ErrorTracker::handle_error`] as needed. + /// + /// `on_last_scope_resolved` is called when the last scope has resolved. + /// + /// `frame_index` should be the currently active frame index which is associated with the scope. + /// (by the time the scope finishes, the active frame index may have changed) + pub fn handle_error_future( + self: &std::sync::Arc, + error_scope_result: impl IntoIterator< + Item = impl std::future::Future> + Send + 'static, + >, + frame_index: u64, + on_last_scope_resolved: impl Fn(&Self, u64) + Send + Sync + 'static, + ) { + let mut error_scope_result = error_scope_result.into_iter().peekable(); + while let Some(error_future) = error_scope_result.next() { + if error_scope_result.peek().is_none() { + let err_tracker = self.clone(); + handle_async_error( + move |error| { + if let Some(error) = error { + err_tracker.handle_error(error, frame_index); + } + on_last_scope_resolved(&err_tracker, frame_index); + }, + error_future, + ); + break; + } + + let err_tracker = self.clone(); + handle_async_error( + move |error| { + if let Some(error) = error { + err_tracker.handle_error(error, frame_index); + } + }, + error_future, + ); + } + } + + /// Logs a wgpu error to the tracker. + /// + /// If the error happened already already, it will be deduplicated. + /// + /// `frame_index` should be the frame index associated with the error scope. + /// Since errors are reported on the `device timeline`, not the `content timeline`, + /// this may not be the currently active frame index! + pub fn handle_error(&self, error: wgpu::Error, frame_index: u64) { + match error { + wgpu::Error::OutOfMemory { source: _ } => { + re_log::error!("A wgpu operation caused out-of-memory: {error}"); + } + wgpu::Error::Validation { + source: _source, + description, + } => { + let entry = ErrorEntry { + last_occurred_frame_index: frame_index, + description: description.clone(), + }; + + cfg_if::cfg_if! { + if #[cfg(webgpu)] { + if self.errors.lock().insert( + WrappedContextError(description.clone()), + entry + ).is_none() { + re_log::error!( + "WGPU error in frame {}: {}", frame_index, description + ); + } + } else { + match _source.downcast::() { + Ok(ctx_err) => { + if ctx_err + .cause + .downcast_ref::() + .is_some() + { + // Actual command encoder errors never carry any meaningful + // information: ignore them. + return; + } + + let ctx_err = WrappedContextError(ctx_err); + if self.errors.lock().insert(ctx_err, entry).is_none() { + re_log::error!( + "WGPU error in frame {}: {}", frame_index, description + ); + } + } + Err(err) => re_log::error!("Wgpu operation failed: {err}"), + } + } + } + } + } + } +} diff --git a/crates/re_renderer/src/error_handling/mod.rs b/crates/re_renderer/src/error_handling/mod.rs new file mode 100644 index 000000000000..6692e05808a3 --- /dev/null +++ b/crates/re_renderer/src/error_handling/mod.rs @@ -0,0 +1,32 @@ +mod error_tracker; +mod wgpu_error_scope; + +#[cfg(not(webgpu))] +mod wgpu_core_error; + +#[cfg(not(webgpu))] +mod now_or_never; + +pub use error_tracker::ErrorTracker; +pub use wgpu_error_scope::WgpuErrorScope; + +// ------- + +fn handle_async_error( + resolve_callback: impl FnOnce(Option) + 'static, + error_future: impl std::future::Future> + Send + 'static, +) { + cfg_if::cfg_if! { + if #[cfg(webgpu)] { + wasm_bindgen_futures::spawn_local(async move { + resolve_callback(error_future.await); + }); + } else { + if let Some(error) = now_or_never::now_or_never(error_future) { + resolve_callback(error); + } else { + re_log::error_once!("Expected wgpu errors to be ready immediately when using any of the wgpu-core based (native & webgl) backends."); + } + } + } +} diff --git a/crates/re_renderer/src/error_handling/now_or_never.rs b/crates/re_renderer/src/error_handling/now_or_never.rs new file mode 100644 index 000000000000..26cd6e69449c --- /dev/null +++ b/crates/re_renderer/src/error_handling/now_or_never.rs @@ -0,0 +1,49 @@ +//! Future utility copied from Bevy +//! +//! +//! It is used there for a very similar purpose: catching errors on native wgpu which are known to be non-asynchronous. + +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, +}; + +/// Consumes the future, polls it once, and immediately returns the output +/// or returns `None` if it wasn't ready yet. +/// +/// This will cancel the future if it's not ready. +#[allow(unsafe_code)] +pub fn now_or_never(mut future: F) -> Option { + let noop_waker = noop_waker(); + let mut cx = Context::from_waker(&noop_waker); + + // SAFETY: `future` is not moved and the original value is shadowed + let future = unsafe { Pin::new_unchecked(&mut future) }; + + match future.poll(&mut cx) { + Poll::Ready(x) => Some(x), + Poll::Pending => None, + } +} + +#[allow(unsafe_code)] +unsafe fn noop_clone(_data: *const ()) -> RawWaker { + noop_raw_waker() +} + +#[allow(unsafe_code)] +unsafe fn noop(_data: *const ()) {} + +const NOOP_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop); + +fn noop_raw_waker() -> RawWaker { + RawWaker::new(std::ptr::null(), &NOOP_WAKER_VTABLE) +} + +#[allow(unsafe_code)] +fn noop_waker() -> Waker { + // SAFETY: the `RawWakerVTable` is just a big noop and doesn't violate any of the rules in `RawWakerVTable`s documentation + // (which talks about retaining and releasing any "resources", of which there are none in this case) + unsafe { Waker::from_raw(noop_raw_waker()) } +} diff --git a/crates/re_renderer/src/error_handling/wgpu_core_error.rs b/crates/re_renderer/src/error_handling/wgpu_core_error.rs new file mode 100644 index 000000000000..e06530da346f --- /dev/null +++ b/crates/re_renderer/src/error_handling/wgpu_core_error.rs @@ -0,0 +1,208 @@ +use std::hash::Hash as _; + +/// Tries downcasting a given value into the specified possibilities (or all of them +/// if none are specified), then run the given expression on the downcasted value. +/// +/// E.g. to `dbg!()` the downcasted value on a wgpu error: +/// ```ignore +/// try_downcast!(my_error => |inner| { dbg!(inner); }) +/// ``` +macro_rules! try_downcast { + ($value:expr => |$binding:pat_param| $do:expr => [$ty:ty, $($tail:ty $(,)*),*]) => { + try_downcast!($value => |$binding| $do => $ty); + try_downcast!($value => |$binding| $do => [$($tail),*]); + }; + ($value:expr => |$binding:pat_param| $do:expr => [$ty:ty $(,)*]) => { + try_downcast!($value => |$binding| $do => $ty); + }; + ($value:expr => |$binding:pat_param| $do:expr => $ty:ty) => { + if let Some($binding) = ($value).downcast_ref::<$ty>() { + break Some({ $do }); + } + }; + ($value:expr => |$binding:pat_param| $do:expr) => { + loop { + try_downcast![$value => |$binding| $do => [ + wgpu_core::command::ClearError, + wgpu_core::command::CommandEncoderError, + wgpu_core::command::ComputePassError, + wgpu_core::command::CopyError, + wgpu_core::command::DispatchError, + wgpu_core::command::DrawError, + wgpu_core::command::ExecutionError, + wgpu_core::command::PassErrorScope, + wgpu_core::command::QueryError, + wgpu_core::command::QueryUseError, + wgpu_core::command::RenderBundleError, + wgpu_core::command::RenderCommandError, + wgpu_core::command::RenderPassError, + wgpu_core::command::ResolveError, + wgpu_core::command::TransferError, + wgpu_core::binding_model::BindError, + wgpu_core::binding_model::BindingTypeMaxCountError, + wgpu_core::binding_model::CreateBindGroupError, + wgpu_core::binding_model::CreatePipelineLayoutError, + wgpu_core::binding_model::GetBindGroupLayoutError, + wgpu_core::binding_model::PushConstantUploadError, + wgpu_core::device::resource::CreateDeviceError, + wgpu_core::device::DeviceError, + wgpu_core::device::RenderPassCompatibilityError, + wgpu_core::pipeline::ColorStateError, + wgpu_core::pipeline::CreateComputePipelineError, + wgpu_core::pipeline::CreateRenderPipelineError, + wgpu_core::pipeline::CreateShaderModuleError, + wgpu_core::pipeline::DepthStencilStateError, + wgpu_core::pipeline::ImplicitLayoutError, + ]]; + + break None; + }}; +} + +fn type_of_var(_: &T) -> std::any::TypeId { + std::any::TypeId::of::() +} + +// --- + +/// An error with some extra deduplication logic baked in. +/// +/// Implemented by default for all relevant wgpu error types, though it might be worth +/// providing a more specialized implementation for errors that are too broad by nature: +/// e.g. a shader error cannot by deduplicated simply using the shader path. +trait DedupableError: Sized + std::error::Error + 'static { + fn hash(&self, state: &mut H) { + type_of_var(self).hash(state); + } + + fn eq(&self, rhs: &(dyn std::error::Error + Send + Sync + 'static)) -> bool { + rhs.downcast_ref::().is_some() + } +} + +/// E.g. to implement `DedupableError` for u32 + u64: +/// ```ignore +/// impl_trait![u32, u64]; +/// ``` +macro_rules! impl_trait { + [$ty:ty, $($rest:ty),+ $(,)*] => { + impl_trait![$ty]; + impl_trait![$($rest),+]; + }; + [$ty:ty $(,)*] => { + impl DedupableError for $ty {} + }; +} + +impl_trait![ + wgpu_core::command::ClearError, + wgpu_core::command::CommandEncoderError, + wgpu_core::command::ComputePassError, + wgpu_core::command::CopyError, + wgpu_core::command::DispatchError, + wgpu_core::command::DrawError, + wgpu_core::command::ExecutionError, + wgpu_core::command::PassErrorScope, + wgpu_core::command::QueryError, + wgpu_core::command::QueryUseError, + wgpu_core::command::RenderBundleError, + wgpu_core::command::RenderCommandError, + wgpu_core::command::RenderPassError, + wgpu_core::command::ResolveError, + wgpu_core::command::TransferError, + wgpu_core::binding_model::BindError, + wgpu_core::binding_model::BindingTypeMaxCountError, + wgpu_core::binding_model::CreateBindGroupError, + wgpu_core::binding_model::CreatePipelineLayoutError, + wgpu_core::binding_model::GetBindGroupLayoutError, + wgpu_core::binding_model::PushConstantUploadError, + wgpu_core::device::resource::CreateDeviceError, + wgpu_core::device::DeviceError, + wgpu_core::device::RenderPassCompatibilityError, + wgpu_core::pipeline::ColorStateError, + wgpu_core::pipeline::CreateComputePipelineError, + wgpu_core::pipeline::CreateRenderPipelineError, + // wgpu_core::pipeline::CreateShaderModuleError, // NOTE: custom impl! + wgpu_core::pipeline::DepthStencilStateError, + wgpu_core::pipeline::ImplicitLayoutError, +]; + +// Custom deduplication for shader compilation errors, based on compiler message. +impl DedupableError for wgpu_core::pipeline::CreateShaderModuleError { + fn hash(&self, state: &mut H) { + type_of_var(self).hash(state); + #[allow(clippy::enum_glob_use)] + use wgpu_core::pipeline::CreateShaderModuleError::*; + match self { + Parsing(err) => err.source.hash(state), + Validation(err) => err.source.hash(state), + _ => {} + } + } + + fn eq(&self, rhs: &(dyn std::error::Error + Send + Sync + 'static)) -> bool { + if rhs.downcast_ref::().is_none() { + return false; + } + let rhs = rhs.downcast_ref::().unwrap(); + + #[allow(clippy::enum_glob_use)] + use wgpu_core::pipeline::CreateShaderModuleError::*; + match (self, rhs) { + (Parsing(err1), Parsing(err2)) => err1.source == err2.source, + (Validation(err1), Validation(err2)) => err1.source == err2.source, + _ => true, + } + } +} + +/// A `wgpu_core::ContextError` with hashing and equality capabilities. +/// +/// Used for deduplication purposes. +#[derive(Debug)] +pub struct WrappedContextError(pub Box); + +impl std::hash::Hash for WrappedContextError { + fn hash(&self, state: &mut H) { + // If we haven't set a debug label ourselves, the label is typically not stable across frames, + // Since wgc fills in the generation counter. + // Snip that part, starting with the last occurrence of -(. + let label = if let Some(index) = self.0.label.find("-(") { + &self.0.label[..index] + } else { + &self.0.label + }; + + label.hash(state); // e.g. "composite_encoder" + self.0.label_key.hash(state); // e.g. "encoder" + self.0.string.hash(state); // e.g. "a RenderPass" + + // try to downcast into something that implements `DedupableError`, and + // then call `DedupableError::hash`. + if try_downcast!(self.0.cause => |inner| DedupableError::hash(inner, state)).is_none() { + re_log::warn!(cause=?self.0.cause, "unknown error cause"); + } + } +} + +impl PartialEq for WrappedContextError { + fn eq(&self, rhs: &Self) -> bool { + let mut is_eq = self.0.label.eq(&rhs.0.label) + && self.0.label_key.eq(rhs.0.label_key) + && self.0.string.eq(rhs.0.string); + + // try to downcast into something that implements `DedupableError`, and + // then call `DedupableError::eq`. + if let Some(finer_eq) = + try_downcast!(self.0.cause => |inner| DedupableError::eq(inner, &*rhs.0.cause)) + { + is_eq |= finer_eq; + } else { + re_log::warn!(cause=?self.0.cause, "unknown error cause"); + } + + is_eq + } +} + +impl Eq for WrappedContextError {} diff --git a/crates/re_renderer/src/error_handling/wgpu_error_scope.rs b/crates/re_renderer/src/error_handling/wgpu_error_scope.rs new file mode 100644 index 000000000000..e9fdaf29b63b --- /dev/null +++ b/crates/re_renderer/src/error_handling/wgpu_error_scope.rs @@ -0,0 +1,39 @@ +use std::sync::Arc; + +/// Wgpu device error scope for all filters that auto closes when exiting the scope unless it was already closed. +/// +/// The expectation is that the scope is manually closed, but this construct is useful to not accidentally +/// leave the scope open when returning early from a function. +/// Opens scopes for all error types. +pub struct WgpuErrorScope { + open: bool, + device: Arc, +} + +impl WgpuErrorScope { + pub fn start(device: &Arc) -> Self { + device.push_error_scope(wgpu::ErrorFilter::Validation); + device.push_error_scope(wgpu::ErrorFilter::OutOfMemory); + // TODO(gfx-rs/wgpu#4866): Internal is missing! + Self { + device: device.clone(), + open: true, + } + } + + pub fn end( + mut self, + ) -> [impl std::future::Future> + Send + 'static; 2] { + self.open = false; + [self.device.pop_error_scope(), self.device.pop_error_scope()] + } +} + +impl Drop for WgpuErrorScope { + fn drop(&mut self) { + if self.open { + drop(self.device.pop_error_scope()); + drop(self.device.pop_error_scope()); + } + } +} diff --git a/crates/re_renderer/src/error_tracker.rs b/crates/re_renderer/src/error_tracker.rs deleted file mode 100644 index 131ac24c85a7..000000000000 --- a/crates/re_renderer/src/error_tracker.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! Special error handling datastructures for debug builds (never crash!). - -use ahash::HashSet; -use parking_lot::Mutex; -use std::{ - hash::Hash, - sync::{ - atomic::Ordering, - atomic::{AtomicI64, AtomicUsize}, - }, -}; -use wgpu_core::error::ContextError; - -// --- - -/// Tries downcasting a given value into the specified possibilities (or all of them -/// if none are specified), then run the given expression on the downcasted value. -/// -/// E.g. to `dbg!()` the downcasted value on a wgpu error: -/// ```ignore -/// try_downcast!(my_error => |inner| { dbg!(inner); }) -/// ``` -macro_rules! try_downcast { - ($value:expr => |$binding:pat_param| $do:expr => [$ty:ty, $($tail:ty $(,)*),*]) => { - try_downcast!($value => |$binding| $do => $ty); - try_downcast!($value => |$binding| $do => [$($tail),*]); - }; - ($value:expr => |$binding:pat_param| $do:expr => [$ty:ty $(,)*]) => { - try_downcast!($value => |$binding| $do => $ty); - }; - ($value:expr => |$binding:pat_param| $do:expr => $ty:ty) => { - if let Some($binding) = ($value).downcast_ref::<$ty>() { - break Some({ $do }); - } - }; - ($value:expr => |$binding:pat_param| $do:expr) => { - loop { - try_downcast![$value => |$binding| $do => [ - wgpu_core::command::ClearError, - wgpu_core::command::CommandEncoderError, - wgpu_core::command::ComputePassError, - wgpu_core::command::CopyError, - wgpu_core::command::DispatchError, - wgpu_core::command::DrawError, - wgpu_core::command::ExecutionError, - wgpu_core::command::PassErrorScope, - wgpu_core::command::QueryError, - wgpu_core::command::QueryUseError, - wgpu_core::command::RenderBundleError, - wgpu_core::command::RenderCommandError, - wgpu_core::command::RenderPassError, - wgpu_core::command::ResolveError, - wgpu_core::command::TransferError, - wgpu_core::binding_model::BindError, - wgpu_core::binding_model::BindingTypeMaxCountError, - wgpu_core::binding_model::CreateBindGroupError, - wgpu_core::binding_model::CreatePipelineLayoutError, - wgpu_core::binding_model::GetBindGroupLayoutError, - wgpu_core::binding_model::PushConstantUploadError, - wgpu_core::device::resource::CreateDeviceError, - wgpu_core::device::DeviceError, - wgpu_core::device::RenderPassCompatibilityError, - wgpu_core::pipeline::ColorStateError, - wgpu_core::pipeline::CreateComputePipelineError, - wgpu_core::pipeline::CreateRenderPipelineError, - wgpu_core::pipeline::CreateShaderModuleError, - wgpu_core::pipeline::DepthStencilStateError, - wgpu_core::pipeline::ImplicitLayoutError, - ]]; - - break None; - }}; - } - -fn type_of_var(_: &T) -> std::any::TypeId { - std::any::TypeId::of::() -} - -// --- - -/// An error with some extra deduplication logic baked in. -/// -/// Implemented by default for all relevant wgpu error types, though it might be worth -/// providing a more specialized implementation for errors that are too broad by nature: -/// e.g. a shader error cannot by deduplicated simply using the shader path. -trait DedupableError: Sized + std::error::Error + 'static { - fn hash(&self, state: &mut H) { - type_of_var(self).hash(state); - } - - fn eq(&self, rhs: &(dyn std::error::Error + Send + Sync + 'static)) -> bool { - rhs.downcast_ref::().is_some() - } -} - -/// E.g. to implement `DedupableError` for u32 + u64: -/// ```ignore -/// impl_trait![u32, u64]; -/// ``` -macro_rules! impl_trait { - [$ty:ty, $($rest:ty),+ $(,)*] => { - impl_trait![$ty]; - impl_trait![$($rest),+]; - }; - [$ty:ty $(,)*] => { - impl DedupableError for $ty {} - }; - } - -impl_trait![ - wgpu_core::command::ClearError, - wgpu_core::command::CommandEncoderError, - wgpu_core::command::ComputePassError, - wgpu_core::command::CopyError, - wgpu_core::command::DispatchError, - wgpu_core::command::DrawError, - wgpu_core::command::ExecutionError, - wgpu_core::command::PassErrorScope, - wgpu_core::command::QueryError, - wgpu_core::command::QueryUseError, - wgpu_core::command::RenderBundleError, - wgpu_core::command::RenderCommandError, - wgpu_core::command::RenderPassError, - wgpu_core::command::ResolveError, - wgpu_core::command::TransferError, - wgpu_core::binding_model::BindError, - wgpu_core::binding_model::BindingTypeMaxCountError, - wgpu_core::binding_model::CreateBindGroupError, - wgpu_core::binding_model::CreatePipelineLayoutError, - wgpu_core::binding_model::GetBindGroupLayoutError, - wgpu_core::binding_model::PushConstantUploadError, - wgpu_core::device::resource::CreateDeviceError, - wgpu_core::device::DeviceError, - wgpu_core::device::RenderPassCompatibilityError, - wgpu_core::pipeline::ColorStateError, - wgpu_core::pipeline::CreateComputePipelineError, - wgpu_core::pipeline::CreateRenderPipelineError, - // wgpu_core::pipeline::CreateShaderModuleError, // NOTE: custom impl! - wgpu_core::pipeline::DepthStencilStateError, - wgpu_core::pipeline::ImplicitLayoutError, -]; - -// Custom deduplication for shader compilation errors, based on compiler message. -impl DedupableError for wgpu_core::pipeline::CreateShaderModuleError { - fn hash(&self, state: &mut H) { - type_of_var(self).hash(state); - #[allow(clippy::enum_glob_use)] - use wgpu_core::pipeline::CreateShaderModuleError::*; - match self { - Parsing(err) => err.source.hash(state), - Validation(err) => err.source.hash(state), - _ => {} - } - } - - fn eq(&self, rhs: &(dyn std::error::Error + Send + Sync + 'static)) -> bool { - if rhs.downcast_ref::().is_none() { - return false; - } - let rhs = rhs.downcast_ref::().unwrap(); - - #[allow(clippy::enum_glob_use)] - use wgpu_core::pipeline::CreateShaderModuleError::*; - match (self, rhs) { - (Parsing(err1), Parsing(err2)) => err1.source == err2.source, - (Validation(err1), Validation(err2)) => err1.source == err2.source, - _ => true, - } - } -} - -// --- - -/// A `wgpu_core::ContextError` with hashing and equality capabilities. -/// -/// Used for deduplication purposes. -#[derive(Debug)] -pub struct WrappedContextError(Box); - -impl std::hash::Hash for WrappedContextError { - fn hash(&self, state: &mut H) { - self.0.label.hash(state); // e.g. "composite_encoder" - self.0.label_key.hash(state); // e.g. "encoder" - self.0.string.hash(state); // e.g. "a RenderPass" - - // try to downcast into something that implements `DedupableError`, and - // then call `DedupableError::hash`. - if try_downcast!(self.0.cause => |inner| DedupableError::hash(inner, state)).is_none() { - re_log::warn!(cause=?self.0.cause, "unknown error cause"); - } - } -} - -impl PartialEq for WrappedContextError { - fn eq(&self, rhs: &Self) -> bool { - let mut is_eq = self.0.label.eq(&rhs.0.label) - && self.0.label_key.eq(rhs.0.label_key) - && self.0.string.eq(rhs.0.string); - - // try to downcast into something that implements `DedupableError`, and - // then call `DedupableError::eq`. - if let Some(finer_eq) = - try_downcast!(self.0.cause => |inner| DedupableError::eq(inner, &*rhs.0.cause)) - { - is_eq |= finer_eq; - } else { - re_log::warn!(cause=?self.0.cause, "unknown error cause"); - } - - is_eq - } -} - -impl Eq for WrappedContextError {} - -// --- - -/// Coalesces wgpu errors until the tracker is `clear()`ed. -/// -/// Used to avoid spamming the user with repeating errors while a pipeline -/// is in a poisoned state. -pub struct ErrorTracker { - tick_nr: AtomicUsize, - - /// This countdown reaching 0 indicates that the pipeline has stabilized into a - /// sane state, which might take a few frames if we've just left a poisoned state. - /// - /// We use this to know when it makes sense to clear the error tracker. - clear_countdown: AtomicI64, - errors: Mutex>, -} - -impl Default for ErrorTracker { - fn default() -> Self { - Self { - tick_nr: AtomicUsize::new(0), - clear_countdown: AtomicI64::new(i64::MAX), - errors: Default::default(), - } - } -} - -impl ErrorTracker { - /// Increment tick count used in logged errors, and clear the tracker as needed. - pub fn tick(&self) { - self.tick_nr.fetch_add(1, Ordering::Relaxed); - - // The pipeline has stabilized back into a sane state, clear - // the error tracker so that we're ready to log errors once again - // if the pipeline gets back into a poisoned state. - if self.clear_countdown.fetch_sub(1, Ordering::Relaxed) == 1 { - self.clear_countdown.store(i64::MAX, Ordering::Relaxed); - self.clear(); - re_log::info!("pipeline back into a sane state!"); - } - } - - /// Resets the tracker. - /// - /// Call this when the pipeline is back into a sane state. - pub fn clear(&self) { - self.errors.lock().clear(); - re_log::debug!("cleared WGPU error tracker"); - } - - /// Logs a wgpu error, making sure to deduplicate them as needed. - pub fn handle_error(&self, error: wgpu::Error) { - // The pipeline is in a poisoned state, errors are still coming in: we won't be - // clearing the tracker until it had at least 2 complete begin_frame cycles - // without any errors (meaning the swapchain surface is stabilized). - self.clear_countdown.store(3, Ordering::Relaxed); - - match error { - wgpu::Error::OutOfMemory { source: _ } => panic!("{error}"), - wgpu::Error::Validation { - source, - description, - } => { - match source.downcast::() { - Ok(ctx_err) => { - if ctx_err - .cause - .downcast_ref::() - .is_some() - { - // Actual command encoder errors never carry any meaningful - // information: ignore them. - return; - } - - let ctx_err = WrappedContextError(ctx_err); - if !self.errors.lock().insert(ctx_err) { - // We've already logged this error since we've entered the - // current poisoned state. Don't log it again. - return; - } - - re_log::error!( - tick_nr = self.tick_nr.load(Ordering::Relaxed), - %description, - "WGPU error", - ); - } - Err(err) => panic!("{err}"), - }; - } - } - } -} diff --git a/crates/re_renderer/src/file_resolver.rs b/crates/re_renderer/src/file_resolver.rs index bce404916ea5..2a65f3743706 100644 --- a/crates/re_renderer/src/file_resolver.rs +++ b/crates/re_renderer/src/file_resolver.rs @@ -458,11 +458,11 @@ mod tests_import_clause { // --- /// The recommended `FileResolver` type for the current platform/target. -#[cfg(all(not(target_arch = "wasm32"), debug_assertions))] // non-wasm + debug build +#[cfg(load_shaders_from_disk)] pub type RecommendedFileResolver = FileResolver; /// The recommended `FileResolver` type for the current platform/target. -#[cfg(not(all(not(target_arch = "wasm32"), debug_assertions)))] // otherwise +#[cfg(not(load_shaders_from_disk))] pub type RecommendedFileResolver = FileResolver<&'static crate::MemFileSystem>; /// Returns the recommended `FileResolver` for the current platform/target. diff --git a/crates/re_renderer/src/file_server.rs b/crates/re_renderer/src/file_server.rs index 4b1e21a38af2..815226679b1f 100644 --- a/crates/re_renderer/src/file_server.rs +++ b/crates/re_renderer/src/file_server.rs @@ -1,91 +1,89 @@ /// A macro to read the contents of a file on disk, and resolve #import clauses as required. /// -/// - On Wasm and/or release builds, this will behave like the standard [`include_str`] +/// - If `load_shaders_from_disk` is disabled, this will behave like the standard [`include_str`] /// macro. -/// - On native debug builds, this will actually load the specified path through +/// - If `load_shaders_from_disk` is enabled, this will actually load the specified path through /// our [`FileServer`], and keep watching for changes in the background (both the root file /// and whatever direct and indirect dependencies it may have through #import clauses). #[macro_export] macro_rules! include_file { ($path:expr $(,)?) => {{ - #[cfg(all(not(target_arch = "wasm32"), debug_assertions))] // non-wasm + debug build - { - // Native debug build, we have access to the disk both while building and while - // running, we just need to interpolated the relative paths passed into the macro. - - let mut resolver = $crate::new_recommended_file_resolver(); - - let root_path = ::std::path::PathBuf::from(file!()); - - // If we're building from within the workspace, `file!()` will return a relative path - // starting at the workspace root. - // We're packing shaders using the re_renderer crate as root instead (to avoid nasty - // problems when publishing: as we lose workspace information when publishing!), so we - // need to make sure to strip the path down. - let root_path = root_path - .strip_prefix("crates/re_renderer") - .map_or_else(|_| root_path.clone(), ToOwned::to_owned); - - let path = root_path - .parent() - .unwrap() - .join($path); - - // If we're building from outside the workspace, `path` is an absolute path already and - // we're good to go; but if we're building from within, `path` is currently a relative - // path that assumes the CWD is the root of re_renderer, we need to make it absolute as - // there is no guarantee that this is where `cargo run` is being run from. - let manifest_path = ::std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let path = manifest_path.join(path); - - use anyhow::Context as _; - $crate::FileServer::get_mut(|fs| fs.watch(&mut resolver, &path, false)) - .with_context(|| format!("include_file!({}) (rooted at {:?}) failed while trying to import physical path {path:?}", $path, root_path)) - .unwrap() - } - - #[cfg(not(all(not(target_arch = "wasm32"), debug_assertions)))] // otherwise - { - // Make sure `workspace_shaders::init()` is called at least once, which will - // register all shaders defined in the workspace into the run-time in-memory - // filesystem. - $crate::workspace_shaders::init(); - - // On windows file!() will return '\'-style paths, but this code may end up - // running in wasm where '\\' will cause issues. If we're actually running on - // windows, `Path` will do the right thing for us. - let path = ::std::path::Path::new(&file!().replace('\\', "/")) - .parent() - .unwrap() - .join($path); - - // If we're building from within the workspace, `file!()` will return a relative path - // starting at the workspace root. - // We're packing shaders using the re_renderer crate as root instead (to avoid nasty - // problems when publishing: as we lose workspace information when publishing!), so we - // need to make sure to strip the path down. - let path = path - .strip_prefix("crates/re_renderer") - .map_or_else(|_| path.clone(), ToOwned::to_owned); - - // If we're building from outside the workspace, `file!()` will return an absolute path - // that might point to anywhere: it doesn't matter, just strip it down to a relative - // re_renderer path no matter what. - let manifest_path = ::std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let path = path - .strip_prefix(&manifest_path) - .map_or_else(|_| path.clone(), ToOwned::to_owned); - - // At this point our path is guaranteed to be hermetic, and we pre-load - // our run-time virtual filesystem using the exact same hermetic prefix. - // - // Therefore, the in-memory filesystem will actually be able to find this path, - // and canonicalize it. - use anyhow::Context as _; - use $crate::file_system::FileSystem; - $crate::get_filesystem().canonicalize(&path) - .with_context(|| format!("include_file!({}) (rooted at {:?}) failed while trying to import virtual path {path:?}", $path, file!())) - .unwrap() + cfg_if::cfg_if! { + if #[cfg(load_shaders_from_disk)] { + // Native debug build, we have access to the disk both while building and while + // running, we just need to interpolated the relative paths passed into the macro. + + let mut resolver = $crate::new_recommended_file_resolver(); + + let root_path = ::std::path::PathBuf::from(file!()); + + // If we're building from within the workspace, `file!()` will return a relative path + // starting at the workspace root. + // We're packing shaders using the re_renderer crate as root instead (to avoid nasty + // problems when publishing: as we lose workspace information when publishing!), so we + // need to make sure to strip the path down. + let root_path = root_path + .strip_prefix("crates/re_renderer") + .map_or_else(|_| root_path.clone(), ToOwned::to_owned); + + let path = root_path + .parent() + .unwrap() + .join($path); + + // If we're building from outside the workspace, `path` is an absolute path already and + // we're good to go; but if we're building from within, `path` is currently a relative + // path that assumes the CWD is the root of re_renderer, we need to make it absolute as + // there is no guarantee that this is where `cargo run` is being run from. + let manifest_path = ::std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let path = manifest_path.join(path); + + use anyhow::Context as _; + $crate::FileServer::get_mut(|fs| fs.watch(&mut resolver, &path, false)) + .with_context(|| format!("include_file!({}) (rooted at {:?}) failed while trying to import physical path {path:?}", $path, root_path)) + .unwrap() + } else { + // Make sure `workspace_shaders::init()` is called at least once, which will + // register all shaders defined in the workspace into the run-time in-memory + // filesystem. + $crate::workspace_shaders::init(); + + // On windows file!() will return '\'-style paths, but this code may end up + // running in wasm where '\\' will cause issues. If we're actually running on + // windows, `Path` will do the right thing for us. + let path = ::std::path::Path::new(&file!().replace('\\', "/")) + .parent() + .unwrap() + .join($path); + + // If we're building from within the workspace, `file!()` will return a relative path + // starting at the workspace root. + // We're packing shaders using the re_renderer crate as root instead (to avoid nasty + // problems when publishing: as we lose workspace information when publishing!), so we + // need to make sure to strip the path down. + let path = path + .strip_prefix("crates/re_renderer") + .map_or_else(|_| path.clone(), ToOwned::to_owned); + + // If we're building from outside the workspace, `file!()` will return an absolute path + // that might point to anywhere: it doesn't matter, just strip it down to a relative + // re_renderer path no matter what. + let manifest_path = ::std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let path = path + .strip_prefix(&manifest_path) + .map_or_else(|_| path.clone(), ToOwned::to_owned); + + // At this point our path is guaranteed to be hermetic, and we pre-load + // our run-time virtual filesystem using the exact same hermetic prefix. + // + // Therefore, the in-memory filesystem will actually be able to find this path, + // and canonicalize it. + use anyhow::Context as _; + use $crate::file_system::FileSystem; + $crate::get_filesystem().canonicalize(&path) + .with_context(|| format!("include_file!({}) (rooted at {:?}) failed while trying to import virtual path {path:?}", $path, file!())) + .unwrap() + } } }}; } @@ -94,7 +92,7 @@ macro_rules! include_file { pub use self::file_server_impl::FileServer; -#[cfg(all(not(target_arch = "wasm32"), debug_assertions))] // non-wasm + debug build +#[cfg(load_shaders_from_disk)] mod file_server_impl { use ahash::{HashMap, HashSet}; use anyhow::Context as _; @@ -289,7 +287,7 @@ mod file_server_impl { } } -#[cfg(not(all(not(target_arch = "wasm32"), debug_assertions)))] // otherwise +#[cfg(not(load_shaders_from_disk))] mod file_server_impl { use ahash::HashSet; use std::path::PathBuf; diff --git a/crates/re_renderer/src/file_system.rs b/crates/re_renderer/src/file_system.rs index 5c9f2723d9e6..fc45498c414c 100644 --- a/crates/re_renderer/src/file_system.rs +++ b/crates/re_renderer/src/file_system.rs @@ -32,13 +32,13 @@ pub trait FileSystem { } /// Returns the recommended filesystem handle for the current platform. -#[cfg(all(not(target_arch = "wasm32"), debug_assertions))] // non-wasm + debug build +#[cfg(load_shaders_from_disk)] pub fn get_filesystem() -> OsFileSystem { OsFileSystem } /// Returns the recommended filesystem handle for the current platform. -#[cfg(not(all(not(target_arch = "wasm32"), debug_assertions)))] // otherwise +#[cfg(not(load_shaders_from_disk))] pub fn get_filesystem() -> &'static MemFileSystem { MemFileSystem::get() } diff --git a/crates/re_renderer/src/lib.rs b/crates/re_renderer/src/lib.rs index 3bb80d0053ec..8d6909a0e7b6 100644 --- a/crates/re_renderer/src/lib.rs +++ b/crates/re_renderer/src/lib.rs @@ -22,6 +22,7 @@ mod context; mod debug_label; mod depth_offset; mod draw_phases; +mod error_handling; mod file_resolver; mod file_server; mod file_system; @@ -35,19 +36,16 @@ mod transform; mod wgpu_buffer_types; mod wgpu_resources; -#[cfg(not(all(not(target_arch = "wasm32"), debug_assertions)))] // wasm or release builds +#[cfg(not(load_shaders_from_disk))] #[rustfmt::skip] // it's auto-generated mod workspace_shaders; -#[cfg(all(not(target_arch = "wasm32"), debug_assertions))] // native debug build -mod error_tracker; - // --------------------------------------------------------------------------- // Exports use allocator::GpuReadbackBuffer; -pub use allocator::GpuReadbackIdentifier; +pub use allocator::GpuReadbackIdentifier; pub use color::Rgba32Unmul; pub use colormap::{ colormap_inferno_srgb, colormap_magma_srgb, colormap_plasma_srgb, colormap_srgb, diff --git a/crates/re_renderer_examples/framework.rs b/crates/re_renderer_examples/framework.rs index 6622b7778bf6..884563f46393 100644 --- a/crates/re_renderer_examples/framework.rs +++ b/crates/re_renderer_examples/framework.rs @@ -238,7 +238,7 @@ impl Application { #[cfg(all(not(target_arch = "wasm32"), debug_assertions))] let frame = match self.surface.get_current_texture() { Ok(frame) => frame, - Err(wgpu::SurfaceError::Outdated) => { + Err(wgpu::SurfaceError::Timeout | wgpu::SurfaceError::Outdated) => { // We haven't been able to present anything to the swapchain for // a while, because the pipeline is poisoned. // Recreate a sane surface to restart the cycle and see if the