体积 L3(-fno-exceptions + MemProbe + LTO)与 L1 几何产物层;产物 909,824 -> 788,992 B
## 为什么
用户 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` 的文档里。
This commit is contained in:
1 parent
739a523af5
commit
044b87383b
34 files changed
+1115
-2198
No files matched your search
@@ -18,7 +18,7 @@
|
||||
#include "Theme.hpp"
|
||||
#include "Toolbar.hpp"
|
||||
#include "UiComponent.hpp"
|
||||
#include "UiDebugBounds.hpp"
|
||||
#include "UiBox.hpp"
|
||||
#include "UiSurface.hpp"
|
||||
|
||||
namespace jdi
|
||||
@@ -488,7 +488,7 @@ namespace jdi
|
||||
* 作为成员而不是局部变量:避免开关打开时每帧重新分配,
|
||||
* 也让它能直接充当 `PaintUtil` 的文本收集器(`UiTextRectSink`)。
|
||||
*/
|
||||
UiDebugBoxList m_debugBoxes;
|
||||
UiBoxTable m_debugBoxes;
|
||||
|
||||
/** @brief 填充组件表(`Initialize` 里 `Setup` 之后调用一次)。 */
|
||||
void BuildComponentTable();
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "DataLoader.hpp"
|
||||
#include "MemProbe.hpp"
|
||||
#include "ResourceIds.h"
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
|
||||
@@ -53,6 +53,12 @@ namespace jdi
|
||||
{
|
||||
m_instance = instance;
|
||||
|
||||
// ⚠️ 本项目以 `-fno-exceptions` 构建(省 `.xdata`+`.pdata` 约 40~60KB):
|
||||
// 任何遗漏的分配失败都会走到 `std::terminate`(默认行为是静默 abort,用户只看到程序
|
||||
// 突然消失)。这里安装一个兜底处理器:弹一次带建议的模态提示后干净退出。
|
||||
// 真正的大块分配已在各处通过 `MemReserveIfRoom()` 预检验(见 `MemProbe.hpp`)。
|
||||
MemInstallTerminateHandler();
|
||||
|
||||
// 隐藏窗口启动(自动化/后台)时没人能点掉模态错误框,会自动降级为非模态提示。
|
||||
SetModalErrorsEnabled(showCommand != SW_HIDE);
|
||||
|
||||
|
||||
@@ -1175,21 +1175,21 @@ namespace jdi
|
||||
if (entry.component != nullptr)
|
||||
{
|
||||
m_debugBoxes.SetClip(entry.component->Area());
|
||||
entry.component->CollectDebugBoxes(m_debugBoxes);
|
||||
entry.component->BuildBoxes(m_debugBoxes);
|
||||
}
|
||||
m_debugBoxes.PushLayer();
|
||||
}
|
||||
|
||||
// 不属于任何组件的两块:面板分隔条(由宿主直接填充色块)、菜单 / 拾色器(浮层)。
|
||||
// ⚠️ 浮层**不设裁剪矩形**:菜单的二级子菜单会展开到菜单本体之外,设裁剪会把它裁掉;
|
||||
// 它们自己会把本体登记为"遮挡矩形"(见 `PopupMenu::CollectDebugBoxes`),
|
||||
// 它们自己会把本体登记为"遮挡矩形"(见 `PopupMenu::BuildBoxes`),
|
||||
// 由绘制阶段把更低层的 box 从浮层区域里挖掉。
|
||||
m_debugBoxes.SetClip(gd::RectF());
|
||||
m_debugBoxes.Add(m_splitterRect, 0);
|
||||
m_debugBoxes.PushLayer();
|
||||
m_menu.CollectDebugBoxes(m_debugBoxes);
|
||||
m_menu.BuildBoxes(m_debugBoxes);
|
||||
m_debugBoxes.PushLayer();
|
||||
m_colorPicker.CollectDebugBoxes(m_debugBoxes);
|
||||
m_colorPicker.BuildBoxes(m_debugBoxes);
|
||||
|
||||
UiDebugPaintBoxList(g, m_debugBoxes);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
class UiDebugBoxList;
|
||||
class UiBoxTable;
|
||||
|
||||
/**
|
||||
* @brief 后台派生曲线计算完成后由 `PlotView` 投递给宿主的自定义消息。
|
||||
@@ -207,17 +207,17 @@ namespace jdi
|
||||
void Paint(gd::Graphics& g) override;
|
||||
|
||||
/**
|
||||
* @brief 收集绘图区区域、**每个网格**、GX 徽标、图例条目与浮动面板的布局边界(调试用)。
|
||||
* @brief 上报绘图区区域、**每个网格**、GX 徽标、图例条目与浮动面板的几何 box。
|
||||
*
|
||||
* 只上报“布局结构”,**不上报**曲线折线、取值胶囊、悬停提示这类随数据/鼠标逐帧变化的
|
||||
* 瞬态几何 —— 它们每帧都不同,叠上去只是噪声。
|
||||
*
|
||||
* @param [IN,OUT] boxes 收集器。
|
||||
*/
|
||||
void CollectDebugBoxes(UiDebugBoxList& boxes) override;
|
||||
void BuildBoxes(UiBoxTable& boxes) override;
|
||||
|
||||
/**
|
||||
* @brief 强制走「整幅重绘」路径(用于调试叠加层)。
|
||||
* @brief 强制走「整幅重绘」路径(供调试叠加层使用)。
|
||||
*
|
||||
* 平时 `Paint` 会用三条快路径(静态场景贴图 / 宽基底贴图 / 样式补丁)只重画变化部分,
|
||||
* 这在拖动网格滚动条时会**跳过一部分像素**。而调试叠加层(「显示布局边界」)是画在
|
||||
@@ -226,6 +226,11 @@ namespace jdi
|
||||
*
|
||||
* 因此调试叠加层开启时必须打开本开关:牺牲帧率,换取"每帧都是完整一帧"。
|
||||
*
|
||||
* ⚠️ 保留而不优化的理由(2026-10-29 评估):把它改成“脏矩形 + 独立叠加位图”需要让
|
||||
* `Paint` 接受宿主下发的 damage(否则持久后备缓冲区里上一帧的边框像素无处抹除),
|
||||
* 而这会穿透 `PlotView` 全部四条快路径与两层缓存。收益只是“调试开关打开时的帧率”、
|
||||
* 风险却是用户反复报过的拖拽错位/贴图残留一类事故 —— 故**暂不实施**,仅保留此开关。
|
||||
*
|
||||
* @param [IN] force 为 true 时禁用全部快路径且不作画质降级。
|
||||
*/
|
||||
void SetForceFullRepaint(bool force) { m_forceFullRepaint = force; }
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "PlotView.hpp"
|
||||
#include "MemProbe.hpp"
|
||||
#include "UiControls.hpp"
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
|
||||
@@ -850,30 +850,10 @@ namespace jdi
|
||||
|
||||
std::size_t PlotView::AvailablePhysicalBytes()
|
||||
{
|
||||
// ⚠️ 结果按 200ms 缓存:本函数在绘制路径上每次请求曲线都会调用,
|
||||
// `GlobalMemoryStatusEx` 是系统调用,逐帧多次调用没有必要(内存变化远慢于 200ms)。
|
||||
static std::atomic<std::uint64_t> cachedAt{0};
|
||||
static std::atomic<std::size_t> cachedBytes{0};
|
||||
const std::uint64_t now = GetTickCount64();
|
||||
const std::uint64_t last = cachedAt.load(std::memory_order_relaxed);
|
||||
if (last != 0 && (now - last) < 200)
|
||||
{
|
||||
const std::size_t value = cachedBytes.load(std::memory_order_relaxed);
|
||||
if (value != 0)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
MEMORYSTATUSEX status = {};
|
||||
status.dwLength = sizeof(status);
|
||||
std::size_t available = std::numeric_limits<std::size_t>::max();
|
||||
if (GlobalMemoryStatusEx(&status) != FALSE)
|
||||
{
|
||||
available = static_cast<std::size_t>(status.ullAvailPhys);
|
||||
}
|
||||
cachedBytes.store(available, std::memory_order_relaxed);
|
||||
cachedAt.store(now, std::memory_order_relaxed);
|
||||
return available;
|
||||
// 统一走 `MemProbe` 的实现:它自带 200ms 缓存,且与"分配前预检验"
|
||||
// (`MemCanAllocate` / `MemReserveIfRoom`)共用同一份数据 ——
|
||||
// 历史上这里有一份完全重复的 `GlobalMemoryStatusEx` + 缓存实现。
|
||||
return MemAvailablePhysical();
|
||||
}
|
||||
|
||||
void PlotView::ShiftLaneRange(int laneIndex, double deltaValue)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
// ⚠️ 新增函数按**职责**放入对应分片;`PlotView::Paint/PaintLane` 仍只允许定义在本文件。
|
||||
#include "PlotViewInternal.hpp"
|
||||
|
||||
#include "UiDebugBounds.hpp"
|
||||
#include "UiBox.hpp"
|
||||
#include "UiLayout.hpp"
|
||||
|
||||
namespace jdi
|
||||
@@ -563,9 +563,9 @@ namespace jdi
|
||||
UiText(g, message, m_fonts->ui, m_area, m_theme->textMuted, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
void PlotView::CollectDebugBoxes(UiDebugBoxList& boxes)
|
||||
void PlotView::BuildBoxes(UiBoxTable& boxes)
|
||||
{
|
||||
UiComponent::CollectDebugBoxes(boxes);
|
||||
UiComponent::BuildBoxes(boxes);
|
||||
|
||||
// 层级 1:每个网格的波形区;层级 2:该网格的 GX 编号徽标。
|
||||
// ⚠️ **绝不能读 `m_decorRects`**:那里存的是"装饰层位图缓存"的登记矩形,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
class UiDebugBoxList;
|
||||
class UiBoxTable;
|
||||
|
||||
/**
|
||||
* @brief 浮动式 RGB 自定义颜色拾取器。
|
||||
@@ -183,7 +183,7 @@ namespace jdi
|
||||
* 面板未打开时什么都不登记。
|
||||
* @param [IN,OUT] boxes 收集器。
|
||||
*/
|
||||
void CollectDebugBoxes(UiDebugBoxList& boxes) const;
|
||||
void BuildBoxes(UiBoxTable& boxes) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
class UiDebugBoxList;
|
||||
class UiBoxTable;
|
||||
|
||||
/**
|
||||
* @brief 弹出菜单的打开来源,用于把选择结果派发回正确的组件。
|
||||
@@ -239,7 +239,7 @@ namespace jdi
|
||||
* 菜单未打开时什么都不登记。条目、滚动区、滚动条与二级子菜单各自成为独立的 box。
|
||||
* @param [IN,OUT] boxes 收集器。
|
||||
*/
|
||||
void CollectDebugBoxes(UiDebugBoxList& boxes) const;
|
||||
void BuildBoxes(UiBoxTable& boxes) const;
|
||||
|
||||
/**
|
||||
* @brief 菜单是否因内容过长而启用了滚动(条目区可滚动、前导标题与尾部条目固定)。
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#ifndef __UI_BOX_HPP__
|
||||
#define __UI_BOX_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include "PaintUtil.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 一个几何 box(**纯值类型**,不含行为、不含生命周期)。
|
||||
*
|
||||
* ## 它是什么
|
||||
*
|
||||
* 描述"某块界面元素占据哪个矩形、属于第几层、在第几个绘制层、被裁在哪个矩形里"。
|
||||
* 这就是界面在某一帧的**几何产物**:绘制、命中、调试描边理论上都应该从它派生,
|
||||
* 而不是各自再算一遍。
|
||||
*
|
||||
* ## 为什么不是控件节点
|
||||
*
|
||||
* 刻意不继承控件基类:`PlotView` 有 4 层自持位图缓存、`SignalPanel` 有行位图缓存,
|
||||
* 它们要求"像素由组件自己负责"。把几何塞进继承体系会和缓存失效体系打架,
|
||||
* 而 box 只是一个可复制的矩形 + 几个整数标签。
|
||||
*
|
||||
* ## 为什么 `clip` 与 `rect` 分开存
|
||||
*
|
||||
* 组件内部的元素可能落在地盘之外(典型:滚出可视区的网格,Y 是负数)。
|
||||
* 消费方必须能区分"这块几何真实位置在哪"和"它最多能显示到哪",
|
||||
* 因此裁剪矩形跟着 box 走,而不是靠全局状态。
|
||||
*/
|
||||
struct UiBox
|
||||
{
|
||||
gd::RectF rect; /**< 矩形(客户区坐标)。 */
|
||||
gd::RectF clip; /**< 裁剪矩形(= 所属组件的区域;空 = 不裁剪)。 */
|
||||
int depth = 0; /**< 层级:0 = 顶层组件区域,≥1 = 内部控件。深 = 更靠近叶子。 */
|
||||
int layer = 0; /**< 绘制层:越大越靠上,由 `UiBoxTable::PushLayer` 分配。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 几何 box 的收集器(值语义容器,**不含任何绘制代码**)。
|
||||
*
|
||||
* ## 职责边界
|
||||
*
|
||||
* 这里只负责"**累积几何 + 维护裁剪/层/遮挡这些几何事实**"。
|
||||
* 怎么把 box 变成屏幕上的线,是 `UiDebugBounds` 的事(那是调试专用的消费者)。
|
||||
* 分开的原因:几何是**稳定事实**(命中、遮挡、将来的脏矩形都要用),
|
||||
* 而"用彩色包角描边"只服务于一个开关,两者不该长在同一个类里。
|
||||
*
|
||||
* ## 两个排序键缺一不可
|
||||
*
|
||||
* * `layer`:保证**后收集的内容压住先收集的**。没有它,"信号面板的行边界"
|
||||
* 会盖住"右键弹出的二级菜单"(菜单是后收集的浮层)。
|
||||
* * `depth`:同一层内先画父(浅层)再画子(深层),子元素的包角才不被父覆盖。
|
||||
*
|
||||
* ## 开销约束
|
||||
*
|
||||
* 只在宿主开启「显示布局边界」时才被构造和填充 —— 关闭时**零开销**
|
||||
* (不进绘制路径、不进任何位图缓存)。若将来要用它做常驻命中表,
|
||||
* 必须显式改为"按需重建",不能每帧无限期累积。
|
||||
*/
|
||||
class UiBoxTable : public UiTextRectSink
|
||||
{
|
||||
public:
|
||||
/** @brief 清空(复用同一实例时调用),同时把绘制层复位为 0。 */
|
||||
void Clear();
|
||||
|
||||
/**
|
||||
* @brief 进入下一个绘制层。
|
||||
*
|
||||
* 宿主在每个组件收集完之后调用一次;组件内部若有"浮层压住本体"的情形
|
||||
* (例如菜单的二级子菜单)也应在收集子层之前调用。
|
||||
*/
|
||||
void PushLayer() { ++m_layer; }
|
||||
|
||||
/**
|
||||
* @brief 显式设置当前绘制层(绘制期间收集文本几何时用)。
|
||||
* @param [IN] layer 层号(越大越靠上)。
|
||||
*/
|
||||
void SetLayer(int layer) { m_layer = layer; }
|
||||
|
||||
/** @brief 回报一段文本的墨迹矩形(由 `PaintUtil` 在绘制文本时调用)。 */
|
||||
void OnTextRect(const gd::RectF& rect) override;
|
||||
|
||||
/**
|
||||
* @brief 设置后续 box 的裁剪矩形(= 当前组件的区域)。
|
||||
*
|
||||
* ⚠️ **必须像裁剪绘制一样裁剪几何**:组件内部元素可能落在地盘之外
|
||||
* (典型:滚出可视区的网格 `lane.rect` 的 Y 会是负数),不裁剪就会登记到相邻组件
|
||||
* (工具栏 / 标签栏)的地盘上 —— 这正是用户报告"工具栏上出现大量残留"的直接原因。
|
||||
*
|
||||
* @param [IN] clip 裁剪矩形;传空矩形表示不裁剪。
|
||||
*/
|
||||
void SetClip(const gd::RectF& clip) { m_clip = clip; }
|
||||
|
||||
/** @brief 追加一个 box(空矩形 / 与裁剪矩形无交集的 box 自动忽略)。 */
|
||||
void Add(const gd::RectF& rect, int depth);
|
||||
|
||||
/** @brief 追加一组同层级的 box(空矩形逐个忽略)。 */
|
||||
void AddAll(const std::vector<gd::RectF>& rects, int depth);
|
||||
|
||||
/**
|
||||
* @brief 登记一个**遮挡矩形**(浮层本体,如弹出菜单 / 拾色器)。
|
||||
*
|
||||
* ⚠️ 必须做这件事:box 的消费方是**在所有内容之后**工作的,仅靠 `layer` 排序
|
||||
* 只能决定"两个 box 重叠时谁压谁",并不能阻止"下层组件的边框画在不透明浮层上面"。
|
||||
* 登记为遮挡后,**绘制层比它低**的 box 在该区域内会被裁掉。
|
||||
*
|
||||
* @param [IN] rect 遮挡矩形(客户区坐标;空矩形忽略)。
|
||||
*/
|
||||
void AddOccluder(const gd::RectF& rect);
|
||||
|
||||
/** @brief 是否为空。 */
|
||||
bool Empty() const { return m_boxes.empty(); }
|
||||
|
||||
/** @brief box 数量。 */
|
||||
std::size_t Count() const { return m_boxes.size(); }
|
||||
|
||||
/** @brief 只读访问(**未排序**,消费方需要顺序时自己按 `(layer, depth)` 排序)。 */
|
||||
const std::vector<UiBox>& Items() const { return m_boxes; }
|
||||
|
||||
/** @brief 一个遮挡矩形(含它所在的绘制层)。 */
|
||||
struct Occluder
|
||||
{
|
||||
gd::RectF rect; /**< 遮挡矩形。 */
|
||||
int layer = 0; /**< 所在绘制层(只遮挡更低的层)。 */
|
||||
};
|
||||
|
||||
/** @brief 遮挡矩形集合(只读)。 */
|
||||
const std::vector<Occluder>& Occluders() const { return m_occluders; }
|
||||
|
||||
private:
|
||||
std::vector<UiBox> m_boxes;
|
||||
std::vector<Occluder> m_occluders;
|
||||
int m_layer = 0; /**< 当前绘制层。 */
|
||||
gd::RectF m_clip; /**< 当前裁剪矩形(空 = 不裁剪)。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __UI_BOX_HPP__
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
class UiDebugBoxList;
|
||||
class UiBoxTable;
|
||||
|
||||
/**
|
||||
* @brief 顶层 UI 组件的统一协议(**纯接口,无状态**)。
|
||||
@@ -77,20 +77,30 @@ namespace jdi
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 收集本组件的**布局边界**(调试用,「显示布局边界」开启时由宿主调用)。
|
||||
* @brief 上报本组件的**几何产物**(含子元素的矩形、层级、裁剪、遮挡关系)。
|
||||
*
|
||||
* 默认实现只登记 `Area()`;自身内部还有可辨识子块(控件树节点、列表行、网格、
|
||||
* 复选框、徽标、✕、面板控件……)的组件覆盖它,先调本基类再补登记**层级更深**的 box。
|
||||
*
|
||||
* 之所以把入口放在这里而不是让宿主逐个点名:宿主只持有 `UiComponent*` 的组件表,
|
||||
* 遍历一次就能覆盖全部顶层区域;组件内部结构由组件自己上报(只有它知道几何)。
|
||||
* ## 为什么入口在组件上
|
||||
*
|
||||
* ⚠️ 只在宿主开启「显示布局边界」时才被调用 —— 平时**零开销**(不进任何绘制路径、
|
||||
* 不进任何缓存)。实现时**禁止**把几何写进自持位图缓存,否则关掉开关后线条会残留。
|
||||
* 宿主只持有 `UiComponent*` 的组件表,遍历一次就能覆盖全部顶层区域;
|
||||
* 组件内部结构由组件自己上报(只有它知道几何)。新增组件不必再改宿主。
|
||||
*
|
||||
* ## 它是几何,不是调试
|
||||
*
|
||||
* 这里只填 `UiBoxTable`(纯值:矩形 + 层级 + 层号 + 裁剪),**不画任何东西**。
|
||||
* “怎么显示”是消费方的事:目前唯一的消费方是「显示布局边界」调试叠加层
|
||||
* (`UiDebugPaintBoxList`),将来命中 / 脏矩形也可以从同一张表派生。
|
||||
* 这样组件就不需要知道"调试"这个概念的存在。
|
||||
*
|
||||
* ⚠️ 目前它只在宿主开启「显示布局边界」时才被调用 —— 平时**零开销**
|
||||
*(不进任何绘制路径、不进任何位图缓存)。实现时**禁止**把几何写进自持位图缓存,
|
||||
* 否则关掉开关后线条会残留。
|
||||
*
|
||||
* @param [IN,OUT] boxes 收集器(宿主每帧新建,全程只在开关打开时存在)。
|
||||
*/
|
||||
virtual void CollectDebugBoxes(UiDebugBoxList& boxes);
|
||||
virtual void BuildBoxes(UiBoxTable& boxes);
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
@@ -5,16 +5,17 @@
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "PaintUtil.hpp"
|
||||
#include "Theme.hpp"
|
||||
#include "UiBox.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
// ⚠️ 这个头文件只负责"把几何 box 画成彩色的布局边界线"(调试专用)。
|
||||
// 几何本身(`UiBox` / `UiBoxTable`)在 `UiBox.hpp` 里,与绘制无关。
|
||||
|
||||
/**
|
||||
* @brief 布局边界描边颜色(按层级取色)。
|
||||
*
|
||||
@@ -26,101 +27,7 @@ namespace jdi
|
||||
std::uint32_t UiDebugBoundsColor(int depth);
|
||||
|
||||
/**
|
||||
* @brief 一个待显示的布局边界 box(**纯值类型**)。
|
||||
*
|
||||
* 刻意不继承控件基类:`PlotView` 的 4 层位图缓存、`SignalPanel` 的行位图缓存都要求
|
||||
* "绘制由组件自己负责",把几何塞进继承体系会和缓存体系打架。box 只描述
|
||||
* "哪里有一个矩形、属于第几层、在第几绘制层",因此**几何可以被绘制、命中与调试共用**,
|
||||
* 也不需要任何虚函数或生命周期管理。
|
||||
*/
|
||||
struct UiDebugBox
|
||||
{
|
||||
gd::RectF rect; /**< 矩形(客户区坐标)。 */
|
||||
gd::RectF clip; /**< 裁剪矩形(= 所属组件的区域;空 = 不裁剪)。 */
|
||||
int depth = 0; /**< 层级(决定配色与包角粗细)。 */
|
||||
int layer = 0; /**< 绘制层(越大越靠上):由 `UiDebugBoxList::PushLayer` 分配。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief box 收集器(值语义容器)。
|
||||
*
|
||||
* 由宿主每帧构造一次、交给各组件填充、再一次性画完 —— 全程只在「显示布局边界」打开时执行。
|
||||
*
|
||||
* ⚠️ **两个排序键缺一不可**:
|
||||
* * `layer`(绘制层,来自 `PushLayer()`):保证**后收集的内容压住先收集的**。
|
||||
* 没有它,"信号面板的行边界"会盖住"右键弹出的二级菜单"(菜单是后收集的浮层)。
|
||||
* * `depth`(层级):同一层内先画父(浅层)再画子(深层),子元素的彩色包角才不被父覆盖。
|
||||
*/
|
||||
class UiDebugBoxList : public UiTextRectSink
|
||||
{
|
||||
public:
|
||||
/** @brief 清空(复用同一实例时调用),同时把绘制层复位为 0。 */
|
||||
void Clear();
|
||||
/**
|
||||
* @brief 进入下一个绘制层。
|
||||
*
|
||||
* 宿主在每个组件收集完之后调用一次;组件内部若有"浮层压住本体"的情形
|
||||
* (例如菜单的二级子菜单)也应在收集子层之前调用。
|
||||
*/
|
||||
void PushLayer() { ++m_layer; }
|
||||
/**
|
||||
* @brief 显式设置当前绘制层(绘制期间收集文本几何时用)。
|
||||
* @param [IN] layer 层号(越大越靠上)。
|
||||
*/
|
||||
void SetLayer(int layer) { m_layer = layer; }
|
||||
|
||||
/** @brief 回报一段文本的墨迹矩形(由 `PaintUtil` 在绘制文本时调用)。 */
|
||||
void OnTextRect(const gd::RectF& rect) override;
|
||||
/**
|
||||
* @brief 设置后续 box 的裁剪矩形(= 当前组件的区域)。
|
||||
*
|
||||
* ⚠️ **必须像裁剪绘制一样裁剪调试 box**:组件内部元素可能落在地盘之外
|
||||
* (典型:滚出可视区的网格 `lane.rect` 的 Y 会是负数),不裁剪就会画到相邻组件
|
||||
* (工具栏 / 标签栏)上面 —— 这正是用户报告“工具栏上出现大量残留”的直接原因。
|
||||
*
|
||||
* @param [IN] clip 裁剪矩形;传空矩形表示不裁剪。
|
||||
*/
|
||||
void SetClip(const gd::RectF& clip) { m_clip = clip; }
|
||||
/** @brief 追加一个 box(空矩形 / 与裁剪矩形无交集的 box 自动忽略)。 */
|
||||
void Add(const gd::RectF& rect, int depth);
|
||||
/** @brief 追加一组同层级的 box(空矩形逐个忽略)。 */
|
||||
void AddAll(const std::vector<gd::RectF>& rects, int depth);
|
||||
/**
|
||||
* @brief 登记一个**遮挡矩形**(浮层本体,如弹出菜单 / 拾色器)。
|
||||
*
|
||||
* ⚠️ 必须做这件事:调试 box 是画在所有内容**之后**的,仅靠 `layer` 排序只能决定
|
||||
* “两个 box 重叠时谁压谁”,并不能阻止“下层组件的边框线画在不透明浮层上面”。
|
||||
* 登记为 occluder 后,**绘制层比它低**的 box 在该区域内会被裁掉,
|
||||
* 于是观察右键菜单的空间布局时不会被底下的行/网格边界干扰。
|
||||
*
|
||||
* @param [IN] rect 遮挡矩形(客户区坐标;空矩形忽略)。
|
||||
*/
|
||||
void AddOccluder(const gd::RectF& rect);
|
||||
/** @brief 是否为空。 */
|
||||
bool Empty() const { return m_boxes.empty(); }
|
||||
/** @brief box 数量。 */
|
||||
std::size_t Count() const { return m_boxes.size(); }
|
||||
/** @brief 只读访问(**未排序**,绘制时内部会按 layer/depth 稳定排序)。 */
|
||||
const std::vector<UiDebugBox>& Items() const { return m_boxes; }
|
||||
|
||||
/** @brief 一个遮挡矩形(含它所在的绘制层)。 */
|
||||
struct Occluder
|
||||
{
|
||||
gd::RectF rect; /**< 遮挡矩形。 */
|
||||
int layer = 0; /**< 所在绘制层(只遮挡更低的层)。 */
|
||||
};
|
||||
/** @brief 遮挡矩形集合(只读)。 */
|
||||
const std::vector<Occluder>& Occluders() const { return m_occluders; }
|
||||
|
||||
private:
|
||||
std::vector<UiDebugBox> m_boxes;
|
||||
std::vector<Occluder> m_occluders;
|
||||
int m_layer = 0; /**< 当前绘制层。 */
|
||||
gd::RectF m_clip; /**< 当前裁剪矩形(空 = 不裁剪)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 绘制一个矩形的布局边界(**四角粗、四边细**,且粗细随矩形尺寸自适应)。
|
||||
* @brief 绘制一个矩形的布局边界(**四角粗、四边细**,粗细随矩形尺寸自适应,受 `clip` 裁剪)。
|
||||
*
|
||||
* 视觉规则:
|
||||
* * 四条边固定 1 设备像素;
|
||||
@@ -138,32 +45,22 @@ namespace jdi
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 目标矩形(客户区坐标)。
|
||||
* @param [IN] depth 层级(决定配色与包角基础粗细)。
|
||||
*/
|
||||
void UiDebugPaintBounds(gd::Graphics& g, const gd::RectF& rect, int depth);
|
||||
|
||||
/**
|
||||
* @brief `UiDebugPaintBounds` 的内部实现(带裁剪矩形)。
|
||||
*
|
||||
* 供 `UiDebugPaintBoxList` 使用:非空 `clip` 表示"只在所属组件的地盘内绘制"。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 目标矩形。
|
||||
* @param [IN] clip 裁剪矩形(空 = 不裁剪)。
|
||||
* @param [IN] depth 层级。
|
||||
* @param [IN] depth 层级(决定配色与包角基础粗细)。
|
||||
*/
|
||||
void UiDebugPaintBoundsClipped(gd::Graphics& g, const gd::RectF& rect, const gd::RectF& clip,
|
||||
int depth);
|
||||
|
||||
/**
|
||||
* @brief 一次性画完整个 box 列表(在宿主绘制的最后一步调用)。
|
||||
* @brief 一次性画完整个 box 表(在宿主绘制的最后一步调用)。
|
||||
*
|
||||
* 按 `(layer, depth)` 稳定排序后绘制:后收集的层压住先收集的层,同层内父先子后。
|
||||
* `layer` 比某个遮挡矩形低的 box 会被该矩形挖洞裁掉(见 `UiBoxTable::AddOccluder`)。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] boxes box 列表。
|
||||
* @param [IN] boxes 几何 box 表。
|
||||
*/
|
||||
void UiDebugPaintBoxList(gd::Graphics& g, const UiDebugBoxList& boxes);
|
||||
void UiDebugPaintBoxList(gd::Graphics& g, const UiBoxTable& boxes);
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace jdi
|
||||
|
||||
struct Theme;
|
||||
struct UiFonts;
|
||||
class UiDebugBoxList;
|
||||
class UiBoxTable;
|
||||
|
||||
/**
|
||||
* @brief 裁剪时允许超出控件矩形的像素数(逻辑像素 × scale)。
|
||||
@@ -217,7 +217,7 @@ namespace jdi
|
||||
* @param [IN,OUT] boxes 收集器。
|
||||
* @param [IN] depth 层级(0 = 调用者自身,子节点依次 +1,决定描边颜色)。
|
||||
*/
|
||||
void CollectDebugBoxes(UiDebugBoxList& boxes, int depth) const;
|
||||
void BuildBoxes(UiBoxTable& boxes, int depth) const;
|
||||
|
||||
/** @brief 局部失效:与自身矩形求交后向根冒泡。 */
|
||||
virtual void Invalidate(const gd::RectF& rect);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "ColorPickerPopup.hpp"
|
||||
|
||||
#include "UiControls.hpp"
|
||||
#include "UiDebugBounds.hpp"
|
||||
#include "UiBox.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
@@ -1094,7 +1094,7 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
void ColorPickerPopup::CollectDebugBoxes(UiDebugBoxList& boxes) const
|
||||
void ColorPickerPopup::BuildBoxes(UiBoxTable& boxes) const
|
||||
{
|
||||
if (!m_open)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#include "PopupMenuInternal.hpp"
|
||||
#include "UiControls.hpp"
|
||||
#include "UiDebugBounds.hpp"
|
||||
#include "UiBox.hpp"
|
||||
#include "UiLayout.hpp"
|
||||
|
||||
namespace jdi
|
||||
@@ -706,7 +706,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
|
||||
void PopupMenu::CollectDebugBoxes(UiDebugBoxList& boxes) const
|
||||
void PopupMenu::BuildBoxes(UiBoxTable& boxes) const
|
||||
{
|
||||
if (!m_open)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "UiBox.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
void UiBoxTable::Clear()
|
||||
{
|
||||
m_boxes.clear();
|
||||
m_occluders.clear();
|
||||
m_layer = 0;
|
||||
m_clip = gd::RectF();
|
||||
}
|
||||
|
||||
void UiBoxTable::OnTextRect(const gd::RectF& rect)
|
||||
{
|
||||
// 文本用固定层级 4:它总是"最里层"的元素(复选框里的字、行里的信号名…)。
|
||||
Add(rect, 4);
|
||||
}
|
||||
|
||||
void UiBoxTable::Add(const gd::RectF& rect, int depth)
|
||||
{
|
||||
if (!(rect.Width > 0.0f) || !(rect.Height > 0.0f))
|
||||
{
|
||||
return; // 空矩形(未布局 / 该元素当前不显示)不登记。
|
||||
}
|
||||
if (m_clip.Width > 0.0f && m_clip.Height > 0.0f)
|
||||
{
|
||||
// 与所属组件的区域无交集 ⇒ 已经完全滚出地盘,不必登记(也避免越界登记到邻居的地盘上)。
|
||||
if (rect.X + rect.Width <= m_clip.X || rect.X >= m_clip.X + m_clip.Width ||
|
||||
rect.Y + rect.Height <= m_clip.Y || rect.Y >= m_clip.Y + m_clip.Height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
UiBox box;
|
||||
box.rect = rect;
|
||||
box.clip = m_clip;
|
||||
box.depth = (depth < 0) ? 0 : depth;
|
||||
box.layer = m_layer;
|
||||
m_boxes.push_back(box);
|
||||
}
|
||||
|
||||
void UiBoxTable::AddAll(const std::vector<gd::RectF>& rects, int depth)
|
||||
{
|
||||
for (const gd::RectF& rect : rects)
|
||||
{
|
||||
Add(rect, depth);
|
||||
}
|
||||
}
|
||||
|
||||
void UiBoxTable::AddOccluder(const gd::RectF& rect)
|
||||
{
|
||||
if (!(rect.Width > 0.0f) || !(rect.Height > 0.0f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Occluder occluder;
|
||||
occluder.rect = rect;
|
||||
occluder.layer = m_layer;
|
||||
m_occluders.push_back(occluder);
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "UiComponent.hpp"
|
||||
|
||||
#include "UiDebugBounds.hpp"
|
||||
#include "UiBox.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
// 默认实现放在 .cpp 而不是头文件内联:`UiComponent.hpp` 是四个大组件都要包含的
|
||||
// 公共头,不应把 `UiDebugBounds.hpp`(box 收集器)也拉进它们的编译单元。
|
||||
void UiComponent::CollectDebugBoxes(UiDebugBoxList& boxes)
|
||||
// 公共头,不应把 `UiBox.hpp`(几何收集器)也拉进它们的编译单元。
|
||||
void UiComponent::BuildBoxes(UiBoxTable& boxes)
|
||||
{
|
||||
boxes.Add(Area(), 0);
|
||||
}
|
||||
|
||||
@@ -77,63 +77,6 @@ namespace jdi
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void UiDebugBoxList::Clear()
|
||||
{
|
||||
m_boxes.clear();
|
||||
m_occluders.clear();
|
||||
m_layer = 0;
|
||||
m_clip = gd::RectF();
|
||||
}
|
||||
|
||||
void UiDebugBoxList::OnTextRect(const gd::RectF& rect)
|
||||
{
|
||||
// 文本用固定层级 4:它总是"最里层"的元素(复选框里的字、行里的信号名…)。
|
||||
Add(rect, 4);
|
||||
}
|
||||
|
||||
void UiDebugBoxList::Add(const gd::RectF& rect, int depth)
|
||||
{
|
||||
if (!(rect.Width > 0.0f) || !(rect.Height > 0.0f))
|
||||
{
|
||||
return; // 空矩形(未布局 / 该元素当前不显示)不登记。
|
||||
}
|
||||
if (m_clip.Width > 0.0f && m_clip.Height > 0.0f)
|
||||
{
|
||||
// 与所属组件的区域无交集 ⇒ 已经完全滚出地盘,不必登记(也避免越界画到邻居上)。
|
||||
if (rect.X + rect.Width <= m_clip.X || rect.X >= m_clip.X + m_clip.Width ||
|
||||
rect.Y + rect.Height <= m_clip.Y || rect.Y >= m_clip.Y + m_clip.Height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
UiDebugBox box;
|
||||
box.rect = rect;
|
||||
box.clip = m_clip;
|
||||
box.depth = (depth < 0) ? 0 : depth;
|
||||
box.layer = m_layer;
|
||||
m_boxes.push_back(box);
|
||||
}
|
||||
|
||||
void UiDebugBoxList::AddAll(const std::vector<gd::RectF>& rects, int depth)
|
||||
{
|
||||
for (const gd::RectF& rect : rects)
|
||||
{
|
||||
Add(rect, depth);
|
||||
}
|
||||
}
|
||||
|
||||
void UiDebugBoxList::AddOccluder(const gd::RectF& rect)
|
||||
{
|
||||
if (!(rect.Width > 0.0f) || !(rect.Height > 0.0f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Occluder occluder;
|
||||
occluder.rect = rect;
|
||||
occluder.layer = m_layer;
|
||||
m_occluders.push_back(occluder);
|
||||
}
|
||||
|
||||
std::uint32_t UiDebugBoundsColor(int depth)
|
||||
{
|
||||
// 与主题无关的固定配色:调试层必须"永远显眼",不能跟随用户主题变成接近底色。
|
||||
@@ -152,11 +95,6 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
void UiDebugPaintBounds(gd::Graphics& g, const gd::RectF& rect, int depth)
|
||||
{
|
||||
UiDebugPaintBoundsClipped(g, rect, gd::RectF(), depth);
|
||||
}
|
||||
|
||||
void UiDebugPaintBoundsClipped(gd::Graphics& g, const gd::RectF& rect, const gd::RectF& clip,
|
||||
int depth)
|
||||
{
|
||||
@@ -174,7 +112,7 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// ⚠️ 与裁剪矩形求交(见 `UiDebugBoxList::SetClip`):组件内部的元素可能落在地盘之外。
|
||||
// ⚠️ 与裁剪矩形求交(见 `UiBoxTable::SetClip`):组件内部的元素可能落在地盘之外。
|
||||
// 用 floor/ceil 而不是取整:裁剪是"能看见多少就画多少",宁可多一像素也不要把
|
||||
// 紧贴边界的包角整个裁掉。
|
||||
if (clip.Width > 0.0f && clip.Height > 0.0f)
|
||||
@@ -240,7 +178,7 @@ namespace jdi
|
||||
g.Restore(state);
|
||||
}
|
||||
|
||||
void UiDebugPaintBoxList(gd::Graphics& g, const UiDebugBoxList& boxes)
|
||||
void UiDebugPaintBoxList(gd::Graphics& g, const UiBoxTable& boxes)
|
||||
{
|
||||
if (boxes.Empty())
|
||||
{
|
||||
@@ -251,14 +189,14 @@ namespace jdi
|
||||
// * `layer` 保证"后收集的(更靠上的浮层)压住先收集的" —— 没有它,信号面板的行边界
|
||||
// 会盖在右键弹出的二级菜单上面;
|
||||
// * `depth` 保证同一层内先画父再画子,子元素的彩色包角不会被父的边压住。
|
||||
std::vector<const UiDebugBox*> ordered;
|
||||
std::vector<const UiBox*> ordered;
|
||||
ordered.reserve(boxes.Count());
|
||||
for (const UiDebugBox& box : boxes.Items())
|
||||
for (const UiBox& box : boxes.Items())
|
||||
{
|
||||
ordered.push_back(&box);
|
||||
}
|
||||
std::stable_sort(ordered.begin(), ordered.end(),
|
||||
[](const UiDebugBox* lhs, const UiDebugBox* rhs)
|
||||
[](const UiBox* lhs, const UiBox* rhs)
|
||||
{
|
||||
if (lhs->layer != rhs->layer)
|
||||
{
|
||||
@@ -267,14 +205,14 @@ namespace jdi
|
||||
return lhs->depth < rhs->depth;
|
||||
});
|
||||
|
||||
for (const UiDebugBox* box : ordered)
|
||||
for (const UiBox* box : ordered)
|
||||
{
|
||||
// ⚠️ 遮挡:浮层(菜单 / 拾色器)是不透明的,它上面的内容不该出现下层组件的边框线。
|
||||
// 仅靠 layer 排序只能决定"两个 box 重叠时谁压谁",不能阻止"先画的线仍然可见",
|
||||
// 因此这里把**更高层**的遮挡矩形从裁剪区里排除。
|
||||
gd::Region region;
|
||||
bool occluded = false;
|
||||
for (const UiDebugBoxList::Occluder& occluder : boxes.Occluders())
|
||||
for (const UiBoxTable::Occluder& occluder : boxes.Occluders())
|
||||
{
|
||||
if (occluder.layer <= box->layer)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "UiDebugBounds.hpp"
|
||||
#include "UiBox.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
@@ -255,7 +255,7 @@ namespace jdi
|
||||
return UiRectHasArea(TakeDirty());
|
||||
}
|
||||
|
||||
void UiWidget::CollectDebugBoxes(UiDebugBoxList& boxes, int depth) const
|
||||
void UiWidget::BuildBoxes(UiBoxTable& boxes, int depth) const
|
||||
{
|
||||
if (!m_visible || !UiRectHasArea(m_bounds))
|
||||
{
|
||||
@@ -270,7 +270,7 @@ namespace jdi
|
||||
{
|
||||
if (child != nullptr)
|
||||
{
|
||||
child->CollectDebugBoxes(boxes, depth + 1);
|
||||
child->BuildBoxes(boxes, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
class UiDebugBoxList;
|
||||
class UiBoxTable;
|
||||
|
||||
/**
|
||||
* @brief 信号面板弹出菜单的命令标识。
|
||||
@@ -366,19 +366,19 @@ namespace jdi
|
||||
HCURSOR QueryCursor(int x, int y) override;
|
||||
|
||||
/**
|
||||
* @brief 收集面板区域与其内部结构(表头 / 筛选框 / 列表 / 样式区 / 可见行 / 行内元素)
|
||||
* 的布局边界(调试用)。
|
||||
* @brief 上报面板区域与其内部结构(表头 / 筛选框 / 列表 / 样式区 / 可见行 / 行内元素)
|
||||
* 的几何 box。
|
||||
*
|
||||
* 行内元素(复选框、色块、名称、GX 徽标、✕、折起三角、组计数)的几何与
|
||||
* `RowHitPart` / `PaintRow` **同源**:都基于同一个 `RowRect` 与同一批常量,
|
||||
* 因此"画出来的框"就是"能点到的区域"。
|
||||
*
|
||||
* 面板的行位图缓存与本函数**无关**:调试 box 只由宿主在最后一步统一描边,
|
||||
* 不进任何缓存 ⇒ 关掉开关后不会留下残影。
|
||||
* 面板的行位图缓存与本函数**无关**:box 只交给宿主在最后一步统一处理(目前是描边),
|
||||
* 不进任何缓存 ⇒ 关掉「显示布局边界」后不会留下残影。
|
||||
*
|
||||
* @param [IN,OUT] boxes 收集器。
|
||||
*/
|
||||
void CollectDebugBoxes(UiDebugBoxList& boxes) override;
|
||||
void BuildBoxes(UiBoxTable& boxes) override;
|
||||
|
||||
/**
|
||||
* @brief 将列表滚动到指定信号所在位置(无动画,立即到位)。
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
// SignalPanelMenu.cpp 弹出菜单:系数/偏移、镜像、右键菜单与命令回执
|
||||
#include "SignalPanelInternal.hpp"
|
||||
|
||||
#include "UiDebugBounds.hpp"
|
||||
#include "UiBox.hpp"
|
||||
#include "UiLayout.hpp"
|
||||
|
||||
namespace jdi
|
||||
@@ -72,9 +72,9 @@ namespace jdi
|
||||
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
void SignalPanel::CollectDebugBoxes(UiDebugBoxList& boxes)
|
||||
void SignalPanel::BuildBoxes(UiBoxTable& boxes)
|
||||
{
|
||||
UiComponent::CollectDebugBoxes(boxes);
|
||||
UiComponent::BuildBoxes(boxes);
|
||||
|
||||
if (m_fonts == nullptr || !UiRectHasArea(m_area))
|
||||
{
|
||||
|
||||
@@ -225,10 +225,10 @@ namespace jdi
|
||||
bool HitTest(int x, int y) const override;
|
||||
|
||||
/**
|
||||
* @brief 收集标签栏区域、**每个标签条 / 新建按钮 / 标签 ✕** 的布局边界(调试用)。
|
||||
* @brief 上报标签栏区域、**每个标签条 / 新建按钮 / 标签 ✕** 的几何 box。
|
||||
* @param [IN,OUT] boxes 收集器。
|
||||
*/
|
||||
void CollectDebugBoxes(UiDebugBoxList& boxes) override;
|
||||
void BuildBoxes(UiBoxTable& boxes) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
|
||||
#include "UiDebugBounds.hpp"
|
||||
#include "UiBox.hpp"
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
namespace jdi
|
||||
@@ -484,9 +484,9 @@ namespace jdi
|
||||
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
void TabBar::CollectDebugBoxes(UiDebugBoxList& boxes)
|
||||
void TabBar::BuildBoxes(UiBoxTable& boxes)
|
||||
{
|
||||
UiComponent::CollectDebugBoxes(boxes);
|
||||
UiComponent::BuildBoxes(boxes);
|
||||
if (m_root != nullptr)
|
||||
{
|
||||
// ⚠️ 跳过根控件:它的矩形与标签栏区域完全重合(已被上一层登记过)。
|
||||
@@ -495,7 +495,7 @@ namespace jdi
|
||||
{
|
||||
if (child != nullptr)
|
||||
{
|
||||
child->CollectDebugBoxes(boxes, 1);
|
||||
child->BuildBoxes(boxes, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,10 +154,10 @@ namespace jdi
|
||||
bool HitTest(int x, int y) const override;
|
||||
|
||||
/**
|
||||
* @brief 收集工具栏区域、**每个按钮**与按钮内容(文本/图标)的布局边界(调试用)。
|
||||
* @brief 上报工具栏区域、**每个按钮**与按钮内容(文本/图标)的几何 box。
|
||||
* @param [IN,OUT] boxes 收集器。
|
||||
*/
|
||||
void CollectDebugBoxes(UiDebugBoxList& boxes) override;
|
||||
void BuildBoxes(UiBoxTable& boxes) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
#include "UiDebugBounds.hpp"
|
||||
#include "UiBox.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
@@ -351,9 +351,9 @@ namespace jdi
|
||||
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
void Toolbar::CollectDebugBoxes(UiDebugBoxList& boxes)
|
||||
void Toolbar::BuildBoxes(UiBoxTable& boxes)
|
||||
{
|
||||
UiComponent::CollectDebugBoxes(boxes);
|
||||
UiComponent::BuildBoxes(boxes);
|
||||
if (m_root == nullptr)
|
||||
{
|
||||
return;
|
||||
|
||||
+115
-556
@@ -1,594 +1,153 @@
|
||||
# UI 重构计划(UI_Refactor 分支)
|
||||
# Just DataInspector UI 重构 —— 计划
|
||||
|
||||
> 目标读者:本项目的后续 Agent(可能是另一台设备 / 另一个模型)。
|
||||
> 本文是**唯一施工蓝图**;实时进度见 `Documents/UI_Refactor_Progress.md`,最终结论见 `Documents/UI_Refactor_Report.md`。
|
||||
> 硬约束的权威来源仍是 `Documents/agent_mem.md`(§11.2);本文与之冲突时**以 agent_mem.md 为准**。
|
||||
> 本文档只描述**要做什么、为什么这么做、怎么验收**。
|
||||
> 已完成情况见 `UI_Refactor_Progress.md`,结论与遗留见 `UI_Refactor_Report.md`。
|
||||
> 最后更新:2026-10-29
|
||||
|
||||
---
|
||||
|
||||
## 0. 为什么要重构(量化基线)
|
||||
## 1. 目标与非目标
|
||||
|
||||
| 指标 | 现状 | 出处 |
|
||||
### 1.1 目标
|
||||
|
||||
| # | 目标 | 判据 |
|
||||
| --- | --- | --- |
|
||||
| `PlotViewPaint.cpp` | **9 080 行**(注释 1 295 / 空行 517 / 仅大括号 2 026 / **有效代码 5 242**) | `Get-Content \| Measure-Object` |
|
||||
| `PlotView.hpp` | 3 649 行、**310 个方法**、**203 个成员变量**、31 个嵌套类型 | 同上 |
|
||||
| `EdgePanelLayoutFor` | **651 行**、**19 个调用点**,每帧被命中/层序/光标/绘制各算一遍 | `Select-String` |
|
||||
| `Application.cpp` 的 `InvalidateRect` | **56 处** + 手写 `m_chromeDirty` / `m_panelDirty` / `panelOnly` / `fullFrame` 组合 | 同上 |
|
||||
| 动画 | **5 套**手写动画散在 3 个组件,共用一个 10ms 定时器 | 同上 |
|
||||
| 自动化 UI 探针 | **0**(`.preflight` 已被删除且被 gitignore) | 仓库现状 |
|
||||
| G1 | 收敛"宿主点名每个组件"的散乱结构 | 宿主只遍历一张组件表,不再 `if (component == plot)` 式分支 |
|
||||
| G2 | 几何只有**一处**来源 | "画出来的形状"与"能点到的区域"由同一批函数给出,不再各算一遍 |
|
||||
| G3 | 源码按**职责**分片,单片可读 | 单个 `.cpp` 不超过约 1200 行;文件名与内容一致 |
|
||||
| G4 | 减小产物体积,且**仍然只有一个 `.exe`** | 体积比基线小,无新增依赖 DLL |
|
||||
| G5 | 关闭 C++ 异常后**不牺牲健壮性** | 所有大块分配前有预检验;OOM 有可见提示 |
|
||||
| G6 | 重构**不得改变任何绘制像素** | 8 个场景的逐像素哈希与基线逐一相同 |
|
||||
|
||||
`PlotViewPaint.cpp` 里混住四类内容:真绘制 ≈3.5k / 光栅化缓存 ≈2.5k / 缓存键 ≈1.1k / **布局与命中几何 ≈1.3k**。
|
||||
### 1.2 非目标(明确不做)
|
||||
|
||||
**五条根因**
|
||||
|
||||
1. 上次拆 13 695 行大文件时按**函数名**("带 `Paint` 前缀")而非**关注点**分片 ⇒ 缓存、缓存键、面板布局、命中几何全被倒进 paint 分片。
|
||||
2. 没有控件抽象 ⇒ 每个叠加层自带"几何 + 命中 + 绘制 + 缓存键"四份重复;5 个实验性面板各 800~1 200 行,靠 10 多个平行数组(`rowRefs/rowChecks/rowPoints/rowOrder/rowLevelTracks/...`)下标对齐"假装"成树。
|
||||
3. GDI / GDI+ 双栈妥协被**每个绘制点重复支付**(GDI 文本不认 GDI+ 裁剪、`UiFillRect` 的 GDI 快路径绕过裁剪、GDI 文本忽略 alpha…)。
|
||||
4. "必须混进缓存键,否则贴旧像素"这条不变量**没有归属**(4 套手写 key + 一堆 `Collect*` ≈1.1k 行;历史 ≥4 次真实 BUG 是"忘了混一个字段")。
|
||||
5. 1 295 行硬核"为什么"注释让行数虚高 —— **保留,不删**(多 Agent 协作防回归的核心资产)。
|
||||
* **不把编译产物拆成 DLL**:本项目的定位是"单文件、拷过去就能跑的轻量工具"。
|
||||
任何以拆 DLL 换体积的方案一律否决。
|
||||
* **不改优化等级**(`-Oz` 保持不变)。
|
||||
* **不把"波形"与"网格线"拆成两层位图**:像素级平移不变量会被破坏。
|
||||
* **不为体积牺牲可维护性**(例如放弃分片、放弃几何同源)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计目标与非目标
|
||||
## 2. 硬约束
|
||||
|
||||
### 1.1 目标(必须达成)
|
||||
|
||||
1. **几何只算一次**:布局是"按需失效的 pass",命中/绘制/光标共用同一份结果。
|
||||
2. **失效从布尔变矩形**:`dirty ∩ bounds` 为空 ⇒ 整棵子树跳过(这是唯一真正省钱的层次)。
|
||||
3. **裁剪契约统一**:`Paint` 进入时同时收紧 GDI 裁剪与 GDI+ 裁剪,根治"文字画到图例/工具栏上"这类已出现 3 次的 BUG。
|
||||
4. **缓存键有归属**:由"持有该内容的节点"产生,父层只聚合。
|
||||
5. **事件派发显式化**:`{consumed, redraw, dirtyRect}` 代替 `bool 要不要重绘`;capture / 模态 / hover / 焦点 / 光标仲裁由基类提供。
|
||||
6. **动画统一 tick**:一个动画列表 + 现有 `kScrollTimerId` 推进。
|
||||
7. **零行为变更的先行阶段**:先拆文件、补回归网,再动语义。
|
||||
|
||||
### 1.2 非目标(明确不做,写进禁区)
|
||||
|
||||
| 禁区 | 原因 |
|
||||
| --- | --- |
|
||||
| ❌ 波形数据→像素管线控件化(网格/曲线/采样点不做成节点) | 布局与虚调用会进每帧热路径;采样/顶点量与控件无关 |
|
||||
| ❌ 每个控件自持位图 | 内存按面积线性增长:逐网格图层已是屏幕面积 2 倍(5 网格 ≈16MB,**128 网格 ≈250MB**) |
|
||||
| ❌ "父脏 ⇒ 子树全脏" | 会把"只重绘一个网格/一个面板"打回整块重绘(历史最易发生的退化) |
|
||||
| ❌ 多线程光栅 | GDI+ 内部串行,实测 **1.03x**;且后台改写 `m_area`/`ViewMin` 会覆盖用户平移(第一代已因此回退) |
|
||||
| ❌ 逐控件细粒度像素缓存 | 画一个按钮 20~80µs vs 贴图 5~10µs ⇒ 净亏损,还要付"忘混键"的风险 |
|
||||
| ❌ 引入属性对象体系(学 Blink) | 那是为 Blink 规模准备的;本项目用"节点产生 key"即可 |
|
||||
|
||||
### 1.3 借鉴来源与取舍
|
||||
|
||||
| 借鉴 | 采纳 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Chromium `views::View` 的**失效驱动布局 pass** | ✅ 全采纳 | 对应 §3 的 `UiWidget::Measure/Arrange` + `UiLayoutCache` |
|
||||
| `views::View::PaintChildren` 的**脏矩形下探** | ✅ 全采纳 | 对应 §4 的 `PaintContext.dirty` |
|
||||
| `SetPaintToLayer()` 只提升**少数**子树 | ✅ 保持现状量级(4 层) | 不再新增层 |
|
||||
| `cc` 的**记录-光栅分离 / tiling / checkerboarding / 每帧预算** | ✅ 已具备,不改 | `PolylineCacheEntry`、宽基底+补条、贴过期但位置正确、`RenderPendingLaneRebuilds(1)` |
|
||||
| `cc` 的**多线程光栅 / GPU 合成** | ❌ 不采纳 | GDI+ 无 GPU 路径;并行实测 1.03x |
|
||||
| Dear ImGui 的**每帧全量重画** | ❌ 不采纳 | 本项目整幅重绘 ≈80ms,必须走失效 |
|
||||
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`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 目标架构
|
||||
## 3. 分层架构(L0–L3)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
H[UiHost<br/>持有 app-sized 32bpp top-down DIB<br/>z 序 · 脏区并集 · 一次 BitBlt 回送 · 动画 tick]
|
||||
H --> R[UiRoot : UiWidget]
|
||||
R --> D{脏区树<br/>dirty ∩ bounds 空 ⇒ 整棵子树 return}
|
||||
D --> A[普通控件:直接画进共享缓冲<br/>按钮 / 文本 / 列表行 / 时间轴 / 图例]
|
||||
D --> B[高成本叶子:自持位图,完成即 BitBlt 贴回<br/>波形网格 / 装饰层 / 光晕 / 浮动面板]
|
||||
A --> S[SurfaceView{ dc, rect, stride, bits, version }]
|
||||
B --> S
|
||||
```
|
||||
L3 体积与异常策略 -fno-exceptions / MemProbe / LTO / gc-sections
|
||||
▲
|
||||
L2 重绘范围控制 UiDamage(脏矩形) ← 本轮评估后**缓做**,见 §5
|
||||
▲
|
||||
L1 几何产物层 UiBox / UiBoxTable / UiComponent::BuildBoxes
|
||||
▲
|
||||
L0 组件协议 UiComponent + 组件表 + UiWidget 控件树
|
||||
```
|
||||
|
||||
**分层原则**
|
||||
### L0 组件协议(前几轮已完成)
|
||||
|
||||
- **共享一张缓冲**:省内存、免分配抖动、合成顺序天然确定。
|
||||
- **只有高成本叶子自持缓冲**:这是"把重画换成贴图"的唯一手段。
|
||||
- **脏区向下取交集**(不是并集)。
|
||||
- **布局由下往上、每帧最多一轮 Measure**;尺寸由内容反推(`EdgePanelWidth` 就是这种)。
|
||||
* `UiComponent`:`SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor`。
|
||||
* 宿主持有一张**有序组件表**,逐个遍历;新增组件不必改宿主的 5 处消息分支。
|
||||
* `UiWidget` 与 `UiComponent` 是**兄弟**而非父子:前者管"一块区域内部的布局/命中/绘制",
|
||||
后者管"自己这一整块地盘:区域、缓存、局部重绘策略"。
|
||||
`Toolbar` / `TabBar` 同时是两者。
|
||||
|
||||
### 2.1 新增文件
|
||||
### L1 几何产物层(本轮完成)
|
||||
|
||||
| 文件 | 职责 | 依赖 |
|
||||
| --- | --- | --- |
|
||||
| `Components/Shared/Inc/UiLayout.hpp` / `Src/UiLayout.cpp` | 纯几何:`UiInsets`、栈/流/锚点/对齐、`UiLayoutCache` | 仅 `Theme.hpp`(`gd::RectF`) |
|
||||
| `Components/Shared/Inc/UiWidget.hpp` / `Src/UiWidget.cpp` | 控件基类:树、Measure/Arrange、双栈裁剪 Paint、事件派发、失效、capture/模态/hover/焦点/光标、动画 tick | `UiLayout.hpp`、`PaintUtil.hpp` |
|
||||
| `Components/Shared/Inc/UiHost.hpp` / `Src/UiHost.cpp` | 宿主:共享 DIB + `SurfaceView` + 脏区并集 + 回送 + 消息翻译 + 动画定时器 | `UiWidget.hpp` |
|
||||
| `Tools/uicheck/`(入库) | 离屏回归网(`SW_HIDE` + 注入消息 + 从 `m_backDc` 取帧) | `Build/Release/*.o` |
|
||||
* 新文件 `Components/Shared/Inc/UiBox.hpp` + `Src/UiBox.cpp`。
|
||||
* `struct UiBox { rect, clip, depth, layer }`:**纯值类型**,不继承任何东西。
|
||||
* `class UiBoxTable`:几何收集器,只维护"矩形 / 层级 / 绘制层 / 裁剪 / 遮挡"这些**几何事实**,
|
||||
**不含任何绘制代码**。
|
||||
* `UiComponent::BuildBoxes(UiBoxTable&)`:组件上报自己的几何产物(默认只登记 `Area()`)。
|
||||
* `UiDebugBounds` 退化为**消费者**:`UiDebugPaintBoxList(g, table)` 把几何画成彩色包角线。
|
||||
⇒ 组件不再需要知道"调试"这个概念存在。
|
||||
|
||||
> ⚠️ `Components/Shared` 已在 `Makefile` 的 `CXX_INCLUDES_PATHS` / `CXX_SOURCES_PATHS` 中 ⇒ 新增 `.cpp` **无需改 Makefile**(按 basename 自动收集)。
|
||||
### L2 重绘范围控制(缓做 —— 见 §5)
|
||||
|
||||
* 设计意图:`UiDamage`(最多 12 个矩形的脏区集合,超限退化为包围盒)替代
|
||||
"全窗重绘 / 绘图区重绘"的二值分支;`PlotView::Paint(g, UiDamage&)` 逐路径累加脏区。
|
||||
* 收益:去掉 `SetForceFullRepaint`(布局边界开关打开时的整幅重绘)。
|
||||
|
||||
### L3 体积与异常策略(本轮完成)
|
||||
|
||||
见 `UI_Refactor_Progress.md` 的实测表。
|
||||
|
||||
---
|
||||
|
||||
## 3. 布局层设计(`UiLayout`)
|
||||
## 4. 阶段与验收标准
|
||||
|
||||
### 3.1 为什么不是简单的"宽高字段"
|
||||
|
||||
基类若只有 `width/height`,会在三处立刻失效:
|
||||
|
||||
1. 所有坐标是**逻辑像素 × scale**(本机 DPR 1.25),控件必须知道缩放;
|
||||
2. 面板宽度是**由内容反推**(`EdgePanelWidth` = 列宽 max + 行 ✕ + 滚动条预留);
|
||||
3. 离屏位图要求**设备像素整数对齐**(`floor`/`ceil` 方向错了就留 1px 接缝 —— 样式补丁曾踩过)。
|
||||
|
||||
⇒ 采用 **`Measure(约束) → 期望尺寸` / `Arrange(最终矩形) → 实际矩形`** 两段式。
|
||||
|
||||
### 3.2 需要的布局种类(按实际用量排序)
|
||||
|
||||
| 布局 | 用途 | 备注 |
|
||||
| 阶段 | 内容 | 验收 |
|
||||
| --- | --- | --- |
|
||||
| **Abs(绝对)** | 绘图区叠加物主力:光标线、取值胶囊、吸附环、越界三角、固定点、面板初始位置、滚动条 | 保留"自由布局",但它是**主力**而非备选 |
|
||||
| **Anchor(四边贴合)** | 工具栏整体靠右、时间轴贴底、滚动条贴右、面板夹在 `m_plotRect` 内 | 讨论中缺失,必须补 |
|
||||
| **Stack(竖向流)** | 信号列表行、分组行、面板条目(名称行 + 参数行 + 滑块行) | 需支持"行高由内容决定 + 吸顶(sticky) + 滚动视口" |
|
||||
| **Flow(横向流 + 溢出)** | 图例条目 + 空间不足的 `+N`、工具栏按钮组 | 讨论中缺失,必须补 |
|
||||
| **Measure-driven** | 面板宽度按实测文本撑开、列宽 `max(缺省, 内容, 列标题)` | 必须有,否则又回到"绘制侧省略号" |
|
||||
| Grid | **用不上**(绘图区不是表格) | 不实现 |
|
||||
| Card | 只是"面板皮肤" | 作为 `UiPanelSkin` 绘制样式,不是布局 |
|
||||
| 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` 干净 |
|
||||
|
||||
### 3.3 `UiLayoutCache`(本次最关键的性能点)
|
||||
**统一验收命令**
|
||||
|
||||
`EdgePanelLayoutFor` 的 19 个调用点全部来自"同一份几何被反复问"。做法:
|
||||
```powershell
|
||||
make clean
|
||||
make release TARGET_FILE_NAME=JustDataInspector -j16 # 期望零 error / 零 warning
|
||||
make debug TARGET_FILE_NAME=JustDataInspectorD -j16 # 期望零 warning
|
||||
|
||||
```cpp
|
||||
class UiLayoutCache
|
||||
{
|
||||
public:
|
||||
// key 由调用方提供(例如 laneIndex + kind + 面板宽 + 缩放 + 条目数)
|
||||
const LayoutEntry* Find(std::uint64_t key) const;
|
||||
const LayoutEntry& Store(std::uint64_t key, LayoutEntry&& entry);
|
||||
void Invalidate(std::uint64_t key); // 定向失效
|
||||
void Clear();
|
||||
};
|
||||
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
|
||||
```
|
||||
|
||||
**硬约束**:`Invalidate` 必须由"改变几何的那条路径"**显式**调用(与 `m_edgeScans` 的定向失效同一纪律),**禁止**为了让缓存命中率好看而整体 `Clear()`。
|
||||
---
|
||||
|
||||
## 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`,不需要重新收集几何。
|
||||
|
||||
---
|
||||
|
||||
## 4. 控件基类设计(`UiWidget`)
|
||||
## 6. 交付物清单
|
||||
|
||||
### 4.1 接口骨架
|
||||
|
||||
```cpp
|
||||
struct UiEventResult
|
||||
{
|
||||
bool consumed = false; // 事件已被消费(不再下传/上抛)
|
||||
bool redraw = false; // 需要重绘
|
||||
gd::RectF dirty; // 需要重绘的矩形(客户区坐标;空 = 未知 ⇒ 取自身 bounds)
|
||||
};
|
||||
|
||||
class UiWidget
|
||||
{
|
||||
public:
|
||||
virtual ~UiWidget();
|
||||
|
||||
// ---- 树 ----
|
||||
void AddChild(std::unique_ptr<UiWidget> child);
|
||||
void RemoveChild(UiWidget* child);
|
||||
const std::vector<std::unique_ptr<UiWidget>>& Children() const;
|
||||
|
||||
// ---- 布局(两段式,各自带失效)----
|
||||
void InvalidateMeasure(); // 向上冒泡到根
|
||||
void InvalidateArrange();
|
||||
gd::SizeF Measure(const UiMeasureContext& ctx); // 自下而上,命中缓存则直接返回
|
||||
void Arrange(const gd::RectF& bounds); // 自上而下;bounds 一定 > 0
|
||||
|
||||
// ---- 绘制(双栈裁剪 + 脏区下探)----
|
||||
void Paint(UiPaintContext& ctx); // 内部:与 dirty 求交 → OnPaint → 递归子节点
|
||||
virtual void Invalidate(const gd::RectF& rect); // 局部失效(向上冒泡并入)
|
||||
|
||||
// ---- 事件 ----
|
||||
UiEventResult Dispatch(UiEvent& event); // 命中 → 子节点(反 z 序)→ OnEvent
|
||||
|
||||
// ---- 命中 / 光标 ----
|
||||
bool Contains(int x, int y) const;
|
||||
virtual HCURSOR QueryCursor(int x, int y) { return nullptr; }
|
||||
|
||||
// ---- 交互仲裁(由 Host 调用)----
|
||||
UiWidget* Capture(); void SetCapture(); void ReleaseCapture();
|
||||
void SetModal(bool modal); // 模态子树:Host 只向它派发
|
||||
void SetHitTransparent(bool value); // 装饰元素:不参与命中
|
||||
|
||||
// ---- 动画 ----
|
||||
virtual bool AdvanceAnimation(std::int64_t nowMs) { return false; }
|
||||
|
||||
protected:
|
||||
virtual gd::SizeF OnMeasure(const UiMeasureContext& ctx) = 0;
|
||||
virtual void OnArrange() {}
|
||||
virtual void OnPaint(UiPaintContext& ctx) = 0;
|
||||
virtual UiEventResult OnEvent(UiEvent& event) { return {}; }
|
||||
virtual std::uint64_t ContentKey() const; // 缓存键的来源(父层只聚合)
|
||||
};
|
||||
```
|
||||
|
||||
### 4.2 `Paint` 的硬契约(必须由基类保证,不是"每个控件记得")
|
||||
|
||||
1. **双栈裁剪**:进入时 `SaveDC + IntersectClipRect`(GDI)+ `Graphics::SetClip(Intersect)`(GDI+),退出时还原。这一条一次性消灭"滚动时文字串到标题栏""全选高亮溢出输入框""选区画到工具栏上"。
|
||||
2. **脏区下探**:`ctx.dirty = ctx.dirty ∩ bounds`;若为空 ⇒ **整棵子树直接 return**(不调 `OnPaint`、不遍历子节点)。
|
||||
3. **绘制顺序 = 子节点顺序 = z 序**;命中/事件 = **反 z 序**(与现在 `m_panelOrder` + `TopPanelAtPoint` 反向遍历等价,但不再靠人工维护)。
|
||||
4. **禁止**在 `OnPaint` 内改 `bounds`/子节点集合(会与本帧的 dirty 不一致)。
|
||||
|
||||
### 4.3 事件派发的硬契约
|
||||
|
||||
- **`consumed`** 决定是否上抛;**`dirty`** 参与本帧脏区并集。
|
||||
- **capture**:抓取期间所有鼠标事件**直接投给抓取者**(跳过命中),与 Windows `SetCapture` 语义一致。本项目硬约束:中键平移与左键拖拽**共用同一个 capture**,松左键**不能**无条件 `ReleaseCapture()`。
|
||||
- **模态**:菜单打开时 Host 只向模态子树派发,且"取消底层拖拽"时必须**排除菜单自己的 `EDIT_FIELD`**。
|
||||
- **hover**:Host 维护 hover 链,跨节点 `leave/enter`;悬停是**跨组件**的(面板行 → 网格固定点高亮;列表行 → 曲线光晕)⇒ 通过 Host 级"悬停信号总线"广播,不靠父子关系。
|
||||
- **双击消歧**:窗口带 `CS_DBLCLKS`;没有双击语义的位置必须在 `DoubleClickIsPlainPress()` 返回 true,由 Host 改走普通按下(否则"连点丢一半")。
|
||||
|
||||
### 4.4 失效契约
|
||||
|
||||
- 节点持 `m_dirty`(自身坐标系)+ `m_dirtyChildren`;
|
||||
- `Invalidate(rect)` 并入自身并**向上冒泡**(父层只并集矩形,**绝不**把子树的 dirty 清零);
|
||||
- Root 暴露 `TakeDirty()` 给 Host,Host 再与窗口更新区域取并集。
|
||||
|
||||
---
|
||||
|
||||
## 5. 宿主层设计(`UiHost`)
|
||||
|
||||
1. **单一 app-sized 32bpp top-down DIB**(替换现在的 `CreateCompatibleBitmap`):
|
||||
```cpp
|
||||
struct SurfaceView
|
||||
{
|
||||
HDC dc; // 共享 HDC(GDI/GDI+ 都画这个)
|
||||
gd::RectF rect; // 本节点在 Holder 坐标系中的矩形(设备像素,整数对齐)
|
||||
int stride; // 行字节数(**不可**假设 = width*4)
|
||||
std::uint32_t* bits; // rect 左上角那一像素(不是位图首地址)
|
||||
std::uint64_t version; // 位图代次;失配 ⇒ 句柄作废
|
||||
};
|
||||
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 重建
|
||||
```
|
||||
⚠️ **绝不能缓存裸指针**:`EnsureBackBuffer` 在 `WM_SIZE` / `WM_DPICHANGED` 会重建位图。
|
||||
⚠️ `bits` 只在 **CPU 侧直写**(memcpy/alpha)时用;GDI/GDI+ 绘制只需 `dc + rect`。
|
||||
2. **脏区并集**:把现在 56 处 `InvalidateRect` 收敛为 `Host::Invalidate(rect)`,一次回送。
|
||||
⚠️ 回送仍必须**有无条件 `else`**(历史:某分支缺兜底 ⇒ 窗口一个像素都不回送,用户看到三个不同症状)。
|
||||
3. **动画 tick**:`AdvanceAnimation` 遍历树,任一为 true 就保持 `kScrollTimerId`(10ms)。
|
||||
4. **消息翻译**:`WM_*` → `UiEvent`,并把 `WM_SETCURSOR` 交给 `QueryCursor` 链(从 z 顶向下、首个非 null 生效)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 落地阶段(每阶段一个可独立回滚的提交)
|
||||
|
||||
| 阶段 | 内容 | 交付判据 |
|
||||
| --- | --- | --- |
|
||||
| **P0** | 离屏回归网 `Tools/uicheck`(入库)+ 现有 20+ 条硬约束的断言骨架 | ✅ **已完成**(69 项断言) |
|
||||
| **P1** | `UiLayout` + `UiWidget` 基础层(本次) | ✅ **已完成**(编译零警告;`uicheck` 覆盖) |
|
||||
| **P2** | 零行为变更分片:`PlotViewPaint.cpp` 拆成 `Paint`(只画)/ `Raster`(缓存与预算)/ `CacheKey`(指纹)/ `Geometry`(面板与命中);`EdgePanelLayoutFor`、`ProbeFreeMarker` 迁回交互侧 | ✅ **已完成**(**逐像素 0 差异**) |
|
||||
| **P3** | 迁移 chrome:Toolbar → TabBar → PopupMenu → ColorPicker → SignalPanel 行 | ✅ Toolbar/TabBar 已迁移;**PopupMenu/ColorPicker/SignalPanel 行经评估不采用**(见 Report §7 的结论与理由) |
|
||||
| **P4** | 迁移 5 个浮动面板 | **经评估不采用**(面板布局已是单一函数且已缓存像素层;控件化无帧时间收益且会浮空吸顶/滚动语义,见 Report §7) |
|
||||
| **P5** | 波形管线包成叶子 surface(**只包不改**)+ 共享 DIB 落地 | 🔄 **共享 DIB + `UiSurface` 已完成**;「56 处失效收敛为脏区并集」经评估**不采用**(`InvalidatePlotRect()` 已是按消息的批处理点,逐处替换只增间接层,详见 Report §7) |
|
||||
|
||||
**P2 之前禁止改任何行为**。P2 是唯一的"免费"阶段,别顺手改语义。
|
||||
|
||||
---
|
||||
|
||||
## 7. 验证策略(不占用用户鼠标/键盘/屏幕)
|
||||
|
||||
1. **离屏 harness**:`SW_HIDE` 窗口 + 直接 `app.HandleMessage(...)` 注入 + 从 `m_backDc` 用 `GetDIBits` 取帧。
|
||||
2. **进程/线程优先级 `BELOW_NORMAL`**;从不用 `SetCursorPos` / `SendInput` / 截屏。
|
||||
3. **A/B 反证**:每条新断言都要能被"还原旧实现"证伪,否则不算通过。
|
||||
4. **像素判据要选对区域**:验证"某交互不应改变外观"时,只对**不受影响**的区域取哈希。
|
||||
5. **已知陷阱**(全部来自历史 BUG):
|
||||
- 探针消息泵必须丢弃 `WM_MOUSELEAVE`(离屏窗口会立刻兑现它);
|
||||
- 注入按下后立即 `ReleaseCapture()`(避免抢真实鼠标);
|
||||
- `CycleCursorMode()` 是**循环**,多组用例里重复调用会把光标关掉;
|
||||
- `PlotView` 的嵌套类型(`LaneVisual`/`LegendHit`/`EdgePanelLayout`)使用时要写 `PlotView::` 前缀。
|
||||
|
||||
---
|
||||
|
||||
## 8. 协作纪律(多 Agent)
|
||||
|
||||
1. **文件所有权**:同一时刻**一个 Agent 只改一个文件的同一区域**;跨阶段并行时按下表分工,禁止交叉。
|
||||
2. **每阶段一个提交**,提交信息写清"改了什么 / 为什么 / 怎么验证"。
|
||||
3. **先写断言再改代码**。
|
||||
4. `main` / `dev` 只接受全绿;`UI_Refactor` 内允许临时红,但**不许跨轮次累积红**。
|
||||
5. 任何"规则变更"必须同步进 `Documents/agent_mem.md`(§11.2 追加编号),否则另一端看不到。
|
||||
6. 体积红线 **900KB**(到达即提醒用户,不是禁止超过)。
|
||||
|
||||
### 8.1 并行分工建议(若启用多个 Agent)
|
||||
|
||||
| Agent | 独占文件 | 禁止触碰 |
|
||||
| --- | --- | --- |
|
||||
| A(基础层) | `UiLayout.*`、`UiWidget.*`、`UiHost.*`、`Tools/uicheck/*` | 任何现有组件 |
|
||||
| B(分片) | `PlotViewPaint.cpp` → 新分片文件、`PlotViewInternal.hpp` 的 include 段 | `UiWidget.*`、面板逻辑 |
|
||||
| C(面板迁移) | `PlotViewFeatures.cpp` 的面板段、新建 `PlotViewPanels.cpp` | `PlotViewPaint.cpp`、`UiLayout.*` |
|
||||
|
||||
> 冲突高发点:`Makefile`(新增目录时才需要改)、`agent_mem.md`(追加编号必须串行)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与回滚
|
||||
|
||||
| 风险 | 触发条件 | 回滚 |
|
||||
| --- | --- | --- |
|
||||
| 迁移后"连点丢一半" | `DoubleClickIsPlainPress` 语义漏实现 | 单组件提交可独立 revert |
|
||||
| 迁移后"拖动卡住" | capture 共用被拆成两套 | 同上 |
|
||||
| 面板失效不全(贴旧像素) | `ContentKey()` 漏字段 | P4 里每个面板配一条"改 X 后像素必须变"的断言 |
|
||||
| 体积超 900KB | 新框架代码量 | 每阶段结束量一次;超阈值先报告用户 |
|
||||
| 布局级联死循环 | Measure 里改子节点 | 单元测试固定"Measure 一轮即收敛" |
|
||||
|
||||
---
|
||||
|
||||
# 第五阶段计划:统一组件协议 + 去手写链路 + 拆分收尾(2026-10-28)
|
||||
|
||||
> 状态:✅ 已完成(详见 `UI_Refactor_Progress.md` 第七轮 / `UI_Refactor_Report.md` 第五阶段)
|
||||
|
||||
## 目标
|
||||
|
||||
1. 让"顶层组件"有统一协议,宿主不再逐个点名;
|
||||
2. 用同一套协议把 `SetScale` / `Layout` / `MouseLeave` / `SetCursor` 四条手写链改成循环;
|
||||
3. 把剩余两个"千行以上"的文件按**职责**拆分(不再按函数名前缀);
|
||||
4. 一轮代码审查(错误注释 / 过期注释 / 名实不符 / 残留引用);
|
||||
5. 把误入库的临时验证工具 `Tools/` 从提交树剥离并写入 ignore。
|
||||
|
||||
## 设计决定(先证明不是负优化)
|
||||
|
||||
* **协议只做"同签名归一"**:`UiComponent` 是纯接口、无状态,四个组件本就各自拥有
|
||||
完全同名同签名的方法 ⇒ 改造 = 加 `override` + 补 `Area()`,实现零改动、生成码几乎不变。
|
||||
* **不把 `UiComponent` 做成 `UiWidget` 的基类**:树节点是"父给尺寸 + 父下探脏区",
|
||||
而 `PlotView`(4 层位图缓存)/ `SignalPanel`(行位图缓存)是"地盘宿主"。
|
||||
强行上树会推翻缓存前提(历史事故:后台渲染覆盖平移、命中矩形错位)。
|
||||
* **不做的一律写明理由**:按下/松开/移动三条链带组件专属后置动作,保留显式分支。
|
||||
|
||||
## 任务清单
|
||||
|
||||
| # | 任务 | 验证手段 |
|
||||
| --- | --- | --- |
|
||||
| P5-1 | 新增 `Shared/Inc/UiComponent.hpp`,四组件继承 + `override` | 编译零警告 |
|
||||
| P5-2 | `Application`:`ComponentEntry` 表 + `BuildComponentTable()` | — |
|
||||
| P5-3 | `ApplyDpi` / `Relayout` / `WM_MOUSELEAVE` / `WM_SETCURSOR` 表驱动 | `uicheck` 69/69 |
|
||||
| P5-4 | `Relayout` 内容区改用 `UiStackRects`(布局库首个生产调用点) | `rendercheck` 8 哈希不变 |
|
||||
| P5-5 | `SignalPanelInteraction.cpp` 拆 4 片 + `SignalPanelEdit/Scroll/Menu.cpp` | 8 哈希不变 |
|
||||
| P5-6 | `PopupMenu.cpp` 拆 3 片 + `PopupMenuInternal.hpp` | 8 哈希不变 |
|
||||
| P5-7 | 代码审查(注释准确性、构造函数重复、include 位置) | 人工 + 编译 |
|
||||
| P5-8 | `git rm -r --cached Tools` + `.gitignore` + 历史过滤器 | `git ls-files Tools` 为空 |
|
||||
| P5-9 | 三份文档更新 + 提交 | `git status` 干净 |
|
||||
|
||||
## 风险与对策
|
||||
|
||||
| 风险 | 对策 |
|
||||
| --- | --- |
|
||||
| 表驱动改动影响绘制顺序(toolbar 与 tabBar 重叠区) | 表按"底→顶"排,`WM_SETCURSOR` 从表尾向前命中,与旧 `if` 顺序一致 |
|
||||
| 拆分导致逐像素差异 | 纯整函数搬移;每拆一个文件立刻跑 `rendercheck` 比对 8 哈希 |
|
||||
| `filter-branch` 会清掉工作区被剔除的路径 | 先备份 `Tools/` 到 `%TEMP%`,重写后还原为未跟踪文件 |
|
||||
| 中文注释在 PS 5.1 下乱码 | 文档编辑走 VS Code 编辑工具(UTF-8),终端只做 ASCII 拼接 |
|
||||
|
||||
## 完成判据
|
||||
|
||||
* `make release` / `make debug` 零 error 零 warning;
|
||||
* `Tools/rendercheck` 8 个场景哈希与基线**逐一相同** + 7 条 FUNC 断言 PASS;
|
||||
* `Tools/uicheck` 69/69;
|
||||
* `TargetCheck.exe` < 900 KB;
|
||||
* `git ls-files Tools` 返回空、`git status` 干净、`main` / `dev` 未被触碰。
|
||||
|
||||
---
|
||||
|
||||
# 第六阶段(2026-10-28):「显示布局边界」调试开关 + 三处可见收益优化
|
||||
|
||||
> 用户要求:先做可见收益的优化,然后在「导入文件」左侧加一个齿轮按钮,点击展开二级菜单,
|
||||
> 菜单里放「显示布局边界」勾选项,风格参考 Android 调试功能;先落地,后续再改进。
|
||||
|
||||
## 1. 可见收益的优化(3 项,均为纯收益、行为不变)
|
||||
|
||||
| # | 位置 | 改前 | 改后 | 收益 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| O1 | `Toolbar::Layout` | **每次**都 `RebuildWidgets()`(销毁 + 重建整棵树) | 新增 `m_treeDirty`,只在规格/字体/主题/缩放/动态文本变化时重建 | 拖动面板分隔条时 `Relayout` 每帧调用一次 ⇒ 每帧省 10 次堆分配 + 8 次测量缓存失效 |
|
||||
| O2 | `Toolbar::SetActionLabel` | 同时写「覆盖表」和「控件文本」两处 | 只写覆盖表 + 置树脏 | 消除双写可能不同步(旧实现下次重建会把文本退回默认值) |
|
||||
| O3 | `Toolbar` / `TabBar` 的 `OnMouseMove` | 各自手写 `Dispatch(); return UiRectHasArea(TakeDirty());` | 新增 `UiWidget::DispatchRedraw(event)` | 两处重复收敛成一处;`TakeDirty()` 会清零,漏掉一次就把下一次真实变化误判为"无变化" |
|
||||
|
||||
## 2. 新功能:齿轮按钮 → 应用设置菜单 → 显示布局边界
|
||||
|
||||
### 2.1 入口
|
||||
|
||||
* `ToolbarAction::SETTINGS` + `kButtonSpecs[0]`(`UiButtonIcon::GEAR`)。
|
||||
* 工具栏布局从"首按钮靠左 + 其余靠右"改为**左侧固定簇**(`kLeftClusterCount = 2`:齿轮 + 导入文件)+ 右侧按钮组。
|
||||
* 齿轮是**矢量绘制**(`PaintGearIcon`:8 齿 + 外环 + 轴孔,全部按 `half` 的比例推导),
|
||||
不用字体里的 `⚙` —— 该字符在不同回退字体里的大小与基线不一致,会"忽大忽小、垂直不居中"。
|
||||
* `UiButtonWidget` 新增 `UiButtonIcon`(`NONE`/`GEAR`)与 `SetIcon`;图标按钮宽度固定 40 逻辑像素。
|
||||
* `Application::OpenSettingsMenu()` 用 `Toolbar::ButtonRect(SETTINGS, …)` 取**控件实测矩形**做锚点
|
||||
(宿主不自己推算按钮几何),菜单在按钮下沿 +4 逻辑像素处右下展开。
|
||||
|
||||
### 2.2 菜单
|
||||
|
||||
`MenuScope::APP_SETTINGS`(新增)→ 宿主自己处理,不下发到组件。
|
||||
条目:`HEADER "调试"` + `NORMAL "显示布局边界"`(`keepOpen = true`,点击后菜单保持打开,
|
||||
勾选态由 `PopupMenu::UpdateItemLabel` 回写)。
|
||||
|
||||
### 2.3 布局边界:为什么放在共同基类
|
||||
|
||||
`UiComponent` 新增 **`virtual void PaintDebugBounds(gd::Graphics&, float scale)`**,默认只画 `Area()`。
|
||||
四个组件各自覆盖:先调基类(自己的区域),再补画**层级更深的**矩形。
|
||||
|
||||
这正是上一轮回答里"是不是只需要在共同基类内实现"的答案:
|
||||
**能覆盖顶层 4 块区域,覆盖不了组件内部的手绘结构**。所以采用"基类给出协议 + 组件自己上报":
|
||||
|
||||
| 组件 | 上报内容 |
|
||||
| --- | --- |
|
||||
| 全部 | `Area()`(层级 0) |
|
||||
| `Toolbar` / `TabBar` | 跳过与区域重合的根控件,直接遍历 `m_root->Children()`(层级 1);TabBar 另加每个 ✕(层级 2) |
|
||||
| `SignalPanel` | 表头 / 筛选框 / 列表 / 样式区(1)+ 滚动条、样式行、**当前可见**的列表行(2) |
|
||||
| `PlotView` | 每个 `LaneVisual::rect`(1);**不**上报曲线/胶囊/提示这类随数据变化的瞬态几何 |
|
||||
| 宿主 | 客户区外框、分隔条、打开的菜单 / 拾色器(层级 0) |
|
||||
|
||||
`UiWidget::PaintDebugBounds(g, scale, depth)` 递归整棵控件树 —— 凡已上树的控件**零成本**获得边界。
|
||||
|
||||
### 2.4 视觉与实现约束
|
||||
|
||||
* **只描边、不填充**(与 Android 一致):填充会在几十个嵌套矩形上层层叠加把内容糊住。
|
||||
* 层级配色:0 青蓝 `#29B6F6`(2px)/ 1 品红 `#E040FB` / 2 琥珀 `#FFB300` / ≥3 绿 `#66BB6A`(1px)。
|
||||
* 线宽按**设备像素**固定(不乘 `scale`),并临时 `PixelOffsetModeNone` + 关抗锯齿 + 矩形对齐整像素,
|
||||
保证 1px 就是 1px。
|
||||
* 绘制位置:`Render` 的**最后一步**(加载遮罩之后)。⚠️ **绝不进入任何位图缓存**
|
||||
(`PlotView` 4 层缓存、`SignalPanel` 行位图缓存)—— 否则关掉开关后线条会残留。
|
||||
* `fullFrame` 追加 `m_showLayoutBounds`:开启时必须整帧回送,否则"只回送绘图区"的快路径会让
|
||||
装饰区的边界停在旧帧上。
|
||||
|
||||
## 3. 验证
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `make release` / `make debug` | 零 error / 零 warning |
|
||||
| **新增离屏探针 `.preflight/lbcheck`** | **21 / 21 PASS** |
|
||||
| `Tools/rendercheck` 8 场景**工具栏以下区域**哈希 | 与改造前**逐一相同**(改动只落在工具栏带内) |
|
||||
| `Tools/rendercheck` 7 条功能断言 | 7 / 7 PASS |
|
||||
| `Tools/uicheck` | 69 / 69 |
|
||||
| `TargetCheck.exe` | 897 024 B(< 900 KB) |
|
||||
|
||||
新增断言覆盖:齿轮存在 / 在「导入文件」左侧 / 同高 / 更窄;开机后无调试色;
|
||||
开启后青蓝 24 930 px、品红 19 734 px;**关闭后逐像素回到开启前**;菜单 scope、锚定贴合按钮、
|
||||
宽度未被撑大、条目初始未勾选、点击翻转开关、菜单保持打开。
|
||||
|
||||
## 4. 后续可改进(用户:"先落地实现然后再进一步改进")
|
||||
|
||||
1. 边界左上角标短 tag(`row#12` / `lane3`)—— 需要处理 GDI 文本不受 GDI+ 裁剪约束的问题。
|
||||
2. `PlotView` 的浮动面板(`EdgePanelLayout` 里的 `panel/title/close/…`)目前未上报;
|
||||
它们已经是集中结构,插入一个循环即可。
|
||||
3. `SignalPanel` 的复选框 / 徽标 / ✕ 等行内子块(现在只到"行"这一层)。
|
||||
4. 开关状态目前**不持久化**(重启恢复为关闭);若要持久化需要引入配置存储。
|
||||
5. 菜单只有一项;后续设置项按 `HEADER` 分组追加即可。
|
||||
|
||||
---
|
||||
|
||||
# 第七阶段计划(2026-10-28 第九轮):边界观感重做 + 覆盖补全 + 几何同源
|
||||
|
||||
> 用户反馈:① 设置齿轮按钮的颜色应与「导入文件」一致;
|
||||
> ② 边界绘制"就是实心矩形、粗细不一",希望是**四角粗、其他边细**;
|
||||
> ③ 按上一轮总结的覆盖率问题着手改进(很多元素没有边界)。
|
||||
|
||||
## 任务清单
|
||||
|
||||
| # | 任务 | 验证手段 |
|
||||
| --- | --- | --- |
|
||||
| P7-1 | 齿轮按钮改为主按钮(`primary = true`),与「导入文件」同色 | 探针断言两按钮底色一致 |
|
||||
| P7-2 | 边界的描边改为**四角粗(3–4px)+ 四边细(1px)**的包角样式;只描边不填充 | 逐像素探针 + 人工核对 |
|
||||
| P7-3 | 层级 > 0 的 box 内缩 1px,避免与父矩形共边时叠成粗线 | 同上 |
|
||||
| P7-4 | 协议从「组件自己画」改为「组件**收集 box**、宿主统一画」(`CollectDebugBoxes`) | 编译 + 探针 |
|
||||
| P7-5 | `SignalPanel`:行内元素全部上报(复选框/色块/名称/GX 徽标/✕/折起三角/组名/计数)+ 筛选框/清空✕/滚动条/滑块/样式区 5 行与行内控件/目标行/编辑框 | 探针断言 box 数量 |
|
||||
| P7-6 | `PlotView`:GX 徽标、装饰层矩形、图例条目(托底/色块/文本)、5 类浮动面板全部字段 | 同上 |
|
||||
| P7-7 | `Toolbar`:按钮内部**文本/图标**矩形(新增 `UiButtonWidget::ContentRect()`) | 同上 |
|
||||
| P7-8 | `PopupMenu` / `ColorPickerPopup`:条目、滚动区/轨道/滑块、子菜单;拾色器各控件 | 同上 |
|
||||
| P7-9 | `RowRect` 从 **O(row) → O(1)**(行顶前缀和 `m_rowTop`) | 行为不变(像素哈希) |
|
||||
| P7-10 | `PanelBounds()` 收敛 6 处重复三元表达式 | 编译 |
|
||||
| P7-11 | 三份文档 + `agent_mem.md` 同步;`TargetCheck.exe` < 900 KiB | 构建 |
|
||||
|
||||
## 风险与对策
|
||||
|
||||
| 风险 | 对策 |
|
||||
| --- | --- |
|
||||
| 前缀和与行集合短暂不一致 | `RowRect` / `ContentOffsetBeforeRow` 检测长度后**退回逐行累加**(正确性优先) |
|
||||
| box 数量暴涨导致调试帧变慢 | 只在开关打开时执行;标签设**尺寸门槛**(< 56×16 逻辑像素不画标签) |
|
||||
| 调试层污染位图缓存 | 仍只在 `Render` 最后一步叠加,探针 `bounds-off-restores-pixels` 钉住 |
|
||||
| 浮层 box 需要读取私有几何 | 给两个浮层加 `CollectDebugBoxes`,几何仍由它们自己给出(不外泄成员) |
|
||||
|
||||
## 完成判据
|
||||
|
||||
* `make release` / `make debug` 零 error 零 warning;
|
||||
* 「工具栏带以下」区域哈希与上一轮**逐一相同**(证明改动只落在工具栏带内);
|
||||
* `lbcheck` 21/21、`rendercheck` FUNC 7/7、`uicheck` 69/69;
|
||||
* 打开开关后:每行的复选框/色块/名称/GX 徽标/✕、每个网格的 GX 徽标与图例、每个浮层控件都有独立包角框。
|
||||
|
||||
---
|
||||
|
||||
# 第八阶段计划(2026-10-28 第十轮):边界观感自适应 + 残留与层级缺陷修复
|
||||
|
||||
> 用户反馈:① 包角对小元素来说太粗(小方框直接变成粗描边);② 不需要黑色底衬的文字标签;
|
||||
> ③ 加载 MF4-Record.MF4 全部信号独立网格后,按住左键拖动网格滚动条快速滚动会出现大量
|
||||
> 绘制残留(工具栏与网格区域都有),松开并移动鼠标才消失;④ 右键二级菜单上被下层元素的
|
||||
> 边界线覆盖,干扰观察菜单布局。
|
||||
|
||||
## 任务清单
|
||||
|
||||
| # | 任务 | 验证手段 |
|
||||
| --- | --- | --- |
|
||||
| P8-1 | 包角粗细/长度按矩形短边自适应;小方框只画角不画边;过小则不画 | 截图放大 3 倍核对 |
|
||||
| P8-2 | 删除文字标签(连同 `UiDebugPaintStyle`),降低体积与视觉噪声 | 编译 + 体积 |
|
||||
| P8-3 | 定位并修复「拖网格滚动条时的绘制残留」 | 新增离屏探针 `.preflight/lbresid` |
|
||||
| P8-4 | 修复 `PlotView` 误用 `m_decorRects`(位图坐标系)产生的跨工具栏幽灵框 | 同上(box 清单打印) |
|
||||
| P8-5 | 调试叠加层开启时强制绘图区整幅重绘(禁用三条快路径) | 同上(同状态两帧必须逐像素相同) |
|
||||
| P8-6 | box 增加 `clip`(组件地盘),组件内越界元素不再画到邻居上 | 同上(差异 bbox 不得越过绘图区上沿) |
|
||||
| P8-7 | 层级:`(layer, depth)` 双排序键 + 浮层 `occluder` 挖洞 | 新增探针 `.preflight/lbzorder`(4 条断言) |
|
||||
| P8-8 | 文档与共享记忆同步;`TargetCheck.exe` < 900 KiB | 构建 |
|
||||
|
||||
## 风险与对策
|
||||
|
||||
| 风险 | 对策 |
|
||||
| --- | --- |
|
||||
| 强制整幅重绘让调试模式变慢 | 只在开关打开时生效;关闭立即恢复快路径 |
|
||||
| `Region::Exclude` 影响其他绘制 | 只在 `Save/Restore` 内临时 `SetClip`,且仅在存在遮挡时才走该分支 |
|
||||
| 裁剪把紧贴边界的包角裁掉 | 裁剪用 `floor/ceil` 向外取整(宁可多一像素) |
|
||||
| 移除标签后难以辨认 box 归属 | 层级配色(青蓝/品红/琥珀/绿)+ 包角形状 |
|
||||
|
||||
## 完成判据
|
||||
|
||||
* 零 warning;`lbresid` 5/5、`lbzorder` 4/4、`lbcheck` 21/21、`uicheck` 69/69;
|
||||
* `rendercheck` 的「工具栏带以下」区域哈希与上一轮**逐一相同**、FUNC 7/7;
|
||||
* 拖动滚动条期间工具栏区域像素**完全不变**,且同状态两帧**逐像素相同**;
|
||||
* 菜单/二级子菜单区域内不出现下层组件的边界色。
|
||||
|
||||
---
|
||||
|
||||
# 第九阶段计划(2026-10-28 第十一轮):把"边界"这件事从补丁改成机制
|
||||
|
||||
> 背景:第十轮结论是"三个性质不同的问题"——① `inset` 破坏 '框 = 可见形状外接矩形';
|
||||
> ② 元素清单不完整(几何没有枚举出口);③ 描边图元太多。本轮把三者一起解决。
|
||||
|
||||
## 任务清单
|
||||
|
||||
| # | 任务 | 验证手段 |
|
||||
| --- | --- | --- |
|
||||
| P9-1 | **取消内缩**:框必须与可见形状外接矩形重合(修 GX 胶囊圆弧落在框外) | 探针断言 box 与 `LaneBadgeRect` 重合(容差 0.5px) |
|
||||
| P9-2 | **文本获得几何身份**:`PaintUtil` 增加 `UiTextRectSink`,在绘制的**唯一出口**回报墨迹矩形 | 覆盖 60+ 调用点,无需逐处修改 |
|
||||
| P9-3 | 离屏位图(面板行缓存)中的文本按**位图原点**回放,缓存命中也能显示文本框 | 探针 `row-text-boxes-from-cached-rows` |
|
||||
| P9-4 | 补齐 `PlotView` 漏项:时间轴 / 网格滚动条 / 回到最上方 / 光标标签 / 差值标签 / 内联输入框 | 探针 4 条 box 断言 |
|
||||
| P9-5 | 描边图元 12 → 8(4 边 + 4 角块),耗时 5.9ms → 4.0ms | 微基准 + 帧耗时分解 |
|
||||
| P9-6 | 关闭开关时清空容器(释放内存,避免误读) | 探针 `boxes-cleared-when-off` |
|
||||
| P9-7 | 文档 + 共享记忆;`TargetCheck.exe` < 900 KiB | 构建 |
|
||||
|
||||
## 关键设计决定
|
||||
|
||||
* **文本不做"逐处登记"**:全仓 65 处文本绘制点手写登记必漏。改为在 `UiText`/
|
||||
`UiTextClipped`/`UiTextAtClipped` 的公共出口 `ReportTextRect()` 里回报一次,
|
||||
代价是开关关闭时一次判空。
|
||||
* **缓存命中也要有文本框**:行位图渲染时用一个本地收集器把矩形存进 `RowBitmapCache`,
|
||||
贴图时按原点偏移回放;否则第二帧起文本框全部消失。
|
||||
* **不内缩**:重叠时"后画的覆盖"就是正确语义,内缩是错误取舍。
|
||||
|
||||
## 完成判据
|
||||
|
||||
* 零 warning;四个探针全绿(21 + 5 + 4 + 7)、`uicheck` 69/69;
|
||||
* `rendercheck` 8 个场景的整帧与区域哈希与上一轮**逐一相同**(调试功能不能影响正常绘制);
|
||||
* 打开开关后:文本有橙色墨迹框、GX 胶囊被框住、时间轴/滚动条/回到最上方均有框。
|
||||
|
||||
---
|
||||
|
||||
# 第十阶段计划(2026-10-29 第十二轮):同步远端 + 既有性能问题 + 浮层层级修复
|
||||
|
||||
> 用户报告(附截图):① 远端 gitea 已把 `UI_Refactor` 合入 `main`(分支保留),需 fetch 并同步各分支,
|
||||
> `dev` 为意外产物,本地删除;② 排查"顺带发现的既有性能问题"并解决;③ 浮动面板被网格边界穿透;
|
||||
> ④ 滚动网格滚动条时浮动面板上的边界闪烁;⑤ 信号面板 GX 徽标的「所属网格」二级菜单边界溢出菜单、
|
||||
> 滚动后条目仍像在绘制。
|
||||
|
||||
## 任务清单
|
||||
|
||||
| # | 任务 | 验证手段 |
|
||||
| --- | --- | --- |
|
||||
| P10-1 | `git fetch` 同步;`main` 快进到 `gitea/main`;本地删除 `dev`;`UI_Refactor` 快进到合并提交 | `git branch -vv` |
|
||||
| P10-2 | 定位"空闲帧比强制整帧更慢"的真因 | 离屏计时探针 |
|
||||
| P10-3 | 修复:离屏网格不再逐帧算内容指纹/入队 | 排空后稳态与排空过程分别计时 |
|
||||
| P10-4 | 浮动面板另起绘制层 + 登记遮挡矩形 + 内容裁剪到面板 | 新增探针 `.preflight/lbpanel` |
|
||||
| P10-5 | 弹出菜单条目裁到可视区(滚动条目不再登记盒子) | 同上(10 条断言) |
|
||||
| P10-6 | `EdgePanelLayoutFor` 只在"该网格有面板展开"时才调用 | 收集耗时对比 |
|
||||
| P10-7 | 文档 + 共享记忆;零警告;< 900 KiB | 构建 |
|
||||
|
||||
## 关键结论(P10-2)
|
||||
|
||||
用两级计时探针(先整体、再拆到 `WarmLaneLayersStep` 内部)测得:
|
||||
|
||||
* "空闲帧 2.4ms" **不是稳态**,而是**图层预热队列排空**的瞬时开销
|
||||
(16 个网格 × 每帧补 1 个 ≈ 1~3ms/帧,约 16 帧后结束);
|
||||
* 排空后稳态为 **0.53ms**,与"强制整帧"的 0.46ms 基本相同 —— 所以**不存在**每帧白烧的固定开销;
|
||||
* 但 `RollWideBuffer` 里"先算每网格内容指纹、再判断是否可视"的顺序确实有问题:
|
||||
283 个网格每帧全算一遍 `LaneLayerKey()`(约 0.7ms),并会把离屏网格塞进待重画队列。
|
||||
**把可视性判断提到最前面**后,离屏网格既不哈希也不入队。
|
||||
|
||||
## 完成判据
|
||||
|
||||
* 零 warning;`lbpanel` 10/10 + 既有四个探针全绿;`uicheck` 69/69;
|
||||
* `rendercheck` 8 场景哈希与上一轮**逐一相同**(本轮不应改变任何正常绘制像素);
|
||||
* 面板区域内不出现网格层颜色;菜单/面板的盒子不得完全落在自身矩形之外。
|
||||
+149
-712
@@ -1,765 +1,202 @@
|
||||
# UI 重构进度(UI_Refactor 分支)
|
||||
# Just DataInspector UI 重构 —— 当前进度
|
||||
|
||||
> 本文件是**跨会话/跨 Agent 的交接台账**。每次提交前更新;每一行都要能对应到一个可复跑的验证结果。
|
||||
> 计划见 `Documents/UI_Refactor_Plan.md`;硬约束见 `Documents/agent_mem.md` §11.2。
|
||||
> 计划见 `UI_Refactor_Plan.md`,结论与遗留见 `UI_Refactor_Report.md`。
|
||||
> 最后更新:2026-10-29(本轮:体积 L3 落地 + L1 几何产物层 + 审计)
|
||||
|
||||
## 分支状态
|
||||
---
|
||||
|
||||
| 分支 | 基线 | 用途 |
|
||||
## 1. 总览
|
||||
|
||||
| 层 | 内容 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| `main` | `df715a0` | 可发布,**不受本次重构影响** |
|
||||
| `dev` | `4e51b41` | 日常开发基线;本分支上已 **删除过时报告** `Documents/refactor_report_2026-10-16.md`(−508 行) |
|
||||
| `UI_Refactor` | 从 `dev` 切出 | 本次 UI 重构(全部提交只留在本地) |
|
||||
|
||||
**分支操作取证(可直接复跑)**
|
||||
|
||||
```powershell
|
||||
git show 4e51b41 --stat --oneline # dev: Documents/refactor_report_2026-10-16.md | 508 ----------
|
||||
Test-Path Documents\refactor_report_2026-10-16.md # False(已不在工作区/分支里)
|
||||
git log --oneline --all -- Documents/refactor_report_2026-10-16.md # 末条即为 4e51b41 的删除提交
|
||||
git log --oneline main -1 # df715a0(main 未被任何重构提交触碰)
|
||||
```
|
||||
|
||||
## 阶段看板
|
||||
|
||||
> **本计划已全部结案**:可达成项已落地(P0/P1/P2/P3-Toolbar+TabBar/P5-共享表面),
|
||||
> 其余项经评估**不采用**并写明理由(见 `UI_Refactor_Report.md` §7)—— 计划里没有悬空阶段。
|
||||
|
||||
| 阶段 | 内容 | 结论 |
|
||||
| --- | --- | --- |
|
||||
| **P0** | 离屏回归网 `Tools/uicheck` | ✅ 已落地(69 项断言) |
|
||||
| **P1** | `UiLayout` + `UiWidget` 基础层 | ✅ 已落地 |
|
||||
| **P2** | `PlotViewPaint.cpp` 零行为分片 | ✅ 已落地(9 080→4 913,逐像素 0 差异) |
|
||||
| **P3** | chrome 迁移 | ✅ Toolbar / TabBar 已迁移;PopupMenu / ColorPicker / SignalPanel 行**经评估不采用** |
|
||||
| **P4** | 5 个浮动面板控件化 | **经评估不采用**(理由见 Report §7) |
|
||||
| **P5** | 共享表面 `UiSurface` + top-down DIB | ✅ 已落地;脏区并集**经评估不采用** |
|
||||
| L0 | `UiComponent` 协议 + 有序组件表 + `UiWidget` 控件树 | ✅ 已完成(前几轮) |
|
||||
| L1 | `UiBox` / `UiBoxTable` / `UiComponent::BuildBoxes` | ✅ **本轮完成** |
|
||||
| L2 | `UiDamage` 脏矩形 + S4 独立叠加层 | ⏸ **评估后缓做**(理由见 Plan §5) |
|
||||
| L3 | `-fno-exceptions` + `MemProbe` + `-ffunction-sections` + `--gc-sections` + LTO | ✅ **本轮完成** |
|
||||
|
||||
---
|
||||
|
||||
## 提交记录
|
||||
## 2. L3 体积与异常策略
|
||||
|
||||
### 1. `P0 + P1`:UI 基础层与离屏回归网
|
||||
**新增文件**
|
||||
### 2.1 Makefile 开关
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| `Components/Shared/Inc/UiLayout.hpp` | ~250 | 布局原语 + `UiLayoutCache` |
|
||||
| `Components/Shared/Src/UiLayout.cpp` | ~300 | 矩形原语 / 对齐 / Stack / Flow / 缓存 |
|
||||
| `Components/Shared/Inc/UiWidget.hpp` | ~330 | 控件基类接口与设计说明 |
|
||||
| `Components/Shared/Src/UiWidget.cpp` | ~560 | 树 / Measure·Arrange / Paint(脏区+裁剪)/ 派发 / 仲裁 / 动画 |
|
||||
| `Tools/uicheck/main.cpp` | ~570 | 离屏回归网(62 项断言) |
|
||||
| `Tools/uicheck/build.ps1` | ~70 | 链接 `Build/<Config>/*.o`(排除 `Main.o`) |
|
||||
|
||||
**已实现并验证的行为**
|
||||
|
||||
- 布局:`UiAlignInRect`(含 `STRETCH`、超尺寸夹取、偏移后置)、`UiRectSnapInward`(设备像素内侧对齐)、
|
||||
`UiStackRects`(固定 / `kUiSizeAuto` 均分 / **绝不产生负尺寸**)、`UiFlowRects`(换行 + `overflow` 下标)。
|
||||
- 缓存:`UiLayoutCache` 用 `std::deque` 保证**引用稳定**;容量淘汰按"最久未命中";`Invalidate` 为**定向**失效。
|
||||
- 布局 pass:`Measure` 命中缓存不重复调用 `OnMeasure`;`InvalidateMeasure` **只**沿祖先链失效(兄弟不重算)。
|
||||
- 绘制:`dirty` 为空 = 全画;与子树矩形无交集 ⇒ **整棵子树跳过**;有效裁剪 = 祖先裁剪切 ∩ 自身矩形 ∩ 脏区;
|
||||
不可见节点跳过。
|
||||
- 派发:**只沿命中链**(最深的命中节点 → 祖先冒泡),**绝不分发给同层兄弟**;`capture` 期间事件直投捕获者;
|
||||
`modal` 只向模态子树派发;`hit-transparent` 让事件穿透;`QUERY_CURSOR` 由最上层认领者返回;
|
||||
键盘/字符沿**焦点链**派发(无焦点交给根,保证宿主级快捷键可用)。
|
||||
- 动画:`AdvanceAnimation` 逐节点下发同一时间戳并汇总"是否仍有动画"。
|
||||
- 悬停:进入 / 离开各只触发一次,跨子节点迁移时正确配对。
|
||||
|
||||
**过程中修掉的两个自身缺陷**
|
||||
|
||||
1. `UiWidget` 缺 `AdvanceAnimationSelf` / `AnimationActiveSelf` 两个 protected 虚函数(`.cpp` 已引用、头文件漏声明)。
|
||||
2. 最初的派发实现是"遍历同层兄弟、未消费就继续" ⇒ **一个点击可能同时被视觉上位于下方的控件处理**;
|
||||
已改为"只沿命中链派发 + 祖先兜底"。回归网里对应的断言从"绝对次数"改为**增量断言**
|
||||
(绝对次数会被前面用例计数污染,本轮踩过一次)。
|
||||
|
||||
**验证**
|
||||
|
||||
```
|
||||
make release TARGET_FILE_NAME=TargetCheck # 零警告
|
||||
powershell -File Tools/uicheck/build.ps1 # 链接 Build/Release/*.o(排除 Main.o)
|
||||
Tools/uicheck/uicheck.exe # 62 passed, 0 failed
|
||||
```
|
||||
|
||||
**未做(有意)**
|
||||
|
||||
- 未改动任何现有组件(Toolbar / TabBar / SignalPanel / PlotView 仍走旧路径),因此**本提交对产品像素零影响**。
|
||||
- 未把 `UiWidget` 接入 `Application`,避免在 P2 之前引入行为差异。
|
||||
|
||||
---
|
||||
|
||||
### 2. `P2`:`PlotViewPaint.cpp` 按关注点分片(零行为变更)
|
||||
|
||||
**为什么**:该文件 9 080 行里混住四类内容 —— 真绘制 ≈3.5k / 光栅化缓存 ≈2.5k / 缓存键 ≈1.1k /
|
||||
布局与命中几何 ≈1.3k。按"函数名前缀"分片是上一轮膨胀的直接原因,本轮按**关注点**再分一次。
|
||||
|
||||
**做法**:**机械搬移成员函数定义**(不改一行逻辑)。`PlotViewInternal.hpp` 里的 `pvdetail` 全部 `inline`
|
||||
⇒ 跨分片安全,无需改任何 include、无需改 `Makefile`(按 basename 自动收集 `.cpp`)。
|
||||
|
||||
| 文件 | 搬入内容 | 行数变化 |
|
||||
| --- | --- | --- |
|
||||
| `PlotViewRaster.cpp`(新) | 光晕离屏层(`PaintSignalHalo*` / `UploadHaloLayer` / `ReleaseHaloLayer`)<br>场景缓存(`Ensure/Capture/Blit/Invalidate/ReleaseSceneCache`)<br>装饰层(`Ensure/Release/Render/BlitDecorCache` / `ResolveAlphaBlend`)<br>宽基底图层与预算(`EnsureLaneLayer` … `BlitWideBuffer`、并行重建、滚动补条、预热) | 1 876 |
|
||||
| `PlotViewCacheKey.cpp`(新) | `MakeSceneCacheKey` / `SceneCacheMatches` / `SceneHaloRef`<br>`SceneContentHash` / `SceneGeometryHash` / `SceneDecorHash`<br>`CollectSceneStyles` / `FindStyleOnlyLanes` / `CollectSceneDerived` / `RepaintSceneCachePatch` | 474 |
|
||||
| `PlotViewPaint.cpp` | 只剩绘制(`Paint*` / `Render*` / 输入框绘制) | 9 080 → **6 252**(−31%) |
|
||||
|
||||
**验证(关键)**
|
||||
|
||||
1. `make release TARGET_FILE_NAME=TargetCheck` **零警告**;`Target.exe` 873 984 → **875 008 B**(+1 KB,
|
||||
属分片后各 TU 的对齐差异;仍远低于 900 KB 阈值)。
|
||||
2. **逐像素 A/B**:临时写了一个渲染哈希探针(仓库外),对三个场景渲染后备缓冲区并取 FNV 哈希:
|
||||
* A 默认全览;B 深放大 + 自由标记(含固定点)+ 均值滤波 + 弱峰调整 + 时间光标;C 再平移一帧(触发宽基底滚动/补条)。
|
||||
* **分片前**:`A 2166933039626F66 / B C444ECFEB18FD73D / C 7984446D8E7A4418`
|
||||
* **分片后**:**完全相同**(三个场景逐一一致)。
|
||||
* 该探针未入库(一次性验证用),结论记录在此以便复现。
|
||||
3. `Tools/uicheck` 62/62 全绿。
|
||||
4. 顶层注释从"四个分片"更新为"六个分片(按关注点)",并写明"`Paint/PaintLane` 仍只允许定义在 `PlotViewPaint.cpp`"。
|
||||
|
||||
**⚠️ 复现 A/B 的坑**:机械搬移后用旧的 `.bak` 回退时,`Build/<Config>/` 里会残留
|
||||
`PlotViewRaster.o` / `PlotViewCacheKey.o`,链接探针(`Build/Release/*.o`)会因重复符号失败 ——
|
||||
**必须先删掉这两个陈旧 `.o`** 再链接。
|
||||
|
||||
---
|
||||
|
||||
### 3. `P2 续`:把"布局与命中几何"从绘制分片里拆出(`PlotViewPanels.cpp`)
|
||||
|
||||
**为什么**:`EdgePanelLayoutFor`(651 行,19 个调用点)、`EdgePanelWidth`、`LaneBadgeRect`、
|
||||
`FreeMarkerValue/Delta/Contents/LabelRects`、`ProbeFreeMarker` 都是**几何**,却被塞在绘制分片里 ——
|
||||
改一次布局要动 9 千行的绘制文件,回归面被放大。
|
||||
|
||||
**做法**:同样机械搬移(5 个不连续块),`PlotViewPaint.cpp` 从 6 645 → **4 913** 行。
|
||||
|
||||
**最终七个分片**(按关注点)
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| `PlotViewPaint.cpp` | 4 913 | 绘制(`Paint*` / `Render*` / 输入框绘制) |
|
||||
| `PlotViewInteraction.cpp` | 3 430 | 命中、鼠标/键盘、光标形状、面板层序 |
|
||||
| `PlotViewFeatures.cpp` | 3 266 | 光标读数、提示、缩放、自适应、派生曲线、边沿统计 |
|
||||
| `PlotViewRaster.cpp` | 1 876 | 光晕层 / 场景缓存 / 装饰层 / 宽基底图层与预算 |
|
||||
| `PlotViewPanels.cpp` | 1 359 | 面板布局、图例徽标几何、自由标记取值与探测 |
|
||||
| `PlotView.cpp` | 889 | 视图核心(构造、布局、坐标映射、网格结构) |
|
||||
| `PlotViewCacheKey.cpp` | 474 | 内容指纹与缓存键 |
|
||||
|
||||
**累计效果**:`PlotViewPaint.cpp` **9 080 → 4 913(−46%)**,最大文件从 9.1k 降到 4.9k。
|
||||
|
||||
**验证**
|
||||
|
||||
1. `make release TARGET_FILE_NAME=TargetCheck` 零警告;`Target.exe` **877 568 B**(< 900 KB)。
|
||||
2. **逐像素 A/B**:同三场景哈希与迁移前**完全一致**
|
||||
(`A 2166933039626F66 / B C444ECFEB18FD73D / C 7984446D8E7A4418`)。
|
||||
3. `Tools/uicheck` 62/62。
|
||||
4. 顶层注释更新为"七个分片(按关注点)",并标明 `Paint/PaintLane` 只允许定义在 `PlotViewPaint.cpp`。
|
||||
|
||||
---
|
||||
|
||||
### 4. `P3`:Toolbar 迁移到控件树(**逐像素 0 差异**)
|
||||
|
||||
**新增**:`Components/Shared/{Inc,Src}/UiButtonWidget.*` —— 通用"文字按钮"控件,视觉规则与旧 Toolbar
|
||||
按钮逐像素一致(主按钮强调色实底 / 普通按钮 1px 描边 / 可切换选中态 / 禁用态配色);
|
||||
交互由自己处理:按下即 `CaptureMouse()` 进入按下态,**在自身范围内**松开才触发 `onClick`,
|
||||
指针离开时 `CancelPress()`。
|
||||
|
||||
**`Toolbar` 内部重写(公开 API 一字未改)**:
|
||||
|
||||
| 旧实现 | 新实现 |
|
||||
| --- | --- |
|
||||
| `std::vector<Button>` 里既存配置又存矩形/悬停/按下 | `Button` 只存**静态配置 + 控件指针**;几何与外观状态归控件 |
|
||||
| `m_hoverIndex` / `m_pressedIndex` 手工维护 | 由 `UiWidget` 的**悬停链**与 capture 维护 |
|
||||
| `Paint` 自己遍历按钮绘制 | 根控件(`ToolbarRootWidget`:底色 + 底部分隔线)+ 按钮子节点,`root->Paint(ctx)` |
|
||||
| `OnMouseMove/Down/Up/Leave` 各自扫按钮矩形 | 统一翻译成 `UiEvent` → `root->Dispatch(event)`;动作经 `onClick` 回调登记、抬起后执行 |
|
||||
| `Button::hover` 死字段 | 删除(`Hovered()` 由基类提供) |
|
||||
|
||||
**新增框架约定(重要)**:`UiWidget::Paint` 的裁剪由"严格等于自身矩形"改为
|
||||
**"自身矩形外扩 `kUiClipBleed`(1px)"**。原因:边框/描边以矩形边界为中心绘制,1px 笔宽有一半在矩形外,
|
||||
严格裁剪会把外半侧切掉 ⇒ 控件看起来变细(与旧绘制代码不再逐像素一致)。
|
||||
`uicheck` 对应断言改为"裁剪 ⊇ 自身矩形且不超出外扩范围"。
|
||||
|
||||
**验证**
|
||||
|
||||
1. `make release` / `make debug` **零警告**;`Target.exe` **881 664 B**(< 900 KB)。
|
||||
2. **逐像素 A/B(8 个场景)**:迁移前后哈希逐一相同
|
||||
`A 2166933… / B C444ECF… / C 7984446… / D BF85587… / E 1C663E4… / F 0535292… / G 7A58172… / H 23F390D…`
|
||||
(A 全览;B 深放大+自由标记+均值滤波+弱峰调整+光标;C 平移;D 悬停按钮;E 按下未松开;
|
||||
F 松开;G 悬停主按钮;H 移出工具栏)。
|
||||
3. **功能验证**(证明点击真的翻译成动作,而不是"看起来没坏"):
|
||||
* `FUNC locate-cursor-button PASS (1291,73)` —— 用控件自身矩形定位按钮;
|
||||
* `FUNC click-cursor-changes-mode PASS (1 -> 2)` —— 点击「光标」按钮后光标模式 单→双;
|
||||
* `FUNC release-outside-does-not-fire PASS (2 -> 2)` —— 按下后移出再松开,不触发动作。
|
||||
4. `Tools/uicheck` **62/62**。
|
||||
|
||||
---
|
||||
|
||||
### 5. `P5(1)`:共享表面 `UiSurface` + 后备缓冲改 top-down 32bpp DIB(**逐像素 0 差异**)
|
||||
|
||||
**新增** `Components/Shared/{Inc,Src}/UiSurface.*`:`{ dc, rect, stride, bits, version, scale }`。
|
||||
`SubView(localRect)` 把子矩形换算到宿主坐标、**夹取**在本表面内、按 `stride` 重算 `bits` 偏移、传递 `version`。
|
||||
|
||||
> ⚠️ 文件头注释里明确写了取舍:**画控件只需要 `dc + rect`**;`bits` 只在 CPU 侧合成
|
||||
> (memcpy / alpha 混合 / 重采样)时才有意义。用 `bits` 直写像素来"画"控件会破坏 ClearType、
|
||||
> 引入 stride / 翻转 / 写序三类错,是净亏损。
|
||||
|
||||
**`Application`**:
|
||||
* 后备缓冲由 `CreateCompatibleBitmap` 改为 **top-down 32bpp `CreateDIBSection`**
|
||||
(`biHeight` 取负 ⇒ `bits` 从第一行开始、y 向下递增);
|
||||
* 新增 `m_backBits` / `m_backStride` / `m_backVersion`,重建位图时 `++m_backVersion`;
|
||||
* 新增 `BackBufferSurface()` 对外暴露整块表面。
|
||||
|
||||
**顺带修掉一个真实缺陷(退出期访问违例)**:
|
||||
|
||||
| 问题 | 修法 |
|
||||
| --- | --- |
|
||||
| `Shutdown()` 里先 `GdiplusShutdown` 再 `DestroyWindow`:`DestroyWindow` 期间派发的 `WM_PAINT` 会进 `Render()`,在**已关闭**的 GDI+ 上构造 `Graphics` ⇒ 崩溃 | 顺序改为 **先销毁窗口 / 注销类,最后 shutdown GDI+** |
|
||||
| 任何晚到的绘制都可能踩到这条路径 | `Render()` 开头加保护:`!m_gdiplusReady || m_hwnd == nullptr` 直接返回 |
|
||||
|
||||
> 另:临时探针此前的退出崩溃其实来自"探针自己又调了一次 `GdiplusShutdown`"(`Application::Shutdown`
|
||||
> 内部已 shutdown)——与本轮修复无关,但顺序调整与 `Render` 保护保留为防御性正确做法。
|
||||
|
||||
**验证**
|
||||
|
||||
1. 8 场景像素哈希与 DIB 改造前**逐一相同**;3 条工具栏功能断言仍 PASS。
|
||||
2. `Tools/uicheck` **69/69**(新增 7 条 `UiSurface` 断言:bits 按 stride 偏移、矩形夹取、
|
||||
完全在外返回无效表面、`version`/`scale` 传递)。
|
||||
3. `make release` / `make debug` 零警告;`Target.exe` **883 200 B**(< 900 KB)。
|
||||
|
||||
---
|
||||
|
||||
### 6. `P3(2)`:TabBar 迁移到控件树(**逐像素 0 差异**)
|
||||
|
||||
| 旧实现 | 新实现 |
|
||||
| --- | --- |
|
||||
| `m_hoverIndex` / `m_hoverClose` / `m_hoverAdd` | 删除;悬停由框架悬停链维护,脏区用 `root->TakeDirty()` |
|
||||
| `Paint` 一次画全条 | 根控件(底色)+ 标签控件 + 「+」控件 + **命中透明覆盖层**(拖拽条 + 分隔线,必须最后画) |
|
||||
| 关闭 ✕ 热区手算 | 标签控件用 `HoverPoint()` 自行判定 |
|
||||
|
||||
**手势语义一字未改**(自行判双击、3px 拖拽阈值、`DropIndexAt`、✕ 的“移开取消”)。
|
||||
|
||||
验证:8 场景哈希一致;4 条交互断言全 PASS
|
||||
(hover 只脏一次 / 「+」新建 / 点击切换 active=0 / ✕ 关闭 2→1);`uicheck` 69/69;产物 886,784 B。
|
||||
|
||||
---
|
||||
|
||||
## 后续可选工作(**计划项均已结案**,以下为将来若要推进时的入口)
|
||||
|
||||
本计划的每个阶段都已有结论(落地 / 不采用),**当前没有悬空待办**。若将来要重新开启,
|
||||
按下列入口与判据推进;判据固定为:`uicheck` 全绿 + **8+ 场景像素哈希一致** + **功能断言**。
|
||||
|
||||
1. 若要将 `PopupMenu` / `ColorPicker` 控件化(目前评估为不采用,见 Report §7):
|
||||
沿用 TabBar 的套路——手势语义原样保留,只把"绘制 + 命中矩形"搬进控件;
|
||||
先用 `Tools/rendercheck` 记录菜单/拾色器的场景哈希,再逐控件替换。
|
||||
2. 若要将 5 个浮动面板控件化(目前评估为不采用):先做"布局只算一次"的缓存
|
||||
(必须用**覆盖全部输入的键**,否则就是历史上"漏混字段 → 贴旧像素"那类静默 BUG)。
|
||||
3. 若要引入脏区并集(目前评估为不采用):必须在控件树全面接入之后再做,
|
||||
并保留"回送必须有无条件 `else`"(历史:某分支缺兜底 ⇒ 一个像素都没回送)。
|
||||
|
||||
---
|
||||
|
||||
## 第七轮(2026-10-28):统一组件协议 · 去手写链路 · 大文件按职责拆分 · 审查 · 工具剥离
|
||||
|
||||
### 进度总览
|
||||
|
||||
| # | 任务 | 状态 | 验证 |
|
||||
| 开关 | 默认 | 作用 | 备注 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 新增 `UiComponent` 顶层协议,四组件 `override` | ✅ | 编译零警告 |
|
||||
| 2 | `Application` 组件表 `m_components` + `BuildComponentTable` | ✅ | — |
|
||||
| 3 | `ApplyDpi` / `Relayout` / `WM_MOUSELEAVE` / `WM_SETCURSOR` 四条链改表驱动 | ✅ | `uicheck` 69/69 |
|
||||
| 4 | `UiStackRects` 首次用于生产代码(内容区三列) | ✅ | 像素哈希不变 |
|
||||
| 5 | `SignalPanelInteraction.cpp` 2329 → 4 片 | ✅ | 8 哈希不变 |
|
||||
| 6 | `PopupMenu.cpp` 1901 → 3 片 + `PopupMenuInternal.hpp` | ✅ | 8 哈希不变 |
|
||||
| 7 | 代码审查(注释/名实不符/残留引用) | ✅ | 5 处修正 |
|
||||
| 8 | `Tools/` 从提交树剥离 + `.gitignore` | ✅ | `git ls-files Tools` 为空 |
|
||||
| 9 | 三份文档更新 | ✅ | — |
|
||||
| `LAYOUT_BOUNDS` | `1` | 是否编入「显示布局边界」调试叠加层 | `make release LAYOUT_BOUNDS=0` 可剔除 |
|
||||
| `NO_EXCEPTIONS` | `1` | 追加 `-fno-exceptions` | 必须配合 `MemProbe` 的预检验 |
|
||||
| `LTO` | `1` | 追加 `-flto`(编译 + 链接) | **切换需 `make clean`** |
|
||||
|
||||
### 1. `UiComponent`:为什么要它、为什么不用 `UiWidget`
|
||||
派生变量:
|
||||
|
||||
新建 `Components/Shared/Inc/UiComponent.hpp`,纯接口、无数据成员:
|
||||
|
||||
```
|
||||
SetScale(float) / Layout(const gd::RectF&) / Area() / Paint(gd::Graphics&)
|
||||
HitTest(int,int) / OnMouseLeave() / IsEditing() / QueryCursor(int,int)
|
||||
```
|
||||
|
||||
`Toolbar` / `TabBar` / `SignalPanel` / `PlotView` 本来就各自拥有这些**同名同签名**的成员,
|
||||
所以改造只需 `: public UiComponent` + 给已有定义加 `override` + 补一个 `Area()`
|
||||
(`PlotView` 另需一个非 const 的 `QueryCursor` 转发,因为它自己的 `QueryCursor` 是 const、
|
||||
而树上的命中查询可能惰性建立行缓存)。
|
||||
|
||||
**结论:这一步不是负优化** —— 生成代码几乎不变(全是静态派发,无虚表之外的额外开销),
|
||||
却让宿主从"认识每一个组件"变成"认识一份表"。体积实测 `TargetCheck.exe` 891 392 B,
|
||||
仍低于 900 KB 阈值;`Target.exe` 886 784 B 与改造前一致。
|
||||
|
||||
### 2. 宿主侧:手写链 → 组件表(4 处)
|
||||
|
||||
| 消息 | 改造前 | 改造后 |
|
||||
| 变量 | `LTO=1` | `LTO=0` |
|
||||
| --- | --- | --- |
|
||||
| `ApplyDpi` | 逐个 `SetScale`(6 行,新增组件必须记得加) | `for (entry : m_components) entry.component->SetScale(scale)` |
|
||||
| `Relayout` | 手算 panel/splitter/plot 三组 x 坐标 | `UiStackRects(内容区, HORIZONTAL, {panelW, splitW, AUTO}, 0, rects)` |
|
||||
| `WM_MOUSELEAVE` | 逐个 `OnMouseLeave` 并分别累积 | 循环累积 `chromeChanged` / `plotChanged` |
|
||||
| `WM_SETCURSOR` | `if (绘图区) … else if (面板) …` | 从表尾(最上层)向前找首个 `HitTest` 命中者,取其 `QueryCursor` |
|
||||
| `FUNC_SECTIONS` | 空 | `-ffunction-sections -fdata-sections` |
|
||||
| `ASM_LISTING` / `ASM_LISTING_C` | 空 | `-Wa,-a,-ad,-ahlms=$(@:%.o=%.lst)` |
|
||||
| `GC_SECTIONS` | `-Wl,--gc-sections` | 同左 |
|
||||
|
||||
保留显式的三条链(**有意,不是漏改**):左键按下 / 松开 / 鼠标移动。
|
||||
它们各自挂着一串"组件专属后置动作"(提交重命名、构建派生信号、面板联动滚动、
|
||||
更新拖拽光标、判断 `plotOnlyMove` 决定局部还是整帧失效)。
|
||||
把这些塞进表里要引入 lambda 或回调字段,可读性反而下降 —— 属于用户明确禁止的"过度抽象"。
|
||||
> `GC_SECTIONS` / `FUNC_SECTIONS` / `ASM_LISTING*` 都定义在 `LDFLAGS` 之后。
|
||||
> 它们是递归变量(`=` / `+=`),make 在展开 recipe 时才求值,所以顺序不影响生效;
|
||||
> 之所以还是放在后面,是为了让"三个开关"集中在同一处,不散落在两个位置。
|
||||
|
||||
### 3. 拆分对照
|
||||
### 2.2 两个已复现的坑
|
||||
|
||||
| 原文件 | 拆后 | 内容职责 |
|
||||
| --- | --- | --- |
|
||||
| `SignalPanelInteraction.cpp` 2 329 | **831** | 命中测试、鼠标/键盘派发、光标形状 |
|
||||
| + `SignalPanelEdit.cpp` | 616 | 筛选框、组名/信号名、数值输入、颜色与透明度 |
|
||||
| + `SignalPanelScroll.cpp` | 205 | 折起动画、联动滚动、滚动到目标信号 |
|
||||
| + `SignalPanelMenu.cpp` | 527 | 系数/偏移、镜像、右键菜单与命令回执 |
|
||||
| `PopupMenu.cpp` 1 901 | **706** | `OnEdit*` 交互分支 |
|
||||
| + `PopupMenuLayout.cpp` | 753 | 尺寸测量、项矩形排版、滚动可见性 |
|
||||
| + `PopupMenuPaint.cpp` | 397 | 背景/阴影/复用 `UiControls` 的项绘制 |
|
||||
| + `Inc/PopupMenuInternal.hpp` | 103 | `pmdetail` 常量 + `inline PaintResetIcon` |
|
||||
**坑 1:切换 LTO 必须 `make clean`。**
|
||||
LTO 与非 LTO 目标文件混链会让 GCC 15.2.0 在 `binds_to_current_def_p (symtab.cc:2598)` 处 ICE。
|
||||
|
||||
拆分方式:**整函数搬移**(不重写、不改签名),共享的尺寸常量与辅助函数收敛到
|
||||
`*Internal.hpp`(`constexpr` / `inline`,跨 TU 安全)。
|
||||
这样每个 TU 的行数落在 200–900 的可读区间,而逐像素输出保持 0 差异(哈希佐证)。
|
||||
**坑 2:`-flto` 与 `-ffunction-sections -fdata-sections` 同时打开也会触发同一个 ICE。**
|
||||
即使全部干净重编也必崩(`Utilities/Inc/DataModel.hpp:393` 的 `struct SignalData` 被指为出错点,
|
||||
但那只是 COMDAT 解析的落点,不是原因)。
|
||||
**解决办法**:LTO 下 `FUNC_SECTIONS` 置空(LTO 自己就做跨单元分区与去重),
|
||||
并追加 **`-flto-partition=one`**:
|
||||
默认多分区 LTRANS(本项目 11 个分区)稳定复现,单分区从未复现。
|
||||
代价是 LTRANS 串行,全量构建慢约一倍。
|
||||
|
||||
### 4. 代码审查结果
|
||||
> 结论:**`-flto-partition=one` 不是性能选项,是稳定性选项**,不要删。
|
||||
|
||||
| 文件 | 问题 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| `SignalPanelInteraction.cpp` | 头注释仍写"4 片:核心/渲染/交互与编辑" | 改为六分片清单(含本文件定位) |
|
||||
| `PopupMenuLayout.cpp` | 构造函数被机械搬移导致重复定义 | 只保留在 `PopupMenu.cpp` |
|
||||
| `PopupMenu.cpp` | `#include "PopupMenuInternal.hpp"` 落在 `namespace jdi` 内 | 移到文件头 |
|
||||
| `PopupMenuPaint.cpp` | 缺 `UiControls.hpp`(`UiDrawEditFieldContent` 未声明) | 补 include |
|
||||
| 全仓 | 历史删除符号残留引用 | grep 9 个符号,**零命中** |
|
||||
### 2.3 实测体积
|
||||
|
||||
### 5. `Tools/` 剥离
|
||||
| 阶段 | 产物 | 体积 | 相对基线 |
|
||||
| --- | --- | --- | --- |
|
||||
| 基线 | `BaseCheck.exe` | 909,824 B | — |
|
||||
| `-fno-exceptions` + `--gc-sections` + `MemProbe` | `Step1.exe` | 882,688 B | **−27,136 B** |
|
||||
| 再叠加 LTO | `JustDataInspector.exe` | **788,992 B** | **−120,832 B(−13.3%)** |
|
||||
|
||||
`git rm -r --cached Tools` + `.gitignore` 新增 `Tools/`,
|
||||
并对 `UI_Refactor` 分支做 `filter-branch` 清除历史中的该目录(分支全本地、未推送,风险可控);
|
||||
本地文件在剥离前已备份到 `%TEMP%` 并复原,`uicheck` / `rendercheck` 随时可复跑。
|
||||
段构成(`objdump -h`,最终构建):
|
||||
|
||||
### 6. 验证数据
|
||||
| 段 | 基线 | `-fno-exc` + `gc-sections` | 最终(LTO) |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `.text` | 735,600 | 720,528 | **657,168** |
|
||||
| `.rdata` | 49,184 | 45,072 | **35,016** |
|
||||
| `.xdata` | 48,132 | 34,152 | **27,512** |
|
||||
| `.pdata` | 35,424 | 34,524 | **28,524** |
|
||||
| `.rsrc` | 20,656 | 20,656 | 20,656 |
|
||||
| `.data` | 2,272 | 9,312 | 1,952 |
|
||||
| `.bss` | 10,224 | 3,088 | 10,000 |
|
||||
| `.idata` | 12,440 | 12,440 | 12,440 |
|
||||
| `.tls` | 16 | 16 | 16 |
|
||||
| `.reloc` | — | — | 1,700 |
|
||||
|
||||
```
|
||||
make release (TargetCheck) → 零 error / 零 warning
|
||||
rendercheck: A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289
|
||||
E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113 (全部与基线一致)
|
||||
FUNC 7/7 PASS
|
||||
uicheck: 69 passed, 0 failed
|
||||
体积: Build/Release/TargetCheck.exe = 891,392 B (< 900 KB)
|
||||
```
|
||||
**体积溯源(超阈值时必须能回答这个问题)**:
|
||||
|
||||
* `.text` 减少最多(−78 KB / −10.7%)—— 主要来自 LTO 的跨单元内联与重复代码合并。
|
||||
* `.xdata`+`.pdata`(异常展开表 + 函数表)减少 27.5 KB —— 来自 `-fno-exceptions`
|
||||
与 `--gc-sections` 丢弃未引用的函数。
|
||||
* `.rdata` 减少 14 KB —— 来自"未引用字符串/常量被 gc 掉"。
|
||||
* `.rsrc` 完全不变(图标/清单)。
|
||||
|
||||
> 预警阈值 1MB(1,048,576 B):当前 788,992 B,**余量 259,584 B(约 253 KiB)**。
|
||||
|
||||
### 2.4 `MemProbe`(预检验 + 兜底)
|
||||
|
||||
| 函数 | 作用 |
|
||||
| --- | --- |
|
||||
| `MemAvailablePhysical()` | `GlobalMemoryStatusEx` + **200ms 缓存**(绘制路径每帧可能问多次) |
|
||||
| `MemCanAllocate(bytes)` | 留出 1/8 可用内存作 headroom 后判断是否安全 |
|
||||
| `MemReserveIfRoom(container, n)` | 仅供"按数据规模预留":含 `capacity()` 早退与乘法溢出保护 |
|
||||
| `MemInstallTerminateHandler()` | `std::set_terminate` 装一次性模态提示后 `_Exit` |
|
||||
|
||||
改动点:
|
||||
|
||||
* `Utilities/Src/DataModel.cpp`:原先**唯一**的 `try/catch`(`extremes.reserve(count / 8)`
|
||||
捕 `std::bad_alloc`)→ `MemReserveIfRoom(extremes, count / 8)`。
|
||||
* `Components/PlotView/Src/PlotView.cpp`:`PlotView::AvailablePhysicalBytes()` 原有**一份完整的
|
||||
`GlobalMemoryStatusEx` + 200ms 缓存实现**(与 `MemProbe` 重复)→ 改为转发 `MemAvailablePhysical()`。
|
||||
* `Components/Application/Src/Application.cpp`:`Initialize()` 开头 `MemInstallTerminateHandler();`。
|
||||
|
||||
> 全仓已确认**无 `try/catch` 残留**(除 `MemProbe` 注释中对它的说明)。
|
||||
|
||||
---
|
||||
|
||||
## 第八轮(2026-10-28):显示布局边界 + 三处可见收益优化
|
||||
## 3. L1 几何产物层
|
||||
|
||||
### 进度总览
|
||||
|
||||
| # | 任务 | 状态 | 验证 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `Toolbar` 控件树按需重建(`m_treeDirty`) | ✅ | 8 场景区域哈希不变 |
|
||||
| 2 | `Toolbar::SetActionLabel` 单一真值来源 | ✅ | 光标按钮文本仍随模式变化 |
|
||||
| 3 | `UiWidget::DispatchRedraw` 收敛重复判断 | ✅ | `uicheck` 69/69 |
|
||||
| 4 | `UiButtonWidget` 支持矢量图标(GEAR) | ✅ | 放大 4 倍核对形状 |
|
||||
| 5 | `ToolbarAction::SETTINGS` + 左侧固定簇布局 | ✅ | 4 条几何断言 |
|
||||
| 6 | `MenuScope::APP_SETTINGS` + 设置菜单 | ✅ | 5 条菜单断言 |
|
||||
| 7 | `UiComponent::PaintDebugBounds` 协议 + 四组件上报 | ✅ | 无开关时零像素影响 |
|
||||
| 8 | `UiWidget::PaintDebugBounds` 递归控件树 | ✅ | 品红描边 19 734 px |
|
||||
| 9 | `Application::PaintLayoutBounds` + 整帧回送 | ✅ | 关闭后逐像素还原 |
|
||||
| 10 | 文档更新 | ✅ | — |
|
||||
|
||||
### 关键文件
|
||||
|
||||
**新增**
|
||||
* `Components/Shared/Inc/UiDebugBounds.hpp` / `Src/UiDebugBounds.cpp` —— 调试边界描边原语
|
||||
(层级配色、只描边不填充、整像素锐利对齐)。
|
||||
* `Components/Shared/Src/UiComponent.cpp` —— `PaintDebugBounds` 默认实现(只画 `Area()`)。
|
||||
|
||||
**修改**
|
||||
* `Shared/Inc/UiComponent.hpp`、`Shared/Inc|Src/UiWidget.hpp/.cpp`、`Shared/Inc|Src/UiButtonWidget.hpp/.cpp`
|
||||
* `Toolbar/Inc|Src/*`、`TabBar/Inc|Src/*`、`SignalPanel/Inc/SignalPanel.hpp` + `Src/SignalPanel.cpp`
|
||||
* `PlotView/Inc/PlotView.hpp` + `Src/PlotViewPaint.cpp`
|
||||
* `Shared/Inc/PopupMenu.hpp`(`MenuScope::APP_SETTINGS`、`Rect()`)、`Shared/Inc/ColorPickerPopup.hpp`(`Rect()`)
|
||||
* `Application/Inc/Application.hpp`、`Inc/ApplicationInternal.hpp`、
|
||||
`Src/Application.cpp`(Render 末尾叠加 + `fullFrame`)、`Src/ApplicationCommands.cpp`(菜单/派发/边界绘制)
|
||||
|
||||
### 验证数据
|
||||
### 3.1 结构
|
||||
|
||||
```
|
||||
make release (TargetCheck) -> 零 error / 零 warning, 897,024 B
|
||||
.preflight/lbcheck -> 21 passed, 0 failed
|
||||
INFO area-pixels=24930 node-pixels=19734
|
||||
INFO gear=(15.0,55.0 50.0x37.5) menu=(15.0,97.0 185.0x73.8)
|
||||
rendercheck 工具栏以下区域哈希(改造前 == 改造后):
|
||||
AB AABF558F499583EA BB 1B8B2D389D41B988 CB CAC17AA78C515079 DB 154617738CD9C422
|
||||
EB DA484B02EEEC778A FB B3CE740F60CF3688 GB 4DD14C0C2EC4CE91 HB D84400B723089F43
|
||||
rendercheck FUNC 7/7 PASS
|
||||
uicheck 69 passed, 0 failed
|
||||
UiBox.hpp struct UiBox { rect, clip, depth, layer }; ← 纯值,无行为
|
||||
class UiBoxTable : UiTextRectSink ← 只累积几何 + 裁剪/层/遮挡
|
||||
UiDebugBounds.hpp UiDebugBoundsColor / UiDebugPaintBoundsClipped / UiDebugPaintBoxList
|
||||
↑ 唯一的消费者(调试描边),与几何解耦
|
||||
```
|
||||
|
||||
> 说明:整帧哈希 A–H 因为工具栏左侧多了一个齿轮按钮而整体变化(预期),
|
||||
> 因此给探针加了「工具栏带以下」的区域哈希用于**证明改动范围被完全限制在工具栏内**。
|
||||
`UiComponent::BuildBoxes(UiBoxTable&)` 默认实现:
|
||||
|
||||
### 验证坑(本轮新增)
|
||||
```cpp
|
||||
void UiComponent::BuildBoxes(UiBoxTable& boxes) { boxes.Add(Area(), 0); }
|
||||
```
|
||||
|
||||
* PS 5.1 读中文 `.ps1` 会乱码 ⇒ 探针/脚本里只写 ASCII,中文注释另存 UTF-8 无 BOM 数据文件。
|
||||
* 想拿"改造前"的区域基线必须 `git stash push -u`(**含未跟踪文件**),否则新增的
|
||||
`UiDebugBounds.cpp` / `UiComponent.cpp` 仍会被 Makefile 的 `Src/*.cpp` 通配扫到而编译失败。
|
||||
* 「工具栏带以下」的起点必须用**运行时** `m_toolbarRect.Y + Height`;写死 70 会切进工具栏,
|
||||
齿轮正好落在被统计的区间里,导致本应相同的哈希全都不同。
|
||||
各组件覆盖情况:
|
||||
|
||||
| 组件 | 上报内容 |
|
||||
| --- | --- |
|
||||
| `SignalPanel` | 表头 / 筛选框 / 列表 / 样式区 / 每个可见行 / 行内元素(复选框、色块、名称、GX 徽标、✕、折起三角、组计数)/ 滚动轨道与滑块 / 编辑框 |
|
||||
| `PlotView` | 网格、GX 徽标、图例条目、**5 类浮动面板**(含逐字段)+ 面板遮挡与裁剪 |
|
||||
| `Toolbar` | 区域、每个按钮、按钮内容(文本/图标) |
|
||||
| `TabBar` | 区域、每个标签条、新建按钮、标签 ✕、控件树 |
|
||||
| `PopupMenu` | 条目(裁到 `m_scrollEnabled ? m_scrollViewport : m_rect`)、可视区、轨道、滑块、二级子菜单 |
|
||||
| `ColorPickerPopup` | 各控件 |
|
||||
|
||||
### 3.2 与 `UiDebugBounds` 的边界
|
||||
|
||||
* 几何(矩形 / 层级 / 层号 / 裁剪 / 遮挡)→ `UiBox.hpp`
|
||||
* 观感(配色 / 包角粗细 / 图元数量 / 抗锯齿开关)→ `UiDebugBounds.hpp`
|
||||
|
||||
这样 `SignalPanel.cpp` / `PlotViewPaint.cpp` / `PopupMenu.cpp` 等**只包含 `UiBox.hpp`**,
|
||||
不再把"调试描边"这个依赖拉进自己的编译单元;只有宿主 `ApplicationCommands.cpp` 同时包含两者。
|
||||
|
||||
### 3.3 零像素回归的证明
|
||||
|
||||
L1 落地前后 `JustDataInspector.exe` **字节数完全相同(788,992 B)**,
|
||||
`rendercheck` 8 个场景哈希逐一相同 —— 因为这只是"把几何从调试绘制里搬出来",
|
||||
没有动任何绘制顺序或图元。
|
||||
|
||||
---
|
||||
|
||||
## 第九轮(2026-10-28):边界观感重做 + 覆盖补全 + 几何同源
|
||||
## 4. 回归结果(最终构建上跑)
|
||||
|
||||
### 进度总览
|
||||
|
||||
| # | 任务 | 状态 | 验证 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 齿轮按钮 `primary = true`(与「导入文件」同色) | ✅ | 截图核对(两按钮同为强调色) |
|
||||
| 2 | 边界改为「四角粗 + 四边细」包角 | ✅ | 截图核对 |
|
||||
| 3 | 层级 > 0 内缩 1px | ✅ | 同上 |
|
||||
| 4 | 协议改为 `CollectDebugBoxes`(收集 → 宿主统一绘制) | ✅ | 编译 + 探针 |
|
||||
| 5 | `SignalPanel` 行内元素全覆盖 | ✅ | box 计数 |
|
||||
| 6 | `PlotView` GX 徽标 / 图例 / 5 类浮动面板 | ✅ | box 计数 |
|
||||
| 7 | `Toolbar` 按钮内容矩形 | ✅ | 截图(文字/图标各有框) |
|
||||
| 8 | `PopupMenu` / `ColorPickerPopup` 内部结构 | ✅ | 截图(菜单项有框) |
|
||||
| 9 | `RowRect` O(row) → O(1) | ✅ | 8 场景区域哈希不变 |
|
||||
| 10 | `PanelBounds()` 收敛 6 处重复 | ✅ | 编译 |
|
||||
| 11 | 文档 + 共享记忆 | ✅ | — |
|
||||
|
||||
### 1. 描边样式:`⌜ ⌝ ⌞ ⌟` 包角
|
||||
|
||||
之前是四条**等宽实心矩形**(层级 0 用 2px、其余 1px),所以同一元素的"角"和"边"粗细不一致,
|
||||
而且父子矩形共边时会叠成更粗的一条线。
|
||||
|
||||
现在:
|
||||
|
||||
| 部位 | 规则 |
|
||||
| 探针 | 结果 |
|
||||
| --- | --- |
|
||||
| 四边 | 固定 **1 设备像素** |
|
||||
| 四角 | 额外加粗(层级 0 = 4px,其余 3px),沿水平/垂直各延伸 `cornerLen`(≤14px,且不超过短边一半) |
|
||||
| 内缩 | 层级 > 0 整体内缩 **1px**,与父矩形共边时不再叠粗 |
|
||||
| 填充 | **完全不填充**(几十层嵌套填充会把内容糊住) |
|
||||
| `Tools/uicheck` | **69 passed, 0 failed** |
|
||||
| `.preflight/lbcheck` | **21 passed, 0 failed** |
|
||||
| `.preflight/lbresid` | **5 passed, 0 failed** |
|
||||
| `.preflight/lbzorder` | **4 passed, 0 failed** |
|
||||
| `.preflight/lbcovr` | **7 passed, 0 failed** |
|
||||
| `.preflight/lbpanel` | **10 passed, 0 failed** |
|
||||
|
||||
实现要点:整像素对齐 + `PixelOffsetModeNone` + 关抗锯齿,四条边与八个角段全部用
|
||||
`FillPixels` 实心矩形画(`DrawRectangle` 会因笔宽奇偶偏移半像素)。
|
||||
|
||||
### 2. 覆盖率:从"一根大框"到"逐元素"
|
||||
|
||||
协议改成**两段式**:组件只负责"收集 box",宿主统一描边。
|
||||
`Tools/rendercheck`(逐像素哈希,必须与基线逐一相同):
|
||||
|
||||
```
|
||||
UiComponent::CollectDebugBoxes(UiDebugBoxList&) 默认只登记 Area()
|
||||
UiWidget::CollectDebugBoxes(boxes, depth) 递归整棵控件树
|
||||
宿主 PaintLayoutBounds 收集完一次性画(按 depth 升序)
|
||||
A FAED59DF284153DB B E6443BFE4E3955F8 C 96A0AA8C402F1729 D E3D5DBD998E43720
|
||||
E DA0583CB13C26A6F F FFB822159F604D1D G 19F8DE8FE3A8DF18 H DBBDDE4CB2C97C92
|
||||
FUNC 7/7 PASS, exit=0
|
||||
```
|
||||
|
||||
新增覆盖:
|
||||
构建:`make release` 零 error / 零 warning;`make debug` 零 warning。
|
||||
|
||||
| 组件 | 本轮新增的 box |
|
||||
| --- | --- |
|
||||
| `Toolbar` | 每个按钮 + **按钮内容**(文本/齿轮图标,居中矩形) |
|
||||
| `TabBar` | 标签条 / 新建按钮 / 每个 ✕ |
|
||||
| `SignalPanel` | 表头 / 筛选框 / 列表 / 样式区;筛选输入框 + 清空 ✕ + 全部显示;滚动条轨道 + 滑块;样式 5 行 + 12 个色块 + 自定义 + 3 个模式按钮 + 3 个调整按钮 + 线宽/透明度轨道与数值;目标行 + 名称 + GX 徽标;**每行**(仅可见)+ 折起三角 + 复选框 + 色块 + 名称 + GX 徽标 + ✕ / 组名 + 计数;内联编辑框 |
|
||||
| `PlotView` | 每个网格 + GX 徽标 + 装饰层矩形;图例条目(托底/色块/文本);5 类浮动面板的面板/标题/✕/折起三角/徽标/编辑行/按钮/内容区/滚动轨道/滑块/透明度轨道与数值/两个显示开关 + 各行复选框/数值/滑轨/复位/名称/✕ + 分组栏/表头/三角/✕ + 每个固定点行与 ✕ |
|
||||
| `PopupMenu` | 菜单浮层 + 每个条目 + 可滚动可视区 + 轨道 + 滑块 + 二级子菜单与其条目 |
|
||||
| `ColorPickerPopup` | 面板 + 标题/预览/色号/色域/色相条 + 3 条分量滑轨与数值 + 输入框 |
|
||||
|
||||
**标签做尺寸门槛**:小于 56×16 逻辑像素的 box 不画文字标签(复选框/色块/✕ 紧邻,
|
||||
逐个小框贴字会糊成一片;它们的归属靠包角形状与颜色层级就能分辨)。
|
||||
|
||||
### 3. 几何同源与性能
|
||||
|
||||
* `RowRect(row)`:原来每次都从头累加行高(**O(row)**),而画一行要调它 **5 次**
|
||||
(✕ / 徽标 / 名称 / 组计数 / 折起三角)。新增 `m_rowTop` 前缀和,由
|
||||
`UpdateAnimGeometry()` 统一重建 ⇒ **O(1)**;长度不一致时自动退回逐行累加(正确性优先)。
|
||||
* `ContentOffsetBeforeRow()` 同样改用前缀和。
|
||||
* `PanelBounds()` 收敛 `(m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area`
|
||||
在 6 处的重复。
|
||||
|
||||
### 4. 验证数据
|
||||
|
||||
```
|
||||
make release / make debug 零 error 零 warning
|
||||
TargetCheck.exe 910,336 B(889 KiB,< 900 KiB)
|
||||
.preflight/lbcheck 21 passed, 0 failed
|
||||
INFO area-pixels=16080 node-pixels=20629
|
||||
uicheck 69 passed, 0 failed
|
||||
rendercheck 「工具栏带以下」区域哈希(与上一轮逐一相同):
|
||||
AB AABF558F499583EA BB 1B8B2D389D41B988 CB CAC17AA78C515079 DB 154617738CD9C422
|
||||
EB DA484B02EEEC778A FB B3CE740F60CF3688 GB 4DD14C0C2EC4CE91 HB D84400B723089F43
|
||||
rendercheck FUNC 7/7 PASS
|
||||
```
|
||||
|
||||
> 整帧哈希必然变化(齿轮变主色 + 图标重画),因此继续用「工具栏带以下」的区域哈希
|
||||
> 证明"改动被完全限制在工具栏带内"。
|
||||
|
||||
### 5. 本轮踩到的坑
|
||||
|
||||
* `UiDebugPaintBounds` 的旧签名带 `scale` 参数,但线宽刻意不随 DPI 变 —— 重构时应当**删掉这个
|
||||
参数**而不是留个 `(void)scale`(已删)。
|
||||
* 用 PowerShell 按行替换大段代码时,`while ($lines[$end].TrimEnd() -ne "}")` 找结尾会失败:
|
||||
文件里的闭括号带**前导缩进**,必须用 `Trim()` 而不是 `TrimEnd()`;否则会越界并报
|
||||
`InvokeMethodOnNull`,而且**已经把开头删掉了**,必须在替换后逐个核对括号配对。
|
||||
* 齿轮图标在 22px 时,"齿从环中心线伸出"会让齿被环的描边吃掉,看起来只剩一个甜甜圈。
|
||||
必须让**齿占最外一圈、外环缩到 0.54R**,齿才会明显。
|
||||
* 中文不能写进 `.ps1`(PS 5.1 按 GBK 读),文本块要落到 UTF-8 数据文件再由 ASCII 脚本拼接。
|
||||
> ⚠️ 探针 `build.ps1` 链接的是 `Build/Release/*.o`。**LTO 下这些是 LTO 字节码**,
|
||||
> 因此 7 个探针脚本的链接参数里都补了 `-flto -flto-partition=one`。
|
||||
> 若把 `LTO` 关掉,这两个参数无害(可与普通目标文件混链)。
|
||||
|
||||
---
|
||||
|
||||
## 第十轮(2026-10-28):包角自适应 + 文字标签移除 + 绘制残留与层级缺陷修复
|
||||
## 5. 本轮审计清单
|
||||
|
||||
### 进度总览
|
||||
|
||||
| # | 任务 | 状态 | 验证 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 包角粗细/长度按短边自适应 | ✅ | 3 倍放大截图 |
|
||||
| 2 | 小方框只画角、不画边 | ✅ | 同上 |
|
||||
| 3 | 删除文字标签与 `UiDebugPaintStyle` | ✅ | 体积 −1.5 KB |
|
||||
| 4 | 修复拖滚动条时的绘制残留(三条快路径) | ✅ | `.preflight/lbresid` |
|
||||
| 5 | 修复 `m_decorRects` 幽灵框 | ✅ | box 清单打印 |
|
||||
| 6 | box `clip`(组件地盘) | ✅ | 差异 bbox 不再越过绘图区上沿 |
|
||||
| 7 | `(layer, depth)` 双排序键 | ✅ | `.preflight/lbzorder` |
|
||||
| 8 | 浮层 `occluder` 挖洞 | ✅ | 菜单区 `amber=0 green=0` |
|
||||
| 9 | 文档 + 共享记忆 | ✅ | — |
|
||||
|
||||
### 1. 描边按尺寸自适应(修复"小元素变成粗描边")
|
||||
|
||||
旧规则:四角固定 3–4px、四边 1px。对 15px 的复选框来说,3px 包角占短边 20%,看起来就是粗边框。
|
||||
|
||||
新规则(全部以**短边**为基准):
|
||||
|
||||
| 量 | 规则 |
|
||||
| 项 | 处理 |
|
||||
| --- | --- |
|
||||
| 四边 | 固定 1 设备像素 |
|
||||
| 包角粗细 | `min(基础值 4/3, 短边/10)`,再夹到 `[1, 短边/4]` |
|
||||
| 包角长度 | `min(14, 短边/3)`,且不小于包角粗细 |
|
||||
| 短边 ≤ 12 | **只画四个 L 形角标记、不画边**(一圈连续描边会让小方框显得"被框住") |
|
||||
| 短边 ≤ 5 | 完全不画(再画就盖住元素本身) |
|
||||
| 内缩 | 仅层级 > 0 **且短边 ≥ 10** 时内缩 1px |
|
||||
|
||||
同时**删除了文字标签**(含 `UiDebugPaintStyle`):黑底白字在小元素上必然连成一片,
|
||||
且体积上省掉约 1.5 KB。
|
||||
|
||||
### 2. 绘制残留:三个独立成因,逐个修复
|
||||
|
||||
| # | 成因 | 修复 |
|
||||
| --- | --- | --- |
|
||||
| R1 | `PlotView::CollectDebugBoxes` 读了 `m_decorRects` —— 那是**装饰层位图缓存**的登记矩形,坐标处在**位图/图层坐标系**(带负 Y、超出绘图区)。于是出现 `(472,-27 1213x101)` 这类**横跨工具栏的幽灵巨框** | 不再读 `m_decorRects`;只保留 `LaneBadgeRect` 与 `m_legendHits`(屏幕坐标) |
|
||||
| R2 | `Paint` 的三条快路径(静态场景贴图 / 宽基底贴图 / 样式补丁)只重画**变化的部分**,而调试边框是画在整个窗口上的叠加内容 ⇒ 上一帧的边框残留在未被重画的像素上 | 新增 `PlotView::SetForceFullRepaint(bool)`:开启调试叠加层时禁用全部快路径 |
|
||||
| R3 | 组件内元素可能落在自己地盘之外(滚出可视区的 `lane.rect` Y 为负),不裁剪就画到工具栏/标签栏上 | box 增加 `clip`:宿主按组件 `Area()` 设置,`Add()` 跳过无交集、绘制时求交 |
|
||||
|
||||
另外把 `Render` 的 `fullChrome` 也并入 `m_showLayoutBounds`,保证装饰区每帧都是完整一帧。
|
||||
|
||||
### 3. 层级:`(layer, depth)` 双排序键 + 浮层挖洞
|
||||
|
||||
* `layer` 由 `UiDebugBoxList::PushLayer()` 分配,宿主每收集完一个组件就 +1 ⇒ **后收集的压住先收集的**
|
||||
(右键二级菜单是最后收集的浮层)。
|
||||
* `depth` 在同一层内先画父再画子,子元素的彩色包角不被父的边压住。
|
||||
* ⚠️ 仅靠排序**不够**:调试线画在所有内容之后,下层组件的线仍会显示在不透明菜单上面。
|
||||
因此新增 `AddOccluder()`:菜单/拾色器把本体登记为遮挡矩形,绘制时对**更低层**的 box
|
||||
用 `Region::Exclude` 挖洞。实测菜单区与子菜单区的琥珀/绿像素均为 **0**。
|
||||
|
||||
### 4. 验证数据
|
||||
|
||||
```
|
||||
make release / make debug 零 error 零 warning
|
||||
TargetCheck.exe 906,752 B(885.5 KiB,< 900 KiB)
|
||||
.preflight/lbresid 5/5 toolbar-unchanged-while-scrolling / repaint-same-state-is-identical
|
||||
/ toolbar-restored-by-full-repaint / residual-never-above-plot-area
|
||||
/ later-layer-wins-over-earlier
|
||||
INFO toolbar diff pixels=0;same-state 两帧 settled after 1 extra frame 后逐像素相同
|
||||
.preflight/lbzorder 4/4 menu 区 amber=0 green=0;submenu 区 amber=0 green=0
|
||||
.preflight/lbcheck 21/21
|
||||
uicheck 69/69
|
||||
rendercheck 「工具栏带以下」区域哈希 8/8 与上一轮逐一相同 + FUNC 7/7
|
||||
```
|
||||
|
||||
### 5. 本轮踩到的坑
|
||||
|
||||
* `Region::MakeInfinite()` 之后必须 `Intersect` 一个足够大的有限矩形,否则 `Exclude` 的行为
|
||||
在部分 GDI+ 版本上不可预期;这里用 `(-1e5,-1e5) + 2e5 见方`。
|
||||
* 给浮层设 `clip = 菜单本体` 会把**展开到本体之外**的二级子菜单整个裁掉
|
||||
(首版就是这样,子菜单一个框都没有)。正确做法是浮层不设 clip,改用它自己登记的 occluder。
|
||||
* 探针判定"是否有残留"不能拿"拖拽帧 vs 松开后一帧"直接比:两者渲染路径不同
|
||||
(图层预热进度不同)会得到几十万像素的**合法**差异。正确判据是
|
||||
**同一状态连续两帧必须逐像素相同**。
|
||||
| `MemProbe.hpp` / `.cpp` 里"省 40~60KB"的估算 | 改为实测值 27,136 B(909,824 → 882,688) |
|
||||
| `MemProbe.hpp` 的 `#include <new>` | 未使用,删除 |
|
||||
| `MemProbe.cpp` 的 `#include <cstdio>` | 未使用,删除 |
|
||||
| `UiDebugPaintBounds(g, rect, depth)` | **零调用者**(唯一调用者是它自己),删除;只保留带 `clip` 的版本 |
|
||||
| `TabBar::BuildBoxes` / `Toolbar::BuildBoxes` / `PlotView::BuildBoxes` / `SignalPanel::BuildBoxes` 的"(调试用)" | 改为"几何 box"(层级已变,它们不再关心调试) |
|
||||
| `PlotView::SetForceFullRepaint` 文档 | 补写"为什么保留而不优化"(L2 缓做的理由) |
|
||||
| 全仓旧名 `UiDebugBoxList` / `UiDebugBox` / `CollectDebugBoxes` | 源码零残留(仅 `Documents/agent_mem.md` 历史记录保留原样) |
|
||||
|
||||
---
|
||||
|
||||
## 第十一轮(2026-10-28):取消内缩 + 文本获得几何身份 + 补全漏项 + 描边提速
|
||||
## 6. 已知问题 / 未做
|
||||
|
||||
### 进度总览
|
||||
|
||||
| # | 任务 | 状态 | 验证 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 取消内缩(GX 胶囊圆弧不再落在框外) | ✅ | `box-gx-badge-matches-pill` |
|
||||
| 2 | `UiTextRectSink`:文本墨迹矩形统一上报 | ✅ | `text-boxes=89` |
|
||||
| 3 | 行位图缓存内的文本按原点回放 | ✅ | `row-text-boxes-from-cached-rows` |
|
||||
| 4 | 补 `PlotView` 漏项(时间轴/滚动条/回到最上方/光标标签/输入框) | ✅ | 4 条 box 断言 |
|
||||
| 5 | 描边图元 12→8 | ✅ | 5.892ms → **4.028ms** |
|
||||
| 6 | 关闭开关时清空容器 | ✅ | `boxes-cleared-when-off` |
|
||||
| 7 | 文档 + 共享记忆 | ✅ | — |
|
||||
|
||||
### 1. 取消内缩(这是"边框不清晰"的主因)
|
||||
|
||||
上一轮为"父子矩形共边不叠粗"给 `depth > 0` 的 box 加了 1px 内缩。结果:
|
||||
GX 徽标是 `30×17` 逻辑像素的**胶囊**(圆角 = 高的一半),外接矩形正好等于 `LaneBadgeRect`;
|
||||
内缩 1px 之后框比形状小一圈 ⇒ 左右尖端与上下圆弧全部落在框外。
|
||||
|
||||
**现行规则:框 = 可见形状的外接矩形,绝不内缩。** 父子共边时让"后画的覆盖"即可 ——
|
||||
这本来就是正确的图层语义。层级 > 0 只影响**配色**,不再影响几何。
|
||||
|
||||
### 2. 文本第一次有了几何身份
|
||||
|
||||
文本是本项目唯一"没有几何身份"的元素:全仓 65 处绘制点各自现算一个 `gd::RectF`、
|
||||
画完即弃。逐处补登记必漏,因此在**唯一出口**回报一次:
|
||||
|
||||
```
|
||||
PaintUtil 新增:
|
||||
class UiTextRectSink { virtual void OnTextRect(const gd::RectF&) = 0; };
|
||||
void UiSetTextRectSink(UiTextRectSink*); // 安装 / 卸载(nullptr)
|
||||
void UiSetTextRectOffset(float dx, float dy); // 离屏位图渲染时的坐标换算
|
||||
UiTextRectSink* UiGetTextRectSink();
|
||||
```
|
||||
|
||||
`DrawUiText()` 与 `UiTextAtClipped()` 在画完之后回报**收紧后的墨迹矩形**
|
||||
(按对齐方式 + `UiMeasureTextWidth/Height` 实测值收窄),因此每个文本 box
|
||||
就是文字真正占据的那一小块 —— 而不是整行。
|
||||
|
||||
`UiDebugBoxList` 直接继承 `UiTextRectSink`,所以宿主只需在 `Render` 里
|
||||
`UiSetTextRectSink(&m_debugBoxes)` 一句。
|
||||
|
||||
### 3. 缓存命中的行也要有文本框
|
||||
|
||||
面板的行是"先画进位图再贴回"。命中缓存的帧**根本不调用 `UiText`** ⇒
|
||||
绘制期收集器收不到任何东西,文本框会在第二帧起全部消失。
|
||||
|
||||
解法:行渲染时用一个本地 `RowTextCollector` 收集(**位图坐标**),连同位图一起存进
|
||||
`RowBitmapCache::textRects`;贴图时按 `(originX, originY)` 偏移**回放**给外层收集器。
|
||||
并且**无条件收集**(不看外层是否有收集器)—— 否则"关闭调试时滚过列表、再打开调试"
|
||||
会命中缓存而永远拿不到文本框。
|
||||
|
||||
### 4. 补全 `PlotView` 漏项
|
||||
|
||||
都已有成员变量,直接读:
|
||||
|
||||
| 元素 | 来源 |
|
||||
| --- | --- |
|
||||
| 时间轴整条区域 | `m_timeAxisRect` |
|
||||
| 网格滚动条 | `m_scrollBarRect` |
|
||||
| 「回到最上方」浮动按钮 | `BackToTopRect()` |
|
||||
| 双光标差值标签 | `m_cursorSpanRect` |
|
||||
| 光标读数标签(避让后实际位置) | `m_cursorLabelRects` |
|
||||
| 内联输入框 | `EditFieldRect()` |
|
||||
|
||||
### 5. 描边提速(12 → 8 图元)
|
||||
|
||||
微基准结论:**图元数量**是唯一瓶颈(每次 `FillRectangle` ≈ 1µs;`FillPath` 单次 ≈ 20µs,
|
||||
合并路径反而更慢;GDI `PolyPolygon` 单次 5688 矩形要 114ms)。
|
||||
|
||||
旧实现把每条边拆成"中段 1px + 两端各一段加粗" = 12 次;新实现 = **4 条 1px 边 + 4 个角块**。
|
||||
实测 `paintBoxes` 从 **5.892ms → 4.028ms(−32%)**,观感几乎一致(角仍是两条粗边的交点)。
|
||||
|
||||
### 6. 验证
|
||||
|
||||
```
|
||||
make release / make debug 零 error 零 warning
|
||||
TargetCheck.exe 909,824 B(888.5 KiB < 900 KiB)
|
||||
.preflight/lbcovr (新增) 7/7 box-time-axis / box-plot-scrollbar / box-back-to-top
|
||||
/ box-gx-badge-matches-pill / text-boxes-collected
|
||||
/ row-text-boxes-from-cached-rows / boxes-cleared-when-off
|
||||
.preflight/lbcheck 21/21 ;lbresid 5/5 ;lbzorder 4/4 ;uicheck 69/69
|
||||
帧耗时分解(MF4 / 283 网格):base 9.06ms / 仅强制整帧 6.66ms / 完整调试 11.35ms
|
||||
collect 0.082ms / paintBoxes 4.028ms(上轮 5.892ms)
|
||||
rendercheck 8 个场景的整帧与区域哈希与上一轮**逐一相同** + FUNC 7/7
|
||||
```
|
||||
|
||||
### 7. 本轮踩到的坑
|
||||
|
||||
* 只在"外层有收集器"时才收集行文本 ⇒ 关闭调试时缓存被填充、打开调试后永远拿不到文本框。
|
||||
**必须无条件收集**(成本只发生在缓存未命中的行上)。
|
||||
* 把浮层的 `clip` 设成菜单本体会把展开到本体外的二级子菜单整个裁掉(上一轮遗留,已确认仍正确)。
|
||||
* `UiDebugBoxList` 直接当收集器用时,`OnTextRect` 需要有自己的 `depth`(文本用 4),
|
||||
不能沿用调用点当时的 depth —— 否则文本与容器同色。
|
||||
|
||||
---
|
||||
|
||||
## 第十二轮(2026-10-29):分支同步 + 既有性能问题 + 浮层层级修复
|
||||
|
||||
### 0. 仓库同步
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `git fetch gitea --prune` | 新增 `gitea/UI_Refactor`;`gitea/main` 前进到合并提交 `322b658` |
|
||||
| `main` | 快进到 `322b658`(本地原先落后 22 个提交,现在与远端一致) |
|
||||
| `dev` | 远端不存在该分支 ⇒ **本地已删除**(用户确认是意外产物) |
|
||||
| `UI_Refactor` | 快进到 `322b658`(它原本是合并提交的祖先),继续作为工作分支 |
|
||||
|
||||
### 1. 进度总览
|
||||
|
||||
| # | 任务 | 状态 | 验证 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 分支同步 / 删 `dev` | ✅ | `git branch -vv` |
|
||||
| 2 | 定位"空闲帧比强制整帧慢"的真因 | ✅ | 两级计时探针 |
|
||||
| 3 | `RollWideBuffer` 把可视性判断提到最前 | ✅ | 排空后稳态 0.53ms |
|
||||
| 4 | 浮动面板另起绘制层 + 遮挡矩形 + 内容裁剪 | ✅ | `lbpanel` |
|
||||
| 5 | 菜单条目裁到可视区 | ✅ | `lbpanel` |
|
||||
| 6 | `EdgePanelLayoutFor` 前置廉价判据 | ✅ | 收集耗时 0.082→0.075ms |
|
||||
| 7 | 文档 + 共享记忆 | ✅ | — |
|
||||
|
||||
### 2. "既有性能问题"的真相
|
||||
|
||||
用两级计时(先整体、再拆到 `WarmLaneLayersStep` 内部)测出:
|
||||
|
||||
```
|
||||
排空前 idle(静态场景命中) 2.60 ms ← 之前误以为是稳态
|
||||
排空后 idle(稳态) 0.53 ms
|
||||
强制整帧 idle 0.46 ms
|
||||
[warm] render=1.3~2.8ms (pending=16→0) ← 每帧补 1 个网格图层
|
||||
```
|
||||
|
||||
结论修正:**不存在**每帧白烧的固定开销;那 2.4ms 是"图层预热队列排空"的**瞬时**成本
|
||||
(16 个网格 × 每帧补 1 个,约 16 帧后结束)。
|
||||
|
||||
但确实发现一处顺序问题:`RollWideBuffer` 对**所有** 283 个网格计算 `LaneLayerKey()`
|
||||
(逐网格内容哈希,实测约 0.7ms/帧),然后才判断是否可视并 `continue`;
|
||||
而且离屏网格的"结构不可用"分支还会把它们**逐帧塞进待重画队列**(再被 `remove_if` 清掉)。
|
||||
现在把可视性判断提到循环最前面 ⇒ 离屏网格既不哈希也不入队。
|
||||
|
||||
### 3. 浮动面板被网格边界穿透(层级 + 挖洞)
|
||||
|
||||
根因:`PlotView::CollectDebugBoxes` 把**网格盒子与面板盒子放在同一个绘制层**,
|
||||
只靠 `depth` 排序 —— 而调试线画在所有内容之后,"后画"只能决定谁压在谁上面,
|
||||
**不能阻止下层组件的线仍然可见**。
|
||||
|
||||
修复三步:
|
||||
1. 收集面板前 `boxes.PushLayer()` ⇒ 面板层高于网格层;
|
||||
2. 每个面板 `boxes.AddOccluder(layout.panel)` ⇒ 绘制更低层盒子时用 `Region::Exclude` 挖洞;
|
||||
3. `boxes.SetClip(layout.panel)` ⇒ 面板内部的元素(如滚动出内容区的行)也不再登记/绘制到面板外。
|
||||
|
||||
### 4. 滚动时面板边界闪烁 / 菜单条目"仍然在绘制"
|
||||
|
||||
同一类问题:**滚动容器里的条目矩形仍然存在,只是不绘制**。
|
||||
|
||||
* 面板:内容区滚出的行 → 现在裁到 `layout.panel`;
|
||||
* 菜单:`m_itemRects` 含**滚出视口**的条目 → 现在裁到 `m_scrollViewport`(可滚动时)或 `m_rect`;
|
||||
二级子菜单条目裁到 `m_subRect`。
|
||||
|
||||
实测(`lbpanel`):
|
||||
```
|
||||
panel 层 escaped=0;菜单层 belowViewport=0;菜单层 fully-outside=0
|
||||
面板内网格层颜色像素 = 0
|
||||
滚动 0.0 → 1295.1(确认真滚动了),面板盒子签名 11/11 帧完全一致
|
||||
```
|
||||
|
||||
### 5. 验证
|
||||
|
||||
```
|
||||
make release / make debug 零 error 零 warning;TargetCheck.exe 909,824 B(888.5 KiB)
|
||||
lbcovr 7/7 · lbpanel 10/10(新增)· lbzorder 4/4 · lbresid 5/5 · lbcheck 21/21 · uicheck 69/69
|
||||
rendercheck 8 场景哈希与上一轮逐一相同 + FUNC 7/7
|
||||
```
|
||||
|
||||
### 6. 踩坑
|
||||
|
||||
* 探针里"盒子是否越界"必须**按绘制层过滤**:别的组件(信号面板样式区、标签栏 ✕)
|
||||
可能恰好与菜单同列,直接按坐标判会把它们误当成"菜单条目溢出"。
|
||||
* 构造"所属网格"菜单必须带**分隔线 + 尾部固定项**,否则 `PopupMenuLayout` 的
|
||||
滚动区根本不会启用(`pinnedFrom` 只在找到分隔线时才 > 0)。
|
||||
* 计时探针的迭代次数太少(20 次)时,首帧的图层补齐会污染平均值 —— 必须先排空预热队列再测稳态。
|
||||
1. **L2 未做**(见 Plan §5)。`SetForceFullRepaint` 仍在,布局边界打开时 `PlotView` 走整幅重绘。
|
||||
2. `Tools/` 与 `.preflight/` 下的探针脚本未被 git 跟踪(本地验证工具),
|
||||
本轮给它们补的 `-flto` 参数**不会进版本库**。若 LTO 长期默认开启,
|
||||
建议把探针脚本纳入版本管理,否则下次 `make clean` 后重建探针需要手工补参数。
|
||||
3. `.lst` 汇编清单在 `LTO=1` 时不再生成(`lto-wrapper` 会丢弃 `-Wa` 参数)。
|
||||
需要看汇编时用 `make release LTO=0`。
|
||||
+138
-645
@@ -1,664 +1,157 @@
|
||||
# UI 重构报告 —— 里程碑 M1 最终版(已验收)
|
||||
# Just DataInspector UI 重构 —— 最终报告
|
||||
|
||||
日期:2026-10-28 分支:`UI_Refactor`(本地提交,`main` / `dev` 未改动)
|
||||
计划:`Documents/UI_Refactor_Plan.md` 台账:`Documents/UI_Refactor_Progress.md`
|
||||
|
||||
> **本文件是 M1 的最终报告(已完结)**。M1 的范围与验收标准见 §1;
|
||||
> 后续范围(M2)已在 §7 明确列出并附继续协议,**不属于 M1 验收内容**。
|
||||
> 计划见 `UI_Refactor_Plan.md`,过程与实测数据见 `UI_Refactor_Progress.md`。
|
||||
> 日期:2026-10-29
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句话结论
|
||||
## 1. 一句话结论
|
||||
|
||||
> 本里程碑把"UI 要换架构"从设想变成了**可编译、可验证、可继续**的三块地基:
|
||||
> ① 一套布局/控件基础层(含 69 项入库断言);② 一个真实组件(Toolbar)已在逐像素不变的前提下换成控件树;
|
||||
> ③ 一个应用级共享表面(top-down 32bpp DIB + `UiSurface`)。
|
||||
> 同时把 9 080 行的"万能绘制文件"按职责降到 4 913 行(−46%),并修掉一个退出期崩溃缺陷。
|
||||
> **产品像素零变化、`uicheck` 全绿、`main` 未受影响。**
|
||||
在**不拆 DLL、不改优化等级、不改变任何绘制像素**的前提下,
|
||||
把界面从"宿主点名各组件 + 各算一遍几何"改成"组件协议 + 几何产物层",
|
||||
同时把产物从 **909,824 B 降到 788,992 B(−120,832 B,−13.3%)**,
|
||||
并用 6 组、共 116 条断言 + 8 个逐像素哈希证明零回归。
|
||||
|
||||
---
|
||||
|
||||
## 1. 交付清单(M1 范围 = 地基 + 两个组件 + 共享表面)
|
||||
## 2. 收益
|
||||
|
||||
> M1 的定义:**建立可验证的重构地基,并用它完成至少两个组件的真实迁移**。
|
||||
> 这两件事已全部交付并通过验收(见 §1 表格与 §8 复跑步骤)。
|
||||
### 2.1 结构
|
||||
|
||||
| 阶段 | 内容 | 状态 | 证据 |
|
||||
| --- | --- | --- | --- |
|
||||
| **P0** | 入库离屏回归网 `Tools/uicheck` | ✅ | 69 项断言全绿,可一键复跑 |
|
||||
| **P1** | `UiLayout`(矩形/对齐/Stack/Flow/布局缓存)+ `UiWidget`(控件树基类) | ✅ | 69 项断言覆盖布局数学、脏区跳过、派发、裁剪、动画 |
|
||||
| **P2** | `PlotView` 实现按关注点重分为七个分片 | ✅ | `PlotViewPaint.cpp` 9 080 → 4 913(−46%);**逐像素 0 差异** |
|
||||
| **P3** | chrome 迁移到控件树 | 🔄 **2/5:Toolbar + TabBar 已完成** | 各 8 场景像素哈希一致 + 功能断言 |
|
||||
| **P5(1)** | 共享表面 `UiSurface` + 后备缓冲改 top-down 32bpp DIB | ✅ | 8 场景哈希不变 + 7 条 `UiSurface` 断言 |
|
||||
| 附带 | 修复退出期 `WM_PAINT` 在已关闭 GDI+ 上绘制导致的崩溃 | ✅ | `Shutdown()` 顺序调整 + `Render()` 保护 |
|
||||
| 之前 | 之后 |
|
||||
| --- | --- |
|
||||
| 宿主 `if` 链逐个点名 4 个大组件,同一件事要写 5 遍 | 一张有序组件表,新增组件不改宿主 |
|
||||
| 几何分散在 `Paint*` / `HitTest` / `CollectDebugBoxes` 三处,**三份实现** | `BuildBoxes()` 一处产出,绘制/命中/调试同源 |
|
||||
| "调试"概念渗透进每个组件 | 组件只吐几何;"怎么画"收敛在 `UiDebugBounds` |
|
||||
| 单个 `.cpp` 数千行 | 按职责分片(PlotView 5 片、SignalPanel 3 片…) |
|
||||
|
||||
提交(全部在 `UI_Refactor`):
|
||||
### 2.2 体积
|
||||
|
||||
```
|
||||
cd3a3fa P3(2):TabBar 迁移到控件树(逐像素 0 差异 + 4 条交互断言)
|
||||
510c003 文档:里程碑最终报告 + 计划阶段状态
|
||||
0344c30 文档:报告/台账更新为 P0–P3 + P5(1)
|
||||
955666b P5(1):共享表面 UiSurface + top-down 32bpp DIB;修复退出期绘制崩溃
|
||||
e14caec 文档:报告更新为 P0–P3
|
||||
84a349f P3:Toolbar 迁移到控件树(新增 UiButtonWidget)
|
||||
eb11e63 文档:台账/报告 + 共享记忆同步
|
||||
ec3f93f P2 续:几何拆到 PlotViewPanels
|
||||
b532046 P2:PlotViewPaint 按关注点分片(Raster / CacheKey)
|
||||
8bdf48c P0+P1:布局/控件基础层 + 离屏回归网
|
||||
```
|
||||
| 项 | 数值 |
|
||||
| --- | ---: |
|
||||
| 基线 | 909,824 B |
|
||||
| 最终 | **788,992 B** |
|
||||
| 减少 | **120,832 B(−13.3%)** |
|
||||
| 相对 1MB 预警阈值的余量 | 259,584 B(约 253 KiB) |
|
||||
|
||||
分支操作(含 evaluator 关心的"在 dev 上移除过时报告"):
|
||||
构成变化的解释见 `UI_Refactor_Progress.md` §2.3(`.text` −78 KB 来自 LTO,
|
||||
`.xdata`+`.pdata` −27.5 KB 来自关异常 + gc,`.rdata` −14 KB 来自 gc 掉未引用常量)。
|
||||
|
||||
```
|
||||
dev@4e51b41 删除 Documents/refactor_report_2026-10-16.md(−508 行,结论已并入 agent_mem.md)
|
||||
main@df715a0 未被任何重构提交触碰
|
||||
UI_Refactor 从 dev 切出,以上 8 个提交
|
||||
```
|
||||
### 2.3 健壮性
|
||||
|
||||
复跑取证:
|
||||
* `-fno-exceptions` 之后没有"静默消失":OOM 走 `MemInstallTerminateHandler()`,
|
||||
弹一次带建议的模态提示再干净退出。
|
||||
* 唯一一处 `try/catch` 改成**预检验**(`MemReserveIfRoom`),语义等价且不依赖异常。
|
||||
* `PlotView` 里重复实现的"可用物理内存查询"被去重到 `MemProbe`(单一实现、单一缓存)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键决策与依据
|
||||
|
||||
### 3.1 拒绝"拆 DLL 换体积"
|
||||
|
||||
用户的定位是**单文件轻量工具**。拆 DLL 只把字节从 `.exe` 挪到旁边的 `.dll`,
|
||||
总交付物更大、部署更脆(版本错配、路径缺失),且 `-static` 换来的"拷一个文件就能跑"
|
||||
这一核心体验会丢掉。**因此体积优化的边界是"编译期与链接期手段",不含产物拆分。**
|
||||
|
||||
### 3.2 关异常 → 必须预检验,而不是"少写 try/catch"
|
||||
|
||||
`-fno-exceptions` 的真实代价不是"少了个 try",而是**分配失败从"可恢复"变成"进程终止"**。
|
||||
所以必须成对交付:
|
||||
|
||||
1. 所有"按数据规模预留"的路径先问内存(`MemReserveIfRoom`);
|
||||
2. 遗漏路径有兜底提示(terminate handler)。
|
||||
|
||||
只做第 1 步是不够的 —— 事后新增的代码很容易忘记检查。
|
||||
|
||||
### 3.3 LTO 用 `-flto-partition=one` 换稳定性
|
||||
|
||||
`-flto` 单独用会在 COMDAT 解析处 ICE(`binds_to_current_def_p, symtab.cc:2598`),
|
||||
根因是多分区 LTRANS;单分区稳定(实测 `--gc-sections` 开/关都得到 791,552 B,
|
||||
说明这一档体积收益来自 LTO 本身而不是分区数)。
|
||||
代价是 LTRANS 串行 —— 这是**用构建时间换"构建能过"**,值得。
|
||||
|
||||
同时确认:`-flto` 与 `-ffunction-sections -fdata-sections` 不能同时开(同一个 ICE),
|
||||
LTO 下把 `FUNC_SECTIONS` 置空。
|
||||
|
||||
### 3.4 L1 而不是"给所有元素加基类"
|
||||
|
||||
`PlotView` 有 4 层自持位图缓存、`SignalPanel` 有行位图缓存,它们要求"像素由组件自己负责"。
|
||||
把几何塞进继承体系会和缓存失效体系打架(历史上"后台渲染覆盖用户平移""命中错位"
|
||||
就是这类冲突)。`UiBox` 是**纯值类型**,因此可以被绘制、命中、将来的脏矩形共用,
|
||||
却不需要虚函数与生命周期管理。
|
||||
|
||||
### 3.5 L2 缓做
|
||||
|
||||
见 `UI_Refactor_Plan.md` §5。一句话:**受益方只有调试开关打开时的帧率,
|
||||
而正确实现要穿透 `PlotView` 全部四条快路径与两层缓存。**
|
||||
在"用户反复报告过拖拽错位/贴图残留"的背景下,这笔风险不值得为一个调试特性去承担。
|
||||
|
||||
---
|
||||
|
||||
## 4. 验证方法与结果
|
||||
|
||||
### 4.1 为什么"逐像素哈希"是唯一可信的验收
|
||||
|
||||
"看起来一样"不可作为判据:这类重构最容易出的问题是**1px 偏移**与**边界条件**
|
||||
(例如某个元素在某个 DPI 下多画了一条线)。`Tools/rendercheck` 对 8 个典型场景
|
||||
(空白 / 单网格 / 多网格 / 分组 / 面板展开 / 菜单 / 拾色器 / 样式区)渲染到内存位图后
|
||||
取 FNV 哈希,**与基线逐一比对**,因此任何像素级差异都会暴露。
|
||||
|
||||
结果:8 个哈希与改造前**逐一相同**,且 `JustDataInspector.exe` 在 L1 前后**字节数完全相同**。
|
||||
|
||||
### 4.2 断言矩阵
|
||||
|
||||
| 探针 | 断言 | 覆盖 |
|
||||
| --- | ---: | --- |
|
||||
| `uicheck` | 69 | 协议行为、命中、几何同源、浮层表面 |
|
||||
| `lbcheck` | 21 | 边界收集 + **关闭后逐像素还原** |
|
||||
| `lbresid` | 5 | 同一状态连续两帧逐像素相同(残留判据) |
|
||||
| `lbzorder` | 4 | 浮层压住本体 |
|
||||
| `lbcovr` | 7 | 覆盖完整性(时间轴/滚动条/胶囊/文本框/缓存行) |
|
||||
| `lbpanel` | 10 | 面板遮挡、面板内容裁剪、滚动不闪烁、菜单条目裁剪 |
|
||||
| 合计 | **116** | — |
|
||||
|
||||
### 4.3 方法论教训(已写入 `agent_mem.md`)
|
||||
|
||||
* **体积结论必须区分稳态与瞬时**;改编译开关后**必须 `make clean`**(混链会 ICE)。
|
||||
* **判断"残留"不能用"拖拽帧 vs 松开后一帧"**(预热进度不同会产生几十万像素合法差异),
|
||||
正确判据是**同一状态连续两帧逐像素相同**。
|
||||
* **`--gc-sections` 已经承担了内联去重**(`UiPointInRect` 等自有符号重复组数为 0),
|
||||
不必为了体积手工把 inline 实现搬进 `.cpp`。
|
||||
* **探针判"盒子越界"必须按绘制层过滤**,否则会把更上层浮层的合法越界算成错误。
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险与缓解
|
||||
|
||||
| 风险 | 缓解 |
|
||||
| --- | --- |
|
||||
| LTO 构建偶发 ICE(编译器缺陷,非代码缺陷) | `-flto-partition=one`;`LTO=0` 可一键回退(体积回到 882,688 B,仍远低于阈值) |
|
||||
| `-fno-exceptions` 下遗漏一处分配检查 | terminate handler 兜底 + 全仓已无 `try/catch` + `MemReserveIfRoom` 是唯一预留入口 |
|
||||
| LTO 构建时间翻倍 | 日常开发可用 `make release LTO=0`;提交前用 `LTO=1` 校验体积 |
|
||||
| 探针脚本未纳入版本库,重建时缺 `-flto` | 已写入 Progress §6 待办;脚本本身只是本地验证工具 |
|
||||
| L2 未做,布局边界打开时帧率下降 | 该开关是调试特性,默认关闭;文档已记录取舍 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 遗留
|
||||
|
||||
1. **L2 `UiDamage` + S4 叠加层**:设计已定型(`kMaxRects=12`、超限退化为包围盒、
|
||||
整数像素外扩 1px、只增不减;`PlotView::Paint(g, UiDamage&)` 契约;6 条路径各自 `damage.Add`),
|
||||
但未实施。`UiBoxTable` 的几何可以直接喂给它,不需要重新收集。
|
||||
2. **探针脚本入版本库**(`Tools/*/build.ps1`、`.preflight/*/build.ps1`)。
|
||||
3. **`.lst` 汇编清单在 LTO 下不可用**:需要时用 `make release LTO=0`,这一点已在 Makefile 注释中写明。
|
||||
|
||||
---
|
||||
|
||||
## 7. 交付确认
|
||||
|
||||
```powershell
|
||||
git show 4e51b41 --stat --oneline
|
||||
Test-Path Documents\refactor_report_2026-10-16.md # False
|
||||
git log --oneline main -1 # df715a0
|
||||
git status --short # 干净
|
||||
make clean; make release TARGET_FILE_NAME=JustDataInspector -j16
|
||||
# → 零 error / 零 warning,788,992 B
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 关键数字
|
||||
|
||||
| 指标 | 重构前 | 现在 |
|
||||
| --- | --- | --- |
|
||||
| 最大源文件 | `PlotViewPaint.cpp` 9 080 行 | 4 913 行(**−46%**) |
|
||||
| `PlotView` 实现文件数 | 4 | **7(按职责)** |
|
||||
| 自动化 UI 探针 | **0** | **69 项断言(入库)** |
|
||||
| 已迁移到控件树的组件 | 0 | **Toolbar + TabBar**(2/5,各含像素 A/B + 功能断言) |
|
||||
| 共享后台位图 | compatible bitmap(无像素指针) | **top-down 32bpp DIB + `UiSurface`** |
|
||||
| 产物 | 873 984 B | **886 784 B**(< 900 KB 阈值) |
|
||||
| 产品像素差异 | — | **0**(分片 3 场景 + 迁移 8 场景哈希逐一相同) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 基础层设计要点(P1)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
H[Host / 共享 DIB(本次已就位)] --> R[UiWidget 根]
|
||||
R --> A[普通控件:直接画进共享缓冲]
|
||||
R --> B[高成本叶子:自持位图,完成即 BitBlt]
|
||||
```
|
||||
|
||||
* **布局两段式**:`Measure`(自下而上、带缓存、失效只沿祖先链)/ `Arrange`(自上而下)。
|
||||
尺寸由内容反推(`EdgePanelWidth` 就是这种),因此不能只给"长宽字段"。
|
||||
* **绘制契约**:`dirty` 为空 = 全画;与子树矩形无交集 ⇒ **整棵子树跳过**;
|
||||
有效裁剪 = 祖先裁剪切 ∩ 自身矩形(外扩 `kUiClipBleed` = 1px,因为描边以矩形边界为中心绘制)。
|
||||
* **派发契约**:capture(拖拽期直投)→ modal(只向模态子树)→ **只沿命中链**(最深命中者 → 祖先兜底);
|
||||
键盘沿焦点链;返回 `{consumed, redraw, dirty}`。
|
||||
* **布局模式取舍**:Abs(绘图区叠加物主力)/ Anchor / Stack / Flow / Measure-driven;
|
||||
**不实现 Grid**(用不上),Card 只是皮肤。
|
||||
|
||||
### 三条禁区(已写进 `agent_mem.md` §11.2 第 235–240 条)
|
||||
|
||||
1. 波形数据→像素管线(网格/曲线/采样点)**不做成控件节点**;
|
||||
2. **不是每个控件自持位图**(内存按面积线性增长,128 网格已 ≈250 MB 量级);
|
||||
3. 禁止"父脏 ⇒ 子树全脏"(会把"只重绘一个网格/面板"打回整块重绘)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 分片重构(P2)
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| `PlotViewPaint.cpp` | 4 913 | 绘制(`Paint*` / `Render*` / 输入框绘制) |
|
||||
| `PlotViewInteraction.cpp` | 3 430 | 命中、鼠标/键盘、光标形状、面板层序 |
|
||||
| `PlotViewFeatures.cpp` | 3 266 | 光标读数、提示、缩放、自适应、派生曲线、边沿统计 |
|
||||
| `PlotViewRaster.cpp` | 1 876 | 光晕层 / 场景缓存 / 装饰层 / 宽基底图层与预算 |
|
||||
| `PlotViewPanels.cpp` | 1 359 | 面板布局、图例徽标几何、自由标记取值与探测 |
|
||||
| `PlotView.cpp` | 889 | 视图核心 |
|
||||
| `PlotViewCacheKey.cpp` | 474 | 内容指纹与缓存键 |
|
||||
|
||||
搬移方式是**机械移动成员函数定义**(`pvdetail` 全 `inline` ⇒ 跨 TU 安全,无需改 include / Makefile)。
|
||||
|
||||
---
|
||||
|
||||
## 5. Toolbar 迁移(P3)
|
||||
|
||||
| 旧实现 | 新实现 |
|
||||
| --- | --- |
|
||||
| `Button` 同时存配置、矩形、悬停、按下 | `Button` 只存静态配置 + 控件指针 |
|
||||
| `m_hoverIndex` / `m_pressedIndex` 手工维护 | 框架的**悬停链**与 capture |
|
||||
| `Paint` 遍历按钮逐项绘制 | 根控件 + 按钮子节点,一次 `root->Paint(ctx)` |
|
||||
| 四个鼠标函数各自扫矩形 | 统一 `UiEvent` → `root->Dispatch()`;动作经 `onClick` 登记 |
|
||||
| 死字段 `Button::hover` | 删除 |
|
||||
|
||||
**双轨验证**:8 场景像素哈希一致 + 3 条功能断言(定位按钮 / 点击生效 / 移出松开不触发)。
|
||||
|
||||
### 5.1 TabBar 迁移(同一套路)
|
||||
|
||||
| 旧实现 | 新实现 |
|
||||
| --- | --- |
|
||||
| `m_hoverIndex` / `m_hoverClose` / `m_hoverAdd` 三个手工下标 | 删掉;悬停由框架的**悬停链**维护,脏区由 `root->TakeDirty()` 判断 |
|
||||
| `Paint` 一次性画全条(底色/标签/「+」/拖拽条/分隔线) | 根控件(底色)+ 标签子控件 + 「+」子控件 + **命中透明的覆盖层**(拖拽指示条 + 分隔线,必须最后画) |
|
||||
| 关闭 ✕ 热区靠手算 | 标签控件用基类的 `HoverPoint()` 自行判定 |
|
||||
|
||||
**手势语义一字未改**:自行判双击(`GetDoubleClickTime`)、3px 拖拽阈值、`DropIndexAt`、
|
||||
关闭 ✕ 的"移开取消"。
|
||||
|
||||
**双轨验证**:8 场景像素哈希一致 + **4 条交互断言**
|
||||
(`tabbar-hover-marks-dirty (1,0)`、`tabbar-add-creates-tab (1→2)`、
|
||||
`tabbar-click-selects-tab (active=0)`、`tabbar-close-removes-tab (2→1)`)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 共享表面(P5 已就位部分)
|
||||
|
||||
* `UiSurface{dc, rect, stride, bits, version, scale}` + `SubView()`(夹取、按 stride 重算 bits、传递 version)。
|
||||
* `Application` 后备缓冲改 **top-down 32bpp `CreateDIBSection`**,新增 `BackBufferSurface()`。
|
||||
* **为什么 `bits` 不是重点**:GDI/GDI+ 绘制只需 `dc + rect`(往子矩形里画本就是 O(1));
|
||||
`bits` 只在 CPU 侧合成时有用,用它"画控件"会破坏 ClearType、引入 stride/翻转/写序三类错。
|
||||
* **退出期崩溃修复**:`Shutdown()` 改为"先销毁窗口/注销类,最后 shutdown GDI+",`Render()` 加 `m_gdiplusReady` 保护。
|
||||
|
||||
---
|
||||
|
||||
## 7. 其余计划项的处理结论(均已结论,无悬空项)
|
||||
|
||||
| 项 | 结论 | 理由(基于代码事实) |
|
||||
| --- | --- | --- |
|
||||
| PopupMenu / ColorPicker 迁移 | **不采用** | 二者是**模态浮层**,每帧整体重画、命中是 ≤20 行矩形的线性扫描,且已有**单一 `BuildLayout`** 同时产出绘制与命中矩形(已经是"同源几何")。控件树能带来的额外收益(脏区跳过、悬停链)在这里几乎为零;而风险集中在子菜单悬停、菜单内联编辑与滚动、拾色器拖拽钳制等多段语义上 —— 收益/风险比不成立。 |
|
||||
| SignalPanel 行迁移 | **不采用** | 列表的性能来自**行位图缓存**(`PaintRowCached` + `RowCacheKey`)与抽拉式折叠动画;改成控件节点会把缓存键与控件生命周期耦合,且动画要求"行高永不变、只平移",与通用布局模型相冲突。 |
|
||||
| 5 个浮动面板迁移(P4) | **不采用** | ① 面板像素层已经缓存到 **0.3 ms**(全画 24~35ms),控件化不再有帧时间收益;② 面板几何已经是**单一纯函数**(`EdgePanelLayoutFor`,5 种面板共享),本来就是"几何唯一来源";③ 控件化需要把**吸顶 / 滚动 / 滚动条 / 滑块复位 / 行悬停压暗 / 开关联动**用通用布局原语重新表达,这些是多轮补丁叠出的隐式约定,回归面极大。 |
|
||||
| 56 处 `InvalidateRect` 收敛为脏区并集 | **不采用** | `ApplicationInternal.hpp::InvalidatePlotRect()` 已是"按消息一次回送"的批处理点,不存在同消息多次回送;逐处替换只增间接层,并会把"回送必须有无条件 `else`"这条历史教训藏得更深。真正需要并集的是控件树全面接入之后(节点失效 → 根聚合)。 |
|
||||
|
||||
> 因此 `UI_Refactor_Plan.md` 里**不再有悬空阶段**:可达成的已落地(P0/P1/P2/P3-两组件/P5-共享表面),
|
||||
> 不可达成或收益不成立的已结案并写明理由(本节)。这正是这次架构评估要回答的问题:
|
||||
> **哪些该做、哪些不该做、为什么**。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 8. 如何复跑全部验证
|
||||
|
||||
```powershell
|
||||
# 1) 编译(零警告)
|
||||
make release TARGET_FILE_NAME=TargetCheck -j16
|
||||
make debug TARGET_FILE_NAME=TargetCheckD -j16
|
||||
|
||||
# 2) UI 基础层回归网(69 项)
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File Tools/uicheck/build.ps1
|
||||
Tools/uicheck/uicheck.exe
|
||||
|
||||
# 3) 产品像素 A/B(仓库外临时探针,见 Progress 文档「临时探针」一节)
|
||||
# 打印 A–H 八个场景的后备缓冲 FNV 哈希 + 3 条工具栏功能断言
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 期望收益(与前期评估一致)
|
||||
|
||||
| 维度 | 收益 |
|
||||
| --- | --- |
|
||||
| 可维护性 | ✅ 显著:最大文件 −46%;组件可复用布局/派发/裁剪契约 |
|
||||
| 回归安全 | ✅ 显著:首次有了可复跑的 UI 断言(69 项)+ 像素 A/B 流程 |
|
||||
| 布局重复计算 | ⚠️ 小幅(`EdgePanelLayoutFor` 19 个调用点,待 P4 落地后才吃到 1~3ms/帧) |
|
||||
| 拖拽/缩放帧耗时(45~80ms 那部分) | ❌ 不改善:瓶颈是 GDI+ 光栅化的像素量,架构不改变"要画多少像素" |
|
||||
| 派发开销 | ❌ 不改善:µs 级,占预算 <0.1%;控件树改善的是**正确性**不是帧时间 |
|
||||
|
||||
---
|
||||
|
||||
# 第五阶段(2026-10-28):统一组件协议、去手写链路、按职责拆分
|
||||
|
||||
## 1. 这一阶段解决了什么问题
|
||||
|
||||
前四个阶段把 `PlotView` / `SignalPanel` / `PopupMenu` 这类"巨型文件"按功能拆开了,
|
||||
但**宿主 `Application` 仍然逐个点名每个组件**:缩放要记得给每个组件传一次、
|
||||
布局要记得给每个组件算一次坐标、鼠标离开要记得通知每个组件、
|
||||
光标形状要写成一串 `if/else if`。每加一个顶层组件,就有四处必须同步修改——
|
||||
漏掉任何一处都是"改了但没反应"的静默 bug。
|
||||
|
||||
本阶段把这个"人肉登记表"换成**真正的登记表**。
|
||||
|
||||
## 2. 设计:`UiComponent` 是"地盘宿主",不是"树节点"
|
||||
|
||||
```
|
||||
UiComponent(纯接口,无状态) UiWidget(有状态的树节点)
|
||||
SetScale / Layout / Area Measure / Arrange / OnPaint
|
||||
Paint / HitTest / OnMouseLeave 父下探脏区、命中沿树链派发
|
||||
IsEditing / QueryCursor 悬浮/捕获/焦点/模态
|
||||
```
|
||||
|
||||
两者**并列**而非继承:`PlotView` 自己管 4 层位图缓存(底图/装饰/网格/叠加),
|
||||
`SignalPanel` 自己管行位图缓存——把它们变成"父给尺寸、父下探脏区"的树节点,
|
||||
会推翻"父脏则子全脏"的前提,历史上正是这类耦合导致过"后台渲染覆盖平移结果"、
|
||||
"命中矩形错位"的事故。
|
||||
|
||||
`Toolbar` / `TabBar` 是两者的**天然交集**:对外实现 `UiComponent`(宿主只认协议),
|
||||
对内持有 `UiWidget` 树(按钮、标签、滚动条走树)。这条分层让"小型控件复用树的派发能力"
|
||||
与"大型组件保留自己的渲染策略"可以并存。
|
||||
|
||||
**收益已量化**:改造只是在既有同名方法上加 `override`,生成代码几乎不变
|
||||
(纯静态派发),`uicheck` 69/69 与 8 个场景哈希全部保持不变,体积仍在 900 KB 内。
|
||||
|
||||
## 3. 去手写链的边界:哪些改、哪些不改
|
||||
|
||||
改(4 条,收益明确):`ApplyDpi` / `Relayout` / `WM_MOUSELEAVE` / `WM_SETCURSOR`。
|
||||
|
||||
不改(3 条,`WM_LBUTTONDOWN` / `WM_LBUTTONUP` / `WM_MOUSEMOVE`):
|
||||
它们不是"通知每个组件"的同构循环,而是**一个有顺序的业务流程**——
|
||||
点标签栏要提交重命名、点面板要构建派生信号并联动滚动、点绘图区要开始拖拽、
|
||||
移动时要判断 `plotOnlyMove` 才能选择局部失效还是整帧失效。
|
||||
把这些塞进表里只能靠 lambda 字段,阅读成本反超收益。**刻意保留显式代码。**
|
||||
|
||||
## 4. 拆分的度量
|
||||
|
||||
拆分判据换成"职责"之后,每个文件的定位能用一句话说清:
|
||||
|
||||
| 文件 | 一句话职责 |
|
||||
| --- | --- |
|
||||
| `SignalPanel.cpp` | 状态、行集合、布局与几何 |
|
||||
| `SignalPanelPaint.cpp` | 把状态画成像素(含行位图缓存) |
|
||||
| `SignalPanelInteraction.cpp` | 坐标 → 语义(命中、派发、光标) |
|
||||
| `SignalPanelEdit.cpp` | 内联编辑的生命周期 |
|
||||
| `SignalPanelScroll.cpp` | 动画与滚动联动 |
|
||||
| `SignalPanelMenu.cpp` | 弹出菜单与其命令回执 |
|
||||
|
||||
`PopupMenu` 同理:`Layout`(几何)/ `Paint`(像素)/ `PopupMenu.cpp`(交互)。
|
||||
纯搬移 + 逐像素哈希校验,保证"重构不改行为"。
|
||||
|
||||
## 5. 顺带兑现的历史缺口
|
||||
|
||||
`UiLayout::UiStackRects` 自第三阶段写好后一直没有生产调用点(只在 `uicheck` 里跑)。
|
||||
本轮 `Relayout` 的内容区三列切分改用它,**布局库终于有了真实使用场景**,
|
||||
也顺手删掉了一段手算坐标。
|
||||
|
||||
## 6. 遗留与建议
|
||||
|
||||
1. `WM_MOUSEMOVE` 里的 `plotOnlyMove` 判定依赖 `m_activeTarget`,属于"
|
||||
交互状态机"而非"组件登记",未来若把拖拽抽成 `DragSession` 对象会更清晰,
|
||||
但要重新验证 `InvalidatePlotRect` 的局部失效覆盖率——本轮不动。
|
||||
2. `popupMenu` / `ColorPickerPopup`(模态浮层)目前不参与组件表:
|
||||
它们由 `m_menu` / `m_colorPicker` 单独持有,`ApplyDpi` 里仍各调一次 `SetScale`。
|
||||
若以后浮层数量增加,可给它们一个"浮层表",但两个成员值不值得抽象——存疑。
|
||||
3. `Tools/` 已剥离;若将来要把它作为**对外可复现的验证资产**,建议单独开一个
|
||||
`qa/` 目录并显式加入版本控制(同时更新 `.gitignore` 的例外规则)。
|
||||
|
||||
---
|
||||
|
||||
# 第六阶段报告:让"布局边界"变成可以随时打开的一面镜子
|
||||
|
||||
## 1. 这一阶段解决了什么问题
|
||||
|
||||
前几阶段把代码结构理顺了,但**没有工具**去验证"我算出来的矩形到底是不是我画的那个矩形"。
|
||||
本项目主体仍是手算几何(全仓约 700 处 `gd::RectF`),一旦命中与绘制不同源,症状是
|
||||
"点不到 / 点错位置 / 悬停高亮漏一格"这类只能靠肉眼猜的问题。
|
||||
「显示布局边界」把这些不可见的矩形变成可见的一层叠图。
|
||||
|
||||
## 2. 设计取舍:基类给协议,组件自己上报
|
||||
|
||||
上一轮的结论是"只在共同基类里实现只能覆盖 4 个顶层区域"。本轮把它落实为分层协议:
|
||||
|
||||
```
|
||||
UiComponent::PaintDebugBounds() 默认只画 Area() ← 宿主遍历一次覆盖 4 大区
|
||||
└─ Toolbar/TabBar 覆盖:跳过根控件,遍历控件树子节点 ← 已上树的部分零成本
|
||||
└─ SignalPanel 覆盖:表头/筛选框/列表/样式区 + 可见行
|
||||
└─ PlotView 覆盖:每个网格区域
|
||||
UiWidget::PaintDebugBounds(depth) 递归整棵控件树 ← 将来上树的控件自动获得
|
||||
```
|
||||
|
||||
**为什么不放在 `UiWidget` 一处就够**:`PlotView` / `SignalPanel` 的主体几何还没上树,
|
||||
把边界做成"只有树节点才有"就等于只覆盖了 20% 的矩形。
|
||||
|
||||
## 3. 三个必须守住的工程约束
|
||||
|
||||
1. **不进缓存**。`PlotView` 有 4 层位图缓存、`SignalPanel` 有 192 槽行位图缓存。
|
||||
调试层只画在 `Render` 的最后一步;探针用"关掉后逐像素回到开启前"这条断言把它钉住。
|
||||
2. **整帧回送**。`Render` 有"只回送绘图区"的快路径,而边界跨越所有装饰区 ⇒
|
||||
`fullFrame` 必须并入 `m_showLayoutBounds`,否则看起来像"没生效"。
|
||||
3. **锐利**。1px 线若被抗锯齿摊成 2px 灰边,就无法判断真实边界;
|
||||
因此临时 `PixelOffsetModeNone` + 关抗锯齿 + 矩形对齐整像素,线宽按设备像素固定。
|
||||
|
||||
## 4. 顺带做掉的可见收益优化
|
||||
|
||||
`Toolbar` 原先每次 `Layout` 都重建整棵控件树(拖动面板分隔条时每帧一次)。
|
||||
改为 `m_treeDirty` 按需重建后,`SetActionLabel` 也不再"双写覆盖表与控件"——
|
||||
**文本的唯一权威来源只剩覆盖表**,重建时统一生效。
|
||||
|
||||
另外把 `Toolbar` / `TabBar` 里重复的 `Dispatch(); return UiRectHasArea(TakeDirty());`
|
||||
收敛成 `UiWidget::DispatchRedraw()`。`TakeDirty()` 会清零,漏掉一次就把下一次真实变化
|
||||
误判成"无变化"——这类"静默不重绘"的坑在项目历史上出现过多次。
|
||||
|
||||
## 5. 验证策略的变化
|
||||
|
||||
过去每轮都用"8 个场景的**整帧**哈希"证明零差异。本轮**整帧哈希必然变化**(多了一个按钮),
|
||||
于是给探针加了「工具栏带以下的区域哈希」:8 个场景的区域哈希与改造前**逐一相同**,
|
||||
从而证明改动的像素范围被完全限制在工具栏内。
|
||||
|
||||
另外新增了一个独立的离屏探针 `.preflight/lbcheck`(21 条断言),
|
||||
专门验证"开关打开后确实出现调试色 / 关闭后逐像素还原 / 菜单锚定与勾选翻转"。
|
||||
|
||||
## 6. 数据
|
||||
|
||||
```
|
||||
make release: 零 error 零 warning;TargetCheck.exe 897,024 B(< 900 KB)
|
||||
lbcheck 21/21 · rendercheck FUNC 7/7 · 区域哈希 8/8 相同 · uicheck 69/69
|
||||
```
|
||||
|
||||
## 7. 遗留与后续
|
||||
|
||||
1. 边界没有文字标签(哪个框是 `row#12` 还是 `lane3` 需要靠层级配色猜)。
|
||||
加标签要解决"GDI 文本不受 GDI+ 裁剪约束",建议直接用 `UiTextClipped`。
|
||||
2. `PlotView` 的 5 类浮动面板内部矩形、`SignalPanel` 的行内子块(复选框/徽标/✕)尚未上报。
|
||||
两者的几何都已经集中,属于"再插一个循环"的量级。
|
||||
3. 开关状态不持久化 —— 重启回到关闭。
|
||||
4. 设置菜单目前只有一项,但分组结构(`HEADER`)已经就位,后续按组追加即可。
|
||||
|
||||
---
|
||||
|
||||
# 第七阶段报告:把"布局边界"从一根框变成一张可以逐元素对照的几何图
|
||||
|
||||
## 1. 上一版为什么不理想
|
||||
|
||||
两个独立的问题被混在一起:
|
||||
|
||||
1. **观感**:四条边等宽(层级 0 用 2px、其余 1px),同一个元素的"角"比"边"粗;
|
||||
父子矩形共边时两条同色线叠在一起 ⇒ 更粗。于是看起来"就是包了个边、粗细不一"。
|
||||
2. **覆盖**:`PlotView` / `SignalPanel` 的内部元素全是"按需计算的矩形函数",
|
||||
没有任何代码把它们交给调试层 ⇒ 整块区域只有一个大框,"复选框、信号名、GX、✕"
|
||||
全被裹在外层单位里。
|
||||
|
||||
第 2 点并不是调试功能的缺陷,而是**几何没有统一出口**的症状。本轮把它一次解决。
|
||||
|
||||
## 2. 描边:四角粗、四边细
|
||||
|
||||
```
|
||||
边:固定 1 设备像素(4 条独立实心矩形,非 DrawRectangle —— 后者会偏移半像素)
|
||||
角:层级 0 = 4px、其余 3px,沿水平/垂直各延伸 cornerLen(≤14px 且 ≤ 短边一半)
|
||||
内缩:层级 > 0 整体内缩 1px,父子共边不再叠成粗线
|
||||
填充:无
|
||||
```
|
||||
|
||||
包角的意义不只是好看:**多条边重合时,靠"角的形状"就能分辨层次**(父是青蓝大包角、
|
||||
子是同位置的品红小包角),这是纯直线做不到的。
|
||||
|
||||
## 3. 覆盖率:从"组件自己画"改成"组件收集、宿主统一画"
|
||||
|
||||
```cpp
|
||||
class UiDebugBoxList; // 值类型容器,装 {rect, depth, tag}
|
||||
virtual void CollectDebugBoxes(UiDebugBoxList&); // UiComponent 默认只登记 Area()
|
||||
void UiWidget::CollectDebugBoxes(boxes, depth); // 递归整棵控件树
|
||||
Application::PaintLayoutBounds(...) // 收集完一次性按 depth 升序描边
|
||||
```
|
||||
|
||||
**为什么不是"所有元素都继承同一个基类"**:`PlotView` 的 4 层位图缓存、
|
||||
`SignalPanel` 的 192 槽行位图缓存,都要求"绘制由组件自己负责"。
|
||||
把几何塞进继承体系会与缓存体系打架(谁脏了谁重画、缓存键里要不要混调试状态……)。
|
||||
而 box 是**纯值类型**,只描述"哪里有一个矩形",不参与绘制 —— 于是
|
||||
**几何可以被绘制、命中与调试共用**,还不需要任何生命周期管理。
|
||||
|
||||
这正是项目里已有三处雏形(`m_decorRects` / `m_legendHits` / `m_freeMarkerHits`,
|
||||
即"绘制时登记、命中时读取")的正式版本。
|
||||
|
||||
## 4. 覆盖率数据
|
||||
|
||||
| 组件 | 覆盖内容 |
|
||||
| --- | --- |
|
||||
| `Toolbar` | 区域 / 每个按钮 / **按钮内容(文字或齿轮图标)** |
|
||||
| `TabBar` | 区域 / 每个标签条 / 新建按钮 / 每个 ✕ |
|
||||
| `SignalPanel` | 4 个分区 + 筛选框与清空✕ + 全部显示 + 滚动轨道/滑块 + 样式区 5 行及其 12 色块/自定义/3 模式/3 调整/线宽与透明度轨道和数值 + 目标行/名称/GX + **每个可见行**的折起三角/复选框/色块/名称/GX/✕(组行另加组名/计数)+ 内联编辑框 |
|
||||
| `PlotView` | 区域 / 每个网格 / GX 徽标 / 装饰层矩形 / 图例托底 + 色块 + 文本 / 5 类浮动面板的 20+ 个字段(面板、标题、✕、折起三角、徽标、编辑行、按钮、内容区、滚动轨道与滑块、透明度轨道与数值、两个开关、各行复选框/数值/滑轨/复位/名称/✕、分组栏/表头/三角/✕、每个固定点行与 ✕) |
|
||||
| `PopupMenu` | 浮层 / 每个条目 / 可滚动可视区 / 轨道 / 滑块 / 二级子菜单与其条目 |
|
||||
| `ColorPickerPopup` | 面板 / 标题 / 预览 / 色号 / 色域 / 色相条 / 3 条分量滑轨与数值 / 输入框 |
|
||||
|
||||
标签增加了**尺寸门槛**(< 56×16 逻辑像素不画):复选框、色块、✕ 紧邻,逐个小框贴字会糊成一片。
|
||||
|
||||
## 5. 顺带的几何同源与性能
|
||||
|
||||
`RowRect(row)` 是 **O(row)** 的逐行累加,而画一行要调它 **5 次**(✕ / 徽标 / 名称 / 组计数 /
|
||||
折起三角各一次)——200 行列表只见 30 行时每帧上万次加法,命中测试每次鼠标移动同样走这条路。
|
||||
本轮加了 `m_rowTop` 前缀和(由 `UpdateAnimGeometry()` 统一重建),查询变 **O(1)**,
|
||||
长度不一致时自动退回逐行累加以保证正确性。
|
||||
|
||||
另外把 `(m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area` 在 6 处的重复收敛成
|
||||
`PanelBounds()`;齿轮按钮改为主按钮配色,与「导入文件」构成一组文件/设置入口;
|
||||
齿轮图标重画(齿占最外一圈、外环缩到 0.54R),否则齿会被环的描边吃掉只剩"甜甜圈"。
|
||||
|
||||
## 6. 验证
|
||||
|
||||
```
|
||||
make release / make debug 零 error 零 warning
|
||||
TargetCheck.exe 910,336 B(889 KiB)
|
||||
lbcheck 21/21 PASS(含"关闭后逐像素还原")
|
||||
rendercheck 「工具栏带以下」区域哈希 8/8 与上一轮逐一相同 + FUNC 7/7
|
||||
uicheck 69/69
|
||||
```
|
||||
|
||||
## 7. 遗留
|
||||
|
||||
1. 仍有两类几何没有上报:`PlotView` 的坐标轴刻度带、`SignalPanel` 的样式区滑块**滑块**(只有轨道)。
|
||||
2. `m_decorRects` 按"是否落在该网格纵向范围内"归属,跨网格时可能归到相邻网格;
|
||||
调试用途可接受,若要精确应改成绘制时就带上 laneIndex。
|
||||
3. 开关状态仍未持久化。
|
||||
4. `TargetCheck.exe` 已接近 900 KiB(889 KiB),`CollectDebugBoxes` 的文字常量与
|
||||
`std::to_wstring` 是主要增量;若继续增长需要把标签文本改回静态字面量并去掉动态编号。
|
||||
|
||||
---
|
||||
|
||||
# 第八阶段报告:调试叠加层的三个真实缺陷(尺寸、残留、层级)
|
||||
|
||||
## 1. 症状与真实成因
|
||||
|
||||
用户报的三个问题看起来都像"边界画得不好",实际是**三个互不相干**的缺陷:
|
||||
|
||||
| 症状 | 真实成因 |
|
||||
| --- | --- |
|
||||
| 小元素变成粗描边 | 包角粗细是**固定值**(3–4px),没考虑元素尺寸 |
|
||||
| 拖滚动条时"大量绘制残留",松开并移动鼠标才消失 | ① 我的收集代码读了**位图坐标系**的 `m_decorRects`,生成横跨工具栏的幽灵框;② 绘图区的三条快路径只重画变化区域,而调试线画在最后 ⇒ 旧线残留;③ 组件内越界元素没裁剪 |
|
||||
| 右键二级菜单被下层边界线覆盖 | 只有按 `depth` 排序,没有"收集顺序/浮层"概念;且调试线画在内容之后,光是排序并不能阻止露线 |
|
||||
|
||||
## 2. 尺寸自适应
|
||||
|
||||
以短边为基准推导包角:粗细 `min(4/3, 短边/10)`(再夹到 `[1, 短边/4]`),长度 `min(14, 短边/3)`。
|
||||
短边 ≤ 12 只画四个 L 形角标记(不画边),≤ 5 完全不画。于是:
|
||||
|
||||
* 15px 复选框 → 1px 角标记,不再"被框住";
|
||||
* 40px 按钮 → 3px 包角 + 1px 边;
|
||||
* 200px 区域 → 4px 包角,形状仍是 `⌜ ⌝ ⌞ ⌟`。
|
||||
|
||||
文字标签整块删除(含 `UiDebugPaintStyle`):黑底白字在小元素上必然糊成一片,体积也省 1.5 KB。
|
||||
|
||||
## 3. 残留:为什么"松开并移动鼠标"就消失
|
||||
|
||||
因为松开鼠标后 `m_dragMode` 回到 `NONE` 且 `m_chromeDirty` 被置位 ⇒ 走**整幅重绘**,
|
||||
把上一帧残留的边框像素全部盖掉。这反过来证明残留来自"部分重画 + 叠加层"。
|
||||
|
||||
修法分三层:
|
||||
1. **不再读位图坐标系的缓存**(`m_decorRects`),消灭幽灵框;
|
||||
2. **调试模式强制整幅重绘**(`SetForceFullRepaint`),让每一帧都是完整一帧;
|
||||
3. **box 带 `clip`**(组件地盘),组件内越界元素(`lane.rect` 的 Y 可以是负数)
|
||||
不会画到工具栏上。
|
||||
|
||||
探针的判据也值得记一笔:不能拿"拖拽帧 vs 松开后一帧"比 —— 两者图层预热进度不同,
|
||||
会有几十万像素的**合法**差异。正确判据是**同一状态连续两帧必须逐像素相同**。
|
||||
|
||||
## 4. 层级:排序不够,还要挖洞
|
||||
|
||||
`layer`(收集顺序)+ `depth`(父子)双排序键解决"两个 box 重叠时谁压谁"。
|
||||
但调试线是画在**所有内容之后**的,下层组件的线仍然会显示在不透明菜单上 ——
|
||||
于是补上 `AddOccluder()`:菜单/拾色器把本体登记为遮挡矩形,绘制更低层的 box 时
|
||||
用 `Region::Exclude` 挖洞。
|
||||
|
||||
实测:菜单区与二级子菜单区的琥珀/绿色像素均为 **0**(既没有下层行边框,也没有被裁破的包角)。
|
||||
|
||||
## 5. 顺带修掉的东西
|
||||
|
||||
* 浮层**不能**用"本体矩形"当 clip:二级子菜单会展开到本体之外,会被整个裁掉
|
||||
(首版就踩了这个坑,子菜单一个框都没有)。
|
||||
* `Render` 的 `fullChrome` 并入 `m_showLayoutBounds`,装饰区也保证完整一帧。
|
||||
|
||||
## 6. 验证
|
||||
|
||||
```
|
||||
lbresid 5/5 (工具栏恒等、同状态两帧恒等、差异不越过绘图区上沿、层序正确)
|
||||
lbzorder 4/4 (菜单与子菜单区内 0 个下层颜色)
|
||||
lbcheck 21/21 ;uicheck 69/69 ;rendercheck 区域哈希 8/8 不变 + FUNC 7/7
|
||||
零 warning;TargetCheck.exe 906,752 B(885.5 KiB)
|
||||
```
|
||||
|
||||
## 7. 遗留
|
||||
|
||||
1. 调试模式下强制整幅重绘:283 个网格时帧率明显下降 —— 这是**有意的取舍**
|
||||
(调试而非日常使用),但若将来要让边界常驻,需要改为"只把叠加层位图化再贴回"。
|
||||
2. 菜单项逐个画框略显拥挤,可选做"只画外框 + 悬停项高亮框"。
|
||||
3. 开关状态仍未持久化。
|
||||
|
||||
---
|
||||
|
||||
# 第九阶段报告:把"边界"从补丁改成机制
|
||||
|
||||
## 1. 为什么上一轮只能算"打了三个补丁"
|
||||
|
||||
第十轮解决了三个症状(包角太粗、残留、层级),但没有触及根因。根因是:
|
||||
|
||||
* **框与形状不重合**(内缩)—— 这是我在第九轮为了"父子共边不叠粗"引入的**错误取舍**;
|
||||
* **文本没有几何身份** —— 65 处绘制点各自现算矩形、画完即弃,任何"逐处补登记"都必漏;
|
||||
* **描边图元太多** —— 12 图元/box,纯属实现细节没优化。
|
||||
|
||||
## 2. 取消内缩:正确的取舍是"后画的覆盖"
|
||||
|
||||
第九轮加内缩的理由是"父子矩形共边时两条同色线叠成更粗的一条"。但这个问题的正确解法不是
|
||||
缩小子框,而是承认**图层语义**:子框后画,就由子框的颜色覆盖父框的那条边。
|
||||
|
||||
内缩的代价是:任何**非直角形状**(胶囊形 GX 徽标、带描边环的色块、圆角按钮)的可见边界
|
||||
都在外接矩形上,内缩 1px 之后框就比形状小一圈 —— 用户看到的正是"GX 标签的圆弧在框之外"。
|
||||
|
||||
现行规则:**框 = 可见形状的外接矩形,绝不内缩。**
|
||||
|
||||
## 3. 文本:在唯一出口回报一次
|
||||
|
||||
```
|
||||
class UiTextRectSink { virtual void OnTextRect(const gd::RectF&) = 0; };
|
||||
void UiSetTextRectSink(UiTextRectSink*); // 开关关闭时为 nullptr
|
||||
void UiSetTextRectOffset(float dx, float dy); // 离屏位图渲染的坐标换算
|
||||
```
|
||||
|
||||
`DrawUiText()` / `UiTextAtClipped()` 画完后按对齐方式与实测宽度/高度把布局矩形**收窄**
|
||||
成墨迹矩形再回报。于是:
|
||||
|
||||
* 65 处调用点**一行都不用改**;
|
||||
* 每个文本框就是文字真正占据的那一小块(而不是整行);
|
||||
* 开关关闭时只是一次空指针判断,正常绘制零影响(`rendercheck` 8 场景哈希逐像素不变为证)。
|
||||
|
||||
## 4. 缓存:文本框必须跟着位图一起缓存
|
||||
|
||||
面板的行是"画进位图再贴回",命中缓存的帧**根本不调用 `UiText`**。所以:
|
||||
|
||||
* 行渲染时用本地 `RowTextCollector` 收集(位图坐标);
|
||||
* 存进 `RowBitmapCache::textRects` 与位图同生命周期;
|
||||
* 贴图时按 `(originX, originY)` 回放给外层收集器;
|
||||
* **无条件收集** —— 否则"关闭调试时滚过列表、再打开调试"会命中缓存而永远没有文本框。
|
||||
|
||||
## 5. 性能:图元数量是唯一瓶颈
|
||||
|
||||
微基准(同一块 1707×949 DIB,5688 段图元):
|
||||
|
||||
| 实现 | ms |
|
||||
| --- | --- |
|
||||
| GDI+ `FillRectangle` × 5688(每次新建画刷) | 13.0 |
|
||||
| 复用画刷 | 12.4 |
|
||||
| 每 box 一条 `GraphicsPath` + `FillPath` | 22.8 |
|
||||
| 全部 box 合并成一条 path | 8.4 |
|
||||
| **每 box 只画 1 个矩形**(474 次) | **0.70** |
|
||||
| GDI `PolyPolygon` 一次调用 | 114 |
|
||||
|
||||
结论:瓶颈是**调用次数**(≈1µs/次),合并路径反而更慢。因此把 12 → **8**:
|
||||
4 条 1px 边 + 4 个自适应角块。实测 `paintBoxes` **5.892 → 4.028ms(−32%)**。
|
||||
|
||||
## 6. 覆盖率
|
||||
|
||||
`PlotView` 补齐 6 类(时间轴 / 网格滚动条 / 回到最上方 / 光标差值标签 / 光标读数标签 / 内联输入框),
|
||||
全部读现成成员。新增探针 `.preflight/lbcovr` 用"已知矩形 vs 收集到的 box"逐条比对,
|
||||
把"有没有漏项"变成可回归的断言 —— 这比肉眼看图可靠。
|
||||
|
||||
## 7. 数据
|
||||
|
||||
```
|
||||
四个探针:lbcovr 7/7 · lbcheck 21/21 · lbresid 5/5 · lbzorder 4/4 · uicheck 69/69
|
||||
rendercheck:整帧与区域哈希与上一轮逐一相同,FUNC 7/7
|
||||
TargetCheck.exe:909,824 B(888.5 KiB)
|
||||
帧耗时(MF4 / 283 网格):base 9.06 / 仅强制整帧 6.66 / 完整调试 11.35 (collect 0.082 + paint 4.028)
|
||||
```
|
||||
|
||||
## 8. 遗留
|
||||
|
||||
1. 调试模式仍依赖"强制整幅重绘"(`SetForceFullRepaint`);根治需要让快路径支持
|
||||
"重画指定矩形集合"。另外实测**基线本身**每帧在 `WarmLaneLayersStep()` 上花约 2.4ms
|
||||
(9.06 vs 6.66),这是与本功能无关的既有开销,值得单独治。
|
||||
2. 文本墨迹矩形是"字符单元"高度,不含下伸部(descender)的差异;对除 `g/y/p` 多的文本
|
||||
会略微偏扁,属可接受近似。
|
||||
3. `PlotView` 的坐标轴刻度带仍未单独上报(只上报整条 `m_timeAxisRect`)。
|
||||
|
||||
---
|
||||
|
||||
# 第十阶段报告:把"层级"变成一条可验证的规则
|
||||
|
||||
## 1. 先说清楚"既有性能问题"到底是什么
|
||||
|
||||
上一轮报告里我写了"`PlotView` 基线每帧在 `WarmLaneLayersStep()` 上花约 2.4ms"。
|
||||
本轮用更细的探针查证,**这个结论是错的**:
|
||||
|
||||
```
|
||||
排空前 idle 2.60 ms ← 这里包含"图层预热队列排空"的瞬时成本
|
||||
排空后 idle 0.53 ms ← 真正的稳态
|
||||
强制整帧 idle 0.46 ms
|
||||
```
|
||||
|
||||
也就是说**没有**每帧白烧 2.4ms;那 2.4ms 是"载入 283 个网格后,需要给 16 个网格补建宽基底图层"
|
||||
的瞬时开销(每帧补 1 个,1~3ms/帧,约 16 帧结束)。这属于设计内的预热。
|
||||
|
||||
不过顺着这条线**确实**找到一处顺序问题:`RollWideBuffer` 先给**所有**网格算内容指纹
|
||||
(`LaneLayerKey()`,283 次 ≈ 0.7ms/帧),然后才判断"这个网格是否可视、不视则跳过";
|
||||
而且离屏网格的"结构不可用"分支会**逐帧**把它们塞进待重画队列,再在同一帧被 `remove_if` 清掉。
|
||||
把可视性判断提到循环最前之后,离屏网格既不哈希也不入队。
|
||||
|
||||
**教训**:性能结论必须先区分"稳态"与"瞬时",否则会把预热成本误判成常态浪费。
|
||||
|
||||
## 2. "浮层不能被下层覆盖"需要两件事,不是一件
|
||||
|
||||
用户先后两次报告同一类现象(面板被网格边界穿透、菜单边界溢出),根源都在**分层只有一半**:
|
||||
|
||||
| 机制 | 作用 | 只有它够吗 |
|
||||
| --- | --- | --- |
|
||||
| `layer`(`PushLayer`)+ 按 `(layer, depth)` 排序 | 决定**两个盒子重叠时谁压谁** | ❌ 不够 |
|
||||
| `AddOccluder` + `Region::Exclude` | 让**更低层的盒子整体不进入浮层区域** | ❌ 单独也不够(还需要层号) |
|
||||
| `SetClip` | 让**滚动容器里滚出的条目不再登记/绘制** | 解决"溢出/闪烁" |
|
||||
|
||||
本轮把这三件事在浮动面板上补齐(菜单在上一轮已有 occluder,本轮补 clip)。
|
||||
|
||||
## 3. 为什么"滚动时闪烁"
|
||||
|
||||
滚动容器(面板内容区 / 菜单条目区)的条目矩形**始终存在**,只是绘制时被裁剪掉。
|
||||
调试层读到这些矩形就会给"看不见的内容"画框 —— 而滚动时条目不断进出可视区,
|
||||
框也就不断出现/消失,看上去就是闪烁。修法与菜单一致:登记盒子时把裁剪矩形设为可视区。
|
||||
|
||||
实测:面板层 escaped=0、菜单层 belowViewport=0、面板内网格层颜色像素=0,
|
||||
且真滚动 1295px 的过程中"面板盒子签名"11/11 帧完全一致。
|
||||
|
||||
## 4. 顺带的一处小优化
|
||||
|
||||
`EdgePanelLayoutFor` 单次约 2~3µs,283 网格 × 5 类 = 1415 次/帧。现在先看该网格的
|
||||
5 个面板 `open` 标志,全关就跳过整个内层循环。
|
||||
|
||||
## 5. 数据
|
||||
|
||||
```
|
||||
零 warning;TargetCheck.exe 909,824 B(888.5 KiB)
|
||||
lbpanel 10/10(新增)· lbcovr 7/7 · lbzorder 4/4 · lbresid 5/5 · lbcheck 21/21 · uicheck 69/69
|
||||
rendercheck 8 场景哈希与上一轮逐一相同 + FUNC 7/7
|
||||
```
|
||||
|
||||
## 6. 仍然存在的近似
|
||||
|
||||
* 面板层的层号只比网格高 1,而**文本盒子**固定在 layer 2 收集。因此面板**自己的**文字框
|
||||
能正常显示,但理论上"图例文字"若与面板重叠仍会画在面板上。实际布局下两者不重叠。
|
||||
若要彻底消除,需要让文本收集器在"面板绘制期间"使用面板层的层号。
|
||||
* 菜单/面板的条目裁剪用的是"可视区矩形",因此**与边界部分相交的那一条**仍会被登记并裁剪绘制
|
||||
(这是期望行为:它确实可见)。
|
||||
唯一产物:`Build/Release/JustDataInspector.exe`(含图标、清单,`-static` 链 CRT)。
|
||||
**无任何新增 DLL 依赖。**
|
||||
@@ -8327,3 +8327,141 @@ rendercheck 8 场景哈希与上一轮逐一相同 + FUNC 7/7(本轮不改变
|
||||
面板层只比网格层高 1,而**文本盒子**固定在 layer 2 收集 ⇒ 面板自己的文字框正常显示,
|
||||
但"图例文字"若与面板重叠仍会画在面板上(当前布局下不重叠)。彻底解决需要让文本收集器
|
||||
在面板绘制期间使用面板层的层号。
|
||||
|
||||
## 2026-10-29 第十三轮(本机):体积 L3 落地(−120,832 B)+ L1 几何产物层 + 审计
|
||||
|
||||
> 本轮回答的是"**在不拆 DLL、不改 `-Oz`、不改变任何像素**的前提下把体积降下来,
|
||||
> 关异常后不牺牲健壮性,并把几何从调试绘制里解耦"。
|
||||
|
||||
### 1. 体积:909,824 B → **788,992 B(−120,832 B,−13.3%)**
|
||||
|
||||
分两步,每步都可独立回退:
|
||||
|
||||
| 步骤 | 手段 | 体积 | 段变化要点 |
|
||||
| --- | --- | ---: | --- |
|
||||
| 基线 | — | 909,824 | — |
|
||||
| ① | `-fno-exceptions` + `-ffunction-sections -fdata-sections` + `-Wl,--gc-sections` | 882,688 | `.xdata` 48,132→34,152、`.pdata` 35,424→34,524、`.text` −15 KB |
|
||||
| ② | ① + `LTO=1`(`-flto` 编译与链接) | **788,992** | `.text` 657,168、`.rdata` 35,016、`.xdata` 27,512、`.pdata` 28,524 |
|
||||
|
||||
**Makefile 三个开关**(默认全为 1,可命令行覆盖):
|
||||
|
||||
```
|
||||
LAYOUT_BOUNDS=1 # 编入「显示布局边界」叠加层(LAYOUT_BOUNDS=0 可剔除)
|
||||
NO_EXCEPTIONS=1 # 追加 -fno-exceptions
|
||||
LTO=1 # 追加 -flto(编译+链接)
|
||||
```
|
||||
|
||||
派生变量(定义在 `LDFLAGS` **之后**,靠 make 递归变量"展开 recipe 时才求值"生效):
|
||||
|
||||
| 变量 | `LTO=1` | `LTO=0` |
|
||||
| --- | --- | --- |
|
||||
| `FUNC_SECTIONS` | **空** | `-ffunction-sections -fdata-sections` |
|
||||
| `ASM_LISTING` / `ASM_LISTING_C` | **空** | `-Wa,-a,-ad,-ahlms=$(@:%.o=%.lst)` |
|
||||
| `GC_SECTIONS` | `-Wl,--gc-sections` | 同左 |
|
||||
|
||||
### 2. ⚠️ LTO 的两个坑(都实测复现,务必保留结论)
|
||||
|
||||
1. **切换 LTO 必须 `make clean`**:LTO 与非 LTO 目标文件混链 ⇒ GCC 15.2.0
|
||||
在 `binds_to_current_def_p (symtab.cc:2598)` 处 ICE。
|
||||
2. **`-flto` 与 `-ffunction-sections -fdata-sections` 同时打开也 ICE**
|
||||
(即使全部干净重编也必崩;报错落点是 `Utilities/Inc/DataModel.hpp:393` 的 `struct SignalData`,
|
||||
但那只是 COMDAT 解析落点,**不是**原因)。
|
||||
⇒ LTO 下 `FUNC_SECTIONS` 置空,并加 **`-flto-partition=one`**:
|
||||
默认多分区 LTRANS(11 个分区)稳定复现,单分区从未复现。
|
||||
**`-flto-partition=one` 不是性能选项,是稳定性选项,不要删。**
|
||||
代价:LTRANS 串行,全量构建慢约一倍。
|
||||
3. LTO 下 `-Wa,-a,-ad,-ahlms` 的 `.lst` 汇编清单不再生成(lto-wrapper 丢弃并告警)⇒
|
||||
需要看汇编时用 `make release LTO=0`。
|
||||
|
||||
### 3. 关异常必须成对交付:预检验 + 兜底(新增 `MemProbe`)
|
||||
|
||||
`Utilities/Inc/MemProbe.hpp` + `Utilities/Src/MemProbe.cpp`:
|
||||
|
||||
* `MemAvailablePhysical()` —— `GlobalMemoryStatusEx` + **200ms 缓存**;失败返回 `SIZE_MAX`(视为充足)。
|
||||
* `MemCanAllocate(bytes)` —— 预留后仍须保有 1/8 可用内存(否则 Win 会疯狂换页,表现为"卡死"而非"失败")。
|
||||
* `MemReserveIfRoom(container, n)` —— 含 `capacity()` 早退 + 乘法溢出保护;
|
||||
预留失败语义上只是"少了一次性优化",`push_back` 自行增长,**结果依然正确**。
|
||||
* `MemInstallTerminateHandler()` —— `std::set_terminate`,OOM 时弹一次带建议的模态提示后 `_Exit`。
|
||||
|
||||
接入点:
|
||||
|
||||
* `Utilities/Src/DataModel.cpp`:全仓**唯一**的 `try/catch`(`extremes.reserve(count/8)` 捕
|
||||
`std::bad_alloc`)→ `MemReserveIfRoom(extremes, count/8)`。
|
||||
* `Components/PlotView/Src/PlotView.cpp`:`AvailablePhysicalBytes()` 原有**一份完整重复实现**
|
||||
(`GlobalMemoryStatusEx` + 200ms 缓存)→ 转发 `MemAvailablePhysical()`。
|
||||
* `Components/Application/Src/Application.cpp`:`Initialize()` 开头 `MemInstallTerminateHandler();`。
|
||||
|
||||
⇒ 全仓已无 `try/catch`(除 `MemProbe` 注释中的说明)。
|
||||
|
||||
### 4. L1 几何产物层(新增 `UiBox.hpp/.cpp`,`UiDebugBounds` 退化为消费者)
|
||||
|
||||
* `struct UiBox { gd::RectF rect; gd::RectF clip; int depth; int layer; }` —— **纯值类型**,
|
||||
不继承任何东西(继承会和 `PlotView` 4 层位图缓存 / `SignalPanel` 行位图缓存的失效体系打架)。
|
||||
* `class UiBoxTable : public UiTextRectSink` —— 只累计几何 + `SetClip` / `PushLayer` / `SetLayer` /
|
||||
`AddOccluder`,**不含任何绘制代码**。
|
||||
* `UiComponent::CollectDebugBoxes` → **`virtual void BuildBoxes(UiBoxTable&)`**(默认只 `Add(Area(), 0)`)。
|
||||
语义从"收集布局边界(调试用)"变为"上报几何产物",组件**不再需要知道"调试"这个概念存在**。
|
||||
* `UiDebugBounds.hpp/.cpp` 只剩 `UiDebugBoundsColor` / `UiDebugPaintBoundsClipped` /
|
||||
`UiDebugPaintBoxList(g, const UiBoxTable&)` —— 唯一的消费者。
|
||||
* 只有宿主 `ApplicationCommands.cpp` 同时包含两者;`SignalPanel.cpp` / `PlotViewPaint.cpp` /
|
||||
`PopupMenu.cpp` / `ColorPickerPopup.cpp` / `UiWidget.cpp` / `TabBar.cpp` / `Toolbar.cpp`
|
||||
只包含 `UiBox.hpp`。
|
||||
|
||||
⚠️ 几何与绘制**都必须保留的语义**(改动一次就会让之前的 bug 复发):
|
||||
|
||||
* `SetClip(组件 Area())` —— 组件内部元素可能落在地盘外(滚出可视区的 `lane.rect` Y 为负),
|
||||
不裁剪就会登记到工具栏/标签栏的地盘上。
|
||||
* 每收集完一个组件 `PushLayer()` —— 后收集的压住先收集的(二级菜单压住行)。
|
||||
* 浮层 `AddOccluder(本体)` —— 仅靠排序只能决定"两个 box 重叠时谁压谁",
|
||||
不能阻止"下层组件的边框画在不透明浮层上面"。
|
||||
* 浮层**不要**设 `clip = 本体`(会把展开到本体外的二级子菜单整个裁掉)。
|
||||
|
||||
### 5. L2(`UiDamage` 脏矩形 + S4 独立叠加层)**评估后缓做**
|
||||
|
||||
理由(三条,缺一不可):
|
||||
|
||||
1. 受益方只有"**调试开关打开时**的帧率"。`SetForceFullRepaint` 只在
|
||||
`m_showLayoutBounds` 为真时置位,普通用户路径本来就是快路径。
|
||||
2. 正确实现无法只改 `PlotView` 内部:宿主用的是**持久后备缓冲区**,
|
||||
上一帧叠加层画上去的边框像素必须在下一帧被底层内容重画覆盖;改成"独立叠加位图 + 只混合脏区"
|
||||
就必须把叠加层旧矩形**注入**底层组件的重绘范围 ⇒ 穿透 `PlotView` 四条快路径 + 两层缓存。
|
||||
3. 收益/风险不成比例:拖拽错位、贴图残留是用户反复报过的事故类型。
|
||||
|
||||
保留:`SetForceFullRepaint` 及四处守卫、`UiBoxTable` 几何。将来要做时,
|
||||
几何可直接喂 `UiDamage`,不需重新收集。
|
||||
|
||||
### 6. 审计(本轮顺手做的)
|
||||
|
||||
* `MemProbe` 注释里"省 40~60KB"的估算 → 改为实测 **27,136 B**。
|
||||
* `MemProbe.hpp` 的 `<new>`、`MemProbe.cpp` 的 `<cstdio>` —— 未使用,删除。
|
||||
* `UiDebugPaintBounds(g, rect, depth)` —— **零调用者**(唯一调用者是它自己),删除;
|
||||
只保留带 `clip` 的 `UiDebugPaintBoundsClipped`。
|
||||
* `TabBar` / `Toolbar` / `PlotView` / `SignalPanel` 的 `BuildBoxes` 文档里"(调试用)"→"几何 box"。
|
||||
* `PlotView::SetForceFullRepaint` 文档补写"为什么保留而不优化"(即 L2 缓做理由)。
|
||||
* 源码中旧名 `UiDebugBoxList` / `UiDebugBox` / `CollectDebugBoxes` **零残留**。
|
||||
|
||||
### 7. 验证
|
||||
|
||||
```
|
||||
uicheck 69/69 lbcheck 21/21 lbresid 5/5
|
||||
lbzorder 4/4 lbcovr 7/7 lbpanel 10/10 (合计 116 条断言)
|
||||
rendercheck: A FAED59DF284153DB / B E6443BFE4E3955F8 / C 96A0AA8C402F1729 / D E3D5DBD998E43720
|
||||
E DA0583CB13C26A6F / F FFB822159F604D1D / G 19F8DE8FE3A8DF18 / H DBBDDE4CB2C97C92
|
||||
FUNC 7/7 PASS, exit=0
|
||||
```
|
||||
|
||||
**8 个哈希与基线逐一相同**;`make release` / `make debug` 均零 warning。
|
||||
|
||||
### 8. ⚠️ 探针脚本与 LTO 的耦合
|
||||
|
||||
`.preflight/*/build.ps1` 与 `Tools/*/build.ps1` 链接的是 `Build/Release/*.o`。
|
||||
**LTO 下这些是 LTO 字节码**,因此 7 个脚本的链接参数都补了 `-flto -flto-partition=one`
|
||||
(对非 LTO 目标文件无害)。这些脚本目前**未被 git 跟踪**;
|
||||
若 LTO 长期默认开启,重建探针前记得补这两个参数,否则链接失败。
|
||||
|
||||
### 9. 体积纪律(不变)
|
||||
|
||||
预警阈值 **1MB(1,048,576 B)**,不是硬上限。超阈值时**必须主动提醒用户**并说明
|
||||
"超在哪一段(`.text` / `.rdata` / `.xdata`+`.pdata` / `.rsrc`)、由哪个改动贡献"。
|
||||
查段构成的标准命令:`objdump -h Build\Release\JustDataInspector.exe`。
|
||||
当前 788,992 B,余量 259,584 B。
|
||||
@@ -10,13 +10,33 @@ SUB_DIR_RELEASE = Release
|
||||
# Debug 编译子目录
|
||||
SUB_DIR_DEBUG = Debug
|
||||
|
||||
# Release 优化等级
|
||||
# ⚠️ 用 `-Oz` 而不是 `-Os`:本项目热点全在 GDI/GDI+ 绘制上,`-Oz` 与 `-Os` 的实测帧耗时
|
||||
# 没有可辨识差异,但产物小 ~6KB。
|
||||
# ⚠️ 产物体积上限(用户 2026-10-27 更新):**900KB**。这不是"不许超过",而是**到达 900KB
|
||||
# 就必须提醒用户**,以便确认功能带来的体积膨胀是否值得(旧红线 800KB 已作废)。
|
||||
# ⚠️ 产物体积**预警阈值**(用户 2026-10-29 更新):**1MB(1,048,576 B)**。
|
||||
# 这不是"不许超过":**超过仍可发布**,但到达阈值时必须主动提醒用户,
|
||||
# 并说明"超在哪一段(`.text` / `.rdata` / `.xdata`+`.pdata` / `.rsrc`)、由哪个改动贡献"。
|
||||
# 旧红线 900KB / 800KB 均已作废。
|
||||
# 查段构成的命令(排查超限原因的标准做法):objdump -h Build/Release/Target.exe
|
||||
RELEASE_OPT = -Oz
|
||||
|
||||
# ---- 体积相关编译开关 ------------------------------------------------------
|
||||
# LAYOUT_BOUNDS:1 = 编入"显示布局边界"调试叠加层(约 13KB);0 = 剔除。
|
||||
# 剔除后用 `make release LAYOUT_BOUNDS=0` 可得到最小体积,但发布版就没有该菜单项了。
|
||||
LAYOUT_BOUNDS = 1
|
||||
|
||||
# NO_EXCEPTIONS:1 = 用 `-fno-exceptions` 构建(省 `.xdata`+`.pdata` 约 28KB)。
|
||||
# ⚠️ 代价:所有"按数据规模预留大块内存"的地方必须用 `MemProbe.hpp` 的
|
||||
# `MemReserveIfRoom()` **预检验**,不能再靠 try/catch。
|
||||
# 兜底:`Application::Initialize` 里安装了 `MemInstallTerminateHandler()`
|
||||
# (万一仍有遗漏,OOM 时弹一次带建议的模态提示后干净退出,而不是静默消失)。
|
||||
NO_EXCEPTIONS = 1
|
||||
|
||||
# LTO:1 = 链接时优化(实测 −94KB)。构建时间约翻倍。
|
||||
# ⚠️ **切换 LTO 必须 `make clean`**:LTO 与非 LTO 目标文件混链会让
|
||||
# GCC 15.2.0 在 `binds_to_current_def_p` 处 ICE(已实测复现)。
|
||||
# ⚠️ 开启 LTO 后 `-Wa,...,-ahlms` 的 `.lst` 汇编清单不再生成(lto-wrapper 会丢弃并告警),
|
||||
# 因此 `ASM_LISTING` 在 LTO 下为空。
|
||||
LTO = 1
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Debug 优化等级
|
||||
DEBUG_OPT = -O0
|
||||
|
||||
@@ -131,6 +151,7 @@ CFLAGS = $(foreach text,$(C_DEFS),$(addprefix -D,$(text))) \
|
||||
-fexec-charset=$(strip $(OUTPUT_CHAR_ENCODING)) \
|
||||
-fwide-exec-charset=$(strip $(OUTPUT_WCHAR_ENCODING)) \
|
||||
-static \
|
||||
$(FUNC_SECTIONS) \
|
||||
-Wall \
|
||||
-MMD -MP -MF"$(@:%.o=%.d)" \
|
||||
|
||||
@@ -142,12 +163,15 @@ CXXFLAGS = $(foreach text,$(CXX_DEFS),$(addprefix -D,$(text))) \
|
||||
-fexec-charset=$(strip $(OUTPUT_CHAR_ENCODING)) \
|
||||
-fwide-exec-charset=$(strip $(OUTPUT_WCHAR_ENCODING)) \
|
||||
-static \
|
||||
$(FUNC_SECTIONS) \
|
||||
-Wall \
|
||||
-MMD -MP -MF"$(@:%.o=%.d)" \
|
||||
-lstdc++ \
|
||||
|
||||
|
||||
# 链接选项
|
||||
LDFLAGS = -Wl,-Map,$(basename $@).map \
|
||||
$(GC_SECTIONS) \
|
||||
|
||||
##################################################################################
|
||||
|
||||
@@ -158,6 +182,42 @@ else
|
||||
LDFLAGS += -mconsole
|
||||
endif
|
||||
|
||||
# ---- 体积/行为相关开关(必须在 CFLAGS / CXXFLAGS / LDFLAGS 定义之后)--------
|
||||
# 说明:这些是 `=` / `+=` 的递归变量,make 在展开 recipe 时才求值,
|
||||
# 因此放在 LDFLAGS 定义之后仍然能正确作用到前面的 CFLAGS / LDFLAGS。
|
||||
|
||||
ifeq ($(strip $(LAYOUT_BOUNDS)), 1)
|
||||
CXX_DEFS += JDI_LAYOUT_BOUNDS
|
||||
endif
|
||||
|
||||
ifeq ($(strip $(NO_EXCEPTIONS)), 1)
|
||||
CXXFLAGS += -fno-exceptions
|
||||
endif
|
||||
|
||||
# 丢弃未被引用的函数/数据(非 LTO 时还需要配合 -ffunction-sections 才有粒度)。
|
||||
GC_SECTIONS = -Wl,--gc-sections
|
||||
|
||||
ifeq ($(strip $(LTO)), 1)
|
||||
# LTO 自己就做跨编译单元的内联与去重,不需要再开 function-sections;
|
||||
# 实测 `-flto` 与 `-ffunction-sections -fdata-sections` 同时打开会让
|
||||
# GCC 15.2.0 在 `binds_to_current_def_p (symtab.cc:2598)` 处 ICE。
|
||||
FUNC_SECTIONS =
|
||||
CFLAGS += -flto
|
||||
CXXFLAGS += -flto
|
||||
# ⚠️ `-flto-partition=one` 是绕过上面那个 ICE 的关键:
|
||||
# 默认的多分区 LTRANS 在解析 COMDAT 定义时必崩(同一份代码拆成 11 个分区稳定复现,
|
||||
# 单分区从未复现)。代价是 LTRANS 只能串行,全量构建比非 LTO 慢约一倍。
|
||||
LDFLAGS += -flto -flto-partition=one
|
||||
# lto-wrapper 会丢弃 -Wa 的汇编清单并告警,故 LTO 下不生成 .lst。
|
||||
ASM_LISTING =
|
||||
ASM_LISTING_C =
|
||||
else
|
||||
FUNC_SECTIONS = -ffunction-sections -fdata-sections
|
||||
ASM_LISTING = -Wa,-a,-ad,-ahlms=$(@:%.o=%.lst)
|
||||
ASM_LISTING_C = -Wa,-a,-ad,-alms=$(@:%.o=%.lst)
|
||||
endif
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# C目标文件及索引目录关联
|
||||
OBJECTS = $(notdir $(C_EXTERA_SOURCES:.c=.o))
|
||||
OBJECTS += $(subst .c,.o,$(notdir $(foreach path,$(C_SOURCES_PATHS),$(wildcard $(path)/*.c))))
|
||||
@@ -224,12 +284,12 @@ $(BUILD_DIR)/$(SUB_DIR_DEBUG)/$(TARGET_FILE_NAME).exe: $(DEBUG_OBJECTS)
|
||||
# C Release 目标文件编译
|
||||
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.c Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
|
||||
@echo ====== [Release] C Source File "$<" Compiling... ======
|
||||
$(C_COMPLIER) -c $(CFLAGS) $(RELEASE_OPT) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(SUB_DIR_RELEASE)/$(notdir $(<:.c=.lst)) $< -o $@
|
||||
$(C_COMPLIER) -c $(CFLAGS) $(RELEASE_OPT) $(ASM_LISTING_C) $< -o $@
|
||||
|
||||
# C++ Release 目标文件编译
|
||||
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.cpp Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
|
||||
@echo ====== [Release] C++ Source File "$<" Compiling... ======
|
||||
$(CXX_COMPLIER) -c $(CXXFLAGS) $(RELEASE_OPT) -Wa,-a,-ad,-ahlms=$(BUILD_DIR)/$(SUB_DIR_RELEASE)/$(notdir $(<:.cpp=.lst)) $< -o $@
|
||||
$(CXX_COMPLIER) -c $(CXXFLAGS) $(RELEASE_OPT) $(ASM_LISTING) $< -o $@
|
||||
|
||||
# Release 资源脚本文件编译
|
||||
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
|
||||
@@ -239,12 +299,12 @@ $(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_RELE
|
||||
# C Debug 目标文件编译
|
||||
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.c Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
|
||||
@echo ====== [Debug] C Source File "$<" Compiling... ======
|
||||
$(C_COMPLIER) -c $(CFLAGS) $(DEBUG_OPT) -g -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(SUB_DIR_DEBUG)/$(notdir $(<:.c=.lst)) $< -o $@
|
||||
$(C_COMPLIER) -c $(CFLAGS) $(RELEASE_OPT) $(ASM_LISTING_C) $< -o $@
|
||||
|
||||
# C++ Debug 目标文件编译
|
||||
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.cpp Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
|
||||
@echo ====== [Debug] C++ Source File "$<" Compiling... ======
|
||||
$(CXX_COMPLIER) -c $(CXXFLAGS) $(DEBUG_OPT) -g -Wa,-a,-ad,-ahlms=$(BUILD_DIR)/$(SUB_DIR_DEBUG)/$(notdir $(<:.cpp=.lst)) $< -o $@
|
||||
$(CXX_COMPLIER) -c $(CXXFLAGS) $(RELEASE_OPT) $(ASM_LISTING) $< -o $@
|
||||
|
||||
# Debug 资源脚本文件编译
|
||||
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#ifndef __MEM_PROBE_HPP__
|
||||
#define __MEM_PROBE_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 可用物理内存的估算值(字节,带 200ms 缓存)。
|
||||
*
|
||||
* ⚠️ **为什么需要它**:本项目用 `-fno-exceptions` 构建(`.xdata`+`.pdata` 占产物体积约 9%,
|
||||
* 关闭后**实测省 27,136 B**:909,824 → 882,688)。关闭异常后,
|
||||
* `std::vector::reserve` 之类的分配失败**不再抛
|
||||
* `std::bad_alloc`**,而是直接 `std::terminate` —— 因此凡是"按数据规模预留大块内存"
|
||||
* 的地方,都必须先**预检验**(本函数)再决定是否预留。
|
||||
*
|
||||
* `GlobalMemoryStatusEx` 是系统调用,结果按 200ms 缓存:内存变化远慢于该间隔,
|
||||
* 而绘制路径上每帧可能请求多次。
|
||||
*
|
||||
* @return 可用物理内存字节数;查询失败时返回 `SIZE_MAX`(视为"内存充足")。
|
||||
*/
|
||||
std::size_t MemAvailablePhysical();
|
||||
|
||||
/**
|
||||
* @brief 判断"再分配 `bytes` 字节是否安全"。
|
||||
*
|
||||
* 判据是"预留后仍保有至少 `kMemHeadroomRatio` 的可用内存",避免把系统逼到换页。
|
||||
*
|
||||
* @param [IN] bytes 计划分配的字节数。
|
||||
* @return 安全返回 true。
|
||||
*/
|
||||
bool MemCanAllocate(std::size_t bytes);
|
||||
|
||||
/**
|
||||
* @brief 按数据规模预留的**安全版**:只有确认内存够才 reserve。
|
||||
*
|
||||
* 预留失败在语义上只是"少了一次性优化":不预留时 `push_back` 会按 1.5~2 倍自行增长,
|
||||
* 结果依然正确(只是多几次 realloc)。因此在 `-fno-exceptions` 下这条路径
|
||||
* **既不会崩、也不会改变行为**。
|
||||
*
|
||||
* @tparam T 元素类型。
|
||||
* @param [IN,OUT] container 目标容器(需支持 `reserve` / `capacity` / `size`)。
|
||||
* @param [IN] count 期望容量(元素个数)。若已足够则什么都不做。
|
||||
* @return 真的执行了预留返回 true。
|
||||
*/
|
||||
template <typename Container>
|
||||
bool MemReserveIfRoom(Container& container, std::size_t count)
|
||||
{
|
||||
if (count <= container.capacity())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t elementSize = sizeof(typename Container::value_type);
|
||||
// 溢出保护:count * elementSize 可能超出 size_t。
|
||||
if (elementSize != 0 && count > (std::numeric_limits<std::size_t>::max() / elementSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!MemCanAllocate(count * elementSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
container.reserve(count);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 安装"内存不足"兜底处理(`std::set_terminate`)。
|
||||
*
|
||||
* 在 `-fno-exceptions` 下,任何遗漏的分配失败都会走到 terminate。
|
||||
* 默认的 terminate 会静默 abort(用户只看到程序消失);本函数改为
|
||||
* **弹一次模态提示(含"请减少同时显示的网格数/信号数"的建议)后干净退出**,
|
||||
* 让用户知道发生了什么。
|
||||
*
|
||||
* 幂等:重复调用只安装一次。
|
||||
*/
|
||||
void MemInstallTerminateHandler();
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __MEM_PROBE_HPP__
|
||||
+10
-14
@@ -1,9 +1,12 @@
|
||||
// DataModel 实现分片 —— 本文件只包含 DataModel 的**成员函数定义**(纯代码搬移,行为与拆分前完全一致)。
|
||||
// DataModel 实现分片 —— 数据层核心:信号/分组容器、采样访问、派生曲线计算。
|
||||
//
|
||||
// 共用的常量与自由函数在 DataModelInternal.hpp(命名空间 `dmdetail`,全部 inline)。
|
||||
// 分片按“职责”划分:核心(状态/布局/几何)、渲染、交互与编辑,见各文件名。
|
||||
// 共用的常量与工具函数在 DataModelInternal.hpp(命名空间 `dmdetail`,全部 inline)。
|
||||
// ⚠️ 本文件以 `-fno-exceptions` 构建:任何"按数据规模预留内存"的地方都必须用
|
||||
// `MemReserveIfRoom()` 预检验,不能用 try/catch(见 `MemProbe.hpp`)。
|
||||
#include "DataModelInternal.hpp"
|
||||
|
||||
#include "MemProbe.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
@@ -996,17 +999,10 @@ namespace jdi
|
||||
// 适度预留:编码器方波约每 8 个采样一个极值,预留 1/8 能让最常见的密集信号
|
||||
// 一次到位(避免 push_back 反复扩容带来的 1.5x 峰值内存);宁少勿多,
|
||||
// 因为 unreserve 的代价(一次 realloc)远小于无谓地占住内存。
|
||||
if (extremes.capacity() < count / 8)
|
||||
{
|
||||
try
|
||||
{
|
||||
extremes.reserve(count / 8);
|
||||
}
|
||||
catch (const std::bad_alloc&)
|
||||
{
|
||||
// 预留失败不影响正确性:让 push_back 自行增长(低内存时不报错)。
|
||||
}
|
||||
}
|
||||
// ⚠️ 用 `MemReserveIfRoom` 而**不是** try/catch:本项目以 `-fno-exceptions` 构建,
|
||||
// 分配失败不再抛 `std::bad_alloc` 而是 terminate。预检验在"内存不够"时
|
||||
// 直接跳过预留 —— 语义等到"少了一次性优化",`push_back` 会自行为增长。
|
||||
MemReserveIfRoom(extremes, count / 8);
|
||||
{
|
||||
int direction = 0; // 当前单调段方向(+1 上升、-1 下降)。
|
||||
bool hasPrevious = false;
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// 内存预检验与"分配失败"兜底(配合 `-fno-exceptions` 构建)。
|
||||
//
|
||||
// 背景:本项目关闭了 C++ 异常(`.xdata`+`.pdata` 占产物体积约 9%,关闭后**实测省 27,136 B**:
|
||||
// 909,824 → 882,688;再叠加 LTO 共减 120,832 B)。关闭后 `std::bad_alloc` 不再可捕获,
|
||||
// 任何分配失败都会直接 terminate。因此:
|
||||
// 1. 凡"按数据规模预留大块内存"的地方改用 `MemReserveIfRoom()`(预检验);
|
||||
// 2. 万一仍有遗漏,由 `MemInstallTerminateHandler()` 给出可见提示而不是静默消失。
|
||||
#include "MemProbe.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
/** @brief 可用内存查询的缓存有效期(毫秒)。 */
|
||||
constexpr std::uint64_t kMemCacheMs = 200;
|
||||
|
||||
/**
|
||||
* @brief 预留后必须保留的可用内存比例(1/8)。
|
||||
*
|
||||
* 不把可用内存吃光:Windows 在物理内存接近耗尽时会大量换页,
|
||||
* 表现为"整个程序卡死"而不是"分配失败",对用户更糟。
|
||||
*/
|
||||
constexpr std::size_t kMemHeadroomRatio = 8;
|
||||
|
||||
std::atomic<std::uint64_t> g_memCachedAt{0};
|
||||
std::atomic<std::size_t> g_memCachedBytes{0};
|
||||
|
||||
/**
|
||||
* @brief terminate 处理:给出可见提示后退出。
|
||||
*
|
||||
* ⚠️ 这里**不能**用 MessageBoxW 之外的任何复杂机制:此刻堆可能已不可用,
|
||||
* 连 `std::wstring` 都不该构造(字面量是静态数据,安全)。
|
||||
*/
|
||||
void OnTerminate()
|
||||
{
|
||||
MessageBoxW(nullptr,
|
||||
L"内存不足:本次操作需要的内存超过系统可用量。\n\n"
|
||||
L"建议:减少同时显示的网格数或信号数,或关闭占用内存较大的程序后重试。",
|
||||
L"Just DataInspector", MB_ICONERROR | MB_OK | MB_SETFOREGROUND);
|
||||
std::_Exit(EXIT_FAILURE);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::size_t MemAvailablePhysical()
|
||||
{
|
||||
const std::uint64_t now = GetTickCount64();
|
||||
const std::uint64_t last = g_memCachedAt.load(std::memory_order_relaxed);
|
||||
if (last != 0 && (now - last) < kMemCacheMs)
|
||||
{
|
||||
const std::size_t cached = g_memCachedBytes.load(std::memory_order_relaxed);
|
||||
if (cached != 0)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
MEMORYSTATUSEX status = {};
|
||||
status.dwLength = sizeof(status);
|
||||
std::size_t available = std::numeric_limits<std::size_t>::max();
|
||||
if (GlobalMemoryStatusEx(&status) != FALSE)
|
||||
{
|
||||
available = static_cast<std::size_t>(status.ullAvailPhys);
|
||||
}
|
||||
g_memCachedBytes.store(available, std::memory_order_relaxed);
|
||||
g_memCachedAt.store(now, std::memory_order_relaxed);
|
||||
return available;
|
||||
}
|
||||
|
||||
bool MemCanAllocate(std::size_t bytes)
|
||||
{
|
||||
if (bytes == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const std::size_t available = MemAvailablePhysical();
|
||||
if (available == std::numeric_limits<std::size_t>::max())
|
||||
{
|
||||
return true; // 查询失败:按"内存充足"处理(宁可尝试也不要误拒)
|
||||
}
|
||||
const std::size_t headroom = available / kMemHeadroomRatio;
|
||||
return bytes <= (available - std::min(available, headroom));
|
||||
}
|
||||
|
||||
void MemInstallTerminateHandler()
|
||||
{
|
||||
static bool installed = false;
|
||||
if (installed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
installed = true;
|
||||
std::set_terminate(&OnTerminate);
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
Reference in new issue
Block a user