572 lines
22 KiB
Rust
572 lines
22 KiB
Rust
// SPDX-License-Identifier: GPL-2.0-only
|
|
//
|
|
// rs-mrxvt — a modernized, distro-agnostic mrxvt-inspired terminal emulator.
|
|
//
|
|
// Copyright (C) 2024 rs-mrxvt contributors
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 2 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along
|
|
// with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
//! Structured GPU detection for the wgpu backend.
|
|
//!
|
|
//! Instead of a bare `bool`, this module probes the system and returns a
|
|
//! [`GpuProbeResult`] that tells you *what* was found (adapter name, backend
|
|
//! type, limits) and *why* each step succeeded or failed. Every probe step
|
|
//! logs a clear message so the user can see exactly what the fallback chain
|
|
//! is doing at startup.
|
|
//!
|
|
//! ## Usage
|
|
//!
|
|
//! ```ignore
|
|
//! let result = GpuDetect::probe();
|
|
//! match result {
|
|
//! GpuProbeResult::Available { adapter_info, backend, .. } => {
|
|
//! log::info!("using GPU: {} via {}", adapter_info.name, backend);
|
|
//! }
|
|
//! GpuProbeResult::Unavailable { reasons } => {
|
|
//! log::warn!("no GPU: {}", reasons.join("; "));
|
|
//! }
|
|
//! }
|
|
//! ```
|
|
//!
|
|
//! ## Fallback semantics
|
|
//!
|
|
//! The probe tries backends in this order (configurable via [`GpuDetectOptions`]):
|
|
//! 1. **Vulkan** — best performance on Linux/BSD.
|
|
//! 2. **Metal** — native on macOS.
|
|
//! 3. **DX12** — native on Windows.
|
|
//! 4. **GL** — broadest compatibility (works on Mesa software, llvmpipe).
|
|
//!
|
|
//! If all fail, the result is `Unavailable` with a list of reasons.
|
|
//! The caller (the backend selection chain) then falls through to
|
|
//! softbuffer → TUI.
|
|
|
|
use std::fmt;
|
|
|
|
/// Which wgpu backend was used (or attempted).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
#[non_exhaustive]
|
|
pub enum GpuBackendType {
|
|
/// Vulkan (Linux, Windows, some Android).
|
|
Vulkan,
|
|
/// Metal (macOS, iOS).
|
|
Metal,
|
|
/// Direct3D 12 (Windows).
|
|
Dx12,
|
|
/// OpenGL / GLES (broadest compatibility, includes software Mesa).
|
|
Gl,
|
|
/// WebGPU (browser / wasm).
|
|
WebGpu,
|
|
}
|
|
|
|
impl fmt::Display for GpuBackendType {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Vulkan => write!(f, "Vulkan"),
|
|
Self::Metal => write!(f, "Metal"),
|
|
Self::Dx12 => write!(f, "DX12"),
|
|
Self::Gl => write!(f, "OpenGL"),
|
|
Self::WebGpu => write!(f, "WebGPU"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Information about a successfully detected GPU adapter.
|
|
#[derive(Debug, Clone)]
|
|
pub struct GpuAdapterInfo {
|
|
/// Human-readable adapter name (e.g. "NVIDIA GeForce RTX 4090").
|
|
pub name: String,
|
|
/// Which backend is driving this adapter.
|
|
pub backend: GpuBackendType,
|
|
/// Vendor ID (e.g. 0x10DE for NVIDIA). 0 if unknown.
|
|
pub vendor_id: u32,
|
|
/// Device ID. 0 if unknown.
|
|
pub device_id: u32,
|
|
/// Backend-specific device type (discrete, integrated, virtual, etc.).
|
|
pub device_type: GpuDeviceType,
|
|
/// Driver name reported by the backend.
|
|
pub driver_name: String,
|
|
/// Driver info string.
|
|
pub driver_info: String,
|
|
/// Maximum texture dimension (1D and 2D).
|
|
pub max_texture_size: u32,
|
|
/// Maximum buffer size in bytes.
|
|
pub max_buffer_size: u64,
|
|
/// Maximum storage buffer binding size.
|
|
pub max_storage_buffer_size: u64,
|
|
}
|
|
|
|
/// What kind of GPU device was detected.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum GpuDeviceType {
|
|
/// Discrete GPU (dedicated VRAM).
|
|
DiscreteGpu,
|
|
/// Integrated GPU (shared system RAM).
|
|
IntegratedGpu,
|
|
/// Virtual / paravirtualized GPU (VM pass-through, virtio-gpu).
|
|
VirtualGpu,
|
|
/// CPU-based software rasterizer (llvmpipe, swiftshader).
|
|
Cpu,
|
|
/// Unknown / other.
|
|
Other,
|
|
}
|
|
|
|
impl fmt::Display for GpuDeviceType {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::DiscreteGpu => write!(f, "discrete GPU"),
|
|
Self::IntegratedGpu => write!(f, "integrated GPU"),
|
|
Self::VirtualGpu => write!(f, "virtual GPU"),
|
|
Self::Cpu => write!(f, "CPU software rasterizer"),
|
|
Self::Other => write!(f, "other"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result of a GPU probe attempt.
|
|
#[derive(Debug, Clone)]
|
|
pub enum GpuProbeResult {
|
|
/// A suitable GPU adapter was found.
|
|
Available {
|
|
/// Information about the detected adapter.
|
|
adapter_info: GpuAdapterInfo,
|
|
/// All backends that were tried and their individual results.
|
|
probe_log: Vec<BackendProbeEntry>,
|
|
},
|
|
/// No suitable GPU adapter was found.
|
|
Unavailable {
|
|
/// Human-readable reasons for each failed probe.
|
|
reasons: Vec<String>,
|
|
/// All backends that were tried and their individual results.
|
|
probe_log: Vec<BackendProbeEntry>,
|
|
},
|
|
}
|
|
|
|
impl GpuProbeResult {
|
|
/// Returns `true` if a GPU adapter was found.
|
|
pub fn is_available(&self) -> bool {
|
|
matches!(self, GpuProbeResult::Available { .. })
|
|
}
|
|
|
|
/// Returns the adapter info if available.
|
|
pub fn adapter_info(&self) -> Option<&GpuAdapterInfo> {
|
|
match self {
|
|
GpuProbeResult::Available { adapter_info, .. } => Some(adapter_info),
|
|
GpuProbeResult::Unavailable { .. } => None,
|
|
}
|
|
}
|
|
|
|
/// Returns a human-readable summary suitable for `--gpu-info`.
|
|
pub fn summary(&self) -> String {
|
|
match self {
|
|
GpuProbeResult::Available { adapter_info, probe_log } => {
|
|
let mut lines = vec![format!(
|
|
"GPU detected: {} ({}, {})",
|
|
adapter_info.name, adapter_info.backend, adapter_info.device_type
|
|
)];
|
|
lines.push(format!(" Vendor: 0x{:04X}", adapter_info.vendor_id));
|
|
lines.push(format!(" Device: 0x{:04X}", adapter_info.device_id));
|
|
lines.push(format!(" Driver: {} ({})", adapter_info.driver_name, adapter_info.driver_info));
|
|
lines.push(format!(" Max tex: {}x{}", adapter_info.max_texture_size, adapter_info.max_texture_size));
|
|
lines.push(format!(" Max buf: {} MB", adapter_info.max_buffer_size / 1_048_576));
|
|
|
|
lines.push(String::new());
|
|
lines.push("Probe log:".into());
|
|
lines.extend(probe_log.iter().map(|e| format!(" {} — {}", e.backend, e.result)));
|
|
lines.join("\n")
|
|
}
|
|
GpuProbeResult::Unavailable { reasons, probe_log } => {
|
|
let mut lines = vec!["No suitable GPU adapter found.".into()];
|
|
lines.push(String::new());
|
|
lines.push("Failure reasons:".into());
|
|
for r in reasons {
|
|
lines.push(format!(" - {r}"));
|
|
}
|
|
lines.push(String::new());
|
|
lines.push("Probe log:".into());
|
|
lines.extend(probe_log.iter().map(|e| format!(" {} — {}", e.backend, e.result)));
|
|
|
|
lines.push(String::new());
|
|
lines.push("Falling back to: softbuffer (CPU rasterizer) or TUI".into());
|
|
lines.join("\n")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A single entry in the probe log — one backend's probe attempt.
|
|
#[derive(Debug, Clone)]
|
|
pub struct BackendProbeEntry {
|
|
/// Which backend was probed.
|
|
pub backend: GpuBackendType,
|
|
/// Whether it was available at the wgpu instance level.
|
|
pub instance_supported: bool,
|
|
/// Human-readable result.
|
|
pub result: String,
|
|
}
|
|
|
|
/// Options that control how GPU detection behaves.
|
|
///
|
|
/// These can come from config (`[gpu]` section) or CLI flags.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct GpuDetectOptions {
|
|
/// Preferred backend order. The probe tries these in sequence and stops
|
|
/// at the first success. Empty = use the default order.
|
|
pub preferred_backends: Vec<GpuBackendType>,
|
|
/// If true, accept software rasterizers (llvmpipe, swiftshader) as valid.
|
|
/// When false, a CPU adapter is treated as "not available" and the probe
|
|
/// continues to the next backend.
|
|
pub accept_software_rasterizer: bool,
|
|
/// If true, force wgpu to use its built-in software fallback adapter
|
|
/// (rendering via CPU even when wgpu is compiled). Useful for debugging.
|
|
pub force_fallback_adapter: bool,
|
|
/// Require a minimum maximum texture size. If the adapter reports less,
|
|
/// it's rejected. 0 = no minimum.
|
|
pub min_texture_size: u32,
|
|
}
|
|
|
|
impl GpuDetectOptions {
|
|
/// Build from the config's `[gpu]` section.
|
|
#[cfg(feature = "gpu")]
|
|
pub fn from_config(gpu_cfg: &crate::config::GpuConfig) -> Self {
|
|
let mut preferred = Vec::new();
|
|
if let Some(ref order) = gpu_cfg.preferred_backend {
|
|
for name in order.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
|
|
match name.to_lowercase().as_str() {
|
|
"vulkan" => preferred.push(GpuBackendType::Vulkan),
|
|
"metal" => preferred.push(GpuBackendType::Metal),
|
|
"dx12" | "directx12" => preferred.push(GpuBackendType::Dx12),
|
|
"gl" | "opengl" => preferred.push(GpuBackendType::Gl),
|
|
_ => {
|
|
log::warn!("unknown preferred_backend '{name}', skipping");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Self {
|
|
preferred_backends: preferred,
|
|
accept_software_rasterizer: gpu_cfg.accept_software_rasterizer,
|
|
force_fallback_adapter: gpu_cfg.force_fallback_adapter,
|
|
min_texture_size: gpu_cfg.min_texture_size,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn device_priority(dt: GpuDeviceType) -> i32 {
|
|
match dt {
|
|
GpuDeviceType::DiscreteGpu => 4,
|
|
GpuDeviceType::IntegratedGpu => 3,
|
|
GpuDeviceType::VirtualGpu => 2,
|
|
GpuDeviceType::Cpu => 1,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
/// The GPU detector. Call [`GpuDetect::probe()`] to run the detection.
|
|
pub struct GpuDetect;
|
|
|
|
impl GpuDetect {
|
|
/// Run the full GPU detection probe.
|
|
///
|
|
/// Tries each wgpu backend in order (or the user's preferred order)
|
|
/// and returns the first adapter that meets the requirements.
|
|
pub fn probe() -> GpuProbeResult {
|
|
Self::probe_with_options(GpuDetectOptions::default())
|
|
}
|
|
|
|
/// Run the probe with custom options (from config or CLI).
|
|
pub fn probe_with_options(opts: GpuDetectOptions) -> GpuProbeResult {
|
|
let mut probe_log = Vec::new();
|
|
let mut all_reasons = Vec::new();
|
|
|
|
// Build the wgpu Backends bitfield from our options or defaults.
|
|
let backends_to_try: Vec<GpuBackendType> = if opts.preferred_backends.is_empty() {
|
|
vec![
|
|
GpuBackendType::Vulkan,
|
|
GpuBackendType::Metal,
|
|
GpuBackendType::Dx12,
|
|
GpuBackendType::Gl,
|
|
]
|
|
} else {
|
|
opts.preferred_backends.clone()
|
|
};
|
|
|
|
log::info!("GPU probe: trying backends [{}]",
|
|
backends_to_try.iter().map(|b| b.to_string()).collect::<Vec<_>>().join(", "));
|
|
|
|
// Enumerate all available adapters. Each backend gets its own focused
|
|
// `wgpu::Instance` (created inside the loop below) so we can attribute
|
|
// adapters to specific backends.
|
|
let adapters: Vec<(GpuBackendType, wgpu::Adapter)> = pollster::block_on(async {
|
|
let mut result = Vec::new();
|
|
for bt in &backends_to_try {
|
|
let wgpu_backend = match bt {
|
|
GpuBackendType::Vulkan => wgpu::Backends::VULKAN,
|
|
GpuBackendType::Metal => wgpu::Backends::METAL,
|
|
GpuBackendType::Dx12 => wgpu::Backends::DX12,
|
|
GpuBackendType::Gl => wgpu::Backends::GL,
|
|
GpuBackendType::WebGpu => wgpu::Backends::BROWSER_WEBGPU,
|
|
};
|
|
|
|
// Create a focused instance for just this backend.
|
|
let focused = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
|
backends: wgpu_backend,
|
|
flags: wgpu::InstanceFlags::default(),
|
|
dx12_shader_compiler: wgpu::Dx12Compiler::default(),
|
|
gles_minor_version: wgpu::Gles3MinorVersion::default(),
|
|
});
|
|
|
|
let adapter = focused.request_adapter(&wgpu::RequestAdapterOptions {
|
|
power_preference: wgpu::PowerPreference::HighPerformance,
|
|
compatible_surface: None,
|
|
force_fallback_adapter: opts.force_fallback_adapter,
|
|
}).await;
|
|
|
|
match adapter {
|
|
Some(a) => {
|
|
let info = a.get_info();
|
|
log::info!(" [{}] found adapter: {}", bt, info.name);
|
|
result.push((*bt, a));
|
|
}
|
|
None => {
|
|
log::info!(" [{}] no adapter found", bt);
|
|
probe_log.push(BackendProbeEntry {
|
|
backend: *bt,
|
|
instance_supported: true,
|
|
result: "no adapter found".into(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
result
|
|
});
|
|
|
|
// Try each adapter and pick the best one.
|
|
//
|
|
// We only keep the `GpuAdapterInfo` (not the `wgpu::Adapter` itself)
|
|
// because `wgpu::Adapter` is not `Clone` and the adapter is never
|
|
// actually consumed after this probe — `GpuProbeResult::Available`
|
|
// only carries the info struct. The wgpu renderer re-creates its own
|
|
// adapter at init time from the cached backend hint.
|
|
let mut best: Option<GpuAdapterInfo> = None;
|
|
|
|
for (bt, adapter) in &adapters {
|
|
let info = adapter.get_info();
|
|
|
|
// Classify device type.
|
|
let device_type = match info.device_type {
|
|
wgpu::DeviceType::DiscreteGpu => GpuDeviceType::DiscreteGpu,
|
|
wgpu::DeviceType::IntegratedGpu => GpuDeviceType::IntegratedGpu,
|
|
wgpu::DeviceType::VirtualGpu => GpuDeviceType::VirtualGpu,
|
|
wgpu::DeviceType::Cpu => GpuDeviceType::Cpu,
|
|
wgpu::DeviceType::Other => GpuDeviceType::Other,
|
|
};
|
|
|
|
// Skip software rasterizers if the user doesn't want them.
|
|
if device_type == GpuDeviceType::Cpu && !opts.accept_software_rasterizer {
|
|
let reason = format!(
|
|
"[{}] adapter '{}' is a CPU software rasterizer (rejected: accept_software_rasterizer=false)",
|
|
bt, info.name
|
|
);
|
|
log::info!(" {}", reason);
|
|
all_reasons.push(reason);
|
|
probe_log.push(BackendProbeEntry {
|
|
backend: *bt,
|
|
instance_supported: true,
|
|
result: format!("CPU rasterizer '{}' (rejected by config)", info.name),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Try to create a device to validate the adapter actually works.
|
|
let device_result = pollster::block_on(async {
|
|
adapter.request_device(
|
|
&wgpu::DeviceDescriptor {
|
|
label: Some("gpu-probe-validation"),
|
|
required_features: wgpu::Features::empty(),
|
|
required_limits: wgpu::Limits::downlevel_defaults(),
|
|
memory_hints: wgpu::MemoryHints::Performance,
|
|
},
|
|
None,
|
|
).await
|
|
});
|
|
|
|
match device_result {
|
|
Ok((device, _queue)) => {
|
|
let limits = device.limits();
|
|
|
|
// Check minimum texture size.
|
|
if opts.min_texture_size > 0
|
|
&& limits.max_texture_dimension_2d < opts.min_texture_size
|
|
{
|
|
let reason = format!(
|
|
"[{}] adapter '{}' max texture size {} < required {}",
|
|
bt, info.name, limits.max_texture_dimension_2d, opts.min_texture_size
|
|
);
|
|
log::info!(" {}", reason);
|
|
all_reasons.push(reason);
|
|
probe_log.push(BackendProbeEntry {
|
|
backend: *bt,
|
|
instance_supported: true,
|
|
result: format!("max_texture_dimension_2d={} < required {}",
|
|
limits.max_texture_dimension_2d, opts.min_texture_size),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
let adapter_info = GpuAdapterInfo {
|
|
name: info.name.clone(),
|
|
backend: *bt,
|
|
vendor_id: info.vendor,
|
|
device_id: info.device,
|
|
device_type,
|
|
driver_name: info.driver.clone(),
|
|
driver_info: info.driver_info.clone(),
|
|
max_texture_size: limits.max_texture_dimension_2d,
|
|
max_buffer_size: limits.max_buffer_size,
|
|
// wgpu 22 renamed this field and narrowed it to u32.
|
|
max_storage_buffer_size: u64::from(limits.max_storage_buffer_binding_size),
|
|
};
|
|
|
|
// Prefer discrete > integrated > virtual > CPU.
|
|
let priority = device_priority(device_type);
|
|
|
|
let best_priority = best.as_ref().map_or(-1i32, |bi| device_priority(bi.device_type));
|
|
|
|
if priority > best_priority {
|
|
log::info!(" [{}] adapter '{}' selected (priority={})", bt, info.name, priority);
|
|
best = Some(adapter_info);
|
|
}
|
|
|
|
probe_log.push(BackendProbeEntry {
|
|
backend: *bt,
|
|
instance_supported: true,
|
|
result: format!("OK — {} ({})", info.name, device_type),
|
|
});
|
|
}
|
|
Err(e) => {
|
|
let reason = format!("[{}] adapter '{}' device creation failed: {}", bt, info.name, e);
|
|
log::warn!(" {}", reason);
|
|
all_reasons.push(reason);
|
|
probe_log.push(BackendProbeEntry {
|
|
backend: *bt,
|
|
instance_supported: true,
|
|
result: format!("device creation failed: {}", e),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
match best {
|
|
Some(adapter_info) => {
|
|
log::info!(
|
|
"GPU selected: {} via {} ({}, vendor=0x{:04X}, device=0x{:04X})",
|
|
adapter_info.name,
|
|
adapter_info.backend,
|
|
adapter_info.device_type,
|
|
adapter_info.vendor_id,
|
|
adapter_info.device_id,
|
|
);
|
|
GpuProbeResult::Available {
|
|
adapter_info,
|
|
probe_log,
|
|
}
|
|
}
|
|
None => {
|
|
if all_reasons.is_empty() {
|
|
all_reasons.push(
|
|
"no wgpu backends compiled into this build (need --features gpu)".into()
|
|
);
|
|
}
|
|
log::warn!("GPU probe failed: {}", all_reasons.join("; "));
|
|
GpuProbeResult::Unavailable {
|
|
reasons: all_reasons,
|
|
probe_log,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Quick check: is *any* GPU likely available?
|
|
///
|
|
/// This is the fast path used by the backend registry's `available()`.
|
|
/// It does not enumerate adapters or create devices — it just checks
|
|
/// whether wgpu can find an adapter at all.
|
|
pub fn is_available() -> bool {
|
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
|
backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
|
|
flags: wgpu::InstanceFlags::default(),
|
|
dx12_shader_compiler: wgpu::Dx12Compiler::default(),
|
|
gles_minor_version: wgpu::Gles3MinorVersion::default(),
|
|
});
|
|
|
|
pollster::block_on(async {
|
|
instance
|
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
|
power_preference: wgpu::PowerPreference::LowPower,
|
|
compatible_surface: None,
|
|
force_fallback_adapter: false,
|
|
})
|
|
.await
|
|
.is_some()
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// Note: these tests require a GPU (or wgpu software fallback) to be present.
|
|
// They're structured so they pass in CI if wgpu can find any adapter
|
|
// (including software), and are skipped if even that fails.
|
|
|
|
#[test]
|
|
fn probe_returns_structured_result() {
|
|
let result = GpuDetect::probe();
|
|
// Just verify it doesn't panic and returns one of the two variants.
|
|
match &result {
|
|
GpuProbeResult::Available { adapter_info, .. } => {
|
|
assert!(!adapter_info.name.is_empty());
|
|
}
|
|
GpuProbeResult::Unavailable { reasons, .. } => {
|
|
assert!(!reasons.is_empty());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn probe_summary_does_not_panic() {
|
|
let result = GpuDetect::probe();
|
|
let _summary = result.summary();
|
|
}
|
|
|
|
#[test]
|
|
fn is_available_is_consistent_with_probe() {
|
|
let quick = GpuDetect::is_available();
|
|
let full = GpuDetect::probe().is_available();
|
|
// They should agree in most cases. The quick check uses LowPower
|
|
// and only Vulkan+GL, while the full probe is more thorough, so
|
|
// full may find something that quick doesn't — but quick should
|
|
// never find something that full doesn't.
|
|
if quick && !full {
|
|
// This is a valid edge case (quick found GL but full rejected
|
|
// it after device validation), so we don't assert equality.
|
|
} else if full && !quick {
|
|
// Full probe found Metal/DX12 but quick only tried Vulkan+GL.
|
|
// Also valid.
|
|
}
|
|
}
|
|
} |