Merge pull request #1688

This commit is contained in:
Brandon Thomas
2026-10-18 09:35:34 -04:00
2 changed files with 181 additions and 8 deletions
+30
View File
@@ -593,3 +593,33 @@ solid.property("ADBE Transform Group").property("ADBE Opacity").addToMotionGraph
assert_eq!(reply["error"], "command `renderQueue.add` is not available right now: select or open a composition", "{reply}");
let _ = std::fs::remove_dir_all(&root);
}
/// `WGPU_ADAPTER_NAME` that matches nothing is a clean `--gpu` error listing the adapters
/// found (wgpu's own helper panics). Read once per process, hence a subprocess.
#[test]
fn gpu_adapter_name_mismatch_is_a_clean_error() {
let out = bin()
.args(["render-frame", "--gpu", "--max-side", "64", "--out", tmp("nomatch.png").to_str().unwrap(), "--json"])
.env("WGPU_ADAPTER_NAME", "NoSuchAdapter-xyz")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(out.status.code(), Some(1), "stdout: {stdout}\nstderr: {}", String::from_utf8_lossy(&out.stderr));
let v: Value = serde_json::from_str(stdout.trim()).unwrap();
let e = v["error"].as_str().unwrap();
assert!(e.contains("WGPU_ADAPTER_NAME=\"NoSuchAdapter-xyz\""), "{e}");
assert!(e.contains("available: [") || e.contains("no adapters on the enabled backends"), "{e}");
}
/// An exported-but-empty `WGPU_ADAPTER_NAME` behaves like unset: it never produces the
/// adapter-name error (whether a GPU exists on this machine is not asserted).
#[test]
fn gpu_adapter_name_empty_is_unset() {
let out = bin()
.args(["render-frame", "--gpu", "--max-side", "64", "--out", tmp("emptyname.png").to_str().unwrap(), "--json"])
.env("WGPU_ADAPTER_NAME", " ")
.output()
.unwrap();
let all = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
assert!(!all.contains("WGPU_ADAPTER_NAME"), "{all}");
}
+151 -8
View File
@@ -140,6 +140,74 @@ pub struct TransferStats {
pub readback_bytes: u64,
}
/// `WGPU_ADAPTER_NAME` trimmed; empty or whitespace-only counts as unset (an empty
/// substring would otherwise match the first adapter and skip the power preference).
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
fn wanted_adapter_name(var: Option<&str>) -> Option<&str> {
var.map(str::trim).filter(|w| !w.is_empty())
}
/// Index of the first adapter whose name contains `want`, compared case-insensitively.
/// Only the name is matched, as in wgpu (the backend is chosen with `WGPU_BACKEND`).
/// `Err` lists each distinct adapter once as `name (backend)`, so a typo or an
/// OS/driver-specific name is easy to spot.
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
fn match_adapter_name(want: &str, adapters: &[(String, String)]) -> Result<usize, String> {
if adapters.is_empty() {
return Err(format!("WGPU_ADAPTER_NAME={want:?}: no adapters on the enabled backends"));
}
let w = want.to_lowercase();
adapters.iter().position(|(n, _)| n.to_lowercase().contains(&w)).ok_or_else(|| {
let mut list: Vec<String> = Vec::new();
for l in adapters.iter().map(|(n, b)| format!("{n} ({b})")) {
if !list.contains(&l) {
list.push(l);
}
}
format!(
"no adapter name matches WGPU_ADAPTER_NAME={want:?} (matched against the name only; \
select a backend with WGPU_BACKEND); available: [{}]",
list.join(", ")
)
})
}
/// The adapter named by `WGPU_ADAPTER_NAME` (case-insensitive substring, like wgpu's
/// `initialize_adapter_from_env`, which panics when none matches). `None` when unset or empty.
#[cfg(not(target_arch = "wasm32"))]
async fn adapter_by_name(instance: &wgpu::Instance) -> Option<Result<wgpu::Adapter, String>> {
let var = std::env::var("WGPU_ADAPTER_NAME").ok();
let want = wanted_adapter_name(var.as_deref())?;
let mut all = instance.enumerate_adapters(wgpu::Backends::all()).await;
let adapters: Vec<(String, String)> = all
.iter()
.map(|a| {
let i = a.get_info();
(i.name, format!("{:?}", i.backend))
})
.collect();
Some(match_adapter_name(want, &adapters).map(|i| all.swap_remove(i)))
}
/// `WGPU_POWER_PREF` (`low` / `high` / `none`), default HighPerformance. wgpu ignores an
/// unknown value silently; warn so a typo is noticed.
#[cfg(not(target_arch = "wasm32"))]
fn power_pref_from_env() -> wgpu::PowerPreference {
wgpu::PowerPreference::from_env().unwrap_or_else(|| {
if let Ok(v) = std::env::var("WGPU_POWER_PREF") {
log::warn!("gpu: ignoring WGPU_POWER_PREF={v:?} (expected low, high or none)");
}
wgpu::PowerPreference::HighPerformance
})
}
#[cfg(target_arch = "wasm32")]
fn power_pref_from_env() -> wgpu::PowerPreference {
wgpu::PowerPreference::HighPerformance
}
#[cfg(target_arch = "wasm32")]
async fn adapter_by_name(_: &wgpu::Instance) -> Option<Result<wgpu::Adapter, String>> {
None
}
impl Transfers {
fn count(&self, up: bool, bytes: usize) {
use std::sync::atomic::Ordering::Relaxed;
@@ -774,19 +842,28 @@ impl GpuContext {
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
#[cfg(not(target_arch = "wasm32"))]
{
// Match eframe's backend override so CLI and headless acceptance tests
// exercise the backend requested for the native viewer.
desc.backends = wgpu::Backends::from_env().unwrap_or(desc.backends);
// Honour the same wgpu env vars as the viewer (WGPU_BACKEND, WGPU_DX12_COMPILER,
// WGPU_VALIDATION, WGPU_DEBUG, WGPU_POWER_PREF), plus WGPU_ADAPTER_NAME (CLI/headless
// only for now; the viewer ignores it). Not full parity: the viewer also changes
// instance flags and has a GL fallback.
// Note: with_env() also resets memory_budget_thresholds to their default (unused here).
desc = desc.with_env();
}
#[cfg(target_arch = "wasm32")]
{
desc.backends = wgpu::Backends::BROWSER_WEBGPU;
}
let instance = wgpu::Instance::new(desc);
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, ..Default::default() })
.await
.map_err(|e| format!("no adapter: {e}"))?;
let (adapter, by_name) = match adapter_by_name(&instance).await {
Some(a) => (a?, true),
None => (
instance
.request_adapter(&wgpu::RequestAdapterOptions { power_preference: power_pref_from_env(), ..Default::default() })
.await
.map_err(|e| format!("no adapter: {e}"))?,
false,
),
};
let limits = adapter.limits();
let required_limits = wgpu::Limits {
max_texture_dimension_2d: limits.max_texture_dimension_2d.min(16384),
@@ -796,7 +873,10 @@ impl GpuContext {
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor { label: Some("effectcraft gpu"), required_limits, ..Default::default() })
.await
.map_err(|e| format!("no device: {e}"))?;
.map_err(|e| match by_name {
true => format!("no device on {} (picked by WGPU_ADAPTER_NAME): {e}", adapter.get_info().name),
false => format!("no device: {e}"),
})?;
let context = GpuContext::new(&adapter, device, queue)?;
// This path owns the device; install handlers only after construction succeeds.
crate::readback::install(&context.device, &context.readbacks);
@@ -1392,3 +1472,66 @@ impl<'g> Enc<'g> {
pub(crate) fn mip_levels(w: u32, h: u32) -> u32 {
u32::BITS - w.max(h).max(1).leading_zeros()
}
#[cfg(test)]
mod adapter_name_tests {
use super::{match_adapter_name, wanted_adapter_name};
fn adapters() -> Vec<(String, String)> {
[("NVIDIA GeForce GTX 1070", "Vulkan"), ("Intel(R) UHD Graphics 630", "Dx12"), ("llvmpipe (LLVM 19.1.7, 256 bits)", "Gl")]
.map(|(n, b)| (n.into(), b.into()))
.to_vec()
}
#[test]
fn matches_case_insensitive_substring() {
assert_eq!(match_adapter_name("intel", &adapters()), Ok(1));
assert_eq!(match_adapter_name("GTX 1070", &adapters()), Ok(0));
assert_eq!(match_adapter_name("LLVMPIPE", &adapters()), Ok(2));
}
#[test]
fn first_match_wins() {
assert_eq!(match_adapter_name("i", &adapters()), Ok(0));
}
#[test]
fn empty_or_blank_name_is_unset_and_value_is_trimmed() {
assert_eq!(wanted_adapter_name(None), None);
assert_eq!(wanted_adapter_name(Some("")), None);
assert_eq!(wanted_adapter_name(Some(" ")), None);
assert_eq!(wanted_adapter_name(Some(" intel ")), Some("intel"));
assert_eq!(match_adapter_name(wanted_adapter_name(Some(" intel ")).unwrap(), &adapters()), Ok(1));
}
#[test]
fn mismatch_lists_name_and_backend() {
let e = match_adapter_name("NoSuch", &adapters()).unwrap_err();
assert!(e.contains("\"NoSuch\""), "{e}");
assert!(e.contains("Intel(R) UHD Graphics 630 (Dx12)"), "{e}");
assert!(e.contains("NVIDIA GeForce GTX 1070 (Vulkan)"), "{e}");
}
#[test]
fn backend_is_not_part_of_the_match_and_message_says_so() {
let e = match_adapter_name("dx12", &adapters()).unwrap_err();
assert!(e.contains("name only"), "{e}");
assert!(e.contains("WGPU_BACKEND"), "{e}");
}
#[test]
fn mismatch_lists_identical_name_and_backend_once() {
let mut a = adapters();
a.insert(2, ("Intel(R) UHD Graphics 630".into(), "Dx12".into()));
a.push(("Intel(R) UHD Graphics 630".into(), "Vulkan".into()));
let e = match_adapter_name("NoSuch", &a).unwrap_err();
assert_eq!(e.matches("Intel(R) UHD Graphics 630 (Dx12)").count(), 1, "{e}");
assert_eq!(e.matches("Intel(R) UHD Graphics 630 (Vulkan)").count(), 1, "{e}");
}
#[test]
fn no_adapters_has_its_own_message() {
let e = match_adapter_name("x", &[]).unwrap_err();
assert!(e.contains("no adapters on the enabled backends"), "{e}");
}
}