修复长距离中键拖拽的绘制错误与抽搐("距离太远"不再退化成排队重画)

症状(用户报告 + TestData/PROBLEM_IMG):按住中键把视界纵向移动好几个网格高(或横向拖很远),
不松开再挪回来时出现大量绘制错误与持续抽搐。

根因:RollWideBuffer 对"位移超出图层余量"只做"排队等后续帧重画"(QueueLaneRebuild + continue):
  * 该帧 BlitWideBuffer 的 srcY = marginY + vOffset 已落在画布之外 → 只能贴出/贴不到像素
    → 网格里出现空白条带(截图里的白色块);
  * 下一帧位移依旧超限 → 每帧都重新入队,而 RenderPendingLaneRebuilds(1) 一帧只补 1 个网格
    → 5 个网格永远补不完 = 持续抽搐;
  * 纵向余量只有 ±marginY(半个网格高 ≈80px),中键纵向拖拽每帧很容易移动 20~150px
    → 几乎必现;横向同理(余量 ±半屏,快速拖拽会超过)。

修法:只有"内容真的变了"(contentStale:外观/网格几何/幅值跨度/可见性…)才排队(那时画面本身
仍是旧内容、不缺口);距离类问题一律本帧就地解决:
  * |shift| < bitmapWidth 且 |vOffset| < 画布高 → 滚动:BitBlt 整块平移 + 只补画暴露的细条
    (横向条 / 纵向条),并同步锚点记账(wideMin / anchorYMax);
  * 任一超出画布(内容整体滚出)→ 本帧立刻整块重画并重新居中
    (RenderLaneLayerRegion(i, layer, 0, bitmapWidth, 0, 0, LayerDefaultWideMin()),实测 0.4ms/网格)。

探针(.preflight/dragcheck,按用户要求"全程按住中键不放"):
  * 图层条带 vs 同锚点新渲染:逐像素(滚动是纯整数平移,必须相同);
  * 覆盖率:(ViewMin - layer.wideMin) * scale == marginX(±1px)且 marginY + vOffset 起的一整个
    网格高都落在画布内 —— 任一条不成立,BlitWideBuffer 就会留下空白条带;
  * 画面结构:网格内"无墨迹行数"与"墨迹占比"对比整幅重绘参考帧(空白条带 / 内容错位探测器)。
场景:上拖半个网格高;按住拖左右/上下/斜向并拖出窗口;纵向拖 4 个网格高再拖回来并小幅摆动;
快速大位移对角拖拽(每帧 150×110px)。A/B 验证探针有效性:临时把纵向滚动关掉
(kLayerRollQuantum = 100000)→ 立刻报 worst layer band diff 76221 / drawing errors 11;
恢复后 0 错误。

两点说明(避免误判):
1. dragcheck 的"画面像素 diff"会稳定出现 ~2.5 万像素(一个网格的 ~11%)差异 —— 那是图层与整幅
   重绘的列相位差(图层画布 2449px 按 1px 列聚合,整幅重绘 1224px 列,相位差 ≤0.5px),对稠密脉冲
   信号(256kHz 编码器)会放大成大片边缘差异;这是长期存在、由"结算帧"收敛的现象,不是本轮缺陷,
   因此画面检查用结构化指标而不是像素 diff。
2. 截图里"整条网格被信号色填满"是正确渲染:把幅值窗口平移到与稠密信号相交的区间时,每列的
   min/max 包络本来就覆盖整个网格(示波器上同样如此)。

验证(全部离屏,不占用鼠标/键盘/屏幕):dragcheck PASS(0 绘制错误;长拖拽 80 帧 + 快速对角 24 帧);
uitest 463/463;panperf PASS(竖直中位 4.4ms、水平 7-10ms);layercheck clobbers=0/24、
pan frames wrong=0/4;cursorcheck 30/30;make clean && make all 零警告。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-12 20:44:51 +08:00
1 parent 59a84ba126
commit 7344bbe9c4
2 files changed
+89 -22

No files matched your search

+23 -22
View File
@@ -7560,7 +7560,6 @@ namespace jdi
// 视界左端当前落在该图层的哪个 x 上;期望它等于 `margin`(左右余量对称)。
const double desired = (Tab().ViewMin() - layer.wideMin) * scale;
const int shift = static_cast<int>(std::llround(desired - static_cast<double>(margin)));
const bool tooFar = (std::abs(shift) >= bitmapWidth - 4);
// 内容(外观 / 网格几何 / 幅值**跨度** / 可见性…)与当前状态不一致。
const bool contentStale = (layer.key != LaneLayerKey(laneIndex));
// 纵向:幅值**平移**不改内容,只改贴图偏移(与横向同构)。
@@ -7570,33 +7569,34 @@ namespace jdi
vOffset = static_cast<int>(
std::llround((layer.anchorYMax - m_lanes[index].yMax) * layer.pxPerValue));
}
const int lanePixels = static_cast<int>(std::ceil(laneHeight));
// 可用的纵向余量 = 画布高 - 可见高(= 上下各 `marginY`);偏移超出它就只能重画。
const int maxRoll = std::max(0, layer.height - lanePixels);
const bool verticalTooFar = (std::abs(vOffset) > maxRoll);
if (contentStale || tooFar || verticalTooFar)
// ⚠️⚠️ **"距离太远"绝不允许退化成"排队等后续帧重画"**:
// 长距离拖拽(用户按着中键把视界移动好几个网格高、或横向拖很远)时,
// 每一帧的位移都会超过余量;若此时只登记待重画,就会出现
// * 本帧 `BlitWideBuffer` 取不到源行 → 网格里出现**空白条带**;
// * 每帧都重新入队 → 一帧只补 1 个网格 → **持续抽搐**且总是补不完。
// 正确做法是:
// * 位移不超过画布 → **滚动**(O(1) BitBlt + 只补画暴露的细条);
// * 位移超过画布(内容整体滚出)→ 本帧**立刻整块重画并重新居中**(一次性),
// 等价于一次普通的整块渲染(实测 0.4ms),不会累积漂移也不会留空白。
// 只有**内容真的变了**(`contentStale`)才交给待重画队列(它可以等后续帧,
// 因为画面本身仍然是"旧内容",不缺口)。
const bool fullRedraw = (std::abs(shift) >= bitmapWidth) ||
(std::abs(vOffset) >= layer.height);
if (fullRedraw)
{
// 登记待重画(本帧不重画,改由 `RenderPendingLaneRebuilds` 在**贴图之前**补画),
// 但仍按现有像素平移,避免网格"卡在原地"。
QueueLaneRebuild(laneIndex);
if (!tooFar && shift != 0)
// 两个方向都滚出画布:直接重新居中 + 整块重画(一次搞定,避免"滚动 + 巨幅补条")。
if (contentStale || std::abs(shift) >= bitmapWidth)
{
if (shift > 0)
{
BitBlt(layer.dc, 0, 0, bitmapWidth - shift, layer.height, layer.dc, shift, 0,
SRCCOPY);
}
else
{
const int amount = -shift;
BitBlt(layer.dc, amount, 0, bitmapWidth - amount, layer.height, layer.dc, 0, 0,
SRCCOPY);
}
layer.wideMin += static_cast<double>(shift) / scale;
QueueLaneRebuild(laneIndex);
}
RenderLaneLayerRegion(laneIndex, layer, 0, bitmapWidth, 0, 0, LayerDefaultWideMin());
continue;
}
if (contentStale)
{
QueueLaneRebuild(laneIndex);
}
// ------------------------------------------------------------------
// 纵向(幅值)滚动:与横向**完全同构**——内容整体上下移动若干行,
@@ -7614,6 +7614,7 @@ namespace jdi
// 折线缓存(键含幅值映射)得以命中;否则每帧滚 1~30 行会让折线每帧重算
// (实测同样的拖动:量化前 3~4ms 的滚动 + 10~14ms 整帧,量化后回到 ~5ms 量级)。
// 余量足够大(上下各 `marginY`),因此"不滚只偏移"绝不会超出画布。
// 上面已保证 |vOffset| < 画布高,所以这里的 BitBlt 高度恒为正。
// ------------------------------------------------------------------
if (std::abs(vOffset) >= kLayerRollQuantum)
{