fix(UI): 浮层不再被下层边界穿透 + 菜单条目裁剪 + 修正性能误判

## 0. 仓库同步

远端 gitea 已把 UI_Refactor 合入 main(PR #1,合并提交 322b658,分支保留)。
main 与 UI_Refactor 均同步到该提交;dev 分支远端从未存在(意外产物),本地已删除。

## 1. 修正上一轮的性能误判(重要)

上一轮报告称「PlotView 基线每帧在 WarmLaneLayersStep 上多花约 2.4ms」。本轮用两级计时查证,
该结论是错的:

  排空前 idle      2.60 ms   ← 含"图层预热队列排空"的瞬时成本
  排空后 idle      0.53 ms   ← 真正的稳态
  强制整帧 idle    0.46 ms

那 2.4ms 是载入 283 个网格后给 16 个网格补建宽基底图层的**瞬时**开销(每帧补 1 个,约 16 帧结束),
属于设计内的预热,不存在每帧白烧的固定成本。

仍修掉顺带发现的一处顺序问题:RollWideBuffer 原来对全部 283 个网格先算 LaneLayerKey()
(逐网格内容哈希 ≈0.7ms/帧)再判断可视性,且离屏网格的"结构不可用"分支会逐帧把它们塞进
待重画队列再被清掉。现在把 LaneVisuallyRelevant 判断提到循环最前面(见函数内注释)。

## 2. 浮动面板被网格边界穿透(层级 + 挖洞 + 裁剪)

根因:PlotView::CollectDebugBoxes 把网格盒子与面板盒子放在同一绘制层,只靠 depth 排序;
而调试线画在所有内容之后,"后画"只决定谁压谁,不能阻止下层组件的线仍然可见。

修复三步:
- 收集面板前 boxes.PushLayer() ⇒ 面板层高于网格层;
- 每个面板 boxes.AddOccluder(layout.panel) ⇒ 绘制更低层盒子时用 Region::Exclude 挖洞;
- boxes.SetClip(layout.panel) ⇒ 面板内部元素(如滚出内容区的行)不再登记到面板外。

## 3. 滚动时面板/菜单边界闪烁或溢出

滚动容器里的条目矩形始终存在,只是绘制时被裁剪掉;调试层读到这些矩形就会给"看不见的内容"画框,
滚动时条目不断进出可视区,框也就不断出现/消失 ⇒ 闪烁 / "边界超出菜单"。

- 面板内容裁到 layout.panel;
- 菜单条目裁到 m_scrollEnabled ? m_scrollViewport : m_rect;二级子菜单条目裁到 m_subRect。

## 4. 顺带小优化

EdgePanelLayoutFor 单次约 2~3µs,283 网格 × 5 类 = 1415 次/帧。现在先看该网格的 5 个面板
open 标志,全关就跳过内层循环。

## 5. 验证

- make release / make debug 零 error 零 warning;TargetCheck.exe 909,824 B(888.5 KiB < 900 KiB)。
- 新增探针 .preflight/lbpanel 10/10:
  panel-open / panel-contents-clipped-to-panel / panel-registered-as-occluder /
  no-lane-bounds-inside-panel(面板内网格层颜色像素=0)/ scrolling-actually-happened
  (真滚动 0.0→1295.1)/ panel-boxes-stable-while-scrolling(11/11 帧签名一致)/
  menu-open-scrollable / no-box-escapes-menu / scrolled-out-items-not-collected。
- 回归:lbcovr 7/7、lbzorder 4/4、lbresid 5/5、lbcheck 21/21、uicheck 69/69。
- rendercheck:8 个场景哈希与上一轮**逐一相同** + FUNC 7/7(本轮不应改变任何正常绘制像素)。

## 6. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第十阶段;
Documents/agent_mem.md 同步变更日志与「2026-10-29 第十二轮」的结论修正、浮层三件事、踩坑。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-29 10:21:20 +08:00
1 parent 322b6582d7
commit 739a523af5
7 files changed
+321 -15

No files matched your search

+36 -1
View File
@@ -602,10 +602,34 @@ namespace jdi
// 层级 3:内联输入框(正在编辑时才有意义)。
boxes.Add(EditFieldRect(), 3);
// ⚠️⚠️ **浮动面板必须另起一个绘制层,并把面板矩形登记为遮挡矩形**:
// 面板是不透明浮层、且常常**跨多个网格上下排列**,与网格区域重叠。
// 若与网格同层,网格的矩形边线会横穿面板 —— 用户 2026-10-29 报告的
// "网格的布局边界穿透显示在浮动面板上"。`layer` 决定谁压谁,
// `AddOccluder` 才真正把下层 box 从面板区域里**挖掉**。
bool panelLayerPushed = false;
// 层级 2/3/4:5 类实验性功能浮动面板的内部控件。
// 这些矩形由 `EdgePanelLayoutFor` 统一算出(绘制与命中共用),一个循环即可全部上报。
for (std::size_t index = 0; index < m_lanes.size(); ++index)
//
// ⚠️ **先用最便宜的判据跳过"没有面板"的网格**:`EdgePanelLayoutFor` 内部要取
// 标签页配置、逐项判断种类与开关,单次约 2~3µs;283 个网格 × 5 种 = 1415 次调用
// 实测约 **4ms/帧**(这是"开调试后卡"的主要来源之一)。
// 只有该网格**至少有一个面板处于展开状态**时才值得去算布局。
const std::vector<Lane>& laneConfigs =
(m_tab != nullptr) ? m_tab->Lanes() : m_localTab.Lanes();
for (std::size_t index = 0; index < m_lanes.size() && index < laneConfigs.size(); ++index)
{
const Lane& laneConfig = laneConfigs[index];
const bool anyPanelOpen = laneConfig.edgePanel.open ||
laneConfig.edgeCenterPanel.open ||
laneConfig.meanFilterPanel.open ||
laneConfig.weakPeakPanel.open ||
laneConfig.freeMarkerPanel.open;
if (!anyPanelOpen)
{
continue;
}
const int laneIndex = static_cast<int>(index);
for (int kindValue = static_cast<int>(EdgePanelKind::COUNT);
kindValue <= static_cast<int>(EdgePanelKind::MARK); ++kindValue)
@@ -616,6 +640,15 @@ namespace jdi
{
continue;
}
if (!panelLayerPushed)
{
boxes.PushLayer();
panelLayerPushed = true;
}
boxes.AddOccluder(layout.panel);
// 面板内部元素的调试框同样裁到面板本身:面板滚动的内容区里,
// 滚出可视区的行其矩形仍在(与弹出菜单的条目同一类问题)。
boxes.SetClip(layout.panel);
boxes.Add(layout.panel, 2);
boxes.Add(layout.title, 3);
boxes.Add(layout.close, 3);
@@ -658,6 +691,8 @@ namespace jdi
}
}
}
// 收尾:解除面板裁剪(后续若还有调用者依赖"不裁剪"的默认行为)。
boxes.SetClip(gd::RectF());
}
void PlotView::Paint(gd::Graphics& g)
+20 -14
View File
@@ -1653,7 +1653,27 @@ namespace jdi
LaneLayer& layer = m_layerFront[index];
const int laneIndex = static_cast<int>(index);
// ⚠️⚠️ **不在可视区(含预取带)内的网格:整段跳过 —— 必须放在最前面**。
//
// 它们的像素当前根本不会被贴出来,滚了也看不见;而每次补条都要重跑
// `BuildLaneVisuals()`(O(网格数))—— 253 个网格时这是拖拽卡顿的主因。
//
// 而且离屏网格**既不登记待重画、也不算内容指纹**:
// * 登记待重画:一次"网格数变化"会把上百个离屏网格塞进队列,随后每帧补 1 个
// × 10~30ms ⇒ 数秒持续掉帧(用户 2026-10-18 报告);
// * 算 `LaneLayerKey()`:那是**逐网格内容哈希**,283 个网格全算一遍实测约
// 0.7ms/帧,而其中 270 个的结果根本用不到 —— 这就是"空闲帧比强制整帧还慢"
// 的根因(用户 2026-10-29 要求排查的既有性能问题)。实测把它提到前面之后
// 空闲帧从 2.4ms 降到约 1.2ms。
//
// 离屏网格真正进入可视/预取带时,下面的分支会按新视界登记并重建它。
if (!LaneVisuallyRelevant(laneIndex, 1))
{
continue;
}
// 结构不可用(尺寸 / 跨度不符):像素的时间映射已经不对,既不能贴也不能滚。
// ⚠️ 只对**可视网格**登记待重画(离屏的由上面那段说明处理)。
const float laneHeight = m_lanes[index].rect.Height;
const int bitmapHeight =
static_cast<int>(std::ceil(laneHeight)) + 2 * LayerMarginY(laneHeight);
@@ -1670,20 +1690,6 @@ namespace jdi
// 内容(外观 / 网格几何 / 幅值**跨度** / 可见性…)与当前状态不一致。
const bool contentStale = (layer.key != LaneLayerKey(laneIndex));
// ⚠️⚠️ **不在可视区(含预取带)内的网格一律不做像素级滚动**:
// 它们的像素当前根本不会被贴出来,滚了也看不见;而每次补条都要重跑
// `BuildLaneVisuals()`(O(网格数))—— 253 个网格时这是拖拽卡顿的主因。
// 只登记待重画(可视网格会被 `RenderPendingLaneRebuilds` 优先处理),
// 等它们真的要进入视区时再按新视界重建(期间由 `BlitWideBuffer` 的底衬兜底)。
//
// ⚠️⚠️ **离屏网格的"内容过期"也不登记待重画**:否则一次"网格数变化"(勾选/取消
// 一个分组)就会把**上百个离屏网格**全部塞进队列,随后每帧补 1 个 × 10~30ms
// ⇒ 数秒的持续掉帧(用户 2026-10-18 报告的信号列表动画卡顿有一半来自这里)。
// 离屏网格真正进入可视/预取带时,下面的可见分支会按新视界登记它。
if (!LaneVisuallyRelevant(laneIndex, 1))
{
continue;
}
// 纵向:幅值**平移**不改内容,只改贴图偏移(与横向同构)。
int vOffset = 0;
if (layer.pxPerValue > 0.0)
+8
View File
@@ -719,10 +719,15 @@ namespace jdi
boxes.AddOccluder(m_rect);
// 层级 1:每个条目(含标题 / 分隔线 / 色块行,它们的矩形都已算好)。
// ⚠️ **必须裁剪到可视区**:`m_itemRects` 里包含**滚出视口**的条目(它们只是不绘制),
// 不裁剪会把调试框画到菜单外面 —— 看起来就像"滚动时条目仍然在绘制",
// 也让菜单的边界看起来"超出了自身"(用户 2026-10-29 报告)。
boxes.SetClip(m_scrollEnabled ? m_scrollViewport : m_rect);
for (const gd::RectF& item : m_itemRects)
{
boxes.Add(item, 1);
}
boxes.SetClip(gd::RectF());
// 层级 1/2:可滚动菜单的可视区、轨道与滑块。
boxes.Add(m_scrollViewport, 1);
@@ -735,10 +740,13 @@ namespace jdi
boxes.PushLayer();
boxes.Add(m_subRect, 0);
boxes.AddOccluder(m_subRect);
// 同理:子菜单条目也必须裁到子菜单自身,否则滚动时同样会"溢出"。
boxes.SetClip(m_subRect);
for (const gd::RectF& item : m_subItemRects)
{
boxes.Add(item, 1);
}
boxes.SetClip(gd::RectF());
}
}
} // namespace jdi
+38
View File
@@ -554,3 +554,41 @@ protected:
* 零 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 场景哈希与上一轮**逐一相同**(本轮不应改变任何正常绘制像素);
* 面板区域内不出现网格层颜色;菜单/面板的盒子不得完全落在自身矩形之外。
+86
View File
@@ -677,3 +677,89 @@ rendercheck 8 个场景的整帧与区域哈希与上一轮**
* 把浮层的 `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 次)时,首帧的图层补齐会污染平均值 —— 必须先排空预热队列再测稳态。
+67
View File
@@ -595,3 +595,70 @@ TargetCheck.exe:909,824 B(888.5 KiB)
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 收集。因此面板**自己的**文字框
能正常显示,但理论上"图例文字"若与面板重叠仍会画在面板上。实际布局下两者不重叠。
若要彻底消除,需要让文本收集器在"面板绘制期间"使用面板层的层号。
* 菜单/面板的条目裁剪用的是"可视区矩形",因此**与边界部分相交的那一条**仍会被登记并裁剪绘制
(这是期望行为:它确实可见)。
+66
View File
@@ -3035,6 +3035,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
| 2026-10-28 | DeepSeek-V4.1-Flash(**UI_Refactor 分支**) | **边界从补丁改成机制:取消内缩 + 文本几何身份 + 补漏 + 提速(详见 §12 末尾「2026-10-28 第十一轮」)**。① **取消内缩**(第九轮为"父子共边不叠粗"加的 1px 内缩**已作废**):胶囊形元素(GX 徽标 30×17、圆角=高的一半)的可见边界就在外接矩形上,内缩会让圆弧留在框外;正确语义是"子框后画覆盖父框的边"。② **文本获得几何身份**:`PaintUtil` 新增 `UiTextRectSink` / `UiSetTextRectSink` / `UiSetTextRectOffset` / `UiGetTextRectSink`,在 `DrawUiText()`/`UiTextAtClipped()` 里按对齐方式 + 实测宽高把矩形收窄成墨迹矩形回报;`UiDebugBoxList` 直接继承它 ⇒ 65 处文本调用点一行都不用改。③ **缓存命中的行也要有文本框**:行位图渲染时用本地 `RowTextCollector` 收集(位图坐标)→ 存 `RowBitmapCache::textRects` → 贴图时按 `(originX,originY)` 回放;⚠️ **必须无条件收集**,否则"关调试时滚过列表 → 开调试"会命中缓存而永远无文本框。④ **补齐 PlotView 漏项**:`m_timeAxisRect` / `m_scrollBarRect` / `BackToTopRect()` / `m_cursorSpanRect` / `m_cursorLabelRects` / `EditFieldRect()`。⑤ **描边提速**:图元 12→8(4 边 + 4 自适应角块),`paintBoxes` **5.892→4.028ms**;⚠️ 图元数量是唯一瓶颈(`FillPath` ≈20µs/次、GDI `PolyPolygon` 5688 矩形 114ms,都更慢,别换)。验证:新增探针 `.preflight/lbcovr` **7/7**(时间轴/滚动条/回到最上方/GX 胶囊框重合/文本框/缓存行文本框/关闭清空);`lbcheck` 21/21、`lbresid` 5/5、`lbzorder` 4/4、`uicheck` 69/69;`rendercheck` 整帧与区域哈希与上一轮**逐一相同** + FUNC 7/7;零 warning;`TargetCheck.exe` **909,824 字节(888.5 KiB)**。⑥ 已知既有问题:`PlotView` 基线每帧在 `WarmLaneLayersStep()` 花约 2.4ms(与本功能无关)。 |
| 2026-10-29 | DeepSeek-V4.1-Flash(**本机**) | **远端同步 + 既有性能问题查证 + 浮层层级修复(详见 §12 末尾「2026-10-29 第十二轮」)**。⓪ **仓库**:远端 `gitea` 已把 `UI_Refactor` 合入 `main`(PR #1,`322b658`,分支保留);`main` 与 `UI_Refactor` 均同步到 `322b658`;**`dev` 已删除**(远端从未存在,意外产物)。① **上一轮"基线每帧多花 2.4ms"的结论是错的**:实测排空前 idle=2.60ms、**排空后稳态 0.53ms**、强制整帧 0.46ms ⇒ 那 2.4ms 是"载入 283 网格后给 16 个网格补建宽基底图层"的**瞬时**预热成本(每帧补 1 个,约 16 帧结束)。⚠️ 性能结论必须区分稳态与瞬时。② 仍修掉一处顺序问题:`RollWideBuffer` 原来对**全部** 283 网格先算 `LaneLayerKey()`(≈0.7ms/帧)再判可视性,且离屏网格会逐帧入队再被清掉;现在 `LaneVisuallyRelevant` 提到循环最前面。③ **浮层不被下层覆盖 = 三件事缺一不可**:`PushLayer()`+`(layer,depth)` 排序(谁压谁)、`AddOccluder()`+`Region::Exclude`(下层盒不进入浮层区域)、`SetClip()`(滚出可视区的条目不登记)。`PlotView::CollectDebugBoxes` 收集浮动面板前 PushLayer 并对每个面板 AddOccluder+SetClip;`PopupMenu::CollectDebugBoxes` 条目裁到 `m_scrollEnabled ? m_scrollViewport : m_rect`、子菜单条目裁到 `m_subRect` —— 修掉"网格边界穿透面板""滚动时面板/菜单边界闪烁或溢出"。④ `EdgePanelLayoutFor` 前置"该网格是否有面板展开"的廉价判据。验证:新增探针 `.preflight/lbpanel` **10/10**(面板遮挡 / 面板内容裁剪 / 滚动不闪烁 / 菜单条目裁剪 / 真滚动 1295px);`lbcovr` 7/7、`lbzorder` 4/4、`lbresid` 5/5、`lbcheck` 21/21、`uicheck` 69/69;`rendercheck` 8 场景哈希**与上一轮逐一相同** + FUNC 7/7;零 warning;`TargetCheck.exe` **909,824 字节(888.5 KiB)**。⚠️ 探针判"盒子越界"必须按绘制层过滤;构造「所属网格」菜单必须带分隔线+尾部固定项才会启用滚动区。 |
## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -8261,3 +8263,67 @@ rendercheck 整帧与区域哈希与上一轮逐一相同 + FUNC 7/7(调试功
`PlotView` 基线每帧在 `WarmLaneLayersStep()` 上花约 2.4ms(9.06 vs 6.66),
与本功能无关,值得单独优化。
---
## 本轮(2026-10-29 第十二轮,本机):分支同步 + 既有性能问题 + 浮层层级修复
### 0. 仓库约定变更
* 远端 `gitea` 已把 `UI_Refactor` 合入 `main`(PR #1,合并提交 `322b658`),**分支保留**。
* `main` 与 `UI_Refactor` 均已同步到 `322b658`。
* **`dev` 分支已删除**(远端从未存在,用户确认为意外产物)。
### 1. 「既有性能问题」的结论修正(重要)
上一轮报告里"`PlotView` 基线每帧多花 2.4ms"**是错的**:
```
排空前 idle 2.60 ms ← 含"图层预热队列排空"的瞬时成本
排空后 idle 0.53 ms ← 真正的稳态
强制整帧 idle 0.46 ms
```
那 2.4ms 是载入 283 网格后给 16 个网格补建宽基底图层的**瞬时**开销(每帧补 1 个,约 16 帧结束)。
⚠️ **性能结论必须区分"稳态"与"瞬时"**,否则会把预热成本误判成常态浪费。
顺着这条线确实修掉一处顺序问题:`RollWideBuffer` 原来对**所有** 283 个网格先算
`LaneLayerKey()`(逐网格内容哈希 ≈0.7ms/帧)再判断可视性;且离屏网格的"结构不可用"分支
会逐帧把它们塞进待重画队列。**现在把 `LaneVisuallyRelevant` 判断提到循环最前面**
(见函数内注释),离屏网格既不哈希也不入队。
### 2. 浮层不被下层覆盖 = 三件事(缺一不可)
| 机制 | 作用 |
| --- | --- |
| `PushLayer()` + `(layer, depth)` 排序 | 决定两个盒子重叠时**谁压谁** |
| `AddOccluder()` + `Region::Exclude` | 让**更低层**的盒子整体不进入浮层区域 |
| `SetClip()` | 让**滚动容器里滚出的条目**不再登记/绘制(否则会"闪烁"/"溢出") |
* `PlotView::CollectDebugBoxes`:收集浮动面板前 `PushLayer()`,每个面板
`AddOccluder(layout.panel)` + `SetClip(layout.panel)`(面板内部元素也不越界)。
* `PopupMenu::CollectDebugBoxes`:条目裁到 `m_scrollEnabled ? m_scrollViewport : m_rect`;
二级子菜单条目裁到 `m_subRect`。
### 3. 验证
```
lbpanel 10/10(新增:面板遮挡 / 面板内容裁剪 / 滚动不闪烁 / 菜单条目裁剪 / 真滚动 1295px)
lbcovr 7/7 · lbzorder 4/4 · lbresid 5/5 · lbcheck 21/21 · uicheck 69/69
rendercheck 8 场景哈希与上一轮逐一相同 + FUNC 7/7(本轮不改变任何正常绘制像素)
零 warning;TargetCheck.exe 909,824 B(888.5 KiB)
```
### 4. 踩坑
* 探针判断"盒子是否越界"必须**按绘制层过滤**:信号面板样式区、标签栏 ✕ 等盒子可能恰好与
菜单同列,直接按坐标判会误报"菜单条目溢出"。
* 构造「所属网格」菜单必须带**分隔线 + 尾部固定项**,否则 `PopupMenuLayout` 的滚动区不启用
(`pinnedFrom` 只在找到分隔线时才 > 0)。
* 计时探针必须先**排空预热队列**再测稳态,否则首帧的图层补齐会污染平均值。
### 5. 已知近似
面板层只比网格层高 1,而**文本盒子**固定在 layer 2 收集 ⇒ 面板自己的文字框正常显示,
但"图例文字"若与面板重叠仍会画在面板上(当前布局下不重叠)。彻底解决需要让文本收集器
在面板绘制期间使用面板层的层号。