.mat 改为流式解码(内存映射 + 逐 chunk 填充);MF4 支持 ##DL/##DZ/##HL;光标数据点被时间面板遮住时改三角

1) .mat(BIG_MAT.mat 1.29GB / 5 信号 x 16,142,471 采样)
   旧链路同时驻留三份内存:整文件 fread 进 vector、整个数据集解压成字节数组、再 memcpy 成
   double;外加 streams 缓存与每条信号各一份时间列拷贝。实测私有内存 +1233.9MB、117s。
   现改为:
   - MatHdf5::Open 用 CreateFileW + CreateFileMappingW + MapViewOfFile(MappedBytes 只读视图,
     解析代码零改动;虚拟地址不等于物理内存,未访问页不驻留);
   - 新增 ReadDatasetInfo(只读元信息)+ StreamDataset(ByteSink)(连续布局 4MB 切片、
     分块布局逐 chunk inflate)+ ReadDoubleStream(SDI #data# 字节流按 8 字节一组直接写 double,
     7 字节尾巴处理非 8 对齐 chunk);ReadNumbers 改为流式转换;
   - MatLoader 先收集信号清单,再按(分组,时间流)合并为一个 TableBlock:时间列按内容去重、
     数据列直接 move;进度改成 0-0.02 映射 / 0.02-0.98 逐流解码 / 1.0。
   结果:117.3s -> 12.5s,私有内存 +1233.9MB -> +740.4MB(最终列数据 738.9MB)。
   关键性能修正:SDI 的 #data# 是 dims=[N]、chunk=[N,1],归一化后每行只有 1 字节;旧的逐行回调
   (且每行分配 coordinate 向量)只有 11MB/s。把目标连续的行合并成一次回调后约 110MB/s。
   顺带把 inflate 的 Huffman 解码改成 9 位快查表,并抽到新的 Deflate.hpp/.cpp 供 MF4 复用。

2) MDF4 数据块
   Problem-MF4.mf4 的第二个数据组把记录写在 ##DL(数据列表,两个 ##DT:131040+504 字节)
   里,旧实现只认 ##DT,整组被跳过,于是 8 个信号只读出 4 个。
   新增 CollectDataSegments(递归展开 ##DL / ##HL,叶子识别 ##DT / ##DV / ##DZ)与
   ReadRecordSegments(跨段拼接、压缩段先 inflate、非压缩段仍按 4MB 分块读),
   压缩段的记录长度改用 max(cg_record_id_len*8, 各通道 bitOffset+bitCount) 推算(载荷是压缩长度)。
   实测 8 个信号:gVP_WindowLearnSt[0] 94 行、gVR_WinOpenReq[0] 9396 行,时间列单调。
   ##SD(已排序数据)仍未实现,这类数据组会被跳过。

   另按要求只做检查、不改代码:该文件中 gVP_WindowLearnSt[0] 与 gVR_WinOpenReq[0] 都存在
   值枚举文本(##CC,cc_type=7;links[4+i] 对应 cc_val[i]),清单已写入 Documents/agent_mem.md。

3) 光标数据点标记
   光标的时间读数与差值胶囊绘制在绘图区底部、且在取值标注之后绘制,靠近底部的数据点圆点
   会被完全盖住。新增 MarkerCoveredByCursorLabel(圆心落入面板或重叠面积达到半个圆)
   与 CoverHiddenMarker(改成向下的三角、锚点放到面板上沿上方),在原始信号与派生曲线胶囊
   两处调用,语义与“数据点在视界下方”一致。

验证(全部离屏:SW_HIDE 窗口 + 只注入合成消息 + BELOW_NORMAL,不占用鼠标键盘屏幕):
- 新增 .preflight/round4check 34/34;A/B:临时禁用 ##DL 支持 => 2 项如实失败;
  临时禁用 CoverHiddenMarker => 3 项如实失败(含像素断言为 0);
- 回归:.preflight/round3check 31/31、editcheck 20/20、check 137/137;
- make release / make debug 零警告;Build/Release/Target.exe 785,920 字节(< 800KB)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-19 20:13:17 +08:00
1 parent be76f523f3
commit 75f0fafd41
10 files changed
+1677 -795

No files matched your search

+158
View File
@@ -2999,6 +2999,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
| 2026-10-19 | DeepSeek-V4.1-Flash(**本机**) | **组名/信号名编辑态点击穿透 + 浮动面板 GX 徽标悬停/点击联动 + 最小网格高度 1.5 倍(详见 §12 末尾「2026-10-19 第三轮」与 §11.2 第 151–153 条)**。① **点击穿透根因**:改名时行高大于输入框高度(上下各 3 逻辑像素),起点落在内边距上会继续走 `OnLButtonDown` 的行分支 ⇒ 分组展开/收起;且第二次快速按下被合成为 `WM_LBUTTONDBLCLK`,旧 `OnDoubleClick` 无编辑态保护 ⇒ 同样折叠/再次改名。修法:`NameEditAbsorbRect`(输入框横向范围 × 整行高度)在 `OnLButtonDown`/`OnEditMouseDown` 一律吸收为文本操作,`OnDoubleClick` 编辑态命中即返回。② **GX 徽标**:`EdgePanelLayout::badge` 成为唯一几何来源;悬停高亮 + 提示气泡(在面板缓存之后绘制,避免被烘进位图)+ 手型光标;点击 `ScrollLaneIntoView` 平滑滚动到该网格(已可见则不动)。③ **最小网格高度 54 → 81 逻辑像素**,并修掉 `m_minLaneHeight` 被二次乘 `m_scale` 的隐患(滚动条判定与实际高度改用同一换算)。验证:新增离屏 `.preflight/round3check` **31/31**(真实 MF4:128 网格、`height=101.25=81×1.25`);**A/B 禁用双击保护 ⇒ 1 项如实失败**;回归 `.preflight/editcheck` 20/20、`.preflight/check` 137/137;`make release`/`make debug` 零警告。 |
| 2026-10-19 | DeepSeek-V4.1-Flash(**本机**) | **`.mat` 流式解码(内存映射 + 逐 chunk 填充)+ MDF4 `##DL/##DZ/##HL` 数据块 + 光标数据点被时间面板遮住改三角(详见 §12 末尾「2026-10-19 第四轮」与 §11.2 第 154–159 条)**。① **`.mat`**:`MatHdf5::Open` 由“整文件 `fread` 进 `vector`”改为 **`MapViewOfFile`**(`MappedBytes` 只读视图,解析代码零改动);新增 `ReadDatasetInfo` + `StreamDataset(ByteSink)`(连续布局 4MB 切片、分块布局逐 chunk inflate)与 `ReadDoubleStream`(SDI `#data#` 字节流 → double,7 字节尾巴处理非 8 对齐 chunk);`ReadNumbers` 改为流式转换;`MatLoader` 先收集信号清单再按(分组, 时间流)合并成 `TableBlock`,时间列按内容去重、数据列 `std::move`,进度改成 `0–0.02 映射 / 0.02–0.98 逐流解码`。实测 `BIG_MAT.mat`(1.29GB / 5×16.14M):**117s → 12.5s**、私有内存 **+1234MB → +740MB**(数据本身 739MB)。② **MDF4**:`CollectDataSegments` 递归展开 `##DL`/`##HL`,识别 `##DT`/`##DV`/`##DZ`;`ReadRecordSegments` 支持跨段拼接与压缩段 inflate;压缩段记录长度改用通道位宽推算。`Problem-MF4.mf4` 由 **4 → 8 个信号**(与 Simulink SDI 一致)。③ **光标标记**:`MarkerCoveredByCursorLabel` + `CoverHiddenMarker`,圆点被底部时间读数/Δt 面板盖住时改为向下三角(原始信号与派生曲线两处)。验证:新增离屏 `.preflight/round4check` **34/34**;**A/B**(禁用 `##DL` ⇒ 2 项失败、禁用 `CoverHiddenMarker` ⇒ 3 项失败);回归 `round3check` 31/31、`editcheck` 20/20、`check` 137/137;`make release`/`make debug` 零警告。 |
## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -3739,6 +3741,38 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
`54 × 1.5625`,且与「是否需要滚动条」的判定(只乘一次)不一致 ⇒ 可能出现“网格溢出却没有滚动条”。
当前值 = **81 逻辑像素**(用户 2026-10-19 要求:原 54 的 1.5 倍,保证最小网格下也看得清跳变)。
新增任何“按最小网格高度推导”的逻辑(滚动条判定、`FitLaneHeight`、内容高度)都必须用同一个换算。
154. ⚠️⚠️ **`.mat` 绝不整文件读入堆内存**(用户 2026-10-19 报告:GB 级 SDI 导出):
`MatHdf5::Open` 现在是 **`CreateFileW` + `CreateFileMappingW` + `MapViewOfFile`**,
`m_data` 是一个“只读字节数组”视图(`MappedBytes`:`size/data/begin/end/[]`),
因此解析代码一行不用改,但**虚拟地址 ≠ 物理内存**,未访问页不驻留。
⚠️ 析构必须 `UnmapViewOfFile` → `CloseHandle(mapping)` → `CloseHandle(file)`(顺序不能反)。
155. ⚠️⚠️ **大数据集解码必须“逐 chunk 解压后直接写进目标 double 列”**:
`MatHdf5::ReadDatasetInfo`(只读元信息)+ `StreamDataset(ByteSink)`(连续布局按 4MB 切片、
分块布局逐 chunk inflate)取代了“整个数据集字节数组 + 再 memcpy 成 double”。
`ReadDoubleStream` 进一步把 SDI 的 `#data#`(uint8 字节流)按 8 字节一组直接写成 double,
并用最多 7 字节的尾巴处理“chunk 边界不与 8 字节对齐”。
⇒ 峰值 = 文件映射 + 最终 double;**不要再恢复 `Dataset.data` 的整块分配**(GB 级下等于翻倍)。
156. ⚠️⚠️ **分块散射必须合并“目标连续的行”**:SDI 的 `#data#` 是 `dims=[N]`、chunk `[N,1]`,
归一化后 rank=2 ⇒ 每行只有 1 字节。若逐行回调(且每行新建 `coordinate` 向量),
实测解压吞吐只有 **11MB/s**(1.29GB 要 117s);把连续行合并成一次 sink 后 **~110MB/s(12.5s)**。
新增 HDF5 数据类型时不要退回“逐行 emit + 每行分配”。
157. ⚠️ **SDI 的时间轴按内容去重**:每条 timeseries 都有自己的时间流,但内容通常完全一致。
加载时若同一分组已有**逐字节相同**的时间列,直接复用同一 `TableBlock`(否则每条信号各存一份:
1.29GB/5 信号的文件多占 ~500MB)。实测私有内存增量 1234MB → **740MB**(数据本身 739MB)。
158. ⚠️⚠️ **MDF4 的数据块不只有 `##DT`**(用户 2026-10-19 报告:SDI 导出的 8 个信号只读出 4 个):
DG 的 data 链接可能是 **`##DL`(数据列表)** / **`##HL`(头部列表)** / **`##DZ`(压缩)**,
记录还可能**跨段**(`##DT` 载荷不是记录长度的整数倍)。正确做法:
`CollectDataSegments`(递归展开 DL/HL,识别 DT/DV/DZ)+ `ReadRecordSegments`
(跨段拼接、压缩段先 inflate、非压缩段仍按 4MB 分块读)。
⚠️ 压缩段不能用「载荷 ÷ 周期数」反推记录长度(那是压缩后长度)⇒ 改用
`max(cg_record_id_len*8, 各通道 bitOffset+bitCount)` 向上对齐到 4 字节。
⚠️ `##SD`(已排序数据)仍未实现,这类数据组会被跳过。
159. ⚠️ **光标数据点圆点被底部「时间读数 / Δt 面板」盖住时要改成向下三角**(用户 2026-10-19 要求):
`MarkerCoveredByCursorLabel`(圆心落入面板或重叠面积 ≥ 半个圆)+ `CoverHiddenMarker`
(置 `offDown`,把锚点放到面板上沿上方 `depth/2 + 1`)。两处调用:原始信号与派生曲线胶囊。
⚠️ 时间读数/Δt 是在取值标注**之后**绘制的,所以圆点必然被盖住;
⚠️ 像素级断言必须钉在**标记锚点附近的小框**:取值胶囊也用信号色作底,
数“面板上方的信号色像素”会把漏画三角误判为通过(A/B 已证)。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
@@ -6489,3 +6523,127 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
滚动条判定改 `laneCount * m_minLaneHeight * m_scale`;其余两处保持 `* m_scale`。
- 实测(本机 125%、窗 1400×900 逻辑、MF4 全部 128 信号各自一格):
`lane height = 101.25px = 81 × 1.25`,`content = 12960px`,滚动条出现且可滚动。
---
## 本轮(2026-10-19 第四轮,本机):`.mat` 流式解码 + MDF4 数据列表 + 光标标记
> 用户三项需求:① `BIG_MAT.mat`(1.29GB,SDI 导出)必须“边解析边填充”,不能再整文件读入;
> 进度要真实反映读取过程并完成相应优化;② `Problem-MF4.mf4` 在 SDI 里能读出 8 个信号,
> 本项目只读出 4 个(未实现的数据块特性);同时确认该文件里 `gVP_WindowLearnSt[0]` /
> `gVR_WinOpenReq[0]` 是否带值枚举文本(**只回答,不改代码**);③ 光标数据点圆点被底部
> 时间读数面板盖住时,改为向下的三角。
> 验证:新增离屏 `.preflight/round4check` **34/34**;A/B 分别 2 / 3 项如实失败;
> 回归 `round3check` 31/31、`editcheck` 20/20、`check` 137/137;`make release`/`make debug` 零警告。
### A. `.mat`:内存映射 + 逐 chunk 直接填充
**旧链路的三份内存**(1.29GB 文件实测私有内存 +1234MB):
1. `MatHdf5::Open` 把整个文件 `fread` 进 `std::vector<uint8_t> m_data`;
2. `DecodeChunked` 把**整个数据集**解压成 `out.data`(未压缩字节,与 double 输出同样大);
3. `MatLoader::ReadStream` 再 `memcpy` 成 `std::vector<double>`;`streams` 缓存所有流,
`AppendSignal` 又整份复制一次;且**每条信号各建一个 `TableBlock`(各带一份时间列)**。
**新链路**:
| 位置 | 做法 |
| --- | --- |
| `MatHdf5::Open` | `CreateFileW` + `CreateFileMappingW` + `MapViewOfFile`;`m_data` 改为 `MappedBytes`(只读视图,支持 `size/data/begin/end/[]`,解析代码零改动) |
| `ReadDatasetInfo` | 只解析元信息(dims/type/layout/chunk/deflate/紧凑字节),不解码数据 |
| `StreamDataset(ByteSink)` | 连续布局按 4MB 切片、分块布局**逐 chunk** inflate 后回调;回调带 `elementOffset` |
| `ReadNumbers` | 流式转换成 double(不再生成整块字节数组) |
| `ReadDoubleStream` | SDI `#data#`(uint8 字节流)按 8 字节一组**直接写** double;7 字节尾巴处理非 8 对齐 chunk |
| `MatLoader` | 先收集信号清单 → 按(分组, 时间流)合并 `TableBlock`;时间列**按内容去重**;数据列 `std::move` |
**性能/内存实测(`BIG_MAT.mat`,5 信号 × 16,142,471 采样)**:
| 指标 | 改造前 | 改造后 |
| --- | --- | --- |
| 加载耗时 | **117.3s** | **12.5s** |
| 私有内存增量 | **+1233.9MB** | **+740.4MB**(最终列数据 738.9MB) |
| 单流解压吞吐 | ~11MB/s | ~110MB/s |
其中 **~10 倍**的吞吐提升来自一处关键修正:SDI 的 `#data#` 是 `dims=[N]`、chunk `[N,1]`,
归一化后 rank=2 ⇒ **每行只有 1 字节**。旧代码每行新建一个 `coordinate` 向量并回调一次
(上千万次小 memcpy + 分配)。现在把“目标连续的行”合并成一次回调(典型整块 1 次)。
另外把 Huffman 解码改成 9 位快查表(对高压缩比数据有效;本例数据基本不可压缩,
大部分 chunk 走“未压缩”直通,因此主要收益来自行合并)。
**进度**:映射阶段 0–0.02(`hasBytes` 与总大小立即就位),逐条样本流解码 0.02–0.98,
收尾 1.0。⚠️ 旧实现把 0–0.55 分给“读文件”,改映射后那一段几乎没有耗时,必须重新分配权重,
否则进度条会“瞬间跳到 55% 再慢慢走”。
**正确性回归**:42MB 的 SDI 样例仍是 **4 分组 / 8 信号**,行数 754 / 801 / 12640 / 523865 全部一致。
### B. MDF4:`##DL` / `##HL` / `##DZ` 数据块
`Problem-MF4.mf4` 的结构(`HD=64`):
```
DG1 data=##DT cycle=94 → 4 个信号(gVP_WindowLearnSt[0/1]、gVP_WinStroke[0/1])
DG2 data=##DL cycle=9396 → 4 个信号(gVP_WinRealPosition[0/1]、gVR_WinOpenReq[0/1])
##DL → ##DT(131040B) + ##DT(504B) (合计 131544 = 9396 × 14B)
DG3 data=0 cycle=0 (空组,一个字符串通道)
```
旧实现要求 `DG` 的 data 链接必须是 `##DT` ⇒ **DG2 整组被跳过** ⇒ 只有 4 个信号。
修法:
- `CollectDataSegments`:递归展开 `##DL`(数据列表)/ `##HL`(头部列表),叶子识别
`##DT`(普通)、`##DV`(数据值)、`##DZ`(压缩);
- `ReadRecordSegments`:跨段拼接记录(`carry` 缓冲)、压缩段先 `InflateZlib`、
非压缩段仍按 4MB 分块读(不整段分配);
- 压缩段的记录长度不能用“载荷 ÷ 周期数”(那是压缩后长度)⇒ 改用
`max(cg_record_id_len*8, 各通道 bitOffset+bitCount)` 向上对齐到 4 字节。
- `##SD`(已排序数据)仍未实现(不在本文件里;遇到会跳过该组)。
实测:**8 个信号**,`gVP_WindowLearnSt[0]` 94 行、`gVR_WinOpenReq[0]` 9396 行,
时间列单调且基本全有限,与 Simulink SDI 显示一致。
### C. 光标数据点被时间读数面板遮住 ⇒ 向下三角
- 新增 `MarkerCoveredByCursorLabel(marker, radius, hit)`:圆心落入面板,或圆的外接矩形与面板
重叠 ≥ 50% 即视为“看不见”。`m_cursorLabelRects` 里已提前登记时间读数与 Δt 胶囊。
- 新增 `CoverHiddenMarker(marker, radius, clipTop)`:把标记置为 `offDown`,锚点放到面板上沿
上方 `kMarkerTriangleDepth/2 + 1`(三角尖端朝下,语义 = 数据点在下方)。
在 `PaintCursor` 的**原始信号**与**派生曲线胶囊**两处调用。
- ⚠️ 像素级验证必须钉在**标记锚点附近的小框**:取值胶囊也用信号色作底,
数“面板上方的信号色像素”会把漏画三角误判为通过(本轮 A/B 实测 255 px 假绿)。
### D. 文件里是否存在枚举文本(只回答,不改代码)
**存在**。两个通道的 `##CC` 都是 `cc_type = 7`(值→文本表),结构为
`links[0..3] = 表名 / 空 / 注释 / 空`,`links[4 + i]` 对应 `cc_val[i]`,最后一个是反向换算 `##CC`。
`gVP_WindowLearnSt[0]`(`valCount=5`,注释 `Enumerated data type: WMRLearnSts`):
| 值 | 文本 |
| --- | --- |
| 0 | `WMRLearnSts_Unlearnd` |
| 1 | `WMRLearnSts_LearningRP0RF0` |
| 2 | `WMRLearnSts_LearningRP1RF0` |
| 3 | `WMRLearnSts_LearndRP1RF0` |
| 4 | `WMRLearnSts_LearndRP1RF1` |
`gVR_WinOpenReq[0]`(`valCount=32`,注释 `Enumerated data type: WinOpenReqAtDrvr`):
| 值 | 文本 | 值 | 文本 |
| --- | --- | --- | --- |
| 0 | `Idle` | 16 | `Open56` |
| 1 | `CloseFull` | 17 | `Open60` |
| 2 | `OpenFull` | 18 | `Open64` |
| 3 | `Open4` | 19 | `Open68` |
| 4 | `Open8` | 20 | `Open72` |
| 5 | `Open12` | 21 | `Open76` |
| 6 | `Open16` | 22 | `Open80` |
| 7 | `Open20` | 23 | `Open84` |
| 8 | `Open24` | 24 | `Open88` |
| 9 | `Open28` | 25 | `Open92` |
| 10 | `Open32` | 26 | `Open96` |
| 11 | `Open36` | 27 | `STOP` |
| 12 | `Open40` | 28 | `VentnReq` |
| 13 | `Open44` | 29 | `open` |
| 14 | `Open48` | 30 | `Cls` |
| 15 | `Open52` | 31 | `Resd` |
(表中文本的前缀 `WinOpenReqAtDrvr_` 已省略;`gVR_WinOpenReq[1]` 的 CC 内容完全相同。)
⚠️ 目前 `ParseChannel` 只认 `cc_type ∈ {0,1}`,所以这两个通道在界面上显示为原始整数;
要显示标签需要按 `cc_type == 7` 读 `links[4..4+valCount-1]` 并与 `cc_val[]` 一一对应,
再随信号传递到界面(本轮按要求**未做代码改动**)。