feat(app-iced): M68 —— iced 0.14 骨架(后端探测/字体基座/自绘标题栏/导航轨/深浅主题/截图通道)
新增 crates/app-iced(package `unlucky-app-iced`,bin `unlucky-launcher-iced`),与 Slint 版并存, 为 D26「UI 从头写」打地基。本 mission 只做骨架:能开窗、颜色/字体对、层级在,不做业务页面。 - 后端(D27):启动探测 wgpu 适配器(只看 device_type,避开 WARP 误判)→ 写 ICED_BACKEND; auto/gpu/cpu 三条路径都保留 tiny-skia 兜底,绝不硬失败;`--renderer=cpu` 跳过探测(省 ~0.85s) - 字体(D28 前半):令牌三平台字体栈 → 用 iced 全局 FontSystem 查装机情况 → 族名与 400/500/700 字重落点(缺档按 Nearest/Synthetic 记录)打一行启动日志 - 令牌适配层 theme::Ui:唯一把 unlucky-design 的值翻成 iced Color/Font/Border/Radius/Theme 的地方; 49 个 M3 角色留在自己的 Scheme 里,iced 的 6 色 Palette 只给 devtools - 骨架:1180×760 无边框窗口、48 高自绘标题栏(drag + 东/南 drag_resize + 最小化/最大化/关闭)、 88 宽导航轨(56×32 SecondaryContainer 药丸,由令牌推导)、页头 56 + 一张 OutlinedCard - 主题:系统主题探测 + --theme,切主题只重建 Ui(Theme::name 变化触发重算),不动 UI 状态 - 截图通道:`--screenshot <png>` + `--screenshot-theme`(真开窗第 3 帧出图),以及 `--diff <a> <b>` 自带逐像素对比(CI 不依赖 Python/ImageMagick) - 图标:令牌 SVG path → iced svg 控件(颜色走 alpha 蒙版,深浅共用一份位图缓存) - 门禁:tests/no_hardcoded_tokens.rs 扫源码挡色值/尺寸字面量 + 挡 slint 依赖(自检也算过) 实测(Windows Server 2025 / 96 DPI / RX 6600 XT):浅/深 × GPU/CPU 四张真开窗截图;GPU vs CPU 差异 0.72% 像素、全部落在字形/图标抗锯齿与 1px 圆角描边、最佳对齐 (0,0)、平坦区颜色逐字节相同; release 体积双后端 6.44 MiB / 仅 CPU 3.75 MiB;首帧 GPU 1.18~1.31s、CPU 33~37ms。 未做/未验证:macOS 与 Wayland 全链路、西/北边缩放热区、letter spacing(iced 0.14 无此属性)、 中文 CDN 字体、玻璃;翻牌步骤写在 DevDocs/SubAgentsTalk/app-iced-骨架与翻牌.md(不入库)。
This commit is contained in:
1 parent
3fcdc3d3e1
commit
df4ff373b7
16 files changed
+4244
-97
No files matched your search
@@ -0,0 +1,238 @@
|
||||
//! 渲染后端探测 + 自动降级(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,
|
||||
// 自动 + 无硬件(含只有 WARP 的机器)/ 没编进 wgpu:CPU 起步。
|
||||
(Backend::Auto, _) => 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
|
||||
assert_eq!(backend_env(Backend::Gpu, &gpu, false), Some("tiny-skia"));
|
||||
assert_eq!(
|
||||
backend_env(Backend::Auto, &gpu, false),
|
||||
Some("tiny-skia,wgpu")
|
||||
);
|
||||
}
|
||||
|
||||
#[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}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user