fix(plotview): 修复面板条目栏悬停时预览曲线高亮丢失 + 极端工况绘制管线提速
一、悬停高亮回归修复 - 根因:高亮画在预览曲线绘制路径里,而该路径在命中静态场景缓存时完全不会被调用 (悬停态又不参与 SceneContentHash),因此缓存命中帧永远看不到高亮。 - 修法:拆出 PaintDerivedCurvesFor(hoverOnly) 两态,新增 PaintDerivedHoverHighlight, 在 paintOverlays() 中紧跟 PaintHoverHighlight 以前景方式绘制(面板之下、光标之上)。 高亮仍按面板种类(均值滤波/中心轴/弱峰调整)分别判定。 二、预览曲线绘制重写(单条 15ms → 1.7ms) - 稠密区间改用与波形基底同一套「逐像素列 min/max 包络带」+ 一次 FillPath; 旧抽稀折线在稠密视界下每段都是竖直满幅,GDI+ 描边代价正比于覆盖像素数 (3200 顶点满幅锯齿抗锯齿 128ms / 无抗锯齿 31ms,包络带 1.9ms)。 - 列密度自适应:中等密度 2px/列,极密(>8 采样/像素)4px/列。 - 稀疏区间保持逐点折线;附带修复「等间隔抽样漏掉尖峰」。 三、预览曲线像素缓存层(Picture cache + pan roll) - 45 条曲线画进带 alpha 的 32bpp DIB,正常帧只做一次 AlphaBlend(1~2ms)。 - 纯平移时整行 memmove 一个 dx 并只重画新露出的竖带(与宽缓冲区滚动同构), 因此拖动视界的每帧代价与信号数无关。 - 内容指纹 = SceneContentHash(含样式/派生参数/网格几何) ⊕ 曲线发布序号;高亮不进指纹。 四、其他热点 - 光标取值胶囊不再逐帧重算并发布 45 条曲线(CurveSpanFor 新增 computeIfMissing), 只取已覆盖的缓存曲线,未命中退到最新曲线或本帧不标注。 - 样式补丁路径不再同步重建网格图层(宽基底 2 倍宽 × 2 倍高,实测单次 2.2 秒), 且开启派生功能时直接走整幅重绘(补丁会覆盖缓存中已烘入的曲线像素)。 实测(15 信号合并一条网格 + 全部实验性功能,1600x900 离屏探针): 拖动视界 264→5.9ms,双光标拖动 187→20.9ms,弱峰滑块 555→89ms, 改信号颜色 2661→200ms,悬停面板行 225→38ms,深放大拖动 207→23.8ms。 全屏 1920x1080:拖动视界 9.3ms。 验证:新增离屏探针 p27 8/8(悬停高亮在缓存命中帧可见、平移/缩放/改颜色后的叠加层 与「完全重画」参考帧逐像素一致 diff=0);p20 9/9、p21 9/9、p22 14/14、p23 8/8、p25 12/12; make release / make debug 零 warning;Release 814,080 B。规则已同步到 Documents/agent_mem.md。
This commit is contained in:
1 parent
ea704b03ea
commit
0c320cf1b3
6 files changed
+797
-33
No files matched your search
@@ -744,6 +744,306 @@ namespace jdi
|
||||
return any;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 「派生预览曲线」像素缓存
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::uint64_t PlotView::DerivedOverlayKey() const
|
||||
{
|
||||
// 内容指纹 = 场景内容(含外观样式 + 派生参数 + 网格幅值范围 + 网格滚动量 + 绘图区几何)。
|
||||
// ⚠️ 视界**不在这里**:平移时要靠它判断“能否只滚动像素”,所以视界单独比对。
|
||||
std::uint64_t hash = SceneContentHash(true, true, true, true);
|
||||
// 后台算好的派生曲线到达时,曲线像素必须更新(`m_curveSerial` 随每次发布自增)。
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
hash ^= m_curveSerial * 1099511628211ull;
|
||||
}
|
||||
// 高亮状态**不参与**:高亮由前景单独绘制(见 `PaintDerivedHoverHighlight`)。
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool PlotView::EnsureDerivedCache(int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_derivedDc != nullptr && m_derivedBits != nullptr && m_derivedW == width &&
|
||||
m_derivedH == height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
ReleaseDerivedCache();
|
||||
HDC screen = GetDC(nullptr);
|
||||
if (screen == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 与装饰层同理:必须是 32bpp DIB 段(带真 alpha + 像素可直接 memmove 滚动)。
|
||||
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;
|
||||
HBITMAP dib = CreateDIBSection(screen, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
ReleaseDC(nullptr, screen);
|
||||
if (dib == nullptr || bits == nullptr)
|
||||
{
|
||||
if (dib != nullptr)
|
||||
{
|
||||
DeleteObject(dib);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
HDC dc = CreateCompatibleDC(nullptr);
|
||||
if (dc == nullptr)
|
||||
{
|
||||
DeleteObject(dib);
|
||||
return false;
|
||||
}
|
||||
m_derivedOldBitmap = SelectObject(dc, dib);
|
||||
m_derivedDc = dc;
|
||||
m_derivedBitmap = dib;
|
||||
m_derivedBits = bits;
|
||||
m_derivedW = width;
|
||||
m_derivedH = height;
|
||||
m_derivedValid = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::ReleaseDerivedCache()
|
||||
{
|
||||
m_derivedValid = false;
|
||||
if (m_derivedDc != nullptr)
|
||||
{
|
||||
if (m_derivedOldBitmap != nullptr)
|
||||
{
|
||||
SelectObject(m_derivedDc, m_derivedOldBitmap);
|
||||
m_derivedOldBitmap = nullptr;
|
||||
}
|
||||
DeleteDC(m_derivedDc);
|
||||
m_derivedDc = nullptr;
|
||||
}
|
||||
if (m_derivedBitmap != nullptr)
|
||||
{
|
||||
DeleteObject(m_derivedBitmap);
|
||||
m_derivedBitmap = nullptr;
|
||||
}
|
||||
m_derivedBits = nullptr;
|
||||
m_derivedW = 0;
|
||||
m_derivedH = 0;
|
||||
}
|
||||
|
||||
bool PlotView::RenderDerivedCache()
|
||||
{
|
||||
m_derivedValid = false;
|
||||
if (m_theme == nullptr || m_fonts == nullptr || m_lanes.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int width = static_cast<int>(std::ceil(m_area.X + m_area.Width));
|
||||
const int height = static_cast<int>(std::ceil(m_area.Y + m_area.Height));
|
||||
if (!EnsureDerivedCache(width, height))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t bytes = static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4u;
|
||||
if (!m_derivedStripActive)
|
||||
{
|
||||
// 整块重画:先把全部像素清成透明。
|
||||
std::memset(m_derivedBits, 0, bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 补条带:只把这条竖带清成透明(其余像素是滚动过来的有效内容)。
|
||||
const int x0 = std::max(0, static_cast<int>(std::floor(m_derivedStripX0)));
|
||||
const int x1 = std::min(width, static_cast<int>(std::ceil(m_derivedStripX1)));
|
||||
for (int y = 0; y < height; ++y)
|
||||
{
|
||||
unsigned char* row = static_cast<unsigned char*>(m_derivedBits) +
|
||||
(static_cast<std::size_t>(y) * static_cast<std::size_t>(width)) * 4u;
|
||||
if (x1 > x0)
|
||||
{
|
||||
std::memset(row + static_cast<std::size_t>(x0) * 4u, 0,
|
||||
static_cast<std::size_t>(x1 - x0) * 4u);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
gd::Graphics g(m_derivedDc);
|
||||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
if (m_derivedStripActive)
|
||||
{
|
||||
// ⚠️ 绘制期的横向裁剪(`PaintDerivedCurvesFor` 只做 `intersect`,不会覆盖这一层)。
|
||||
g.SetClip(gd::RectF(m_derivedStripX0, 0.0f, std::max(0.0f, m_derivedStripX1 - m_derivedStripX0),
|
||||
static_cast<float>(height)),
|
||||
gd::CombineModeIntersect);
|
||||
}
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
PaintDerivedCurvesFor(g, static_cast<int>(index), m_lanes[index], false);
|
||||
}
|
||||
}
|
||||
m_derivedContentKey = DerivedOverlayKey();
|
||||
m_derivedViewMin = Tab().ViewMin();
|
||||
m_derivedViewMax = Tab().ViewMax();
|
||||
m_derivedValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::BlitDerivedCache(gd::Graphics& g)
|
||||
{
|
||||
if (!m_derivedValid || m_derivedDc == nullptr || m_plotRect.Width < 1.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void* proc = ResolveAlphaBlend();
|
||||
if (proc == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using AlphaBlendFn = BOOL(WINAPI*)(HDC, int, int, int, int, HDC, int, int, int, int, BLENDFUNCTION);
|
||||
AlphaBlendFn blend = reinterpret_cast<AlphaBlendFn>(proc);
|
||||
HDC target = g.GetHDC();
|
||||
if (target == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 缓存位图原点 = 客户区 (0,0):源与目标坐标相同。只贴绘图区(曲线被夹在网格内)。
|
||||
const int x = std::max(0, static_cast<int>(std::floor(m_plotRect.X)));
|
||||
const int y = std::max(0, static_cast<int>(std::floor(m_plotRect.Y)));
|
||||
const int w = std::min(m_derivedW - x, static_cast<int>(std::ceil(m_plotRect.Width)));
|
||||
const int h = std::min(m_derivedH - y, static_cast<int>(std::ceil(m_plotRect.Height)));
|
||||
if (w <= 0 || h <= 0)
|
||||
{
|
||||
g.ReleaseHDC(target);
|
||||
return false;
|
||||
}
|
||||
BLENDFUNCTION fn = {};
|
||||
fn.BlendOp = AC_SRC_OVER;
|
||||
fn.SourceConstantAlpha = 255;
|
||||
fn.AlphaFormat = AC_SRC_ALPHA;
|
||||
const BOOL done = blend(target, x, y, w, h, m_derivedDc, x, y, w, h, fn);
|
||||
g.ReleaseHDC(target);
|
||||
return done != FALSE;
|
||||
}
|
||||
|
||||
void PlotView::PaintDerivedOverlayLayer(gd::Graphics& g)
|
||||
{
|
||||
if (m_loadingBusy || m_lanes.empty())
|
||||
{
|
||||
return; // 加载遮罩期间不花时间在派生曲线上(本来也看不见)
|
||||
}
|
||||
// 没有任何派生功能开启时直接跳过(并释放缓存,避免白占内存)。
|
||||
bool anyFeature = false;
|
||||
for (const Lane& lane : Tab().Lanes())
|
||||
{
|
||||
if (lane.edgeCenter || lane.meanFilter || lane.weakPeak)
|
||||
{
|
||||
anyFeature = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anyFeature)
|
||||
{
|
||||
m_derivedValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::uint64_t contentKey = DerivedOverlayKey();
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
const double span = viewMax - viewMin;
|
||||
bool needRender = true;
|
||||
bool rolled = false;
|
||||
m_derivedStripActive = false;
|
||||
if (m_derivedValid && m_derivedContentKey == contentKey && span > 0.0)
|
||||
{
|
||||
const double cachedSpan = m_derivedViewMax - m_derivedViewMin;
|
||||
const double sameSpanTolerance = std::max(1.0e-12, std::fabs(span) * 1.0e-9);
|
||||
if (std::fabs(cachedSpan - span) <= sameSpanTolerance &&
|
||||
m_derivedViewMin == viewMin && m_derivedViewMax == viewMax)
|
||||
{
|
||||
needRender = false; // 完全同源:只贴图
|
||||
}
|
||||
else if (std::fabs(cachedSpan - span) <= sameSpanTolerance && m_plotRect.Width > 1.0f)
|
||||
{
|
||||
// 纯平移:像素整体横移,只重画新露出的竖带(与宽缓冲区的滚动同构)。
|
||||
const double pxPerTime = static_cast<double>(m_plotRect.Width) / span;
|
||||
const int shift = static_cast<int>(
|
||||
std::lround((m_derivedViewMin - viewMin) * pxPerTime));
|
||||
const int width = m_derivedW;
|
||||
if (shift != 0 && std::abs(shift) < width)
|
||||
{
|
||||
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4u;
|
||||
const std::size_t moveBytes =
|
||||
rowBytes - static_cast<std::size_t>(std::abs(shift)) * 4u;
|
||||
unsigned char* bits = static_cast<unsigned char*>(m_derivedBits);
|
||||
if (bits != nullptr && moveBytes > 0)
|
||||
{
|
||||
for (int y = 0; y < m_derivedH; ++y)
|
||||
{
|
||||
unsigned char* row = bits + static_cast<std::size_t>(y) * rowBytes;
|
||||
if (shift > 0)
|
||||
{
|
||||
// 内容右移:从本行右端往前搬(`memmove` 自带重叠安全)。
|
||||
std::memmove(row + static_cast<std::size_t>(shift) * 4u, row, moveBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 内容左移:源在本行右侧 —— 目标**不能**越过行首(否则越界)。
|
||||
std::memmove(row, row + static_cast<std::size_t>(-shift) * 4u, moveBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
const float plotLeft = m_plotRect.X;
|
||||
const float plotRight = m_plotRect.X + m_plotRect.Width;
|
||||
if (shift > 0)
|
||||
{
|
||||
m_derivedStripX0 = plotLeft;
|
||||
m_derivedStripX1 = std::min(plotRight, plotLeft + static_cast<float>(shift) + 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_derivedStripX0 = std::max(plotLeft, plotRight + static_cast<float>(shift) - 1.0f);
|
||||
m_derivedStripX1 = plotRight;
|
||||
}
|
||||
if (m_derivedStripX1 > m_derivedStripX0 + 0.5f)
|
||||
{
|
||||
m_derivedStripActive = true;
|
||||
rolled = true;
|
||||
}
|
||||
}
|
||||
needRender = true;
|
||||
}
|
||||
}
|
||||
if (needRender)
|
||||
{
|
||||
if (!RenderDerivedCache())
|
||||
{
|
||||
m_derivedStripActive = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_derivedStripActive = false;
|
||||
if (!BlitDerivedCache(g))
|
||||
{
|
||||
// 无法 AlphaBlend:退回逐条曲线直接绘制(正确性优先,代价高但极少发生)。
|
||||
if (!rolled)
|
||||
{
|
||||
m_derivedValid = false;
|
||||
}
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
PaintLaneDerivedOverlays(g, static_cast<int>(index), m_lanes[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PlotView::EnsureLaneLayer(LaneLayer& layer, int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
|
||||
Reference in new issue
Block a user