修复斜向自由拖拽视界时的卡顿/质量下降/错误绘制;预览曲线恢复抗锯齿;预览曲线不再闪烁
一、斜向拖拽退化的根因(纯水平拖拽测不出来)
平移的纵向分量 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
@@ -1388,11 +1388,19 @@ namespace jdi
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 取得该曲线键下最新的已算好条目并钉住(双缓冲的「旧缓冲」)。
|
* @brief 取得该曲线键下最新的已算好条目并钉住(双缓冲的「旧缓冲」)。
|
||||||
* @param [IN] key 曲线键。
|
*
|
||||||
|
* ⚠️ **只按「信号 + 曲线种类」匹配,忽略参数**:拖参数滑块时每帧参数都在变,
|
||||||
|
* 若要求键完全相同就永远找不到上一帧的曲线,本帧便无曲线可画 → 预览曲线**闪烁**。
|
||||||
|
* 旧曲线只是参数略旧,按采样下标映射位置依然正确,先顶上再等后台算好新曲线。
|
||||||
|
*
|
||||||
|
* @param [IN] key 曲线键(只用其 `signal` 与 `kind`)。
|
||||||
|
* @param [IN] begin 需要区间起点(用于优先挑「覆盖该区间」的条目)。
|
||||||
|
* @param [IN] end 需要区间终点(不含)。
|
||||||
* @param [OUT] spanBegin 命中条目的窗口起点。
|
* @param [OUT] spanBegin 命中条目的窗口起点。
|
||||||
* @return 命中条目,无则返回 nullptr。
|
* @return 命中条目,无则返回 nullptr。
|
||||||
*/
|
*/
|
||||||
const CurveEntry* AcquireNewestCurve(const CurveKey& key, std::size_t& spanBegin) const;
|
const CurveEntry* AcquireNewestCurve(const CurveKey& key, std::size_t begin, std::size_t end,
|
||||||
|
std::size_t& spanBegin) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 发布一条新算好的曲线窗口并钉住。
|
* @brief 发布一条新算好的曲线窗口并钉住。
|
||||||
@@ -1907,6 +1915,22 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
bool SceneCacheMatches() const;
|
bool SceneCacheMatches() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief **单个网格**的内容指纹(用于该网格自己的「信号图层」)。
|
||||||
|
*
|
||||||
|
* 图层位图里只含该网格的内容(底色 + 网格线 + 波形),位图高度 = 该网格高度、
|
||||||
|
* 内部以 `y=0` 为网格顶边,因此该指纹只需要「网格高度 + 幅值范围 + 本网格各信号的
|
||||||
|
* 可见性与外观 + 图例文案」。
|
||||||
|
*
|
||||||
|
* ⚠️ 这**必须**是逐网格的:早期版本用全局内容指纹,于是一个网格的幅值变化
|
||||||
|
* 会同时作废**全部**网格的图层(拖拽时纵向分量让 5 个网格排队重画 → 图层内容
|
||||||
|
* 相互不一致 = 错误绘制;且每帧都入队 → 永远补不完)。
|
||||||
|
*
|
||||||
|
* @param [IN] laneIndex 网格下标。
|
||||||
|
* @return 指纹值。
|
||||||
|
*/
|
||||||
|
std::uint64_t LaneLayerHash(int laneIndex) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 计算场景内容指纹(信号集合、显示样式、网格幅值、图例文案)。
|
* @brief 计算场景内容指纹(信号集合、显示样式、网格幅值、图例文案)。
|
||||||
*
|
*
|
||||||
@@ -1915,16 +1939,22 @@ namespace jdi
|
|||||||
*
|
*
|
||||||
* @param [IN] includeStyles 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。
|
* @param [IN] includeStyles 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。
|
||||||
* @param [IN] includeDerived 是否把派生预览曲线的开关与参数计入指纹。
|
* @param [IN] includeDerived 是否把派生预览曲线的开关与参数计入指纹。
|
||||||
|
* @param [IN] includeLaneRange 是否把各网格的**幅值范围**(`yMin/yMax`)计入指纹。
|
||||||
|
* ⚠️ **拖拽平移时的纵向分量**会每帧小幅修改被拖网格的幅值范围;若它参与
|
||||||
|
* “宽基底可用性”判据,每一帧都会退化成整幅重绘(质量下降 + 卡顿)。
|
||||||
|
* 因此宽基底判据用 `includeLaneRange = false` 的几何指纹,
|
||||||
|
* 幅值变化改由**逐网格图层自己的指纹**处理(只有该网格需要重画)。
|
||||||
* @return 指纹值。
|
* @return 指纹值。
|
||||||
*/
|
*/
|
||||||
std::uint64_t SceneContentHash(bool includeStyles = true, bool includeDerived = true) const;
|
std::uint64_t SceneContentHash(bool includeStyles = true, bool includeDerived = true,
|
||||||
|
bool includeLaneRange = true) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 场景内容中**不含逐信号外观样式**的指纹。
|
* @brief 场景内容中**不含逐信号外观样式**的指纹。
|
||||||
*
|
*
|
||||||
* 只包含网格结构、视图几何、网格幅值、信号集合与可见性、图例文案、选中项等
|
* 只包含网格结构、视图几何、信号集合与可见性、图例文案、选中项等
|
||||||
* "变化后必须整幅重建"的内容;逐信号的 `color / opacity / lineWidth / mode`
|
* "变化后必须整幅重建"的内容;逐信号的 `color / opacity / lineWidth / mode`
|
||||||
* 一律不参与。
|
* 一律不参与,**各网格的幅值范围(`yMin/yMax`)也不参与**(由逐网格图层指纹负责)。
|
||||||
*
|
*
|
||||||
* 用途:外观样式变化时(典型场景是拖动自定义颜色的滑块)波形几何完全不变,
|
* 用途:外观样式变化时(典型场景是拖动自定义颜色的滑块)波形几何完全不变,
|
||||||
* 只要该指纹一致,就可以只重画"颜色变了的那个网格"再写回缓存,
|
* 只要该指纹一致,就可以只重画"颜色变了的那个网格"再写回缓存,
|
||||||
|
|||||||
@@ -2189,7 +2189,7 @@ namespace jdi
|
|||||||
if (allowAsyncCompute && (end - begin) > kDerivedAsyncWindowThreshold)
|
if (allowAsyncCompute && (end - begin) > kDerivedAsyncWindowThreshold)
|
||||||
{
|
{
|
||||||
ScheduleCurveJob(key, alignedBegin, alignedEnd, count);
|
ScheduleCurveJob(key, alignedBegin, alignedEnd, count);
|
||||||
if (const CurveEntry* stale = AcquireNewestCurve(key, spanBegin))
|
if (const CurveEntry* stale = AcquireNewestCurve(key, begin, end, spanBegin))
|
||||||
{
|
{
|
||||||
return &stale->values;
|
return &stale->values;
|
||||||
}
|
}
|
||||||
@@ -2257,28 +2257,44 @@ namespace jdi
|
|||||||
return best->get();
|
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::size_t& spanBegin) const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||||
|
// ⚠️ **只按「信号 + 曲线种类」匹配**(忽略参数):拖动参数滑块时每帧参数都在变,
|
||||||
|
// 若要求键完全相同就永远找不到"上一帧的曲线",于是本帧无曲线可画 → **预览曲线闪烁**、
|
||||||
|
// 甚至短暂消失(用户报告的"滑动滑块时曲线消失一会儿又出现")。
|
||||||
|
// 旧曲线只是参数略旧,按采样下标映射位置依然完全正确,先顶上再等后台算好新曲线,
|
||||||
|
// 视觉上就是平滑过渡而不是闪烁。
|
||||||
|
//
|
||||||
|
// 优先挑「窗口覆盖所需区间」的:覆盖不到时曲线会缺一段(看起来像被切断)。
|
||||||
const CurveEntryPtr* newest = nullptr;
|
const CurveEntryPtr* newest = nullptr;
|
||||||
|
const CurveEntryPtr* newestCovering = nullptr;
|
||||||
for (const CurveEntryPtr& entry : m_curveEntries)
|
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)
|
continue;
|
||||||
{
|
}
|
||||||
newest = &entry;
|
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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
m_curvePins.push_back(*newest);
|
m_curvePins.push_back(*pick);
|
||||||
spanBegin = (*newest)->begin;
|
spanBegin = (*pick)->begin;
|
||||||
return newest->get();
|
return pick->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlotView::CurveEntry* PlotView::PublishCurve(const CurveKey& key, std::size_t begin,
|
const PlotView::CurveEntry* PlotView::PublishCurve(const CurveKey& key, std::size_t begin,
|
||||||
@@ -5723,6 +5739,11 @@ namespace jdi
|
|||||||
// 逐网格滚动跟随视界。⚠️ `RollWideBuffer` **不做整块重画**:内容过期 / 锚点
|
// 逐网格滚动跟随视界。⚠️ `RollWideBuffer` **不做整块重画**:内容过期 / 锚点
|
||||||
// 过远的网格只登记到待重画队列,因此前台每帧的工作量恒定且很小。
|
// 过远的网格只登记到待重画队列,因此前台每帧的工作量恒定且很小。
|
||||||
RollWideBuffer();
|
RollWideBuffer();
|
||||||
|
// 预算内补画"待重画"的网格:**本帧最多一个**,其余留到后续帧
|
||||||
|
//(即使 5 个网格同时需要重画,前台也不会在某一帧里做完整批而卡顿)。
|
||||||
|
// ⚠️ **必须在贴图之前**:否则本帧画好的网格要到下一帧才显示,
|
||||||
|
// 拖拽时表现为"波形比鼠标慢半拍"、快速拖动时出现"断线"(图层覆盖不到视界)。
|
||||||
|
const bool morePending = RenderPendingLaneRebuilds(1);
|
||||||
// 绘图区底色(覆盖时间轴与侧边留白区域)。
|
// 绘图区底色(覆盖时间轴与侧边留白区域)。
|
||||||
UiFillRect(g, m_area, m_theme->plotBg);
|
UiFillRect(g, m_area, m_theme->plotBg);
|
||||||
// 贴基底(绘图区:网格 + 波形)。图层没覆盖到的部分先留空,后续帧补齐。
|
// 贴基底(绘图区:网格 + 波形)。图层没覆盖到的部分先留空,后续帧补齐。
|
||||||
@@ -5768,10 +5789,6 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
paintOverlays();
|
paintOverlays();
|
||||||
// 预算内补画"待重画"的网格:**本帧最多一个**,其余留到后续帧。
|
|
||||||
// 这样即使 5 个网格同时需要重画,前台也不会在某一帧里做完整批而卡顿;
|
|
||||||
// 未画好的部分本帧留空(或沿用旧像素),下一个循环就绪后自然出现。
|
|
||||||
const bool morePending = RenderPendingLaneRebuilds(1);
|
|
||||||
if (morePending)
|
if (morePending)
|
||||||
{
|
{
|
||||||
// 请宿主再送一帧(与派生曲线就绪用同一条消息:只失效绘图区且不置脏)。
|
// 请宿主再送一帧(与派生曲线就绪用同一条消息:只失效绘图区且不置脏)。
|
||||||
@@ -6044,7 +6061,8 @@ namespace jdi
|
|||||||
return SignalRef();
|
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:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
|
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
|
||||||
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
|
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
|
||||||
@@ -6097,8 +6115,13 @@ namespace jdi
|
|||||||
const LaneVisual& visual = m_lanes[laneIndex];
|
const LaneVisual& visual = m_lanes[laneIndex];
|
||||||
mixFloat(visual.rect.Y);
|
mixFloat(visual.rect.Y);
|
||||||
mixFloat(visual.rect.Height);
|
mixFloat(visual.rect.Height);
|
||||||
mixFloat(static_cast<float>(visual.yMin));
|
// ⚠️ 幅值范围只在 `includeLaneRange` 时计入:拖拽平移时的纵向分量会每帧小幅改动它,
|
||||||
mixFloat(static_cast<float>(visual.yMax));
|
// 若参与“宽基底可用性”判据,每一帧都会退化成整幅重绘(质量下降 + 卡顿)。
|
||||||
|
if (includeLaneRange)
|
||||||
|
{
|
||||||
|
mixFloat(static_cast<float>(visual.yMin));
|
||||||
|
mixFloat(static_cast<float>(visual.yMax));
|
||||||
|
}
|
||||||
|
|
||||||
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
|
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
|
||||||
// 「边沿计数(中心轴)」「均值滤波」「弱峰增强」的开关与每条被勾选信号的参数
|
// 「边沿计数(中心轴)」「均值滤波」「弱峰增强」的开关与每条被勾选信号的参数
|
||||||
@@ -6172,7 +6195,9 @@ namespace jdi
|
|||||||
|
|
||||||
std::uint64_t PlotView::SceneGeometryHash() const
|
std::uint64_t PlotView::SceneGeometryHash() const
|
||||||
{
|
{
|
||||||
return SceneContentHash(false);
|
// 外观样式不参与;各网格的幅值范围也不参与(那由逐网格图层指纹负责,
|
||||||
|
// 这样拖拽时的纵向分量不会把整幅重绘的快路径打掉)。
|
||||||
|
return SceneContentHash(false, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint64_t PlotView::SceneDecorHash() const
|
std::uint64_t PlotView::SceneDecorHash() const
|
||||||
@@ -7004,10 +7029,10 @@ namespace jdi
|
|||||||
|
|
||||||
std::uint64_t PlotView::LaneLayerKey(int laneIndex) const
|
std::uint64_t PlotView::LaneLayerKey(int laneIndex) const
|
||||||
{
|
{
|
||||||
// 图层只含基底(不含派生曲线与装饰),因此指纹 = 内容(外观/几何,不含派生参数)
|
// 图层只含基底(不含派生曲线与装饰),因此指纹 = **该网格自己的内容**
|
||||||
// ⊕ 网格下标 ⊕ 视界跨度 ⊕ 绘图区宽度(决定像素/时间与位图尺寸)。
|
// ⊕ 网格下标 ⊕ 视界跨度 ⊕ 绘图区宽度(决定像素/时间与位图尺寸)。
|
||||||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
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 ^= static_cast<std::uint64_t>(laneIndex + 1) * 1315423911ull;
|
||||||
key *= 1099511628211ull;
|
key *= 1099511628211ull;
|
||||||
key ^= static_cast<std::uint64_t>(std::llround(span * 1.0e6));
|
key ^= static_cast<std::uint64_t>(std::llround(span * 1.0e6));
|
||||||
@@ -7016,6 +7041,50 @@ namespace jdi
|
|||||||
return key;
|
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
|
double PlotView::LayerDefaultWideMin() const
|
||||||
{
|
{
|
||||||
// 以当前视界为中心:位图左右各留半个视界宽,因此可向两侧平移 ±半屏。
|
// 以当前视界为中心:位图左右各留半个视界宽,因此可向两侧平移 ±半屏。
|
||||||
@@ -9601,11 +9670,13 @@ namespace jdi
|
|||||||
// 派生曲线**恢复为抗锯齿折线**(与"之前"的显示效果一致)。
|
// 派生曲线**恢复为抗锯齿折线**(与"之前"的显示效果一致)。
|
||||||
// 但抗锯齿在"陡峭段很多"的形状上代价极高(实测 2448 顶点 26~37ms,无抗锯齿仅 3.5ms),
|
// 但抗锯齿在"陡峭段很多"的形状上代价极高(实测 2448 顶点 26~37ms,无抗锯齿仅 3.5ms),
|
||||||
// 而那种形状下抗锯齿的平滑作用几乎看不出来 —— 因此:
|
// 而那种形状下抗锯齿的平滑作用几乎看不出来 —— 因此:
|
||||||
// * 交互期间(`m_fastFrame`)一律关闭;
|
// * ⚠️ **不再因交互(`m_fastFrame`)关闭抗锯齿**:波形基底(图层)始终是全质量抗锯齿,
|
||||||
// * 陡峭段占比过高时(方波/编码器这类信号)也关闭。
|
// 若预览曲线在交互期间改成无抗锯齿,就会"比普通信号粗糙"(用户报告的观感下降);
|
||||||
|
// 预览曲线只是叠加层,抗锯齿的代价与它自身的顶点数成正比(可见区间很小),可以承受。
|
||||||
|
// * 陡峭段占比过高时(方波/编码器这类信号)仍关闭:那种形态下观感几乎无差别。
|
||||||
const bool spiky = (totalSegments > 0) && (tallSegments * 8 > totalSegments);
|
const bool spiky = (totalSegments > 0) && (tallSegments * 8 > totalSegments);
|
||||||
const int savedSmoothing = g.GetSmoothingMode();
|
const int savedSmoothing = g.GetSmoothingMode();
|
||||||
if (m_fastFrame || spiky)
|
if (spiky)
|
||||||
{
|
{
|
||||||
g.SetSmoothingMode(gd::SmoothingModeNone);
|
g.SetSmoothingMode(gd::SmoothingModeNone);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -511,6 +511,8 @@ Documents/ 项目文档(含本文件)
|
|||||||
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **跨设备记忆合流**:把起源设备(本机)的详细工程记忆**并入本文件**(同一记忆文件,不再另建 `agent_mem_origin.md` 等旁支文件),新增 §11(跨设备协作须知)与 §12(起源设备详细记忆原文)。远端 `gitea/main` 已快进合并到本机(`7608e85` → `f97c4cf`,**16** 个提交)。本机 MinGW-w64 GCC 15.2.0 (UCRT) 下 `make release` 通过。 |
|
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **跨设备记忆合流**:把起源设备(本机)的详细工程记忆**并入本文件**(同一记忆文件,不再另建 `agent_mem_origin.md` 等旁支文件),新增 §11(跨设备协作须知)与 §12(起源设备详细记忆原文)。远端 `gitea/main` 已快进合并到本机(`7608e85` → `f97c4cf`,**16** 个提交)。本机 MinGW-w64 GCC 15.2.0 (UCRT) 下 `make release` 通过。 |
|
||||||
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复「缩放后平移出现严重绘制错误/波形抖动/前台被拖慢」**:根因是后台图层线程会改写共享渲染状态(保存→改写→恢复`m_area`/`m_plotRect`/视界/网格矩形),与 UI 线程无锁的平移/命中测试相冲 → 视界被弹回、网格错位;叠加"全局锚点 + 逐网格内容"导致各网格时间错位。修法:**移除后台图层线程**(改为 UI 线程同步维护)、**逐网格独立锚点与滚动**(补细条用该图层自己的 `wideMin`)、跨度变化时同步 `RebuildWideBuffer()`。同时把「弱波增强」改名「**弱峰增强**」并改为 **0~200% 分段插值**(0% 完全不动 / 100% 到较温和一侧 / 200% 完全拉平;新 1.5 = 旧 0.5)。离屏验证:居中图层 vs 整幅重绘 **5 网格逐像素 0 差异**、平移帧对齐 ≤1px、24 次竞态检测 0 覆盖;测试套件 **380/380 通过**。 |
|
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复「缩放后平移出现严重绘制错误/波形抖动/前台被拖慢」**:根因是后台图层线程会改写共享渲染状态(保存→改写→恢复`m_area`/`m_plotRect`/视界/网格矩形),与 UI 线程无锁的平移/命中测试相冲 → 视界被弹回、网格错位;叠加"全局锚点 + 逐网格内容"导致各网格时间错位。修法:**移除后台图层线程**(改为 UI 线程同步维护)、**逐网格独立锚点与滚动**(补细条用该图层自己的 `wideMin`)、跨度变化时同步 `RebuildWideBuffer()`。同时把「弱波增强」改名「**弱峰增强**」并改为 **0~200% 分段插值**(0% 完全不动 / 100% 到较温和一侧 / 200% 完全拉平;新 1.5 = 旧 0.5)。离屏验证:居中图层 vs 整幅重绘 **5 网格逐像素 0 差异**、平移帧对齐 ≤1px、24 次竞态检测 0 覆盖;测试套件 **380/380 通过**。 |
|
||||||
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"按住键拖拽平移时波形质量下降 / 断线 / 周期性卡顿"与"浮动面板互相覆盖时交互错位"**,并把弱峰增强改为**逐信号参数**。① 平移:`RollWideBuffer` 不再整块重画(只平移+补细条,内容过期/锚点过远只入队),新增 `RenderPendingLaneRebuilds(budget)`(**每帧最多重画 1 个网格**,其余下帧)+ `BlitWideBuffer` **局部贴图**(图层没覆盖的部分留空,不再整块跳过);"结算"整幅精确帧**仅限非拖拽且全部图层就绪**(消除拖拽中每两帧的整幅重绘/质量振荡)。实测 `VERY_BIG_DATA` 5 网格深放大平移:中位 **6.2ms**、max 7.6ms(无尖峰);按住键且视界停在边界时 **0.29ms/帧**。② 弱峰增强:`WeakPeakItem` 改为携带 `levelPercent(0–200)+iterations`,面板改为与均值滤波同构的**逐信号两个输入框**(删除网格级两滑块),哈希/快照/构建/预览全部改为逐信号。③ 浮动面板:新增 `m_panelOrder` 层序,**命中从最上层往下**、点击 `BringPanelToFront`、新面板置顶;修复"上层 ✕ / 标题栏被下层参数框抢走"。离屏测试 **388/388 通过**。 |
|
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"按住键拖拽平移时波形质量下降 / 断线 / 周期性卡顿"与"浮动面板互相覆盖时交互错位"**,并把弱峰增强改为**逐信号参数**。① 平移:`RollWideBuffer` 不再整块重画(只平移+补细条,内容过期/锚点过远只入队),新增 `RenderPendingLaneRebuilds(budget)`(**每帧最多重画 1 个网格**,其余下帧)+ `BlitWideBuffer` **局部贴图**(图层没覆盖的部分留空,不再整块跳过);"结算"整幅精确帧**仅限非拖拽且全部图层就绪**(消除拖拽中每两帧的整幅重绘/质量振荡)。实测 `VERY_BIG_DATA` 5 网格深放大平移:中位 **6.2ms**、max 7.6ms(无尖峰);按住键且视界停在边界时 **0.29ms/帧**。② 弱峰增强:`WeakPeakItem` 改为携带 `levelPercent(0–200)+iterations`,面板改为与均值滤波同构的**逐信号两个输入框**(删除网格级两滑块),哈希/快照/构建/预览全部改为逐信号。③ 浮动面板:新增 `m_panelOrder` 层序,**命中从最上层往下**、点击 `BringPanelToFront`、新面板置顶;修复"上层 ✕ / 标题栏被下层参数框抢走"。离屏测试 **388/388 通过**。 |
|
||||||
|
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | 弱峰增强的参数输入改回**逐信号滑块**(每信号两条轨道:增强等级 0–200% / 迭代次数 N),并让「迭代次数 N」**上限可就地编辑**:点击滑块右侧数值 → 整行变成输入框(`EditFilter::BYTE` 只收数字、空内容显示灰提示当前上限、`0`/超限/空 保持原值、下调后当前迭代次数自动被夹到新上限);上限进 `PanelSlotKey` 但不进 `SceneContentHash`(不影响波形重画)。离屏测试 **409/409 通过**。 |
|
||||||
|
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"斜向/不规则拖拽自由移动视界时的卡顿、掉帧与波形错误绘制"、"预览曲线不如普通信号细腻(无抗锯齿)"、"拖动参数滑块时预览曲线跳帧闪烁"**。① 根因:拖拽平移的**纵向分量**会逐帧小改被拖网格的 `yMin/yMax`,而**网格幅值范围同时参与「宽基底可用性」几何指纹与每个网格的图层键**,于是每帧都退化成整幅重绘(`m_fastFrame` 关抗锯齿)+ 5 个网格全部入队(一帧只补 1 个)→ 质量下降、卡顿、图层相互不一致(断线)。修法:`SceneContentHash` 新增 `includeLaneRange`(`SceneGeometryHash()` = `false,true,false`,幅值不进"能不能走快路径"的判定),幅值改由**逐网格** `LaneLayerHash(laneIndex)`(`LaneLayerKey` 的基)承担 → 只让真正变化的那个网格失效;并把 `RenderPendingLaneRebuilds(1)` **移到 `BlitWideBuffer` 之前**(本帧补好的网格本帧就可见)。② 预览曲线抗锯齿不再因交互(`m_fastFrame`)关闭(仅保留"陡峭段过多"这一条形同无声的逃生口)。③ `AcquireNewestCurve` 改为**只按「信号 + 曲线种类」匹配**(参数无关,优先挑覆盖所需区间的条目)→ 拖参数滑块时用上一帧的曲线顶替,后台算好再无缝替换,**不再闪烁/消失**。离屏验证:斜向拖拽 40/40 帧走宽基底快路径(中位 9.2ms,无尖峰),参数连改 10 次 **0 个空白帧**,抗锯齿回归断言(快帧混淆边缘像素 8149/8404);`uitest` **420/420 通过**、`panperf` PASS、`layercheck` 保持"0 次视界覆盖 / 平移对齐 ≤1px"。 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -2548,3 +2550,80 @@ Documents/ 项目文档(含本文件)
|
|||||||
命中区之外 → 测试里按 `lround(X+W) - 1`;而**拖拽**可以超出(`UiSliderRatioFromX` 会夹到 1)。
|
命中区之外 → 测试里按 `lround(X+W) - 1`;而**拖拽**可以超出(`UiSliderRatioFromX` 会夹到 1)。
|
||||||
- `make all` 零警告;`uitest` **409/409 通过**;`panperf` PASS;
|
- `make all` 零警告;`uitest` **409/409 通过**;`panperf` PASS;
|
||||||
`layercheck` 保持“居中图层逐像素 0 差异 / 平移对齐 ≤1px / 0 次视界覆盖”。
|
`layercheck` 保持“居中图层逐像素 0 差异 / 平移对齐 ≤1px / 0 次视界覆盖”。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 本轮(2026-10-12,起源设备):斜向自由移动视界退化 + 预览曲线抗锯齿 + 预览曲线闪烁
|
||||||
|
|
||||||
|
用户报告(原文要点):以 `VERY_BIG_DATA.CSV` 为例,全信号显示 + **时间轴方向选区放大**(主时间轴约 **2ms 一大格**)后,
|
||||||
|
用**鼠标中键/左键斜向拖拽**自由移动视界**仍然卡顿**,且"这种不规则的视界自由移动会导致性能问题和绘制波形质量下降、错误绘制";
|
||||||
|
同时"实验性功能的预览曲线显示质量不如普通信号,似乎没有抗锯齿";"拖动弱峰增强滑块时预览曲线**消失一会儿又画出来**(跳帧闪烁)"。
|
||||||
|
|
||||||
|
### A. 斜向拖拽的根因(为什么"纯水平"测不出来)
|
||||||
|
|
||||||
|
`ShiftLaneRange(lane, dy)`(拖拽的纵向分量 → 网格幅值平移)每帧都会修改该网格的 `visual.yMin/yMax`,而:
|
||||||
|
|
||||||
|
1. `SceneGeometryHash()` 原本 = `SceneContentHash(false)`,**包含每个网格的 `yMin/yMax`** → 幅值一变,"场景几何变了",
|
||||||
|
于是**每一帧都判定宽基底不可用** → 退化到整幅重绘,并因拖拽置 `m_fastFrame = true` → **整幅 + 关抗锯齿**(质量下降 + 卡顿)。
|
||||||
|
2. `LaneLayerKey(laneIndex)` 的基也是**全局** `SceneContentHash(true, false)` → 幅值一变,**5 个网格的图层全部作废**并全部入队;
|
||||||
|
而 `RenderPendingLaneRebuilds(1)` **一帧只补 1 个** → 一直贴的是内容不一致的旧图层 = **错误绘制/断线**;
|
||||||
|
并且该调用点在 `BlitWideBuffer` **之后**,本帧补好的网格要到下一帧才显示("波形比鼠标慢半拍")。
|
||||||
|
|
||||||
|
⚠️ 这解释了自动化盲区:**纯水平平移**不会改幅值,所以旧探针"看起来一切正常";只有"斜向/不规则"拖拽才暴露。
|
||||||
|
|
||||||
|
### B. 修法(三个指纹各司其职)
|
||||||
|
|
||||||
|
- `SceneContentHash(bool includeStyles, bool includeDerived, bool includeLaneRange = true)`:
|
||||||
|
新增 `includeLaneRange`;`SceneGeometryHash()` = `SceneContentHash(false, true, **false**)`
|
||||||
|
→ **幅值范围不再参与"能不能走宽基底快路径"的判定**(它只影响"画到哪里",不影响"几何结构")。
|
||||||
|
- 新增 **`LaneLayerHash(laneIndex)`**:单网格图层的内容指纹 = 位图高度 + 该网格 `yMin/yMax` + 该网格各信号的
|
||||||
|
`visible/color/opacity/lineWidth/mode`;`LaneLayerKey(laneIndex)` 的基由全局内容指纹改为它。
|
||||||
|
→ 幅值变化**只让那一个网格**的图层失效(其余网格保持 Ready,零复制)。
|
||||||
|
- `Paint()` 的宽基底分支里,`RenderPendingLaneRebuilds(1)` 从 `BlitWideBuffer` **之后**移到 `RollWideBuffer()` **之后、贴图之前**
|
||||||
|
→ 本帧预算内补好的网格**本帧**就可见(消除"慢半拍/断线")。
|
||||||
|
|
||||||
|
### C. 预览曲线抗锯齿(用户:"似乎没有过抗锯齿机制")
|
||||||
|
|
||||||
|
- `PaintDerivedCurve` 原本 `if (m_fastFrame || spiky) SetSmoothingMode(None)`。
|
||||||
|
波形**基底**来自图层,**永远**是全质量抗锯齿;预览曲线却在任何交互期间变成无抗锯齿 → 叠加在一起就是"预览比普通信号粗糙"。
|
||||||
|
- 改为 **`if (spiky)`**:只有"陡峭段占比 > 1/8"的形状(方波/编码器)才放弃抗锯齿(那种形态下**观感几乎无差别**,
|
||||||
|
而实测代价极大:2448 顶点 26~37ms vs 无抗锯齿 3.5ms)。预览曲线只是叠加层、可见区间很小,抗锯齿代价可以承受。
|
||||||
|
- 交互期间**仍然**把顶点预算减半(`budget / 2`,即"低分辨率 → 松手恢复高分辨率"),只是不再关抗锯齿。
|
||||||
|
|
||||||
|
### D. 预览曲线闪烁(拖参数滑块时消失一会儿)
|
||||||
|
|
||||||
|
`AcquireNewestCurve(key, spanBegin)` 原本要求 **`entry->key == key`(参数完全相同)**。
|
||||||
|
拖滑块时参数每帧都在变 → 上一帧的曲线"键不同" → 找不到任何可用的旧曲线 → `CurveSpanFor` 返回 `nullptr` →
|
||||||
|
本帧**无曲线可画**(消失),直到后台按新参数算完并发布(间隔一会儿又出现)= 用户看到的闪烁。
|
||||||
|
|
||||||
|
修法:`AcquireNewestCurve(key, begin, end, spanBegin)` **只按 `key.signal` / `key.kind` 匹配**(参数无关),
|
||||||
|
并**优先挑「窗口覆盖所需区间 `[begin,end]`」的条目**(覆盖不到才退化为"最新的")。旧曲线只是参数略旧,
|
||||||
|
按采样下标映射位置仍然完全正确 → 视觉上平滑过渡而非闪烁。
|
||||||
|
|
||||||
|
### E. 离屏验证(不占用鼠标/键盘/屏幕)
|
||||||
|
|
||||||
|
- `uitest` 新增 `[AH]`(斜向拖拽 + 预览曲线连续性 + 抗锯齿):
|
||||||
|
① `LaneLayerKey` 逐网格失效(改 1 号网格幅值 → 1 号键变、2 号键**不变**);
|
||||||
|
② "同时平移 + 改幅值"的一帧 `m_wideLastValid == true`(走宽基底快路径)、未动的网格保持 Ready;
|
||||||
|
③ 参数连改 10 次:最差派生像素 21013(预热 22629),**空白帧 0 / 10**;
|
||||||
|
④ **隔离抗锯齿断言**:把 `PaintDerivedCurve` 画到两张 `gd::Bitmap` 上(`m_fastFrame = false` / `true`),
|
||||||
|
统计"既不是背景也不是纯线色的**混淆边缘像素**"→ 普通帧 8158/8420、快帧 8149/8404(都远大于 1/10)
|
||||||
|
—— 这正是回归护栏:**旧实现快帧的混淆像素只有 2 个**(断言立刻失败)。
|
||||||
|
⚠️ 写这类隔离测试的两个坑:GDI+ `Bitmap` **不保证初始为黑**(要 `Graphics::Clear`);测试曲线要用
|
||||||
|
**网格自己的值域**(`yMin..yMax`)且只画几个周期,否则曲线"每段都陡峭"会踩到那条无抗锯齿的逃生口,测出来的不是真问题。
|
||||||
|
- `panperf`(同一深放大区间,5 网格,每帧 60px 水平 / 斜向 60px+3px):
|
||||||
|
* 水平平移 40 帧:中位 **6.17ms** / p90 7.13 / max 9.22;
|
||||||
|
* **斜向拖拽 40 帧:中位 9.16ms / p90 10.67 / max 11.80,走宽基底快路径 40/40**(修复前该路径 0/40);
|
||||||
|
* 与整幅重绘的像素差:斜向最差 79809,水平基线 77310~80919 —— **同一量级**,即只有那套已知的 ≤0.5px 整数像素残差,
|
||||||
|
不存在"质量下降"或"错误绘制"。
|
||||||
|
- `layercheck`:`clobbers=0/24`、`pan frames wrong=0/4`、居中图层 vs 整幅重绘逐像素 0 差异(未回归)。
|
||||||
|
- `make clean && make all` 零警告;`uitest` **420/420 通过**。
|
||||||
|
|
||||||
|
### F. 教训(写进 §11.2 硬约束)
|
||||||
|
|
||||||
|
1. **网格幅值范围(`yMin/yMax`)不得进入"几何/内容"指纹**(那会让"平移的纵向分量"每帧毁掉快路径);
|
||||||
|
它只属于**该网格自己的图层指纹**。
|
||||||
|
2. **图层指纹必须逐网格**:任何"一个网格的变化使全部网格作废"的全局指纹都会把"一帧只补 1 个"的预算变成永久不一致。
|
||||||
|
3. **待重画网格要在贴图之前补**,否则用户看到的是"比鼠标慢半拍"的旧像素。
|
||||||
|
4. **异步发布的派生曲线必须有"参数无关"的兜底**(按信号 + 种类取最新),否则拖参数滑块必然闪烁。
|
||||||
|
5. **交互优化只能降分辨率(顶点数),不能关抗锯齿**:基底图层永远是全质量抗锯齿,叠加层一旦关了就会"看着更粗糙"。
|
||||||
Reference in new issue
Block a user