修复悬停高亮变长方形色块/联动失效 + 信号列表动画卡顿真因(行位图缓存)
一、悬停高亮恢复"整条曲线发光"(上一轮裁成鼠标附近一段 ⇒ 满幅方波上就是个长方形色块) 光晕的语义是"整条曲线一起发光",对编码器这种满幅方波,一列包络本来就跨满网格高, 裁成 144px 宽 × 满高的窗口在屏幕上就是一个像选区框的色块(用户 2026-10-18 第二轮报告)。 修法(PlotViewPaint.cpp): - PaintSignalHaloFor 恢复**整条**几何(包络带 FillPath / 折线 DrawLines); - 新增 PaintSignalHaloCached:光晕几何只取决于「信号 + 视界 + 网格几何 + 样式」,**与鼠标无关** ⇒ 渲染一次进离屏位图,之后每帧只做一次 AlphaBlend(源位图预乘 alpha,用 256×256 查表); - 缓存键与 AcquirePolyline 同源,**不含网格屏幕位置**(滚动网格只按新位置贴同一张位图)。 210ms/帧(整条描边)→ 一次性 15ms 渲染 + 每帧 ~2ms 贴图。 halocheck 校验:悬停必命中(60/60 位置)、光晕贴合带宽(outsideBand=0)、不越出网格、 bbox 覆盖整条曲线(不是中段方块)、列表行悬停联动仍生效。 二、信号列表动画卡顿的三个真因(探针 panelstutter:129 个可见网格 + 反复"展开→切组复选框→收起") 1) 面板每帧重画 ~36 行 × ~0.35ms(十来个 GDI+ 原语)⇒ panel.Paint 24~35ms; 2) 动画帧里仍在预热网格图层:每帧重建 1 个网格图层,2500 万采样信号 10~30ms ⇒ 帧被拉长; 3) 勾选分组会把上百个**离屏**网格排进待重画队列,随后每帧补 1 个 ⇒ 数秒持续掉帧。 修法: 1) 行位图缓存(SignalPanel::PaintRowCached):按"行的可见内容"做键,整行渲染进离屏 DIB, 命中时一次 BitBlt。panel.Paint 24~35ms → 5~6ms。 ⚠️ 必须 DIB + 内存 DC(gd::Bitmap 上 GDI 会关闭 ClearType,整列文字退化); ⚠️ 行内几何在 RowRect 里平移(GDI 文本不认 GDI+ 世界变换,否则文字画到小位图之外); ⚠️ 渲染到行位图时不按列表矩形裁剪,裁剪放到贴图时的 DC(GDI 不认 GDI+ 裁剪, 必须自己 SaveDC + IntersectClipRect,折起动画时行不许画到组名上); ⚠️ 缓存上限 192 行(≈10MB)。静态画面逐像素一致(面板区域 54/275200 不同)。 2) 新增 PlotView::SetAnimationBusy:UI 动画期间不预热网格图层(队列保留,动画结束后继续)。 3) 待重画队列只保留可视 + 预取带内的网格;RollWideBuffer 不再登记离屏网格。 实测:展开 p50 35ms → 13ms、收起 33ms → 13ms、动画帧内图层重建 56 次 → 0 次。 三、试过并放弃(避免以后重走) - ExtTextOutW 替代 DrawTextW:快 2~4 倍但光栅化不同(面板 10% 像素差异)⇒ 观感变化,放弃; - 用 gd::Bitmap 做行位图:文字丢失 ClearType,放弃。 四、验证(全部离屏,不占用鼠标/键盘/屏幕) - 新增 .preflight/perf/panelstutter_main.cpp(8/8)、halocheck_main.cpp(16/16); - 回归 verify 49/49、panelanim 158/158、hoverdrag 20/20、gridanim 93/93、lanescroll 23/23、 datacheck 69/69、edgecheck 46/46、errcheck 19/19、panflash 19/19、stress 6/6; - make release / make debug 零警告(TARGET_FILE_NAME=TargetCheck/TargetCheckD)。 文档:Documents/agent_mem.md 追加本轮小节(含 4 条新规则:高亮语义不得为性能让步、 GDI 文本的三个隐形陷阱、面板卡顿先量"每行成本×可见行数"与"本帧是否顺带做绘图区重活"、 动画帧内绝不做可延迟的整帧级工作)。
This commit is contained in:
1 parent
0efaa8c0ca
commit
24f0bf849d
11 files changed
+853
-197
No files matched your search
@@ -4,6 +4,8 @@
|
||||
// 分片按“职责”划分:核心(状态/布局/几何)、渲染、交互与编辑,见各文件名。
|
||||
#include "SignalPanelInternal.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
@@ -186,14 +188,10 @@ namespace jdi
|
||||
g.SetClip(m_rowClipRect, gd::CombineModeIntersect);
|
||||
}
|
||||
|
||||
if (m_rows[static_cast<std::size_t>(row)].kind == RowKind::GROUP)
|
||||
{
|
||||
PaintGroupRow(g, row);
|
||||
}
|
||||
else
|
||||
{
|
||||
PaintSignalRow(g, row);
|
||||
}
|
||||
// ⚠️ 整行走**位图缓存**:分组折起/展开动画只是在平移行,
|
||||
// 行的像素一个也没变;逐行重画十来个 GDI+ 原语要 0.35ms/行
|
||||
//(一屏 35~40 行 ⇒ 每帧 12~15ms),这是"只有文字的列表却滚动卡顿"的真因。
|
||||
PaintRowCached(g, row);
|
||||
g.Restore(rowSave);
|
||||
ClearRowClip();
|
||||
}
|
||||
@@ -227,6 +225,14 @@ namespace jdi
|
||||
// 否则列表滚动时顶部那些“部分可见”的行会把组名/信号名画到筛选框上。
|
||||
// 另外,分组折起动画期间行高会连续缩小 —— 此时必须裁到**本行矩形**内,
|
||||
// 否则垂直居中的文字会溢出到相邻行上。
|
||||
//
|
||||
// ⚠️ **行位图缓存渲染时反而不能裁剪**(`m_rowClipRender`):位图是整行内容,
|
||||
// 裁剪留到贴图时由 GDI+ 裁剪区完成(否则部分滚出上边界的行会永久丢掉顶部像素)。
|
||||
if (m_rowClipRender)
|
||||
{
|
||||
UiText(g, text, font, rect, argb, align);
|
||||
return;
|
||||
}
|
||||
UiTextClipped(g, text, font, rect, argb, align, m_rowClipActive ? m_rowClipRect : m_listRect);
|
||||
}
|
||||
|
||||
@@ -235,14 +241,251 @@ namespace jdi
|
||||
// 夹取而不是裁剪:纯色填充下两者等价,而夹取对 `UiFillRect` 的 GDI 快路径同样有效
|
||||
//(大面积不透明矩形会改走 GDI `FillRect`,它同样不受 GDI+ 裁剪约束)。
|
||||
gd::RectF clipped = rect;
|
||||
clipped.Intersect(m_rowClipActive ? m_rowClipRect : m_listRect);
|
||||
if (clipped.Width <= 0.0f || clipped.Height <= 0.0f)
|
||||
if (!m_rowClipRender)
|
||||
{
|
||||
return;
|
||||
clipped.Intersect(m_rowClipActive ? m_rowClipRect : m_listRect);
|
||||
if (clipped.Width <= 0.0f || clipped.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
UiFillRect(g, clipped, argb);
|
||||
}
|
||||
|
||||
void SignalPanel::InvalidateRowCache()
|
||||
{
|
||||
for (RowBitmapCache& cache : m_rowBitmapCache)
|
||||
{
|
||||
if (cache.dc != nullptr)
|
||||
{
|
||||
DeleteDC(cache.dc);
|
||||
cache.dc = nullptr;
|
||||
}
|
||||
if (cache.dib != nullptr)
|
||||
{
|
||||
DeleteObject(cache.dib);
|
||||
cache.dib = nullptr;
|
||||
}
|
||||
}
|
||||
m_rowBitmapCache.clear();
|
||||
m_rowBitmapLiveCount = 0;
|
||||
}
|
||||
|
||||
std::uint64_t SignalPanel::RowCacheKey(int row) const
|
||||
{
|
||||
std::uint64_t hash = 1469598103934665603ull;
|
||||
const auto mix = [&hash](std::uint64_t value)
|
||||
{
|
||||
hash ^= value;
|
||||
hash *= 1099511628211ull;
|
||||
};
|
||||
const auto mixFloat = [&mix](float value)
|
||||
{
|
||||
std::uint32_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
mix(bits);
|
||||
};
|
||||
const auto mixText = [&mix](const std::wstring& text)
|
||||
{
|
||||
mix(text.size());
|
||||
for (const wchar_t character : text)
|
||||
{
|
||||
mix(static_cast<std::uint64_t>(character));
|
||||
}
|
||||
};
|
||||
|
||||
if (row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||
{
|
||||
return hash;
|
||||
}
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
mix(static_cast<std::uint64_t>(item.kind));
|
||||
mix(static_cast<std::uint64_t>(item.groupIndex + 1));
|
||||
mix(static_cast<std::uint64_t>(item.signalIndex + 1));
|
||||
mixFloat(item.height);
|
||||
mixFloat(m_scale);
|
||||
mix(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(m_theme)));
|
||||
mix(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(m_fonts)));
|
||||
mix(static_cast<std::uint64_t>((m_hoverRow == row) ? (static_cast<int>(m_hoverPart) + 1) : 0));
|
||||
|
||||
if (item.kind == RowKind::GROUP)
|
||||
{
|
||||
const SignalGroup* group =
|
||||
(m_dataSet != nullptr) ? m_dataSet->GroupAt(item.groupIndex) : nullptr;
|
||||
mixText((group != nullptr) ? group->name : std::wstring());
|
||||
mixText(GroupStatsText(row));
|
||||
mix(IsGroupCollapsed(item.groupIndex) ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(item.displayCount + 1));
|
||||
if (m_tab != nullptr)
|
||||
{
|
||||
mix(m_tab->GroupAllVisible(item.groupIndex) ? 1ull : 0ull);
|
||||
mix(m_tab->GroupHasVisible(item.groupIndex) ? 2ull : 0ull);
|
||||
}
|
||||
const bool editing = (m_editField == StyleEditField::GROUP_TITLE && m_editRow == row);
|
||||
mix(editing ? 1ull : 0ull);
|
||||
if (editing)
|
||||
{
|
||||
mixText(m_edit.Text());
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||||
const SignalData* signal = (m_dataSet != nullptr) ? m_dataSet->Resolve(ref) : nullptr;
|
||||
const SignalStyle* style = StyleOf(ref);
|
||||
mixText((signal != nullptr) ? signal->name : std::wstring());
|
||||
mix((SelectedRef() == ref) ? 1ull : 0ull);
|
||||
if (style != nullptr)
|
||||
{
|
||||
mix(static_cast<std::uint64_t>(style->color));
|
||||
mixFloat(style->opacity);
|
||||
}
|
||||
if (m_tab != nullptr)
|
||||
{
|
||||
mix(m_tab->IsVisible(ref) ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(m_tab->LaneOfSignal(ref) + 1));
|
||||
}
|
||||
const bool editing = (m_editField == StyleEditField::SIGNAL_NAME && m_editRow == row);
|
||||
mix(editing ? 1ull : 0ull);
|
||||
if (editing)
|
||||
{
|
||||
mixText(m_edit.Text());
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
void SignalPanel::PaintRowCached(gd::Graphics& g, int row)
|
||||
{
|
||||
if (m_dataSet == nullptr || m_tab == nullptr || m_theme == nullptr || row < 0 ||
|
||||
row >= static_cast<int>(m_rows.size()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const gd::RectF rect = RowRect(row);
|
||||
if (rect.Height <= 0.5f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int originX = static_cast<int>(std::floor(m_listRect.X));
|
||||
const int originY = static_cast<int>(std::floor(rect.Y));
|
||||
const int width = static_cast<int>(std::ceil(m_listRect.Width));
|
||||
const int height = static_cast<int>(std::ceil(rect.Height));
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ⚠️ 上限检查必须在**取引用之前**:`InvalidateRowCache()` 会清空容器,
|
||||
// 清理后原有引用(`RowBitmapCache&`)就悬空了。
|
||||
if (m_rowBitmapLiveCount >= kRowBitmapCacheLimit)
|
||||
{
|
||||
InvalidateRowCache();
|
||||
}
|
||||
if (m_rowBitmapCache.size() < m_rows.size())
|
||||
{
|
||||
m_rowBitmapCache.resize(m_rows.size());
|
||||
}
|
||||
RowBitmapCache& cache = m_rowBitmapCache[static_cast<std::size_t>(row)];
|
||||
const std::uint64_t key = RowCacheKey(row);
|
||||
const bool sameSize = (cache.dc != nullptr) && cache.width == width && cache.height == height;
|
||||
if (!sameSize || cache.key != key)
|
||||
{
|
||||
if (!sameSize)
|
||||
{
|
||||
if (cache.dc != nullptr)
|
||||
{
|
||||
DeleteDC(cache.dc);
|
||||
cache.dc = nullptr;
|
||||
if (m_rowBitmapLiveCount > 0)
|
||||
{
|
||||
--m_rowBitmapLiveCount;
|
||||
}
|
||||
}
|
||||
if (cache.dib != nullptr)
|
||||
{
|
||||
DeleteObject(cache.dib);
|
||||
cache.dib = nullptr;
|
||||
}
|
||||
// ⚠️ DIB + 内存 DC(而不是 `gd::Bitmap`):GDI 在 GDI+ 位图的临时 HDC 上
|
||||
// 会关闭 ClearType,导致列表文字丢失亚像素渲染、与正文观感不一致。
|
||||
BITMAPINFO info = {};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = width;
|
||||
info.bmiHeader.biHeight = -height; // 自上而下
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
void* bits = nullptr;
|
||||
HDC memoryDc = CreateCompatibleDC(nullptr);
|
||||
HBITMAP dib = (memoryDc != nullptr)
|
||||
? CreateDIBSection(memoryDc, &info, DIB_RGB_COLORS, &bits, nullptr, 0)
|
||||
: nullptr;
|
||||
if (memoryDc == nullptr || dib == nullptr)
|
||||
{
|
||||
if (dib != nullptr)
|
||||
{
|
||||
DeleteObject(dib);
|
||||
}
|
||||
if (memoryDc != nullptr)
|
||||
{
|
||||
DeleteDC(memoryDc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
SelectObject(memoryDc, dib);
|
||||
cache.dc = memoryDc;
|
||||
cache.dib = dib;
|
||||
cache.width = width;
|
||||
cache.height = height;
|
||||
++m_rowBitmapLiveCount;
|
||||
}
|
||||
{
|
||||
gd::Graphics rowGraphics(cache.dc);
|
||||
rowGraphics.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
rowGraphics.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
rowGraphics.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||
rowGraphics.Clear(ToGdiColor(m_theme->panelBg));
|
||||
// ⚠️ 用**几何平移**而不是 `TranslateTransform`(见 `m_rowRenderActive`)。
|
||||
m_rowRenderActive = true;
|
||||
m_rowRenderOriginX = static_cast<float>(originX);
|
||||
m_rowRenderOriginY = static_cast<float>(originY);
|
||||
m_rowClipRender = true;
|
||||
if (m_rows[static_cast<std::size_t>(row)].kind == RowKind::GROUP)
|
||||
{
|
||||
PaintGroupRow(rowGraphics, row);
|
||||
}
|
||||
else
|
||||
{
|
||||
PaintSignalRow(rowGraphics, row);
|
||||
}
|
||||
m_rowClipRender = false;
|
||||
m_rowRenderActive = false;
|
||||
}
|
||||
cache.key = key;
|
||||
}
|
||||
// ⚠️ 整块 1:1 搬运(不经过 GDI+ 采样/混合),字的亚像素边缘才能原样保留。
|
||||
// ⚠️ **必须自己给 DC 设裁剪**:`Graphics::GetHDC()` 交给我们的 DC 上,(a) GDI 绘制
|
||||
// 不受 GDI+ `SetClip` 的约束(这是本文件多处注释过的坑),(b) 折起/展开动画时行会
|
||||
// 滑进组名栏,必须裁到 `m_rowClipRect`。因此这里显式 `IntersectClipRect` 后再 `BitBlt`。
|
||||
const HDC targetDc = g.GetHDC();
|
||||
if (targetDc != nullptr)
|
||||
{
|
||||
const int saved = SaveDC(targetDc);
|
||||
if (saved != 0)
|
||||
{
|
||||
const gd::RectF clip = m_rowClipActive ? m_rowClipRect : m_listRect;
|
||||
IntersectClipRect(targetDc, static_cast<int>(std::floor(clip.X)),
|
||||
static_cast<int>(std::floor(clip.Y)),
|
||||
static_cast<int>(std::ceil(clip.X + clip.Width)),
|
||||
static_cast<int>(std::ceil(clip.Y + clip.Height)));
|
||||
}
|
||||
BitBlt(targetDc, originX, originY, width, height, cache.dc, 0, 0, SRCCOPY);
|
||||
if (saved != 0)
|
||||
{
|
||||
RestoreDC(targetDc, saved);
|
||||
}
|
||||
g.ReleaseHDC(targetDc);
|
||||
}
|
||||
}
|
||||
void SignalPanel::PaintGroupRow(gd::Graphics& g, int row)
|
||||
{
|
||||
if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||
|
||||
Reference in new issue
Block a user