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

+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)