//! 渲染后端探测 + 自动降级(D27)。 //! //! ## 为什么需要探测 //! //! iced 0.14 的 `Renderer` 本身就是「wgpu → tiny-skia」的运行时回退枚举(M60 Q1): //! **不设** `ICED_BACKEND` 时,拿不到 GPU 适配器会自动降级到 CPU,零代码。 //! 需要自研的只有**判据**: //! //! - 本机会枚举出 `Microsoft Basic Render Driver [Cpu]`(= WARP 软光栅), //! 所以「有没有适配器」不能当「有没有 GPU」(M60 实测),必须看 `device_type`。 //! - 没有硬件 GPU 时要**主动**把 CPU 放到候选首位(`tiny-skia,wgpu`), //! 否则 wgpu 会先花时间尝试 WARP,观感与行为都难料。 //! //! ## 语义订正(与 M60 备忘录的差异,以源码 + 实测为准) //! //! M60 记「`ICED_BACKEND=wgpu` 会**关闭**回退」。读 `iced_renderer-0.14.0/src/fallback.rs` //! 得到的是:**候选列表**语义——列表里没有 `tiny-skia` 时才真的关闭回退。 //! 所以本 crate 的「优先 GPU」写 `wgpu,tiny-skia`(首选 GPU,拿不到仍降级,**绝不硬失败**), //! 而不是 `wgpu`。 use crate::Backend; /// 单个适配器的摘要(只留日志要用的字段)。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AdapterSummary { /// 适配器名(如 `AMD Radeon RX 6600 XT`)。 pub name: String, /// `device_type` 的字符串形式(`DiscreteGpu` / `IntegratedGpu` / `Cpu` / …)。 pub device_type: &'static str, } /// 探测结论。 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Probe { /// 枚举到的全部适配器(含 WARP 这类软光栅)。 pub adapters: Vec, /// 是否存在**真**硬件适配器(`device_type` 既不是 `Cpu` 也不是 `Other`)。 pub hardware: bool, } impl Probe { /// 探测本机渲染能力(编入 `wgpu` 时真的枚举适配器)。 #[cfg(feature = "gpu")] pub fn run() -> Self { run_wgpu() } /// 探测本机渲染能力(`--no-default-features` 的纯 CPU 构建:没有 wgpu,直接判定无硬件)。 #[cfg(not(feature = "gpu"))] pub fn run() -> Self { Self::default() } /// 一行日志:适配器清单 + 判定。 pub fn describe(&self) -> String { if self.adapters.is_empty() { return format!("adapters=0 hardware={}", self.hardware); } let list = self .adapters .iter() .map(|adapter| format!("{} [{}]", adapter.name, adapter.device_type)) .collect::>() .join(" | "); format!( "adapters={} [{list}] hardware={}", self.adapters.len(), self.hardware ) } } /// 按请求与探测结果决定 `ICED_BACKEND` 的值(`None` = 不干预,交给 iced 自己回退)。 /// /// 纯函数,好让「降级矩阵」被测试钉死。 pub fn backend_env(request: Backend, probe: &Probe, gpu_compiled: bool) -> Option<&'static str> { match (request, gpu_compiled) { // 命令行 --software-renderer / --renderer=cpu:只留 CPU 候选。 (Backend::Cpu, _) => Some("tiny-skia"), // 用户点名要 GPU,但没编进 wgpu:只能 CPU,日志里说明。 (Backend::Gpu, false) => Some("tiny-skia"), // 优先 GPU,但保留 tiny-skia 兜底(候选列表语义:不会硬失败)。 (Backend::Gpu, true) => Some("wgpu,tiny-skia"), // 自动:有硬件就让 iced 走默认顺序(wgpu 优先,自动回退)。 (Backend::Auto, true) if probe.hardware => None, // 自动 + 没编进 wgpu:只有 CPU 候选,别把 wgpu 写进列表(会与「未编入 wgpu」的日志自相矛盾)。 (Backend::Auto, false) => Some("tiny-skia"), // 自动 + 有 wgpu 但没硬件(含只有 WARP 的机器):CPU 起步,wgpu 留作兜底。 (Backend::Auto, true) => Some("tiny-skia,wgpu"), } } /// 把决策落到进程环境变量,并把探测结果 + 生效值打进日志。 /// /// 返回**实际生效**的后端描述(`"auto (iced: wgpu -> tiny-skia)"` 之类)。 pub fn apply(request: Backend, probe: &Probe) -> String { let gpu_compiled = cfg!(feature = "gpu"); let env = backend_env(request, probe, gpu_compiled); // ICED_BACKEND 是进程级环境变量,必须在 `iced::application(..).run()` 之前写。 // 仓库是 edition 2021 → `set_var` 仍是安全 API(升 2024 后要改成子进程 exec 自身)。 if let Some(value) = env { std::env::set_var("ICED_BACKEND", value); } let effective = if gpu_compiled { match (request, env) { (Backend::Cpu, _) => "tiny-skia (CPU,强制)".to_owned(), (_, None) => "auto (wgpu -> tiny-skia)".to_owned(), (_, Some("wgpu,tiny-skia")) => "wgpu.then(tiny-skia) (优先 GPU)".to_owned(), (_, Some(value)) => format!("{value} (CPU 起步)"), } } else { "tiny-skia (本构建未编入 wgpu,只有 CPU)".to_owned() }; let note = if !gpu_compiled && request != Backend::Cpu { ";本构建未编入 wgpu(--no-default-features),只能 CPU" } else { "" }; eprintln!( "[probe] {} :: request={} -> ICED_BACKEND={}{note}", probe.describe(), request.name(), env.unwrap_or("") ); eprintln!("[probe] renderer = {effective}"); effective } #[cfg(feature = "gpu")] fn run_wgpu() -> Probe { // `iced::wgpu` 就是 wgpu crate 本身(`pub use iced_renderer::wgpu::wgpu` 再导出), // 所以不需要单独依赖 wgpu,版本也不会和 iced 打架。 use iced::wgpu::{Backends, DeviceType, Instance, InstanceDescriptor}; let instance = Instance::new(&InstanceDescriptor { backends: Backends::all(), ..Default::default() }); let adapters = instance .enumerate_adapters(Backends::all()) .into_iter() .map(|adapter| { let info = adapter.get_info(); let device_type = match info.device_type { DeviceType::DiscreteGpu => "DiscreteGpu", DeviceType::IntegratedGpu => "IntegratedGpu", DeviceType::VirtualGpu => "VirtualGpu", DeviceType::Cpu => "Cpu", DeviceType::Other => "Other", }; AdapterSummary { name: info.name, device_type, } }) .collect::>(); let hardware = adapters .iter() .any(|adapter| !matches!(adapter.device_type, "Cpu" | "Other")); Probe { adapters, hardware } } #[cfg(test)] mod tests { use super::*; fn probe(types: &[&'static str]) -> Probe { let adapters = types .iter() .map(|device_type| AdapterSummary { name: "adapter".to_owned(), device_type, }) .collect::>(); Probe { adapters, hardware: types .iter() .any(|device_type| !matches!(*device_type, "Cpu" | "Other")), } } #[test] fn warp_only_machine_counts_as_no_gpu() { // 本机实测会枚举出 WARP:`Microsoft Basic Render Driver [Cpu]` let warp = probe(&["Cpu"]); assert!(!warp.hardware, "只有 WARP 不能当「有 GPU」"); assert_eq!( backend_env(Backend::Auto, &warp, true), Some("tiny-skia,wgpu") ); } #[test] fn auto_with_real_gpu_leaves_iced_alone() { let gpu = probe(&["DiscreteGpu", "Cpu"]); assert!(gpu.hardware); assert_eq!(backend_env(Backend::Auto, &gpu, true), None); } #[test] fn explicit_choices_always_keep_a_fallback() { let gpu = probe(&["DiscreteGpu"]); let none = probe(&[]); // 优先 GPU 但候选列表里留 tiny-skia:拿不到适配器时降级而不是报错 assert_eq!( backend_env(Backend::Gpu, &gpu, true), Some("wgpu,tiny-skia") ); assert_eq!( backend_env(Backend::Gpu, &none, true), Some("wgpu,tiny-skia") ); assert_eq!(backend_env(Backend::Cpu, &gpu, true), Some("tiny-skia")); // 没编进 wgpu 的构建绝不写 wgpu(连候选列表也不许出现,reviewer P3-4) assert_eq!(backend_env(Backend::Gpu, &gpu, false), Some("tiny-skia")); assert_eq!(backend_env(Backend::Auto, &gpu, false), Some("tiny-skia")); assert_eq!(backend_env(Backend::Auto, &none, false), Some("tiny-skia")); } #[test] fn describe_lists_every_adapter() { let text = probe(&["DiscreteGpu", "Cpu"]).describe(); assert!(text.contains("DiscreteGpu"), "{text}"); assert!(text.contains("Cpu"), "{text}"); assert!(text.contains("hardware=true"), "{text}"); } }