复审 r1(reviewer-accscope-code,p2-2items / fix-then-merge)的两条阻塞 + 四条顺手的 p3。 ## p2-1 `[window]` 的三个窗口属性曾是格式串常量(U1:常量冒充观测结果) 第一版把 `resizable=true decorations=false transparent=false` 写在格式化串里,与交给 iced 的 `window::Settings` 没有任何绑定;定向变异把 `settings.decorations` 改成 `true`,**371 个测试全绿**, 而验收包里 `[caps]` 与 `[window]` 会互相矛盾。 - 新增 `acceptance::window::WindowFacts` + `from_settings()`:字段只能从**真实 settings** 抽出来, 没有手抄入口;`window_text()` 逐字段打印。 - 窗口设置收成单一产地 `app/boot/diagnostics::window_settings()`,`launch()` 与 `[window]`/`[caps]` 两行日志共用同一个 `&window::Settings`;`pages/harness.rs` 同样从自己那份 settings 取。 - 守门测试 `window_settings_match_what_the_acceptance_logs_claim`,含「from_settings 必须逐字段 搬运」的反向断言。变异实证:`window_settings` 的 `decorations → true` ⇒ 红并点名; `from_settings` 里写死 `decorations: false` ⇒ 红(第一次写测试时这条漏抓,补强后实测红)。 ## p2-2 语言维度空转、且没有任何结论断言 `lang::resolve` 是 setting → 平台源 → env,macOS 的 AppleLanguages 几乎恒有值 ⇒ `LANG=zh_CN` / `LANG=en_US` 两档同值,而原脚本只断言「`[i18n]` 这行存在」——整条链退化成 `source=default lang=en` 时核心集一条都不红。 - 脚本加两条结论断言:`^\[i18n\].*platform=applelanguages:[A-Za-z-]+` 与 ` source=applelanguages` (退化日志上实测红、健康日志上实测绿)。 - `REPORT.md` 语言行下注明:env 优先级低于平台源,正常 Mac 上两档同值,这一维验的是「平台源 真读到了 + 中文字体回退」,不是中英对照。 ## p3(复审 + 第二路自跑复核同源的三条) - `[perf] boot probe=0ms` → `probe=skipped`(没计时就不报数;降级重开那一轮改为**真计时**)。 变异实证:把 skipped 写成 0ms ⇒ `perf_lines_carry_measured_millis` 红。 - `[env] gpu=` 排序改成 `DiscreteGpu > IntegratedGpu > 其它`:本机原来报核显,而 iced 实际选的是 独显(现在报 `AMD Radeon RX 6600 XT [DiscreteGpu]`,与 `Selected:` 一致)。 - `[i18n] setting=<未设置>` → `<未接线>`(配置侧根本没读取点,写「未设置」等于把「我们没读」 说成「用户没设」)。 - `[backend] final=` 失败行:`refuse` / `none` 两条路是 `final=none`(什么都没跑起来),只有 `respawn` 才是兜底名;`pages/harness.rs` 不再写死 `via="refuse"`,改用共用的 `failure_via()`。 实跑负例:`[backend] final=none result=failed primary=wgpu fallback=tiny-skia via=refuse`。 - 脚本 `[tick]` 那处唯一未锚定的 `field()`:加 `^` 锚定、正则改 `clock=[a-z0-9-]+`(真值是 `idle-1hz`,旧 `[a-z]+` 把它截成 `idle`,靠前缀巧合通过);断言也改成 `clock=idle-1hz`。 - 脚本 `[backend] final` 取值改 `field_backend()`:优先 `result=started`(respawn 场景一条日志里 有父 failed + 子 started 两条,旧 `head -1` 会把「这次没跑起来」写进报告)。 - 脚本 `exec 3>&1` 全程留痕(终端提示也进 `logs/00-run.log`)、跑前清空 `$OUT/shots/`、 python3 缺失时在 REPORT 里写明「几何列未采」而不是留空。 - `renderer/mod.rs` 补一段依据:为什么「单项 ICED_BACKEND ⇒ 没有静默回退」成立(`iced_tiny_skia` 的 `with_backend` 只接受 `tiny-skia`),以及它依赖 iced 实现细节的两层缓解。 ## 结构 `app/boot.rs` 400 行零余量 → 拆成 `boot.rs`(248) + `boot/fallback.rs`(117,兜底/重开/退出码) + `boot/diagnostics.rs`(110,窗口设置与四段报告);`run()` 回到「探测 → 字体 → 打印 → 起 iced」。 `acceptance` 再把 `[window]` 拆成 `window.rs`(142)。 门禁:`cargo fmt --check` / `clippy --all-targets -- -D warnings` / `test`(373 lib + 6 integration)全绿。
249 lines
9.9 KiB
Rust
249 lines
9.9 KiB
Rust
//! 启动编排:**探测 → 字体 → 打印诊断 → 起 iced**(四步,别把它撑回一坨)。
|
||
//!
|
||
//! ## 三条纪律(每条都对应一次返工)
|
||
//!
|
||
//! 1. **后端起不来时的两条路策略不同**(判据表第 8/9 行):交互启动换进程重开、取证启动
|
||
//! (`--screenshot`)拒绝出图 —— 实现与测试都在 [`fallback`]。
|
||
//! 2. **窗口设置只有一个产地**([`diagnostics::window_settings`]),`[window]` / `[caps]` 两行日志
|
||
//! 从**同一个** `&window::Settings` 取字段。M98 r1 p2-1 的教训:把 `decorations=false` 写进
|
||
//! 格式化串,等于让日志与真实行为可以各走各的(踩坑记录 U1)。
|
||
//! 3. **没测过的就写 `skipped`**,不许拿 `0ms` 冒充实测([`diagnostics::report_font`] 的 `probe`)。
|
||
//!
|
||
//! 为什么重开必须是**换进程**:Windows 实测本进程里再 `run()` 一次直接 panic
|
||
//! (`iced_winit-0.14.1/src/lib.rs:81` → `Create event loop: RecreationAttempt`)。
|
||
|
||
use std::time::{Duration, Instant};
|
||
|
||
use iced::Task;
|
||
|
||
use unlucky_design::color::SchemeFlavor;
|
||
|
||
use crate::cli::Options;
|
||
use crate::fonts;
|
||
use crate::probe;
|
||
use crate::renderer::{self, acceptance, Decision};
|
||
use crate::theme::Families;
|
||
|
||
use super::message::Message;
|
||
use super::state::{App, STARTED};
|
||
use super::{MIN_SIZE, WINDOW_SIZE};
|
||
|
||
mod diagnostics;
|
||
mod fallback;
|
||
|
||
use fallback::{on_failure, respawn, OnFailure};
|
||
|
||
/// 启动 iced(阻塞到退出)。`--diff` 不走这里,见 `main.rs`。
|
||
pub(crate) fn run(options: Options) -> iced::Result {
|
||
let _ = STARTED.set(Instant::now());
|
||
let boot_started = Instant::now();
|
||
|
||
// ① 探测(判据取在哪、日志在 `report_boot` 里统一打)
|
||
let boot = boot_probe(&options);
|
||
let decision = boot.decision;
|
||
diagnostics::report_boot(&options, &boot.probe, &decision);
|
||
|
||
// ② 字体(`fonts` 自己打两行既有日志;新加的那行在 `report_font` 里)
|
||
let font_started = Instant::now();
|
||
let resolved = fonts::resolve();
|
||
let font_ms = font_started.elapsed();
|
||
eprintln!("[font] 求值耗时 {:.0} ms", font_ms.as_secs_f64() * 1000.0);
|
||
eprintln!("{}", resolved.describe());
|
||
if resolved.has_synthetic() {
|
||
eprintln!(
|
||
"[font] 警告:有字重落在「合成加粗」档(观感降级),D28 后半(CDN 字体/按档换字族)要处理"
|
||
);
|
||
}
|
||
let families = resolved.families();
|
||
|
||
// ③ 打印(主题来源 + 字体回退链 + 启动耗时)
|
||
// 截图模式:`--screenshot-theme` > `--theme` > **Light**(唯一取值口 `Options::capture_flavor`)。
|
||
// **不再有**「等系统主题到达」的闸门——CI 的 xvfb 没有主题会话,等它就是挂死(见 cli 文档)。
|
||
if options.screenshot.is_some() && options.shot_flavor().is_none() {
|
||
eprintln!("[shot] 未指定主题,按 Light 出图(D30 可复现;CI 冒烟依赖这条默认值)");
|
||
}
|
||
let flavor = options.capture_flavor().unwrap_or(SchemeFlavor::Light);
|
||
diagnostics::report_theme(&options, flavor);
|
||
diagnostics::report_font(
|
||
resolved.platform,
|
||
boot.probe_ms,
|
||
font_ms,
|
||
boot_started.elapsed(),
|
||
);
|
||
|
||
// ④ 起 iced(失败按判据表第 8/9 行分流;具名原因在分流之前就打到同一个 stderr 上了)
|
||
let launch_started = Instant::now();
|
||
let result = match launch(&options, families, flavor, decision.primary) {
|
||
Err(iced::Error::GraphicsCreationFailed(error)) => {
|
||
renderer::report_failure(decision.primary, &error);
|
||
match on_failure(decision, options.screenshot.as_ref()) {
|
||
Some(OnFailure::Respawn(fallback)) => {
|
||
respawn(decision.primary, fallback, &error.to_string())
|
||
}
|
||
Some(OnFailure::Refuse) => {
|
||
// 取证启动:**拒绝降级出图**(换了后端出的图会让报告撒谎)。
|
||
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,
|
||
};
|
||
let launch_elapsed = launch_started.elapsed();
|
||
|
||
diagnostics::report_outcome(
|
||
&decision,
|
||
result.as_ref().err().map(|error| {
|
||
renderer::named_graphics_error(error).unwrap_or_else(|| error.to_string())
|
||
}),
|
||
acceptance::failure_via(decision.fallback.is_some(), options.screenshot.is_some()),
|
||
if options.screenshot.is_some() {
|
||
"screenshot"
|
||
} else {
|
||
"interactive"
|
||
},
|
||
launch_elapsed,
|
||
);
|
||
|
||
result
|
||
}
|
||
|
||
/// 按一次后端启动 iced。`backend` 决定这次写进 `ICED_BACKEND` 的唯一一项。
|
||
fn launch(
|
||
options: &Options,
|
||
families: Families,
|
||
flavor: SchemeFlavor,
|
||
backend: renderer::Kind,
|
||
) -> iced::Result {
|
||
std::env::set_var("ICED_BACKEND", backend.env_value());
|
||
|
||
let boot_options = options.clone();
|
||
|
||
// M98:`ULL_WINDOW_SIZE=1520x900` 覆盖尺寸(矩阵的「最小 / 宽屏」两档靠它,且必须走**真外壳**
|
||
// ——`pages_demo` example 的标题栏是留白带,出不了「三键位置」那张图)。
|
||
let (size, size_source) = acceptance::window_size(
|
||
(WINDOW_SIZE.width, WINDOW_SIZE.height),
|
||
(MIN_SIZE.width, MIN_SIZE.height),
|
||
);
|
||
let settings = diagnostics::window_settings(size);
|
||
// 两行日志与开窗用的是**同一个** settings(见模块文档纪律 2)
|
||
diagnostics::report_window(&settings, options.scale_factor, size_source);
|
||
|
||
let application = iced::application(
|
||
move || {
|
||
(
|
||
App::new(boot_options.clone(), families, flavor),
|
||
Task::done(Message::Boot),
|
||
)
|
||
},
|
||
App::update,
|
||
App::view,
|
||
)
|
||
.title(App::title)
|
||
.theme(App::theme)
|
||
.subscription(App::subscription)
|
||
.default_font(default_font(families))
|
||
.window(settings);
|
||
|
||
match options.scale_factor {
|
||
Some(scale) => application.scale_factor(move |_| scale).run(),
|
||
None => application.run(),
|
||
}
|
||
}
|
||
|
||
/// [`boot_probe`] 的产物:判据 + 探测到的适配器 + **探测耗时(没测就是 `None`)**。
|
||
///
|
||
/// 为什么把 `Probe` 带出来:`[env]` 的 `gpu=` / `hardware=` 必须是**这一次真枚举到的**结果
|
||
/// (再枚举一遍多花 0.65~1.2 s,还可能给出不同状态)。判据与诊断看同一份数据。
|
||
struct Boot {
|
||
decision: Decision,
|
||
probe: probe::Probe,
|
||
probe_ms: Option<Duration>,
|
||
}
|
||
|
||
/// 渲染后端探测 + 写 `ICED_BACKEND`,返回判据产物(供 `run` 决定要不要换进程重开)。
|
||
fn boot_probe(options: &Options) -> Boot {
|
||
// 「上一步失败后重开」的那一次:别再探(会得到同一个结论,然后无限重开)。兜底必须是 None
|
||
// ——由 `decision::fallback_round` 单点保证。
|
||
if let Some(kind) = renderer::forced_fallback() {
|
||
let decision = renderer::fallback_round(kind);
|
||
std::env::set_var("ICED_BACKEND", kind.env_value());
|
||
eprintln!(
|
||
"[probe] 本次是降级重开({}={}):跳过探测 -> ICED_BACKEND={}",
|
||
renderer::FALLBACK_ENV,
|
||
kind.env_value(),
|
||
kind.env_value()
|
||
);
|
||
eprintln!("[probe] {}", decision.describe());
|
||
// 验收要 `[env]` 里的真实适配器:这一轮**也枚举一次,并如实计时**(没计时就 `None`
|
||
// ⇒ 日志写 `probe=skipped`,不写 0ms)。
|
||
let (probe, probe_ms) = if acceptance::enabled() {
|
||
let started = Instant::now();
|
||
let probe = probe::Probe::run();
|
||
(probe, Some(started.elapsed()))
|
||
} else {
|
||
(probe::Probe::default(), None)
|
||
};
|
||
return Boot {
|
||
decision,
|
||
probe,
|
||
probe_ms,
|
||
};
|
||
}
|
||
|
||
// 只有 `auto` 需要探测(`cpu`/`gpu` 的决策与硬件无关,枚举 13 个适配器本机要 0.65~1.2 s)。
|
||
// 验收模式例外:`[env]` 那行要 GPU 名,不管请求是哪个后端。
|
||
let needs_probe = options.backend == crate::Backend::Auto || acceptance::enabled();
|
||
let probe_started = Instant::now();
|
||
let probe = if needs_probe {
|
||
probe::Probe::run()
|
||
} else {
|
||
probe::Probe::default()
|
||
};
|
||
let probe_ms = needs_probe.then(|| probe_started.elapsed());
|
||
|
||
// `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") {
|
||
match probe_ms {
|
||
Some(elapsed) => eprintln!(
|
||
"[probe] 探测耗时 {:.0} ms(wgpu 实例 + 全后端适配器枚举)",
|
||
elapsed.as_secs_f64() * 1000.0
|
||
),
|
||
None => eprintln!(
|
||
"[probe] 探测耗时 跳过(--renderer={} 与硬件无关,未枚举)",
|
||
options.backend.name()
|
||
),
|
||
}
|
||
}
|
||
Boot {
|
||
decision,
|
||
probe,
|
||
probe_ms,
|
||
}
|
||
}
|
||
|
||
/// 默认字体(正文档解析出来的族)。
|
||
fn default_font(families: Families) -> iced::Font {
|
||
iced::Font {
|
||
family: iced::font::Family::Name(families.text),
|
||
..iced::Font::DEFAULT
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "boot/tests.rs"]
|
||
mod tests;
|