装饰层改用带 alpha 的独立图层(消除 GX/图例"直角白边")+ 图例圆角托底 + 预览曲线内存释放
一、GX 编号 / 图例「圆角矩形外多出一圈直角白边」的根因与修复
- 根因:装饰层原本是"从后备缓冲区**抓取一块不透明矩形**"(CaptureDecorCache + BitBlt),
圆角之外(属于波形)的像素被一起烘进缓存;为了让这块矩形像素能跨视界复用,
又在圆角矩形外铺了一层**直角**的不透明底色(OpaqueBaseRect)。两者叠加 ⇒ 圆角之外
显示的是网格底色而不是波形 ⇒ 在密集波形上就是一圈直角白边(Y 轴刻度胶囊同理)。
- 修法:把装饰层改成**带真 alpha 的独立图层**:
* m_decorDc 改为 32bpp CreateDIBSection(自上而下),像素可直接读写 alpha;
* 新增 RenderDecorCache():把装饰**单独绘制**进一张全透明的位图(不再从后备缓冲区抓取),
圆角之外保持全透明;位图覆盖范围为「客户区原点 → 绘图区右下角」,绘制坐标与正常绘制
完全一致(GDI 文本不认 GDI+ 变换,绝不能做坐标平移);
* BlitDecorCache() 改用 AlphaBlend(AC_SRC_ALPHA,GDI+ 写入 DIB 段即预乘 alpha)逐块合成
⇒ 圆角之外透出的是**当前**波形,既没有白边也不会残留旧波形像素;
* AlphaBlend 经 GetProcAddress(gdi32!GdiAlphaBlend)(回退 msimg32!AlphaBlend)解析,
不新增链接依赖(两台协作设备工具链不同,避免改 Makefile);
* alpha 修补:GDI 文本(DrawTextW/ExtTextOutW)会把写入像素的 alpha 清成 0,
若不修补文字处会变成"透明洞",因此在装饰矩形内把「A==0 且有颜色」的像素还原为不透明;
* 删除全部外扩的直角不透明底色:Y 轴胶囊与图例托底都只画圆角形状本身;
* 补齐「+N」(图例空间不足)与「(无可见信号)」提示的装饰矩形(否则画进缓存却不被贴回)。
- 装饰缓存的每帧开销因此更低:BlitDecorCache 约 0.19ms/帧(原 BitBlt 方案约 0.3ms),
RenderDecorCache 仅在装饰内容变化时执行(约 2.4ms / 5 网格)。
二、图例样式:与 GX 编号徽标同款圆角托底 + 悬停/选中配色统一
- 每个图例条目 = 一个圆角矩形托底(kLegendPlateRadius=4,cardBg 底 + border 描边),
内容为色块 + 信号名 —— "近似于一个 GX 标签,只是内容换成了图例信息"。
- 选中态与悬停态同配色(accentSoft 底 + accent 描边 + textStrong 文字),与 GX 徽标悬停一致;
悬停高亮画在贴图之后,并**在不透明托底上原样重绘色块与文字**(否则内容会被盖住),
同时保留对应信号的曲线光晕;LegendHit 新增 radius 字段。
- 选中态与左侧信号列表同源(DataSet::Selected()):面板选中 → 图例高亮;图例点选 → 面板行选中。
三、预览曲线内存:thread_local 工作缓冲封顶 + 关闭功能即释放
- 根因:DataModel 里三处 thread_local 工作缓冲(弱峰调整的 current/next/极值表、
ApplyWeakPeakStage 的位移表、迭代均值滤波的中转表)。整段信号(2580 万采样)计算时
每条后台线程常驻数百 MB,而后台曲线线程池最多 4 条线程 ⇒ 2GB 以上;thread_local
只在线程退出时才析构 ⇒ 关掉预览不释放,只有 PauseDerivedAsync()(关闭文件分组会调用)
join 线程后才归还(用户实测 1.42GB → 4.21GB 且关不掉)。
- 修法:① 保留容量封顶(kReuseBufferMaxElements=4M + TrimReuseBuffer + ReuseBufferScope
作用域守卫,所有退出路径都会裁剪):超大计算改为一次性分配、用完即释放,内存不再随
线程数放大;中/小规模仍保留容量以维持"多阶弱峰调整 991ms → 577ms"的收益。
② 新增 AnyDerivedPreviewEnabled() / TrimDerivedCurveDataIfIdle(),在
SetLaneWeakPeak / SetLaneMeanFilter / SetLaneEdgeCenterCount(..., false) 里调用 ——
关闭最后一个预览功能时立即 PauseDerivedAsync()(join 线程 ⇒ thread_local 析构 ⇒ 归还内存)
并清空均值/弱峰曲线缓存。
- 实测(.preflight/perf/stylecheck mode 2,VERY_BIG_DATA):
baseline 1202MB → 弱峰(10 迭代,1 条) 1399MB → 5 条曲线 1792MB → **关闭面板后 1203MB**。
⚠️ 计算期间的瞬时峰值仍可达约 2.2GB(整段 O(n) 计算的固有代价),但计算结束立刻归还。
四、验证(全部离屏,不占用鼠标/键盘/屏幕)
- .preflight/perf/verify_main.cpp(离屏 Application harness)**49/49 通过**,新增/强化:
装饰位图圆角外 alpha≈0、中心 alpha=255(直接读 m_decorBits,证明没有直角白边);
选中图例=悬停配色(accentSoft 像素 未选中 0 / 选中 1525);平移(宽基底)帧的装饰与整幅重绘
逐像素一致(只比较装饰不透明像素:worst=1 / 29554px);图例溢出的「+N」提示已登记且可见。
- .preflight/perf/stylecheck_main.cpp:mode 0(装饰重建/贴回耗时)、mode 1(密集波形下导出
GX+图例角落 PNG 目视)、mode 2(预览曲线内存增长/释放)、mode 4(装饰位图 alpha 形状检查)。
- zoomcheck mode 13:收敛帧 vs 整幅重绘 **逐像素 0 差异**(此前为 3503)。
- make clean && make all 零警告。
This commit is contained in:
1 parent
7c32eca004
commit
1b3ab4ac09
4 files changed
+461
-153
No files matched your search
@@ -1328,6 +1328,35 @@ namespace jdi
|
||||
m_curveSceneDirty.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool PlotView::AnyDerivedPreviewEnabled() const
|
||||
{
|
||||
if (m_tab == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const Lane& lane : m_tab->Lanes())
|
||||
{
|
||||
if (lane.weakPeak || lane.meanFilter || lane.edgeCenter)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PlotView::TrimDerivedCurveDataIfIdle()
|
||||
{
|
||||
if (AnyDerivedPreviewEnabled())
|
||||
{
|
||||
return; // 还有网格在用预览曲线:缓存仍然有用,不能清
|
||||
}
|
||||
// 停掉后台线程池:这会让各线程的 `thread_local` 工作缓冲**析构并归还内存** ——
|
||||
// 这是"关掉预览后内存不下降"的根因(thread_local 只在线程退出时释放)。
|
||||
PauseDerivedAsync();
|
||||
m_meanCurveCache.clear();
|
||||
m_weakPeakCurveCache.clear();
|
||||
}
|
||||
|
||||
bool PlotView::FitTimeToLane(int laneIndex)
|
||||
{
|
||||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||||
@@ -1896,6 +1925,12 @@ namespace jdi
|
||||
{
|
||||
lane.edgeCenterPanelOpen = false;
|
||||
}
|
||||
// 关闭后若本标签页**已无任何预览曲线功能**,立即释放派生曲线数据(缓存 + 后台线程的
|
||||
// thread_local 工作缓冲)—— 否则关掉面板后内存不会下降(用户报告)。
|
||||
if (!enabled)
|
||||
{
|
||||
TrimDerivedCurveDataIfIdle();
|
||||
}
|
||||
// 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。
|
||||
RebuildWideBuffer();
|
||||
return true;
|
||||
@@ -1984,6 +2019,12 @@ namespace jdi
|
||||
{
|
||||
lane.weakPeakPanelOpen = false;
|
||||
}
|
||||
// 关闭后若本标签页**已无任何预览曲线功能**,立即释放派生曲线数据(缓存 + 后台线程的
|
||||
// thread_local 工作缓冲)—— 否则关掉面板后内存不会下降(用户报告)。
|
||||
if (!enabled)
|
||||
{
|
||||
TrimDerivedCurveDataIfIdle();
|
||||
}
|
||||
// 预建「宽缓冲基底」(不含派生曲线),使用户首次拖动该滑块时无需从零重建(避免一次性尖峰)。
|
||||
RebuildWideBuffer();
|
||||
return true;
|
||||
@@ -2067,6 +2108,12 @@ namespace jdi
|
||||
{
|
||||
lane.meanFilterPanelOpen = false;
|
||||
}
|
||||
// 关闭后若本标签页**已无任何预览曲线功能**,立即释放派生曲线数据(缓存 + 后台线程的
|
||||
// thread_local 工作缓冲)—— 否则关掉面板后内存不会下降(用户报告)。
|
||||
if (!enabled)
|
||||
{
|
||||
TrimDerivedCurveDataIfIdle();
|
||||
}
|
||||
// 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。
|
||||
RebuildWideBuffer();
|
||||
return true;
|
||||
@@ -6053,28 +6100,31 @@ namespace jdi
|
||||
{
|
||||
PaintLaneDerivedOverlays(g, static_cast<int>(i), m_lanes[i]);
|
||||
}
|
||||
// 装饰(Y 轴刻度 + 图例):基底不含(否则平移会重影)。
|
||||
// 内容指纹一致 → 直接整块贴回缓存(省掉占整幅渲染一半以上的文字绘制);
|
||||
// 否则用 `m_renderDecorOnly` 重画一遍并**立即重新捕获**。
|
||||
// 装饰(Y 轴刻度 + GX 编号 + 图例):基底不含(否则平移会重影)。
|
||||
// 内容指纹一致 → 直接按 `AlphaBlend` 贴回缓存(省掉文字绘制);
|
||||
// 否则先把装饰单独绘制进透明缓存位图,再贴回。
|
||||
// ⚠️ 贴回的是**带 alpha 的圆角图形**:圆角之外是全透明,
|
||||
// 因此透出的是**当前**波形 —— 既没有"直角白边",也不会残留旧波形像素。
|
||||
m_useDecorCache = m_decorValid && (m_decorKey == SceneDecorHash());
|
||||
if (!m_useDecorCache && RenderDecorCache())
|
||||
{
|
||||
m_decorKey = SceneDecorHash();
|
||||
m_useDecorCache = true;
|
||||
}
|
||||
if (m_useDecorCache)
|
||||
{
|
||||
// 贴回用**捕获时**的矩形(命中缓存时各网格不会重建矩形列表)。
|
||||
BlitDecorCache(g);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_decorRects.clear();
|
||||
// 退化路径(无法建立透明缓存,例如系统不提供 AlphaBlend):
|
||||
// 直接把装饰画进后备缓冲区(就地绘制,不做缓存)。
|
||||
m_renderDecorOnly = true;
|
||||
for (std::size_t i = 0; i < m_lanes.size(); ++i)
|
||||
{
|
||||
PaintLane(g, static_cast<int>(i), m_lanes[i], wideTimeTicks);
|
||||
}
|
||||
m_renderDecorOnly = false;
|
||||
if (CaptureDecorCache(g))
|
||||
{
|
||||
m_decorKey = SceneDecorHash();
|
||||
}
|
||||
}
|
||||
paintOverlays();
|
||||
if (morePending)
|
||||
@@ -6127,10 +6177,10 @@ namespace jdi
|
||||
// 图例命中矩形每帧重建,避免残留已消失的信号。
|
||||
m_legendHits.clear();
|
||||
|
||||
// 「装饰层」缓存决策:快速帧(拖拽/平移)期间、且装饰内容与上次捕获完全一致时,
|
||||
// 跳过 Y 轴胶囊与图例的绘制(约占整幅重绘的一半),改用上次捕获的像素贴回。
|
||||
// 「装饰层」:装饰**不在网格循环里就地绘制**,而是统一由带 alpha 的装饰层贴回
|
||||
// (这样圆角之外透出的是当前波形,不会出现"直角白边")。
|
||||
m_decorRects.clear();
|
||||
m_useDecorCache = m_decorValid && (m_decorKey == SceneDecorHash());
|
||||
m_useDecorCache = true;
|
||||
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
@@ -6139,16 +6189,15 @@ namespace jdi
|
||||
|
||||
PaintTimeAxis(g, timeTicks, timeStep);
|
||||
|
||||
// 装饰层:命中缓存则上一轮网格循环已经跳过绘制,这里整块贴回;
|
||||
// 否则本轮已经真正绘制了装饰,立即捕获(保证像素与指纹同源)。
|
||||
if (m_useDecorCache)
|
||||
// 装饰层:缓存与当前内容同源则直接贴回;否则先把装饰绘制进透明缓存位图再贴回。
|
||||
if (!m_decorValid || m_decorKey != SceneDecorHash())
|
||||
{
|
||||
BlitDecorCache(g);
|
||||
}
|
||||
else if (CaptureDecorCache(g))
|
||||
{
|
||||
m_decorKey = SceneDecorHash();
|
||||
if (RenderDecorCache())
|
||||
{
|
||||
m_decorKey = SceneDecorHash();
|
||||
}
|
||||
}
|
||||
BlitDecorCache(g);
|
||||
|
||||
// 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容,
|
||||
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
|
||||
@@ -6274,11 +6323,8 @@ namespace jdi
|
||||
if (badge.Width > 0.0f)
|
||||
{
|
||||
const float radius = kLaneBadgeHeight * m_scale * 0.5f;
|
||||
// 不透明底色铺满外接矩形(与缓存里的画法一致,避免圆角之外残留旧像素)。
|
||||
const gd::RectF base(std::floor(badge.X) - 1.0f, std::floor(badge.Y) - 1.0f,
|
||||
std::ceil(badge.X + badge.Width) - std::floor(badge.X) + 2.0f,
|
||||
std::ceil(badge.Y + badge.Height) - std::floor(badge.Y) + 2.0f);
|
||||
UiFillRect(g, base, m_theme->cardBg | 0xFF000000u);
|
||||
// 只重画**圆角形状本身**(不透明):圆角之外保持"贴图层"的像素,
|
||||
// 因此不会出现直角白边。
|
||||
UiFillRoundRect(g, badge, radius, m_theme->accentSoft);
|
||||
UiDrawRoundRect(g, badge, radius, m_theme->accent, 1.0f);
|
||||
UiText(g, L"G" + std::to_wstring(m_hoverBadge + 1), m_fonts->monoSmall, badge,
|
||||
@@ -6320,17 +6366,18 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 条目托底高亮:**不透明**圆角矩形(避免半透明叠加出杂色),并在其上**原样重绘
|
||||
// 色块与文字** —— 否则不透明托底会把图例内容整块盖住(只剩一个空色块)。
|
||||
// 条目悬停高亮:**圆角托底 + 与选中态相同的高亮**(用户明确要求"悬停效果与 GX 标签
|
||||
// 相同、选中状态与悬停提示效果相同"),并在其上**原样重绘色块与文字** ——
|
||||
// 不透明托底若只画底色会把图例内容整块盖住(只剩一个空色块)。
|
||||
for (const LegendHit& hit : m_legendHits)
|
||||
{
|
||||
if (hit.ref == m_hoverLegend)
|
||||
{
|
||||
const float radius = kLegendPlateRadius * m_scale;
|
||||
const float radius = (hit.radius > 0.0f) ? hit.radius : kLegendPlateRadius * m_scale;
|
||||
UiFillRoundRect(g, hit.rect, radius, m_theme->accentSoft);
|
||||
UiDrawRoundRect(g, hit.rect, radius, m_theme->accent, 1.0f);
|
||||
UiDrawSwatch(g, hit.swatch, hit.swatchColor, hit.swatchOpacity, 1.5f * m_scale, m_scale);
|
||||
UiText(g, hit.label, m_fonts->uiSmall, hit.text, hit.textColor, TextAlign::LEFT);
|
||||
UiText(g, hit.label, m_fonts->uiSmall, hit.text, m_theme->textStrong, TextAlign::LEFT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -6771,6 +6818,11 @@ namespace jdi
|
||||
std::vector<double> timeTicks;
|
||||
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
|
||||
|
||||
// ⚠️ **补丁必须就地绘制装饰**(而不是"跳过装饰"):补丁范围覆盖整个受影响网格
|
||||
// (含网格左缘的 Y 轴胶囊与图例),若跳过绘制,这部分像素会被网格底色/波形覆盖,
|
||||
// 而补丁结果还会写回场景缓存 —— 表现为"改样式时图例/Y 轴读数闪掉一下"。
|
||||
const bool savedUseDecorCache = m_useDecorCache;
|
||||
m_useDecorCache = false;
|
||||
for (const int laneIndex : repaint)
|
||||
{
|
||||
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
|
||||
@@ -6778,6 +6830,7 @@ namespace jdi
|
||||
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
|
||||
}
|
||||
}
|
||||
m_useDecorCache = savedUseDecorCache;
|
||||
g.Restore(saveState);
|
||||
}
|
||||
|
||||
@@ -7176,7 +7229,7 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_decorDc != nullptr && m_decorW == width && m_decorH == height)
|
||||
if (m_decorDc != nullptr && m_decorBits != nullptr && m_decorW == width && m_decorH == height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -7186,15 +7239,37 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_decorDc = CreateCompatibleDC(screen);
|
||||
m_decorBitmap = CreateCompatibleBitmap(screen, width, height);
|
||||
// ⚠️ **必须用 32bpp DIB 段**(而不是 `CreateCompatibleBitmap`):
|
||||
// 装饰要带真 alpha(圆角之外全透明),才能用 `AlphaBlend` 合成到波形之上;
|
||||
// 同时 DIB 段的像素可直接写(用于 alpha 修补与清空)。
|
||||
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 (m_decorDc == nullptr || m_decorBitmap == nullptr)
|
||||
if (dib == nullptr || bits == nullptr)
|
||||
{
|
||||
ReleaseDecorCache();
|
||||
if (dib != nullptr)
|
||||
{
|
||||
DeleteObject(dib);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
m_decorOldBitmap = SelectObject(m_decorDc, m_decorBitmap);
|
||||
HDC dc = CreateCompatibleDC(nullptr);
|
||||
if (dc == nullptr)
|
||||
{
|
||||
DeleteObject(dib);
|
||||
return false;
|
||||
}
|
||||
m_decorOldBitmap = SelectObject(dc, dib);
|
||||
m_decorDc = dc;
|
||||
m_decorBitmap = dib;
|
||||
m_decorBits = bits;
|
||||
m_decorW = width;
|
||||
m_decorH = height;
|
||||
return true;
|
||||
@@ -7219,57 +7294,103 @@ namespace jdi
|
||||
DeleteObject(m_decorBitmap);
|
||||
m_decorBitmap = nullptr;
|
||||
}
|
||||
m_decorBits = nullptr;
|
||||
m_decorW = 0;
|
||||
m_decorH = 0;
|
||||
}
|
||||
|
||||
bool PlotView::CaptureDecorCache(gd::Graphics& g)
|
||||
void* PlotView::ResolveAlphaBlend()
|
||||
{
|
||||
if (m_decorRects.empty())
|
||||
// `AlphaBlend` 由 msimg32 导出、并由 gdi32 的 `GdiAlphaBlend` 实现;
|
||||
// 两个来源都试一遍,取到即缓存(静态局部变量,仅解析一次)。
|
||||
static void* cached = nullptr;
|
||||
static bool resolved = false;
|
||||
if (resolved)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
resolved = true;
|
||||
HMODULE gdi = GetModuleHandleW(L"gdi32.dll");
|
||||
if (gdi != nullptr)
|
||||
{
|
||||
cached = reinterpret_cast<void*>(GetProcAddress(gdi, "GdiAlphaBlend"));
|
||||
}
|
||||
if (cached == nullptr)
|
||||
{
|
||||
HMODULE msimg = LoadLibraryW(L"msimg32.dll");
|
||||
if (msimg != nullptr)
|
||||
{
|
||||
cached = reinterpret_cast<void*>(GetProcAddress(msimg, "AlphaBlend"));
|
||||
}
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
bool PlotView::RenderDecorCache()
|
||||
{
|
||||
m_decorValid = false;
|
||||
if (m_theme == nullptr || m_fonts == nullptr || m_lanes.empty())
|
||||
{
|
||||
m_decorValid = false;
|
||||
return false;
|
||||
}
|
||||
// 用绘图区矩形作为缓存位图尺寸(装饰都在绘图区内)。
|
||||
const int width = static_cast<int>(std::ceil(m_area.Width));
|
||||
const int height = static_cast<int>(std::ceil(m_area.Height));
|
||||
// 位图覆盖「客户区原点 → 绘图区右下角」:绘制坐标与正常绘制完全一致,
|
||||
// 不需要任何平移(GDI 文本不认 GDI+ 变换,平移会让文字错位)。
|
||||
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 (!EnsureDecorCache(width, height))
|
||||
{
|
||||
m_decorValid = false;
|
||||
return false;
|
||||
}
|
||||
HDC source = g.GetHDC();
|
||||
if (source == nullptr)
|
||||
// 清空为全透明(圆角之外必须保持透明,贴回时才不会盖住波形)。
|
||||
std::memset(m_decorBits, 0, static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4u);
|
||||
|
||||
m_decorRects.clear();
|
||||
{
|
||||
m_decorValid = false;
|
||||
return false;
|
||||
gd::Graphics g(m_decorDc);
|
||||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||
|
||||
const bool savedUse = m_useDecorCache;
|
||||
m_useDecorCache = false; // 本次是"真正绘制":不允许走"跳过绘制"的分支
|
||||
m_renderDecorOnly = true; // 只画装饰(跳过网格线与波形)
|
||||
const std::vector<double> noTicks;
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
PaintLane(g, static_cast<int>(index), m_lanes[index], noTicks);
|
||||
}
|
||||
m_renderDecorOnly = false;
|
||||
m_useDecorCache = savedUse;
|
||||
}
|
||||
const int originX = static_cast<int>(std::floor(m_area.X));
|
||||
const int originY = static_cast<int>(std::floor(m_area.Y));
|
||||
bool any = false;
|
||||
|
||||
// ⚠️ **alpha 修补**:GDI 的文本绘制(`DrawTextW`)会把它写入的像素 alpha 清成 0
|
||||
// (GDI 不维护 alpha),若原样保留,贴上后文字位置会变成"透明洞"(透出波形)。
|
||||
// 在这里把装饰矩形内"有颜色但 alpha 为 0"的像素还原为不透明即可 —— 装饰本身
|
||||
// 是画在不透明底板上的,这些像素本来就该不透明。
|
||||
for (const gd::RectF& rect : m_decorRects)
|
||||
{
|
||||
const int x0 = static_cast<int>(std::floor(rect.X)) - originX;
|
||||
const int y0 = static_cast<int>(std::floor(rect.Y)) - originY;
|
||||
const int x1 = static_cast<int>(std::ceil(rect.X + rect.Width)) - originX;
|
||||
const int y1 = static_cast<int>(std::ceil(rect.Y + rect.Height)) - originY;
|
||||
if (x1 <= x0 || y1 <= y0)
|
||||
const int x0 = std::max(0, static_cast<int>(std::floor(rect.X)));
|
||||
const int y0 = std::max(0, static_cast<int>(std::floor(rect.Y)));
|
||||
const int x1 = std::min(width, static_cast<int>(std::ceil(rect.X + rect.Width)));
|
||||
const int y1 = std::min(height, static_cast<int>(std::ceil(rect.Y + rect.Height)));
|
||||
for (int y = y0; y < y1; ++y)
|
||||
{
|
||||
continue;
|
||||
unsigned char* row = static_cast<unsigned char*>(m_decorBits) +
|
||||
(static_cast<std::size_t>(y) * static_cast<std::size_t>(width)) * 4u;
|
||||
for (int x = x0; x < x1; ++x)
|
||||
{
|
||||
unsigned char* p = row + static_cast<std::size_t>(x) * 4u;
|
||||
if (p[3] == 0 && (p[0] | p[1] | p[2]) != 0)
|
||||
{
|
||||
p[3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
BitBlt(m_decorDc, x0, y0, x1 - x0, y1 - y0, source, x0 + originX, y0 + originY, SRCCOPY);
|
||||
any = true;
|
||||
}
|
||||
g.ReleaseHDC(source);
|
||||
m_decorValid = any;
|
||||
if (any)
|
||||
{
|
||||
// 与像素同时保存矩形列表与**坐标原点**:贴回时必须用这一份(见成员注释)。
|
||||
m_decorCapturedRects = m_decorRects;
|
||||
m_decorOriginX = originX;
|
||||
m_decorOriginY = originY;
|
||||
}
|
||||
return any;
|
||||
|
||||
m_decorCapturedRects = m_decorRects;
|
||||
m_decorValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::BlitDecorCache(gd::Graphics& g)
|
||||
@@ -7278,29 +7399,41 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void* proc = ResolveAlphaBlend();
|
||||
if (proc == nullptr)
|
||||
{
|
||||
return false; // 极小概率:系统不提供 AlphaBlend(贴回会带透明洞,故直接放弃)
|
||||
}
|
||||
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;
|
||||
}
|
||||
// 用**捕获时**的原点推算源偏移,但按**绝对坐标**写入:
|
||||
// 即使两帧的 `m_area` 不同(宽缓冲区渲染会临时改写它),装饰也不会错位。
|
||||
// 装饰位图原点 = 客户区 (0,0):源坐标与目标坐标**相同**。
|
||||
BLENDFUNCTION fn = {};
|
||||
fn.BlendOp = AC_SRC_OVER;
|
||||
fn.SourceConstantAlpha = 255;
|
||||
fn.AlphaFormat = AC_SRC_ALPHA; // 源为**预乘** alpha(GDI+ 写入 DIB 段即预乘)
|
||||
bool any = false;
|
||||
for (const gd::RectF& rect : m_decorCapturedRects)
|
||||
{
|
||||
const int destX = static_cast<int>(std::floor(rect.X));
|
||||
const int destY = static_cast<int>(std::floor(rect.Y));
|
||||
const int srcX = destX - m_decorOriginX;
|
||||
const int srcY = destY - m_decorOriginY;
|
||||
const int w = static_cast<int>(std::ceil(rect.X + rect.Width)) - destX;
|
||||
const int h = static_cast<int>(std::ceil(rect.Y + rect.Height)) - destY;
|
||||
const int x = static_cast<int>(std::floor(rect.X));
|
||||
const int y = static_cast<int>(std::floor(rect.Y));
|
||||
const int w = static_cast<int>(std::ceil(rect.X + rect.Width)) - x;
|
||||
const int h = static_cast<int>(std::ceil(rect.Y + rect.Height)) - y;
|
||||
if (w <= 0 || h <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BitBlt(target, destX, destY, w, h, m_decorDc, srcX, srcY, SRCCOPY);
|
||||
if (blend(target, x, y, w, h, m_decorDc, x, y, w, h, fn) != FALSE)
|
||||
{
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
g.ReleaseHDC(target);
|
||||
return true;
|
||||
return any;
|
||||
}
|
||||
|
||||
bool PlotView::EnsureLaneLayer(LaneLayer& layer, int width, int height)
|
||||
@@ -8788,7 +8921,10 @@ namespace jdi
|
||||
// 渲染「宽缓冲区」时跳过:宽缓冲只存"基底"(网格/波形,不含派生曲线与装饰),
|
||||
// 派生曲线改由前台叠加层绘制(见 `PaintLaneDerivedOverlays`),
|
||||
// 这样拖派生参数滑块时基底不变、可复用宽缓冲像素。
|
||||
if (!m_renderWide)
|
||||
// ⚠️ `m_renderDecorOnly`(把装饰单独画进透明缓存位图)时**也必须跳过**:
|
||||
// 派生曲线是"随时间视界变化"的内容,若烘进装饰位图,贴回时会把旧视界的
|
||||
// 预览曲线永久留在画面上。
|
||||
if (!m_renderWide && !m_renderDecorOnly)
|
||||
{
|
||||
PaintLaneDerivedOverlays(g, laneIndex, lane);
|
||||
}
|
||||
@@ -8877,35 +9013,18 @@ namespace jdi
|
||||
pillZero.push_back(tick == 0.0);
|
||||
}
|
||||
|
||||
// 记录胶囊矩形(供装饰层捕获/贴回;略微外扩以容纳圆角与文字边缘)。
|
||||
// 记录胶囊矩形(供装饰层绘制/贴回;略微外扩以容纳圆角与文字边缘)。
|
||||
for (const gd::RectF& pill : pills)
|
||||
{
|
||||
m_decorRects.push_back(gd::RectF(pill.X - 2.0f * m_scale, pill.Y - 1.0f * m_scale,
|
||||
pill.Width + 4.0f * m_scale, pill.Height + 2.0f * m_scale));
|
||||
}
|
||||
// 命中装饰缓存:跳过重绘(文字 + 圆角底板),由上层整块贴回。
|
||||
// ⚠️ 底板是**圆角且不透明**:装饰位图带 alpha,圆角之外必须保持全透明,
|
||||
// 贴回时才能透出当前波形(避免"圆角外多出一圈直角白边")。
|
||||
if (!m_useDecorCache)
|
||||
{
|
||||
if (!pills.empty())
|
||||
{
|
||||
// 胶囊底板**不透明**:装饰像素因此与时间视界无关,可以跨视界整块复用;
|
||||
// 半透明底板会把旧视界的波形混进缓存像素,平移贴回时在底板内部留下残影。
|
||||
// ⚠️ 先用不透明底色**铺满每个胶囊的外接矩形**(含圆角之外的几个像素),
|
||||
// 再画圆角胶囊:否则圆角之外的波形像素会被装饰缓存烘进去,
|
||||
// 平移贴回时在胶囊边缘留下一圈"旧波形残留"。
|
||||
const std::uint32_t pillBase = m_theme->cardBg | 0xFF000000u;
|
||||
for (const gd::RectF& pill : pills)
|
||||
{
|
||||
const gd::RectF outer(pill.X - 2.0f * m_scale, pill.Y - 1.0f * m_scale,
|
||||
pill.Width + 4.0f * m_scale, pill.Height + 2.0f * m_scale);
|
||||
UiFillRect(g,
|
||||
gd::RectF(std::floor(outer.X) - 1.0f, std::floor(outer.Y) - 1.0f,
|
||||
std::ceil(outer.X + outer.Width) - std::floor(outer.X) + 2.0f,
|
||||
std::ceil(outer.Y + outer.Height) - std::floor(outer.Y) + 2.0f),
|
||||
pillBase);
|
||||
}
|
||||
UiFillRoundRects(g, pills, 3.0f * m_scale, pillBase);
|
||||
}
|
||||
UiFillRoundRects(g, pills, 3.0f * m_scale, m_theme->cardBg | 0xFF000000u);
|
||||
for (std::size_t index = 0; index < pills.size(); ++index)
|
||||
{
|
||||
const gd::RectF& pill = pills[index];
|
||||
@@ -9115,16 +9234,17 @@ namespace jdi
|
||||
return gd::RectF(item.left - platePadX, top - platePadY, item.width + platePadX * 2.0f,
|
||||
height + platePadY * 2.0f);
|
||||
};
|
||||
// 不透明底色矩形:把托底外接的**整数像素**再外扩 1px。装饰缓存的捕获边界由
|
||||
// `floor/ceil` 得出,必须完全落在这块不透明填充之内,否则边缘会残留旧波形像素。
|
||||
const auto OpaqueBaseRect = [](const gd::RectF& rect)
|
||||
{
|
||||
const float x0 = std::floor(rect.X) - 1.0f;
|
||||
const float y0 = std::floor(rect.Y) - 1.0f;
|
||||
const float x1 = std::ceil(rect.X + rect.Width) + 1.0f;
|
||||
const float y1 = std::ceil(rect.Y + rect.Height) + 1.0f;
|
||||
return gd::RectF(x0, y0, x1 - x0, y1 - y0);
|
||||
};
|
||||
// 条目托底 = 与 GX 编号徽标同一套风格的**圆角矩形**(用户明确要求:
|
||||
// "相当于一个 GX 标签,只是内容换成了图例信息")。
|
||||
const float plateRadius = kLegendPlateRadius * m_scale;
|
||||
// 「+N」(图例空间不足)与「(无可见信号)」提示的矩形。
|
||||
// ⚠️ 它们同样属于装饰层、必须登记进 `m_decorRects`:否则会被画进装饰位图
|
||||
// 却因不在贴回列表里而永远看不见。
|
||||
const bool hasMoreText = (visibleCount > static_cast<int>(items.size())) &&
|
||||
(contentRight + 20.0f * m_scale <= maxX);
|
||||
const bool hasEmptyText = (visibleCount == 0);
|
||||
const gd::RectF moreTextRect(contentRight, top, 24.0f * m_scale, height);
|
||||
const gd::RectF emptyTextRect(legendBand.X, top, lane.rect.Width, height);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 2) 命中矩形:**无论是否命中装饰缓存都必须重建**。
|
||||
@@ -9149,11 +9269,13 @@ namespace jdi
|
||||
LegendHit hit;
|
||||
hit.ref = item.ref;
|
||||
hit.rect = PlateRect(item);
|
||||
hit.radius = plateRadius;
|
||||
hit.swatch =
|
||||
gd::RectF(item.left, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize);
|
||||
hit.text = gd::RectF(item.left + swatchSize + 5.0f * m_scale, top,
|
||||
std::max(8.0f, item.textWidth + 2.0f), height);
|
||||
hit.label = item.label;
|
||||
// 「选中」与「悬停」用同一套配色(用户明确要求),因此文字色在这里也按选中态取。
|
||||
hit.textColor = item.selected ? m_theme->textStrong : m_theme->text;
|
||||
if (resolved.style != nullptr)
|
||||
{
|
||||
@@ -9167,13 +9289,21 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 装饰矩形:**逐块托底**(徽标一块 + 每个条目一块),而不是覆盖整条图例带 ——
|
||||
// 覆盖整条带会把中间的波形像素也烘进缓存,平移贴回时露出"旧波形的像素残留"。
|
||||
// 装饰矩形:**逐块**(徽标一块 + 每个条目一块),而不是覆盖整条图例带 ——
|
||||
// 覆盖整条带没有必要,且逐块能减少装饰层贴图面积。
|
||||
m_decorRects.push_back(badgeRect);
|
||||
for (const LegendItem& item : items)
|
||||
{
|
||||
m_decorRects.push_back(PlateRect(item));
|
||||
}
|
||||
if (hasEmptyText)
|
||||
{
|
||||
m_decorRects.push_back(emptyTextRect);
|
||||
}
|
||||
if (hasMoreText)
|
||||
{
|
||||
m_decorRects.push_back(moreTextRect);
|
||||
}
|
||||
|
||||
// 命中装饰缓存:徽标与图例的像素改由装饰层贴回,跳过重绘(命中矩形已在上方重建)。
|
||||
if (m_useDecorCache)
|
||||
@@ -9182,14 +9312,14 @@ namespace jdi
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 3) 绘制。⚠️ 每块托底都先用**不透明底色铺满外接矩形**再画圆角:
|
||||
// 圆角之外的那几个像素若仍是波形,会被装饰缓存烘进去,平移贴回时表现为
|
||||
// "徽标 / 图例边缘残留旧波形"。底色 = 网格底色,铺满外接矩形在视觉上不可见。
|
||||
// 3) 绘制。托底是**圆角矩形**(与 GX 编号徽标同风格):
|
||||
// "一个 GX 标签,只是内容换成了图例信息"(用户明确要求)。
|
||||
// ⚠️ **不再铺任何外扩的不透明直角底色**:装饰位图带 alpha,圆角之外必须保持
|
||||
// 全透明,贴回时才能透出**当前**波形 —— 否则圆角外会多出一圈"直角白边"。
|
||||
// ⚠️ 徽标与条目的**悬停 / 激活高亮不在这里**(随鼠标变化,画在贴图之后,
|
||||
// 见 PaintLegendHighlight)—— 否则会被缓存冻结,悬停时看不到任何反馈。
|
||||
// ------------------------------------------------------------------
|
||||
{
|
||||
UiFillRect(g, OpaqueBaseRect(badgeRect), m_theme->cardBg | 0xFF000000u);
|
||||
UiFillRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->cardBg);
|
||||
UiDrawRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->border, 1.0f);
|
||||
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
|
||||
@@ -9203,10 +9333,11 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
const gd::RectF plate = PlateRect(item);
|
||||
UiFillRect(g, OpaqueBaseRect(plate), m_theme->cardBg | 0xFF000000u);
|
||||
UiFillRoundRect(g, plate, kLegendPlateRadius * m_scale,
|
||||
item.selected ? m_theme->selection : m_theme->cardBg);
|
||||
UiDrawRoundRect(g, plate, kLegendPlateRadius * m_scale,
|
||||
// 选中态 = 悬停态配色(`accentSoft` 底 + `accent` 描边),
|
||||
// 与 GX 编号徽标的鼠标悬停效果完全一致。
|
||||
UiFillRoundRect(g, plate, plateRadius,
|
||||
item.selected ? m_theme->accentSoft : m_theme->cardBg);
|
||||
UiDrawRoundRect(g, plate, plateRadius,
|
||||
item.selected ? m_theme->accent : m_theme->border, 1.0f);
|
||||
|
||||
// 改名编辑态:整块托底变成输入框(与左侧列表双击改名一致)。
|
||||
@@ -9226,23 +9357,17 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
if (visibleCount == 0)
|
||||
if (hasEmptyText)
|
||||
{
|
||||
UiText(g, L"(无可见信号)", m_fonts->uiSmall,
|
||||
gd::RectF(legendBand.X, top, lane.rect.Width, height), m_theme->textMuted,
|
||||
UiText(g, L"(无可见信号)", m_fonts->uiSmall, emptyTextRect, m_theme->textMuted,
|
||||
TextAlign::LEFT);
|
||||
}
|
||||
else if (static_cast<int>(items.size()) < visibleCount)
|
||||
else if (hasMoreText)
|
||||
{
|
||||
// 图例空间不足时提示剩余信号数量,避免误认为信号丢失。
|
||||
const std::wstring moreText =
|
||||
L"+" + std::to_wstring(visibleCount - static_cast<int>(items.size()));
|
||||
if (contentRight + 20.0f * m_scale <= maxX)
|
||||
{
|
||||
UiText(g, moreText, m_fonts->uiSmall,
|
||||
gd::RectF(contentRight, top, 24.0f * m_scale, height), m_theme->textMuted,
|
||||
TextAlign::LEFT);
|
||||
}
|
||||
UiText(g, moreText, m_fonts->uiSmall, moreTextRect, m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user