CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
CI / runtime smoke (ubuntu x64, xvfb) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
首个 iced + Material 3 Expressive 版本的源码快照。 完整历史与日常提交在 dev 分支;main 只放阶段性快照。
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;
|