fix(renderer): M97 后端判据 —— 静默降级改具名错误,取证通道拒绝降级出图
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` 的「真机验收」一节。
This commit is contained in:
1 parent
45cd44f756
commit
31cdfd5bbd
13 files changed
+1260
-157
No files matched your search
@@ -0,0 +1,151 @@
|
||||
//! 把 `iced_wgpu` / `wgpu` 的 `log` 输出转到 stderr(判据表第 8 行的取证通道)。
|
||||
//!
|
||||
//! ## 为什么需要它
|
||||
//!
|
||||
//! `iced_wgpu` 在**真的**创建设备时会自己打一行:
|
||||
//!
|
||||
//! ```text
|
||||
//! Selected: AdapterInfo { name: "Apple M1", backend: Metal, device_type: IntegratedGpu, … }
|
||||
//! ```
|
||||
//!
|
||||
//! 这行只有「wgpu 走到适配器那一步」才会出现,所以它是**GPU 真生效的实证**——不是我们的推断。
|
||||
//! 但它走 `log` crate,而本进程从来没装过 logger,于是 M1 那份报告里这行根本不存在:
|
||||
//! 「看不到」被当成了「没有」。装一个只放行 GPU 相关 target 的 logger,成本 60 行,
|
||||
//! 换来的是以后每次验收都能直接看见 wgpu 说了什么。
|
||||
//!
|
||||
//! ## 过滤
|
||||
//!
|
||||
//! 按 **target 分级**,不是一刀切:`wgpu_hal` / `wgpu_core` 在 Info 级别会刷出上百行 Vulkan
|
||||
//! loader 噪声(M97 实测:启动时 90 行 registry 扫描),把真正有用的
|
||||
//! `iced_wgpu: Selected: AdapterInfo{…}` 埋掉。
|
||||
//!
|
||||
//! | `ULL_GPU_LOG` | `iced_wgpu*` | `wgpu*`(hal / core) | 用途 |
|
||||
//! |---|---|---|---|
|
||||
//! | 未设 / `info` | Info | **Warn** | 默认:看得见选中的适配器/格式 + wgpu 的告警 |
|
||||
//! | `warn` | Warn | Warn | 只留告警(启动日志最干净) |
|
||||
//! | `debug` | Debug | Debug | 全量(诊断用;含 Vulkan loader 噪声) |
|
||||
//! | `off` | — | — | 不装 logger(回到 M97 之前,仅用于对照实验) |
|
||||
//!
|
||||
//! 其它 target(`winit`、`iced_winit` …)一律不放行:它们只会把启动日志淹掉。
|
||||
|
||||
use std::sync::Once;
|
||||
|
||||
/// 装 logger(进程内只生效一次;重复调用无害)。
|
||||
pub fn install() {
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
let Some(level) = level_from_env() else {
|
||||
return;
|
||||
};
|
||||
// `set_boxed_logger` 只能成功一次;已被别人装过就安静让位,不 panic。
|
||||
if log::set_boxed_logger(Box::new(Bridge { level })).is_ok() {
|
||||
log::set_max_level(level);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// `ULL_GPU_LOG` → 过滤级别;`None` = 别装(`off`)。
|
||||
fn level_from_env() -> Option<log::LevelFilter> {
|
||||
parse_level(&std::env::var("ULL_GPU_LOG").unwrap_or_default())
|
||||
}
|
||||
|
||||
/// 取值解释(纯函数,测试直接喂字符串,不碰进程环境)。
|
||||
fn parse_level(value: &str) -> Option<log::LevelFilter> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"off" | "0" | "no" | "false" => None,
|
||||
"warn" | "warning" => Some(log::LevelFilter::Warn),
|
||||
"debug" => Some(log::LevelFilter::Debug),
|
||||
"trace" => Some(log::LevelFilter::Trace),
|
||||
_ => Some(log::LevelFilter::Info),
|
||||
}
|
||||
}
|
||||
|
||||
/// 某个 target 在这个总级别下实际放行到哪一级。
|
||||
///
|
||||
/// `wgpu_hal`/`wgpu_core` 在 `info` 档被压到 `warn`:它们的 info 是 Vulkan loader 的原文,
|
||||
/// 对「到底用了哪个后端」没有信息量,只有噪声。
|
||||
fn target_level(level: log::LevelFilter, target: &str) -> log::LevelFilter {
|
||||
if level == log::LevelFilter::Info && !target.starts_with("iced_wgpu") {
|
||||
return log::LevelFilter::Warn;
|
||||
}
|
||||
level
|
||||
}
|
||||
|
||||
/// 只放行渲染后端相关 target 的 logger。
|
||||
struct Bridge {
|
||||
level: log::LevelFilter,
|
||||
}
|
||||
|
||||
impl log::Log for Bridge {
|
||||
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
|
||||
let target = metadata.target();
|
||||
is_gpu_target(target) && metadata.level() <= target_level(self.level, target)
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record<'_>) {
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
// iced_wgpu 用 `{:#?}` 打多行结构体;压成一行才好 grep / 贴验收报告。
|
||||
let message = record.args().to_string().replace('\n', " ");
|
||||
eprintln!("[gpu] {}: {}", record.target(), message.trim_end());
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
/// `iced_wgpu::window::compositor` / `wgpu_core::…` / `wgpu_hal::…` 都算。
|
||||
fn is_gpu_target(target: &str) -> bool {
|
||||
["iced_wgpu", "wgpu"]
|
||||
.iter()
|
||||
.any(|prefix| target.starts_with(prefix))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_gpu_targets_pass() {
|
||||
assert!(is_gpu_target("iced_wgpu::window::compositor"));
|
||||
assert!(is_gpu_target("wgpu_core::instance"));
|
||||
assert!(is_gpu_target("wgpu_hal::metal"));
|
||||
assert!(!is_gpu_target("iced_winit::lib"));
|
||||
assert!(!is_gpu_target("winit::event_loop"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_env_values_are_interpreted() {
|
||||
assert_eq!(parse_level(""), Some(log::LevelFilter::Info));
|
||||
assert_eq!(parse_level("info"), Some(log::LevelFilter::Info));
|
||||
assert_eq!(parse_level(" WARN "), Some(log::LevelFilter::Warn));
|
||||
assert_eq!(parse_level("off"), None);
|
||||
assert_eq!(parse_level("0"), None);
|
||||
}
|
||||
|
||||
/// 默认档(`info`)下 `wgpu_hal`/`wgpu_core` 被压到 Warn:它们是 Vulkan loader 原文,
|
||||
/// M97 实测一屏 90 行噪声,会把 `Selected: AdapterInfo` 埋掉。
|
||||
#[test]
|
||||
fn default_level_mutes_wgpu_noise_but_keeps_adapter_line() {
|
||||
use log::LevelFilter;
|
||||
assert_eq!(
|
||||
target_level(LevelFilter::Info, "iced_wgpu::window::compositor"),
|
||||
LevelFilter::Info,
|
||||
"选中适配器那行必须留"
|
||||
);
|
||||
assert_eq!(
|
||||
target_level(LevelFilter::Info, "wgpu_hal::vulkan::instance"),
|
||||
LevelFilter::Warn,
|
||||
"Vulkan loader 噪声必须压掉"
|
||||
);
|
||||
assert_eq!(
|
||||
target_level(LevelFilter::Info, "wgpu_core::instance"),
|
||||
LevelFilter::Warn
|
||||
);
|
||||
// 显式要 debug 时全放行(诊断用)。
|
||||
assert_eq!(
|
||||
target_level(LevelFilter::Debug, "wgpu_hal::vulkan::instance"),
|
||||
LevelFilter::Debug
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user