mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Merge pull request #1688
This commit is contained in:
@@ -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
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user