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
Generated
+1
@@ -6921,6 +6921,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"iced",
|
||||
"image",
|
||||
"log",
|
||||
"unlucky-cli",
|
||||
"unlucky-design",
|
||||
"unlucky-i18n",
|
||||
|
||||
@@ -63,6 +63,12 @@ unlucky-cli = { path = "../cli", version = "0.1" }
|
||||
# 所以系统字体扫描留在这里(`src/fonts/cjk.rs`),本 crate 只把族名喂给它的纯函数。
|
||||
unlucky-i18n = { path = "../i18n", version = "0.1" }
|
||||
|
||||
# M97:判据表第 8 行的取证通道。`iced_wgpu` 在真创建设备时会 `log::info!("Selected: {adapter:#?}")`,
|
||||
# 但进程里从来没人装 logger,于是「看不见」被当成了「没发生」。这里装一个只放行 `iced_wgpu*` /
|
||||
# `wgpu*` 的 logger(见 `src/renderer/log_bridge.rs`),把这行捞出来。
|
||||
# 已是 iced 传递依赖树里的版本(0.4.34),不新增任何传递依赖;MIT OR Apache-2.0,许可白名单内。
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
|
||||
# X11 / Wayland 后端与 Linux 主题检测只在 Linux 上启用:macOS 没有 X11/Wayland(走 Metal +
|
||||
# 系统外观),Windows 走 DX12,这三个 feature 在那边没有意义。同一个包在两处写 features 是
|
||||
# **取并集**(Cargo 的 feature 解析是加法),所以 Linux 上拿到的仍是上面 5 个 + 这 3 个,
|
||||
|
||||
@@ -7,38 +7,50 @@
|
||||
//! 请求是 `Auto`,实际是 tiny-skia(CPU)——如果面板 α / 玻璃的降级判据只看请求值,
|
||||
//! D36 明写的「CPU 档面板 α 强制 1.0」**恰好在它最该生效的场景失效**(复审 r1 P2-2)。
|
||||
//!
|
||||
//! 所以判据统一走 [`effective_backend`]:它把 `ICED_BACKEND` 的候选列表翻译回 [`Backend`]。
|
||||
//! 所以判据统一走 [`effective_backend`]:它把 `ICED_BACKEND` 翻译回 [`Backend`]。
|
||||
//! 这里是**唯一**的翻译点,别在别处再读一遍环境变量。
|
||||
//!
|
||||
//! ## M97 之后这条链是准的(以前不是)
|
||||
//!
|
||||
//! 以前 `ICED_BACKEND` 可能是**候选列表**(`wgpu,tiny-skia`),运行期 wgpu 静默回退到
|
||||
//! tiny-skia 时这个变量还停在 wgpu ⇒ 本模块按 **GPU 档**判 ⇒ **半透明在 CPU 渲染器上照开**。
|
||||
//! 也就是说「静默降级」不只让日志说谎,还让 D36 的降级判据一起说谎。
|
||||
//!
|
||||
//! 现在 `ICED_BACKEND` **永远是单项**([`crate::renderer::Kind`]),而且 `app::boot` 的重试
|
||||
//! 在降级重开前会把它改成 `tiny-skia`,于是:**真降级到 CPU 的那一次,面板 α 会正确地强制 1.0**。
|
||||
//!
|
||||
//! ## 已知边界(诚实登记)
|
||||
//!
|
||||
//! `ICED_BACKEND="wgpu,tiny-skia"` 表示「优先 wgpu,拿不到适配器再回退 tiny-skia」。运行期到底
|
||||
//! 选了谁只有渲染器自己知道,本 crate 拿不到(iced 0.14 没有公开的「当前后端」查询)。
|
||||
//! 这种机器按 **GPU 档**判(面板 α 生效);如果 wgpu 在运行期回退到 tiny-skia,面板 α 会
|
||||
//! 在 CPU 渲染器上生效——成本比 D27 的顾虑略高,但不会画错。要彻底收口得让 iced 暴露实际后端。
|
||||
//! `iced` 0.14 没有公开的「当前后端」查询,所以本模块读的仍是**我们请求的那一项**。
|
||||
//! 单项 + 重试([`crate::renderer`] 判据表第 8 行)把「请求 ≠ 实际」的窗口压到只剩一种:
|
||||
//! 重试那次自己也失败——那时候进程会带着具名错误退出,不会带着错的档位继续画。
|
||||
|
||||
use crate::renderer::Kind;
|
||||
use crate::Backend;
|
||||
|
||||
/// `ICED_BACKEND` 的候选列表 → 实际档位(纯函数,好测)。
|
||||
/// `ICED_BACKEND` 的取值 → 后端(纯函数,好测)。
|
||||
///
|
||||
/// | `ICED_BACKEND` | 含义(`probe::backend_env` 的产物) | 结论 |
|
||||
/// 逗号列表按**首项**解释:兼容 M97 之前写下的值(以及外部手设的实验值),
|
||||
/// 但本 crate 不再产出那种值(守门测试见 `probe::tests::apply_writes_exactly_one_backend`)。
|
||||
#[must_use]
|
||||
pub(super) fn parse_kind(env: &str) -> Option<Kind> {
|
||||
match env.trim().split(',').next()?.trim() {
|
||||
"tiny-skia" => Some(Kind::TinySkia),
|
||||
"wgpu" => Some(Kind::Wgpu),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `ICED_BACKEND` → 实际档位(纯函数,好测)。
|
||||
///
|
||||
/// | `ICED_BACKEND` | 含义(`probe::apply` 的产物) | 结论 |
|
||||
/// |---|---|---|
|
||||
/// | `tiny-skia` | 只有 CPU 候选(`--software-renderer` / 未编 wgpu / Auto 无线程硬件) | [`Backend::Cpu`] |
|
||||
/// | `wgpu,tiny-skia` | 点名要 GPU(保留回退) | [`Backend::Gpu`] |
|
||||
/// | `tiny-skia,wgpu` | Auto + 有 wgpu 无硬件(CPU 起步、wgpu 兜底) | [`Backend::Cpu`] |
|
||||
/// | 未设 | Auto + 有硬件(交给 iced 默认顺序) | 请求值(`Auto`) |
|
||||
/// | `tiny-skia` | 只有 CPU(`--renderer=cpu` / 未编 wgpu / 探测无硬件 / wgpu 失败后重开) | [`Backend::Cpu`] |
|
||||
/// | `wgpu` | 判据表第 3、6 行:起步就是 GPU(起不来会打印具名错误并重开成 `tiny-skia`) | [`Backend::Gpu`] |
|
||||
/// | 未设 / 别的值 | 不是本 crate 写的(外部手设 / M97 之前的旧进程) | 请求值(不猜) |
|
||||
#[must_use]
|
||||
pub(super) fn backend_from_env(request: Backend, env: Option<&str>) -> Backend {
|
||||
let Some(list) = env.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return request;
|
||||
};
|
||||
// 候选列表的第一项就是起步渲染器:`tiny-skia` 起步 = CPU 档(D36 的降级对象)。
|
||||
let first = list.split(',').next().unwrap_or(list).trim();
|
||||
match first {
|
||||
"tiny-skia" => Backend::Cpu,
|
||||
"wgpu" => Backend::Gpu,
|
||||
_ => request,
|
||||
}
|
||||
env.and_then(parse_kind).map_or(request, Kind::tier)
|
||||
}
|
||||
|
||||
/// 本进程实际生效的后端(判据 = `probe::apply` 写下的 `ICED_BACKEND`)。
|
||||
@@ -47,25 +59,49 @@ pub(super) fn effective_backend(request: Backend) -> Backend {
|
||||
backend_from_env(request, std::env::var("ICED_BACKEND").ok().as_deref())
|
||||
}
|
||||
|
||||
/// 本进程实际生效的**后端名**(`None` = 变量没写 / 不认识)。
|
||||
///
|
||||
/// 与 [`effective_backend`] 的区别:那个给「档位」,这个给「名字」,用于**取证日志**
|
||||
/// (`[shot] … renderer=wgpu`)。两张图分辨不出 GPU/CPU 的时候,靠的就是这一行。
|
||||
#[must_use]
|
||||
pub(super) fn effective_kind() -> Option<Kind> {
|
||||
std::env::var("ICED_BACKEND")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.and_then(parse_kind)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::backend_from_env;
|
||||
use crate::Backend;
|
||||
use unlucky_presenter::prefs::{PANEL_ALPHA_MIN, WINDOW_ALPHA_MIN};
|
||||
|
||||
/// 抓得住:`probe::backend_env` 的每一种产物都被翻译成正确的档位(含最容易踩的
|
||||
/// 「Auto + 没独显」= `tiny-skia,wgpu` → CPU)。
|
||||
/// 抓不住:运行期 wgpu 真回退到 tiny-skia 的情况(iced 没暴露实时候选,见模块文档)。
|
||||
/// 抓得住:`probe::apply` 现在产出的**每一种单项值**都被翻译成正确的档位,
|
||||
/// 且 M97 之前写下的逗号列表仍按首项解释(向后兼容)。
|
||||
/// 抓不住:运行期重试降级那一刻的时序(那由 `app::boot` 的重试保证,见 `renderer` 判据表第 8 行)。
|
||||
#[test]
|
||||
fn every_iced_backend_list_maps_to_a_tier() {
|
||||
fn every_iced_backend_value_maps_to_a_tier() {
|
||||
// 本 crate 现在只产单项(`probe::tests::apply_writes_exactly_one_backend` 守这一条)。
|
||||
assert_eq!(
|
||||
backend_from_env(Backend::Auto, Some("tiny-skia")),
|
||||
Backend::Cpu
|
||||
Backend::Cpu,
|
||||
"M97 判据表第 5 行:探测无硬件 ⇒ CPU 档"
|
||||
);
|
||||
assert_eq!(
|
||||
backend_from_env(Backend::Auto, Some("tiny-skia,wgpu")),
|
||||
backend_from_env(Backend::Auto, Some("wgpu")),
|
||||
Backend::Gpu,
|
||||
"M97 判据表第 6 行:有硬件 ⇒ GPU 档"
|
||||
);
|
||||
assert_eq!(
|
||||
backend_from_env(Backend::Cpu, Some("tiny-skia")),
|
||||
Backend::Cpu,
|
||||
"Auto + 无硬件:CPU 起步 ⇒ CPU 档"
|
||||
"强制 CPU 也是 CPU 档"
|
||||
);
|
||||
// 兼容 M97 之前的逗号列表(外部手设 / 旧进程留下):按首项解释。
|
||||
assert_eq!(
|
||||
backend_from_env(Backend::Auto, Some("tiny-skia,wgpu")),
|
||||
Backend::Cpu
|
||||
);
|
||||
assert_eq!(
|
||||
backend_from_env(Backend::Auto, Some("wgpu,tiny-skia")),
|
||||
@@ -75,7 +111,7 @@ mod tests {
|
||||
backend_from_env(Backend::Gpu, Some("wgpu,tiny-skia")),
|
||||
Backend::Gpu
|
||||
);
|
||||
// 未设 / 空白:保持请求值(Auto + 有硬件 ⇒ 交给 iced,按 GPU 档判)。
|
||||
// 未设 / 空白:保持请求值(不是本 crate 写的,不猜)。
|
||||
assert_eq!(backend_from_env(Backend::Auto, None), Backend::Auto);
|
||||
assert_eq!(backend_from_env(Backend::Auto, Some(" ")), Backend::Auto);
|
||||
// 不认识的取值不猜:保留请求值。
|
||||
@@ -94,7 +130,7 @@ mod tests {
|
||||
fn auto_with_a_software_renderer_forces_the_panel_opaque() {
|
||||
use crate::pages::appearance::alpha::{effective_glass, effective_panel_alpha};
|
||||
|
||||
let backend = backend_from_env(Backend::Auto, Some("tiny-skia,wgpu"));
|
||||
let backend = backend_from_env(Backend::Auto, Some("tiny-skia"));
|
||||
assert_eq!(backend, Backend::Cpu);
|
||||
assert_eq!(
|
||||
effective_panel_alpha(backend, 0.55),
|
||||
@@ -106,8 +142,30 @@ mod tests {
|
||||
assert!(crate::platform::effective_window_alpha(backend, 0.8).is_some());
|
||||
|
||||
// 反方向:GPU 档不做这个降级(否则「强制」就没意义了)。
|
||||
let gpu = backend_from_env(Backend::Auto, Some("wgpu,tiny-skia"));
|
||||
let gpu = backend_from_env(Backend::Auto, Some("wgpu"));
|
||||
assert_eq!(effective_panel_alpha(gpu, 0.55), 0.55);
|
||||
assert!((PANEL_ALPHA_MIN - 0.4).abs() < 1e-9 && (WINDOW_ALPHA_MIN - 0.08).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// **M97 的新守门**:wgpu 失败重开成 `tiny-skia` 之后,档位必须跟着变成 CPU 档。
|
||||
///
|
||||
/// 这是修之前做不到的一条:那时变量停在 `wgpu,tiny-skia`,静默回退到 tiny-skia 之后
|
||||
/// 判据仍然读 `Gpu` ⇒ 半透明在 CPU 渲染器上照开。现在 `app::boot` 重开前会改写这个值,
|
||||
/// 这条测试钉的是「改写成 tiny-skia 之后判据确实翻转」。
|
||||
#[test]
|
||||
fn retry_fallback_flips_the_tier_to_cpu() {
|
||||
use crate::pages::appearance::alpha::effective_panel_alpha;
|
||||
|
||||
let before = backend_from_env(Backend::Auto, Some("wgpu"));
|
||||
assert_eq!(before, Backend::Gpu);
|
||||
assert_eq!(effective_panel_alpha(before, 0.55), 0.55);
|
||||
|
||||
let after = backend_from_env(Backend::Auto, Some("tiny-skia"));
|
||||
assert_eq!(after, Backend::Cpu);
|
||||
assert_eq!(
|
||||
effective_panel_alpha(after, 0.55),
|
||||
1.0,
|
||||
"D36:真降级到 CPU 后,面板 α 必须强制 1.0"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
//! 启动:后端探测 → 字体求值 → `iced::application(..).run()`。
|
||||
//!
|
||||
//! ## M97:`run()` 为什么要重试
|
||||
//!
|
||||
//! `ICED_BACKEND` 现在永远是**单项**(见 [`crate::renderer`] 的判据表)。单项的代价是
|
||||
//! iced 不再自己做回退,于是「wgpu 起不来」会变成 `iced::Error::GraphicsCreationFailed`
|
||||
//! ——**这正是我们要的**:那里面装着 wgpu 的具名错误(哪个 backend、卡在 adapter 还是 device)。
|
||||
//! 拿到它以后打印、然后换 `tiny-skia` 重开一次,安全网照样在,但降级不再是静默的。
|
||||
//!
|
||||
//! 重试只在起步项真的失败时发生,而且**只重试一次**;字体栈不重算(那是启动最贵的一段)。
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -9,6 +18,7 @@ use unlucky_design::color::SchemeFlavor;
|
||||
use crate::cli::Options;
|
||||
use crate::fonts;
|
||||
use crate::probe;
|
||||
use crate::renderer::{self, Decision, Kind};
|
||||
use crate::theme::Families;
|
||||
|
||||
use super::message::Message;
|
||||
@@ -19,7 +29,7 @@ use super::{MIN_SIZE, WINDOW_SIZE};
|
||||
pub(crate) fn run(options: Options) -> iced::Result {
|
||||
let _ = STARTED.set(Instant::now());
|
||||
|
||||
boot_probe(&options);
|
||||
let decision = boot_probe(&options);
|
||||
|
||||
let font_started = Instant::now();
|
||||
let resolved = fonts::resolve();
|
||||
@@ -41,6 +51,92 @@ pub(crate) fn run(options: Options) -> iced::Result {
|
||||
eprintln!("[shot] 未指定主题,按 Light 出图(D30 可复现;CI 冒烟依赖这条默认值)");
|
||||
}
|
||||
let flavor = options.capture_flavor().unwrap_or(SchemeFlavor::Light);
|
||||
|
||||
// 起步后端起不来时**换一个进程**重开(winit 不允许在本进程重建事件循环,见
|
||||
// `renderer::run_once` 的文档)。具名原因在换进程之前就打到同一个 stderr 上了。
|
||||
match launch(&options, families, flavor, decision.primary) {
|
||||
Err(iced::Error::GraphicsCreationFailed(error)) => {
|
||||
renderer::report_failure(decision.primary, &error);
|
||||
match decision.fallback {
|
||||
// 交互启动:安全网优先,降级重开,用户还能用。
|
||||
Some(fallback) if options.screenshot.is_none() => {
|
||||
respawn(decision.primary, fallback)
|
||||
}
|
||||
// 取证启动(`--screenshot`):**拒绝降级出图**。图是要拿去下结论的,
|
||||
// 换了后端出的图会撒谎——以前就是这么把 CPU 截图当 GPU 结论用的。
|
||||
Some(_) => {
|
||||
eprintln!(
|
||||
"[gpu] 拒绝降级出图:本次是取证启动(--screenshot {}),换后端出的图会撒谎。",
|
||||
options
|
||||
.screenshot
|
||||
.as_ref()
|
||||
.map_or_else(String::new, |path| path.display().to_string())
|
||||
);
|
||||
eprintln!(
|
||||
"[gpu] 想用 CPU 出图请显式加 --renderer=cpu / --software-renderer(CI 就是这么跑的)。"
|
||||
);
|
||||
Err(iced::Error::GraphicsCreationFailed(error))
|
||||
}
|
||||
None => Err(iced::Error::GraphicsCreationFailed(error)),
|
||||
}
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全网:起一个**新进程**用 `fallback` 后端重开,然后带着它的退出码退出。
|
||||
///
|
||||
/// 为什么必须是新进程:winit 一个进程只能建一次事件循环
|
||||
/// (`iced_winit-0.14.1/src/lib.rs:81` → `Create event loop: RecreationAttempt`)。
|
||||
///
|
||||
/// 只在**交互启动**时走到这里;取证启动(`--screenshot`)宁可红着退出也不换后端出图
|
||||
/// (判据表第 8 行 vs 第 9 行——**这两条路的策略不同,别合成一条**)。
|
||||
///
|
||||
/// ## `ULL_GPU_FALLBACK` 是防自杀循环的唯一闸门
|
||||
///
|
||||
/// 子进程看到 [`renderer::FALLBACK_ENV`] **必须跳过探测**(`boot_probe` 的第一段),换成
|
||||
/// `tiny-skia` 直接跑;否则子进程会算出和父进程一样的结论(auto + 有硬件 → wgpu)、再失败、
|
||||
/// 再 respawn —— **无限递归重开**。这种循环极难复现也极难诊断(每次重开都像是「刚刚启动」),
|
||||
/// 所以闸门放在探测入口的第一个分支,不放在 respawn 那一侧。
|
||||
fn respawn(failed: Kind, fallback: Kind) -> ! {
|
||||
let exe = std::env::current_exe().unwrap_or_else(|err| {
|
||||
eprintln!("[gpu] 降级重开失败:拿不到自身路径({err})");
|
||||
std::process::exit(2);
|
||||
});
|
||||
eprintln!(
|
||||
"[gpu] 降级 → {}({} 已失败,原因见上两行;换一个进程重开:winit 不允许同进程建两次事件循环)。\
|
||||
下一次的日志从 `[probe] 本次是降级重开` 开始。",
|
||||
fallback.name(),
|
||||
failed.name()
|
||||
);
|
||||
|
||||
let status = std::process::Command::new(exe)
|
||||
.args(std::env::args().skip(1))
|
||||
.env(renderer::FALLBACK_ENV, fallback.env_value())
|
||||
.env("ICED_BACKEND", fallback.env_value())
|
||||
.status();
|
||||
|
||||
// 退出码**透传**子进程的:上层脚本/CI 靠它判断这次到底是成功还是失败。
|
||||
// 子进程被信号杀掉时 `code()` 是 `None`(Unix 才有),那种情况退回 2,别假装成功。
|
||||
let code = match status {
|
||||
Ok(status) => status.code().unwrap_or(2),
|
||||
Err(err) => {
|
||||
eprintln!("[gpu] 降级重开失败:{err}");
|
||||
2
|
||||
}
|
||||
};
|
||||
std::process::exit(code)
|
||||
}
|
||||
|
||||
/// 按一次后端启动 iced。`backend` 决定这次写进 `ICED_BACKEND` 的唯一一项。
|
||||
fn launch(
|
||||
options: &Options,
|
||||
families: Families,
|
||||
flavor: SchemeFlavor,
|
||||
backend: Kind,
|
||||
) -> iced::Result {
|
||||
std::env::set_var("ICED_BACKEND", backend.env_value());
|
||||
|
||||
let boot_options = options.clone();
|
||||
|
||||
let application = iced::application(
|
||||
@@ -80,8 +176,26 @@ pub(crate) fn run(options: Options) -> iced::Result {
|
||||
}
|
||||
}
|
||||
|
||||
/// 渲染后端探测 + 写 `ICED_BACKEND`。
|
||||
fn boot_probe(options: &Options) {
|
||||
/// 渲染后端探测 + 写 `ICED_BACKEND`,返回判据产物(供 `run` 决定要不要换进程重开)。
|
||||
fn boot_probe(options: &Options) -> Decision {
|
||||
// 本进程是「上一步失败后重开」的那一次:别再探一次(会得到同一个结论,然后无限重开)。
|
||||
if let Some(kind) = renderer::forced_fallback() {
|
||||
std::env::set_var("ICED_BACKEND", kind.env_value());
|
||||
let decision = Decision {
|
||||
primary: kind,
|
||||
fallback: None,
|
||||
reason: "fallback-after-failure",
|
||||
};
|
||||
eprintln!(
|
||||
"[probe] 本次是降级重开({}={}):跳过探测 -> ICED_BACKEND={}",
|
||||
renderer::FALLBACK_ENV,
|
||||
kind.env_value(),
|
||||
kind.env_value()
|
||||
);
|
||||
eprintln!("[probe] {}", decision.describe());
|
||||
return decision;
|
||||
}
|
||||
|
||||
let probe_started = Instant::now();
|
||||
// 只有 `auto` 需要探测:`cpu` 与 `gpu` 的决策与「有没有硬件 GPU」无关,
|
||||
// 而枚举 13 个适配器在本机要 0.65~1.2 s(实测),纯属白花。
|
||||
@@ -92,7 +206,11 @@ fn boot_probe(options: &Options) {
|
||||
probe::Probe::default()
|
||||
};
|
||||
let probe_ms = probe_started.elapsed().as_secs_f64() * 1000.0;
|
||||
probe::apply(options.backend, &probe);
|
||||
|
||||
// `ULL_PREFLIGHT=1` 时先自己验一遍 wgpu(判据表第 7 行);默认不跑,见 preflight 模块文档。
|
||||
let preflight = renderer::preflight::run_if_requested();
|
||||
let decision = probe::apply_with_preflight(options.backend, &probe, preflight.as_ref());
|
||||
|
||||
if cfg!(feature = "gpu") {
|
||||
if needs_probe {
|
||||
eprintln!("[probe] 探测耗时 {probe_ms:.0} ms(wgpu 实例 + 全后端适配器枚举)");
|
||||
@@ -103,6 +221,7 @@ fn boot_probe(options: &Options) {
|
||||
);
|
||||
}
|
||||
}
|
||||
decision
|
||||
}
|
||||
|
||||
/// 默认字体(正文档解析出来的族)。
|
||||
|
||||
@@ -36,6 +36,7 @@ use unlucky_presenter::router::Route;
|
||||
use unlucky_presenter::theme::CONFIG_POLL_TICKS;
|
||||
|
||||
use crate::pages;
|
||||
use crate::renderer::Kind;
|
||||
|
||||
use super::message::{route_of, Message};
|
||||
use super::state::{App, IDLE_TICK, TICK, TICK_MS, WATCH_WINDOW};
|
||||
@@ -326,11 +327,12 @@ impl App {
|
||||
match crate::shot::save(screenshot, &path) {
|
||||
Ok(summary) => {
|
||||
eprintln!("{}", summary.describe());
|
||||
// M97:打**实际**后端名(`wgpu`/`tiny-skia`),不是请求值——`renderer=auto` 曾经出现在每张验收图的日志里。
|
||||
eprintln!(
|
||||
"[shot] screen={} theme={} renderer={} route={:?}",
|
||||
self.options.screen.name(),
|
||||
crate::shot::flavor_slug(&self.ui),
|
||||
self.options.backend.note(),
|
||||
super::backend::effective_kind().map_or("?", Kind::name),
|
||||
self.state.route()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
//! | `cli` | `--renderer` / `--software-renderer` / `--theme` / `--screenshot*` 参数 | 业务开关 |
|
||||
//! | `probe` | 渲染后端探测(D27)+ 写 `ICED_BACKEND`,失败自动降级 CPU | 绘制 |
|
||||
//! | `fonts` | 字体栈求值(D28 前半):平台族名 → 系统字体库校验 → 启动日志 | 中文字体下载 |
|
||||
//! | `renderer` | **渲染后端判据表**(M97)+ wgpu 预检 + `log` 桥:什么条件选 wgpu / 什么时候降级 CPU / 日志该长什么样 | 实际绘帧 |
|
||||
//! | `app` / `shell` | Elm 三件套 + 自绘标题栏 / 导航轨 / 内容骨架 | 业务逻辑 |
|
||||
//! | `shot` | `window::screenshot` → PNG(验收通道) | 图像比对 |
|
||||
//! | `icons` | 令牌里的 SVG path → iced `svg` 控件 | 画新图标 |
|
||||
@@ -27,6 +28,7 @@
|
||||
|
||||
pub mod pages;
|
||||
pub mod platform;
|
||||
pub mod renderer;
|
||||
pub mod shot;
|
||||
pub mod theme;
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ fn main() -> ExitCode {
|
||||
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) => {
|
||||
|
||||
@@ -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}"),
|
||||
}
|
||||
|
||||
@@ -8,16 +8,25 @@
|
||||
//!
|
||||
//! - 本机会枚举出 `Microsoft Basic Render Driver [Cpu]`(= WARP 软光栅),
|
||||
//! 所以「有没有适配器」不能当「有没有 GPU」(M60 实测),必须看 `device_type`。
|
||||
//! - 没有硬件 GPU 时要**主动**把 CPU 放到候选首位(`tiny-skia,wgpu`),
|
||||
//! 否则 wgpu 会先花时间尝试 WARP,观感与行为都难料。
|
||||
//! - 没有硬件 GPU 时要**主动**把 CPU 放到候选首位,否则 wgpu 会先花时间尝试 WARP,
|
||||
//! 观感与行为都难料。
|
||||
//!
|
||||
//! ## 语义订正(与 M60 备忘录的差异,以源码 + 实测为准)
|
||||
//! ## M97 订正:判据在 [`crate::renderer::decide`],日志不再说谎
|
||||
//!
|
||||
//! M60 记「`ICED_BACKEND=wgpu` 会**关闭**回退」。读 `iced_renderer-0.14.0/src/fallback.rs`
|
||||
//! 得到的是:**候选列表**语义——列表里没有 `tiny-skia` 时才真的关闭回退。
|
||||
//! 所以本 crate 的「优先 GPU」写 `wgpu,tiny-skia`(首选 GPU,拿不到仍降级,**绝不硬失败**),
|
||||
//! 而不是 `wgpu`。
|
||||
//! 本模块只负责**探测**(`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;
|
||||
|
||||
/// 单个适配器的摘要(只留日志要用的字段)。
|
||||
@@ -72,49 +81,37 @@ impl Probe {
|
||||
}
|
||||
}
|
||||
|
||||
/// 按请求与探测结果决定 `ICED_BACKEND` 的值(`None` = 不干预,交给 iced 自己回退)。
|
||||
/// 把 [`renderer::decide`] 的结论落到进程环境变量,并把**判据**打进日志(不带预检)。
|
||||
///
|
||||
/// 纯函数,好让「降级矩阵」被测试钉死。
|
||||
pub fn backend_env(request: Backend, probe: &Probe, gpu_compiled: bool) -> Option<&'static str> {
|
||||
match (request, gpu_compiled) {
|
||||
// 命令行 --software-renderer / --renderer=cpu:只留 CPU 候选。
|
||||
(Backend::Cpu, _) => Some("tiny-skia"),
|
||||
// 用户点名要 GPU,但没编进 wgpu:只能 CPU,日志里说明。
|
||||
(Backend::Gpu, false) => Some("tiny-skia"),
|
||||
// 优先 GPU,但保留 tiny-skia 兜底(候选列表语义:不会硬失败)。
|
||||
(Backend::Gpu, true) => Some("wgpu,tiny-skia"),
|
||||
// 自动:有硬件就让 iced 走默认顺序(wgpu 优先,自动回退)。
|
||||
(Backend::Auto, true) if probe.hardware => None,
|
||||
// 自动 + 没编进 wgpu:只有 CPU 候选,别把 wgpu 写进列表(会与「未编入 wgpu」的日志自相矛盾)。
|
||||
(Backend::Auto, false) => Some("tiny-skia"),
|
||||
// 自动 + 有 wgpu 但没硬件(含只有 WARP 的机器):CPU 起步,wgpu 留作兜底。
|
||||
(Backend::Auto, true) => Some("tiny-skia,wgpu"),
|
||||
}
|
||||
/// 这个入口给「没有重试能力」的调用方(组件库 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` 时才有值)。
|
||||
///
|
||||
/// 返回**实际生效**的后端描述(`"auto (iced: wgpu -> tiny-skia)"` 之类)。
|
||||
pub fn apply(request: Backend, probe: &Probe) -> String {
|
||||
/// 注意这里是「判据」不是「结果」:日志里不会出现「实际用了哪个后端」这种我们观测不到的话。
|
||||
/// 实际后端的取证有两条,都在 [`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 env = backend_env(request, probe, gpu_compiled);
|
||||
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 自身)。
|
||||
if let Some(value) = env {
|
||||
std::env::set_var("ICED_BACKEND", value);
|
||||
}
|
||||
|
||||
let effective = if gpu_compiled {
|
||||
match (request, env) {
|
||||
(Backend::Cpu, _) => "tiny-skia (CPU,强制)".to_owned(),
|
||||
(_, None) => "auto (wgpu -> tiny-skia)".to_owned(),
|
||||
(_, Some("wgpu,tiny-skia")) => "wgpu.then(tiny-skia) (优先 GPU)".to_owned(),
|
||||
(_, Some(value)) => format!("{value} (CPU 起步)"),
|
||||
}
|
||||
} else {
|
||||
"tiny-skia (本构建未编入 wgpu,只有 CPU)".to_owned()
|
||||
};
|
||||
std::env::set_var("ICED_BACKEND", decision.primary.env_value());
|
||||
|
||||
let note = if !gpu_compiled && request != Backend::Cpu {
|
||||
";本构建未编入 wgpu(--no-default-features),只能 CPU"
|
||||
@@ -126,18 +123,24 @@ pub fn apply(request: Backend, probe: &Probe) -> String {
|
||||
"[probe] {} :: request={} -> ICED_BACKEND={}{note}",
|
||||
probe.describe(),
|
||||
request.name(),
|
||||
env.unwrap_or("<unset>")
|
||||
decision.primary.env_value()
|
||||
);
|
||||
eprintln!("[probe] renderer = {effective}");
|
||||
eprintln!("[probe] {}", decision.describe());
|
||||
if decision.primary == renderer::Kind::Wgpu {
|
||||
eprintln!(
|
||||
"[probe] 取证:出现 `[gpu] iced_wgpu: …Selected: AdapterInfo…` ⇒ GPU 真生效;\
|
||||
出现 `[gpu] wgpu 初始化失败` ⇒ 已降级 CPU"
|
||||
);
|
||||
}
|
||||
|
||||
effective
|
||||
decision
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu")]
|
||||
fn run_wgpu() -> Probe {
|
||||
// `iced::wgpu` 就是 wgpu crate 本身(`pub use iced_renderer::wgpu::wgpu` 再导出),
|
||||
// 所以不需要单独依赖 wgpu,版本也不会和 iced 打架。
|
||||
use iced::wgpu::{Backends, DeviceType, Instance, InstanceDescriptor};
|
||||
use iced::wgpu::{Backends, Instance, InstanceDescriptor};
|
||||
|
||||
let instance = Instance::new(&InstanceDescriptor {
|
||||
backends: Backends::all(),
|
||||
@@ -149,16 +152,9 @@ fn run_wgpu() -> Probe {
|
||||
.into_iter()
|
||||
.map(|adapter| {
|
||||
let info = adapter.get_info();
|
||||
let device_type = match info.device_type {
|
||||
DeviceType::DiscreteGpu => "DiscreteGpu",
|
||||
DeviceType::IntegratedGpu => "IntegratedGpu",
|
||||
DeviceType::VirtualGpu => "VirtualGpu",
|
||||
DeviceType::Cpu => "Cpu",
|
||||
DeviceType::Other => "Other",
|
||||
};
|
||||
AdapterSummary {
|
||||
name: info.name,
|
||||
device_type,
|
||||
device_type: renderer::preflight::device_type_name(info.device_type),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -195,39 +191,12 @@ mod tests {
|
||||
// 本机实测会枚举出 WARP:`Microsoft Basic Render Driver [Cpu]`
|
||||
let warp = probe(&["Cpu"]);
|
||||
assert!(!warp.hardware, "只有 WARP 不能当「有 GPU」");
|
||||
assert_eq!(
|
||||
backend_env(Backend::Auto, &warp, true),
|
||||
Some("tiny-skia,wgpu")
|
||||
assert_ne!(
|
||||
renderer::decide(Backend::Auto, &warp, true).primary,
|
||||
renderer::Kind::Wgpu
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_with_real_gpu_leaves_iced_alone() {
|
||||
let gpu = probe(&["DiscreteGpu", "Cpu"]);
|
||||
assert!(gpu.hardware);
|
||||
assert_eq!(backend_env(Backend::Auto, &gpu, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_choices_always_keep_a_fallback() {
|
||||
let gpu = probe(&["DiscreteGpu"]);
|
||||
let none = probe(&[]);
|
||||
// 优先 GPU 但候选列表里留 tiny-skia:拿不到适配器时降级而不是报错
|
||||
assert_eq!(
|
||||
backend_env(Backend::Gpu, &gpu, true),
|
||||
Some("wgpu,tiny-skia")
|
||||
);
|
||||
assert_eq!(
|
||||
backend_env(Backend::Gpu, &none, true),
|
||||
Some("wgpu,tiny-skia")
|
||||
);
|
||||
assert_eq!(backend_env(Backend::Cpu, &gpu, true), Some("tiny-skia"));
|
||||
// 没编进 wgpu 的构建绝不写 wgpu(连候选列表也不许出现,reviewer P3-4)
|
||||
assert_eq!(backend_env(Backend::Gpu, &gpu, false), Some("tiny-skia"));
|
||||
assert_eq!(backend_env(Backend::Auto, &gpu, false), Some("tiny-skia"));
|
||||
assert_eq!(backend_env(Backend::Auto, &none, false), Some("tiny-skia"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe_lists_every_adapter() {
|
||||
let text = probe(&["DiscreteGpu", "Cpu"]).describe();
|
||||
@@ -235,4 +204,23 @@ mod tests {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
//! 判据的**纯函数**部分:`请求 × 探测 × 是否编入 wgpu` → 起步后端 + 兜底后端。
|
||||
//!
|
||||
//! 表本身(什么条件选什么、日志该打什么、谁能验证)在 [`super`] 的模块文档顶部。
|
||||
//! 这里只放可测的东西:判据一旦掺进运行期副作用(写环境变量、看日志、开窗),
|
||||
//! 「降级矩阵」就再也写不成表驱动测试了——M97 的教训。
|
||||
|
||||
use crate::{Backend, Probe};
|
||||
|
||||
/// 一个具体渲染后端(`ICED_BACKEND` 的单项取值)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
/// `ICED_BACKEND=wgpu`。
|
||||
Wgpu,
|
||||
/// `ICED_BACKEND=tiny-skia`。
|
||||
TinySkia,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
/// 写进 `ICED_BACKEND` 的取值(也是 `iced_renderer` 认的名字)。
|
||||
#[must_use]
|
||||
pub fn env_value(self) -> &'static str {
|
||||
match self {
|
||||
Kind::Wgpu => "wgpu",
|
||||
Kind::TinySkia => "tiny-skia",
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志里的名字。
|
||||
#[must_use]
|
||||
pub fn name(self) -> &'static str {
|
||||
self.env_value()
|
||||
}
|
||||
|
||||
/// 对应的「档位」(D36 的 CPU 档 / GPU 档判据用)。
|
||||
#[must_use]
|
||||
pub fn tier(self) -> Backend {
|
||||
match self {
|
||||
Kind::Wgpu => Backend::Gpu,
|
||||
Kind::TinySkia => Backend::Cpu,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 判据产物:起步后端 + **本 crate 自己**的兜底(不是 iced 的候选回退)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Decision {
|
||||
/// 写进 `ICED_BACKEND` 的唯一一项。
|
||||
pub primary: Kind,
|
||||
/// 起步失败后重开的后端;`None` = 没有兜底(强制 CPU / 没编 wgpu)。
|
||||
pub fallback: Option<Kind>,
|
||||
/// 为什么这么判(进日志,验收就靠它)。
|
||||
pub reason: &'static str,
|
||||
}
|
||||
|
||||
impl Decision {
|
||||
/// 一行给人看的决策描述。
|
||||
#[must_use]
|
||||
pub fn describe(&self) -> String {
|
||||
match self.fallback {
|
||||
Some(fallback) => format!(
|
||||
"renderer = {} reason={} 兜底={}(失败会先打印具名错误)",
|
||||
self.primary.name(),
|
||||
self.reason,
|
||||
fallback.name()
|
||||
),
|
||||
None => format!(
|
||||
"renderer = {} reason={} 兜底=none(候选表唯一项)",
|
||||
self.primary.name(),
|
||||
self.reason
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 判据纯函数:请求 × 探测 × 是否编入 wgpu → 决策。
|
||||
///
|
||||
/// **必须是纯函数**:M97 的教训是判据一旦掺了运行期副作用(写环境变量、看日志),
|
||||
/// 就再也写不出「降级矩阵」的表驱动测试了。
|
||||
#[must_use]
|
||||
pub fn decide(request: Backend, probe: &Probe, gpu_compiled: bool) -> Decision {
|
||||
match (request, gpu_compiled) {
|
||||
// 1. 用户点名 CPU:只有 CPU,且**不给兜底**(强制就是强制)。
|
||||
(Backend::Cpu, _) => Decision {
|
||||
primary: Kind::TinySkia,
|
||||
fallback: None,
|
||||
reason: "forced-cpu",
|
||||
},
|
||||
// 2. 点名 GPU 但没编 wgpu:只能 CPU,没得兜。
|
||||
(Backend::Gpu, false) => Decision {
|
||||
primary: Kind::TinySkia,
|
||||
fallback: None,
|
||||
reason: "gpu-not-compiled",
|
||||
},
|
||||
// 3. 点名 GPU 且编了:GPU 起步,CPU 兜底(绝不硬失败)。
|
||||
(Backend::Gpu, true) => Decision {
|
||||
primary: Kind::Wgpu,
|
||||
fallback: Some(Kind::TinySkia),
|
||||
reason: "forced-gpu",
|
||||
},
|
||||
// 4. auto + 没编 wgpu。
|
||||
(Backend::Auto, false) => Decision {
|
||||
primary: Kind::TinySkia,
|
||||
fallback: None,
|
||||
reason: "auto-gpu-not-compiled",
|
||||
},
|
||||
// 5. auto + 只有 WARP/llvmpipe 这类软光栅:CPU 起步,wgpu 留作最后兜底。
|
||||
(Backend::Auto, true) if !probe.hardware => Decision {
|
||||
primary: Kind::TinySkia,
|
||||
fallback: Some(Kind::Wgpu),
|
||||
reason: "auto-no-hardware-adapter",
|
||||
},
|
||||
// 6. auto + 有硬件适配器:GPU 起步,CPU 兜底。
|
||||
(Backend::Auto, true) => Decision {
|
||||
primary: Kind::Wgpu,
|
||||
fallback: Some(Kind::TinySkia),
|
||||
reason: "auto-hardware-adapter",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 预检失败时把 wgpu 降下来(判据表第 7 行)。
|
||||
///
|
||||
/// 只在 `ULL_PREFLIGHT=1` 且预检真的失败时调用:这时候**别去开那个窗**——重试一次要闪两下窗,
|
||||
/// 而且原因已经拿在手里了。
|
||||
#[must_use]
|
||||
pub fn demote_after_preflight(decision: Decision) -> Decision {
|
||||
if decision.primary != Kind::Wgpu {
|
||||
return decision;
|
||||
}
|
||||
Decision {
|
||||
primary: Kind::TinySkia,
|
||||
fallback: Some(Kind::Wgpu),
|
||||
reason: "preflight-failed",
|
||||
}
|
||||
}
|
||||
|
||||
/// 取证通道(`--screen=gallery`)用:**剥掉兜底**。
|
||||
///
|
||||
/// 硬规则(M97 定):**取证工具宁可红着退出,也不能悄悄换一种后端继续出图**。
|
||||
/// 以前 gallery 在 wgpu 挂掉时会静默出 CPU 截图,而报告上写的是「GPU 渲染截图」——
|
||||
/// 所有视觉结论的可信度都被这一条悄悄打折,而且从输出上看不出来。
|
||||
#[must_use]
|
||||
pub fn strict(decision: Decision) -> Decision {
|
||||
Decision {
|
||||
fallback: None,
|
||||
reason: "gallery-strict",
|
||||
..decision
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn probe(types: &[&'static str]) -> Probe {
|
||||
Probe {
|
||||
adapters: types
|
||||
.iter()
|
||||
.map(|device_type| crate::probe::AdapterSummary {
|
||||
name: "adapter".to_owned(),
|
||||
device_type,
|
||||
})
|
||||
.collect(),
|
||||
hardware: types
|
||||
.iter()
|
||||
.any(|device_type| !matches!(*device_type, "Cpu" | "Other")),
|
||||
}
|
||||
}
|
||||
|
||||
/// 判据表的一行:`请求, 探测结论, 是否编入 wgpu, 期望起步, 期望兜底, 期望 reason`。
|
||||
type Case = (Backend, Probe, bool, Kind, Option<Kind>, &'static str);
|
||||
|
||||
/// 判据表第 1~6 行逐行钉死(表驱动)。谁动了 `decide` 的矩阵,这条先红。
|
||||
#[test]
|
||||
fn criterion_table_rows_1_to_6() {
|
||||
let hardware = || probe(&["IntegratedGpu"]);
|
||||
let software = || probe(&["Cpu"]);
|
||||
let cases: &[Case] = &[
|
||||
// 1 强制 CPU:不给兜底
|
||||
(
|
||||
Backend::Cpu,
|
||||
hardware(),
|
||||
true,
|
||||
Kind::TinySkia,
|
||||
None,
|
||||
"forced-cpu",
|
||||
),
|
||||
(
|
||||
Backend::Cpu,
|
||||
software(),
|
||||
false,
|
||||
Kind::TinySkia,
|
||||
None,
|
||||
"forced-cpu",
|
||||
),
|
||||
// 2 点名 GPU 但没编
|
||||
(
|
||||
Backend::Gpu,
|
||||
hardware(),
|
||||
false,
|
||||
Kind::TinySkia,
|
||||
None,
|
||||
"gpu-not-compiled",
|
||||
),
|
||||
// 3 点名 GPU 且编了
|
||||
(
|
||||
Backend::Gpu,
|
||||
hardware(),
|
||||
true,
|
||||
Kind::Wgpu,
|
||||
Some(Kind::TinySkia),
|
||||
"forced-gpu",
|
||||
),
|
||||
(
|
||||
Backend::Gpu,
|
||||
software(),
|
||||
true,
|
||||
Kind::Wgpu,
|
||||
Some(Kind::TinySkia),
|
||||
"forced-gpu",
|
||||
),
|
||||
// 4 auto + 没编
|
||||
(
|
||||
Backend::Auto,
|
||||
hardware(),
|
||||
false,
|
||||
Kind::TinySkia,
|
||||
None,
|
||||
"auto-gpu-not-compiled",
|
||||
),
|
||||
// 5 auto + 只有软光栅
|
||||
(
|
||||
Backend::Auto,
|
||||
software(),
|
||||
true,
|
||||
Kind::TinySkia,
|
||||
Some(Kind::Wgpu),
|
||||
"auto-no-hardware-adapter",
|
||||
),
|
||||
// 6 auto + 有硬件(macOS M1 走这条)
|
||||
(
|
||||
Backend::Auto,
|
||||
hardware(),
|
||||
true,
|
||||
Kind::Wgpu,
|
||||
Some(Kind::TinySkia),
|
||||
"auto-hardware-adapter",
|
||||
),
|
||||
];
|
||||
|
||||
for (request, probe, gpu_compiled, primary, fallback, reason) in cases {
|
||||
let got = decide(*request, probe, *gpu_compiled);
|
||||
assert_eq!(got.primary, *primary, "request={request:?} reason={reason}");
|
||||
assert_eq!(
|
||||
got.fallback, *fallback,
|
||||
"request={request:?} reason={reason}"
|
||||
);
|
||||
assert_eq!(got.reason, *reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// **安全网守门**:除了「强制 CPU」和「没编 wgpu」,任何一条判据都必须留兜底。
|
||||
///
|
||||
/// 抓不住:兜底那次重开本身失败(那是 iced 的事,见判据表第 8 行)。
|
||||
#[test]
|
||||
fn every_decision_keeps_an_exit() {
|
||||
for request in [Backend::Auto, Backend::Gpu, Backend::Cpu] {
|
||||
for gpu_compiled in [true, false] {
|
||||
for types in [&[][..], &["Cpu"][..], &["IntegratedGpu"][..]] {
|
||||
let decision = decide(request, &probe(types), gpu_compiled);
|
||||
let forced = request == Backend::Cpu || !gpu_compiled;
|
||||
assert_eq!(
|
||||
decision.fallback.is_none(),
|
||||
forced,
|
||||
"{decision:?} 必须留兜底(request={request:?} gpu_compiled={gpu_compiled})"
|
||||
);
|
||||
// 兜底不能和起步项相同,否则重试等于原地打转。
|
||||
assert_ne!(decision.fallback, Some(decision.primary), "{decision:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 预检失败只降 wgpu 起步的那些行;`--renderer=cpu` 一行都不许动。
|
||||
#[test]
|
||||
fn preflight_demotes_only_wgpu_decisions() {
|
||||
let hardware = probe(&["IntegratedGpu"]);
|
||||
let auto = decide(Backend::Auto, &hardware, true);
|
||||
let demoted = demote_after_preflight(auto);
|
||||
assert_eq!(demoted.primary, Kind::TinySkia);
|
||||
assert_eq!(demoted.reason, "preflight-failed");
|
||||
assert_eq!(demoted.fallback, Some(Kind::Wgpu), "wgpu 还能当最后兜底");
|
||||
|
||||
let cpu = decide(Backend::Cpu, &hardware, true);
|
||||
assert_eq!(demote_after_preflight(cpu), cpu, "强制 CPU 不许被预检改写");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe_names_the_backend_and_the_reason() {
|
||||
let hardware = probe(&["DiscreteGpu"]);
|
||||
let text = decide(Backend::Auto, &hardware, true).describe();
|
||||
assert!(text.contains("renderer = wgpu"), "{text}");
|
||||
assert!(text.contains("reason=auto-hardware-adapter"), "{text}");
|
||||
assert!(text.contains("兜底=tiny-skia"), "{text}");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
//! 渲染后端的**判据表**(M97)+ 三段可观测性基建。
|
||||
//!
|
||||
//! 本模块回答一个问题:**这个进程到底跑在哪个渲染器上,凭什么这么断定。**
|
||||
//!
|
||||
//! ## 判据表(验收直接对照这张表)
|
||||
//!
|
||||
//! `ICED_BACKEND` **永远是单项**(不是候选列表)。理由见下节「为什么不再用候选列表」:
|
||||
//! 单项 ⇒ 「跑起来了 ⇒ 就是它」,不存在需要事后猜的空间。`兜底` 列是**本 crate 自己的重试**,
|
||||
//! 不是 iced 的回退,所以它一定伴随一行具名错误日志。
|
||||
//!
|
||||
//! | # | 条件 | 选哪个 | 日志该打印什么 | 谁能验证 |
|
||||
//! |---|---|---|---|---|
|
||||
//! | 1 | `--renderer=cpu` / `--software-renderer` | `tiny-skia` | `renderer = tiny-skia reason=forced-cpu` | 三平台(CI 只用这条) |
|
||||
//! | 2 | `--renderer=gpu` 但本构建没编 wgpu | `tiny-skia` | `reason=gpu-not-compiled` + `;本构建未编入 wgpu` | 三平台(`--no-default-features`) |
|
||||
//! | 3 | `--renderer=gpu` 且编了 wgpu | `wgpu`,兜底 `tiny-skia` | `renderer = wgpu reason=forced-gpu 兜底=tiny-skia` | Windows / Linux 本机;macOS 需真机 |
|
||||
//! | 4 | `--renderer=auto` 但没编 wgpu | `tiny-skia` | `reason=auto-gpu-not-compiled` | 三平台(`--no-default-features`) |
|
||||
//! | 5 | `auto` + 枚举不到硬件适配器(只有 WARP / llvmpipe) | `tiny-skia`,兜底 `wgpu` | `reason=auto-no-hardware-adapter` | Windows 本机(软渲染 VM) |
|
||||
//! | 6 | `auto` + 有硬件适配器 | `wgpu`,兜底 `tiny-skia` | `reason=auto-hardware-adapter` | Windows 本机 **√**;macOS 需真机 |
|
||||
//! | 7 | 同上但 `ULL_PREFLIGHT=1` 预检失败 | `tiny-skia`,兜底 `wgpu` | `[gpu-preflight] FAILED stage=… error=…` + `reason=preflight-failed` | 三平台(可用 `WGPU_BACKEND=gl` 造) |
|
||||
//! | 8 | 任何一行选了 wgpu,但 iced 真初始化失败,且**交互启动**(无 `--screenshot`) | 具名错误 → **换一个进程**重开 `tiny-skia` | `[gpu] wgpu 初始化失败(具名):…` → `[gpu] 降级 → tiny-skia(…换一个进程重开…)` → 子进程 `[probe] 本次是降级重开(ULL_GPU_FALLBACK=tiny-skia)` | **Windows 已验**(`WGPU_BACKEND=metal` 造);macOS 需真机 |
|
||||
//! | 9 | 同上行,但**取证启动**(带 `--screenshot`,含 `--screen=gallery`) | 具名错误 → **不出图**,退出码 1 | `[gpu] wgpu 初始化失败(具名):…` → `[gpu] 拒绝降级出图:本次是取证启动(--screenshot …)` | **Windows 已验**(同造法);macOS 需真机 |
|
||||
//!
|
||||
//! **第 8/9 行的硬规则(M97 定)**:
|
||||
//!
|
||||
//! - 取证工具(`--screenshot`)宁可红着退出,也不能悄悄换一种后端继续出图。以前 gallery 在
|
||||
//! wgpu 挂掉时会静默降级到 tiny-skia 照样落 PNG,而报告上写的是「GPU 渲染截图」——所有视觉
|
||||
//! 结论的可信度都被悄悄打折,而且从输出上看不出来。现在每张图的日志里都带
|
||||
//! **实际**后端名(`[shot] … renderer=wgpu`),图能自证来源。
|
||||
//! - 交互启动保留安全网:真无 GPU / wgpu 起不来时降级到 tiny-skia,不崩。
|
||||
//!
|
||||
//! **降级为什么是换进程**:第一版写的是「打印错误 → 在本进程里再 `run()` 一次」,Windows 实测
|
||||
//! 直接 panic(`iced_winit-0.14.1/src/lib.rs:81` → `Create event loop: RecreationAttempt`)——
|
||||
//! winit 不允许一个进程建两次事件循环。所以安全网是 `respawn`:起一个子进程、只换
|
||||
//! `ICED_BACKEND` + `ULL_GPU_FALLBACK`,然后带着子进程的退出码退出。子进程看到
|
||||
//! `ULL_GPU_FALLBACK` 就跳过探测(否则会得到同一个结论、无限重开)。
|
||||
//!
|
||||
//! `ULL_PREFLIGHT` 开关的代价(**实测**,不是估计):Windows RX 6600 XT / Vulkan 上
|
||||
//! `took=717ms`(首次要扫一遍 Vulkan loader),不可用时 `took=0ms`。打开它等于启动多 ~0.7 s,
|
||||
//! 所以默认关。它测不了 `create_surface`(那时还没有窗口),代替不了真跑一次,只用来「开窗前拿原因」。
|
||||
//! 注意:`ULL_PREFLIGHT=1` + 预检失败 + `--screenshot` 时**仍会**降级出图(`reason=preflight-failed`,
|
||||
//! 图上是 `renderer=tiny-skia`)——那条路径不是静默的,但图确实是 CPU 的,读图前先读日志。
|
||||
//!
|
||||
//! ## 怎么从日志判断「有没有静默降级」
|
||||
//!
|
||||
//! | 你看到 | 结论 |
|
||||
//! |---|---|
|
||||
//! | `[probe] renderer = wgpu` 且之后有 `[gpu] iced_wgpu: …Selected: AdapterInfo…` | **真在走 GPU**(这行是 iced_wgpu 自己在创建设备时打的,不是我们的猜测) |
|
||||
//! | `[gpu] wgpu 初始化失败(具名):…` | wgpu 起不来,**原因就在这行里**;交互启动已换进程降级 CPU,取证启动已拒绝出图 |
|
||||
//! | `[gpu] 拒绝降级出图:…` | 这张图**没有**产出(取证启动 + wgpu 起不来)。想用 CPU 出图请显式 `--renderer=cpu` |
|
||||
//! | `[gpu] 降级 → tiny-skia(…换一个进程重开…)` | 交互启动降级了;后面必然跟一行 `[probe] 本次是降级重开` |
|
||||
//! | `[probe] renderer = tiny-skia reason=…`(`reason` 不是 `forced-cpu`) | 探测判定不该上 GPU,**原因在 `reason`** |
|
||||
//! | `[shot] … renderer=wgpu` / `renderer=tiny-skia` | 这张图**实际**由哪个后端产出(M97 之前这里打的是请求值 `auto`,等于没打) |
|
||||
//! | 只有 `[probe] renderer = …` 一行、没有 `[gpu] iced_wgpu:` 也没有取证行 | 那是 M97 之前的旧日志——**它从来没观测过实际后端**,不可采信 |
|
||||
//!
|
||||
//! ## 为什么不再用候选列表(M97 的根因订正)
|
||||
//!
|
||||
//! M97 之前:`auto` + 有硬件 → **不设** `ICED_BACKEND`,把决策交给 iced 的
|
||||
//! `fallback::Compositor`;那条路径里 wgpu 的失败被 `errors` 向量装起来、tiny-skia 一成功就整体丢弃
|
||||
//! (`iced_renderer-0.14.0/src/fallback.rs`,我们改不了)。日志只剩 `renderer = auto (wgpu -> tiny-skia)`
|
||||
//! ——而这行是 `probe.rs` 里按 `env == None` 打出来的**常量字符串**,跟实际后端无关。
|
||||
//! 于是 macOS M1 上「枚举到 Apple M1」与「最终是不是真在用 GPU」彻底脱钩:**看起来正常但可能在跑 CPU**。
|
||||
//!
|
||||
//! 现在的唯一判据:`ICED_BACKEND` 只写一项 ⇒ 能跑起来就必定是它;起不来就必然是
|
||||
//! `iced::Error::GraphicsCreationFailed` 里那串具名错误(哪个 backend、reason 是什么),
|
||||
//! 由 `app/boot.rs` 打印后决定「换进程降级」还是「拒绝出图」。
|
||||
//!
|
||||
//! ## 三段基建
|
||||
//!
|
||||
//! | 模块 | 作用 | 默认 |
|
||||
//! |---|---|---|
|
||||
//! | [`preflight`] | 在 iced 之前自己跑一遍 wgpu 的「实例→适配器→设备」,把失败原因拿在手里 | **关**(`ULL_PREFLIGHT=1` 打开;会重复创建实例/设备,别放默认路径) |
|
||||
//! | [`log_bridge`] | 把 `iced_wgpu` / `wgpu` 的 `log` 输出转到 stderr(`Selected: AdapterInfo{…}` 这行就是 GPU 生效的实证) | 开(`ULL_GPU_LOG=off` 可关) |
|
||||
//!
|
||||
//! ## 真机验收(macOS Metal 路径 —— **需真机复核**,Windows 验不了这一段)
|
||||
//!
|
||||
//! 判据表第 3/6/8/9 行的 macOS 分支只能真机验。下面每段都可直接粘贴;`# 期望` 后面是**必须能在
|
||||
//! stderr 里 grep 到的原文**(M98 的验收包会直接把这些字符串编成断言)。
|
||||
//!
|
||||
//! ```bash
|
||||
//! # 0) 一次
|
||||
//! cargo build -p unlucky-app-iced
|
||||
//!
|
||||
//! # 1) 交互启动:这才是判「M1 到底有没有在用 GPU」的那次。看完关窗即可。
|
||||
//! ./target/debug/unlucky-launcher-iced --renderer=auto
|
||||
//! # 期望(这两行同一次运行里都要有):
|
||||
//! # [probe] renderer = wgpu reason=auto-hardware-adapter 兜底=tiny-skia
|
||||
//! # [gpu] iced_wgpu::window::compositor: Selected: AdapterInfo { name: "Apple M1",
|
||||
//! # 上面第二行里的 backend 必须是 Metal(同一行靠后)。
|
||||
//! # 若出现下面任一行 ⇒ 这次**不是** GPU,把那段原文(含上一行)发回来:
|
||||
//! # [gpu] wgpu 初始化失败(具名):
|
||||
//! # [gpu] 降级 → tiny-skia
|
||||
//! # [probe] 本次是降级重开(ULL_GPU_FALLBACK=tiny-skia)
|
||||
//!
|
||||
//! # 2) 开窗前拿原因(不建窗;多花 ~0.7 s)
|
||||
//! ULL_PREFLIGHT=1 ./target/debug/unlucky-launcher-iced --renderer=auto
|
||||
//! # 期望:[gpu-preflight] ok adapter="Apple M1" type=IntegratedGpu backend=Metal took=
|
||||
//!
|
||||
//! # 3) 取证出图(D30 通道;这一张图自证来源)
|
||||
//! ./target/debug/unlucky-launcher-iced --screen=gallery --screenshot /tmp/gallery.png
|
||||
//! # 期望(三行都该有):
|
||||
//! # [gpu] iced_wgpu::window::compositor: Selected: AdapterInfo { name: "Apple M1",
|
||||
//! # [shot] 1180x760 scale=1 -> /tmp/gallery.png
|
||||
//! # [shot] screen=gallery theme=light renderer=wgpu
|
||||
//! # 若 wgpu 起不来 ⇒ 没有 /tmp/gallery.png、退出码 1、且打印 [gpu] 拒绝降级出图:
|
||||
//!
|
||||
//! # 4) 强制 CPU 对照(必须与 M97 之前逐字一致)
|
||||
//! ./target/debug/unlucky-launcher-iced --renderer=cpu --screenshot /tmp/cpu.png
|
||||
//! # 期望:
|
||||
//! # [probe] adapters=0 hardware=false :: request=cpu -> ICED_BACKEND=tiny-skia
|
||||
//! # [probe] renderer = tiny-skia reason=forced-cpu 兜底=none(候选表唯一项)
|
||||
//! # [shot] screen=shell theme=light renderer=tiny-skia
|
||||
//!
|
||||
//! # 5) 负例:取证启动遇到 wgpu 不可用时必须红着退出(Mac 上用一个不存在的后端来造)
|
||||
//! WGPU_BACKEND=dx12 ./target/debug/unlucky-launcher-iced --screen=gallery --screenshot /tmp/bad.png
|
||||
//! # 期望:**没有** /tmp/bad.png、退出码 1、并且打印:
|
||||
//! # [gpu] wgpu 初始化失败(具名):multiple errors occurred: [GraphicsAdapterNotFound { backend: "wgpu", reason: RequestFailed("no adapter was found for the options requested:
|
||||
//! # [gpu] 拒绝降级出图:本次是取证启动(--screenshot /tmp/bad.png),换后端出的图会撒谎。
|
||||
//! ```
|
||||
//!
|
||||
//! 第 5 段在 Windows 上已用同构的 `WGPU_BACKEND=metal` 验过(`metal support not compiled in`
|
||||
//! → 具名错误 → 拒绝出图 → exit 1);macOS 上要确认打印的是 `dx12 support not compiled in` 那类
|
||||
//! wgpu 原话,而不是我们自己的话。
|
||||
|
||||
pub mod decision;
|
||||
pub mod log_bridge;
|
||||
pub mod preflight;
|
||||
|
||||
pub use decision::{decide, demote_after_preflight, strict, Decision, Kind};
|
||||
|
||||
/// 把 `iced::Error` 里的**具名**图形错误掏出来(`Display` 那句笼统的话不算原因)。
|
||||
///
|
||||
/// `iced::Error::GraphicsCreationFailed(inner)` 的 `Display` 只有
|
||||
/// 「the application graphics context could not be created」,**没有** `{0}`;
|
||||
/// 真正的 backend 名与 `Reason` 在内层 `graphics::Error` 里。所以这里同时给 Display 与 Debug。
|
||||
#[must_use]
|
||||
pub fn named_graphics_error(error: &iced::Error) -> Option<String> {
|
||||
match error {
|
||||
iced::Error::GraphicsCreationFailed(inner) => Some(format!("{inner}|详情 {inner:?}")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 跑一次 iced;起步后端起不来时把**具名**原因交给调用方(不在这里重试)。
|
||||
///
|
||||
/// ## 为什么不能在本进程里重试(M97 实测)
|
||||
///
|
||||
/// 第一版这里是「打印错误 + 再 `run()` 一次」。Windows 实测直接 panic:
|
||||
///
|
||||
/// ```text
|
||||
/// thread 'main' panicked at iced_winit-0.14.1/src/lib.rs:81:
|
||||
/// Create event loop: RecreationAttempt
|
||||
/// ```
|
||||
///
|
||||
/// winit 不允许一个进程建两次事件循环,所以**进程内重试在架构上就不成立**。
|
||||
/// 安全网因此改成**换一个进程重开**(`app/boot.rs` 的 `respawn`),
|
||||
/// 猜测的写法别再加回来。
|
||||
pub fn run_once(decision: Decision, attempt: impl FnOnce(Kind) -> iced::Result) -> iced::Result {
|
||||
attempt(decision.primary)
|
||||
}
|
||||
|
||||
/// 降级重开用的环境变量:子进程看到它就跳过探测、直接用它指向的后端(见 `app::boot::boot_probe`)。
|
||||
///
|
||||
/// ⚠️ 这是**防无限重开**的唯一闸门:子进程若不跳过探测,会算出同一个结论(auto + 有硬件 → wgpu)、
|
||||
/// 再失败、再 respawn,变成自杀式递归。子进程侧的判据在 `boot_probe` 的第一个分支。
|
||||
///
|
||||
/// 它也是**进程间**的交接约定,不是给用户设的开关(用户入口仍是 `--renderer`)。
|
||||
pub const FALLBACK_ENV: &str = "ULL_GPU_FALLBACK";
|
||||
|
||||
/// 读 [`FALLBACK_ENV`]:本进程是不是「上一步失败后重开」的那一次。
|
||||
#[must_use]
|
||||
pub fn forced_fallback() -> Option<Kind> {
|
||||
match std::env::var(FALLBACK_ENV).ok()?.trim() {
|
||||
"tiny-skia" => Some(Kind::TinySkia),
|
||||
"wgpu" => Some(Kind::Wgpu),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 打印**具名**失败原因(三行以内,`stage`/`backend`/`reason` 都可 grep)。
|
||||
///
|
||||
/// `iced::Error::GraphicsCreationFailed` 的 `Display` **不包含**内部错误
|
||||
/// (`#[error("the application graphics context could not be created")]`,没有 `{0}`),
|
||||
/// 所以这里必须显式把内层错误打出来,否则又变回「只知道失败,不知道为什么」。
|
||||
pub fn report_failure(backend: Kind, error: &(impl std::fmt::Display + std::fmt::Debug)) {
|
||||
eprintln!("[gpu] {} 初始化失败(具名):{error}", backend.name());
|
||||
eprintln!("[gpu] 失败详情(具名):{error:?}");
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
//! wgpu 自检(判据表第 7 行):在把控制权交给 iced **之前**自己跑一遍
|
||||
//! 「实例 → 适配器 → 设备」,把 wgpu 的具名错误拿在手里。
|
||||
//!
|
||||
//! ## 为什么默认关
|
||||
//!
|
||||
//! 它会**重复** iced 马上要做的活(再建一个实例、再要一个设备),macOS 上一个 Metal 设备
|
||||
//! 不是免费的;而且它测不了 `create_surface`(那时还没有窗口),所以它**代替不了**真跑一次。
|
||||
//! 它的价值只有一个:让「不上 GPU」这个结论在**开窗之前**就有原因,验收时不用盯着一个闪窗猜。
|
||||
//!
|
||||
//! 开关:`ULL_PREFLIGHT=1`(判据表里登记过,不是隐藏口子)。
|
||||
//!
|
||||
//! ## 它镜像的是谁
|
||||
//!
|
||||
//! 逐字对着 `iced_wgpu-0.14.0/src/window/compositor.rs` 的 `Compositor::request()` 抄的:
|
||||
//! `Backends::from_env().unwrap_or(all)`、`PowerPreference::from_env().unwrap_or(HighPerformance)`、
|
||||
//! `Limits { max_bind_groups: 2, max_non_sampler_bindings: 2048, ..default }`。
|
||||
//! 差一处都会让预检和真实路径说不同的话,那时预检就不是证据而是噪声了。
|
||||
|
||||
/// 预检开关的环境变量名(`1` / `true` 打开)。
|
||||
pub const ENV_SWITCH: &str = "ULL_PREFLIGHT";
|
||||
|
||||
/// 预检成功:拿到的适配器摘要。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Verdict {
|
||||
/// 适配器名(`Apple M1` / `AMD Radeon RX 6600 XT` / `Microsoft Basic Render Driver`)。
|
||||
pub adapter: String,
|
||||
/// `device_type` 的字符串形式。
|
||||
pub device_type: &'static str,
|
||||
/// 命中哪个 wgpu 后端(`Metal` / `Vulkan` / `Dx12` / `Gl`)。
|
||||
pub backend: String,
|
||||
}
|
||||
|
||||
impl Verdict {
|
||||
/// 一行日志。
|
||||
#[must_use]
|
||||
pub fn describe(&self) -> String {
|
||||
format!(
|
||||
"ok adapter={:?} type={} backend={}",
|
||||
self.adapter, self.device_type, self.backend
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 预检失败:卡在哪一步 + wgpu 自己的错误文本。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Failure {
|
||||
/// 失败阶段(日志里 `stage=` 的取值)。
|
||||
pub stage: &'static str,
|
||||
/// wgpu 具名错误(`Display`,原样保留,不翻译成「失败」)。
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
impl Failure {
|
||||
/// 一行日志(`stage=` 可直接 grep)。
|
||||
#[must_use]
|
||||
pub fn describe(&self) -> String {
|
||||
format!("FAILED stage={} error={}", self.stage, self.detail)
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户是否要求预检。
|
||||
#[must_use]
|
||||
pub fn requested() -> bool {
|
||||
std::env::var(ENV_SWITCH).is_ok_and(|value| matches!(value.trim(), "1" | "true" | "yes" | "on"))
|
||||
}
|
||||
|
||||
/// 按 `ULL_PREFLIGHT` 决定跑不跑;跑了就顺手把结论打进 stderr(带耗时,别让开关成本变成传说)。
|
||||
///
|
||||
/// 返回 `None` = 没要求预检(调用方按判据表 1~6 行原样走)。
|
||||
#[must_use]
|
||||
pub fn run_if_requested() -> Option<Result<Verdict, Failure>> {
|
||||
if !requested() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let verdict = run();
|
||||
let took = started.elapsed().as_secs_f64() * 1000.0;
|
||||
match &verdict {
|
||||
Ok(ok) => eprintln!("[gpu-preflight] {} took={took:.0}ms", ok.describe()),
|
||||
Err(failure) => {
|
||||
eprintln!("[gpu-preflight] {} took={took:.0}ms", failure.describe());
|
||||
}
|
||||
}
|
||||
Some(verdict)
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu")]
|
||||
/// 真跑一遍 wgpu 初始化(不开窗、不建 surface)。
|
||||
///
|
||||
/// # Errors
|
||||
/// 无适配器 / 设备请求被拒时返回 [`Failure`],`detail` 是 wgpu 的原话。
|
||||
pub fn run() -> Result<Verdict, Failure> {
|
||||
use iced::wgpu::{
|
||||
Backends, ExperimentalFeatures, Features, Instance, InstanceDescriptor, Limits,
|
||||
MemoryHints, PowerPreference, RequestAdapterOptions, Trace,
|
||||
};
|
||||
|
||||
let backends = Backends::from_env().unwrap_or_else(Backends::all);
|
||||
let instance = Instance::new(&InstanceDescriptor {
|
||||
backends,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let options = RequestAdapterOptions {
|
||||
power_preference: PowerPreference::from_env().unwrap_or(PowerPreference::HighPerformance),
|
||||
// 没有窗口,测不了 surface 兼容性——这一条只能靠真跑一次(判据表第 8 行)。
|
||||
compatible_surface: None,
|
||||
force_fallback_adapter: false,
|
||||
};
|
||||
|
||||
let adapter = block_on(instance.request_adapter(&options)).map_err(|error| Failure {
|
||||
stage: "request-adapter",
|
||||
detail: format!("{options:?}: {error}"),
|
||||
})?;
|
||||
|
||||
let info = adapter.get_info();
|
||||
let device_type = device_type_name(info.device_type);
|
||||
|
||||
// iced 的 `request_device` 失败时不打日志、只把错误塞进 `RequestDeviceFailed`,
|
||||
// 所以我们在这里先请求一次,把它的话原样抄下来。
|
||||
let _ = block_on(adapter.request_device(&iced::wgpu::DeviceDescriptor {
|
||||
label: Some("unlucky-launcher gpu preflight"),
|
||||
required_features: Features::empty(),
|
||||
required_limits: Limits {
|
||||
max_bind_groups: 2,
|
||||
max_non_sampler_bindings: 2048,
|
||||
..Limits::default()
|
||||
},
|
||||
memory_hints: MemoryHints::MemoryUsage,
|
||||
trace: Trace::Off,
|
||||
experimental_features: ExperimentalFeatures::disabled(),
|
||||
}))
|
||||
.map_err(|error| Failure {
|
||||
stage: "request-device",
|
||||
detail: error.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(Verdict {
|
||||
adapter: info.name,
|
||||
device_type,
|
||||
backend: format!("{:?}", info.backend),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "gpu"))]
|
||||
/// 本构建没编 wgpu:预检必然是「不可用」,但这不是错误——判据表第 4 行已经说明原因。
|
||||
///
|
||||
/// # Errors
|
||||
/// 恒返回 [`Failure`](`stage=not-compiled`)。
|
||||
pub fn run() -> Result<Verdict, Failure> {
|
||||
Err(Failure {
|
||||
stage: "not-compiled",
|
||||
detail: "本构建未编入 wgpu(--no-default-features)".to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu")]
|
||||
/// `DeviceType` → 日志里的字符串(探测与预检**共用**这一处,别各写一份 `match`)。
|
||||
#[must_use]
|
||||
pub fn device_type_name(device_type: iced::wgpu::DeviceType) -> &'static str {
|
||||
use iced::wgpu::DeviceType;
|
||||
match device_type {
|
||||
DeviceType::DiscreteGpu => "DiscreteGpu",
|
||||
DeviceType::IntegratedGpu => "IntegratedGpu",
|
||||
DeviceType::VirtualGpu => "VirtualGpu",
|
||||
DeviceType::Cpu => "Cpu",
|
||||
DeviceType::Other => "Other",
|
||||
}
|
||||
}
|
||||
|
||||
/// 把 wgpu 的 future 跑到底。
|
||||
///
|
||||
/// 只用 `std`:`Waker` 由 `std::task::Wake` 提供(安全 API),运行期负责 `park`。
|
||||
/// 不引入 futures 执行器——这里只有一个 future,而且它在 wgpu 的回调线程上唤醒。
|
||||
#[cfg(feature = "gpu")]
|
||||
fn block_on<F: std::future::Future>(future: F) -> F::Output {
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll, Wake, Waker};
|
||||
|
||||
struct Unpark(std::thread::Thread);
|
||||
|
||||
impl Wake for Unpark {
|
||||
fn wake(self: Arc<Self>) {
|
||||
self.0.unpark();
|
||||
}
|
||||
|
||||
fn wake_by_ref(self: &Arc<Self>) {
|
||||
self.0.unpark();
|
||||
}
|
||||
}
|
||||
|
||||
let waker = Waker::from(Arc::new(Unpark(std::thread::current())));
|
||||
let mut context = Context::from_waker(&waker);
|
||||
let mut future = std::pin::pin!(future);
|
||||
|
||||
loop {
|
||||
match future.as_mut().poll(&mut context) {
|
||||
Poll::Ready(output) => return output,
|
||||
Poll::Pending => std::thread::park(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn failure_line_is_greppable() {
|
||||
let failure = Failure {
|
||||
stage: "request-device",
|
||||
detail: "no device request succeeded".to_owned(),
|
||||
};
|
||||
let text = failure.describe();
|
||||
assert!(
|
||||
text.starts_with("FAILED stage=request-device error="),
|
||||
"{text}"
|
||||
);
|
||||
assert!(text.contains("no device request succeeded"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verdict_line_lists_the_three_facts() {
|
||||
let verdict = Verdict {
|
||||
adapter: "Apple M1".to_owned(),
|
||||
device_type: "IntegratedGpu",
|
||||
backend: "Metal".to_owned(),
|
||||
};
|
||||
let text = verdict.describe();
|
||||
assert!(text.contains("adapter=\"Apple M1\""), "{text}");
|
||||
assert!(text.contains("type=IntegratedGpu"), "{text}");
|
||||
assert!(text.contains("backend=Metal"), "{text}");
|
||||
}
|
||||
|
||||
/// 没编 wgpu 的构建:预检必须说清原因,而不是一句「失败」。
|
||||
#[cfg(not(feature = "gpu"))]
|
||||
#[test]
|
||||
fn unbuilt_gpu_names_the_reason() {
|
||||
let failure = run().unwrap_err();
|
||||
assert_eq!(failure.stage, "not-compiled");
|
||||
assert!(failure.detail.contains("wgpu"), "{}", failure.detail);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user