Files
Unlucky-Launcher/crates/app-iced/src/renderer/log_bridge.rs
T
AskaEth 31cdfd5bbd 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` 的「真机验收」一节。
2026-11-03 00:26:55 +08:00

152 lines
5.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 把 `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
);
}
}