修复悬停高亮变长方形色块/联动失效 + 信号列表动画卡顿真因(行位图缓存)
一、悬停高亮恢复"整条曲线发光"(上一轮裁成鼠标附近一段 ⇒ 满幅方波上就是个长方形色块) 光晕的语义是"整条曲线一起发光",对编码器这种满幅方波,一列包络本来就跨满网格高, 裁成 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
@@ -1067,7 +1067,22 @@ namespace jdi
|
||||
*/
|
||||
void RefreshLanes();
|
||||
|
||||
/**
|
||||
* @brief 设置「前台正在播放 UI 动画」标志(信号列表滚动 / 分组折起展开动画)。
|
||||
*
|
||||
* ⚠️ 为 true 时**暂停网格图层预热**(`WarmLaneLayersStep`):预热每帧要重画 1 个网格图层,
|
||||
* 而单个巨型信号(2500 万采样)的图层重建实测 **10~30ms** —— 它会直接把 60fps 的
|
||||
* 动画打成一帧一跳(用户 2026-10-18 报告:信号列表只有文字,滚动/展开却明显卡顿)。
|
||||
* 动画结束后预热会自然继续(待重画队列不丢),因此只是"推迟",不改变最终画面。
|
||||
* @param [IN] busy 是否正在播放动画。
|
||||
*/
|
||||
void SetAnimationBusy(bool busy) { m_animationBusy = busy; }
|
||||
|
||||
private:
|
||||
/** @brief 前台是否正在播放 UI 动画(见 `SetAnimationBusy`)。 */
|
||||
bool m_animationBusy = false;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief 单个网格的绘制信息。
|
||||
*/
|
||||
@@ -2764,11 +2779,87 @@ namespace jdi
|
||||
* @param [IN] resolved 已解析的信号。
|
||||
* @param [IN] lane 该信号所在网格(单点信号需要它把数据点换算到屏幕坐标)。
|
||||
* @param [IN] cached 该信号的折线缓存项(包络带模式下会走带外扩填充)。
|
||||
* @param [IN] windowCenterX 描边窗口中心(屏幕 x);仅当 `windowed` 为 true 时生效。
|
||||
* @param [IN] windowed 是否只描边窗口内的顶点(见 `kHaloWindowHalfWidth` 的说明)。
|
||||
*/
|
||||
void PaintSignalHaloFor(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
const PolylineCacheEntry& cached, float windowCenterX, bool windowed);
|
||||
const PolylineCacheEntry& cached);
|
||||
|
||||
/**
|
||||
* @brief 绘制**悬停**光晕:把整条光晕渲染进一张离屏位图并缓存,命中时只贴图。
|
||||
*
|
||||
* ⚠️ 悬停光晕必须**沿整条曲线**发光(用户 2026-10-18 第二轮报告:上一版只画
|
||||
* 鼠标左右 ±72px 的一段,在满幅方波上看起来就是一个**长方形色块**,像选区框)。
|
||||
* 而整条光晕的直接绘制代价很高(稠密包络带填充实测 ~14ms/帧,加粗折线描边更是
|
||||
* 200ms+/帧)。折中方案:光晕的几何只取决于「信号 + 视图 + 网格几何 + 样式」,
|
||||
* **与鼠标位置无关**,因此渲染一次进离屏位图后每帧只贴图(~0.5ms),
|
||||
* 并随视图/几何/样式变化自动失效重建。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文(调用方已设置绘图区裁剪)。
|
||||
* @param [IN] resolved 已解析的信号。
|
||||
* @param [IN] lane 该信号所在网格。
|
||||
* @param [IN] cached 该信号的折线缓存项。
|
||||
*/
|
||||
void PaintSignalHaloCached(gd::Graphics& g, const ResolvedSignal& resolved,
|
||||
const LaneVisual& lane, const PolylineCacheEntry& cached);
|
||||
|
||||
/**
|
||||
* @brief 悬停光晕的离屏缓存(见 `PaintSignalHaloCached`)。
|
||||
*
|
||||
* 键 = 决定光晕几何的全部输入(与 `AcquirePolyline` 的键同源),
|
||||
* 唯独**不含网格的屏幕纵向位置**:滚动网格时只需按新位置贴同一张位图。
|
||||
*/
|
||||
struct HaloLayerCache
|
||||
{
|
||||
bool valid = false; /**< 位图内容是否有效。 */
|
||||
SignalRef ref; /**< 信号引用。 */
|
||||
const SignalGroup* group = nullptr; /**< 所属分组。 */
|
||||
std::size_t sampleCount = 0; /**< 采样点数。 */
|
||||
double viewMin = 0.0; /**< 时间轴显示下界。 */
|
||||
double viewMax = 0.0; /**< 时间轴显示上界。 */
|
||||
float plotX = 0.0f; /**< 绘图区左边界。 */
|
||||
float plotWidth = 0.0f; /**< 绘图区宽度。 */
|
||||
float laneHeight = 0.0f; /**< 网格高度。 */
|
||||
double laneYMin = 0.0; /**< 网格幅值下界。 */
|
||||
double laneYMax = 0.0; /**< 网格幅值上界。 */
|
||||
float lineWidth = 1.0f; /**< 线宽。 */
|
||||
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
|
||||
float gain = 1.0f; /**< 绘制系数。 */
|
||||
bool absolute = false; /**< 是否绝对值。 */
|
||||
bool mirrorVertical = false; /**< 是否垂直镜像。 */
|
||||
bool mirrorHorizontal = false; /**< 是否水平镜像。 */
|
||||
float offsetY = 0.0f; /**< Y 偏移。 */
|
||||
double offsetTime = 0.0; /**< 时间偏移。 */
|
||||
int originX = 0; /**< 位图左上角(屏幕 x)。 */
|
||||
int originY = 0; /**< 位图左上角(屏幕 y)。 */
|
||||
std::unique_ptr<gd::Bitmap> bitmap; /**< 32bpp ARGB 光晕图层(直通 alpha)。 */
|
||||
|
||||
/**
|
||||
* @brief 预乘 alpha 的 DIB 副本(每帧真正贴出去的那张)。
|
||||
*
|
||||
* ⚠️ 每帧贴图必须走 GDI `AlphaBlend` + `AC_SRC_ALPHA`:
|
||||
* `gd::Graphics::DrawImage` 贴一张 32bpp 半透明位图实测约 **6ms**(每帧都付),
|
||||
* 而 `AlphaBlend` 对同一张图约 **0.3ms**。
|
||||
* `AlphaBlend` 要求源位图的 RGB **已按 alpha 预乘**,因此重建时把
|
||||
* `bitmap` 拷进这个 DIB 并顺带做预乘(只付一次)。
|
||||
*/
|
||||
HDC dc = nullptr;
|
||||
HBITMAP dib = nullptr;
|
||||
void* bits = nullptr;
|
||||
int dibWidth = 0;
|
||||
int dibHeight = 0;
|
||||
};
|
||||
|
||||
mutable HaloLayerCache m_haloLayer;
|
||||
|
||||
/** @brief 释放悬停光晕的离屏位图(析构 / 重建尺寸时使用)。 */
|
||||
void ReleaseHaloLayer();
|
||||
|
||||
/**
|
||||
* @brief 把 `m_haloLayer.bitmap` 拷进预乘 alpha 的 DIB(必要时先创建/重建 DIB)。
|
||||
* @param [IN] width 需要的位图宽度。
|
||||
* @param [IN] height 需要的位图高度。
|
||||
* @return DIB 可用返回 true。
|
||||
*/
|
||||
bool UploadHaloLayer(int width, int height);
|
||||
|
||||
/**
|
||||
* @brief 绘制某条信号的悬停/拖拽光晕(折线描边版本)。
|
||||
@@ -2781,12 +2872,9 @@ namespace jdi
|
||||
* @param [IN] lane 该信号所在网格。
|
||||
* @param [IN] points 折线顶点。
|
||||
* @param [IN] breaks 每段折线的起始下标。
|
||||
* @param [IN] windowCenterX 描边窗口中心(屏幕 x);仅当 `windowed` 为 true 时生效。
|
||||
* @param [IN] windowed 是否只描边窗口内的顶点(见 `kHaloWindowHalfWidth` 的说明)。
|
||||
*/
|
||||
void PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
const std::vector<gd::PointF>& points, const std::vector<std::size_t>& breaks,
|
||||
float windowCenterX, bool windowed);
|
||||
const std::vector<gd::PointF>& points, const std::vector<std::size_t>& breaks);
|
||||
|
||||
/**
|
||||
* @brief 单点信号那个数据点绘制时的半径(与散点绘制同源)。
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace jdi
|
||||
ReleaseDecorCache();
|
||||
ReleaseWideBuffer();
|
||||
ReleasePanelCache();
|
||||
ReleaseHaloLayer();
|
||||
}
|
||||
|
||||
void PlotView::Setup(const UiFonts* fonts, const Theme* theme)
|
||||
|
||||
@@ -148,23 +148,15 @@ namespace jdi
|
||||
inline const float kMarkerTriangleGap = 4.0f;
|
||||
inline const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */
|
||||
/**
|
||||
* @brief 悬停光晕"描边窗口"的半宽(逻辑像素,使用时乘缩放)。
|
||||
* @brief 悬停光晕缓存中「关闭抗锯齿」的顶点数阈值。
|
||||
*
|
||||
* ⚠️ 光晕是**用加粗 GDI+ 折线描边**画出来的,而 GDI+ 描边的代价与
|
||||
* 「线段数 × 每段覆盖的行数」成正比:编码器/方波这类**满幅竖向锯齿**信号
|
||||
* 1 条 2503 顶点的折线实测要 **210ms/帧**(用户 2026-10-18 报告:
|
||||
* 鼠标划过巨型信号的曲线时整帧卡死、浮动提示不再跟随鼠标)。
|
||||
* 因此只描边鼠标左右各 `kHaloWindowHalfWidth` 像素内的顶点:
|
||||
* 顶点数降到几十个 ⇒ 单帧 <1ms,而"鼠标附近那条曲线发光"正是悬停提示该有的语义。
|
||||
* 光晕是**沿整条曲线**画出来的(不能只画鼠标附近的一段:在满幅方波上那看起来
|
||||
* 就是一个长方形色块,像选区框 —— 用户 2026-10-18 第二轮报告)。它的代价只
|
||||
* 在「光晕缓存重建」时付一次,因此通常可直接用抗锯齿;但顶点很多的区间
|
||||
* (深放大后的方波:每段都跨满网格高)GDI+ 抗锯齿的扫描线代价 ≈ 线段数 × 覆盖行数,
|
||||
* 实测可差 20 倍以上。这类密集情形关闭抗锯齿(几何完全不变,半透明光晕上看不出差别)。
|
||||
*/
|
||||
inline const float kHaloWindowHalfWidth = 72.0f;
|
||||
/**
|
||||
* @brief 顶点数达到该值才启用描边窗口裁剪。
|
||||
*
|
||||
* 顶点少的信号(一屏内只有几百个采样)整条描边的代价本来就极小,
|
||||
* 整条发光观感更好,因此不做裁剪。
|
||||
*/
|
||||
inline const size_t kHaloWindowMinPoints = 360;
|
||||
inline const size_t kHaloNoAaMinPoints = 360;
|
||||
inline const size_t kPolylineCacheLimit = 64; /**< 折线顶点缓存条目上限。 */
|
||||
inline const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */
|
||||
inline const float kMaxScatterRadius = 12.0f; /**< 散点最大半径(逻辑像素)。 */
|
||||
|
||||
@@ -1043,26 +1043,25 @@ namespace jdi
|
||||
}
|
||||
|
||||
void PlotView::PaintSignalHaloFor(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
const PolylineCacheEntry& cached, float windowCenterX, bool windowed)
|
||||
const PolylineCacheEntry& cached)
|
||||
{
|
||||
if (resolved.style == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// 稠密信号(走"像素列包络带"):光晕改为**填充与曲线本身同一几何的包络带**。
|
||||
// 稠密信号(走"像素列包络带"):光晕 = **填充与曲线本身同一几何**的包络带。
|
||||
//
|
||||
// ⚠️ 绝不能用「加粗折线描边」画稠密信号的光晕:包络带的顶点序列是
|
||||
// 「每列上/下包络交替」的满幅竖锯齿,GDI+ 描边的代价 = 线段数 × 覆盖行数,
|
||||
// 实测 2503 个顶点要 **210ms/帧**(用户 2026-10-18 报告:鼠标划过巨型信号的
|
||||
// 曲线时整帧卡死、浮动提示不再跟随鼠标)。而 `FillPath` 同一条几何只需 ~1ms,
|
||||
// 且观感更贴切(整条曲线一起"发光",与曲线本身逐像素同源)。
|
||||
// ⚠️ 填充也**只填鼠标附近的一段**(见 `kHaloWindowHalfWidth`):半透明填充
|
||||
// 的代价与「覆盖面积 + 路径顶点数」成正比,整条满幅带实测 ~13ms/帧。
|
||||
// 实测 2503 个顶点要 **210ms**。而 `FillPath` 同一条几何只要 ~14ms,
|
||||
// 且观感与原实现(沿折线描粗)一致:整条曲线一起"发光"。
|
||||
// ⚠️ 光晕**必须覆盖整条曲线**:曾经只画鼠标附近 ±72px 的一段,在满幅方波上
|
||||
// 看起来就是一个**长方形色块**(像选区框),用户 2026-10-18 第二轮明确报告。
|
||||
// 代价问题改由 `PaintSignalHaloCached`(渲染一次 + 每帧贴图)解决。
|
||||
// ------------------------------------------------------------------
|
||||
if (resolved.style->mode != DrawMode::SCATTER && cached.bandMode && !cached.bandBreaks.empty())
|
||||
{
|
||||
const float halfWidth = kHaloWindowHalfWidth * m_scale;
|
||||
const int savedSmoothing = g.GetSmoothingMode();
|
||||
g.SetSmoothingMode(gd::SmoothingModeNone); // 半透明填充:抗锯齿没有观感收益
|
||||
gd::GraphicsPath bandPath;
|
||||
@@ -1077,44 +1076,11 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 段内布局 = 「上包络正向(`half` 个)+ 下包络反向(`half` 个)」
|
||||
//(见 `EmitEnvelopeBand`),因此窗口只需换算成列区间。
|
||||
// 段内布局 = 「上包络正向(`half` 个)+ 下包络反向(`half` 个)」。
|
||||
const std::size_t half = total / 2;
|
||||
std::size_t columnFirst = 0;
|
||||
std::size_t columnLast = half;
|
||||
if (windowed)
|
||||
{
|
||||
while (columnFirst < half &&
|
||||
cached.band[start + columnFirst].X < windowCenterX - halfWidth - 1.0f)
|
||||
{
|
||||
++columnFirst;
|
||||
}
|
||||
while (columnLast > columnFirst &&
|
||||
cached.band[start + columnLast - 1].X > windowCenterX + halfWidth + 1.0f)
|
||||
{
|
||||
--columnLast;
|
||||
}
|
||||
// 两侧各多含 1 列,避免窗口边界处的半透明边缘被切出直边。
|
||||
if (columnFirst > 0)
|
||||
{
|
||||
--columnFirst;
|
||||
}
|
||||
if (columnLast < half)
|
||||
{
|
||||
++columnLast;
|
||||
}
|
||||
}
|
||||
if (columnLast <= columnFirst + 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t columns = columnLast - columnFirst;
|
||||
bandPath.StartFigure();
|
||||
bandPath.AddLines(cached.band.data() + start + columnFirst, static_cast<INT>(columns));
|
||||
// 下包络反向存储:列 c 在 `start + half + (half - 1 - c)`,
|
||||
// 因此列区间 `[columnFirst, columnLast)` 对应一段**正向可读**的连续区间。
|
||||
bandPath.AddLines(cached.band.data() + start + half + (half - columnLast),
|
||||
static_cast<INT>(columns));
|
||||
bandPath.AddLines(cached.band.data() + start, static_cast<INT>(half));
|
||||
bandPath.AddLines(cached.band.data() + start + half, static_cast<INT>(half));
|
||||
bandPath.CloseFigure();
|
||||
}
|
||||
if (bandPath.GetPointCount() > 0)
|
||||
@@ -1126,13 +1092,231 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
// 其余情形一律用折线描边(与"之前"的画法一致)。
|
||||
PaintSignalHalo(g, resolved, lane, cached.points, cached.breaks, windowCenterX, windowed);
|
||||
PaintSignalHalo(g, resolved, lane, cached.points, cached.breaks);
|
||||
}
|
||||
|
||||
void PlotView::PaintSignalHaloCached(gd::Graphics& g, const ResolvedSignal& resolved,
|
||||
const LaneVisual& lane, const PolylineCacheEntry& cached)
|
||||
{
|
||||
if (resolved.style == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 光晕只可能画在该网格内:位图 = 网格矩形 ∩ 绘图区(外加少量余量容纳加粗线宽)。
|
||||
const float pad = (resolved.style->lineWidth + 8.0f) * m_scale;
|
||||
const float left = std::max(lane.rect.X, m_plotRect.X) - pad;
|
||||
const float top = std::max(lane.rect.Y, m_plotRect.Y) - pad;
|
||||
const float right =
|
||||
std::min(lane.rect.X + lane.rect.Width, m_plotRect.X + m_plotRect.Width) + pad;
|
||||
const float bottom =
|
||||
std::min(lane.rect.Y + lane.rect.Height, m_plotRect.Y + m_plotRect.Height) + pad;
|
||||
if (!(right > left + 1.0f) || !(bottom > top + 1.0f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int targetX = static_cast<int>(std::floor(left));
|
||||
const int targetY = static_cast<int>(std::floor(top));
|
||||
const int width = static_cast<int>(std::ceil(right)) - targetX;
|
||||
const int height = static_cast<int>(std::ceil(bottom)) - targetY;
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HaloLayerCache& cache = m_haloLayer;
|
||||
// 键 = 决定光晕几何的全部输入(与 `AcquirePolyline` 的键同源)。
|
||||
// ⚠️ **不含网格的屏幕位置**:滚动网格时几何不变,只需按新位置贴同一张位图。
|
||||
const bool sameSize = cache.bitmap && static_cast<int>(cache.bitmap->GetWidth()) == width &&
|
||||
static_cast<int>(cache.bitmap->GetHeight()) == height;
|
||||
const bool sameKey =
|
||||
cache.valid && sameSize && cache.ref == cached.ref && cache.group == cached.group &&
|
||||
cache.sampleCount == cached.sampleCount && cache.viewMin == cached.viewMin &&
|
||||
cache.viewMax == cached.viewMax && cache.plotX == cached.plotX &&
|
||||
cache.plotWidth == cached.plotWidth && cache.laneHeight == cached.laneHeight &&
|
||||
cache.laneYMin == cached.laneYMin && cache.laneYMax == cached.laneYMax &&
|
||||
cache.lineWidth == cached.lineWidth && cache.mode == resolved.style->mode &&
|
||||
cache.gain == resolved.style->gain && cache.absolute == resolved.style->absolute &&
|
||||
cache.mirrorVertical == resolved.style->mirrorVertical &&
|
||||
cache.mirrorHorizontal == resolved.style->mirrorHorizontal &&
|
||||
cache.offsetY == resolved.style->offsetY &&
|
||||
cache.offsetTime == resolved.style->offsetTime;
|
||||
|
||||
if (!sameKey)
|
||||
{
|
||||
if (!sameSize)
|
||||
{
|
||||
cache.bitmap = std::make_unique<gd::Bitmap>(width, height, PixelFormat32bppARGB);
|
||||
}
|
||||
{
|
||||
gd::Graphics layerGraphics(cache.bitmap.get());
|
||||
layerGraphics.Clear(gd::Color(0, 0, 0, 0));
|
||||
layerGraphics.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
layerGraphics.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
// 位图坐标 = 屏幕坐标 - 目标原点,光晕几何可直接沿用屏幕坐标。
|
||||
layerGraphics.TranslateTransform(static_cast<gd::REAL>(-targetX),
|
||||
static_cast<gd::REAL>(-targetY));
|
||||
if (cached.points.size() > kHaloNoAaMinPoints)
|
||||
{
|
||||
// 顶点很多的区间(例如深放大后的方波:每段都跨满网格高):
|
||||
// 抗锯齿描边的扫描线代价 ≈ 线段数 × 覆盖行数(实测可差 20 倍),
|
||||
// 而这么密的半透明光晕上平滑作用肉眼不可辨。几何完全不变。
|
||||
layerGraphics.SetSmoothingMode(gd::SmoothingModeNone);
|
||||
}
|
||||
PaintSignalHaloFor(layerGraphics, resolved, lane, cached);
|
||||
}
|
||||
cache.ref = cached.ref;
|
||||
cache.group = cached.group;
|
||||
cache.sampleCount = cached.sampleCount;
|
||||
cache.viewMin = cached.viewMin;
|
||||
cache.viewMax = cached.viewMax;
|
||||
cache.plotX = cached.plotX;
|
||||
cache.plotWidth = cached.plotWidth;
|
||||
cache.laneHeight = cached.laneHeight;
|
||||
cache.laneYMin = cached.laneYMin;
|
||||
cache.laneYMax = cached.laneYMax;
|
||||
cache.lineWidth = cached.lineWidth;
|
||||
cache.mode = resolved.style->mode;
|
||||
cache.gain = resolved.style->gain;
|
||||
cache.absolute = resolved.style->absolute;
|
||||
cache.mirrorVertical = resolved.style->mirrorVertical;
|
||||
cache.mirrorHorizontal = resolved.style->mirrorHorizontal;
|
||||
cache.offsetY = resolved.style->offsetY;
|
||||
cache.offsetTime = resolved.style->offsetTime;
|
||||
cache.originX = targetX;
|
||||
cache.originY = targetY;
|
||||
cache.valid = true;
|
||||
if (!UploadHaloLayer(width, height))
|
||||
{
|
||||
cache.valid = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 贴图:`AlphaBlend` + `AC_SRC_ALPHA`(源位图已预乘)。命中缓存时只做这一次位块搬运。
|
||||
if (cache.dc == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const HDC targetDc = g.GetHDC();
|
||||
if (targetDc != nullptr)
|
||||
{
|
||||
BLENDFUNCTION blend = {};
|
||||
blend.BlendOp = AC_SRC_OVER;
|
||||
blend.SourceConstantAlpha = 0xFF;
|
||||
blend.AlphaFormat = AC_SRC_ALPHA;
|
||||
::AlphaBlend(targetDc, targetX, targetY, width, height, cache.dc, 0, 0, width, height,
|
||||
blend);
|
||||
g.ReleaseHDC(targetDc);
|
||||
}
|
||||
}
|
||||
|
||||
bool PlotView::UploadHaloLayer(int width, int height)
|
||||
{
|
||||
HaloLayerCache& cache = m_haloLayer;
|
||||
if (!cache.bitmap)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (cache.dib == nullptr || cache.dibWidth != width || cache.dibHeight != height)
|
||||
{
|
||||
ReleaseHaloLayer();
|
||||
BITMAPINFO info = {};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = width;
|
||||
info.bmiHeader.biHeight = -height; // 自上而下(与 GDI+ 的扫描行顺序一致)
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
HDC memoryDc = CreateCompatibleDC(nullptr);
|
||||
if (memoryDc == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void* bits = nullptr;
|
||||
HBITMAP dib = CreateDIBSection(memoryDc, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
if (dib == nullptr || bits == nullptr)
|
||||
{
|
||||
DeleteDC(memoryDc);
|
||||
if (dib != nullptr)
|
||||
{
|
||||
DeleteObject(dib);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
SelectObject(memoryDc, dib);
|
||||
cache.dc = memoryDc;
|
||||
cache.dib = dib;
|
||||
cache.bits = bits;
|
||||
cache.dibWidth = width;
|
||||
cache.dibHeight = height;
|
||||
}
|
||||
|
||||
// 预乘 alpha:`AlphaBlend` 的 `AC_SRC_ALPHA` 要求源 RGB 已乘过 alpha。
|
||||
// 用 256×256 查表代替逐像素除法(1.6M 像素时可省 ~5ms)。
|
||||
static std::uint8_t premultiply[256 * 256];
|
||||
static bool premultiplyReady = false;
|
||||
if (!premultiplyReady)
|
||||
{
|
||||
for (int alpha = 0; alpha < 256; ++alpha)
|
||||
{
|
||||
for (int value = 0; value < 256; ++value)
|
||||
{
|
||||
premultiply[(alpha << 8) | value] =
|
||||
static_cast<std::uint8_t>((value * alpha + 127) / 255);
|
||||
}
|
||||
}
|
||||
premultiplyReady = true;
|
||||
}
|
||||
gd::Rect rect(0, 0, width, height);
|
||||
gd::BitmapData data = {};
|
||||
if (cache.bitmap->LockBits(&rect, gd::ImageLockModeRead, PixelFormat32bppARGB, &data) !=
|
||||
gd::Ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
unsigned char* destination = static_cast<unsigned char*>(cache.bits);
|
||||
for (int y = 0; y < height; ++y)
|
||||
{
|
||||
const unsigned char* source =
|
||||
static_cast<const unsigned char*>(data.Scan0) + static_cast<std::size_t>(y) * data.Stride;
|
||||
unsigned char* row = destination + static_cast<std::size_t>(y) * width * 4u;
|
||||
for (int x = 0; x < width; ++x)
|
||||
{
|
||||
const int alpha = source[x * 4 + 3];
|
||||
const int table = alpha << 8;
|
||||
row[x * 4 + 0] = premultiply[table | source[x * 4 + 0]];
|
||||
row[x * 4 + 1] = premultiply[table | source[x * 4 + 1]];
|
||||
row[x * 4 + 2] = premultiply[table | source[x * 4 + 2]];
|
||||
row[x * 4 + 3] = static_cast<unsigned char>(alpha);
|
||||
}
|
||||
}
|
||||
cache.bitmap->UnlockBits(&data);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::ReleaseHaloLayer()
|
||||
{
|
||||
HaloLayerCache& cache = m_haloLayer;
|
||||
if (cache.dc != nullptr)
|
||||
{
|
||||
DeleteDC(cache.dc);
|
||||
cache.dc = nullptr;
|
||||
}
|
||||
if (cache.dib != nullptr)
|
||||
{
|
||||
DeleteObject(cache.dib);
|
||||
cache.dib = nullptr;
|
||||
}
|
||||
cache.bits = nullptr;
|
||||
cache.dibWidth = 0;
|
||||
cache.dibHeight = 0;
|
||||
cache.valid = false;
|
||||
}
|
||||
|
||||
void PlotView::PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
const std::vector<gd::PointF>& points,
|
||||
const std::vector<std::size_t>& breaks, float windowCenterX,
|
||||
bool windowed)
|
||||
const std::vector<std::size_t>& breaks)
|
||||
{
|
||||
// ⚠️ **只有单个数据点**的信号(默认样式是「点线」,那个点本身是一个粗圆点):
|
||||
// 折线光晕无从下手(不足两个顶点),改为在数据点外包一圈强调色圆环。
|
||||
@@ -1169,18 +1353,11 @@ namespace jdi
|
||||
const float ringRadius = radius + 2.5f * m_scale;
|
||||
const std::size_t scatterStride =
|
||||
std::max<std::size_t>(1, points.size() / kMaxScatterPoints);
|
||||
// 散点数量本身有上限(`kMaxScatterPoints`),逐点画圈是 O(数千) 的小代价;
|
||||
// 但仍按窗口过滤,保持"只高亮鼠标附近"的一致语义(点少时窗口外也全画)。
|
||||
const bool windowScatter = windowed && points.size() > kHaloWindowMinPoints;
|
||||
const float halfWidth = kHaloWindowHalfWidth * m_scale;
|
||||
// 散点数量本身有上限(`kMaxScatterPoints`),逐点画圈是 O(数千) 的小代价。
|
||||
gd::Pen ring(ToGdiColor(WithAlpha(m_theme->accent, 0x88)), 1.6f * m_scale);
|
||||
for (std::size_t index = 0; index < points.size(); index += scatterStride)
|
||||
{
|
||||
const gd::PointF& point = points[index];
|
||||
if (windowScatter && std::fabs(point.X - windowCenterX) > halfWidth)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
g.DrawEllipse(&ring, point.X - ringRadius, point.Y - ringRadius, ringRadius * 2.0f,
|
||||
ringRadius * 2.0f);
|
||||
}
|
||||
@@ -1196,12 +1373,6 @@ namespace jdi
|
||||
halo.SetLineJoin(gd::LineJoinRound);
|
||||
halo.SetStartCap(gd::LineCapRound);
|
||||
halo.SetEndCap(gd::LineCapRound);
|
||||
// 顶点多的信号只描边**鼠标左右一小段**(见 `kHaloWindowHalfWidth` 的说明):
|
||||
// 整条描边在满幅锯齿信号上要 100~200ms/帧,鼠标移动时整帧卡死。
|
||||
const bool windowStroke = windowed && points.size() > kHaloWindowMinPoints;
|
||||
const float halfWidth = kHaloWindowHalfWidth * m_scale;
|
||||
const float windowLeft = windowCenterX - halfWidth;
|
||||
const float windowRight = windowCenterX + halfWidth;
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
{
|
||||
const std::size_t start = breaks[k];
|
||||
@@ -1210,46 +1381,7 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!windowStroke)
|
||||
{
|
||||
g.DrawLines(&halo, points.data() + start, static_cast<INT>(stop - start));
|
||||
continue;
|
||||
}
|
||||
// 窗口内的顶点在段内**连续**(折线沿时间单调),因此顺序扫描一次、按连续区段绘制。
|
||||
// 多含一个窗口外的顶点,让跨越窗口边界的线段完整画出(不出现断头)。
|
||||
std::size_t runStart = stop;
|
||||
std::size_t runStop = stop;
|
||||
for (std::size_t index = start; index < stop; ++index)
|
||||
{
|
||||
const bool inside = (points[index].X >= windowLeft - 1.0f) &&
|
||||
(points[index].X <= windowRight + 1.0f);
|
||||
if (inside && runStart == stop)
|
||||
{
|
||||
runStart = (index > start) ? (index - 1) : index;
|
||||
runStop = index + 1;
|
||||
}
|
||||
else if (inside)
|
||||
{
|
||||
runStop = index + 1;
|
||||
}
|
||||
else if (runStart != stop)
|
||||
{
|
||||
if (index + 1 < stop)
|
||||
{
|
||||
runStop = index + 2;
|
||||
}
|
||||
if (runStop > runStart + 1)
|
||||
{
|
||||
g.DrawLines(&halo, points.data() + runStart, static_cast<INT>(runStop - runStart));
|
||||
}
|
||||
runStart = stop;
|
||||
runStop = stop;
|
||||
}
|
||||
}
|
||||
if (runStart != stop && runStop > runStart + 1)
|
||||
{
|
||||
g.DrawLines(&halo, points.data() + runStart, static_cast<INT>(runStop - runStart));
|
||||
}
|
||||
g.DrawLines(&halo, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1302,16 +1434,9 @@ namespace jdi
|
||||
const PolylineCacheEntry* cached = AcquirePolyline(haloRef, resolved, lane);
|
||||
if (cached != nullptr)
|
||||
{
|
||||
// 光晕只描边鼠标附近的一段(见 `kHaloWindowHalfWidth`):鼠标不在绘图区内时
|
||||
//(列表/样式区悬停)以绘图区中心为窗口中心,保证代价恒定、不平白卡一帧。
|
||||
const bool mouseInPlot = (m_mouseX >= m_plotRect.X &&
|
||||
m_mouseX <= m_plotRect.X + m_plotRect.Width &&
|
||||
m_mouseY >= m_plotRect.Y &&
|
||||
m_mouseY <= m_plotRect.Y + m_plotRect.Height);
|
||||
const float haloCenterX =
|
||||
mouseInPlot ? static_cast<float>(m_mouseX)
|
||||
: (m_plotRect.X + m_plotRect.Width * 0.5f);
|
||||
PaintSignalHaloFor(g, resolved, lane, *cached, haloCenterX, true);
|
||||
// 整条光晕一次性渲进离屏位图并缓存(命中时只贴图,~0.5ms/帧),
|
||||
// 因此光晕**沿整条曲线**发光而不随鼠标移动(见 `PaintSignalHaloCached`)。
|
||||
PaintSignalHaloCached(g, resolved, lane, *cached);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1384,8 +1509,8 @@ namespace jdi
|
||||
const LaneVisual& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
if (const PolylineCacheEntry* cached = AcquirePolyline(m_hoverLegend, resolved, lane))
|
||||
{
|
||||
// 图例悬停:鼠标就在图例条目上(网格左上角),以它为窗口中心。
|
||||
PaintSignalHaloFor(g, resolved, lane, *cached, static_cast<float>(m_mouseX), true);
|
||||
// 图例悬停:整条曲线一起发光(与鼠标命中的光晕完全同一种画法与代价)。
|
||||
PaintSignalHaloCached(g, resolved, lane, *cached);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2729,6 +2854,18 @@ namespace jdi
|
||||
[this](int laneIndex) { return LaneVisuallyRelevant(laneIndex); });
|
||||
const double defaultWideMin = LayerDefaultWideMin();
|
||||
// 本批要重画的网格:`budget` 足够大时一次取完(视界已停时调用方会传很大的预算)。
|
||||
// ⚠️ **离屏网格一律丢弃**(不重画、也不留在队列里):它们的像素当前不可见,
|
||||
// 而单个巨型信号的图层重建要 10~30ms。真正进入可视/预取带时 `RollWideBuffer`
|
||||
// 会按新视界重新登记它们(见那里的说明)。历史实现会排空整个队列 ⇒ 勾选一个分组后
|
||||
// 连续数秒每帧 10~30ms(用户 2026-10-18 报告的列表动画卡顿)。
|
||||
m_layerRebuildPending.erase(
|
||||
std::remove_if(m_layerRebuildPending.begin(), m_layerRebuildPending.end(),
|
||||
[this](int laneIndex) { return !LaneVisuallyRelevant(laneIndex, 1); }),
|
||||
m_layerRebuildPending.end());
|
||||
if (m_layerRebuildPending.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int available = static_cast<int>(m_layerRebuildPending.size());
|
||||
const int take = std::min(budget, available);
|
||||
std::vector<int> batch;
|
||||
@@ -3190,6 +3327,12 @@ namespace jdi
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ⚠️ **UI 动画期间绝不预热**:单个巨型信号的图层重建要 10~30ms,塞进动画帧就会
|
||||
// 让"只有文字的信号列表"看起来卡顿(用户 2026-10-18 报告)。队列保留,动画结束后继续。
|
||||
if (m_animationBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(m_plotRect.Width > 8.0f) || !(Tab().ViewMax() - Tab().ViewMin() > 0.0))
|
||||
{
|
||||
return;
|
||||
@@ -3288,12 +3431,13 @@ namespace jdi
|
||||
// `BuildLaneVisuals()`(O(网格数))—— 253 个网格时这是拖拽卡顿的主因。
|
||||
// 只登记待重画(可视网格会被 `RenderPendingLaneRebuilds` 优先处理),
|
||||
// 等它们真的要进入视区时再按新视界重建(期间由 `BlitWideBuffer` 的底衬兜底)。
|
||||
//
|
||||
// ⚠️⚠️ **离屏网格的"内容过期"也不登记待重画**:否则一次"网格数变化"(勾选/取消
|
||||
// 一个分组)就会把**上百个离屏网格**全部塞进队列,随后每帧补 1 个 × 10~30ms
|
||||
// ⇒ 数秒的持续掉帧(用户 2026-10-18 报告的信号列表动画卡顿有一半来自这里)。
|
||||
// 离屏网格真正进入可视/预取带时,下面的可见分支会按新视界登记它。
|
||||
if (!LaneVisuallyRelevant(laneIndex, 1))
|
||||
{
|
||||
if (contentStale)
|
||||
{
|
||||
QueueLaneRebuild(laneIndex);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 纵向:幅值**平移**不改内容,只改贴图偏移(与横向同构)。
|
||||
@@ -3926,7 +4070,7 @@ namespace jdi
|
||||
if (highlighted)
|
||||
{
|
||||
// 拖拽光晕整条描边并进入静态场景缓存(拖拽期间不随鼠标变化,不必裁剪)。
|
||||
PaintSignalHaloFor(g, resolved, lane, *cached, 0.0f, false);
|
||||
PaintSignalHaloFor(g, resolved, lane, *cached);
|
||||
}
|
||||
|
||||
if (drawLine && !breaks.empty())
|
||||
|
||||
Reference in new issue
Block a user