From 2324dbb6c35cda489291a19e78f4ad0f6c2ed4cf Mon Sep 17 00:00:00 2001 From: Inner-Daemons Date: Thu, 28 May 2026 16:47:28 -0500 Subject: [PATCH 1/2] Implement custom backend overriding --- CHANGELOG.md | 8 +++ deno_webgpu/lib.rs | 2 + tests/src/init.rs | 1 + tests/tests/wgpu-gpu/device.rs | 5 +- tests/tests/wgpu-gpu/mem_leaks.rs | 5 +- wgpu-info/src/cli.rs | 2 +- wgpu-info/src/report.rs | 3 +- wgpu-info/src/tests.rs | 116 +++++++++++++++++++++++++++++- wgpu-types/src/backend.rs | 9 +++ wgpu/src/api/instance.rs | 41 ++++++++++- 10 files changed, 184 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9896a8d1b6..121a1b4b07a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,14 @@ Zero-size buffer bindings are still not permitted. `BufferBinding` now implement By @beholdnec in [#8505](https://github.com/gfx-rs/wgpu/pull/8505). +#### Overriding instance creation + +Instance creation can now be overridden by calling `set_instance_factory`. This lets you coerce other code using wgpu to use a custom backend. + +Requires the `custom` feature. Can be explicitly opted out of by setting `InstanceDescriptor.backend_options.skip_custom_backend_library` when creating an instance. + +By @inner-daemons in [#9606](https://github.com/gfx-rs/wgpu/pull/9606). + ### Added/New Features #### General diff --git a/deno_webgpu/lib.rs b/deno_webgpu/lib.rs index fa34aba9da8..280d9262ed6 100644 --- a/deno_webgpu/lib.rs +++ b/deno_webgpu/lib.rs @@ -200,6 +200,8 @@ impl GPU { }, gl: wgpu_types::GlBackendOptions::default(), noop: wgpu_types::NoopBackendOptions::default(), + // Noop in wgpu-core + skip_custom_backend_library: true, }, display: None, }, diff --git a/tests/src/init.rs b/tests/src/init.rs index 9ed3c5df95f..6dd72cc99de 100644 --- a/tests/src/init.rs +++ b/tests/src/init.rs @@ -87,6 +87,7 @@ pub fn initialize_instance(backends: wgpu::Backends, params: &TestParameters) -> enable: !cfg!(target_arch = "wasm32"), ..Default::default() }, + skip_custom_backend_library: false, } .with_env(), #[cfg(not(all( diff --git a/tests/tests/wgpu-gpu/device.rs b/tests/tests/wgpu-gpu/device.rs index 1d76536aa5b..35b1f37f46d 100644 --- a/tests/tests/wgpu-gpu/device.rs +++ b/tests/tests/wgpu-gpu/device.rs @@ -61,7 +61,7 @@ static DEVICE_LIFETIME_CHECK: GpuTestConfiguration = GpuTestConfiguration::new() .run_sync(|ctx| { ctx.instance.poll_all(false); - let pre_report = ctx.instance.generate_report().unwrap(); + let pre_report = ctx.instance.generate_report(); let TestingContext { instance, @@ -73,6 +73,9 @@ static DEVICE_LIFETIME_CHECK: GpuTestConfiguration = GpuTestConfiguration::new() drop(queue); drop(device); + let Some(pre_report) = pre_report else { + return; + }; let post_report = instance.generate_report().unwrap(); assert_ne!( diff --git a/tests/tests/wgpu-gpu/mem_leaks.rs b/tests/tests/wgpu-gpu/mem_leaks.rs index 3f4b0ecdf8d..0572ae555e9 100644 --- a/tests/tests/wgpu-gpu/mem_leaks.rs +++ b/tests/tests/wgpu-gpu/mem_leaks.rs @@ -26,7 +26,10 @@ async fn draw_test_with_reports( use wgpu::util::DeviceExt; - let global_report = ctx.instance.generate_report().unwrap(); + // Custom backends may fail here but should still be testable. + let Some(global_report) = ctx.instance.generate_report() else { + return; + }; let report = global_report.hub_report(); assert_eq!(report.devices.num_allocated, 1); assert_eq!(report.queues.num_allocated, 1); diff --git a/wgpu-info/src/cli.rs b/wgpu-info/src/cli.rs index b7fe25a5939..ea980870bec 100644 --- a/wgpu-info/src/cli.rs +++ b/wgpu-info/src/cli.rs @@ -84,7 +84,7 @@ pub fn main() -> anyhow::Result<()> { crate::report::GpuReport::from_json(&json).context("Could not parse JSON")? } // Generate the report natively - None => crate::report::GpuReport::generate(), + None => crate::report::GpuReport::generate(false), }; // Setup output writer diff --git a/wgpu-info/src/report.rs b/wgpu-info/src/report.rs index d4d9c929b5c..8f435018bfc 100644 --- a/wgpu-info/src/report.rs +++ b/wgpu-info/src/report.rs @@ -17,9 +17,10 @@ pub struct GpuReport { } impl GpuReport { - pub fn generate() -> Self { + pub fn generate(skip_custom: bool) -> Self { let instance = wgpu::Instance::new({ let mut desc = wgpu::InstanceDescriptor::new_without_display_handle(); + desc.backend_options.skip_custom_backend_library = skip_custom; desc.backend_options.dx12.shader_compiler = Dx12Compiler::StaticDxc; desc.flags = wgpu::InstanceFlags::debugging(); desc.with_env() diff --git a/wgpu-info/src/tests.rs b/wgpu-info/src/tests.rs index faacd8cfd62..cb510ab3f1d 100644 --- a/wgpu-info/src/tests.rs +++ b/wgpu-info/src/tests.rs @@ -1,4 +1,116 @@ -use std::{fs::File, io::BufWriter}; +use std::{collections::BTreeMap, fs::File, io::BufWriter}; + +/// Prints a pretty diff of 2 json strings +fn unified_diff(label: &str, a: &str, b: &str) -> String { + use std::{fs, process::Command}; + let dir = std::env::temp_dir(); + let a_path = dir.join(format!("wgpu-info-{label}-custom.json")); + let b_path = dir.join(format!("wgpu-info-{label}-core.json")); + fs::write(&a_path, a).unwrap(); + fs::write(&b_path, b).unwrap(); + let out = Command::new("diff") + .args(["-u", "--label", "with-custom", "--label", "without-custom"]) + .args([&a_path, &b_path]) + .output() + .unwrap(); + String::from_utf8(out.stdout).unwrap() +} + +fn adapter_key(info: &wgpu::AdapterInfo) -> String { + format!("{:?}/{}", info.backend, info.name) +} + +// Normalized view of an adapter for comparison purposes. +// Texture format features sorted by format name for deterministic JSON output +// (HashMap iteration order is random). +#[derive(serde::Serialize)] +struct NormalizedAdapter<'a> { + info: &'a wgpu::AdapterInfo, + features: &'a wgpu::Features, + limits: &'a wgpu::Limits, + downlevel_caps: &'a wgpu::DownlevelCapabilities, + texture_format_features: BTreeMap, +} + +fn normalize(dev: &crate::report::AdapterReport) -> NormalizedAdapter<'_> { + let texture_format_features = dev + .texture_format_features + .iter() + .map(|(fmt, feats)| (format!("{fmt:?}"), feats)) + .collect(); + NormalizedAdapter { + info: &dev.info, + features: &dev.features, + limits: &dev.limits, + downlevel_caps: &dev.downlevel_caps, + texture_format_features, + } +} + +fn to_json(value: &impl serde::Serialize) -> String { + serde_json::to_string_pretty(value).unwrap() +} + +#[test] +fn custom_backend_matches_wgpu_core() { + let with_custom = crate::report::GpuReport::generate(false); + let without_custom = crate::report::GpuReport::generate(true); + + let custom_map: std::collections::HashMap = with_custom + .devices + .iter() + .map(|d| (adapter_key(&d.info), d)) + .collect(); + + let without_map: std::collections::HashMap = + without_custom + .devices + .iter() + .map(|d| (adapter_key(&d.info), d)) + .collect(); + + let mut failures: Vec = Vec::new(); + + // Every custom-backend adapter must exist in wgpu-core and match it. + for custom_dev in &with_custom.devices { + let key = adapter_key(&custom_dev.info); + let Some(core_dev) = without_map.get(&key) else { + failures.push(format!( + "Adapter '{key}' present in custom-backend run but missing from wgpu-core run" + )); + continue; + }; + + let a = to_json(&normalize(custom_dev)); + let b = to_json(&normalize(core_dev)); + + if a != b { + failures.push(format!( + "Adapter '{}' differs:\n{}", + key, + unified_diff(&key.replace('/', "_"), &a, &b) + )); + } + } + + // Every wgpu-core adapter must also be present in the custom-backend run. + for core_dev in &without_custom.devices { + let key = adapter_key(&core_dev.info); + if !custom_map.contains_key(&key) { + failures.push(format!( + "Adapter '{key}' present in wgpu-core run but missing from custom-backend run" + )); + } + } + + if !failures.is_empty() { + panic!( + "GpuReport differs for {} adapter(s):\n{}", + failures.len(), + failures.join("\n") + ); + } +} const ENV_VAR_SAVE: &str = "WGPU_INFO_SAVE_GPUCONFIG_REPORT"; @@ -10,7 +122,7 @@ const ENV_VAR_SAVE: &str = "WGPU_INFO_SAVE_GPUCONFIG_REPORT"; // Needs to be kept in sync with the test in xtask/src/test.rs #[test] fn generate_gpuconfig_report() { - let report = crate::report::GpuReport::generate(); + let report = crate::report::GpuReport::generate(false); // If we don't get the env var, just test that we can generate the report, but don't save it // to avoid a race condition when other tests are reading the file. diff --git a/wgpu-types/src/backend.rs b/wgpu-types/src/backend.rs index b71cabaa58e..10a535574ec 100644 --- a/wgpu-types/src/backend.rs +++ b/wgpu-types/src/backend.rs @@ -215,6 +215,12 @@ pub struct BackendOptions { pub dx12: Dx12BackendOptions, /// Options for the noop backend, [`Backend::Noop`]. pub noop: NoopBackendOptions, + /// If false and the `custom` feature is enabled for wgpu, and if an override + /// instance factory was setup, wgpu will return a custom instance created from + /// that factory. + /// + /// Noop on wgpu-core. + pub skip_custom_backend_library: bool, } impl BackendOptions { @@ -227,6 +233,8 @@ impl BackendOptions { gl: GlBackendOptions::from_env_or_default(), dx12: Dx12BackendOptions::from_env_or_default(), noop: NoopBackendOptions::from_env_or_default(), + skip_custom_backend_library: crate::env::var("WGPU_NO_CUSTOM_BACKEND").as_deref() + == Some("1"), } } @@ -239,6 +247,7 @@ impl BackendOptions { gl: self.gl.with_env(), dx12: self.dx12.with_env(), noop: self.noop.with_env(), + skip_custom_backend_library: false, } } } diff --git a/wgpu/src/api/instance.rs b/wgpu/src/api/instance.rs index d192959e81f..ac2b3955b8d 100644 --- a/wgpu/src/api/instance.rs +++ b/wgpu/src/api/instance.rs @@ -3,6 +3,28 @@ use core::future::Future; use crate::{dispatch::InstanceInterface, util::Mutex, *}; +#[cfg(custom)] +static INSTANCE_FACTORY: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0); + +/// Register a factory that can intercept [`Instance::new`] for custom backends. +/// +/// The factory receives the [`InstanceDescriptor`] and returns `Ok(Instance)` to +/// take ownership of the request, or `Err(desc)` to fall through to the built-in backend. +/// +/// Only the first call takes effect; subsequent calls are ignored. +/// +/// This can be safely called from a constructor. +#[cfg(custom)] +pub fn set_instance_factory(f: fn(InstanceDescriptor) -> Result) { + // 0 is the sentinel for "not set"; fn pointers are never null. + let _ = INSTANCE_FACTORY.compare_exchange( + 0, + f as usize, + core::sync::atomic::Ordering::Release, + core::sync::atomic::Ordering::Relaxed, + ); +} + bitflags::bitflags! { /// WGSL language extensions. /// @@ -59,8 +81,23 @@ impl Instance { /// /// - If no backend feature for the active target platform is enabled, /// this method will panic; see [`Instance::enabled_backend_features()`]. - #[allow(clippy::allow_attributes, unreachable_code)] - pub fn new(desc: InstanceDescriptor) -> Self { + #[allow(clippy::allow_attributes, unreachable_code, unused_mut)] + pub fn new(mut desc: InstanceDescriptor) -> Self { + #[cfg(custom)] + if !desc.backend_options.skip_custom_backend_library { + let addr = INSTANCE_FACTORY.load(core::sync::atomic::Ordering::Acquire); + if addr != 0 { + // SAFETY: addr was written by set_instance_factory via `f as usize`, + // where f is a valid fn pointer of this exact type. + let factory: fn(InstanceDescriptor) -> Result = + unsafe { core::mem::transmute(addr) }; + match factory(desc) { + Ok(inst) => return inst, + Err(returned_desc) => desc = returned_desc, + } + } + } + if Self::enabled_backend_features().is_empty() { panic!( "No wgpu backend feature that is implemented for the target platform was enabled. \ From c81dbdcb89539e70d899cfd82e93b9a385fbaf24 Mon Sep 17 00:00:00 2001 From: Inner Daemons <85136135+inner-daemons@users.noreply.github.com> Date: Sat, 30 May 2026 17:28:47 -0500 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- wgpu-types/src/backend.rs | 5 +++-- wgpu/src/api/instance.rs | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/wgpu-types/src/backend.rs b/wgpu-types/src/backend.rs index 10a535574ec..d23f52c8b62 100644 --- a/wgpu-types/src/backend.rs +++ b/wgpu-types/src/backend.rs @@ -216,7 +216,7 @@ pub struct BackendOptions { /// Options for the noop backend, [`Backend::Noop`]. pub noop: NoopBackendOptions, /// If false and the `custom` feature is enabled for wgpu, and if an override - /// instance factory was setup, wgpu will return a custom instance created from + /// instance factory was set up, wgpu will return a custom instance created from /// that factory. /// /// Noop on wgpu-core. @@ -247,7 +247,8 @@ impl BackendOptions { gl: self.gl.with_env(), dx12: self.dx12.with_env(), noop: self.noop.with_env(), - skip_custom_backend_library: false, + skip_custom_backend_library: self.skip_custom_backend_library + || crate::env::var("WGPU_NO_CUSTOM_BACKEND").as_deref() == Some("1"), } } } diff --git a/wgpu/src/api/instance.rs b/wgpu/src/api/instance.rs index ac2b3955b8d..45851bda8c6 100644 --- a/wgpu/src/api/instance.rs +++ b/wgpu/src/api/instance.rs @@ -11,18 +11,25 @@ static INSTANCE_FACTORY: core::sync::atomic::AtomicUsize = core::sync::atomic::A /// The factory receives the [`InstanceDescriptor`] and returns `Ok(Instance)` to /// take ownership of the request, or `Err(desc)` to fall through to the built-in backend. /// -/// Only the first call takes effect; subsequent calls are ignored. +/// Only the first call takes effect. +/// +/// Returns `true` if this call registered the factory, or `false` if a factory +/// was already registered and this call was ignored. /// /// This can be safely called from a constructor. #[cfg(custom)] -pub fn set_instance_factory(f: fn(InstanceDescriptor) -> Result) { +pub fn set_instance_factory( + f: fn(InstanceDescriptor) -> Result, +) -> bool { // 0 is the sentinel for "not set"; fn pointers are never null. - let _ = INSTANCE_FACTORY.compare_exchange( - 0, - f as usize, - core::sync::atomic::Ordering::Release, - core::sync::atomic::Ordering::Relaxed, - ); + INSTANCE_FACTORY + .compare_exchange( + 0, + f as usize, + core::sync::atomic::Ordering::Release, + core::sync::atomic::Ordering::Relaxed, + ) + .is_ok() } bitflags::bitflags! {