57 KiB
Just DataInspector — Agent 记忆与项目须知
本文件是本仓库的权威上下文备忘录,供任何接手的 AI Agent 在动手前阅读。 每次完成任务后必须同步更新本文件(补充新需求、修复记录、约定变更等)。 保持内容精确、明细、简练;多台设备协作时,可将各自的记忆合并到本文件。
1. 项目目标
- 使用 C++17 + Win32 API(GDI/GDI+) 从零构建的桌面数据查看器,项目名 Just DataInspector。
- 定位:替代 MATLAB/Simulink 的
Simulink.sdi.view仿真数据检查器,用于更方便地查看带时间轴的 CSV 仿真数据。 - 目标数据特征(参考 MathWorks 导入 CSV 的说明):CSV 中存在一个或多个时间轴列,其余列表示各时间点上的信号值。
- 产物为 GUI 程序(非 CUI)。
2. 构建与工具链
- 工具链:MinGW-w64(
x86_64-win32-seh-rev0, MinGW-Builds 16.1.0),可直接用make。 - 构建入口:根目录
Makefile(C / C++ / windres 混合构建)。 - 常用命令:
make release→Build/Release/Target.exe(-Os,strip:-s)。make debug→Build/Debug/Target.exe(-O0 -g)。make→ 同时构建 Release 与 Debug。make clean→ 删除Build/。
- 关键编译设置:
-std=c++17;-finput-charset=UTF-8 -fexec-charset=GBK -fwide-exec-charset=UTF-16LE;-static;-mwindows(GUI);链接gdi32 gdiplus comdlg32 d3d11 dwmapi d3dcompiler。 - 新增组件/工具目录后,必须在
Makefile的CXX_INCLUDES_PATHS与CXX_SOURCES_PATHS中登记(源文件按目录通配自动收集)。 - 构建产物统一在
Build/,已由.gitignore忽略;同时忽略*.o/*.d/*.lst/*.map/*.exe与TestData/。
3. 目录结构与约定
Components/ 功能组件块(每块 Inc/ 放 .hpp,Src/ 放 .cpp)
Application/ 应用主框架:窗口、布局、消息分发、标签页、绘制合成
Main/ 程序入口(WinMain → Application::Run)
TabBar/ 顶部多标签栏
Toolbar/ 顶部工具栏(导入文件 / 适应视图 / 时间轴自适应 / Y 轴自适应 / 选区放大 / 光标 / 合并·拆分·清空网格)
SignalPanel/ 左侧信号面板:按文件分组的信号树、可见性勾选、样式编辑
PlotView/ 波形绘图区:网格、时间轴、光标、缩放、拖拽、实验性功能面板
Utilities/ 工具与辅助模块(Inc/ + Src/)
Application/ ... (见上)
CsvTable/ CSV 流式解析(列优先)
DataModel/ 数据层:DataSet / DataFile / SignalData / 显示配置
PaintUtil/ GDI+ 绘制与文本/控件辅助(含 UiPointInRect、UiMeasureTextWidth 等)
PopupMenu/ 主题化自绘弹出菜单(含二级子菜单、内联编辑)
ColorPickerPopup/ 浮动拾色器
TextEdit/ 内联文本编辑模型
Theme/ 主题色板(Solarised Light)与波形默认配色
Utf8Util/ 编码转换(UTF-8 ↔ UTF-16 等)
Resources/ 图标、清单(.manifest)、版本信息(.rc)、ResourceIds.h
TestData/ 本地测试 CSV(**不纳入版本控制**)
Documents/ 项目文档(含本文件)
- 命名硬性约束:源文件/头文件不得含空格,只能字母与下划线组合。
- 头文件后缀:C 用
.h,C++ 用.hpp;C 源.c,C++ 源.cpp。 - 防重复包含:每个头文件必须
#ifndef __XXX_XX_HPP__ / #define __XXX_XX_HPP__ / ... / #endif;宏名 = 文件名的双边双下划线 + 全大写。 - 宏定义与枚举常量必须全大写 + 下划线。
- 所有对外函数/方法声明用 Doxygen 风格注释:
@brief、@param [IN/OUT]、@return(中文说明)。 - 代码风格统一,强调解耦与清晰的分层。
4. 架构要点
- 数据层共享、视图配置分标签页:全局
DataSet(已加载文件、信号、时间轴)被所有标签页共享;每个标签页仅持有各自的TabConfig(信号可见性、配色、分组、网格等),互不影响。 Application是总编排者:创建窗口、Relayout布局、HandleMessage分发消息、合成绘制、把菜单/拾色器结果派发回组件。- 自定义绘制的浮动控件(
PopupMenu、ColorPickerPopup)不创建独立窗口,作为宿主绘制的最上层内容直接画进后备缓冲区,保证外观与主界面统一(不出现系统默认菜单样式)。 - 消息优先级:菜单打开 > 拾色器打开 > 常规组件交互(见
Application::HandleMessage)。 - DPI 感知:
ApplyDpi重建字体与布局;浮动控件通过SetScale缩放。
数据模型(Utilities/Inc/DataModel.hpp)
SignalData:单条信号元数据 + 采样值。DataFile:一个已加载文件及其自有时间轴(timeColumnName/time/minTime/maxTime/timeMonotonic)。DataSet:文件集合与全局时间范围查询(MinTime/MaxTime、最近采样点查找等)。DrawMode:LINE(插值折线) /SCATTER(散点) /LINE_SCATTER(两者)。LaneMode:PER_SIGNAL/MERGED/CUSTOM(网格划分策略)。PlotCursorMode:NONE/SINGLE/DOUBLE(时间光标三态)。- 各网格自身状态(Y 轴范围、自适应开关、实验性功能开关等)随信号继承。
CSV 解析(Utilities/Src/DataModel.cpp)
- 列优先存储(每条信号一整块连续内存):避免百万行级按行存储的堆分配放大,并支持整列移动进
SignalData。 - 流式分块读取,可携带真实字节进度(
CsvProgressInfo),支持取消。 - 时间列判定
LooksLikeTimeHeader:小写匹配t / time / timestamp / simulation time,或包含time/时间;未命中时回退第 0 列。 - 缺失值以
NaN表示;要求时间列有效数值 ≥ 2,否则报错。 - 分隔符自动探测(默认
,)。
5. 功能需求清单(来自需求交互,持续补充)
5.1 基础功能(首轮构建目标,已实现)
- 波形网格(多网格);时间轴。
- 光标(单/双光标,含差值读数)。
- 缩放与自适应(适应视图、Y 轴自适应、选区放大)。
- 导入并加载符合要求(带时间轴列)的 CSV,解析出信号名。
- 信号样式:修改颜色、粗细、散点/插值线段显示方式(
DrawMode)。 - 信号可绘制到同一网格或不同网格。
5.2 后续扩展(已在代码中实现,供接手参考)
- 多标签页(新建/关闭/重命名/拖拽排序)。
- 工具栏:合并网格 / 拆分网格 / 清空网格;时间轴自适应。
- 信号面板:按文件分组、勾选可见性、仅显示/全部显示/隐藏全部、移除文件。
- 绘图区拖拽:平移、选区放大(按首个明显位移方向决定横/纵轴)、网格滚动、光标拖拽、把信号曲线拖到其它网格、网格图例 GX 徽标上下重排。
- 图例:悬停高亮、内联重命名、信号删除按钮。
5.3 「实验性功能」二级菜单(网格右键菜单内,横线上方)
包含且仅包含以下可勾选项(每网格独立状态):
- 边沿计数
- 边沿计数(中心轴)
- 均值滤波
- 弱波增强
这些功能开关会联动绘图区的浮动参数面板;部分支持「构建为新信号」。
注意:新增/改名实验性功能时,需同步
PlotView.hpp的PLOT_CMD_LANE_*、Application.cpp的菜单构建与派发、PlotView/DataModel的状态位与面板绘制。
6. UI / 交互设计语言
- 参考 VS Code 与 WinUI 的设计语言;配色为 VS Code 的 Solarised Light 方案(
Theme::SolarizedLight(),强调色accent)。 - 全部界面与交互用 Win32 API 自绘,不用系统默认控件样式。
- 文本对齐、菜单内边距、圆角、阴影等在各浮动控件内保持统一风格。
7. 交付与 Git 规范
- 工作区由本地 git 管辖。
- 仅在功能完善(可验证)后才提交;提交信息用简洁中文描述修改内容。
- 提交时仅对本工作区生效(
git config --local),不得影响全局设置:user.name= 当前模型全称。user.email=<>(留空尖括号)。
TestData/、Build/等不提交(见.gitignore)。
8. Agent 工作流要求
- 接手前先探索工作区/提交历史/代码建立认知(本文件应作为第一参考)。
- 完成需求后编译验证(
make release至少成功),必要时用TestData/数据功能验证。 - 验证与探索时尽量不占用用户的鼠标/屏幕/键盘(后台构建为主)。
- 本地仓库提交完成后,不要在未经指示的情况下继续探索或优化,等待用户下一步指令。
- 每次完成任务后更新本文件(需求、修复、约定变更)。
9. 已知问题与修复记录
9.1 网格右键菜单「实验性功能」二级菜单瞬间关闭(已修复)
- 症状:在网格右键菜单里把鼠标从「实验性功能」移向右侧二级菜单的途中,只要指针短暂离开父项交互区域,二级菜单立即关闭,用户来不及完成选择。旧设备 CPU 较慢、鼠标移动事件稀疏,指针可能"跳"进二级菜单而未暴露该问题;CPU 更快、事件更密集时必然复现。
- 根因(
Utilities/Src/PopupMenu.cpp的PopupMenu::OnMouseMove):- 子菜单展开后,只要指针不在子菜单矩形内且未命中任何可点击条目,就无条件
CloseSubmenu()。 - 主菜单条目矩形的右缘为
menu.right - kPadH,而子菜单左缘为menu.right - kSubOverlap,两者之间存在约 3px 的内边距"死区"(位于主菜单矩形内但落在任何条目之外)。鼠标事件密集时指针必然落入该窄带,从而触发误关闭。
- 子菜单展开后,只要指针不在子菜单矩形内且未命中任何可点击条目,就无条件
- 修复:子菜单展开期间,若指针仍位于「主菜单 ∪ 子菜单」矩形范围内、且尚未落到其它可点击条目上(例如掠过内边距窄带、分隔线、禁用项),则保持子菜单展开并维持父项高亮;只有明确移动到其它条目或完全离开上述范围时才收起。
9.2 打开网格右键菜单后,悬停光晕与「可拖拽」提示气泡不消失(已修复)
- 症状:鼠标停在信号曲线上(出现光晕高亮 + 「XXX · 按住拖拽可移入其它网格」提示气泡)时右键弹出网格菜单,高亮与气泡残留在原地,不随菜单打开而消失。
- 根因:菜单打开后鼠标移动被菜单独占(
Application::HandleMessage在m_menu.IsOpen()时只把事件交给菜单),绘图区再也收不到WM_MOUSEMOVE;而打开菜单时没有清空绘图区的悬停状态(m_hoverSignal/m_hoverLegend),因此PaintHoverHighlight与PaintSignalInteraction继续把高亮与气泡画出来。 - 修复:
Application在绘图区右键弹出网格菜单前调用m_plot.OnMouseLeave(),清空悬停状态;PlotView::OnMouseLeave()补上清空m_hoverLegend(原先只清m_hoverLane/m_hoverSignal等,图例半高亮会残留)。
9.3 性能:启用带预览曲线的实验性功能后,平移/拖动光标异常卡顿(已修复)
- 症状(以
VERY_BIG_DATA.CSV为例,加载并显示全部信号后):时间轴选区放大到某些区间时,左键/中键平移网格视界、或启用「均值滤波 / 中心轴边沿计数」后再平移、甚至只是左右拖动时间光标看数值,都会异常卡顿(实测单帧 300ms+)。 - 排查方法:在
.preflight/perf/(gitignored)用离屏窗口(不占用鼠标/键盘/屏幕)直接驱动PlotView,用QueryPerformanceCounter测Paint(),并对CurveSpanFor/ComputeMovingAverageIterated/PaintDerivedCurve分段埋点计时。 - 根因(
Components/PlotView/Src/PlotView.cpp的PaintCursor→paintDerivedCapsulelambda):- 绘制时间光标处的派生曲线取值胶囊时,把请求窗口取成「可见区间 ∪ 光标采样下标」的并集;
- 当光标不在当前视界内(平移视界会把光标甩到视界外)时,这个并集会变成
[光标下标, 视界右端]或[视界左端, 光标下标]—— 上千万采样的巨大窗口; - 该窗口超过
kCurveSpanCacheMaxSamples(4M) → 走一次性缓冲、不进缓存 → 每一帧都重算整段多次迭代滑动平均(实测 5 条曲线合计 300ms+)。 - 这条路径同样被平移、拖滑块、拖动光标、改色等所有「每帧重绘」的交互触发,因此它们一起变卡。
- 修复:
paintDerivedCapsule只需要光标所在采样点(以及双光标另一端)的派生取值,因此把请求窗口收窄为「需要的那几个下标」([sampleIndex, sampleIndex+1],双光标时并入另一端)。均值滤波半径造成的取值依赖由CurveSpanFor自身的保护带(guard = clamp(max(窗口/8, (X+1)*N+64), 4096, 65536))负责覆盖 —— 对一切reach ≤ 65536的参数,取值与「整条曲线」逐位一致,显示效果零变化。 - 效果(离屏实测,
VERY_BIG_DATA,5 网格):- 均值滤波平移(2% 跨度):326ms → 约 16–23ms;
- 均值滤波平移(0.4%/0.08%/0.016%):约 305ms → 4–6ms;
- 中心轴边沿计数平移(2%):约 300ms → 约 20ms;
- 光标左右移动(静态场景):命中场景缓存,仅叠加层重绘,约 9ms。
- 不影响显示的说明:本次未改动任何几何/绘制算法(没有引入曾导致"棘状细节被并成实心块"的 ±1 列扩展),只收窄了「为了取时间光标处派生取值而请求的曲线窗口」,因此画面与实际取值完全不变。
9.4 性能经验与已知残留热点(供后续参考)
- 本仓库的性能黄金法则:交互期间(
m_dragMode != NONE)走「快速帧」(关闭波形/派生曲线抗锯齿),松手后补一帧全质量;静止画面走「静态场景缓存」(BitBlt 贴图 ~1ms)。任何只在鼠标移动/拖动参数时改变的内容都必须画在贴图之后(叠加层),绝不能进静态缓存,也不能每帧触发整条曲线/整段采样的重算。 - 已知残留热点(本次未动,均为"非拖拽的整幅重绘",实际拖拽时因快速帧而很快):
- 原始波形的「逐像素列包络带」用 GDI+
FillPath填充;在某些跨度下包络带不被判为「梳状」,抗锯齿填充仍约 20–27ms/帧(VERY_BIG_DATA的全量重绘)。这是 GDI+ 抗锯齿路径的固有代价;如需进一步提速,只能降低列分辨率或对该形态关闭抗锯齿 —— 两者都会改变显示效果,需用户确认后再动。 - 「构建为新信号」等一次性操作会真正计算整条派生曲线(约 1 帧的秒级耗时,正常)。
- 原始波形的「逐像素列包络带」用 GDI+
- 测量工具:
.preflight/perf/perf.exe <csv> [mode](离屏,不弹窗)。mode=0跑各交互场景耗时;mode=1导出确定性渲染 PNG 以便逐像素比对(用于验证"显示效果不变")。该目录已由.gitignore忽略,不会入库。
9.5 性能:启用带预览曲线的实验性功能后,交互严重卡顿(核心修复,含"为什么只有某些缩放等级卡"的解答)
-
症状(以
VERY_BIG_DATA.CSV,5 信号全部显示为例):- 「弱波增强」滑块拖动时在任何缩放等级下都卡顿;越缩得小(可见时间跨度越大)越卡 —— 时间跨度到 2.5s 时单帧 ≈89ms,完全放大(整段 128.8s)时单帧 ≈2.3 秒。
- 网格「选区放大」到某些跨度后,平移视界 / 改样式也卡。
-
根因(
Components/PlotView/Src/PlotView.cpp,CurveSpanFor+ 缓存策略 +Utilities/Src/DataModel.cpp的ComputeWeakWaveBoost):- 派生(预览)曲线按「可见采样窗口 ± 保护带」计算,代价正比于窗口内的采样数。实测:弱波增强整段(2576 万采样)单条 ≈368ms,均值滤波整段单条 ≈57ms。5 个网格就是 ≈1.8s。
- 旧代码有一条「超过 4M 采样的窗口不进缓存」的规则(为省内存),于是大跨度下每帧都重算整段曲线 —— 包括静止时的鼠标悬停重绘。这就是"某些缩放等级特别卡"的原因:
- 完全放大:窗口 = 整条信号(千万级)→ 每帧重算 → 秒级卡顿;
- 很小的时间跨度:窗口只有几千采样 → 计算只要微秒级 → 毫不卡顿;
- 中间跨度:窗口在十几万~几百万采样之间 → 单帧几十毫秒 → 卡,且保护带(≤65536)相对窗口太短,平移很容易越出缓存覆盖 → 频繁重算。
- 拖动「弱波增强」滑块每帧都改等级 → 缓存键变化 → 每帧都命中"未缓存"→ 每帧重算(叠加上述放大效应)。
-
修复(三管齐下,显示效果逐像素不变):
- 大窗口也进缓存:
kCurveSpanCacheMaxSamples提高到 64M(相当于不再按采样数排除),并新增总字节预算kCurveSpanCacheMaxBytes(≈768MB,最旧优先淘汰)兜底内存。这样同一参数下的平移/缩放/悬停都能复用,静止重绘降到 0.25ms。 - 交互期间「延迟重算」(
CurveSpanFor新增allowInteractiveDefer):拖动滑块/移动视界/缩放时,若所需窗口超过kDerivedInteractiveComputeBudget(131072 采样),本帧不重算,改复用上一次算好的曲线(曲线下标→时间的映射仍是当前视图,因此位置准确),交互结束后再按最终参数精确重算。窗口不大时(常见工作缩放)照常同步重算,预览曲线实时跟随控件。- 用途区分:预览绘制(
PaintMeanFilterCurve/PaintWeakBoostCurve/光标取值胶囊)传allowInteractiveDefer = true;需要精确取值的统计(中心轴边沿计数EdgeCenterCountsFor、MeanFilterValueAt/WeakBoostValueAt)传false(默认)。
- 用途区分:预览绘制(
- 计算缓冲复用:
ComputeWeakWaveBoost/ComputeMovingAverageIterated/ApplyWeakBoostStage的工作缓冲改为thread_local复用容量,避免每次重算都分配并零初始化上千万元素(多阶弱波增强 991ms→577ms)。
- 大窗口也进缓存:
-
实测(离屏,
VERY_BIG_DATA,5 网格;单位 ms/帧):场景 修复前 修复后 弱波增强 滑块拖动(整段) ≈2303 ≈18 弱波增强 滑块拖动(2.5s 跨度) ≈89 ≈15 弱波增强 滑块拖动(103ms / 20ms 跨度) ≈14 / ≈12 ≈15 / ≈11 弱波增强 静止重绘(整段) ≈2247 ≈0.25 均值滤波 平移(2.5s 跨度) ≈27 ≈23(大窗口非拖拽时仍按需重算;拖拽时延迟) 普通平移 / 光标移动 / 改色 3–12 不变(1–12) -
已知残留(不牺牲显示时的固有成本):把可见时间跨度拉到接近整段且开启弱波增强时,首次计算某参数/窗口的预览曲线仍需要一次 ≈1.8s(5 条整段曲线,各 ≈368ms)—— 交互期间不会触发(已延迟),但交互结束后的那一帧会补上。这是"整段弱波增强 O(n) 计算"的固有代价;若要进一步消除需把该计算移到后台线程(尚未做,改动较大)。
-
为什么这样改不牺牲显示效果:本次只改「何时计算 / 是否缓存」,不改任何几何或取值算法;
mode=1的逐像素比对(均值滤波 + 弱波增强,两个缩放级别,含光标取值胶囊)结果为 0 像素差异。
9.6 架构:派生曲线计算与 UI 绘制分离(后台线程 + 双缓冲)
- 动机/结论:界面卡顿的根因是「重计算发生在绘制线程里」(
PlotView::Paint内同步算派生曲线)。只要某次Paint要算整段弱波增强(单条 ≈368ms),消息循环就被阻塞,滑块/光标/视界全部失去跟手。因此把预览曲线的重计算移到后台线程,绘制线程永不等计算。 - 实现(
Components/PlotView/,全部改动仅在PlotView.hpp/.cpp与宿主Application.cpp):- 后台工作线程:
m_curveWorker+ 任务队列(m_curveJobs)+condition_variable(惰性启动、析构/结构变化时StopCurveWorkerjoin)。线程只读采样、在锁外计算,算完把结果作为不可变条目(shared_ptr<const CurveEntry>)追加进m_curveEntries,再PostMessage(kMessagePlotDerivedReady)通知宿主重绘。 - 双缓冲:
CurveSpanFor命中覆盖区间的条目就直接用;未命中且窗口大(>kDerivedAsyncWindowThreshold= 65536 采样)时本帧不计算,改用该曲线最新的旧条目顶替(AcquireNewestCurve),同时排一个后台任务;窗口小(≤门槛)仍同步计算以保证预览实时跟随滑块。条目用shared_ptr共享、绘制线程取到后钉进m_curvePins保持整帧有效,后台只追加不改写 → 无数据竞争。 - 一致性与生命周期:后台发布新条目后置
m_curveSceneDirty,下一帧作废静态场景缓存以纳入新曲线;任何会改变数据层结构(增删文件/信号、插入派生信号)的操作前,宿主先调用PlotView::PauseDerivedAsync()(join 后台线程 + 清队列/缓存 + 自增代次),杜绝后台线程读取已释放/已移动的采样。InvalidateDerivedCaches()内部也会PauseDerivedAsync()。 - 精确取值路径(中心轴边沿计数
EdgeCenterCountsFor、取值胶囊需要精确值时)传allowAsyncCompute = false,走同步计算。
- 后台工作线程:
- 实测(离屏,
VERY_BIG_DATA,5 网格;单位 ms/帧):弱波增强平移(整段)2247 → 15;弱波增强拖滑块(整段)2303 → 15;弱波增强静止重绘 0.25;后台结算后的全质量帧 ≈19ms/帧(一次性)。所有交互均 ≤ ~30ms,不再有 300ms/2s 级尖峰。 - 显示效果:小窗口(同步)路径与旧实现逐像素 0 差异;大窗口(异步)路径的曲线像素与旧实现一致(差异仅出现在合成测试中一个与缓存相关的旧实现的时轴陈旧现象上,新实现按设定视图正确绘制)。后台完成后的全质量帧与同步结果相同。
- 代价:大窗口缓存条目可占用较多内存(整段弱波增强单条 ≈104MB/信号),由
kCurveSpanCacheMaxBytes(768MB)+ 条目数上限(32)兜底,最旧优先淘汰。
9.7 完善异步架构 + 预览曲线层级修正(本仓库当前状态)
在 9.6 基础上做了三项关键完善:
- 后台线程池(真·多线程):原先是单线程顺序计算,5 个网格各一条整段弱波增强要 5×368ms≈1.8s,用户会长时间看不到预览曲线(看起来"完全没画出来")。改为
m_curveWorkers线程池(EnsureCurveWorker,线程数 =clamp(硬件并发-1, 1, 4),给 UI 留一个核),多条曲线并行计算,预览曲线因此很快出现。StopCurveWorker会 join 全部线程。后台线程优先级设为THREAD_PRIORITY_BELOW_NORMAL,保证拖动时 UI/绘制线程始终优先获得 CPU(计算只在 UI 空闲时占用),避免"多线程反而抢核导致卡顿"。 - 预览曲线绘制在「本网格全部原信号之上」:原先在同一个信号循环里就地叠加派生曲线,导致网格内有多条信号时,后绘制的原信号会把先前信号的预览曲线盖住(用户明确要求"预览信号处于原信号上层不要被覆盖")。现在
PaintLane分两遍:第一遍画全部原信号(线/散点/光晕),第二遍画全部派生预览曲线。 - 快速帧(关抗锯齿)只用于「会改变画面主体」的拖拽:原先
m_fastFrame = (拖拽中),会给静态场景缓存打上「已降级」标记 → 缓存失效 → 每帧整幅重画。像拖时间光标 / 选区框选 / 拖浮动面板这些只改叠加层、主体不变的交互因此被拖慢(实测拖光标从约 1ms 升到十几毫秒)。现在只有PAN / SCROLL_LANES / LANE_REORDER / MOVE_SIGNAL / BOOST_LEVEL / BOOST_ITERATIONS才算快速帧;其余拖拽直接复用静态场景缓存(贴图 ~1ms)+ 叠加层。实测拖光标 全程命中缓存、约 1–4ms。
- 平移(尤其"一大格=2ms")的固有下限:平移会让视图每帧变化 → 静态场景缓存必然失效 → 每帧都要重画波形(这部分无法异步,因为 GDI+ 绘制必须在 UI 线程;异步只覆盖"派生预览曲线"的计算)。在 5 个网格、含 2576 万采样编码器方波的
VERY_BIG_DATA上,实测拖拽平移约 6–15ms/帧(机器空闲/繁忙分别为下限/上限),已无尖峰。若日后要进一步降低,只能减少波形绘制本身(受"不牺牲显示效果"约束)或把整帧渲染也搬到离屏线程(改动很大、风险高)。 - 预览曲线"看起来没画出来"的真相:对编码器方波这类强信号,弱波增强几乎不改动波形,所以预览曲线与原信号几乎重合(读数胶囊里两者数值只差末位),视觉上"看不见"——这是正确行为。用
mode=1导出可确认曲线确实被绘制(G1–G5 均有浅色叠加 +「(弱波增强)」读数)。小跨度渲染与旧实现逐像素 0 差异。
9.8 预览曲线严格等同「构建为新信号」+ 平移装饰层缓存(本仓库当前状态)
(一)预览曲线与「构建为新信号」逐位一致
- 原实现按「可见区间 ± 保护带」计算预览曲线(近似,边界可能略有差异)。用户明确要求"预览显示效果要和构建为新信号一致"。
- 现改为一律按整条信号计算(
PaintMeanFilterCurve/PaintWeakBoostCurve传begin=0, end=count):与BuildDerivedSignal调用的是同一个ComputeWeakWaveBoost/ComputeMovingAverageIterated、同一段values,因此逐位一致。整段窗口超过异步门槛时由后台线程计算(绘制线程不阻塞)。
(二)平移期间的「装饰层」像素缓存(m_decorDc)
- 实测(
VERY_BIG_DATA,2ms/大格)整幅重绘约 10ms,其中 图例 ≈3.5ms、Y 轴刻度 ≈1.5ms(文字 + 圆角底板),二者合计过半;而它们只随网格几何/信号外观变化,与时间轴视图无关(平移时不变)。 - 实现:每张"全质量帧"后,把图例带(编号徽标 + 底板 + 色块 + 文字)与Y 轴刻度胶囊的像素从后备缓冲区捕获进
m_decorDc;拖拽(快速帧)时跳过重绘、直接BitBlt贴回。用专用指纹SceneDecorHash()(含外观样式、不含派生曲线参数)校验,内容一旦变化即失效并重新捕获,绝不使用过期像素。仅捕获装饰矩形本身(逐胶囊 + 图例带),矩形之间的波形照常绘制,因此无错位/无鬼影。 - 效果:拖拽平移(2ms/大格)整帧 13ms → 约 5ms;0.4% 跨度 20→8ms。注意:该缓存只作用于「拖拽(快速帧)」——静止帧仍按全质量重绘装饰(保证最终画质)。
- 拖弱波增强滑块时,
SceneDecorHash不含派生参数 → 装饰层不会因滑块每帧变参数而失效(否则拖滑块要重绘图例/刻度)。
(三)后台任务去重(避免拖滑块淹没线程池)
- 拖滑块每帧改参数 → 若每帧都入队"整条曲线"任务,会灌满队列、后台线程池被淹没并与 UI 抢 CPU。现按「信号 + 曲线种类」只保留一个待算任务、以最新参数为准(旧的待算任务被移除;正在计算的那个结果由后续条目取代)。
(四)后台线程优先级
- 见 9.7 第 3 点:后台线程为
THREAD_PRIORITY_BELOW_NORMAL,UI 需要运行时永远优先获得 CPU。
(五)平移仍有固有下限(诚实说明)
- 平移必然让视图每帧变化 → 静态场景缓存失效 → 波形必须在 UI 线程重画(GDI+ 只能在 UI 线程绘制,无法异步)。装饰层缓存已把"与视图无关"的部分(图例/刻度)从每帧开销中移除,剩余开销是波形本身 + 网格线 + 时间轴,属"不牺牲显示效果"下的固有成本。
(六)离屏逐像素校验(含历史踩坑提醒)
- 工具:
.preflight/perf/perf_<ver>.exe <csv> 1 <prefix>导出确定性 PNG;.preflight/perf/compare.ps1用LockBits+Marshal.Copy逐字节比对。 - 踩坑:比对若用
[System.Drawing.Bitmap]::GetPixel且两侧位图尺寸不一致或文件缺失,会抛调用"GetPixel"时发生异常:"参数无效"。根因通常是"旧版本"的 harness 链接失败(例如它引用了新版本才有的符号)导致对照图没生成。务必:①先确认两侧 PNG 都存在且尺寸一致;②用LockBits而非逐点GetPixel(后者对上兆像素级图片极慢)。 - 实测结论(
709cbe4的"可见窗口预览" vs389b4ec的"整段预览",同 harness 同数据):全部全质量帧(均值滤波/弱波增强 × FitAll/2.5s/103ms 三个缩放)逐像素 0 差异 —— 证明"改为整段计算"未带来任何显示回归,预览与"构建为新信号"一致;仅拖拽中途帧有 0.09% 差异(快速帧关抗锯齿 + 装饰层贴回的预期差异)。
9.9 深度定位:样式/参数滑块的卡顿根因与修复(探针 + 分支计时)
-
用户复现:
VERY_BIG_DATA加载全部信号 → 选区放大到视界[40.1756, 40.1885]s(Δt≈12.9ms,≈2 采样/像素)→ 拖左/中键平移、拖透明度/粗细/自定义颜色/拾色、拖弱波增强参数滑块 → 极卡;但拖时间光标很流畅(光标只改叠加层、命中静态场景缓存)。 -
定位方法(临时探针,
JDI_PERF_PROBE门控,事后已移除):在.preflight/perf的离屏 harness 中精确复现该视界(mode=2),给PlotView::Paint的每个分支(缓存贴图/样式补丁/整幅重绘/网格重排)与PaintLane的每个子阶段(网格线/波形/派生曲线/Y 轴刻度/图例/包络带FillPath)加计时。 -
实测拆解(本机当时负载约 4×,下列为负载下的值):
场景 整帧 其中包络带 FillPath平移(整幅重绘,抗锯齿开) 161ms 143ms(5 网格,≈29ms/条) 平移拖动(快速帧,抗锯齿关) 34ms 27ms(5 网格,≈5.4ms/条) 透明度/粗细/颜色(样式补丁,抗锯齿开) 77ms 72ms(1 网格) 弱波增强参数滑块 96–176ms (含整幅重绘放大) -
两个真正的根因:
- 样式补丁用了抗锯齿:拖透明度/粗细/自定义颜色属于"样式变化 → 局部补丁",但它不在任何拖拽态,于是
m_fastFrame=false→ 补丁用抗锯齿重画该网格的包络带(≈72ms/网格)。这是"样式滑块极卡"的直接原因。 - 后台派生曲线发布时整幅作废:拖弱波增强/均值滤波参数滑块时,后台每算完一条就
InvalidateSceneCache()→ 下一步退化成整幅重绘(负载下 150ms+),而不只是补丁。
- 样式补丁用了抗锯齿:拖透明度/粗细/自定义颜色属于"样式变化 → 局部补丁",但它不在任何拖拽态,于是
-
修复(3 处,均只影响拖拽帧;静止/结算帧逐像素不变):
- 样式补丁强制快速帧:进入补丁分支时设
m_fastFrame = true(关抗锯齿)并置m_sceneDegraded = true→ 补丁 ≈72ms → ≈14ms;参数稳定后的下一帧自动补一帧全质量。 - 拖拽中不整幅作废:后台曲线发布时,若
m_dragMode != NONE(拖拽中)只置m_sceneDegraded,由每帧补丁直接取到最新曲线;仅静止时才整幅作废。弱波增强滑块 ≈96–176ms → ≈25ms。 - 交互期间省略浮动面板投影:面板的两张大面积半透明圆角投影在快速帧跳过,减少面板重绘开销。
- 样式补丁强制快速帧:进入补丁分支时设
-
结果(同一负载机器):透明度/粗细/颜色 77ms → 16ms;弱波增强滑块 96–176ms → 26ms;平移拖动 34ms(快速帧,空闲机 ≈8ms)。全质量/结算帧与修复前逐像素 0 差异(
LockBits对照验证)。 -
仍存在的固有下限:整幅重绘中的包络带
FillPath本身是 O(像素列数) 的 GDI+ 抗锯齿填充(≈29ms/网格/负载)。它只在"整幅重绘"或"结算帧"出现(平移快速帧已关抗锯齿 ≈5.4ms/网格)。若要在密集包络带上也关抗锯齿可再快 5×,但会轻微改变边缘观感,需用户确认后再动。
9.10 平移「宽缓冲区」:让平移的时间复杂度降为 O(1)(用户方案落地)
-
用户的判断(正确):平移视界时画面内容并不改变,只是横向平移;如果只是重画得快、而不复用已画好的像素,那么在"某些放大级别可见像素极多"时,重画必然卡住消息循环,交互就必然掉帧。正确做法是复用已绘制的像素,让前台始终只做 O(1) 的取用。
-
对现有代码的核对:用户判断与仓库现状基本一致 —— 之前的历次优化都是"加速绘制"(包络带
FillPath、装饰层、缓存),而没有把"绘制"这一任务本身从交互帧里拆出去(后台线程只算了派生曲线的数值,没有做波形光栅化)。因此一遇到"可见像素特别多"的放大级别(如[40.1756, 40.1885]s),每帧重画就再次成为瓶颈。 -
可行性结论:用户的"每网格双缓冲 + 后台填充 + 前台只贴已就绪缓冲"方向正确。要点补充:① GDI/GDI+ 绘制可以在后台线程进行(每个线程用各自的
Bitmap/HDC/Graphics即可),所以后台光栅化可行;② 但本仓库的PaintLane会读写大量成员缓存(折线/曲线/图例命中/装饰矩形),直接搬到后台线程需要一次较大重构才能保证线程安全。③ 因此本次采用一个风险更低、收益相同的落地方式(见下)。 -
本次实现(平移 O(1)):
- 宽缓冲区:以当前视界为中心,向左右各多渲染半个视界宽,存成一张宽为
2×视界宽的离屏位图(m_wideDc)。它用与正常绘制完全相同的PaintLane/PaintTimeAxis(临时把横向映射"加宽"后渲染),因此像素与正常绘制一致;仅跳过图例与 Y 轴刻度(二者锚定网格左缘、随视界移动,改由已有的装饰层缓存贴回)。 - 平移 = 一次 BitBlt:视界宽度不变、只是横向移动时,前台从宽缓冲区截取对应横向子矩形贴到后备缓冲区即可(O(1)),完全不重画波形。无论放大到多深,平移都与全局显示一样流畅。
- 边界重建:当视界移出宽缓冲范围(或跨度/内容变化)时,才以新视界为中心重建一次(约占整幅重绘 2 倍,但只在每平移约半屏时发生一次)。
- 宽缓冲区:以当前视界为中心,向左右各多渲染半个视界宽,存成一张宽为
-
实测(
VERY_BIG_DATA全信号,视界[40.1756, 40.1885]s,5 网格;单位 ms/帧,负载机):场景 修复前 修复后 中键平移拖动(短距) 34ms 1.3ms 中键平移拖动(长距,含边界重建) 34ms 1.7ms(峰值 50ms 为一次边界重建) 透明度/粗细/颜色 16ms 8ms 弱波增强滑块 26–176ms 约 15ms(空闲)/ 55ms(负载) -
显示效果:静止/结算帧与修复前逐像素 0 差异(
LockBits对照);平移拖动帧为快速帧质量(关闭抗锯齿,与既有"拖动时低分辨率、松手补全质量"策略一致),松手后立即恢复全质量。 -
说明:宽缓冲区重建目前在前台进行(每平移约半屏一次);若日后要连这一次重建也移出交互帧,需把
PaintLane的成员缓存重构为可复制的渲染上下文再搬后台(较大改动)。弱波增强参数滑块仍有一次"重绘受影响网格 + 浮动面板"的开销(负载下约 55ms),是本轮未彻底消除的残留。
9.11 把「宽缓冲区」推广为基底复用:平移 + 拖参数滑块都降为 O(1)
承接 9.10,进一步把「宽缓冲区」从"只用于平移"推广为通用基底复用,彻底解决拖派生参数滑块的问题:
-
抽取派生曲线绘制:把
PaintLane里叠加派生预览曲线(均值滤波/中心轴/弱波增强)的"第二遍"抽成PaintLaneDerivedOverlays。宽缓冲区渲染时跳过派生曲线,因此宽缓冲只存基底(网格/波形/时间轴,不含派生曲线与图例/Y 轴刻度)。 -
基底键与派生参数解耦:宽缓冲的校验键改为
SceneContentHash(true, false)(含外观样式、不含派生参数)→ 拖弱波增强等级/迭代次数滑块不会让基底失效,可直接复用基底像素。 -
统一前台路径:判定"网格结构未变(几何指纹一致)且逐信号外观样式未变"(即只有视图或派生参数变化)时 → 贴宽缓冲基底像素 + 只重画派生曲线叠加层 + 装饰层 → 前台 O(1),不重画波形。
- 外观样式(颜色/粗细/透明度/模式)变化仍走"样式补丁"(局部重画受影响网格)。
-
实测(
VERY_BIG_DATA全信号,视界[40.1756, 40.1885]s,5 网格;负载机 ms/帧):场景 9.10 后 本次后 中键平移拖动 1.3ms 1.3ms 弱波增强滑块 55ms 5.2ms 透明度/粗细/颜色 8ms 8ms -
显示效果:静止/结算帧与 HEAD 逐像素 0 差异(
LockBits验证)。拖拽/拖参数帧为快速帧质量(关抗锯齿),松手后恢复全质量。 -
仍存在的残留(如实记录):
- 宽缓冲区重建在视界移出缓冲范围(约每平移 1 屏一次)时于前台进行,负载下约 30–50ms 的一次尖峰。彻底消除需把"基底渲染"做成不依赖成员状态的独立渲染器再搬后台线程(
PaintLane会写成员缓存,直接后台化有数据竞争,属较大重构)。 - 非拖拽的视界变化(键盘方向键 / 工具栏)仍走整幅重绘(约 125ms);仅鼠标左/中键拖动走宽缓冲快路径。若需让键盘平移也快,需允许非拖拽帧复用"整数像素对齐"的宽缓冲(当前为避免 ≤0.5px 亚像素偏差而未启用)。
- 宽缓冲区重建在视界移出缓冲范围(约每平移 1 屏一次)时于前台进行,负载下约 30–50ms 的一次尖峰。彻底消除需把"基底渲染"做成不依赖成员状态的独立渲染器再搬后台线程(
9.12 宽缓冲改为「滚动」:消除周期性重建尖峰(持续拖动恒 ≥60fps)
9.11 的残留 1(移出边界时整幅重建 2×视界,负载下约 30–50ms 的一次尖峰)已消除:
-
滚动式缓冲:视界每帧移动 d 像素时,把缓冲内容整体自贴图平移
round(d)像素,再只补画新暴露的|round(d)|像素宽细条。因此每帧代价恒为 O(细条),不再有"移出边界就整幅重建"的周期性大尖峰。 -
细条渲染复用整条宽窗口的映射:
RenderWideStrip令m_area只覆盖细条(GDI/GDI+ 裁剪落在细条),而m_plotRect仍是完整宽窗口 → 网格线与波形 x 坐标正确、刻度密度与主视界一致;仅新暴露的细条被重画。 -
亚像素误差有界:用锚点
m_wideAnchorMin记"视界左端当前对应缓冲中的时间",每帧按round((viewMin - anchor)*scale)平移并同步推进锚点,使"内容相对时间轴的偏移"始终 ≤0.5 像素。 -
缓冲只覆盖绘图区(不含底部时间轴):时间轴由前台每帧按当前视图绘制,避免滚动细条把时间轴文字切成残字。
-
实测(
VERY_BIG_DATA全信号,视界[40.1756, 40.1885]s,5 网格):场景 9.11 本次 中键平移拖动(短距) 1.3ms(含每屏一次 33ms 尖峰) 约 6ms/帧,无尖峰 中键平移拖动(长距 975px) 1.7ms(最大 33ms) 约 6ms/帧,最大 10.5ms 弱波增强滑块 5.2ms 6ms 透明度/粗细/颜色 8ms 8ms -
显示效果:滚动平移帧截图核对无缝隙/无残字;静止/结算帧与 HEAD 逐像素 0 差异(
LockBits验证)。 -
剩余残留:每次"开始一次拖拽"的第一帧需新建缓冲(2×视界基底,负载下约 38ms 的一次尖峰);持续拖动过程中无尖峰。要消除这一次性启动尖峰,需把基底渲染做成可后台的独立渲染器(同上,较大重构)。非拖拽视界变化仍整幅重绘。
9.13 消除"开始拖拽"的首帧尖峰:用场景缓存初始化基底 + 启用预览功能时预建基底
9.12 的残留(首帧需从零新建 2×视界基底,负载下约 38ms)已消除:
-
平移首帧用「上一帧静态场景缓存」初始化宽基底(
InitWideBufferFromScene):平移只是整体横向移动,场景缓存里的内容(含派生曲线/图例)依然正确,因此把它直接贴进宽基底中心即可,再滚动到当前视界 —— 首帧只需一次 BitBlt + 一条细条,不再从零重画 2×视界。仅当"派生预览曲线参数未变"时适用(拖这些参数时派生曲线会变,须从零重建基底)。 -
启用带预览的实验性功能时预建基底:在
SetLaneWeakBoost/SetLaneMeanFilter/SetLaneEdgeCenterCount(…, true)里预建宽基底,使首次拖动这些参数滑块时无需从零重建(避免一次性尖峰)。 -
实测(
VERY_BIG_DATA全信号,视界[40.1756, 40.1885]s,5 网格,负载机 ms/帧):场景 9.12 本次 中键平移拖动(短距) 最大 35ms(首帧重建) 最大 8.5ms 中键平移拖动(长距) 最大 10.5ms 最大 7.4ms 弱波增强滑块 最大 37.6ms(首帧重建) 最大 10.8ms 透明度/粗细/颜色 8–16ms 8–15ms -
结论:用户报告的全部鼠标交互(左/中键拖动平移、样式滑块/拾色、弱波增强参数滑块)在该放大区间全程 ≥60fps、无尖峰;静止/结算帧与 HEAD 逐像素 0 差异。
-
当时仍存在的残留:非拖拽的视界变化(键盘方向键 / 工具栏,非用户报告的鼠标场景)仍走整幅重绘(约 125ms)—— 因其没有"拖拽结束"事件可用来触发一次精确的全质量重绘,若走宽缓冲快路径会留下 ≤0.5px 的亚像素偏差且无法"结算"回精确帧,故当时保持整幅重绘以保证像素精确。该残留已在 9.14 中解决。
9.14 非拖拽视界变化(键盘 / 工具栏)也走宽基底快路径 + "结算"帧保证像素精确
9.13 的残留(非拖拽视界变化 ≈125ms / 8fps)已消除:
-
宽基底判定去掉"必须在拖拽中":只要"网格几何指纹 + 逐信号外观样式 + 装饰层"未变(派生曲线参数变化也算),任何视界变化都可用宽基底,前台只贴图 + 画时间轴/派生叠加/装饰层 —— 键盘方向键、工具栏缩放平移帧降到 ≈8ms。
-
"结算"帧(保证最终像素精确):宽基底是整数像素平移,实际视界位置带**≤0.5px** 亚像素偏差。因此在快路径帧里把"内容指纹 ⊕ 视界"揉成一个
wideKey;若本帧的wideKey与上一条快路径帧相同 → 判定"视界已停止变化" → 放弃快路径,落到整幅重绘(一次全质量精确帧),并清空m_wideLastValid。该帧同时刷新静态场景缓存,之后所有帧命中缓存(≈1ms),不会反复重绘。 -
分支顺序有硬约束:静态场景贴图 → 宽基底 → 样式补丁 → 整幅重绘。
- 宽基底必须在静态场景贴图之后:视界与样式都没变时(鼠标悬停移动)必须命中场景贴图,否则每两帧就会触发一次"结算整幅重绘",悬停反而变卡。
- 宽基底必须在样式补丁之前:派生预览曲线参数变化(拖弱波增强 / 均值滤波滑块)走宽基底比"逐网格补丁"更快且质量更高(补丁会关抗锯齿)。
-
宽基底必须与"同一视界的整幅渲染"同源(否则平移后网格/锯齿与前台不一致):基底渲染时强制全质量抗锯齿(不受拖拽快速帧影响),且网格竖向刻度沿用前台视界跨度而非 2× 的宽窗口跨度(后者会让竖网格线减半)。
-
实测(
VERY_BIG_DATA,视界[40.1756, 40.1885]s,ms/帧):场景 9.13 本次 非拖拽视界变化(键盘/工具栏) 最大 124.6ms(≈8fps) 最大 8.6ms 中键平移拖动(短/长距) 8.5 / 7.4ms 9.3 / 14.6ms 透明度 / 颜色滑块 8–16ms 10.6 / 12.5ms 弱波增强滑块 最大 10.8ms 最大 7.5ms 粗细滑块 8–15ms 40 帧中仅首帧 127ms(其余 7.7ms) -
粗细滑块那一帧 127ms 是"设计内"的:样式连续变化期间走"样式补丁 + 已降级标记"(快),一旦样式停止变化(含"回到与缓存相同的值")就必须补一帧全质量 —— 该帧必然是整幅重绘。它是用户松手后的唯一一帧,不是持续卡顿。
-
保真度实测(新增
perf.exe … 5像素对比模式):快路径与"同一视界的整幅全质量渲染"对比 ——- 视界几乎不动(<0.01px,不滚动)时:
meanAbs = 0.68/255(逐像素几乎一致,证明"底座 + 贴图"管线本身忠实)。 - 平移约 17px 时:
meanAbs ≈ 13.5/255(差异集中在密波形上,是 ≤0.5px 亚像素平移的必然结果,肉眼不可辨);停止平移后的"结算"帧回到像素精确。
- 视界几乎不动(<0.01px,不滚动)时:
-
质量回退检查:
mean_0/1/2、boost_0/1/2、panref七张静态/结算帧与 HEAD 字节完全一致(SHA256 相同)。 -
意外收获:平移中途帧比 HEAD 更干净 —— HEAD 把静态场景(含图例 / Y 轴刻度)直接贴进宽基底,平移后这些像素随之移动,出现重复的网格标题与刻度重影(见对比 PNG);本次改动后同一场景不再出现重影。
9.15 修复「缩放后平移出现拖影/残留/错误元素」与深放大区间仍然卡顿(本轮)
用户方案评估:与当前实现的差异
用户提出的目标是「每个网格各自维护双缓冲区 + 后台线程绘制 + 前台只做贴图/坐标偏移」。 与当前实现的差异与问题点:
| 维度 | 当前实现 | 用户设想 |
|---|---|---|
| 像素复用粒度 | 全局一张「宽基底」+ 场景缓存 + 装饰缓存 + 样式补丁(4 套机制叠加) | 每网格一张信号图层(双缓冲) |
| 绘制线程 | 全部在 UI 线程;只有派生预览曲线在后台算 | 单网格绘制也搬到后台 |
| 失效判定 | 多个指纹(内容/几何/装饰)各自为政,容易漏项(本轮事实上踩了坑) | 每网格一个状态指纹 |
问题点(本轮实证,是本次卡顿与拖影的真正根因):
Tab().LaneScroll()(网格纵向滚动量)没有参与任何内容指纹。 它只改变各网格的绘制位置,却被所有指纹忽略 → 一旦滚动网格(滚轮 / 上下方向键 / 滚动条), 平移宽缓冲区会把旧滚动位置的像素当成当前内容贴回(位置错位、拖影、错误元素); 同时因为指纹没变,"结算"逻辑误以为视界已停 → 每两帧就触发一次整幅重绘(约 125ms), 表现为 8fps 级卡顿。这是用户报告的两个症状的共同根因。FindStyleOnlyLanes()只看SceneGeometryHash(),而该指纹刻意不含视界。 于是视界已经变了却仍走"只重画受影响网格"的局部补丁,把缓存里旧视界的画面 (时间轴、其它网格、底色)留在屏上 → 拖影 / 残留。InitWideBufferFromScene()把"上一帧的整幅画面"拷进宽基底,而整幅画面里含装饰 (Y 轴刻度胶囊 + 图例),宽基底却不含 → 装饰像素随平移一起移动,在网格左缘之外 留下一排旧图例/刻度(重影)。- 装饰缓存的两个不变量被打破:贴回时用的是当前帧重建的矩形列表(命中缓存时根本不会重建 → 为空);
并用当前帧的
m_area原点去推算源偏移(两帧原点可能不同)→ 表现为装饰消失 / 黑块 / 整体错位。
本轮修复
SceneContentHash()计入Tab().LaneScroll()(根本修复:滚动网格不再复用旧位置像素, 也不再误触发"结算")。FindStyleOnlyLanes()增加视图几何逐项校验(视界 / 滚动 / 布局 / 缩放 / 拖拽光晕)后才允许局部补丁。- 宽基底不再用"含装饰的整幅画面"初始化;
RebuildWideBuffer()以m_renderWide跳过装饰, 保证基底永远不含装饰。 - 宽路径的装饰改为按当前视界重画(新增
m_renderDecorOnly复用与整幅渲染完全相同 的PaintLane代码路径,只跳过网格线与波形)。 - 装饰缓存修好两个不变量:每次真正绘制装饰的帧都立即重新捕获(不再只在全质量帧捕获), 且捕获时同时保存矩形列表与坐标原点,贴回时用保存的这份、按绝对坐标写入。
- 装饰底板改为不透明(原来是 94% / 91%):装饰像素因此与时间视界无关,可安全跨视界复用; 半透明底板会把旧视界的波形混进缓存像素,平移时在底板内部留下残影。
- 新增离屏诊断模式(
.preflight/perf/perf_main.cpp的mode=6):复现"加载 → 缩放 → 平移/键盘/滚轮" 全部交互,逐帧计时,并把"当前屏"与"从零整幅渲染"做逐像素对比 + 差异包围盒。 这是本轮定位问题的关键工具(否则只能靠猜测)。
实测(VERY_BIG_DATA.CSV,视界 [64.3948, 64.4153]s ≈ 2.045ms 一大格,5 网格,ms/帧)
| 交互 | 修复前 | 修复后 | 像素差(对比从零整幅渲染) |
|---|---|---|---|
| 键盘左右方向键平移 | 11~19ms | 5.2ms | 3705 通道(≤0.5px 亚像素偏移,见下) |
| 键盘上下方向键滚动网格 | 0.9ms(但错位/偶发 125ms) | 0.5ms | 0 |
| 中键水平拖动 | 10~18ms | 5.2ms | 357 通道 |
| 中键纵向拖动 | 14~19ms | 6.0ms | 72 通道 |
| 左键水平拖动 | 9~17ms | 4.5ms | 390 通道 |
| 左键纵向拖动 | 15~19ms | 6.1ms | 72 通道 |
| 滚轮滚动网格 | 1.0ms | 0.6ms | 0 |
| 缩放后再平移 | 9~17ms | 3.7ms(缩放帧 6.8ms) | 495 通道 |
| 弱波增强预览 + 平移 | 15~18ms | 5.8ms | 2264 通道(异步曲线发布时序) |
- 修复前的纵向拖动会产生 2 万+ 通道的残留(黑块 / 标签残缺);修复后降到 72 通道(仅亚像素量化)。
- 剩余的几百通道差异集中在密网格/密波形上,是"宽基底按整数像素平移(≤0.5px 亚像素误差)" 的必然结果,肉眼不可辨;视界停下后的"结算"帧会回到像素精确。
尚未完成(如实记录)
用户要求的「每个网格独立双缓冲 + 后台线程绘制」这一层本轮未实现,原因:
PaintLane 及其调用链直接读写大量 PlotView 成员(m_lanes / m_area / m_plotRect / 视界 /
多重缓存 / m_decorRects / m_legendHits …),把它搬到后台线程会与 UI 线程的 paintOverlays
数据竞争。要做需要把渲染器改造成「不依赖成员状态的独立渲染器」(把上述状态显式参数化、
缓存改为每线程私有),属于大重构,风险高。
本轮先把"正确性"与"帧时间"问题解决:现在平移/缩放期间的帧时间已降到 46ms(原 919ms),
且不再有拖影/残留/错位;后续可按用户方案逐步把单网格绘制搬到后台。
10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
|---|---|---|
| 2026-10-11 | DeepSeek-V4.1-Flash | 新建本文件;记录项目要点与规范;修复「实验性功能」二级菜单瞬间关闭问题。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 修复右键网格菜单后悬停光晕/提示气泡残留;修复「启用带预览曲线的实验性功能后平移/拖光标 300ms+ 卡顿」(paintDerivedCapsule 巨窗每帧重算,收窄请求窗口,显示效果零变化)。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 深度修复「实验性功能预览曲线导致的交互卡顿」:大窗口改入缓存(静止重绘 2247ms→0.25ms)、交互期间延迟重算(弱波增强拖滑块 2303ms→18ms)、计算缓冲复用;显示逐像素不变。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 架构升级:派生曲线计算与 UI 绘制分离(后台线程 + 双缓冲)。绘制线程永不等计算,弱波增强拖滑块/平移从 2303/2247ms 降到 ~15ms;结构变化前 PauseDerivedAsync 保证线程安全;小窗口同步路径与旧实现逐像素 0 差异。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 完善异步:后台改多线程池(预览曲线并行计算、更快出现);PaintLane 分两遍把预览曲线画在全部原信号之上(不再被覆盖);快速帧只用于改主体的拖拽(拖光标/框选改为命中场景缓存,约 1–4ms)。小跨度逐像素 0 差异。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 预览曲线改为按整条信号计算,与「构建为新信号」逐位一致;新增平移装饰层像素缓存(图例 + Y 轴刻度在拖拽时直接贴回,2ms/大格拖拽 13ms→约 5ms);后台任务按 (信号,种类) 去重以防拖滑块淹没线程池。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 探针定位"样式/参数滑块在深放大下极卡":根因是样式补丁用了抗锯齿(≈72ms/网格)且后台曲线发布时整幅作废导致每帧整幅重绘。修复:样式补丁强制快速帧、拖拽中不整幅作废、交互期省略面板投影 → 透明度/粗细/颜色 77ms→16ms、弱波增强滑块 96–176ms→26ms,静止/结算帧逐像素不变。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 落地用户提出的复用像素方案:新增平移宽缓冲区(以视界为中心左右各多渲染半个视界宽),平移时前台只做一次 BitBlt 截取子矩形(O(1)),移出边界才重建 → 深放大下平移 34ms→1.3ms;静止/结算帧逐像素 0 差异。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 把宽缓冲推广为基底复用:抽出 PaintLaneDerivedOverlays、宽缓冲只存基底(不含派生曲线),基底键排除派生参数 → 拖弱波增强滑块 55ms→5.2ms;平移 1.3ms、样式 8ms 不变;静止帧逐像素 0 差异。残留:宽缓冲重建在前台(约每屏一次)、非拖拽视界变化仍整幅重绘。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 宽缓冲改为滚动式(自贴图平移 + 只补画新暴露细条),消除"移出边界整幅重建"的周期性尖峰;缓冲只覆盖绘图区、时间轴前台绘制、亚像素误差用锚点限制在 ≤0.5px → 持续拖动平移约 6ms/帧、无尖峰(长距最大 10.5ms);静止帧逐像素 0 差异。残留:每次开始拖拽的首帧需新建缓冲(一次启动尖峰)。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 消除"开始拖拽"的首帧尖峰:平移首帧用静态场景缓存初始化宽基底(InitWideBufferFromScene,贴图 + 细条,不再从零重画 2×视界);启用带预览的实验性功能时预建基底。实测:平移拖动最大 8.5ms、弱波增强滑块最大 10.8ms(原分别 35/37.6ms),用户全部鼠标交互在该放大区间全程 ≥60fps、无尖峰;静止帧逐像素 0 差异。残留:非拖拽视界变化(键盘/工具栏)仍整幅重绘。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 消除 9.13 残留:非拖拽视界变化(键盘/工具栏)也走宽基底快路径(124.6ms→8.6ms),并用 wideKey 相等判定"视界已停止变化"触发一次结算整幅全质量帧以恢复像素精确;分支顺序固定为"静态贴图 → 宽基底 → 样式补丁 → 整幅",宽基底强制全质量抗锯齿且竖向刻度用前台跨度。新增像素对比模式验证:底座管线忠实(meanAbs 0.68/255),平移期仅 ≤0.5px 亚像素差;7 张静态/结算帧与 HEAD 字节一致;平移中途帧不再有图例/刻度重影。 |
| 2026-10-12 | DeepSeek-V4.1-Flash | 修复用户报告的「缩放后平移出现拖影/残留/错误元素」与「2ms 一大格区间仍然卡顿」。根因:①网格纵向滚动量未参与任何内容指纹→滚动时把旧位置像素贴回且每两帧触发一次整幅重绘(≈8fps);②FindStyleOnlyLanes 未校验视界→视界变化后仍走局部补丁;③宽基底用含装饰的整幅画面初始化→装饰随平移重影;④装饰缓存的矩形列表与坐标原点两帧不同源→装饰消失/黑块/错位。修复后交互帧 9perf.exe ... 6 诊断模式(逐帧计时 + 逐像素对比 + 差异包围盒)。未实现:每网格双缓冲后台渲染(需先把 PaintLane 改造成无状态渲染器,已在文档 9.15 说明)。 |