修复垂直中键拖拽后再平移的网格纵向跳变/绘制错误

症状:对某网格做一次纵向中键拖拽(幅值窗口上下平移)后,不松开中键、只做一点点
水平(或垂直)位移,该网格波形整体纵向错位(最多 15px),看起来像"纵向跳帧/抽搐"。
松开后再按下中键做同样的微小水平移动同样复现,且与缩放等级无关。

根因:RenderLaneLayerRegion 结束时无条件把 layer.anchorYMax 重置为"当前 yMax"。
该赋值只对"整块重画 / 纵向补条(整宽)"成立(那两种情况整块像素都对齐到当前映射),
对"横向补细条(整高)"不成立 —— 横向滚动只改一条竖带,其余像素仍在原锚点上。
纵向拖动以 kLayerRollQuantum = 16px 量化:|vOffset| < 16px 时不滚动、交给贴图偏移,
因此拖动停下时图层会残留一个 <16px 的纵向偏移且 anchorYMax != 当前 yMax。第一次水平
微移(哪怕只有 3px)就把该残余偏移静默清零(vOffset 变成 0),而像素并未上移 →
整个网格的波形相对正确位置纵向错位,错位量恰好等于那个残余量。

修法:RenderLaneLayerRegion 新增形参 double anchorYMax(本次渲染像素所对应的纵向锚点),
内部以它作为幅值映射基准(yMin = anchorYMax - laneSpan)渲染,并写回
layer.anchorYMax = anchorYMax。各调用点:
  * 整块重画 / 纵向补条 / 待重画补齐:传当前 yMax(纵向补条传滚动后的 layer.anchorYMax,
    与已滚动像素同一映射);
  * 横向补细条:传 layer.anchorYMax(保持不变),细条因此与其余像素纵向同源,
    残余偏移继续由贴图偏移承担。
vOffset == 0 时新逻辑与旧逻辑完全等价,故既有"仅水平/仅纵向平移"的像素结果不变。

验证:新增离屏探针 .preflight/perf/pancheck(窗口 SW_HIDE + 直接注入消息 + 读图层内部
anchorYMax/当前 yMax/vOffset + 把持按钮帧与释放后的整幅全质量帧做纵向互相关)。
修复前 A 场景 H step 1 起 vOff 从 -15.00 被清零为 +0.00、lane0 best dy=+15px(差异 39245);
修复后全程保持 -15.00、best dy=0(差异 1220,仅已知 <=0.5px 列相位残差)。
对照/中间网格/长距水平/斜向/松开重按共 7 个场景在缩放 0/8/26 步下全部 dy=0
(中间网格 +1px 为整像素量化残差)。make clean && make all 零警告。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-12 22:58:27 +08:00
1 parent 7344bbe9c4
commit de27d9b94f
3 files changed
+93 -12

No files matched your search

+9 -1
View File
@@ -2319,10 +2319,18 @@ namespace jdi
* @param [IN] wideMin 位图 `x=0` 对应的时间。
* **必须与调用方的意图一致**:整块重画传 `LayerDefaultWideMin()`(以当前视界居中);
* 滚动补细条则传 `layer.wideMin`(与该图层已有像素同一映射,否则会留下接缝)。
* @param [IN] anchorYMax 本次渲染的像素所对应的**纵向锚点**(= 该网格的 `yMax`)。
* ⚠️ **必须与"本次改动的那部分像素"一致**:
* - 整块重画 / 纵向补条(整宽):整块图层都对齐到锚点 → 传**当前** `yMax`
* (纵向补条则传 `layer.anchorYMax` 滚动后的值,与已滚动的像素同一映射);
* - **横向补细条(整高)**:只改一条竖带,其余像素仍在**原锚点**上,
* 因此必须传 `layer.anchorYMax`(**绝不能传当前 `yMax`**)—— 否则贴图偏移被
* 静默清零、而像素并未上移,整个网格的波形会纵向错位(用户报告的
* "垂直拖动后再横向平移出现纵向跳变/绘制错误")。
* @return 渲染成功返回 true。
*/
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW, int stripY,
int stripH, double wideMin);
int stripH, double wideMin, double anchorYMax);
/**
* @brief 前台补画各网格的**分界线**(网格顶部那条较重的横线)。
*
+30 -11
View File
@@ -7319,7 +7319,9 @@ namespace jdi
{
continue;
}
if (!RenderLaneLayerRegion(laneIndex, layer, 0, 0, 0, 0, defaultWideMin))
// 整块重画:整个图层都对齐到"当前 `yMax`"(锚点 = 当前 yMax)。
if (!RenderLaneLayerRegion(laneIndex, layer, 0, 0, 0, 0, defaultWideMin,
m_lanes[static_cast<std::size_t>(laneIndex)].yMax))
{
continue;
}
@@ -7330,7 +7332,7 @@ namespace jdi
}
bool PlotView::RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW,
int stripY, int stripH, double wideMin)
int stripY, int stripH, double wideMin, double anchorYMax)
{
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
{
@@ -7410,6 +7412,13 @@ namespace jdi
m_lanes[i].yMin = savedLanes[i].yMin;
m_lanes[i].yMax = savedLanes[i].yMax;
}
// ⚠️ **该网格的幅值映射必须以本次渲染的纵向锚点为准**(而不是"当前 `yMax`"):
// - 整块重画 / 纵向补条:锚点就是当前 `yMax`(纵向补条是滚动后的值),二者一致;
// - **横向补细条**:只重画一条竖带,其余像素仍在**原锚点**上;若这里用当前 `yMax`
// 渲染,竖带与其余像素的纵向映射就不同源,贴图后出现撕裂/错位。
// 详见 `RenderLaneLayerRegion` 的 `anchorYMax` 形参说明。
m_lanes[static_cast<std::size_t>(laneIndex)].yMax = anchorYMax;
m_lanes[static_cast<std::size_t>(laneIndex)].yMin = anchorYMax - laneSpan;
// 该网格在本图层中的位置:横向铺满整条宽图,纵向铺满整张画布。
// 画布 = 「可见网格 + 上下各 `marginY` 行余量」。**上下扩展量按"像素行"精确换算**
//(而不是上下各扩展固定的数值量):位图高度是整数(`ceil(laneHeight) + 2*marginY`),
@@ -7474,9 +7483,9 @@ namespace jdi
layer.wideMin = wideMin;
layer.span = span;
layer.marginY = marginY;
// 纵向锚点 = 渲染时该网格的 `yMax`(可见窗口顶行对应的数值)。
// 贴图时按 `(anchorYMax - 当前 yMax) * pxPerValue` 得到纵向偏移像素数。
layer.anchorYMax = savedLanes[static_cast<std::size_t>(laneIndex)].yMax;
// 纵向锚点 = 本次渲染的像素所对应的 `yMax`(由调用方按"改动了哪部分像素"决定;
// 横向补细条必须沿用图层原锚点,否则残余的纵向偏移会被静默清零 —— 见函数说明)。
layer.anchorYMax = anchorYMax;
layer.pxPerValue = pxPerValue;
layer.key = LaneLayerKey(laneIndex);
layer.valid = true;
@@ -7505,7 +7514,9 @@ namespace jdi
bool ok = true;
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
{
if (!RenderLaneLayerRegion(static_cast<int>(index), m_layerFront[index], 0, 0, 0, 0, wideMin))
// 整块重画:每个图层都以"当前 `yMax`"为纵向锚点。
if (!RenderLaneLayerRegion(static_cast<int>(index), m_layerFront[index], 0, 0, 0, 0, wideMin,
m_lanes[index].yMax))
{
ok = false;
}
@@ -7590,7 +7601,9 @@ namespace jdi
{
QueueLaneRebuild(laneIndex);
}
RenderLaneLayerRegion(laneIndex, layer, 0, bitmapWidth, 0, 0, LayerDefaultWideMin());
// 整块重画:重新居中 + 纵向锚点取**当前** `yMax`。
RenderLaneLayerRegion(laneIndex, layer, 0, bitmapWidth, 0, 0, LayerDefaultWideMin(),
m_lanes[static_cast<std::size_t>(laneIndex)].yMax);
continue;
}
if (contentStale)
@@ -7603,7 +7616,7 @@ namespace jdi
// 再只补画新暴露的那一条(整宽 × 行数)。
//
// ⚠️ **必须先做纵向再做横向**:横向补条由 `RenderLaneLayerRegion` 渲染,
// 而该函数会把图层的纵向锚点重置为"当前 `yMax`";若先补横向、那时锚点还没跟上,
// 而该函数按传入的**纵向锚点**渲染细条;若先补横向,那时锚点还没跟上,
// 补出来的细条会带着一个错的纵向偏移(贴图后表现为波形错位/撕裂)。
// ⚠️ 没有这一步时:幅值一变就超出余量 → 每帧都排队**整块重画**(画布比可见网格高
// 3 倍,代价很大)→ 拖动时"抽搐";而且在重画完成前,`BlitWideBuffer` 只能贴出部分
@@ -7639,8 +7652,9 @@ namespace jdi
}
// 锚点随之移动(与横向 `layer.wideMin += shift / scale` 同一套记账方式)。
layer.anchorYMax -= static_cast<double>(roll) / layer.pxPerValue;
// 补画暴露的那一条(整宽)。
RenderLaneLayerRegion(laneIndex, layer, 0, bitmapWidth, stripY, stripH, layer.wideMin);
// 补画暴露的那一条(整宽)。锚点用**滚动后**的值 —— 该条与已滚动的像素同一映射。
RenderLaneLayerRegion(laneIndex, layer, 0, bitmapWidth, stripY, stripH, layer.wideMin,
layer.anchorYMax);
}
if (shift == 0)
@@ -7664,9 +7678,14 @@ namespace jdi
// 只补画新暴露的细条(每帧代价恒为 O(细条))。
// ⚠️ 细条必须用**该图层更新后的映射**渲染(传 `layer.wideMin`);若改用"当前视界"
// 的映射,细条与已平移的旧像素之间会有 ≤0.5px 的错位接缝 → 拖动时看起来在抖动。
// ⚠️ **纵向锚点必须沿用 `layer.anchorYMax`**(不能传当前 `yMax`):横向补条只改一条
// 竖带,其余像素仍在原锚点上;同时该函数按锚点渲染这条竖带,因此竖带与其余像素
// 纵向同源。若在这里把锚点重置为当前 `yMax`,残余的纵向偏移会被静默清零、而像素
// 并未上移 → 整个网格的波形纵向错位("垂直拖动后再横向平移出现纵向跳变")。
const int stripWidth = std::min(std::abs(shift), bitmapWidth);
const int stripX = (shift > 0) ? (bitmapWidth - stripWidth) : 0;
RenderLaneLayerRegion(laneIndex, layer, stripX, stripWidth, 0, 0, layer.wideMin);
RenderLaneLayerRegion(laneIndex, layer, stripX, stripWidth, 0, 0, layer.wideMin,
layer.anchorYMax);
}
}
+54
View File
@@ -488,6 +488,59 @@ Documents/ 项目文档(含本文件)
前台只在做位图搬运。
- 跨进程验证:`BackgroundLayerRenderCount()` 为公开只读接口,`perf.exe ... 6` 结束时会打印总量。
### 9.16 修复「垂直中键拖拽后再平移,网格内容纵向跳变 / 绘制错误」(本轮)
#### 症状与复现
`VERY_BIG_DATA.CSV`(5 信号全选)下,任意缩放等级都对同一根网格成立:
1. 对某网格(如最上层「编码器A相」)按中键**纵向**拖拽约半个网格高,让该网格幅值窗口上下平移;
2. **不松开**中键,只做**一点点水平位移**。
预期只有水平移动,实际该网格的波形**整体纵向错位**(最多 15px,视拖动落点而定),看起来像"纵向跳帧/抽搐"。
若改为"松开后再按下中键做同样的微小水平移动",同样复现。
#### 根因:横向补细条时静默重置了图层的纵向锚点
图层用 `LaneLayer::anchorYMax`(渲染时该网格的 `yMax`)描述**像素的纵向映射**;贴图偏移
`vOffset = round((anchorYMax - 当前 yMax) * pxPerValue)`。纵向拖动用 `kLayerRollQuantum = 16px` **量化**:
`|vOffset| < 16px` 时不滚动,交给贴图偏移处理 —— 于是拖动停下时图层里会残留一个 `< 16px` 的纵向偏移,
而 `anchorYMax != 当前 yMax`。
问题出在 `RenderLaneLayerRegion` 结束时**无条件** `layer.anchorYMax = 当前 yMax`。该赋值对
「整块重画」「纵向补条(整宽)」成立(那两种情况整块像素都对齐到当前映射),但对
**「横向补细条(整高)」不成立**:横向滚动只改一条竖带,其余像素仍在**原锚点**上。于是第一次水平移动
(哪怕只有 3px)就把残余偏移**静默清零**(`vOffset` 变成 0),而像素并没有上移 → 整个网格的波形
相对正确位置纵向错位恰好等于那个残余量。
#### 修法
`RenderLaneLayerRegion` 新增形参 `double anchorYMax`(本次像素所对应的纵向锚点):内部以它作为幅值映射
基准(`yMin = anchorYMax - laneSpan`)渲染,并在结束时写入 `layer.anchorYMax = anchorYMax`。各调用点:
- 整块重画 / 纵向补条 / 待重画补齐:传**当前** `yMax`(纵向补条传滚动后的 `layer.anchorYMax`,与已滚动像素同一映射);
- **横向补细条**:传 `layer.anchorYMax`(**保持不变**),细条因此与其余像素纵向同源,残余偏移继续由贴图偏移承担。
#### 验证(离屏,`.preflight/perf/pancheck`,窗口 `SW_HIDE` + 直接注入消息,不动鼠标/键盘/屏幕)
探针逐帧读出图层内部状态(`anchorYMax` / 当前 `yMax` / `vOffset`),并把持按钮帧与
**释放后的整幅全质量帧**做纵向互相关,报告该网格的最佳纵向位移 `dy`。
| 场景(`VERY_BIG_DATA`,缩放 0 / 8 / 26 步) | 修复前 | 修复后 |
| --- | --- | --- |
| A 纵向拖半个网格高 → 持按钮水平微移 | **dy=+15px**(lane0 差异 39245) | **dy=0**(差异 1220,仅已知 ≤0.5px 列相位残差) |
| B 仅纵向拖(对照) | dy=0 | dy=0 |
| C 仅水平拖(对照) | dy=0 | dy=0 |
| D 中间网格纵向+水平 | — | dy=+1(整像素量化残差) |
| E 长距水平拖(越过 ±半屏余量)后再微移 | — | dy=0 |
| F 斜向拖后再水平 | — | dy=0 |
| G 纵向拖 → 松开 → 再按下中键水平微移 | — | dy=0 |
- 修复前 A 场景 `H step 1` 起 `vOff` 从 `-15.00` 被清零为 `+0.00`;修复后全程保持 `-15.00`(贴图偏移继续生效)。
- `vOffset == 0` 时新逻辑与旧逻辑**完全等价**(横向补条传的锚点就是当前 `yMax`),因此既有的
「仅水平平移」「仅纵向平移」行为与像素结果不变(已由对照场景 B/C 覆盖)。
- `make clean && make all` 零警告。
---
## 10. 变更日志(本文件维护记录)
@@ -518,6 +571,7 @@ Documents/ 项目文档(含本文件)
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"上下拖动之后,左右/上下/斜向拖动都出现绘制错误"(真因:图层条带渲染时 GDI 网格线没被限制在条带内),并调整「弱峰调整」的弱峰/弱谷定义、默认幅度与数值符号显示**。① 根因:`PaintLane` 的 GDI 网格线裁剪矩形只限制了横向(`RectF(m_area.X, clipTop, m_area.Width, …)`),而**纵向补条是全宽 × 窄 y** —— GDI 绘制不受 GDI+ 裁剪约束,于是横向网格线被画到条带之外的已正确像素上、**覆盖在波形之上**,并**永久烘进图层**,随后每次平移都带着它一起漂移(用户看到"到处都有绘制错误")。修法:`laneClip = IntersectRect(m_area, 网格可见矩形)` 同时用于 `SetClip` 与 GDI 裁剪。② 新增离屏探针 `.preflight/dragcheck`(真文件 + 真中键拖拽消息:先上拖半个网格高,再左右/上下/斜向、最后拖出窗口;每帧把图层画布的有效条带与"同一 `wideMin` 的新渲染画布"逐像素比较):修复前被拖网格整行不一致(2410px ≈ 2 行全宽),修复后 ≤171px(仅补条边界的抗锯齿边缘)。③ 弱峰调整:**「是否偏弱」改为**"波峰必须同时低于左右两侧各自的(相邻波峰+相邻波谷)均值;波谷反之";默认幅度 **0%**(不改动);滑块右侧数值**始终带符号**(`+150%` / `-150%` / `0%`,验证 +150% 与 -150% 墨迹宽度一致且都比 `0%` 宽 ≥8px)。验证:`uitest` **441/441**、`dragcheck`/`panperf`/`layercheck`/`cursorcheck` 全过、`make clean && make all` 零警告。 |
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **「弱峰调整」升级为三组识别规则 + 滑动条 ±400% 梯度驱动**:第一类(两侧均值都要满足,原规则)在滑动条 0–200% 内走完自身 0–200%;第二类("或":任一侧均值满足即可)从 100% 起参与,own 幅度 = clamp(|a|-100%,0,200%);第三类(只与左右同类极值比较,最宽松)从 200% 起参与,own 幅度 = clamp(|a|-200%,0,200%);负值完全对称。每组自身的 ±200% 映射统一为:+100% → 左右同类极值中较温和的一侧、**+200% → 左右同类极值的均值**(不再取较大的那个);-100% → 相邻异类极值中较近的一个、**-200% → 相邻异类极值的均值**(不再取最小的那个)。同时满足多组时取**幅度绝对值最大**的组。验证:`uitest` **463/463**(第一类点 0/50/100/150/200% → 520/570/620/**625/630**;只满足第二类的峰 130 → 100% 不动、150%/200%/250%/300%/400% = 140/150/162.5/175/175;只满足第三类的峰 195 → 200% 不动、250%/300%/400% = 197.5/200/200;±400% 夹取;滑块中间 = 0%、两端 = ±400%)、`panperf`/`layercheck`/`cursorcheck`/`dragcheck` 全过、零警告。 |
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"按住中键把视界纵向移动好几个网格高(或横向拖很远)后,出现大量绘制错误与持续抽搐"**:根因是 `RollWideBuffer` 对"位移超出余量"只做**排队等后续帧重画**(`QueueLaneRebuild` + `continue`)—— 该帧 `BlitWideBuffer` 取不到源行 → 网格里出现**空白条带**;且每帧都重新入队而 `RenderPendingLaneRebuilds(1)` 一帧只补 1 个网格 → **补不完 = 持续抽搐**;纵向余量只有 ±半个网格高,正常速度的纵向拖拽每帧就超过它,故几乎必现。修法:**只有"内容真的变了"才排队**,距离类问题一律本帧就地解决 —— 位移小于画布则滚动(O(1) BitBlt + 只补暴露细条),超出画布则**立刻整块重画并重新居中**(0.4ms/网格)。验证:新增探针 `.preflight/dragcheck` 按用户要求**全程按住中键不放**地做检查(图层条带逐像素 / 画布覆盖率 / 画面空白行数与墨迹占比),并把纵向滚动临时关掉做 A/B:立刻报 `layer band diff 76221 / drawing errors 11`,恢复后 **0 错误**;`uitest` **463/463**、`panperf`/`layercheck`/`cursorcheck` 全过、`make clean && make all` 零警告。 |
| 2026-10-12 | DeepSeek-V4.1-Flash(**另一台设备**) | **修复「垂直中键拖拽后再平移,网格内容纵向跳变 / 绘制错误」**(详见 §9.16):根因是 `RenderLaneLayerRegion` 结束时**无条件**把 `layer.anchorYMax` 重置为"当前 `yMax`"—— 对整块重画 / 纵向补条成立,但**横向补细条(只改一条竖带、其余像素仍在原锚点)**不成立。纵向拖动以 `kLayerRollQuantum=16px` 量化,停下时图层会残留 `<16px` 的纵向偏移且 `anchorYMax != 当前 yMax`;第一次水平微移就把该偏移**静默清零**(`vOffset→0`)而像素未动 → 整个网格波形纵向错位(最多 15px)。修法:`RenderLaneLayerRegion` 新增形参 `anchorYMax`,内部以它作幅值映射基准(`yMin = anchorYMax - laneSpan`)渲染并写回 `layer.anchorYMax`;横向补条传 `layer.anchorYMax`(保持不变),整块重画/纵向补条传当前(或滚动后)`yMax`。`vOffset==0` 时新旧逻辑完全等价。验证:新增离屏探针 `.preflight/perf/pancheck`(窗口 `SW_HIDE` + 注入消息 + 读图层内部 `anchorYMax`/`vOffset` + 持按钮帧 vs 释放后整幅帧的纵向互相关):修复前 A 场景 `dy=+15px`(lane0 差异 39245),修复后 **`dy=0`**(差异 1220,仅已知列相位残差);对照/中间网格/长距水平/斜向/松开重按共 7 个场景在缩放 0/8/26 步下全部 `dy=0`(中间网格 `+1px` 为整像素量化残差);`make clean && make all` 零警告。 |
---