diff --git a/Components/PlotView/Src/PlotViewPaint.cpp b/Components/PlotView/Src/PlotViewPaint.cpp index 8dd2c42..4fa3403 100644 --- a/Components/PlotView/Src/PlotViewPaint.cpp +++ b/Components/PlotView/Src/PlotViewPaint.cpp @@ -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& 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(index); for (int kindValue = static_cast(EdgePanelKind::COUNT); kindValue <= static_cast(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) diff --git a/Components/PlotView/Src/PlotViewRaster.cpp b/Components/PlotView/Src/PlotViewRaster.cpp index 9693e8d..7564b98 100644 --- a/Components/PlotView/Src/PlotViewRaster.cpp +++ b/Components/PlotView/Src/PlotViewRaster.cpp @@ -1653,7 +1653,27 @@ namespace jdi LaneLayer& layer = m_layerFront[index]; const int laneIndex = static_cast(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(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) diff --git a/Components/Shared/Src/PopupMenu.cpp b/Components/Shared/Src/PopupMenu.cpp index 9e2af09..92b0b0c 100644 --- a/Components/Shared/Src/PopupMenu.cpp +++ b/Components/Shared/Src/PopupMenu.cpp @@ -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 diff --git a/Documents/UI_Refactor_Plan.md b/Documents/UI_Refactor_Plan.md index f65cb65..da633a1 100644 --- a/Documents/UI_Refactor_Plan.md +++ b/Documents/UI_Refactor_Plan.md @@ -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 场景哈希与上一轮**逐一相同**(本轮不应改变任何正常绘制像素); +* 面板区域内不出现网格层颜色;菜单/面板的盒子不得完全落在自身矩形之外。 diff --git a/Documents/UI_Refactor_Progress.md b/Documents/UI_Refactor_Progress.md index 9db4197..df63a9c 100644 --- a/Documents/UI_Refactor_Progress.md +++ b/Documents/UI_Refactor_Progress.md @@ -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 次)时,首帧的图层补齐会污染平均值 —— 必须先排空预热队列再测稳态。 diff --git a/Documents/UI_Refactor_Report.md b/Documents/UI_Refactor_Report.md index 4a10518..22cf97a 100644 --- a/Documents/UI_Refactor_Report.md +++ b/Documents/UI_Refactor_Report.md @@ -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 收集。因此面板**自己的**文字框 + 能正常显示,但理论上"图例文字"若与面板重叠仍会画在面板上。实际布局下两者不重叠。 + 若要彻底消除,需要让文本收集器在"面板绘制期间"使用面板层的层号。 +* 菜单/面板的条目裁剪用的是"可视区矩形",因此**与边界部分相交的那一条**仍会被登记并裁剪绘制 + (这是期望行为:它确实可见)。 diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 32d3048..a9a9139 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -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 收集 ⇒ 面板自己的文字框正常显示, +但"图例文字"若与面板重叠仍会画在面板上(当前布局下不重叠)。彻底解决需要让文本收集器 +在面板绘制期间使用面板层的层号。