Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 9 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 setup, wgpu will return a custom instance created from
Comment thread
inner-daemons marked this conversation as resolved.
Outdated
/// 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,7 @@ impl BackendOptions {
gl: self.gl.with_env(),
dx12: self.dx12.with_env(),
noop: self.noop.with_env(),
skip_custom_backend_library: false,
Comment thread
inner-daemons marked this conversation as resolved.
Outdated
}
}
}
Expand Down
41 changes: 39 additions & 2 deletions wgpu/src/api/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Instance, InstanceDescriptor>) {
// 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,
);
Comment thread
inner-daemons marked this conversation as resolved.
Outdated
}
Comment on lines +7 to +33

bitflags::bitflags! {
/// WGSL language extensions.
///
Expand Down Expand Up @@ -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<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