release: v0.2.4 里程碑快照(孤儿提交,无父;树 = dev 68362e5)
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
本批内容见 CHANGELOG.md 的 0.2.4 段。按 D41 用孤儿快照推 main,保留「公开史只有里程碑」的模型。
This commit is contained in:
commit
a5d1dec495
1580 files changed
+320938
No files matched your search
@@ -0,0 +1,78 @@
|
||||
# 后端 × 中文:什么断言只在软渲染下成立,什么必须在 GPU 下看(M187)
|
||||
|
||||
本文件登记一条**通道级缺口**,不是一条 bug:自动化证据全部走软件渲染,所以「只有 GPU 后端
|
||||
才出现」的缺陷在 CI 里永远是盲区。下次有人要验「文字/像素在 GPU 上是否正常」时,先读这一页。
|
||||
|
||||
## 1. 现状:所有自动出图都是 `--software-renderer`
|
||||
|
||||
| 通道 | 位置 | 后端 |
|
||||
|---|---|---|
|
||||
| CI 冒烟截屏 | `.github/workflows/ci.yml:101` | `--software-renderer`(强制 CPU) |
|
||||
| CI 交互冒烟 | `.github/workflows/ci.yml:246`、`:283` | `--software-renderer` |
|
||||
| Linux 容器冒烟 | `packaging/linux-smoke-docker.sh:90` | `--software-renderer` |
|
||||
| D30 视觉验收(人工,按协议出图) | `DevDocs/PlanDoc/`(视觉验收协议) | 同上(CI 同款命令) |
|
||||
|
||||
`--renderer=auto`(默认)会挑 wgpu;`--software-renderer` 强制 tiny-skia。CI 走后者是**有意的**:
|
||||
容器/无 GPU 机器上没有 Vulkan/DX12 设备,硬要 wgpu 会红在一句「adapter 创建失败」上,与皮肤无关。
|
||||
|
||||
**代价**:tiny-skia 与 wgpu 是两套完全不同的文字光栅化路径(前者 `cosmic-text` + CPU 位图上传,
|
||||
后者经 `cryoglyph` 上传到 GPU 字形图集)。所以「GPU 侧字形图集出错」这类缺陷,**任何自动证据都抓不到**。
|
||||
|
||||
## 2. 这轮实测:M190 的「owned 文字在 wgpu 下丢字」在本 branch 上**没有复现**
|
||||
|
||||
同一份构建、同一串中文、同一字号,一次 wgpu 一次 tiny-skia:
|
||||
|
||||
| 图 | 后端 | 内容 |
|
||||
|---|---|---|
|
||||
| `DevDocs/artifacts/m187/ab-wgpu.png` | wgpu(Vulkan,AMD RX 6600 XT) | 四行:`text_role_owned` / `text_role` × 大字号 / 正文字号,逐字相同 |
|
||||
| `DevDocs/artifacts/m187/ab-tinyskia.png` | tiny-skia(`--software-renderer`) | 同上,与 wgpu 那张逐字一致 |
|
||||
| `DevDocs/artifacts/m187/probe-acc-wgpu.png` | wgpu | 账户页设备码面板(M190 钉的那一行「剩 14:59」在 wgpu 下**正常画出**) |
|
||||
|
||||
结论(只对这份构建、这台机器成立,不推广):`frame::text_role_owned` 与 `frame::text_role`
|
||||
产出的控件在小规模中文下**行为一致**,两种后端都能画;把缺陷归因到「owned 路径」在本 branch
|
||||
上没有证据支撑。要复现 M190 的现象,需要它当时的构建/环境。
|
||||
|
||||
## 3. 分工:哪些结论只信软渲染,哪些必须在 GPU 上看
|
||||
|
||||
| 断言类型 | 软渲染(tiny-skia)够吗 | 必须在 GPU 下复核 | 理由 |
|
||||
|---|---|---|---|
|
||||
| 版面几何(位置、宽高、折行、间距) | ✅ 够 | 建议一次 | 几何由布局树决定,两后端共用 |
|
||||
| 文字**在不在**(整行缺失 / 空块) | ⚠️ 只能证明 CPU 路径 | **必须** | GPU 字形图集是独立实现(`cryoglyph`) |
|
||||
| 字形形状 / 连字 / 每字形字体落点(「个别字换了字体」) | ⚠️ 同上 | **必须** | 两种后备字体选择可能不同 |
|
||||
| 颜色 / 对比度 / 主题令牌 | ✅ 够(纯色由令牌给) | 建议一次 | 面板混合色只有 GPU 路径可验 |
|
||||
| 材质层 / 高程阴影 | ⚠️ | **必须** | 阴影在 wgpu 与 tiny-skia 是两套画法 |
|
||||
| 整窗 α / 无边框圆角(Win32) | ❌ 不看 | **必须**(且只在 Windows) | 与渲染后端无关,靠 DWM 下发 |
|
||||
| 背景图 / 壁纸取色 / 图片缩放 | ⚠️ | **必须** | GPU 侧走纹理采样 |
|
||||
| SVG 图标(含描边宽度) | ✅ 够 | 建议一次 | 光栅化后端不同 |
|
||||
|
||||
读法:**「有没有画出来」与「字形对不对」这两类,只认 GPU 图**;几何与令牌只认一份即可。
|
||||
|
||||
## 4. 怎么在 GPU 下出图(本机命令)
|
||||
|
||||
```bash
|
||||
# GPU(wgpu;会自动挑适配器,日志里 [gpu] Selected: AdapterInfo{…} 是「真用上 GPU」的实证)
|
||||
unlucky-launcher-iced --screenshot out-gpu.png --screenshot-theme=light
|
||||
|
||||
# CPU(tiny-skia)
|
||||
unlucky-launcher-iced --screenshot out-cpu.png --screenshot-theme=light --software-renderer
|
||||
|
||||
# 两图逐像素比对(本程序自带,不需要 Python/PIL)
|
||||
unlucky-launcher-iced --diff out-gpu.png out-cpu.png
|
||||
```
|
||||
|
||||
`--diff` 的退出码:`0` 一致 / `1` 有差异 / `2` 参数错。**几何应当一致**(D30 的硬项),
|
||||
颜色与字形抗锯齿允许不同——所以「有差异」不等于坏,要**看着图**判。
|
||||
|
||||
启动失败弹窗的两态各两张(wgpu / tiny-skia)就是这么出的:
|
||||
|
||||
```bash
|
||||
ULL_DIALOG_FIXTURE=crash-report unlucky-launcher-iced --screenshot dialog-crash-wgpu.png
|
||||
ULL_DIALOG_FIXTURE=no-logs unlucky-launcher-iced --screenshot dialog-no-logs-wgpu.png
|
||||
```
|
||||
|
||||
## 5. 机器条件(写下来,免得下次怀疑「没 GPU」)
|
||||
|
||||
本机 `--renderer=auto` 的实测选择:`AMD Radeon RX 6600 XT` / Vulkan / `device_type: DiscreteGpu`。
|
||||
无独显的机器上 wgpu 会落到 `Microsoft Basic Render Driver`(CPU 实现,`device_type: Cpu`)——
|
||||
那种图**不能**算 GPU 证据(它仍是 CPU 光栅化,只是走了 DX12 接口)。
|
||||
判据:看 `[gpu] Selected:` 那行的 `device_type`,`Cpu` 一律不算。
|
||||
@@ -0,0 +1,197 @@
|
||||
# 下载设置到底改了谁(M193)
|
||||
|
||||
一句话:**`download.concurrency` / `download.retry_limit` / `download.gh_proxy` 现在真的进引擎**;
|
||||
其余四个 `download.*` 键(`region` / `mirror_preference` / `proxy_mode` / `proxy_url`)**引擎侧零消费者**,
|
||||
界面上改了不会改变任何下载行为——这一页把它们逐条标出来,别再让「设置项说假话」。
|
||||
|
||||
出处:M190 的 204 条 finding 复核(`DevDocs/PlanDoc/research/191-finding-triage.md` §3 A-3)挖出的
|
||||
「用户能改、改了没用」缺口:schema 说并发 8,引擎恒跑 64,两处各写一个数。
|
||||
|
||||
## 1. 取证:八个下载类键的真实生效值
|
||||
|
||||
「真实生效值」是**改造前**(M193 之前)代码里实际会用的值,不是界面上写的值。
|
||||
|
||||
| 键 | schema 默认 / 范围 | 引擎侧消费者 | 真实生效值(改造前) | 本轮 |
|
||||
|---|---|---|---|---|
|
||||
| `download.concurrency` | 8 / 1–256 | **无**(恒用 `EngineConfig::default()`) | **64**(`Semaphore` 容量) | ✅ 接进引擎,范围收成 1–64 |
|
||||
| `download.retry_limit` | 3 / 0–10 | **无** | 3(碰巧与 schema 相同,但改它无效) | ✅ 接进引擎,范围 0–10(0 与 1 都 = 只试一次) |
|
||||
| `download.gh_proxy` | 空串 / ≤256 字符(M197 登记) | 共享引擎装配点(`shared::build` 的 gh-proxy 候选链)+ CLI 的 `download::tuning` | 地区档案里的内置值(镜像优先时 `gh-proxy.com`) | ✅ 下沉到共享装配点,GUI 与四条 CLI 命令一致 |
|
||||
| `download.region` | auto / auto·cn·global | **无**(`RegionProfile::pinned` 无生产调用点) | 永远按「时区+语言」自动判定 | ❌ 未接(见 §5) |
|
||||
| `download.mirror_preference` | auto / auto·official·mirror | **无** | 永远按地区模式(镜像优先/直连) | ❌ 未接(见 §5) |
|
||||
| `download.proxy_mode` | none / none·system·manual | **无**(`http::build_client` 从不调 `.proxy()`) | 只走环境变量代理(reqwest 默认),`system`/`manual` 都不生效 | ❌ 未接(见 §5) |
|
||||
| `download.proxy_url` | "" | **无** | 空(从不使用) | ❌ 未接(见 §5) |
|
||||
| `download.action.detect_region` | (动作键,不是设置) | presenter 的 `detect_region` | 清 `region.json` + 失效共享引擎 | ✅ 本来就真 |
|
||||
|
||||
复现方法(改造前):`grep -rn "download.concurrency" crates/` 只命中 `setting/keys.rs` 的定义与
|
||||
agent 的测试;同理 `download.proxy_mode` / `proxy_url` / `mirror_preference` 只有 presenter 的显示行。
|
||||
|
||||
## 2. 唯一来源:默认值谁说了算
|
||||
|
||||
```text
|
||||
crates/core/src/download/config.rs
|
||||
DEFAULT_MAX_CONCURRENCY = 8 ← 唯一写死数字的地方
|
||||
MAX_CONCURRENCY = 64 ← 硬上限
|
||||
DEFAULT_MAX_ATTEMPTS_PER_SOURCE = 3
|
||||
MAX_ATTEMPTS_PER_SOURCE = 10
|
||||
│
|
||||
├─→ EngineConfig::default() (引擎默认)
|
||||
├─→ DownloadTuning::default() (宿主没灌任何值时)
|
||||
└─→ setting::keys 的 schema 默认值与范围 (界面/AI/CLI 看到的契约)
|
||||
```
|
||||
|
||||
守卫 `the_schema_contract_is_single_sourced`(`download/config.rs`,它合并了原先的「默认值相等」与
|
||||
「区间同源」两条)让「改一处另一个不变」直接变红——这是防 8 vs 64 复发的唯一手段。
|
||||
另有 `the_key_docs_quote_the_real_limits` 钉住 schema 描述里的数字(见 §6.1.1 表格的第三行)。
|
||||
|
||||
## 3. 接线链(宿主 → 引擎)
|
||||
|
||||
```text
|
||||
config/launcher.json
|
||||
download.concurrency ─┐
|
||||
download.retry_limit ─┼→ presenter::host::settings::download::tuning_of
|
||||
download.gh_proxy ────┘ │ DownloadTuning::from_settings(默认值/夹取/回落都在 core)
|
||||
↓
|
||||
shared::set_tuning(<cache 根>, tuning) 变了吗?没变就什么都不做
|
||||
↓ 变了 → 淘汰该根的登记项
|
||||
shared::engine_for(<cache 根>) → shared::build
|
||||
↓
|
||||
EngineConfig { max_concurrency, max_attempts_per_source } + RegionProfile.gh_proxy
|
||||
↓
|
||||
Semaphore(容量) / attempts_for() / MirrorTable::expand() 的 gh-proxy 候选
|
||||
```
|
||||
|
||||
**同步点是显式的**,两处,共用同一个函数 `apply_tuning`:
|
||||
|
||||
* `presenter/src/host/engine.rs::engine()` —— 每次取引擎前(幂等,值没变就一行不动);
|
||||
* `presenter/src/host/dispatch.rs` 的 `SetSetting` 分支 —— 用户改完设置立刻生效,并让常驻持有
|
||||
引擎的自更新 `Factory` 换一份(`refresh_update_factory`)。
|
||||
|
||||
调优按**数据根**存(与引擎登记处同键),不是进程级:同一进程里多个数据根时互不串味。
|
||||
|
||||
## 3.1 哪些路径吃这套设置
|
||||
|
||||
**四条 CLI 命令与 GUI 都吃**(M203 起。M193 时期这张表把三条 CLI 记成「自建裸引擎、零消费者」,那已不成立——教训见本节末)。
|
||||
|
||||
| 路径 | 入口 | 吃设置吗 |
|
||||
|---|---|---|
|
||||
| GUI 的**全部**下载:内容库(Modrinth/CurseForge)、版本清单、资源/库/日志文件、字体、JDK 安装、自更新 | `presenter::host::engine::engine` → `core::download::shared::engine_for` | ✅ 吃(并发/尝试次数/gh-proxy 都吃) |
|
||||
| CLI `ull install` | `crates/cli/src/commands/install/mod.rs:78` 的 `build_engine` → `crates/cli/src/download.rs::engine` | ✅ 吃(并发 / 尝试次数 / gh-proxy) |
|
||||
| CLI `ull java` | `crates/cli/src/commands/java.rs:312` 同上 | ✅ 吃(并发 + 尝试次数 + gh-proxy) |
|
||||
| CLI `ull pack` | `crates/cli/src/commands/pack.rs:286` 同上 | ✅ 吃(并发 / 尝试次数 / gh-proxy) |
|
||||
| CLI `ull update`(**自更新路径**) | `crates/cli/src/commands/update.rs:64` 复用 `install::build_engine` | ✅ 吃(**并发这条路径上不可观测**:自更新只下**一个** artifact,闸门开 1 还是 8 都只有一条连接;真正随设置变的是每源尝试次数与 gh-proxy 前缀) |
|
||||
|
||||
四条 CLI 路径共用同一次装配:`crates/cli/src/download.rs` 从 **CLI 自己的** `launcher.json`(与
|
||||
`ull config set` 同一份文件)读三个 `download.*` 键 → `DownloadTuning::from_settings`(默认值/夹取/回落
|
||||
都在 core)→ `shared::engine_with_tuning(<cache 根>, tuning)`。所以 `presenter` 的 `shared::build` 不再是
|
||||
**全仓唯一**的装配点,但装配规则仍只有 core 那一份。
|
||||
|
||||
回落仍是静默的:`SettingStore::load` 永不失败(坏值只进 `problems()`),类型不对 → `None`,越界 → 出厂
|
||||
值,不报错、不中断安装。历史教训保留在此:M193 把出厂并发从 64 改成 8 之后,当时还在用裸
|
||||
`EngineConfig::default()` 的那几条命令**静默**跟着降了——「用出厂值」和「读到了设置」在输出上一模一样,
|
||||
这正是 §6 里三条 CLI 变异要抓的东西。
|
||||
|
||||
## 4. 三态语义:`download.gh_proxy`
|
||||
|
||||
| 值 | 含义 | 效果 |
|
||||
|---|---|---|
|
||||
| 空串(出厂) | **没有覆盖**,跟随内置默认 | 镜像优先/国内判定时套 `mirrors.json` 里的 `gh-proxy.com` |
|
||||
| `off`(也认 `none`/`direct`/`disable`/`disabled`/`no`) | 明确关掉 | 一个 gh-proxy 候选都不生成 |
|
||||
| 其它非空 | 覆盖前缀 | 用它替换内置前缀(类别仍只在镜像优先/国内判定时启用) |
|
||||
|
||||
**空串 ≠ 关**:「我还没决定」和「我明确不要」是两件事。设置页的说明文案(presenter 行 + iced 行)
|
||||
两档必须都写出来——这是最容易让用户以为「关掉了、其实还在套前缀」的地方。
|
||||
|
||||
边界:非法值(负数、超上限、手改坏值)**一律回落出厂默认**,不报错、不阻断下载;schema 在写入口
|
||||
就拒绝越界值,引擎侧再夹一次是纵深防御(老文件里可能躺着旧 schema 允许的大数)。
|
||||
|
||||
**`download.retry_limit = 0` 是合法值**(P2-1 复审的结论):旧版 GUI 的档位表里就有 `0`,所以
|
||||
范围保留 `0~10`,语义定为 **`0` 与 `1` 都 = 只试一次、不重试**(`attempts = max(1, value)`)。
|
||||
这样老配置零迁移、`ull config get` 读到的 0 与引擎真做的事一致(试 1 次),不会再出现
|
||||
「界面显示 0、引擎按 3 跑」。
|
||||
|
||||
## 5. 没接的四个键(已登记为 finding)
|
||||
|
||||
`download.region` / `download.mirror_preference` / `download.proxy_mode` / `download.proxy_url`
|
||||
**引擎侧零消费者**:界面上改了不改变任何下载行为。要接它们分别需要:
|
||||
|
||||
* `region` → `RegionProfile::pinned(mode, now)`(值 `cn`/`global` 映射到 `MirrorFirst`/`Direct`,
|
||||
并把 `trigger` 置成 `UserPinned` 让 24h 重判不覆盖它);
|
||||
* `mirror_preference` → 引擎候选排序里插一档「强制官方优先 / 强制镜像优先」;
|
||||
* `proxy_mode` / `proxy_url` → `http::build_client` 里加 `.proxy(reqwest::Proxy::all(url)?)`。
|
||||
|
||||
三条都在 M193 的 scope 外(`http.rs` / 引擎候选排序 / 一处新装配),已单独立 finding。
|
||||
|
||||
## 6. 守卫与变异证明(逐条只改实现侧,跑完即还原)
|
||||
|
||||
| # | 变异(只改实现侧) | 守卫用例 | 结果 |
|
||||
|---|---|---|---|
|
||||
| M1 | `shared::build` 的 `tune.apply_to(...)` 换回裸 `EngineConfig::default()` | `download::shared::tests::the_setting_and_the_gh_proxy…` | 红(探针 8,期望 2) |
|
||||
| M2 | `shared::build` 的 `tune.apply_gh_proxy(region)` 换回 `region` | 同上 | 红(`off` 档仍有 gh-proxy 候选) |
|
||||
| M3 | 删掉 `set_tuning` 里的 `registry().remove` | 同上 | 红(换参数后交回旧引擎) |
|
||||
| M4 | 删掉构造点的 `clamp(1, MAX_CONCURRENCY)` | 同上 | 红(9999 张许可真开了) |
|
||||
| M5 | `EngineConfig::default()` 的并发写回字面量 64 | `download::config::tests::the_schema_contract_is_single_sourced` | 红 |
|
||||
| M6 | schema 的并发默认值写成字面量 16 | 同上 | 红(两处各写一个数) |
|
||||
| M7 | 删掉 `host/engine.rs::engine` 里的 `apply_tuning` | `host::settings::tests::the_concurrency_setting…` | 红(探针 8,期望 2) |
|
||||
| M8 | 下载设置页的 gh-proxy 行改回 `("Preferred source", "Automatic (region)")` | `the_downloads_rows_state_semantics…` | 红 |
|
||||
| M9 | `from_settings` 的尝试次数去掉 `.max(1)`(0 原样交给引擎) | `download::config::tests::missing_or_illegal_values_fall_back_to_the_defaults` | 红(`left: 0 / right: 1`) |
|
||||
| M10 | schema 描述里的「1~64」改成「1~99」(第三处字面量) | `download::config::tests::the_key_docs_quote_the_real_limits` | 红 |
|
||||
| M11 | `commands::pack::build_engine` 换回裸 `EngineConfig::default()`(其余三条不动) | `cli::download::tests::pack_engine_takes_the_concurrency_from_the_setting_store` | 红(探针 8 ≠ 2),另四条 CLI 用例绿 |
|
||||
| M12 | `commands::update::engine` 换回裸 `EngineConfig::default()` | `cli::download::tests::update_engine_takes_the_concurrency_from_the_setting_store` | 红(8 ≠ 4),另四条绿 |
|
||||
| M13 | `download::tuning` 不读 `KEY_GH_PROXY`(第三参传 `None`) | `cli::download::tests::the_gh_proxy_setting_reaches_the_candidate_chain` | 红(`off` 档仍有 gh-proxy 候选) |
|
||||
|
||||
## 6.1 gh-proxy 那一行的形状(P3-1):M201 之后已解决
|
||||
|
||||
M193 时期这一组把「GitHub 加速前缀」的三态画成三行(`GitHub 加速前缀 / 留空 = 跟随内置默认`、
|
||||
`关掉它 / 填 off = 关闭`、`换掉它 / 填 URL 前缀 = 覆盖`)。这是**已知的观感问题**:
|
||||
①`关掉它` / `换掉它` 读起来像按钮而不是取值说明;②同一个设置画成三行比三态本身更难懂。
|
||||
|
||||
当时的结论是「正确的家是下一条『iced 设置数据通路』mission」——**那一条已落地(M201)**,现状:
|
||||
|
||||
| 项 | M193 时期 | M201 之后 |
|
||||
|---|---|---|
|
||||
| 控件 | 三行语义行(`&'static str`) | **一行真控件**:标题 + 说明 + 输入框 + 「应用」按钮(`pages/settings/download.rs::text_row`) |
|
||||
| 三态怎么说 | 拆成三行、每行一句 | 一句话说完(留空 / off / 前缀),末尾接「当前生效:…」(presenter 的 `gh_proxy_state`,读的是**引擎生效档位**,不是文件原文,见 P3-5) |
|
||||
| 当前值 | 不显示 | 输入框里是快照里那个值(`States::settings` ← `CoreEvent::SettingsChanged`) |
|
||||
| 改它 | 改不了 | 输入 → 本地草稿 → 点「应用」→ `AppCommand::SetSetting` → host 写盘 + 重发快照 |
|
||||
|
||||
那三行**还在** `GROUP.rows` 里,但只在**没有快照**时画(harness / `pages_demo` / 首帧,走 `frame::demo_card`):
|
||||
它们说的是语义(「出厂 8 · 可调 1~64」)而不是当前值,所以不会再被当成界面上的真值(M193 用户投诉的形状)。
|
||||
有快照时这一组画的是真控件,`GROUP.rows` 一行都不画。
|
||||
|
||||
### 6.1.1 「数字的第二来源」现状(P3-2 登记,M207 复核)
|
||||
|
||||
| 位置 | 能不能引用常量 | 守卫 |
|
||||
|---|---|---|
|
||||
| `presenter/src/host/settings/download.rs` 的行描述 | 能(`format!` + `DEFAULT_MAX_CONCURRENCY` / `MAX_CONCURRENCY`) | 已改;**有快照时界面显示的就是这一份文案** |
|
||||
| `app-iced/src/pages/settings/download.rs` 的 `GROUP.rows` | 不能:`&'static [(&'static str, &'static str)]` 是编译期字面量,要改运行时生成得动 `Group` 的类型 | 只有「字符串还在」的探针(`the_downloads_rows_state_semantics_and_never_fake_a_current_value` 断言 `"1~64"` 在):改常量不会红 |
|
||||
| `setting/keys.rs` 的 `SettingSpec.doc` | 不能:`doc: &'static str`(`spec.rs:183`),`concat!` 只吃字面量、插不进常量 | ✅ `the_key_docs_quote_the_real_limits`(`config.rs`):把「1~64」「出厂 8」「出厂 3」与常量比对,改常量不改文案就红 |
|
||||
|
||||
第二行的**地位变了(M201)**:它不再是产品界面上的数字来源——有快照时界面画的是第一行那份(用常量
|
||||
`format!` 出来的),`GROUP.rows` 只在没有快照的兜底屏出现,而且那里的量词是「出厂 / 可调」而不是「当前」。
|
||||
于是它现在的风险是「兜底演示行与常量不一致(演示屏说谎)」,不是「设置页骗人」。
|
||||
|
||||
`SettingSpec.doc` 那一处**不是注释**:`catalog.rs` 会把它当 JSON schema 的 `description` 发出去
|
||||
(AI 工具与文档可见),所以它必须有守卫——这也是三处里唯一补上了守卫的一处。
|
||||
|
||||
## 7. 没验的部分
|
||||
|
||||
* **真实网络下的并发/重试行为未验**:守卫读的是 `Semaphore` 的真实许可数与 `attempts_for()` 的
|
||||
配置读点;「2 条真连接同时下载时服务器只看到 2 条」和「某个源真的被试 3 次」没有在真网络下跑过
|
||||
(离线回环能验连接复用,验不了限流的端到端时序)。
|
||||
* **macOS / Linux 未验**:本轮只在 Windows 上编译与跑测试。
|
||||
* **`download.gh_proxy` 的端到端只到「候选生成」为止**(键已合入,M197/M203 之后不再是「登记了但读不到」):
|
||||
三处各有守卫——core 的「调优 → 候选链」(`gh_proxy_override_reaches_the_profile_and_the_chain`)、
|
||||
CLI 的「文件 → 候选链」(`the_gh_proxy_setting_reaches_the_candidate_chain`)、GUI 的「文件 → 宿主 →
|
||||
core 三态」(`the_gh_proxy_key_travels_from_the_file_to_the_core_tri_state`)。**没验的是真网络下那个前缀
|
||||
能不能通**:离线只验候选链的生成,没发过一次真的 `gh-proxy.com` 请求。
|
||||
* **真机上的手点交互没验**(设置页自 M201 起已是真控件;M193 时期文档里那句「这一组只能看、改不了」已作废):
|
||||
Downloads 组的行来自 presenter 快照(`presenter::host::settings::groups` → `CoreEvent::SettingsChanged` →
|
||||
`pages::Model.settings`),画的是 choice chip / 文本输入 + 「应用」/ 动作按钮,写回走
|
||||
`AppCommand::SetSetting`;没有快照时(harness / `pages_demo` / 首帧)才退回 `GROUP.rows` 那几行语义演示行。
|
||||
本次(M207)只读了代码与单测,没在 GPU 窗口里点过 chip / 输入 / 应用,也没量过「点了以后界面何时变」
|
||||
(设计是**不做乐观回显**,只认 host 发回的快照——这一点同样没在真机上确认过)。
|
||||
* **UA 里的联系方式在真站点上的后果未知**(M207):`User-Agent` 现在只有一份定义
|
||||
(`core::download::http::USER_AGENT` = `Unlucky-Launcher/<CARGO_PKG_VERSION> (+仓库 URL)`),
|
||||
两条 HTTP 路径发的是同一个字符串。证据只在**离线回环**上(发出去的字节 ==
|
||||
`EngineConfig::default().user_agent`);**没有向 Modrinth / CurseForge 发过一条带新 UA 的真实请求**,
|
||||
限流阈值、CF 会不会因为多了 `(+URL)` 而拒、Modrinth 是否会因此放宽,全部未知。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -0,0 +1,94 @@
|
||||
# 日志在哪(M188)
|
||||
|
||||
一句话:**启动器自己写的东西全在 `<数据根>/logs/` 一个目录里**,游戏自己的东西在实例运行目录里。
|
||||
设置页那行「日志文件夹」显示的就是前者的绝对路径,旁边有 *打开文件夹* 与 *复制路径*;日志页页头
|
||||
也有 *打开文件夹*。两个入口走同一个落点(`crates/app-iced/src/app/logs_folder.rs`),失败时输出的
|
||||
文本带完整路径。
|
||||
|
||||
## 1. 三个数据模式下的绝对路径
|
||||
|
||||
`<数据根>` 由 `core::platform::data_dir` 的三模式解析(便携 / 标准 / 自定义):
|
||||
|
||||
| 模式 | 启动器数据目录(= `<数据根>`) | 日志根 |
|
||||
|---|---|---|
|
||||
| portable | `<exe 目录>/.ULL` | `<exe 目录>/.ULL/logs` |
|
||||
| standard | Windows `%APPDATA%\ULL` · macOS `~/Library/Application Support/ULL` · Linux `$XDG_DATA_HOME/ULL`(缺省 `~/.local/share/ULL`) | `<上者>/logs` |
|
||||
| custom | `<自定义根>/.ULL` | `<自定义根>/.ULL/logs` |
|
||||
|
||||
GUI 的 `--data-dir <dir>` 是 custom 模式的一种入口:那时日志根是 `<dir>/.ULL/logs`。设置页显示的
|
||||
路径与 `launcher.log` 的实际落点取的是**同一份解析结果**(`SettingsBridge` → `<home>/config/launcher.json`
|
||||
→ `<home>`),所以两者不会各说各话。
|
||||
|
||||
## 2. 目录里有什么
|
||||
|
||||
| 路径 | 写者 | 是什么 |
|
||||
|---|---|---|
|
||||
| `logs/launcher.log` | `core::launch::launcher_log` | 启动器自己的运行日志:启动拍(主题 / 语言 / 路由 / 窗口)、后端探测与降级、字体链、每一次启动的命令行事实 / spawn 结果 / 退出码、打开日志目录等用户动作的结果。 |
|
||||
| `logs/launcher.1.log` | 同上(滚动) | 上一份日志。**只留一份**历史。 |
|
||||
| `logs/game/<session-id>.jsonl` | `core::launch::session` | 每次启动一份 JSONL 事件流(命令行已脱敏、每行 stdout/stderr、退出、诊断)。 |
|
||||
| `logs/game/index.jsonl` | `core::launch::session_index` | 一会话一行摘要的索引,日志页左栏读它。 |
|
||||
| `logs/install.log` | `presenter::host::install_journal` | 安装流水,每阶段一行。 |
|
||||
| `logs/session-log-location.json` | `presenter::host::logs::note_location` | 会话日志现在在哪 / 以前在哪 / 搬没搬(`moved: 0`),每次启动重写。 |
|
||||
| `data/agent/sessions/game/…` | **旧版本** | 只读兼容,见 §4。 |
|
||||
|
||||
### 上限与滚动
|
||||
|
||||
- 单文件上限 `launcher_log::MAX_BYTES = 1 MiB`;每次追加前检查,超过就把 `launcher.log` 改名成
|
||||
`launcher.1.log`(旧的那份被替换),新文件从 0 开始。
|
||||
- 保留份数:**1**。所以 `logs/` 里启动器日志的稳态占用 ≤ 约 2 MiB(+ 同目录的会话 jsonl)。
|
||||
- 为什么不做按日期滚动:事故复现往往在「刚刚那一次」,份数比历史长度更重要;而无限份数会让「导出
|
||||
诊断包」变成搬砖。
|
||||
|
||||
## 3. 哪些行进 launcher.log、哪些只进 stderr
|
||||
|
||||
判据(复审后收紧成这一句):**逐帧度量不接;显式取证开关与一次性事故行可接**。实现是
|
||||
`crates/app-iced/src/app/diag.rs` 的 `diag!` 宏(stderr + 文件双写,行首 `[tag]` 就是日志里的 scope)。
|
||||
|
||||
| 进 launcher.log | 只进 stderr |
|
||||
|---|---|
|
||||
| 启动编排:`[probe]`(`request=… -> ICED_BACKEND=…`、降级重开、探测耗时)、`[font]`、`[theme]`、`[appearance]`、`[gpu]` 的降级与拒绝降级、`[boot]`(含后端 request/effective)、`[shot]` 的通道提示 | 验收/出图矩阵的**逐帧度量**:`[window]` / `[perf]` / `[caps]` / `[theme] source=…`(`renderer::acceptance/**`,一次启动上百行,会冲掉事故现场) |
|
||||
| **一次性事故行**:`[gpu] <backend> 初始化失败(具名)` 与 `[gpu] 失败详情(具名)`(`renderer::report_failure`,复审 P2-1)、`[gpu] <target>: …Selected: AdapterInfo…`(`renderer/log_bridge.rs` 转出的 wgpu 日志)、`[gpu-preflight]`(`ULL_PREFLIGHT=1`) | 组件几何探针的断言输出(`components/**` 的 `[feedback-probe]`、gallery 页面里的 `[gallery]`) |
|
||||
| 用户动作的结果:`[logs]`(打开日志文件夹成功/失败)、`[settings]` 写盘失败、`[i18n]` 语言没落盘、`[update]` 的忽略/重启、`[agent]` / `[content]` / `[page]` 的落点提示 | 每条日志行的正文里再套的括号(不是标签) |
|
||||
| 显式开关下的一次性行:`[tick]`(`ULL_TICK_TRACE=1`,≈1 行/秒,约 4.8 小时才滚一次)、`[corner]`(`app/update/appearance.rs` 打,`platform/win32/corner.rs` 只负责 `format_line`)、`[appearance]` / `[theme]` 的切换 | — |
|
||||
|
||||
`[corner]` 到底在哪一侧,看**打印点**:唯一打印点是 `app/update/appearance.rs`(走 `diag!`),
|
||||
`platform/win32/corner.rs::format_line` 只拼字符串、不打印 ⇒ 它**进** launcher.log。
|
||||
|
||||
## 3.1 还没接进日志的行(本 mission 之外)
|
||||
|
||||
- `renderer::acceptance/**` 的 `[window]` / `[perf]` / `[caps]`:逐帧/矩阵取证,按上面的判据**故意不接**。
|
||||
- `components/**` 的几何探针(`[feedback-probe]` / `[gallery]`):测试通道,不进产品日志。
|
||||
|
||||
(`renderer/mod.rs` 的 `report_failure`、`renderer/log_bridge.rs`、`renderer/preflight.rs`、
|
||||
`probe.rs:apply_with_preflight` 已在 M188 复审后接上。)
|
||||
|
||||
## 4. 会话日志搬家:策略是**不搬**
|
||||
|
||||
M188 把新会话从 `data/agent/sessions/game/` 挪到 `logs/game/`(目录名里的 `agent` 是历史遗留,
|
||||
它从来只装游戏会话)。旧目录的处理方式:
|
||||
|
||||
- **原样保留、继续读**:`presenter::host::logs` 与 `cli` 的会话读取都是两个根合并,同一个
|
||||
`session_id` 同时存在时以 `logs/` 那条为准;
|
||||
- **不搬、不删**:搬运用户已有数据是不可逆动作,中途失败会留下半份会话,崩溃弹窗里的会话关联也会断;
|
||||
- **留记录**:`logs/session-log-location.json` 写明两个根、旧根在不在、旧根里几个文件、`moved: 0`、
|
||||
`policy: "read-only-compat"`。
|
||||
|
||||
## 5. 游戏自己的日志(不归启动器写)
|
||||
|
||||
在实例的运行目录里(隔离实例是 `<游戏目录>/versions/<隔离名>/…`,共享实例直接是 `.minecraft`):
|
||||
|
||||
- `logs/latest.log`、`logs/<日期>-<序号>.log.gz` —— 游戏自己的日志;
|
||||
- `crash-reports/crash-*.txt` —— **只在 JVM 活到崩溃处理器那一步时才有**;
|
||||
- `hs_err_pid*.log` —— JVM 自己崩(native crash)时才写。
|
||||
|
||||
模组 / 缺库 / Java 选错把游戏弄死在 Minecraft 初始化之前时,上面三个都不会出现——那不是启动器
|
||||
丢了日志,而是游戏没机会写。那种时候的证据是 `logs/game/<session-id>.jsonl` 与 `logs/launcher.log`。
|
||||
|
||||
## 6. 没验的部分
|
||||
|
||||
- **真机「双击启动后日志确实在」**:本轮只在开发机上用 `cargo test` 与本地进程验证,Windows 桌面
|
||||
双击启动需要用户实测。
|
||||
- **macOS / Linux 的日志根**:路径由同一条解析链给出(`standard_launcher_home` 有单测覆盖三个平台),
|
||||
但「Mac 上点打开文件夹真的会开 Finder」「Linux 上没有 `xdg-open` 时会怎样」没有真机验证。
|
||||
- **只读数据目录**:日志写不进去时按设计静默放过(best-effort),此时设置页显示的路径仍然正确,
|
||||
但文件不存在。
|
||||
Reference in new issue
Block a user