## 为什么
用户 2026-10-29 要求:按既定计划完整实现改进,但
① **不得把编译产物拆成 DLL**(项目定位是单文件轻量工具,最终只能有一个 .exe);
② 异常处理改用**预检验**而不是 try/catch(-fno-exceptions 省体积,且 try/catch 本身开销大);
③ 先删除 Documents 内三份 UI 重构文档再按"计划 / 当前进度 / 最终报告"重建;
④ 顺便审计代码,删除无用/过时注释,修复名实不符;
⑤ 提交到本地仓库,期间不要占用用户的鼠标/键盘/屏幕。
## 改了什么
### 1. 体积(909,824 B -> 788,992 B,-120,832 B,-13.3%)
Makefile 新增三个可覆盖开关(默认全为 1):
- `LAYOUT_BOUNDS` 编入/剔除「显示布局边界」叠加层
- `NO_EXCEPTIONS` 追加 `-fno-exceptions`
- `LTO` 追加 `-flto`(编译 + 链接)
派生:`FUNC_SECTIONS`(LTO 下为空)、`GC_SECTIONS`(`-Wl,--gc-sections`)、
`ASM_LISTING`/`ASM_LISTING_C`(LTO 下为空,因为 lto-wrapper 会丢弃 `-Wa` 清单)。
两个实测复现的坑(已写进 Makefile 注释):
- **切换 LTO 必须 `make clean`**:LTO 与非 LTO 目标文件混链会让 GCC 15.2.0 在
`binds_to_current_def_p (symtab.cc:2598)` 处 ICE。
- **`-flto` 与 `-ffunction-sections -fdata-sections` 同时打开也 ICE**(干净重编也必崩,
报错落点是 `DataModel.hpp:393`,但那是 COMDAT 解析落点、不是原因)⇒
LTO 下 `FUNC_SECTIONS` 置空,并加 `-flto-partition=one`:默认 11 个分区的 LTRANS
稳定复现,单分区从未复现。**这是稳定性选项,不是性能选项。**
### 2. 关异常必须成对交付:预检验 + 兜底
新增 `Utilities/Inc/MemProbe.hpp` + `Utilities/Src/MemProbe.cpp`:
- `MemAvailablePhysical()`:`GlobalMemoryStatusEx` + 200ms 缓存
- `MemCanAllocate(bytes)`:预留后仍须保有 1/8 可用内存
- `MemReserveIfRoom(container, n)`:含 `capacity()` 早退与乘法溢出保护
- `MemInstallTerminateHandler()`:OOM 时弹一次带建议的模态提示后 `_Exit`(而不是静默消失)
接入:
- `Utilities/Src/DataModel.cpp`:全仓**唯一**的 `try/catch`(`extremes.reserve` 捕
`std::bad_alloc`)改为 `MemReserveIfRoom(extremes, count / 8)`
- `Components/PlotView/Src/PlotView.cpp`:`AvailablePhysicalBytes()` 原有一份**完整重复**
的 `GlobalMemoryStatusEx` + 缓存实现 → 转发 `MemAvailablePhysical()`
- `Components/Application/Src/Application.cpp`:`Initialize()` 开头安装 terminate handler
### 3. L1 几何产物层
新增 `Components/Shared/Inc/UiBox.hpp` + `Src/UiBox.cpp`:
- `struct UiBox { rect, clip, depth, layer }` —— 纯值类型,不继承任何东西
(继承会和 PlotView 4 层位图缓存 / SignalPanel 行位图缓存的失效体系打架)
- `class UiBoxTable : public UiTextRectSink` —— 只累计几何 + 裁剪/层/遮挡,**不含绘制代码**
`UiComponent::CollectDebugBoxes` → **`virtual void BuildBoxes(UiBoxTable&)`**:
组件从"收集布局边界(调试用)"改为"上报几何产物",不再需要知道"调试"这个概念存在。
`UiDebugBounds.hpp/.cpp` 退化为唯一的消费者(配色 + 包角描边 + 遮挡挖洞)。
除宿主 `ApplicationCommands.cpp` 外,所有组件源码现在只包含 `UiBox.hpp`。
### 4. 审计
- `MemProbe` 里"省 40~60KB"的估算 → 改为实测 27,136 B
- `MemProbe.hpp` 的 `<new>`、`MemProbe.cpp` 的 `<cstdio>`:未使用,删除
- `UiDebugPaintBounds(g, rect, depth)`:零调用者,删除(只保留带 clip 的版本)
- `TabBar`/`Toolbar`/`PlotView`/`SignalPanel` 的 `BuildBoxes` 文档"(调试用)"→"几何 box"
- `PlotView::SetForceFullRepaint` 补写"为什么保留而不优化"
### 5. 文档
`Documents/UI_Refactor_Plan.md` / `_Progress.md` / `_Report.md` 先 `git rm` 再按
「计划 / 当前进度 / 最终报告」重建;`Documents/agent_mem.md` 追加第十三轮记录。
## 怎么验证
```
make clean
make release TARGET_FILE_NAME=JustDataInspector -j16 # 零 error / 零 warning,788,992 B
make debug TARGET_FILE_NAME=JustDataInspectorD -j16 # 零 warning
uicheck 69/69 lbcheck 21/21 lbresid 5/5 lbzorder 4/4 lbcovr 7/7 lbpanel 10/10
rendercheck: 8 个场景哈希与改造前逐一相同 + FUNC 7/7 PASS, exit=0
A FAED59DF284153DB B E6443BFE4E3955F8 C 96A0AA8C402F1729 D E3D5DBD998E43720
E DA0583CB13C26A6F F FFB822159F604D1D G 19F8DE8FE3A8DF18 H DBBDDE4CB2C97C92
```
体积溯源(`objdump -h`,基线 → 最终):
`.text` 735,600 → 657,168;`.rdata` 49,184 → 35,016;
`.xdata` 48,132 → 27,512;`.pdata` 35,424 → 28,524;`.rsrc` 20,656 不变。
## 没做什么(有意为之)
- **没有拆 DLL**(用户明确要求单文件)
- **没有改优化等级**(`-Oz` 保持)
- **L2(`UiDamage` 脏矩形 + 独立叠加层)评估后缓做**:受益方只有"调试开关打开时的帧率",
而正确实现必须把叠加层旧矩形注入底层组件重绘范围,穿透 PlotView 四条快路径 + 两层缓存;
在用户反复报过"拖拽错位 / 贴图残留"的背景下,风险与收益不成比例。理由写在
`Documents/UI_Refactor_Plan.md` §5 与 `PlotView::SetForceFullRepaint` 的文档里。
154 lines
7.7 KiB
Markdown
154 lines
7.7 KiB
Markdown
# Just DataInspector UI 重构 —— 计划
|
||
|
||
> 本文档只描述**要做什么、为什么这么做、怎么验收**。
|
||
> 已完成情况见 `UI_Refactor_Progress.md`,结论与遗留见 `UI_Refactor_Report.md`。
|
||
> 最后更新:2026-10-29
|
||
|
||
---
|
||
|
||
## 1. 目标与非目标
|
||
|
||
### 1.1 目标
|
||
|
||
| # | 目标 | 判据 |
|
||
| --- | --- | --- |
|
||
| G1 | 收敛"宿主点名每个组件"的散乱结构 | 宿主只遍历一张组件表,不再 `if (component == plot)` 式分支 |
|
||
| G2 | 几何只有**一处**来源 | "画出来的形状"与"能点到的区域"由同一批函数给出,不再各算一遍 |
|
||
| G3 | 源码按**职责**分片,单片可读 | 单个 `.cpp` 不超过约 1200 行;文件名与内容一致 |
|
||
| G4 | 减小产物体积,且**仍然只有一个 `.exe`** | 体积比基线小,无新增依赖 DLL |
|
||
| G5 | 关闭 C++ 异常后**不牺牲健壮性** | 所有大块分配前有预检验;OOM 有可见提示 |
|
||
| G6 | 重构**不得改变任何绘制像素** | 8 个场景的逐像素哈希与基线逐一相同 |
|
||
|
||
### 1.2 非目标(明确不做)
|
||
|
||
* **不把编译产物拆成 DLL**:本项目的定位是"单文件、拷过去就能跑的轻量工具"。
|
||
任何以拆 DLL 换体积的方案一律否决。
|
||
* **不改优化等级**(`-Oz` 保持不变)。
|
||
* **不把"波形"与"网格线"拆成两层位图**:像素级平移不变量会被破坏。
|
||
* **不为体积牺牲可维护性**(例如放弃分片、放弃几何同源)。
|
||
|
||
---
|
||
|
||
## 2. 硬约束
|
||
|
||
1. **单一可执行文件**:不得引入额外的 `.dll` / 运行时依赖。
|
||
2. **体积预警阈值 = 1MB(1,048,576 B)**。这不是硬上限:超过仍可发布,
|
||
但必须主动提醒用户,并说明"超在哪一段(`.text` / `.rdata` / `.xdata`+`.pdata` / `.rsrc`)、
|
||
由哪个改动贡献"。旧的 900KB / 800KB 红线**已作废**。
|
||
3. **异常处理用预检验,不用 try/catch**:`-fno-exceptions` 下 `try/catch` 不会被编译,
|
||
因此所有"按数据规模预留大块内存"的地方必须先问 `MemReserveIfRoom()`。
|
||
4. **像素级零回归**:验收以 `Tools/rendercheck` 的 8 个场景哈希为准。
|
||
5. **中文注释按 UTF-8 无 BOM 保存**;构建参数用 `-finput-charset=UTF-8 -fexec-charset=GBK`。
|
||
|
||
---
|
||
|
||
## 3. 分层架构(L0–L3)
|
||
|
||
```
|
||
L3 体积与异常策略 -fno-exceptions / MemProbe / LTO / gc-sections
|
||
▲
|
||
L2 重绘范围控制 UiDamage(脏矩形) ← 本轮评估后**缓做**,见 §5
|
||
▲
|
||
L1 几何产物层 UiBox / UiBoxTable / UiComponent::BuildBoxes
|
||
▲
|
||
L0 组件协议 UiComponent + 组件表 + UiWidget 控件树
|
||
```
|
||
|
||
### L0 组件协议(前几轮已完成)
|
||
|
||
* `UiComponent`:`SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor`。
|
||
* 宿主持有一张**有序组件表**,逐个遍历;新增组件不必改宿主的 5 处消息分支。
|
||
* `UiWidget` 与 `UiComponent` 是**兄弟**而非父子:前者管"一块区域内部的布局/命中/绘制",
|
||
后者管"自己这一整块地盘:区域、缓存、局部重绘策略"。
|
||
`Toolbar` / `TabBar` 同时是两者。
|
||
|
||
### L1 几何产物层(本轮完成)
|
||
|
||
* 新文件 `Components/Shared/Inc/UiBox.hpp` + `Src/UiBox.cpp`。
|
||
* `struct UiBox { rect, clip, depth, layer }`:**纯值类型**,不继承任何东西。
|
||
* `class UiBoxTable`:几何收集器,只维护"矩形 / 层级 / 绘制层 / 裁剪 / 遮挡"这些**几何事实**,
|
||
**不含任何绘制代码**。
|
||
* `UiComponent::BuildBoxes(UiBoxTable&)`:组件上报自己的几何产物(默认只登记 `Area()`)。
|
||
* `UiDebugBounds` 退化为**消费者**:`UiDebugPaintBoxList(g, table)` 把几何画成彩色包角线。
|
||
⇒ 组件不再需要知道"调试"这个概念存在。
|
||
|
||
### L2 重绘范围控制(缓做 —— 见 §5)
|
||
|
||
* 设计意图:`UiDamage`(最多 12 个矩形的脏区集合,超限退化为包围盒)替代
|
||
"全窗重绘 / 绘图区重绘"的二值分支;`PlotView::Paint(g, UiDamage&)` 逐路径累加脏区。
|
||
* 收益:去掉 `SetForceFullRepaint`(布局边界开关打开时的整幅重绘)。
|
||
|
||
### L3 体积与异常策略(本轮完成)
|
||
|
||
见 `UI_Refactor_Progress.md` 的实测表。
|
||
|
||
---
|
||
|
||
## 4. 阶段与验收标准
|
||
|
||
| 阶段 | 内容 | 验收 |
|
||
| --- | --- | --- |
|
||
| P1–P6 | 文件按职责拆分、`UiComponent` 协议、组件表、布局边界功能 | 编译 + `uicheck` |
|
||
| P7 | 布局边界"几何同源"(`RowRect` 前缀和、文本墨迹矩形、浮层层级/遮挡/裁剪) | `lbcovr` / `lbzorder` / `lbresid` / `lbpanel` |
|
||
| P8 | 体积:`-fno-exceptions` + `MemProbe` + `-ffunction-sections` + `--gc-sections` | 体积下降且 8 哈希不变 |
|
||
| P9 | 体积:LTO(含 `-flto-partition=one` 稳定性约束) | 同上,且可重复构建 |
|
||
| P10 | L1 几何产物层(`UiBox` / `UiBoxTable` / `BuildBoxes`) | 同上,且源码零残留旧名 |
|
||
| P11 | 代码审计(过时注释 / 名实不符 / 死代码) | 零 warning + 全量回归 |
|
||
| P12 | 文档重建 + 本地提交 | `git status` 干净 |
|
||
|
||
**统一验收命令**
|
||
|
||
```powershell
|
||
make clean
|
||
make release TARGET_FILE_NAME=JustDataInspector -j16 # 期望零 error / 零 warning
|
||
make debug TARGET_FILE_NAME=JustDataInspectorD -j16 # 期望零 warning
|
||
|
||
objdump -h Build\Release\JustDataInspector.exe # 段构成,用于体积溯源
|
||
|
||
Tools\uicheck\uicheck.exe # 69/69
|
||
.preflight\lbcheck\lbcheck.exe # 21/21
|
||
.preflight\lbresid\lbresid.exe # 5/5
|
||
.preflight\lbzorder\lbzorder.exe # 4/4
|
||
.preflight\lbcovr\lbcovr.exe # 7/7
|
||
.preflight\lbpanel\lbpanel.exe # 10/10
|
||
Tools\rendercheck\rendercheck.exe # 8 哈希不变 + FUNC 7/7
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 明确缓做的部分及理由
|
||
|
||
### L2 `UiDamage` + 独立叠加层(S4)
|
||
|
||
**为什么缓做**
|
||
|
||
1. 受益方只有"**调试开关打开时**的帧率"。`SetForceFullRepaint` 只在
|
||
「显示布局边界」开启时被置位(`Application::Render` 里 `m_plot.SetForceFullRepaint(m_showLayoutBounds)`),
|
||
普通用户路径上**本来就是快路径**。
|
||
2. 正确实现无法只改 `PlotView` 内部:宿主用的是**持久后备缓冲区**,
|
||
上一帧叠加层画上去的边框像素必须在下一帧被"底层内容重画"覆盖掉。
|
||
若改成"独立叠加位图 + 只混合脏区",就必须把叠加层的旧矩形**注入**到底层组件的重绘范围里 ——
|
||
这要穿透 `PlotView` 的全部四条快路径(静态场景贴图 / 宽基底贴图 / 样式补丁 / 网格层重建)
|
||
与两层缓存。
|
||
3. 风险与收益不成比例:用户已在 2026-10-28 反复报告过"拖拽错位 / 贴图残留"一类事故,
|
||
而这类事故正是从"某条快路径漏了某块像素"开始的。
|
||
|
||
**保留了什么**:`SetForceFullRepaint` 及其四处守卫、`UiBoxTable` 的几何产物。
|
||
将来若真要做,只需把 `UiBoxTable` 的 `rect` 直接喂给 `UiDamage`,不需要重新收集几何。
|
||
|
||
---
|
||
|
||
## 6. 交付物清单
|
||
|
||
```
|
||
Components/Shared/Inc/UiBox.hpp 新增(几何产物层)
|
||
Components/Shared/Src/UiBox.cpp 新增
|
||
Components/Shared/Inc/UiDebugBounds.hpp 精简为"只画"(消费者)
|
||
Components/Shared/Src/UiDebugBounds.cpp 精简为"只画"
|
||
Components/Shared/Inc/UiComponent.hpp CollectDebugBoxes → BuildBoxes
|
||
Utilities/Inc/MemProbe.hpp 新增(分配预检验 + terminate 兜底)
|
||
Utilities/Src/MemProbe.cpp 新增
|
||
Makefile LAYOUT_BOUNDS / NO_EXCEPTIONS / LTO 三个开关
|
||
Documents/UI_Refactor_{Plan,Progress,Report}.md 重建
|
||
```
|