修复斜向自由拖拽视界时的卡顿/质量下降/错误绘制;预览曲线恢复抗锯齿;预览曲线不再闪烁
一、斜向拖拽退化的根因(纯水平拖拽测不出来)
平移的纵向分量 ShiftLaneRange 每帧都会小改被拖网格的 yMin/yMax,而网格幅值范围同时参与
「宽基底可用性」的几何指纹(SceneGeometryHash 原本含 yMin/yMax)与每个网格图层键的基
(LaneLayerKey 原本用全局 SceneContentHash):
* 前者 → 每帧都判定宽基底不可用 → 退化成整幅重绘,且拖拽置 m_fastFrame=true → 关抗锯齿
(绘制质量下降 + 卡顿);
* 后者 → 一个网格的幅值变化让全部 5 个网格的图层作废并全部入队,而
RenderPendingLaneRebuilds(1) 一帧只补 1 个 → 长期贴内容不一致的旧图层 = 错误绘制/断线。
二、修法
- SceneContentHash(includeStyles, includeDerived, includeLaneRange) 新增 includeLaneRange;
SceneGeometryHash() = SceneContentHash(false, true, false) → 幅值范围不再参与
「能不能走宽基底快路径」的判定。
- 新增逐网格 LaneLayerHash(laneIndex)(位图高度 + 该网格 yMin/yMax + 该网格各信号的
visible/color/opacity/lineWidth/mode);LaneLayerKey(laneIndex) 的基由全局内容指纹改为它
→ 幅值变化只让那一个网格失效,其余网格保持 Ready。
- Paint 宽基底分支:RenderPendingLaneRebuilds(1) 从 BlitWideBuffer 之后移到
RollWideBuffer() 之后、贴图之前 → 本帧预算内补好的网格本帧就可见(消除「比鼠标慢半拍」与断线)。
三、预览曲线抗锯齿
- PaintDerivedCurve 不再因 m_fastFrame 关闭抗锯齿(仅保留「陡峭段占比 > 1/8」的逃生口)。
波形基底来自图层、永远是全质量抗锯齿;预览曲线是叠加层,交互期间关 AA 就会「比普通信号粗糙」。
交互期间仍把顶点预算减半(低分辨率 → 松手恢复高分辨率)。
四、预览曲线闪烁
- AcquireNewestCurve(key, begin, end, spanBegin) 改为只按「信号 + 曲线种类」匹配(参数无关),
并优先挑窗口覆盖 [begin,end] 的条目;拖参数滑块时先用上一帧的曲线顶替,后台按新参数算好后无缝替换,
不再出现「消失一会儿又画出来」。
五、验证(全部离屏,不占用鼠标/键盘/屏幕)
- uitest 新增 [AH]:逐网格图层失效 / 斜向帧走宽基底快路径 / 参数连改 10 次 0 空白帧 /
隔离抗锯齿断言(快帧混淆边缘像素 8149 vs 8404,旧实现该值为 2)→ 420/420 通过;
make clean && make all 零警告。
- panperf:水平平移中位 6.17ms;斜向拖拽中位 9.16ms、p90 10.67ms、max 11.80ms,
走宽基底快路径 40/40(修复前 0/40),像素差与水平基线同量级(仅已知的 ≤0.5px 整数像素残差)。
- layercheck:clobbers=0/24、pan frames wrong=0/4、居中图层 vs 整幅重绘逐像素 0 差异。
This commit is contained in:
1 parent
a30a6fcf4d
commit
0afa6360ae
3 files changed
+209
-29
No files matched your search
@@ -2189,7 +2189,7 @@ namespace jdi
|
||||
if (allowAsyncCompute && (end - begin) > kDerivedAsyncWindowThreshold)
|
||||
{
|
||||
ScheduleCurveJob(key, alignedBegin, alignedEnd, count);
|
||||
if (const CurveEntry* stale = AcquireNewestCurve(key, spanBegin))
|
||||
if (const CurveEntry* stale = AcquireNewestCurve(key, begin, end, spanBegin))
|
||||
{
|
||||
return &stale->values;
|
||||
}
|
||||
@@ -2257,28 +2257,44 @@ namespace jdi
|
||||
return best->get();
|
||||
}
|
||||
|
||||
const PlotView::CurveEntry* PlotView::AcquireNewestCurve(const CurveKey& key,
|
||||
const PlotView::CurveEntry* PlotView::AcquireNewestCurve(const CurveKey& key, std::size_t begin,
|
||||
std::size_t end,
|
||||
std::size_t& spanBegin) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
// ⚠️ **只按「信号 + 曲线种类」匹配**(忽略参数):拖动参数滑块时每帧参数都在变,
|
||||
// 若要求键完全相同就永远找不到"上一帧的曲线",于是本帧无曲线可画 → **预览曲线闪烁**、
|
||||
// 甚至短暂消失(用户报告的"滑动滑块时曲线消失一会儿又出现")。
|
||||
// 旧曲线只是参数略旧,按采样下标映射位置依然完全正确,先顶上再等后台算好新曲线,
|
||||
// 视觉上就是平滑过渡而不是闪烁。
|
||||
//
|
||||
// 优先挑「窗口覆盖所需区间」的:覆盖不到时曲线会缺一段(看起来像被切断)。
|
||||
const CurveEntryPtr* newest = nullptr;
|
||||
const CurveEntryPtr* newestCovering = nullptr;
|
||||
for (const CurveEntryPtr& entry : m_curveEntries)
|
||||
{
|
||||
if (entry->key == key)
|
||||
if (entry->key.signal != key.signal || entry->key.kind != key.kind)
|
||||
{
|
||||
if (newest == nullptr || entry->serial > (*newest)->serial)
|
||||
{
|
||||
newest = &entry;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (newest == nullptr || entry->serial > (*newest)->serial)
|
||||
{
|
||||
newest = &entry;
|
||||
}
|
||||
if (entry->begin <= begin && entry->end >= end &&
|
||||
(newestCovering == nullptr || entry->serial > (*newestCovering)->serial))
|
||||
{
|
||||
newestCovering = &entry;
|
||||
}
|
||||
}
|
||||
if (newest == nullptr)
|
||||
const CurveEntryPtr* pick = (newestCovering != nullptr) ? newestCovering : newest;
|
||||
if (pick == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
m_curvePins.push_back(*newest);
|
||||
spanBegin = (*newest)->begin;
|
||||
return newest->get();
|
||||
m_curvePins.push_back(*pick);
|
||||
spanBegin = (*pick)->begin;
|
||||
return pick->get();
|
||||
}
|
||||
|
||||
const PlotView::CurveEntry* PlotView::PublishCurve(const CurveKey& key, std::size_t begin,
|
||||
@@ -5723,6 +5739,11 @@ namespace jdi
|
||||
// 逐网格滚动跟随视界。⚠️ `RollWideBuffer` **不做整块重画**:内容过期 / 锚点
|
||||
// 过远的网格只登记到待重画队列,因此前台每帧的工作量恒定且很小。
|
||||
RollWideBuffer();
|
||||
// 预算内补画"待重画"的网格:**本帧最多一个**,其余留到后续帧
|
||||
//(即使 5 个网格同时需要重画,前台也不会在某一帧里做完整批而卡顿)。
|
||||
// ⚠️ **必须在贴图之前**:否则本帧画好的网格要到下一帧才显示,
|
||||
// 拖拽时表现为"波形比鼠标慢半拍"、快速拖动时出现"断线"(图层覆盖不到视界)。
|
||||
const bool morePending = RenderPendingLaneRebuilds(1);
|
||||
// 绘图区底色(覆盖时间轴与侧边留白区域)。
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
// 贴基底(绘图区:网格 + 波形)。图层没覆盖到的部分先留空,后续帧补齐。
|
||||
@@ -5768,10 +5789,6 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
paintOverlays();
|
||||
// 预算内补画"待重画"的网格:**本帧最多一个**,其余留到后续帧。
|
||||
// 这样即使 5 个网格同时需要重画,前台也不会在某一帧里做完整批而卡顿;
|
||||
// 未画好的部分本帧留空(或沿用旧像素),下一个循环就绪后自然出现。
|
||||
const bool morePending = RenderPendingLaneRebuilds(1);
|
||||
if (morePending)
|
||||
{
|
||||
// 请宿主再送一帧(与派生曲线就绪用同一条消息:只失效绘图区且不置脏)。
|
||||
@@ -6044,7 +6061,8 @@ namespace jdi
|
||||
return SignalRef();
|
||||
}
|
||||
|
||||
std::uint64_t PlotView::SceneContentHash(bool includeStyles, bool includeDerived) const
|
||||
std::uint64_t PlotView::SceneContentHash(bool includeStyles, bool includeDerived,
|
||||
bool includeLaneRange) const
|
||||
{
|
||||
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
|
||||
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
|
||||
@@ -6097,8 +6115,13 @@ namespace jdi
|
||||
const LaneVisual& visual = m_lanes[laneIndex];
|
||||
mixFloat(visual.rect.Y);
|
||||
mixFloat(visual.rect.Height);
|
||||
mixFloat(static_cast<float>(visual.yMin));
|
||||
mixFloat(static_cast<float>(visual.yMax));
|
||||
// ⚠️ 幅值范围只在 `includeLaneRange` 时计入:拖拽平移时的纵向分量会每帧小幅改动它,
|
||||
// 若参与“宽基底可用性”判据,每一帧都会退化成整幅重绘(质量下降 + 卡顿)。
|
||||
if (includeLaneRange)
|
||||
{
|
||||
mixFloat(static_cast<float>(visual.yMin));
|
||||
mixFloat(static_cast<float>(visual.yMax));
|
||||
}
|
||||
|
||||
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
|
||||
// 「边沿计数(中心轴)」「均值滤波」「弱峰增强」的开关与每条被勾选信号的参数
|
||||
@@ -6172,7 +6195,9 @@ namespace jdi
|
||||
|
||||
std::uint64_t PlotView::SceneGeometryHash() const
|
||||
{
|
||||
return SceneContentHash(false);
|
||||
// 外观样式不参与;各网格的幅值范围也不参与(那由逐网格图层指纹负责,
|
||||
// 这样拖拽时的纵向分量不会把整幅重绘的快路径打掉)。
|
||||
return SceneContentHash(false, true, false);
|
||||
}
|
||||
|
||||
std::uint64_t PlotView::SceneDecorHash() const
|
||||
@@ -7004,10 +7029,10 @@ namespace jdi
|
||||
|
||||
std::uint64_t PlotView::LaneLayerKey(int laneIndex) const
|
||||
{
|
||||
// 图层只含基底(不含派生曲线与装饰),因此指纹 = 内容(外观/几何,不含派生参数)
|
||||
// 图层只含基底(不含派生曲线与装饰),因此指纹 = **该网格自己的内容**
|
||||
// ⊕ 网格下标 ⊕ 视界跨度 ⊕ 绘图区宽度(决定像素/时间与位图尺寸)。
|
||||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||||
std::uint64_t key = SceneContentHash(true, false);
|
||||
std::uint64_t key = LaneLayerHash(laneIndex);
|
||||
key ^= static_cast<std::uint64_t>(laneIndex + 1) * 1315423911ull;
|
||||
key *= 1099511628211ull;
|
||||
key ^= static_cast<std::uint64_t>(std::llround(span * 1.0e6));
|
||||
@@ -7016,6 +7041,50 @@ namespace jdi
|
||||
return key;
|
||||
}
|
||||
|
||||
std::uint64_t PlotView::LaneLayerHash(int laneIndex) 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);
|
||||
};
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
return hash;
|
||||
}
|
||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
mixFloat(visual.rect.Height);
|
||||
mixFloat(static_cast<float>(visual.yMin));
|
||||
mixFloat(static_cast<float>(visual.yMax));
|
||||
const std::vector<SignalRef>& signals = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].signals;
|
||||
mix(signals.size());
|
||||
for (const SignalRef& ref : signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
mix(static_cast<std::uint64_t>(ref.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.signalIndex));
|
||||
mix(resolved.visible ? 1ull : 0ull);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
mix(static_cast<std::uint64_t>(resolved.style->color));
|
||||
mixFloat(resolved.style->opacity);
|
||||
mixFloat(resolved.style->lineWidth);
|
||||
mix(static_cast<std::uint64_t>(resolved.style->mode));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
double PlotView::LayerDefaultWideMin() const
|
||||
{
|
||||
// 以当前视界为中心:位图左右各留半个视界宽,因此可向两侧平移 ±半屏。
|
||||
@@ -9601,11 +9670,13 @@ namespace jdi
|
||||
// 派生曲线**恢复为抗锯齿折线**(与"之前"的显示效果一致)。
|
||||
// 但抗锯齿在"陡峭段很多"的形状上代价极高(实测 2448 顶点 26~37ms,无抗锯齿仅 3.5ms),
|
||||
// 而那种形状下抗锯齿的平滑作用几乎看不出来 —— 因此:
|
||||
// * 交互期间(`m_fastFrame`)一律关闭;
|
||||
// * 陡峭段占比过高时(方波/编码器这类信号)也关闭。
|
||||
// * ⚠️ **不再因交互(`m_fastFrame`)关闭抗锯齿**:波形基底(图层)始终是全质量抗锯齿,
|
||||
// 若预览曲线在交互期间改成无抗锯齿,就会"比普通信号粗糙"(用户报告的观感下降);
|
||||
// 预览曲线只是叠加层,抗锯齿的代价与它自身的顶点数成正比(可见区间很小),可以承受。
|
||||
// * 陡峭段占比过高时(方波/编码器这类信号)仍关闭:那种形态下观感几乎无差别。
|
||||
const bool spiky = (totalSegments > 0) && (tallSegments * 8 > totalSegments);
|
||||
const int savedSmoothing = g.GetSmoothingMode();
|
||||
if (m_fastFrame || spiky)
|
||||
if (spiky)
|
||||
{
|
||||
g.SetSmoothingMode(gd::SmoothingModeNone);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user