Files
DeepSeek V4.1 Flash 044b87383b 体积 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` 的文档里。
2026-10-29 11:53:54 +08:00

247 lines
9.0 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "UiDebugBounds.hpp"
#include <algorithm>
#include <cmath>
namespace jdi
{
namespace
{
/** @brief 四条边的描边宽度(设备像素)。 */
constexpr int kEdgeStroke = 1;
/** @brief 层级 0 的包角基础粗细(设备像素)。 */
constexpr int kAreaCornerThick = 4;
/** @brief 层级 ≥1 的包角基础粗细(设备像素)。 */
constexpr int kNodeCornerThick = 3;
/** @brief 包角沿边延伸的长度上限(设备像素)。 */
constexpr int kCornerLenMax = 14;
/** @brief 短边不超过该值时**只画四个角块、不画边**(小方框画满一圈会像粗边框)。 */
constexpr int kEdgelessThreshold = 12;
/** @brief 短边不超过该值时完全不画(再画就盖住元素本身了)。 */
constexpr int kSkipThreshold = 5;
/** @brief 一个已对齐到整像素、可直接填充的矩形。 */
struct PixelRect
{
int left = 0;
int top = 0;
int right = -1;
int bottom = -1;
int Width() const { return right - left + 1; }
int Height() const { return bottom - top + 1; }
bool Valid() const { return right >= left && bottom >= top; }
};
/**
* @brief 用整数像素矩形填充(要求调用方已把像素偏移模式设为 `None`)。
*
* 走 GDI+ 而不是 `UiFillRect`:这里需要**精确的 1px 覆盖**,而 `UiFillRect` 的快
* 路径按 `floor/ceil` 取整并可能转交 GDI,混进调试层会带来额外状态切换开销。
*
* ⚠️ **每次图元调用约 1µs**(实测 474 个 box × 12 段 = 5.9ms/帧),因此这里刻意
* 把图元数量从 12 压到 8;并且**不要**改写成 `GraphicsPath` —— `FillPath` 单次约
* 20µs,比 12 段 `FillRectangle` 还慢(本机实测 22.8ms vs 13.0ms)。
*/
void FillPixels(gd::Graphics& g, int x, int y, int width, int height, std::uint32_t argb)
{
if (width <= 0 || height <= 0)
{
return;
}
gd::SolidBrush brush{gd::Color(argb)};
g.FillRectangle(&brush, x, y, width, height);
}
/** @brief 把逻辑矩形对齐到整像素(**不内缩**,见头文件说明)。 */
PixelRect AlignToPixels(const gd::RectF& rect)
{
if (!(rect.Width > 0.0f) || !(rect.Height > 0.0f))
{
return PixelRect();
}
PixelRect out;
out.left = static_cast<int>(std::lround(rect.X));
out.top = static_cast<int>(std::lround(rect.Y));
out.right = static_cast<int>(std::lround(rect.X + rect.Width)) - 1;
out.bottom = static_cast<int>(std::lround(rect.Y + rect.Height)) - 1;
if (out.right < out.left)
{
out.right = out.left;
}
if (out.bottom < out.top)
{
out.bottom = out.top;
}
return out;
}
} // namespace
std::uint32_t UiDebugBoundsColor(int depth)
{
// 与主题无关的固定配色:调试层必须"永远显眼",不能跟随用户主题变成接近底色。
switch (depth)
{
case 0:
return 0xFF29B6F6u; // 青蓝:顶层组件区域
case 1:
return 0xFFE040FBu; // 品红:组件内部的功能分区
case 2:
return 0xFFFFB300u; // 琥珀:具体控件 / 行
case 3:
return 0xFF66BB6Au; // 绿:更深的子块(复选框、色块、✕ …)
default:
return 0xFFFF7043u; // 橙:文本墨迹矩形
}
}
void UiDebugPaintBoundsClipped(gd::Graphics& g, const gd::RectF& rect, const gd::RectF& clip,
int depth)
{
const int level = (depth < 0) ? 0 : depth;
// 先判断"值不值得画":元素太小时画上去就是糊住它本身。
if (std::min(rect.Width, rect.Height) <= static_cast<float>(kSkipThreshold))
{
return;
}
PixelRect pixels = AlignToPixels(rect);
if (!pixels.Valid())
{
return;
}
// ⚠️ 与裁剪矩形求交(见 `UiBoxTable::SetClip`):组件内部的元素可能落在地盘之外。
// 用 floor/ceil 而不是取整:裁剪是"能看见多少就画多少",宁可多一像素也不要把
// 紧贴边界的包角整个裁掉。
if (clip.Width > 0.0f && clip.Height > 0.0f)
{
PixelRect clipped;
clipped.left = std::max(pixels.left, static_cast<int>(std::floor(clip.X)));
clipped.top = std::max(pixels.top, static_cast<int>(std::floor(clip.Y)));
clipped.right =
std::min(pixels.right, static_cast<int>(std::ceil(clip.X + clip.Width)) - 1);
clipped.bottom =
std::min(pixels.bottom, static_cast<int>(std::ceil(clip.Y + clip.Height)) - 1);
if (!clipped.Valid())
{
return;
}
pixels = clipped;
}
const int width = pixels.Width();
const int height = pixels.Height();
const int shortSide = std::min(width, height);
const std::uint32_t color = UiDebugBoundsColor(level);
// 包角粗细随尺寸自适应("小元素变成粗描边"的修复点):
// 基准 4/3px 只在足够大的矩形上使用;短边越小越细,且不超过短边的 1/4。
const int baseThick = (level == 0) ? kAreaCornerThick : kNodeCornerThick;
int cornerThick = std::max(1, std::min(baseThick, shortSide / 10));
cornerThick = std::max(1, std::min(cornerThick, std::max(1, shortSide / 4)));
// 包角长度随短边变化:短边的 1/3,不超过上限,也不小于角本身。
int cornerLen = std::min(kCornerLenMax, std::max(1, shortSide / 3));
cornerLen = std::max(cornerLen, cornerThick);
const gd::GraphicsState state = g.Save();
// 描边必须锐利:关抗锯齿 + 关半像素偏移,矩形已对齐整像素 ⇒ 1px 就是 1px。
g.SetSmoothingMode(gd::SmoothingModeNone);
g.SetPixelOffsetMode(gd::PixelOffsetModeNone);
// ⚠️ 图元数量直接决定耗时(每个约 1µs),因此这里固定为**最多 8 次**:
// 4 条 1px 边 + 4 个加粗角块。
// 旧实现把每条边拆成"中段 1px + 两端各一段加粗"共 12 次,观感几乎相同
// (角本来就是"两条粗边的交点"),却多花 50% 时间。
// 短边很小的矩形只画 4 个角块(画满一圈会让复选框/色块看起来"被框住")。
const bool drawEdges = (shortSide > kEdgelessThreshold);
if (drawEdges)
{
FillPixels(g, pixels.left, pixels.top, width, kEdgeStroke, color); // 上
FillPixels(g, pixels.left, pixels.bottom, width, kEdgeStroke, color); // 下
FillPixels(g, pixels.left, pixels.top, kEdgeStroke, height, color); // 左
FillPixels(g, pixels.right, pixels.top, kEdgeStroke, height, color); // 右
}
// 四个加粗角块(粗细随尺寸自适应,见上)。
// * 大矩形:角块是 `cornerThick` 见方的方块,与四条 1px 边接成 `⌜ ⌝ ⌞ ⌟`;
// * 小方框(不画边):角块沿水平方向拉长到 `cornerLen`,让"四个角"更明显。
const int blockW = std::min(drawEdges ? cornerThick : cornerLen, width);
const int blockH = std::min(cornerThick, height);
FillPixels(g, pixels.left, pixels.top, blockW, blockH, color);
FillPixels(g, pixels.right - blockW + 1, pixels.top, blockW, blockH, color);
FillPixels(g, pixels.left, pixels.bottom - blockH + 1, blockW, blockH, color);
FillPixels(g, pixels.right - blockW + 1, pixels.bottom - blockH + 1, blockW, blockH, color);
g.Restore(state);
}
void UiDebugPaintBoxList(gd::Graphics& g, const UiBoxTable& boxes)
{
if (boxes.Empty())
{
return;
}
// ⚠️ 排序键是 `(layer, depth)`:
// * `layer` 保证"后收集的(更靠上的浮层)压住先收集的" —— 没有它,信号面板的行边界
// 会盖在右键弹出的二级菜单上面;
// * `depth` 保证同一层内先画父再画子,子元素的彩色包角不会被父的边压住。
std::vector<const UiBox*> ordered;
ordered.reserve(boxes.Count());
for (const UiBox& box : boxes.Items())
{
ordered.push_back(&box);
}
std::stable_sort(ordered.begin(), ordered.end(),
[](const UiBox* lhs, const UiBox* rhs)
{
if (lhs->layer != rhs->layer)
{
return lhs->layer < rhs->layer;
}
return lhs->depth < rhs->depth;
});
for (const UiBox* box : ordered)
{
// ⚠️ 遮挡:浮层(菜单 / 拾色器)是不透明的,它上面的内容不该出现下层组件的边框线。
// 仅靠 layer 排序只能决定"两个 box 重叠时谁压谁",不能阻止"先画的线仍然可见",
// 因此这里把**更高层**的遮挡矩形从裁剪区里排除。
gd::Region region;
bool occluded = false;
for (const UiBoxTable::Occluder& occluder : boxes.Occluders())
{
if (occluder.layer <= box->layer)
{
continue;
}
if (!occluded)
{
region.MakeInfinite();
occluded = true;
}
region.Exclude(occluder.rect);
}
if (!occluded)
{
UiDebugPaintBoundsClipped(g, box->rect, box->clip, box->depth);
continue;
}
// 已有裁剪矩形时先与它求交,再设进 Graphics(region 天然支持这种"挖洞"裁剪)。
if (box->clip.Width > 0.0f && box->clip.Height > 0.0f)
{
region.Intersect(box->clip);
}
const gd::GraphicsState state = g.Save();
g.SetClip(&region, gd::CombineModeIntersect);
UiDebugPaintBoundsClipped(g, box->rect, gd::RectF(), box->depth);
g.Restore(state);
}
}
} // namespace jdi