Headless pictures draw on the GPU, with the viewport's pipelines (CPU rasterizer as last tier) (#1555)
Thanks @jyviko.
This commit is contained in:
1 parent
48322f6464
commit
1a0b33fcf3
54 files changed
+3525
-1998
No files matched your search
@@ -0,0 +1,261 @@
|
||||
//! What draws a headless picture: every adapter the platform's native backends offer is
|
||||
//! classified as hardware (a discrete, integrated or virtual GPU) or software (a GPU driver that
|
||||
//! runs on the CPU, such as llvmpipe, lavapipe or WARP), and a [`Policy`] picks one. The last
|
||||
//! tier, below any adapter, is SolidCraft's own CPU rasterizer (no surface analysis, highlights
|
||||
//! or legends): `auto` lands there only when no adapter qualifies, `cpu` forces it, `hardware`
|
||||
//! and `software` never use it. macOS uses Metal only, and has no software adapter. `SOLIDCRAFT_GPU`
|
||||
//! (`auto`, `hardware`, `software`, `cpu`) sets the policy from the environment; `WGPU_BACKEND`
|
||||
//! still narrows the backends.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Whether an adapter is a GPU or a software rasterizer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Tier {
|
||||
Hardware,
|
||||
Software,
|
||||
}
|
||||
|
||||
/// Which adapters a headless device may open.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum Policy {
|
||||
/// The best hardware adapter, else a software one.
|
||||
#[default]
|
||||
Auto,
|
||||
/// A hardware adapter or nothing.
|
||||
Hardware,
|
||||
/// A software adapter or nothing.
|
||||
Software,
|
||||
/// The CPU rasterizer, whatever adapters there are.
|
||||
Cpu,
|
||||
}
|
||||
|
||||
/// Where a policy lands: an adapter of a tier, or the CPU rasterizer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Landing {
|
||||
Hardware,
|
||||
Software,
|
||||
Cpu,
|
||||
}
|
||||
|
||||
impl Landing {
|
||||
/// What a picture's reply names as its renderer: `gpu`, `gpu-software` or `cpu`.
|
||||
pub fn renderer(self) -> &'static str {
|
||||
match self {
|
||||
Landing::Hardware => "gpu",
|
||||
Landing::Software => "gpu-software",
|
||||
Landing::Cpu => "cpu",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tier> for Landing {
|
||||
fn from(t: Tier) -> Self {
|
||||
match t {
|
||||
Tier::Hardware => Landing::Hardware,
|
||||
Tier::Software => Landing::Software,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
/// `auto`, `hardware`, `software` or `cpu` (case-insensitive).
|
||||
pub fn parse(s: &str) -> Option<Policy> {
|
||||
match s.trim().to_ascii_lowercase().as_str() {
|
||||
"auto" => Some(Policy::Auto),
|
||||
"hardware" | "hw" | "gpu" => Some(Policy::Hardware),
|
||||
"software" | "sw" => Some(Policy::Software),
|
||||
"cpu" => Some(Policy::Cpu),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The policy `SOLIDCRAFT_GPU` names; [`Policy::Auto`] when it is unset, and an error when
|
||||
/// it names none.
|
||||
pub fn from_env() -> Result<Policy, String> {
|
||||
match std::env::var("SOLIDCRAFT_GPU") {
|
||||
Ok(v) => Policy::parse(&v).ok_or_else(|| format!("SOLIDCRAFT_GPU must be auto, hardware, software or cpu, not `{v}`")),
|
||||
Err(_) => Ok(Policy::Auto),
|
||||
}
|
||||
}
|
||||
|
||||
/// `auto`, `hardware`, `software` or `cpu`.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Policy::Auto => "auto",
|
||||
Policy::Hardware => "hardware",
|
||||
Policy::Software => "software",
|
||||
Policy::Cpu => "cpu",
|
||||
}
|
||||
}
|
||||
|
||||
fn admits(self, tier: Tier) -> bool {
|
||||
match self {
|
||||
Policy::Auto => true,
|
||||
Policy::Hardware => tier == Tier::Hardware,
|
||||
Policy::Software => tier == Tier::Software,
|
||||
Policy::Cpu => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the policy lands when the best qualifying adapter is of tier `adapter` (`None`: no
|
||||
/// adapter qualifies): that adapter; else the CPU rasterizer under `auto` (and always under
|
||||
/// `cpu`); else nowhere, and a picture is an error.
|
||||
pub fn lands_on(self, adapter: Option<Tier>) -> Option<Landing> {
|
||||
match (self, adapter) {
|
||||
(Policy::Cpu, _) => Some(Landing::Cpu),
|
||||
(_, Some(t)) => Some(t.into()),
|
||||
(Policy::Auto, None) => Some(Landing::Cpu),
|
||||
(Policy::Hardware | Policy::Software, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One adapter as the survey saw it.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct AdapterReport {
|
||||
pub name: String,
|
||||
pub backend: String,
|
||||
pub device_type: String,
|
||||
pub driver: String,
|
||||
pub tier: Tier,
|
||||
}
|
||||
|
||||
/// The backends a headless device looks at on this platform: Metal on macOS, Vulkan and D3D12
|
||||
/// on Windows, Vulkan and GL elsewhere; `WGPU_BACKEND` narrows them further.
|
||||
pub fn native_backends() -> wgpu::Backends {
|
||||
let native = if cfg!(target_os = "macos") {
|
||||
wgpu::Backends::METAL
|
||||
} else if cfg!(target_os = "windows") {
|
||||
wgpu::Backends::DX12 | wgpu::Backends::VULKAN
|
||||
} else {
|
||||
wgpu::Backends::VULKAN | wgpu::Backends::GL
|
||||
};
|
||||
match wgpu::Backends::from_env() {
|
||||
Some(env) if !(env & native).is_empty() => env & native,
|
||||
_ => native,
|
||||
}
|
||||
}
|
||||
|
||||
fn tier_of(t: wgpu::DeviceType) -> Tier {
|
||||
match t {
|
||||
wgpu::DeviceType::Cpu => Tier::Software,
|
||||
wgpu::DeviceType::DiscreteGpu | wgpu::DeviceType::IntegratedGpu | wgpu::DeviceType::VirtualGpu | wgpu::DeviceType::Other => Tier::Hardware,
|
||||
}
|
||||
}
|
||||
|
||||
/// Preference among adapters (higher first): a discrete GPU, an integrated one, a virtual one,
|
||||
/// an unknown kind, a software rasterizer.
|
||||
fn rank(t: wgpu::DeviceType) -> u8 {
|
||||
match t {
|
||||
wgpu::DeviceType::DiscreteGpu => 4,
|
||||
wgpu::DeviceType::IntegratedGpu => 3,
|
||||
wgpu::DeviceType::VirtualGpu => 2,
|
||||
wgpu::DeviceType::Other => 1,
|
||||
wgpu::DeviceType::Cpu => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn report(info: &wgpu::AdapterInfo) -> AdapterReport {
|
||||
AdapterReport {
|
||||
name: info.name.clone(),
|
||||
backend: format!("{:?}", info.backend),
|
||||
device_type: format!("{:?}", info.device_type),
|
||||
driver: [info.driver.as_str(), info.driver_info.as_str()].iter().filter(|s| !s.is_empty()).copied().collect::<Vec<_>>().join(" "),
|
||||
tier: tier_of(info.device_type),
|
||||
}
|
||||
}
|
||||
|
||||
fn instance() -> wgpu::Instance {
|
||||
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle_from_env();
|
||||
desc.backends = native_backends();
|
||||
wgpu::Instance::new(desc)
|
||||
}
|
||||
|
||||
/// Every adapter the native backends offer, best first.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn adapters(instance: &wgpu::Instance) -> Vec<wgpu::Adapter> {
|
||||
let mut all = pollster::block_on(instance.enumerate_adapters(native_backends()));
|
||||
all.sort_by_key(|a| std::cmp::Reverse(rank(a.get_info().device_type)));
|
||||
all
|
||||
}
|
||||
|
||||
/// What a survey found, which adapter the policy picks (`chosen` indexes `adapters`) and where
|
||||
/// it lands (`None`: nowhere, a picture is an error).
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Survey {
|
||||
pub backends: String,
|
||||
pub adapters: Vec<AdapterReport>,
|
||||
pub chosen: Option<usize>,
|
||||
pub lands_on: Option<Landing>,
|
||||
}
|
||||
|
||||
/// Every adapter the native backends offer, best first, and which one `policy` picks.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn survey(policy: Policy) -> Survey {
|
||||
let found: Vec<AdapterReport> = adapters(&instance()).iter().map(|a| report(&a.get_info())).collect();
|
||||
let chosen = found.iter().position(|r| policy.admits(r.tier));
|
||||
let lands_on = policy.lands_on(chosen.and_then(|i| found.get(i)).map(|r| r.tier));
|
||||
Survey { backends: format!("{:?}", native_backends()), adapters: found, chosen, lands_on }
|
||||
}
|
||||
|
||||
/// The adapter `policy` picks, with its report; `None` when no adapter qualifies.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn pick(policy: Policy) -> Option<(wgpu::Adapter, AdapterReport)> {
|
||||
adapters(&instance())
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
let r = report(&a.get_info());
|
||||
(a, r)
|
||||
})
|
||||
.find(|(_, r)| policy.admits(r.tier))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn policies_parse_and_admit_their_tiers() {
|
||||
assert_eq!(Policy::parse(" Hardware "), Some(Policy::Hardware));
|
||||
assert_eq!(Policy::parse("sw"), Some(Policy::Software));
|
||||
assert_eq!(Policy::parse("metal"), None);
|
||||
assert_eq!(Policy::parse("CPU"), Some(Policy::Cpu));
|
||||
assert!(!Policy::Cpu.admits(Tier::Software) && !Policy::Cpu.admits(Tier::Hardware));
|
||||
assert!(Policy::Auto.admits(Tier::Software) && Policy::Auto.admits(Tier::Hardware));
|
||||
assert!(!Policy::Hardware.admits(Tier::Software));
|
||||
assert!(!Policy::Software.admits(Tier::Hardware));
|
||||
}
|
||||
|
||||
/// Hardware GPU, then a software adapter, then the CPU rasterizer: `auto` walks all three,
|
||||
/// `hardware` and `software` never fall to the CPU, and `cpu` always lands there.
|
||||
#[test]
|
||||
fn policies_land_on_their_tiers() {
|
||||
use Landing as L;
|
||||
let (hw, sw) = (Some(Tier::Hardware), Some(Tier::Software));
|
||||
assert_eq!([hw, sw, None].map(|t| Policy::Auto.lands_on(t)), [Some(L::Hardware), Some(L::Software), Some(L::Cpu)]);
|
||||
assert_eq!(Policy::Hardware.lands_on(hw), Some(L::Hardware));
|
||||
assert_eq!(Policy::Hardware.lands_on(None), None);
|
||||
assert_eq!(Policy::Software.lands_on(sw), Some(L::Software));
|
||||
assert_eq!(Policy::Software.lands_on(None), None);
|
||||
assert_eq!([hw, sw, None].map(|t| Policy::Cpu.lands_on(t)), [Some(L::Cpu); 3]);
|
||||
assert_eq!([L::Hardware, L::Software, L::Cpu].map(L::renderer), ["gpu", "gpu-software", "cpu"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn software_rasterizers_rank_last() {
|
||||
assert_eq!(tier_of(wgpu::DeviceType::Cpu), Tier::Software);
|
||||
assert!(rank(wgpu::DeviceType::IntegratedGpu) > rank(wgpu::DeviceType::Cpu));
|
||||
assert!(rank(wgpu::DeviceType::DiscreteGpu) > rank(wgpu::DeviceType::IntegratedGpu));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_surveys_metal_only() {
|
||||
assert_eq!(native_backends() & !wgpu::Backends::METAL, wgpu::Backends::empty());
|
||||
let s = survey(Policy::Auto);
|
||||
assert!(s.adapters.iter().all(|a| a.backend == "Metal"), "{s:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user