代码复审 p2-2items / 视觉复审 p2-2items 的修复轮(配色重拍等 M70 定案)。
M69 迁移(2 行)
- nav 轨宽:nav_rail_width(metrics) 函数体改用 metrics.nav_rail_width
- theme::STROKE_PX 改为 pub use unlucky_design::stroke::HAIRLINE as STROKE_PX
P2-1 零硬编码守门补洞(reviewer 造样 5 个漏网样本全部补上,并进自检正向断言)
- COLOR 调用表补 from_rgb8 / rgb8 / rgba(`from_rgb(` 子串匹配不到 `from_rgb8(` 是根因)
- 新增 `Color { r: .., g: .. }` 结构体字面量检查
- 尺寸规则遇到 Length::Fixed( / Padding::from( / Size::new( 往里剥一层再判数字
- 新增渲染层回归守门 pill_never_overrides_its_size_with_fill(禁 nav.rs 出现 center_*( ):
视觉复审 P2-1 的病根就是 center_*(Length::Fill) 把容器尺寸覆盖成 Fill
视觉 P2-1 药丸 88×32 → 56×32(实测 x=16..71)
- 去掉 center_x/center_y(Fill);居中改由父 column 的 align_x + 显式 width(轨宽) 完成
(只去掉 center_* 还不够:column 会 shrink 到 56,药丸贴左缘 x=0..55,是同一个坑的第二层)
视觉 P2-2 / 用户点名 caption 图标 46 → 24(按钮容器保持 46×48)
- button 内包 container(...).width(24).height(24).center(Length::Fill):
只设 width/height 会让容器缩到 24 并贴 button 内容左上角;center(Fill) 才既撑满又居中
视觉 P3-1 东侧/南侧缩放热区改成叠层
- 原来是 row 的兄弟列,吃掉 4px 布局宽(右留白 28、关闭键离窗角 4px);
现在用 stack! + 对齐容器叠在工作区上,布局宽回到 1180(卡片右缘 x=1155 = 右留白 24)
P3 清单
1. --data-dir 接受并忽略(翻牌前 CI 兼容;现役 smoke 命令行可原样跑,exit 0)+ 单测
2. --diff 内建 ±N px 平移扫描(--diff-scan),CI 不再依赖 numpy
3. 截图模式等 SystemTheme 到达才出图(否则暗色系统上会拍到浅色)
4. (Auto, 未编入 wgpu) 返回 tiny-skia,不再把 wgpu 写进候选列表
5. --renderer=gpu 也跳过适配器枚举(决策与探测无关,省 ~0.6~1.2s)
6. icons::handle 加 debug_assert_eq!(IconKind::ALL[kind as usize], kind)
7. 轨顶留白改 container padding-top,实测 24(原来 spacer + column spacing = 28)
顺带:shot.rs 拆成 shot/mod.rs + shot/diff.rs(≤400 行门禁)
门禁:fmt OK / clippy -D warnings 干净 / 46+5 测试全过 / cargo check --workspace --all-targets OK
体积:双后端 6,769,664 B(6.46 MiB)、仅 CPU 3,955,712 B(3.77 MiB)
截图:r1 代码验证图(r1-verify / hover / pressed / compare-nav-rail)已出;4 张交付主图待 M70 定案后重拍
239 lines
8.8 KiB
Rust
239 lines
8.8 KiB
Rust
//! 渲染后端探测 + 自动降级(D27)。
|
||
//!
|
||
//! ## 为什么需要探测
|
||
//!
|
||
//! iced 0.14 的 `Renderer` 本身就是「wgpu → tiny-skia」的运行时回退枚举(M60 Q1):
|
||
//! **不设** `ICED_BACKEND` 时,拿不到 GPU 适配器会自动降级到 CPU,零代码。
|
||
//! 需要自研的只有**判据**:
|
||
//!
|
||
//! - 本机会枚举出 `Microsoft Basic Render Driver [Cpu]`(= WARP 软光栅),
|
||
//! 所以「有没有适配器」不能当「有没有 GPU」(M60 实测),必须看 `device_type`。
|
||
//! - 没有硬件 GPU 时要**主动**把 CPU 放到候选首位(`tiny-skia,wgpu`),
|
||
//! 否则 wgpu 会先花时间尝试 WARP,观感与行为都难料。
|
||
//!
|
||
//! ## 语义订正(与 M60 备忘录的差异,以源码 + 实测为准)
|
||
//!
|
||
//! M60 记「`ICED_BACKEND=wgpu` 会**关闭**回退」。读 `iced_renderer-0.14.0/src/fallback.rs`
|
||
//! 得到的是:**候选列表**语义——列表里没有 `tiny-skia` 时才真的关闭回退。
|
||
//! 所以本 crate 的「优先 GPU」写 `wgpu,tiny-skia`(首选 GPU,拿不到仍降级,**绝不硬失败**),
|
||
//! 而不是 `wgpu`。
|
||
|
||
use crate::Backend;
|
||
|
||
/// 单个适配器的摘要(只留日志要用的字段)。
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct AdapterSummary {
|
||
/// 适配器名(如 `AMD Radeon RX 6600 XT`)。
|
||
pub name: String,
|
||
/// `device_type` 的字符串形式(`DiscreteGpu` / `IntegratedGpu` / `Cpu` / …)。
|
||
pub device_type: &'static str,
|
||
}
|
||
|
||
/// 探测结论。
|
||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||
pub struct Probe {
|
||
/// 枚举到的全部适配器(含 WARP 这类软光栅)。
|
||
pub adapters: Vec<AdapterSummary>,
|
||
/// 是否存在**真**硬件适配器(`device_type` 既不是 `Cpu` 也不是 `Other`)。
|
||
pub hardware: bool,
|
||
}
|
||
|
||
impl Probe {
|
||
/// 探测本机渲染能力(编入 `wgpu` 时真的枚举适配器)。
|
||
#[cfg(feature = "gpu")]
|
||
pub fn run() -> Self {
|
||
run_wgpu()
|
||
}
|
||
|
||
/// 探测本机渲染能力(`--no-default-features` 的纯 CPU 构建:没有 wgpu,直接判定无硬件)。
|
||
#[cfg(not(feature = "gpu"))]
|
||
pub fn run() -> Self {
|
||
Self::default()
|
||
}
|
||
|
||
/// 一行日志:适配器清单 + 判定。
|
||
pub fn describe(&self) -> String {
|
||
if self.adapters.is_empty() {
|
||
return format!("adapters=0 hardware={}", self.hardware);
|
||
}
|
||
|
||
let list = self
|
||
.adapters
|
||
.iter()
|
||
.map(|adapter| format!("{} [{}]", adapter.name, adapter.device_type))
|
||
.collect::<Vec<_>>()
|
||
.join(" | ");
|
||
|
||
format!(
|
||
"adapters={} [{list}] hardware={}",
|
||
self.adapters.len(),
|
||
self.hardware
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 按请求与探测结果决定 `ICED_BACKEND` 的值(`None` = 不干预,交给 iced 自己回退)。
|
||
///
|
||
/// 纯函数,好让「降级矩阵」被测试钉死。
|
||
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"),
|
||
}
|
||
}
|
||
|
||
/// 把决策落到进程环境变量,并把探测结果 + 生效值打进日志。
|
||
///
|
||
/// 返回**实际生效**的后端描述(`"auto (iced: wgpu -> tiny-skia)"` 之类)。
|
||
pub fn apply(request: Backend, probe: &Probe) -> String {
|
||
let gpu_compiled = cfg!(feature = "gpu");
|
||
let env = backend_env(request, probe, gpu_compiled);
|
||
|
||
// 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()
|
||
};
|
||
|
||
let note = if !gpu_compiled && request != Backend::Cpu {
|
||
";本构建未编入 wgpu(--no-default-features),只能 CPU"
|
||
} else {
|
||
""
|
||
};
|
||
|
||
eprintln!(
|
||
"[probe] {} :: request={} -> ICED_BACKEND={}{note}",
|
||
probe.describe(),
|
||
request.name(),
|
||
env.unwrap_or("<unset>")
|
||
);
|
||
eprintln!("[probe] renderer = {effective}");
|
||
|
||
effective
|
||
}
|
||
|
||
#[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};
|
||
|
||
let instance = Instance::new(&InstanceDescriptor {
|
||
backends: Backends::all(),
|
||
..Default::default()
|
||
});
|
||
|
||
let adapters = instance
|
||
.enumerate_adapters(Backends::all())
|
||
.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,
|
||
}
|
||
})
|
||
.collect::<Vec<_>>();
|
||
|
||
let hardware = adapters
|
||
.iter()
|
||
.any(|adapter| !matches!(adapter.device_type, "Cpu" | "Other"));
|
||
|
||
Probe { adapters, hardware }
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn probe(types: &[&'static str]) -> Probe {
|
||
let adapters = types
|
||
.iter()
|
||
.map(|device_type| AdapterSummary {
|
||
name: "adapter".to_owned(),
|
||
device_type,
|
||
})
|
||
.collect::<Vec<_>>();
|
||
Probe {
|
||
adapters,
|
||
hardware: types
|
||
.iter()
|
||
.any(|device_type| !matches!(*device_type, "Cpu" | "Other")),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn warp_only_machine_counts_as_no_gpu() {
|
||
// 本机实测会枚举出 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")
|
||
);
|
||
}
|
||
|
||
#[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();
|
||
assert!(text.contains("DiscreteGpu"), "{text}");
|
||
assert!(text.contains("Cpu"), "{text}");
|
||
assert!(text.contains("hardware=true"), "{text}");
|
||
}
|
||
}
|