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
@@ -30,6 +30,7 @@ use unlucky_design::color::SchemeFlavor;
|
||||
use crate::cli::Backend;
|
||||
use crate::fonts;
|
||||
use crate::probe::{self, Probe};
|
||||
use crate::renderer;
|
||||
use crate::shot;
|
||||
use crate::theme::Ui;
|
||||
|
||||
@@ -195,8 +196,15 @@ fn flavor(raw: &str) -> Result<SchemeFlavor, String> {
|
||||
|
||||
/// 启动(阻塞到出图或关窗)。
|
||||
///
|
||||
/// ## 取证通道的硬规则(M97)
|
||||
///
|
||||
/// **宁可红着退出,也不能悄悄换一种后端继续出图。** 以前这条路径在 wgpu 挂掉时会静默降级到
|
||||
/// tiny-skia,然后照样落一张 PNG,而报告上写的是「GPU 渲染截图」——所有视觉结论的可信度都被
|
||||
/// 悄悄打折,输出上看不出来。现在:① 决策被 [`renderer::strict`] 剥掉兜底;② 起不来就带着
|
||||
/// **具名**错误返回;③ 每张图的日志里都带 `backend=`,图能自证来源。
|
||||
///
|
||||
/// # Errors
|
||||
/// 后端探测/字体求值失败、截图落盘失败时返回人话。
|
||||
/// 后端探测/字体求值失败、**渲染后端起不来(含 iced 的具名图形错误)**、截图落盘失败时返回人话。
|
||||
pub fn run(request: Request) -> Result<(), String> {
|
||||
let started = Instant::now();
|
||||
|
||||
@@ -206,41 +214,50 @@ pub fn run(request: Request) -> Result<(), String> {
|
||||
} else {
|
||||
Probe::default()
|
||||
};
|
||||
probe::apply(request.backend, &probe);
|
||||
// 剥掉兜底:gallery 没有「偷偷换 CPU」这个选项。
|
||||
let decision = renderer::strict(probe::apply(request.backend, &probe));
|
||||
eprintln!(
|
||||
"[shot] 出图后端 = {} reason={}(取证通道无回退:起不来就红着退出,绝不换 CPU 出图)",
|
||||
decision.primary.name(),
|
||||
decision.reason
|
||||
);
|
||||
|
||||
let resolved = fonts::resolve();
|
||||
eprintln!("{}", resolved.describe());
|
||||
let families = resolved.families();
|
||||
|
||||
let boot = request.clone();
|
||||
let application = iced::application(
|
||||
move || {
|
||||
(
|
||||
Harness {
|
||||
ui: Ui::new(boot.flavor, families),
|
||||
model: demo_model(boot.screen, boot.width),
|
||||
request: boot.clone(),
|
||||
window: None,
|
||||
frames: 0,
|
||||
},
|
||||
Task::done(Message::Boot),
|
||||
)
|
||||
},
|
||||
Harness::update,
|
||||
Harness::view,
|
||||
)
|
||||
.theme(Harness::theme)
|
||||
.subscription(Harness::subscription)
|
||||
.default_font(iced::Font {
|
||||
family: iced::font::Family::Name(families.text),
|
||||
..iced::Font::DEFAULT
|
||||
})
|
||||
.window(window::Settings {
|
||||
size: iced::Size::new(request.width, request.height),
|
||||
decorations: false,
|
||||
resizable: true,
|
||||
..window::Settings::default()
|
||||
});
|
||||
let application = |backend: renderer::Kind| {
|
||||
let boot = request.clone();
|
||||
iced::application(
|
||||
move || {
|
||||
(
|
||||
Harness {
|
||||
ui: Ui::new(boot.flavor, families),
|
||||
model: demo_model(boot.screen, boot.width),
|
||||
request: boot.clone(),
|
||||
window: None,
|
||||
frames: 0,
|
||||
backend: backend.name(),
|
||||
},
|
||||
Task::done(Message::Boot),
|
||||
)
|
||||
},
|
||||
Harness::update,
|
||||
Harness::view,
|
||||
)
|
||||
.theme(Harness::theme)
|
||||
.subscription(Harness::subscription)
|
||||
.default_font(iced::Font {
|
||||
family: iced::font::Family::Name(families.text),
|
||||
..iced::Font::DEFAULT
|
||||
})
|
||||
.window(window::Settings {
|
||||
size: iced::Size::new(request.width, request.height),
|
||||
decorations: false,
|
||||
resizable: true,
|
||||
..window::Settings::default()
|
||||
})
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"[harness] screen={} width={} height={} slug={}(布局档位 {})",
|
||||
@@ -251,11 +268,23 @@ pub fn run(request: Request) -> Result<(), String> {
|
||||
super::layout::WidthClass::of(request.width).name()
|
||||
);
|
||||
|
||||
match request.scale_factor {
|
||||
Some(scale) => application.scale_factor(move |_| scale).run(),
|
||||
None => application.run(),
|
||||
}
|
||||
.map_err(|error| format!("iced 启动失败:{error}"))?;
|
||||
// 兜底已被 `strict` 剥掉,所以这条最多跑一次;具名错误的口径与 App 启动一致,
|
||||
// 但**取证通道不许降级**:出不了图就红着退出,不换后端(那是另一处 iced 行为)。
|
||||
renderer::run_once(decision, |backend| {
|
||||
std::env::set_var("ICED_BACKEND", backend.env_value());
|
||||
let application = application(backend);
|
||||
match request.scale_factor {
|
||||
Some(scale) => application.scale_factor(move |_| scale).run(),
|
||||
None => application.run(),
|
||||
}
|
||||
})
|
||||
.map_err(|error| match renderer::named_graphics_error(&error) {
|
||||
Some(named) => format!(
|
||||
"渲染后端 {} 初始化失败(取证通道无回退,未出图):{named}",
|
||||
decision.primary.name()
|
||||
),
|
||||
None => format!("iced 启动失败:{error}"),
|
||||
})?;
|
||||
|
||||
eprintln!(
|
||||
"[harness] 用时 {:.0} ms",
|
||||
@@ -271,6 +300,8 @@ struct Harness {
|
||||
model: Model,
|
||||
window: Option<window::Id>,
|
||||
frames: u32,
|
||||
/// 出这张图的后端(`wgpu` / `tiny-skia`)。图要能**自证来源**——以前它证不了(M97)。
|
||||
backend: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -308,7 +339,11 @@ impl Harness {
|
||||
match shot::save(&screenshot, &path) {
|
||||
Ok(summary) => {
|
||||
eprintln!("{}", summary.describe());
|
||||
eprintln!("[harness] slug={}", self.request.slug());
|
||||
eprintln!(
|
||||
"[harness] slug={} backend={}(这张图的来源,自证用)",
|
||||
self.request.slug(),
|
||||
self.backend
|
||||
);
|
||||
}
|
||||
Err(error) => eprintln!("[harness] 截图失败:{error}"),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user