288 信号卡顿根治 + 中心轴面板重做 + 标签页默认状态

一、卡顿真因(实测 29.8ms/帧 → 6.6ms/帧)
1. 中心轴面板高度没有上限:288 个信号时面板高 9996px(绘图区只有 1141px)
   ⇒ PanelRectInsideLayer 恒为假 ⇒ 面板像素缓存一次都存不进去 ⇒ 每帧全量重画。
2. PanelGlobalKey 混入鼠标驱动状态(m_hoverSignal / m_hoverLane / m_hoverCursor /
   CursorTime / EdgeRangeMin/Max)⇒ 鼠标一动键就变 ⇒ 所有面板每帧重画;
   PanelSlotKey 里还会为每个信号调一次 EdgeCenterCountsFor(单帧 288 次查询)。
3. 缓存命中路径的投影(与面板同大的半透明抗锯齿圆角矩形)每帧重画,GDI+ 实测 > 20ms。

修法:
* 新增 PlotView::InputIsInteractive()(拖拽中 / 鼠标 140ms 内移动过)。交互期间冻结
  全部鼠标驱动项与统计数字、跳过投影、统计预算降到 8 万(静止时 40 万)。
  m_meanRowHoverLane/Ref 例外 —— 它直接改面板像素且只在进出某行时变化。
* kEdgeScanPairsPerFrame 由 1,200,000 降为 400,000。
* 中心轴面板重做为可滚动列表 ⇒ 面板高度被绘图区夹住、缓存可用、只画可见行。

二、「中心轴边沿计数」面板重做(与「均值滤波」同风格)
条目栏为信号色叠底的可折叠栏 + 右端复选框;栏下同一行显示「上升沿 / 下降沿」;
再下面两行滑块(均值点数 / 迭代次数,标签不带 X/N),数值可点击输入上限、
右侧复位按钮;整个面板可滚动;未勾选时条目栏不可展开。
新增 Lane::edgeCenterScroll + SetLaneEdgeCenterScroll;交互复用 MEAN_* 拖拽模式 +
m_meanDragCenter / m_editMeanCenter 标志分派;光标形状与均值滤波一致。

三、标签页
* 唯一标签页点 ✕ 不再弹警告:直接 erase 后 NewTab(true),名字必然为「视图 1」。
* 新建标签页(含手动新建)默认:组全部折起、无选中信号(清空全局 Selected)、
  样式区无目标。

四、验证
.preflight/bugcheck.exe 59/59(A1 空闲 0.7ms、A2 平移 2.5ms、A3 双光标 2.4ms、
A4 自由标记 5.2ms、A5 中心轴拖动 6.6ms;B3 真实 WM_TIMER+WM_PAINT 路径;
B5 快速连点与动画反向;C 中心轴面板交互;D 标签页默认状态)
panelcheck 71/71、markercheck 13/13、ringcheck 6/6
make release / make debug 零 warning
This commit is contained in:
DeepSeek-V4.1-Flash committed 2026-11-02 03:33:28 +08:00
1 parent 8271879ede
commit 944c074ab3
11 files changed
+618 -122

No files matched your search

+142 -31
View File
@@ -815,8 +815,18 @@ namespace jdi
// 就会在同一系统 tick 内连画多帧而**完全不推进**统计 —— 用户报告
// “按住横向光标不动时统计卡住不更新”“回车确认参数后不移动鼠标就停在部分值”。
++m_edgeScanFrame;
m_edgeScanBudget = kEdgeScanPairsPerFrame;
// ⚠️⚠️ **交互帧必须用小预算**(用户 2026-11-02 报告):288 个信号合并到同一网格、
// 开启中心轴边沿计数后,拖动横向光标每帧要 30ms,界面完全跟不上手。中心轴每个采样对
// 还要按「均值点数 × 迭代次数」算一次均值窗口 ⇒ 120 万对 ≈ 3 千万次浮点运算/帧。
// 现在:正在拖拽、或鼠标刚刚移动过(`kEdgeScanInteractiveIdleMs` 内)⇒ 用交互预算
// (1/5),优先保证界面跟手;鼠标停下约 0.14s 后自动恢复高预算、把统计快速补齐。
// ⚠️⚠️ **交互帧必须用小预算**(用户 2026-11-02 报告):288 个信号合并到同一网格、
// 开启中心轴边沿计数后,拖动横向光标每帧要 30ms,界面完全跟不上手。中心轴每个采样对
// 还要按「均值点数 × 迭代次数」算一次均值窗口 ⇒ 120 万对 ≈ 3 千万次浮点运算/帧。
// 现在:正在拖拽、或鼠标刚刚移动过(`kEdgeScanInteractiveIdleMs` 内)⇒ 用交互预算
// (1/5),优先保证界面跟手;鼠标停下约 0.14s 后自动恢复高预算、把统计快速补齐。
m_edgeScanBudget =
InputIsInteractive() ? kEdgeScanPairsPerFrameInteractive : kEdgeScanPairsPerFrame;
// 双缓冲:清掉上一帧钉住的曲线条目(本帧会按需重新钉住),并清理已被更新条目覆盖 /
// 超出内存预算的旧条目。此刻没有任何条目被钉住,回收是安全的。
m_curvePins.clear();
@@ -3483,33 +3493,89 @@ namespace jdi
mix(static_cast<std::uint64_t>(m_laneDragFrom + 2));
mix(static_cast<std::uint64_t>(m_laneDragTarget + 2));
mix(static_cast<std::uint64_t>(m_laneDragMouseY + 4096));
mix(static_cast<std::uint64_t>(m_hoverLane + 2));
mix(m_cursorHover ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(m_hoverCursor + 2));
// ⚠️⚠️ **交互期间冻结全部“随鼠标变化”的项**(用户 2026-11-02 报告“移动鼠标/拖动光标卡顿”):
// 本键是**全部**面板像素缓存的总开关(`panelGlobalSame`)—— 只要它每帧变,所有面板都会
// 重画。鼠标在绘图区移动时 `m_hoverSignal`(甚至 `m_hoverLane` / `m_hoverCursor`)会不停
// 变化,于是“移动鼠标 → 每帧重画全部面板(288 个信号的中心轴面板 22ms)”成了死循环。
// 这些叠加层都画在**面板之下**、面板本身不透明且贴回时只贴圆角形状之内,因此交互期间
// 直接复用面板像素是安全的;鼠标停下后键自动恢复,面板重画一次即可。
const bool interactivePanelKey = InputIsInteractive();
if (interactivePanelKey)
{
mix(0x9E3779B9ull);
mix(0x9E3779B9ull);
}
else
{
mix(static_cast<std::uint64_t>(m_hoverLane + 2));
mix(m_cursorHover ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(m_hoverCursor + 2));
}
mix(static_cast<std::uint64_t>(m_activeCursor));
mix(m_dragMoved ? 1ull : 0ull);
mixRect(m_rubberRect);
mixRef(m_hoverSignal);
mixRef(m_hoverLegend);
if (interactivePanelKey)
{
mix(0x9E3779B9ull);
mix(0x9E3779B9ull);
}
else
{
mixRef(m_hoverSignal);
mixRef(m_hoverLegend);
}
mixRef(m_dragSignal);
mixRef(SelectedRef());
mix(static_cast<std::uint64_t>(m_edgePanelDragLane + 2));
mix(static_cast<std::uint64_t>(m_edgePanelDragKind));
// ⚠️ 「均值滤波 / 中心轴边沿计数」**条目栏的悬停高亮**必须**始终**参与全局键:
// 它直接改变面板像素(行头叠一层强调色),而且只在鼠标真正进入/离开某一行时才变化。
// 若跟着上面那些项一起在交互期间冻结,拖动鼠标到条目栏上高亮就不会出现(用户要求
// “鼠标处于条目栏上方时对应曲线/条目需要高亮提示”)。
mix(static_cast<std::uint64_t>(m_meanRowHoverLane + 2));
mixRef(m_meanRowHoverRef);
// 时间光标与统计区间:面板标题的时间后缀、以及「中心轴」行的上升/下降沿数字都依赖它们。
// ⚠️⚠️ **交互期间必须用固定占位值**:拖动光标时时间每帧都在变,键一变就要重画全部面板。
mix(static_cast<std::uint64_t>(Tab().CursorMode()));
mixDouble(Tab().CursorTime());
mixDouble(Tab().CursorTimeAlt());
if (interactivePanelKey)
{
mixDouble(0.0);
mixDouble(0.0);
}
else
{
mixDouble(Tab().CursorTime());
mixDouble(Tab().CursorTimeAlt());
}
mix(EdgeRangeActive() ? 1ull : 0ull);
mixDouble(EdgeRangeMin());
mixDouble(EdgeRangeMax());
if (interactivePanelKey)
{
// 统计区间来自选区时不随鼠标变化;但若它来自光标就会每帧变 —— 一并冻结。
mixDouble(0.0);
mixDouble(0.0);
}
else
{
mixDouble(EdgeRangeMin());
mixDouble(EdgeRangeMax());
}
// 跟随鼠标的叠加层(悬停光晕 / 图例半高亮 / 信号拖拽虚影 / 提示动画)位置随时在变:
// 只要它存在,就把所有面板缓存作废,避免它的残影被一起缓存进面板像素。
//
// ⚠️⚠️ **交互期间必须忽略它**(用户 2026-11-02 报告“移动鼠标/拖动光标卡顿”):
// 鼠标在绘图区里移动时 `m_hoverSignal` 常常有效(悬停在某条曲线上),这里就会把
// **鼠标坐标**混进全局键 ⇒ 键每帧都变 ⇒ 全部面板每帧重画。288 个信号的中心轴面板
// 一块就要 ~23ms(实测:面板开 27.6ms / 面板关 4.3ms)。
// 而悬停光晕、拖拽虚影都画在**面板之下**,面板本身不透明且贴回时只贴圆角形状之内
// (见下面的 `CreateRoundRectRgn` 裁剪 + 每帧重画投影),因此交互期间直接复用面板像素
// 是安全的;鼠标停下后键自动恢复,面板重画一次即可。
const bool mouseDrivenOverlay =
m_hoverSignal.IsValid() || m_hoverLegend.IsValid() ||
m_dragMode == PlotDragMode::MOVE_SIGNAL || m_dragMode == PlotDragMode::RUBBER_SELECT ||
HintNeedsAnimation();
!interactivePanelKey &&
(m_hoverSignal.IsValid() || m_hoverLegend.IsValid() ||
m_dragMode == PlotDragMode::MOVE_SIGNAL || m_dragMode == PlotDragMode::RUBBER_SELECT ||
HintNeedsAnimation());
mix(mouseDrivenOverlay ? 1ull : 0ull);
if (mouseDrivenOverlay)
{
@@ -3717,7 +3783,17 @@ namespace jdi
// 才会显示出早已算完的正确结果。
// ⇒ 把该面板当前要显示的 (上升沿, 下降沿, 是否扫完) 一并 mix 进去:推进会让键变化 ⇒
// 面板重绘 ⇒ 显示随进度更新,直到扫完(扫完后数字不再变,重新命中缓存)。
if (kind == EdgePanelKind::COUNT)
//
// ⚠️⚠️ **但交互期间必须跳过这一整段**(用户 2026-11-02 报告“拖动横向光标卡顿”):
// * 计算这一段本身要为**每个信号**调一次 `EdgeCountsFor` / `EdgeCenterCountsFor`
// (含区间边界块扫描)—— 288 个信号时单帧 288 次查询;
// * 而且中间数字每帧都在变 ⇒ 键每帧都变 ⇒ 面板像素缓存**永远不命中**,
// 于是每帧都要把整块面板(288 行 × 十来个 GDI+ 原语)重画一遍 ⇒ 实测 30ms/帧。
// 拖动/鼠标移动期间数字本来就在飞速重算,**显示中间值毫无意义**;此时让键保持稳定
// (用一个固定常量占位)⇒ 面板直接贴上一帧像素 ⇒ 真正跟手。鼠标停下
// `kEdgeScanInteractiveIdleMs` 之后恢复完整键 ⇒ 面板重画并显示最终数字。
const bool interactiveKey = InputIsInteractive();
if (kind == EdgePanelKind::COUNT && !interactiveKey)
{
for (const SignalRef& ref : lane.signals)
{
@@ -3737,7 +3813,7 @@ namespace jdi
mix(complete ? 1ull : 0ull);
}
}
else if (kind == EdgePanelKind::CENTER)
else if (kind == EdgePanelKind::CENTER && !interactiveKey)
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
@@ -4330,6 +4406,9 @@ namespace jdi
(m_editTarget == PlotEditTarget::NONE) && EnsurePanelCache();
const bool panelGlobalSame =
panelCacheReady && m_panelValid && panelGlobalKey == m_panelGlobalKey;
// 交互帧(拖动 / 鼠标刚移动)的标志:缓存命中的贴图分支据此跳过昂贵的投影重画
//(见下面 `UiFillRoundRect` 的说明)。
const bool interactiveFrame = InputIsInteractive();
// ⚠️ **按层序从下往上绘制**(与命中测试、像素缓存的层叠顺序完全一致):
// `m_panelOrder` 队尾 = 最上层 = 最后绘制。
SyncPanelOrder();
@@ -4399,12 +4478,13 @@ namespace jdi
const bool meanFilter = (kind == EdgePanelKind::MEAN);
const bool weakPeak = (kind == EdgePanelKind::PEAK);
const bool freeMarker = (kind == EdgePanelKind::MARK);
// ⚠️ `itemized` 只表示**旧的逐条目排布**(中心轴 / 弱峰调整)。
// 「均值滤波」自 2026-10-30 起有自己的**可滚动分支**(`if (meanFilter)` 内),
// 若它仍被算作 `itemized`,旧分支会**再画一遍**每行的名称 + 复选框 ——
// 且旧分支**没有任何裁切**,滚动时这些元素就会画到标题栏上
//(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
const bool itemized = (centerAxis || weakPeak);
// ⚠️ `itemized` 只表示**旧的逐条目排布**(现在只剩「弱峰调整」)。
// 「均值滤波」自 2026-10-30 起、**「中心轴边沿计数」自 2026-11-02 起**都有自己的
// **可滚动分支**(`if (listStyle)` 内)。若它们仍被算作 `itemized`,旧分支会
// **再画一遍**每行的名称 + 复选框 —— 且旧分支**没有任何裁切**,滚动时这些元素就会
// 画到标题栏上(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
const bool listStyle = (meanFilter || centerAxis);
const bool itemized = weakPeak;
const bool hasButton = (meanFilter || weakPeak);
{
if (index >= count)
@@ -4476,7 +4556,13 @@ namespace jdi
// (否则平移会重画全部面板),于是整块贴回时就把旧波形"复印"了一圈。
// 现在:① 先把**投影**按本帧重新画一遍(它在圆角形状之外,不能靠缓存);
// ② 用圆角区域裁剪后只 BitBlt 面板本体(圆角之外仍是本帧场景)。
if (!m_fastFrame)
// ⚠️⚠️ **投影不能每帧重画**(用户 2026-11-02 报告“移动鼠标/拖动光标卡顿”):
// 它是一块面积与面板同大的**半透明抗锯齿圆角矩形**(中心轴面板高近 950px、
// 宽 400px),GDI+ 软件渲染这一块实测要 **20ms 以上** —— 而缓存命中的
// 贴图本身只要零点几毫秒。交互期间(拖动 / 鼠标刚移动)直接跳过投影:
// 面板圆角之外保留上一帧的像素(此时那块区域本来也是静止的场景),
// 鼠标停下后键恢复、面板重画一次即补上投影。
if (!m_fastFrame && !interactiveFrame)
{
UiFillRoundRect(g, gd::RectF(panel.X + 1.0f * m_scale,
panel.Y + 2.0f * m_scale, panel.Width,
@@ -4852,7 +4938,7 @@ namespace jdi
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
// (「均值点数」/「迭代次数」);内容超高时可滚动(滚动条在内容区右侧)。
if (meanFilter)
if (listStyle)
{
const int meanSave = g.Save();
// 内容必须裁到内容区:滚动时行会越出可视范围。
@@ -4938,6 +5024,10 @@ namespace jdi
{
extendRow(layout.rowIterResets[rowIndex]);
}
if (rowIndex < layout.rowStats.size())
{
extendRow(layout.rowStats[rowIndex]);
}
if (rowBottom <= layout.content.Y ||
rowTop >= layout.content.Y + layout.content.Height)
{
@@ -4945,7 +5035,10 @@ namespace jdi
}
const std::uint32_t signalColor = StyleColor(*resolved.style);
const UiInk ink = UiContrastInk(signalColor);
const Lane::MeanFilterItem* item = tabLane.FindMeanFilterItem(ref);
// 「均值滤波」看 `meanFilterItems`、「中心轴边沿计数」看 `edgeCenterItems`
// (两者都是 `Lane::MeanFilterItem`,字段完全一致)。
const Lane::MeanFilterItem* item = meanFilter ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref);
const bool checked = (item != nullptr);
// 内容区已由 `g.SetClip(layout.content)` 裁切(行可以“半截”显示,滚动更顺滑);
@@ -4987,6 +5080,19 @@ namespace jdi
continue;
}
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
if (centerAxis && rowIndex < layout.rowStats.size() &&
layout.rowStats[rowIndex].Width > 0.0f)
{
std::size_t rising = 0;
std::size_t falling = 0;
bool complete = true;
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style, item->points,
item->iterations, EdgeRangeActive(), EdgeRangeMin(),
EdgeRangeMax(), rising, falling, complete);
paintCounts(layout.rowStats[rowIndex], rising, falling, complete);
}
// 两行参数滑块(标签 / 轨道 / 数值 / 复位按钮)。
const auto paintMeanSlider = [&](const gd::RectF& track,
const gd::RectF& valueRect,
@@ -5031,11 +5137,15 @@ namespace jdi
hot ? m_theme->accent : m_theme->textMuted, m_scale);
};
const int pointsMax = std::max(kMeanFilterPointsMin,
// ⚠️ 「均值点数」的最小值:**均值滤波 = 1**(用户 2026-10-30 要求不能为 0,
// 0 与不做平滑完全等价,看起来像功能坏了);**中心轴边沿计数 = 0**
// (0 = 不平滑,直接对原始采样值统计边沿 —— 这是有意义的选项,保持旧语义)。
const int pointsFloor = centerAxis ? 0 : kMeanFilterPointsMin;
const int pointsMax = std::max(pointsFloor,
std::min(kMeanFilterPointsMaxLimit,
item->maxPoints));
const int points =
std::max(kMeanFilterPointsMin, std::min(pointsMax, item->points));
std::max(pointsFloor, std::min(pointsMax, item->points));
const int iterationsMax = std::max(kMeanFilterIterationsMin,
std::min(kMeanFilterIterationsMaxLimit,
item->maxIterations));
@@ -5043,10 +5153,11 @@ namespace jdi
kMeanFilterIterationsMin, std::min(iterationsMax, item->iterations));
const bool pointsActive = (m_dragMode == PlotDragMode::MEAN_POINTS) &&
(m_meanDragLane == static_cast<int>(index)) &&
(m_meanDragRef == ref);
(m_meanDragRef == ref) && (m_meanDragCenter == centerAxis);
const bool iterationsActive =
(m_dragMode == PlotDragMode::MEAN_ITERATIONS) &&
(m_meanDragLane == static_cast<int>(index)) && (m_meanDragRef == ref);
(m_meanDragLane == static_cast<int>(index)) && (m_meanDragRef == ref) &&
(m_meanDragCenter == centerAxis);
if (rowIndex < layout.rowLevelTracks.size())
{
paintMeanSlider(layout.rowLevelTracks[rowIndex],
@@ -5057,7 +5168,7 @@ namespace jdi
? layout.rowLevelResets[rowIndex]
: gd::RectF(),
kMeanFilterPointsSliderLabel, points,
kMeanFilterPointsMin, pointsMax, pointsActive);
pointsFloor, pointsMax, pointsActive);
}
if (rowIndex < layout.rowIterTracks.size())
{
@@ -5434,7 +5545,7 @@ namespace jdi
{
continue;
}
if (itemized || freeMarker || meanFilter)
if (itemized || freeMarker || listStyle)
{
// 逐条目绘制已在上面完成(自由标记同样是逐条目排布,
// 均值滤波走自己的可滚动分支)。