复审结论 p2-1/1 item + fix-then-merge:主线成立,但「启动器自己因 GPU / 渲染后端起不来」的 事故原因仍只进 stderr —— 双击启动没有 stderr,那条路径上等于没有日志。 - renderer/mod.rs::report_failure 的两行(`[gpu] <backend> 初始化失败(具名)` / `[gpu] 失败详情(具名)`)改走 diag!; - renderer/log_bridge.rs 转出的 wgpu 日志(含 `…Selected: AdapterInfo…`,GPU 真生效的唯一证据 行)改走 diag!; - renderer/preflight.rs 的 `[gpu-preflight]`(ULL_PREFLIGHT=1)改走 diag!; - probe.rs::apply_with_preflight 的三行 `[probe]` 改走 diag!。 - app/diag.rs 模块文档 + docs/logs-location.md §3 的判据改准: 「逐帧度量不接;显式取证开关与一次性事故行可接」,并修掉 `[corner]` 两处自相矛盾 (唯一打印点在 app/update/appearance.rs,走 diag! ⇒ 进日志;corner.rs 只 format_line)。 - 新守门 renderer::tests::a_backend_startup_failure_lands_in_the_launcher_log: 断言那两行真的落进 launcher.log;变异(改回纯 eprintln)→ 红。 - README 两语的 launcher.log 行补上「渲染后端起不来的具名原因」。
249 lines
8.7 KiB
Rust
249 lines
8.7 KiB
Rust
//! 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;
|
||
// M188:显式开关(`ULL_PREFLIGHT=1`)下的一次性行,按 `app::diag` 的判据可以进
|
||
// `launcher.log`——用户开这个开关就是为了拿到「为什么起不来」的证据。
|
||
match &verdict {
|
||
Ok(ok) => {
|
||
crate::app::diag::diag!("[gpu-preflight] {} took={took:.0}ms", ok.describe());
|
||
}
|
||
Err(failure) => {
|
||
crate::app::diag::diag!("[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);
|
||
}
|
||
}
|