diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index bd0ed01..aeea274 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -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 前台补画各网格的**分界线**(网格顶部那条较重的横线)。 * diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 6d57065..e743e0b 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -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(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(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(laneIndex)].yMax = anchorYMax; + m_lanes[static_cast(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(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(index), m_layerFront[index], 0, 0, 0, 0, wideMin)) + // 整块重画:每个图层都以"当前 `yMax`"为纵向锚点。 + if (!RenderLaneLayerRegion(static_cast(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(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(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); } } diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index d95312b..03f6414 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -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` 零警告。 | ---