Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions deno_webgpu/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
1 change: 1 addition & 0 deletions tests/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 4 additions & 1 deletion tests/tests/wgpu-gpu/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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!(
Expand Down
5 changes: 4 additions & 1 deletion tests/tests/wgpu-gpu/mem_leaks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion wgpu-info/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion wgpu-info/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
116 changes: 114 additions & 2 deletions wgpu-info/src/tests.rs
Original file line number Diff line number Diff line change
@@ -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()
Comment on lines +6 to +16
}

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<String, &'a wgpu::TextureFormatFeatures>,
}

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<String, &crate::report::AdapterReport> = with_custom
.devices
.iter()
.map(|d| (adapter_key(&d.info), d))
.collect();

let without_map: std::collections::HashMap<String, &crate::report::AdapterReport> =
without_custom
.devices
.iter()
.map(|d| (adapter_key(&d.info), d))
.collect();

let mut failures: Vec<String> = 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";

Expand All @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions wgpu-types/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 set up, wgpu will return a custom instance created from
/// that factory.
///
/// Noop on wgpu-core.
Comment on lines +218 to +222
pub skip_custom_backend_library: bool,
}

impl BackendOptions {
Expand All @@ -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"),
}
}

Expand All @@ -239,6 +247,8 @@ impl BackendOptions {
gl: self.gl.with_env(),
dx12: self.dx12.with_env(),
noop: self.noop.with_env(),
skip_custom_backend_library: self.skip_custom_backend_library
|| crate::env::var("WGPU_NO_CUSTOM_BACKEND").as_deref() == Some("1"),
}
}
}
Expand Down
48 changes: 46 additions & 2 deletions wgpu/src/api/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,35 @@ 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.
///
/// 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<Instance, InstanceDescriptor>,
) -> bool {
// 0 is the sentinel for "not set"; fn pointers are never null.
INSTANCE_FACTORY
.compare_exchange(
0,
f as usize,
core::sync::atomic::Ordering::Release,
core::sync::atomic::Ordering::Relaxed,
)
.is_ok()
}
Comment on lines +7 to +33

bitflags::bitflags! {
/// WGSL language extensions.
///
Expand Down Expand Up @@ -59,8 +88,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<Instance, InstanceDescriptor> =
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. \
Expand Down
Loading