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:
1 parent
8271879ede
commit
944c074ab3
11 files changed
+618
-122
No files matched your search
@@ -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)
|
||||
{
|
||||
// 逐条目绘制已在上面完成(自由标记同样是逐条目排布,
|
||||
// 均值滤波走自己的可滚动分支)。
|
||||
|
||||
Reference in new issue
Block a user