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` 的「真机验收」一节。
99 lines
4.3 KiB
Rust
99 lines
4.3 KiB
Rust
//! 入口。所有实际逻辑在 lib(`unlucky_app_iced`)里:
|
||
//!
|
||
//! 一个二进制两种用法(M82),分发在进 UI 之前完成:
|
||
//!
|
||
//! - 被子命令命中(`instances` / `install` / `launch` / `accounts` / `sessions` / `logs` /
|
||
//! `diag` / `config` / `java` / `agent`,或 `--help` / `--version`)→ 直接转给
|
||
//! `unlucky_cli::run`,**不初始化任何 UI**(不 probe 后端、不扫字体、不碰图形库)。
|
||
//! 子命令表不在这里抄一份:唯一真相源是 `crates/cli` 的解析器
|
||
//! (`unlucky_cli::cli::looks_like_cli`,本文件只调用)。
|
||
//! - 其余参数(`--software-renderer` / `--renderer` / `--theme` / `--screenshot*` /
|
||
//! `--diff` …):开窗跑骨架,`--screenshot` 出图后退出;`--diff <a> <b>` 只读两张 PNG
|
||
//! 做逐像素对比(D30 的 GPU/CPU 几何一致性检查),不开窗。
|
||
//!
|
||
//! 零 UI 依赖的 `ull` 仍然保留(D18):它是唯一连 fontconfig 都不需要的产物,
|
||
//! 给服务器 / CI / Termux 这类没有图形库的机器用。
|
||
//!
|
||
//! 退出码:`0` 成功 / `1` 截图有差异(diff 模式)或 CLI 用户错误 / `2` 参数或运行错误。
|
||
|
||
use std::process::ExitCode;
|
||
|
||
use unlucky_app_iced::shot;
|
||
use unlucky_app_iced::Options;
|
||
|
||
fn main() -> ExitCode {
|
||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||
if let Some(code) = run_cli_if_requested(&args) {
|
||
return code;
|
||
}
|
||
|
||
// M97:把 iced_wgpu / wgpu 的 `log` 输出接到 stderr。`Selected: AdapterInfo{…}` 那行
|
||
// 是「GPU 真生效」的实证——没有它,日志只能证明我们**请求过** GPU。
|
||
// CLI 子命令在上面就返回了;`--diff` 虽然也装,但它不碰 wgpu,不会多出任何一行。
|
||
unlucky_app_iced::renderer::log_bridge::install();
|
||
|
||
match run_gui() {
|
||
Ok(code) => code,
|
||
Err(message) => {
|
||
eprintln!("[cli] {message}");
|
||
eprintln!(
|
||
"用法:unlucky-launcher-iced [--renderer=auto|gpu|cpu] [--software-renderer] \
|
||
[--theme=light|dark] [--screenshot <png>] [--screenshot-theme=light|dark] \
|
||
[--scale-factor <f32>] [--diff <a.png> <b.png>]"
|
||
);
|
||
ExitCode::from(2)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 子命令命中 CLI 就跑 CLI 并以它的退出码收尾;返回 `None` 表示「这不是 CLI 调用,去开窗」。
|
||
///
|
||
/// 这条路径上**不构造窗口、不探后端、不扫字体、不依赖任何图形库**(这些库缺失时同一条命令照样
|
||
/// 跑完,实测见 packaging/README.md §7.4)。`unlucky-launcher-iced launch …` 的耗时因此与首帧无关——
|
||
/// 这句是结构性判断,**没有重量**(原因见 §10-8)。
|
||
///
|
||
/// 「这条命令行算不算 CLI 调用」的判据不在这里重复:`unlucky_cli::cli::looks_like_cli` 是唯一
|
||
/// 一处(M82 复审去重),两个 GUI 二进制都只调它,改语义不会漂。
|
||
fn run_cli_if_requested(args: &[String]) -> Option<ExitCode> {
|
||
if !unlucky_cli::cli::looks_like_cli(args) {
|
||
return None;
|
||
}
|
||
let mut stdout = std::io::stdout();
|
||
let mut stderr = std::io::stderr();
|
||
let code = unlucky_cli::run(args, &mut stdout, &mut stderr);
|
||
let _ = std::io::Write::flush(&mut stdout);
|
||
let _ = std::io::Write::flush(&mut stderr);
|
||
Some(ExitCode::from(u8::try_from(code).unwrap_or(2)))
|
||
}
|
||
|
||
/// 开窗路径:解析 GUI 参数 → (`--diff` 走比对)→ 起 UI。
|
||
fn run_gui() -> Result<ExitCode, String> {
|
||
let options = Options::from_env()?;
|
||
|
||
if let Some((left, right)) = options.diff.clone() {
|
||
let scan = i32::try_from(options.diff_scan).unwrap_or(i32::MAX);
|
||
return match shot::diff_files_with_scan(&left, &right, scan) {
|
||
Ok(diff) => {
|
||
eprintln!("{}", diff.describe());
|
||
if diff.is_identical() {
|
||
Ok(ExitCode::SUCCESS)
|
||
} else {
|
||
Ok(ExitCode::from(1))
|
||
}
|
||
}
|
||
Err(message) => {
|
||
eprintln!("[diff] {message}");
|
||
Ok(ExitCode::from(2))
|
||
}
|
||
};
|
||
}
|
||
|
||
match unlucky_app_iced::run(options) {
|
||
Ok(()) => Ok(ExitCode::SUCCESS),
|
||
Err(error) => {
|
||
eprintln!("[run] {error}");
|
||
Ok(ExitCode::FAILURE)
|
||
}
|
||
}
|
||
}
|