M97 的入口是一份 macOS M1 真机报告:探测认出了 Apple M1(adapters=1 /
hardware=true),日志却写着 `renderer = auto (wgpu -> tiny-skia)`,看起来
像在跑 CPU。读源码发现那行是**常量**:`probe.rs` 按 `ICED_BACKEND` 未设就把
`"auto (wgpu -> tiny-skia)"` 打出来,与实际后端无关。真正的失败被 iced 吞掉:
`iced_renderer-0.14.0/src/fallback.rs` 的 `Compositor::with_backend` 把 wgpu 的
错误装进 `errors` 向量,tiny-skia 一成功就整体丢弃。
## 判据改为单项,不再交给 iced 回退
`ICED_BACKEND` 现在**永远是单项**(`wgpu` / `tiny-skia`):能跑起来就必定是它;
起不来就必然是 `iced::Error::GraphicsCreationFailed` 里的具名错误。判据是纯函数
`renderer::decide(request, probe, gpu_compiled)`,落在 `renderer/decision.rs`,
表驱动测试逐行钉死 1~6 行 + 「除强制 CPU / 未编 wgpu 外必须留兜底」的守门。
判据表(条件 / 选哪个 / 日志该打什么 / 谁能验证)在 `renderer/mod.rs` 顶部。
## 安全网改成换进程
第一版是「打印错误 → 在本进程再 run() 一次」,Windows 实测直接 panic:
`Create event loop: RecreationAttempt`(winit 一个进程只能建一次事件循环)。
改成 `respawn`:子进程只换 `ICED_BACKEND` + `ULL_GPU_FALLBACK`,父进程透传它的
退出码。子进程看到 `ULL_GPU_FALLBACK` 必须跳过探测,否则会无限递归重开。
## 取证通道不许自己降级
带 `--screenshot`(含 `--screen=gallery`)时 wgpu 起不来就**不出图**、退出码 1,
因为换了后端出的图会撒谎;交互启动才允许降级。每张图的日志现在打**实际**后端
(`[shot] … renderer=wgpu`),以前打的是请求值 `auto`——同一个病(在需要结果的
地方打请求)在仓库里出现了两次,这是第二次。
## 可观测性
- `log` 桥(`renderer/log_bridge.rs`)把 `iced_wgpu` 的
`Selected: AdapterInfo{…}` 接到 stderr:这是本仓库第一次能自证「真在用 GPU」。
按 target 分级(`iced_wgpu*` Info、`wgpu*` Warn),否则 Vulkan loader 会刷 90 行噪声。
- `renderer/preflight.rs`(默认关,`ULL_PREFLIGHT=1`):开窗前拿 wgpu 的具名错误。
实测 Windows RX 6600 XT 上 took=717ms,所以不进默认路径。
- `app/backend.rs` 的档位判据跟着改成读单项;**真降级到 CPU 的那一次面板 α 现在会
正确强制 1.0**(以前静默降级后仍按 GPU 档判)。
Windows 实测:auto → wgpu + `Selected: AdapterInfo{…backend: Vulkan}` 出图;
`--renderer=cpu` 逐字不变;`WGPU_BACKEND=metal` 造失败 → 具名错误 → 交互路径降级
不崩(首帧 531ms)/ 取证路径拒绝出图 exit 1。macOS Metal 路径需真机复核,
命令与期望日志行写在 `renderer/mod.rs` 的「真机验收」一节。
227 lines
8.7 KiB
Rust
227 lines
8.7 KiB
Rust
//! 渲染后端探测 + 自动降级(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 放到候选首位,否则 wgpu 会先花时间尝试 WARP,
|
||
//! 观感与行为都难料。
|
||
//!
|
||
//! ## M97 订正:判据在 [`crate::renderer::decide`],日志不再说谎
|
||
//!
|
||
//! 本模块只负责**探测**(`Probe::run`)和**落地**([`apply`]);决策表、兜底、日志格式
|
||
//! 全部搬到了 [`crate::renderer`] 的模块文档里(那张判据表就是验收标准)。
|
||
//!
|
||
//! 三条被删掉/改掉的东西,别再加回来:
|
||
//!
|
||
//! 1. **`ICED_BACKEND` 不再写候选列表**(`wgpu,tiny-skia` / `tiny-skia,wgpu` 这种)。
|
||
//! 列表语义让 iced 自己回退,而它的回退是静默的:`iced_renderer-0.14.0/src/fallback.rs`
|
||
//! 把 wgpu 的错误装进 `errors` 向量,tiny-skia 一成功就整体丢弃。现在只写**一项**,
|
||
//! 起不来就必然是具名错误,由 `app::boot` 打印并重开降级。
|
||
//! 2. **`auto` + 有硬件不再「不设变量」**。那等于把决策权交给一条我们看不见的路径。
|
||
//! 3. **`renderer = auto (wgpu -> tiny-skia)` 这行删了**。它是按「变量未设」打出来的常量,
|
||
//! 跟实际后端无关——M97 的 macOS 报告就是被它误导的。
|
||
|
||
use crate::renderer::{self, Decision};
|
||
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<AdapterSummary>,
|
||
/// 是否存在**真**硬件适配器(`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::<Vec<_>>()
|
||
.join(" | ");
|
||
|
||
format!(
|
||
"adapters={} [{list}] hardware={}",
|
||
self.adapters.len(),
|
||
self.hardware
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 把 [`renderer::decide`] 的结论落到进程环境变量,并把**判据**打进日志(不带预检)。
|
||
///
|
||
/// 这个入口给「没有重试能力」的调用方(组件库 harness,`--screen=gallery`)用:它写完变量就
|
||
/// 直接 `iced::application(..).run()`,拿不到 `app/boot.rs` 的「换进程降级」。
|
||
pub fn apply(request: Backend, probe: &Probe) -> Decision {
|
||
apply_with_preflight(request, probe, None)
|
||
}
|
||
|
||
/// 同上,但接受 [`renderer::preflight`] 的结果(`ULL_PREFLIGHT=1` 时才有值)。
|
||
///
|
||
/// 注意这里是「判据」不是「结果」:日志里不会出现「实际用了哪个后端」这种我们观测不到的话。
|
||
/// 实际后端的取证有两条,都在 [`crate::renderer`] 的判据表里:
|
||
/// ① `[gpu] iced_wgpu: …Selected: AdapterInfo…`(iced 自己在创建设备时打的);
|
||
/// ② 起不来时的具名错误 + `app/boot.rs` 的重开降级。
|
||
///
|
||
/// 预检失败就把 wgpu 降下来(判据表第 7 行),免得为了拿一条错误去闪一次窗。
|
||
pub fn apply_with_preflight(
|
||
request: Backend,
|
||
probe: &Probe,
|
||
preflight: Option<&Result<renderer::preflight::Verdict, renderer::preflight::Failure>>,
|
||
) -> Decision {
|
||
let gpu_compiled = cfg!(feature = "gpu");
|
||
let mut decision = renderer::decide(request, probe, gpu_compiled);
|
||
|
||
if matches!(preflight, Some(Err(_))) {
|
||
decision = renderer::demote_after_preflight(decision);
|
||
}
|
||
|
||
// ICED_BACKEND 是进程级环境变量,必须在 `iced::application(..).run()` 之前写。
|
||
// 仓库是 edition 2021 → `set_var` 仍是安全 API(升 2024 后要改成子进程 exec 自身)。
|
||
std::env::set_var("ICED_BACKEND", decision.primary.env_value());
|
||
|
||
let note = if !gpu_compiled && request != Backend::Cpu {
|
||
";本构建未编入 wgpu(--no-default-features),只能 CPU"
|
||
} else {
|
||
""
|
||
};
|
||
|
||
eprintln!(
|
||
"[probe] {} :: request={} -> ICED_BACKEND={}{note}",
|
||
probe.describe(),
|
||
request.name(),
|
||
decision.primary.env_value()
|
||
);
|
||
eprintln!("[probe] {}", decision.describe());
|
||
if decision.primary == renderer::Kind::Wgpu {
|
||
eprintln!(
|
||
"[probe] 取证:出现 `[gpu] iced_wgpu: …Selected: AdapterInfo…` ⇒ GPU 真生效;\
|
||
出现 `[gpu] wgpu 初始化失败` ⇒ 已降级 CPU"
|
||
);
|
||
}
|
||
|
||
decision
|
||
}
|
||
|
||
#[cfg(feature = "gpu")]
|
||
fn run_wgpu() -> Probe {
|
||
// `iced::wgpu` 就是 wgpu crate 本身(`pub use iced_renderer::wgpu::wgpu` 再导出),
|
||
// 所以不需要单独依赖 wgpu,版本也不会和 iced 打架。
|
||
use iced::wgpu::{Backends, 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();
|
||
AdapterSummary {
|
||
name: info.name,
|
||
device_type: renderer::preflight::device_type_name(info.device_type),
|
||
}
|
||
})
|
||
.collect::<Vec<_>>();
|
||
|
||
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::<Vec<_>>();
|
||
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_ne!(
|
||
renderer::decide(Backend::Auto, &warp, true).primary,
|
||
renderer::Kind::Wgpu
|
||
);
|
||
}
|
||
|
||
#[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}");
|
||
}
|
||
|
||
/// 判据矩阵本身在 [`crate::renderer::decide`] 的测试里逐行钉死(表驱动 + 兜底守门)。
|
||
/// 这里只守 `apply` 这一层的**落地**契约:写进环境变量的永远是**单项**。
|
||
///
|
||
/// 抓得住:谁把候选列表(`wgpu,tiny-skia`)加回来——那会让 iced 的静默回退复活。
|
||
/// 抓不住:iced 运行期真的选了谁(那是判据表第 8 行的事)。
|
||
#[test]
|
||
fn apply_writes_exactly_one_backend() {
|
||
let gpu = probe(&["IntegratedGpu"]);
|
||
for request in [Backend::Auto, Backend::Gpu, Backend::Cpu] {
|
||
let decision = renderer::decide(request, &gpu, true);
|
||
let value = decision.primary.env_value();
|
||
assert!(
|
||
!value.contains(','),
|
||
"ICED_BACKEND 只许单项,收到 {value:?}"
|
||
);
|
||
assert!(matches!(value, "wgpu" | "tiny-skia"), "{value}");
|
||
}
|
||
}
|
||
}
|