.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

+27
View File
@@ -1984,6 +1984,33 @@ namespace jdi
void PlaceCursorLabel(gd::RectF& rect, float preferredTop, float textHeight, float clipTop,
float clipBottom);
/**
* @brief 判断数据点圆点是否被「光标时间读数 / Δt 面板」盖住。
*
* 光标的时间读数与 Δt 胶囊绘制在绘图区底部、且在取值标注**之后**绘制,
* 因此靠近底部的数据点圆点会被它们完全遮住(用户 2026-10-19 报告“看不清数据点在哪”)。
* 判定为“盖住”时调用方应把圆点改成**向下的三角**(与数据点在视界下方同一语义)。
*
* @param [IN] marker 数据点标记。
* @param [IN] radius 圆点半径。
* @param [OUT] hit 遮住它的面板矩形(用于把三角贴在面板上沿)。
* @return 被盖住返回 true。
*/
bool MarkerCoveredByCursorLabel(const DataMarker& marker, float radius, gd::RectF& hit) const;
/**
* @brief 若数据点圆点被底部时间读数/Δt 面板盖住,把它改成**向下的三角**。
*
* 语义与“数据点位于视界下方”一致(用户 2026-10-19 要求):圆点既然看不见,
* 就用贴面板上沿的向下三角告知“这里有个数据点、它在下侧”。
* 已经是越界三角的标记不做任何修改。
*
* @param [IN,OUT] marker 标记几何。
* @param [IN] radius 圆点半径。
* @param [IN] clipTop 网格可见区上边界(三角不会越过它)。
*/
void CoverHiddenMarker(DataMarker& marker, float radius, float clipTop) const;
/**
* @brief 统计某信号穿越横向光标幅值的上/下边沿数量,结果带缓存。
*
@@ -2802,6 +2802,51 @@ namespace jdi
return marker;
}
bool PlotView::MarkerCoveredByCursorLabel(const DataMarker& marker, float radius,
gd::RectF& hit) const
{
const gd::RectF circle(marker.x - radius, marker.y - radius, radius * 2.0f, radius * 2.0f);
for (const gd::RectF& rect : m_cursorLabelRects)
{
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
{
continue;
}
// 圆心落在面板内 ⇒ 肯定被遮住(最常见:圆点正好压在时间读数上)。
if (UiPointInRect(rect, marker.x, marker.y))
{
hit = rect;
return true;
}
// 否则看重叠面积:超过圆点外接矩形的一半也算“基本看不见”。
gd::RectF overlap = circle;
overlap.Intersect(rect);
if (overlap.Width > 0.0f && overlap.Height > 0.0f &&
overlap.Width * overlap.Height >= circle.Width * circle.Height * 0.5f)
{
hit = rect;
return true;
}
}
return false;
}
void PlotView::CoverHiddenMarker(DataMarker& marker, float radius, float clipTop) const
{
if (marker.offUp || marker.offDown || marker.offLeft || marker.offRight)
{
return; // 已经是越界三角:保持原语义
}
gd::RectF covering;
if (!MarkerCoveredByCursorLabel(marker, radius, covering))
{
return;
}
marker.offDown = true;
marker.y = std::max(clipTop + kMarkerEdgeInset * m_scale,
covering.Y - (kMarkerTriangleDepth * 0.5f + 1.0f * m_scale));
}
void PlotView::PlaceCursorLabel(gd::RectF& rect, float preferredTop, float textHeight,
float clipTop, float clipBottom)
{
@@ -4989,6 +4989,10 @@ namespace jdi
{
marker.y = labelRect.Y + labelRect.Height * 0.5f;
}
// ⚠️ 数据点圆点被底部的「时间读数 / Δt 面板」盖住时,圆点根本看不见
//(时间读数在取值标注之后绘制、直接盖在上面)。此时改成**向下的三角**。
CoverHiddenMarker(marker, radius, clipTop);
PaintDataMarker(g, marker, radius, signalColor, m_theme->cardBg);
const UiInk ink = UiContrastInk(signalColor);
@@ -5113,6 +5117,8 @@ namespace jdi
{
centerMarker.y = centerRect.Y + centerRect.Height * 0.5f;
}
// 同原始信号:圆点被底部时间读数面板盖住时改为向下三角。
CoverHiddenMarker(centerMarker, radius, clipTop);
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
// 颜色样式与原始信号一致(同底色/同描边/同文字配色)。
const UiInk centerInk = UiContrastInk(centerColor);
+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[]` 一一对应,
再随信号传递到界面(本轮按要求**未做代码改动**)。
+37
View File
@@ -0,0 +1,37 @@
#ifndef __DEFLATE_HPP__
#define __DEFLATE_HPP__
#include <cstddef>
#include <cstdint>
#include <vector>
namespace jdi
{
/**
* @brief 解压一个 zlib(RFC 1950)流(内含 DEFLATE / RFC 1951 数据)。
*
* 本项目**不链接 zlib**(`-lz` 会让 Release 产物多出约 46KB),只用这一份紧凑实现:
* * HDF5 分块数据集的 deflate 过滤器(MATLAB `.mat`);
* * MDF4 的 `##DZ` 压缩数据块。
*
* ⚠️ 只接受「无预设字典」的 deflate 流;末尾的 Adler32 校验值不做验证(只跳过)。
*
* @param [IN] source 压缩数据起始指针(含 2 字节 zlib 头)。
* @param [IN] sourceSize 压缩数据长度(含头与 4 字节尾)。
* @param [OUT] out 解压结果(函数内部先 `clear`,再追加写入)。
* @param [IN] expectedSize 期望的解压长度;>0 时预分配容量,避免反复扩容。
* @return 成功返回 true。
*/
bool InflateZlib(const std::uint8_t* source, std::size_t sourceSize,
std::vector<std::uint8_t>& out, std::size_t expectedSize = 0);
/**
* @brief 判断一段数据是否是 zlib 流(2 字节头:CM=8、FCHECK 正确、无预设字典)。
* @param [IN] source 数据起始指针。
* @param [IN] sourceSize 数据长度。
* @return 很像 zlib 流返回 true。
*/
bool LooksLikeZlib(const std::uint8_t* source, std::size_t sourceSize);
} // namespace jdi
#endif
+85 -7
View File
@@ -75,17 +75,37 @@ namespace jdi
std::uint64_t chunkAddress = 0; /**< 分块数据集的 B-tree 地址。 */
std::vector<std::size_t> chunkDims;
bool deflate = false; /**< 是否使用了 deflate(zlib)过滤器。 */
std::uint64_t dataAddress = 0; /**< 连续布局的数据起始地址(已加基地址)。 */
std::uint64_t dataBytes = 0; /**< 连续布局的数据字节数。 */
/** 紧凑布局的字节(在布局消息里,数据量很小)。 */
std::vector<std::uint8_t> compact;
bool ok = false;
/** @brief 元素总数(各维长度相乘)。 */
std::uint64_t ElementCount() const
{
std::uint64_t count = 1;
for (const std::size_t dim : dims)
{
count *= dim;
}
return dims.empty() ? 0 : count;
}
};
/** @brief 读文件进度回调:`已读字节 / 总字节`。返回 false 表示请求取消。 */
using OpenProgress = std::function<bool(std::uint64_t bytesRead, std::uint64_t totalBytes)>;
/**
* @brief 打开文件(一次性读入内存;`.mat` 通常几十 MB,可接受)。
* @brief 打开文件(**内存映射**,不把整个文件读进堆内存)。
*
* ⚠️ 早期版本把整个文件一次读入 `std::vector`(注释里假设“`.mat` 通常几十 MB”),
* 但 SDI 导出的录波文件可能是 **GB 级**:整文件读入 + 分块解码 + 再拷成 double 会同时
* 驻留“文件 + 未压缩字节 + double”三份内存。现改为:
* * 映射文件(只占虚拟地址空间,按页面需求驻留);
* * 分块数据集逐 chunk 解压并**直接写入**自持有的 double 列(见 `ReadDoubleStream`);
* * 进度回调在映射完成后立即报告一次(字节已全部可访问),后续解码阶段由 `MatLoader` 驱动。
*
* ⚠️ 按块读取并回调 `progress`:加载遮罩层的"已加载大小 / 总大小 / 百分比"
* 全靠它驱动。省略回调时行为不变。
* @param [IN] path 文件路径。
* @param [OUT] error 失败原因。
* @param [IN] progress 读取进度回调(可为空)。
@@ -95,6 +115,12 @@ namespace jdi
const OpenProgress& progress = OpenProgress());
void Close();
/** @brief 析构时释放映射(否则栈上对象的映射会留到进程退出)。 */
~MatHdf5() { Close(); }
/** @brief 文件字节数(映射长度)。 */
std::uint64_t FileBytes() const { return static_cast<std::uint64_t>(m_data.size()); }
/** @brief 根分组地址。 */
std::uint64_t Root() const { return m_root; }
@@ -110,15 +136,45 @@ namespace jdi
/** @brief 读取数据集定义(含解码后的原始数据)。 */
bool ReadDataset(std::uint64_t address, Dataset& out) const;
/** @brief 只读取数据集定义(不解码数据;大数据用 `StreamDataset`)。 */
bool ReadDatasetInfo(std::uint64_t address, Dataset& out) const;
/**
* @brief 字节流回调:把数据集内容按块吐出(**不生成整块缓冲**)。
* @param [IN] bytes 块起始指针(仅在回调期间有效)。
* @param [IN] byteCount 块字节数。
* @param [IN] elementOffset 该块在数据集中的起始**元素**下标(行优先)。
* @return 返回 false 中止遍历。
*/
using ByteSink = std::function<bool(const std::uint8_t* bytes, std::size_t byteCount,
std::uint64_t elementOffset)>;
/**
* @brief 流式遍历数据集字节(连续布局按 4MB 切片;分块布局逐个 chunk 解压)。
* @param [IN] dataset `ReadDatasetInfo` 得到的信息。
* @param [IN] sink 字节回调。
* @return 成功返回 true。
*/
bool StreamDataset(const Dataset& dataset, const ByteSink& sink) const;
/** @brief 读取字符串属性(如 `MATLAB_class`、`MATLAB_fields`)。 */
std::string AttributeString(std::uint64_t address, const char* name) const;
/** @brief 读取 `char` 数据集为宽字符串(MATLAB 的 char 是 uint16)。 */
bool ReadText(std::uint64_t address, std::wstring& out) const;
/** @brief 读取数值数据集为 double 序列。 */
/** @brief 读取数值数据集为 double 序列(流式转换,不生成整块字节缓冲)。 */
bool ReadNumbers(std::uint64_t address, std::vector<double>& out) const;
/**
* @brief 读取 Simulink SDI 的字节流数据集(`#data#`)为 double 序列。
*
* SDI 的 `#data#` 是 `uint8` 数据集,长度 = 采样数 × 8:按 8 字节一组重新解释为 double。
* 实现上逐 chunk 解压并直接写入输出缓冲(不产生整块字节数组,也不二次拷贝),
* 因此 GB 级文件也不会出现“文件 + 未压缩字节 + double”三份内存叠加。
*/
bool ReadDoubleStream(std::uint64_t address, std::vector<double>& out) const;
/** @brief 读取引用数据集(HDF5 对象引用 = 对象头地址,相对基地址)。 */
bool ReadReferences(std::uint64_t address, std::vector<std::uint64_t>& out) const;
@@ -139,14 +195,36 @@ namespace jdi
void ReadMessages(std::uint64_t address, std::vector<Message>& messages) const;
bool ParseType(const std::uint8_t* data, std::size_t size, std::size_t offset, Type& out,
std::size_t& consumed) const;
bool DecodeChunked(const Dataset& dataset, std::size_t elementSize,
std::vector<std::uint8_t>& out) const;
void WalkBtree(std::uint64_t node, std::uint64_t heapData,
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
std::vector<std::uint8_t> m_data;
/**
* @brief 内存映射的文件视图(代替“整个文件读进 `vector`”)。
*
* ⚠️ 行为与 `std::vector<std::uint8_t>` 的**只读子集**一致(`size/data/begin/end/[]`),
* 因此所有“按偏移取字节”的解析代码一行都不用改;但**虚拟地址不等于物理内存**:
* 只有真正被解析到的页才驻留,GB 级文件不会再先吃掉一整个文件大小的私有内存。
*/
struct MappedBytes
{
const std::uint8_t* pointer = nullptr;
std::size_t length = 0;
const std::uint8_t* data() const { return pointer; }
std::size_t size() const { return length; }
const std::uint8_t& operator[](std::size_t index) const { return pointer[index]; }
const std::uint8_t* begin() const { return pointer; }
const std::uint8_t* end() const { return pointer + length; }
void clear()
{
pointer = nullptr;
length = 0;
}
};
MappedBytes m_data; /**< 文件映射视图(只读;语义同字节数组)。 */
void* m_fileHandle = nullptr; /**< `HANDLE`(避免头文件引入 windows.h)。 */
void* m_mapHandle = nullptr;
std::uint64_t m_base = 0;
std::uint64_t m_root = 0;
int m_offsetSize = 8;
+427
View File
@@ -0,0 +1,427 @@
// 内置 inflate(RFC 1950 / 1951)—— 供 HDF5 分块与 MDF4 `##DZ` 共用。
//
// ⚠️ **不能用 zlib(`-lz`)**:链接 libz.a 会让 Release 产物多出约 46KB,
// 而这里只需要「解压」这一个功能,自带的紧凑实现只要几 KB。
#include "Deflate.hpp"
namespace jdi
{
namespace
{
/** @brief 位流读取器(LSB 优先,与 DEFLATE 一致)。 */
struct BitReader
{
const std::uint8_t* data = nullptr;
std::size_t size = 0;
std::size_t position = 0;
std::uint32_t buffer = 0;
int bits = 0;
bool error = false;
bool Fill(int count)
{
while (bits < count)
{
if (position >= size)
{
error = true;
return false;
}
buffer |= static_cast<std::uint32_t>(data[position++]) << bits;
bits += 8;
}
return true;
}
std::uint32_t Read(int count)
{
if (count <= 0)
{
return 0;
}
if (!Fill(count))
{
return 0;
}
const std::uint32_t value = buffer & ((1u << count) - 1u);
buffer >>= count;
bits -= count;
return value;
}
void Align()
{
buffer = 0;
bits = 0;
}
};
/** @brief 规范 Huffman 表(只存计数与按长度排好序的符号 + 快查表)。 */
constexpr int kFastBits = 9;
constexpr int kFastSize = 1 << kFastBits;
struct HuffmanTable
{
std::uint16_t count[16] = {};
std::uint16_t symbol[320] = {};
/**
* @brief 前缀码快查表(9 位):`fastLength[peek] != 0` 时可直接得到符号。
*
* ⚠️ 逐位解码(每读 1 bit 就查一次表)是解压的绝对瓶颈:GB 级 `.mat` 实测
* 只有 ~11MB/s,而 9 位查表一次就能吃掉绝大多数符号(典型单字节字面量长度 8~9)。
*/
std::uint16_t fastSymbol[kFastSize] = {};
std::uint8_t fastLength[kFastSize] = {};
};
bool BuildHuffman(HuffmanTable& table, const std::uint8_t* lengths, int total)
{
for (int index = 0; index < 16; ++index)
{
table.count[index] = 0;
}
for (int index = 0; index < total; ++index)
{
++table.count[lengths[index]];
}
if (table.count[0] == total)
{
return true; // 全部长度为 0:该表没有任何码(动态块里可能是距离表)
}
int left = 1;
for (int length = 1; length < 16; ++length)
{
left <<= 1;
left -= table.count[length];
if (left < 0)
{
return false; // 超订(over-subscribed)
}
}
std::uint16_t offsets[16] = {};
offsets[1] = 0;
for (int length = 1; length < 15; ++length)
{
offsets[length + 1] = static_cast<std::uint16_t>(offsets[length] + table.count[length]);
}
for (int index = 0; index < total; ++index)
{
if (lengths[index] != 0)
{
table.symbol[offsets[lengths[index]]++] = static_cast<std::uint16_t>(index);
}
}
// 建 9 位快查表:按规范码给符号分配 code,再按 DEFLATE 的 LSB-first 位序反转。
for (int index = 0; index < kFastSize; ++index)
{
table.fastLength[index] = 0;
}
int code = 0;
int symbolIndex = 0;
for (int length = 1; length < 16; ++length)
{
for (int i = 0; i < table.count[length]; ++i)
{
const int symbol = table.symbol[symbolIndex + i];
if (length <= kFastBits)
{
int reversed = 0;
for (int bit = 0; bit < length; ++bit)
{
reversed |= ((code >> bit) & 1) << (length - 1 - bit);
}
const int step = 1 << length;
for (int fill = reversed; fill < kFastSize; fill += step)
{
table.fastSymbol[fill] = static_cast<std::uint16_t>(symbol);
table.fastLength[fill] = static_cast<std::uint8_t>(length);
}
}
++code;
}
code <<= 1;
symbolIndex += table.count[length];
}
return true;
}
int DecodeSymbol(BitReader& reader, const HuffmanTable& table)
{
// 快路径:一次填到 9 位,查表命中则直接返回(绝大多数文字/长度符号)。
if (reader.bits >= kFastBits || reader.Fill(kFastBits))
{
const int peek = static_cast<int>(reader.buffer & (kFastSize - 1));
const int length = table.fastLength[peek];
if (length > 0)
{
reader.buffer >>= length;
reader.bits -= length;
return table.fastSymbol[peek];
}
}
else
{
// 结尾处不足 9 位:交给慢路径逐位解码(它才是"数据真的结束"的判据)。
reader.error = false;
}
int code = 0;
int first = 0;
int index = 0;
for (int length = 1; length < 16; ++length)
{
code |= static_cast<int>(reader.Read(1));
if (reader.error)
{
return -1;
}
const int count = table.count[length];
if (code - first < count)
{
return table.symbol[index + (code - first)];
}
index += count;
first = (first + count) << 1;
code <<= 1;
}
return -1;
}
const std::uint16_t kLengthBase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13,
15, 17, 19, 23, 27, 31, 35, 43, 51, 59,
67, 83, 99, 115, 131, 163, 195, 227, 258};
const std::uint8_t kLengthExtra[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
const std::uint16_t kDistanceBase[30] = {1, 2, 3, 4, 5, 7, 9, 13,
17, 25, 33, 49, 65, 97, 129, 193,
257, 385, 513, 769, 1025, 1537, 2049, 3073,
4097, 6145, 8193, 12289, 16385, 24577};
const std::uint8_t kDistanceExtra[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6,
6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
} // namespace
bool LooksLikeZlib(const std::uint8_t* source, std::size_t sourceSize)
{
if (source == nullptr || sourceSize < 6)
{
return false;
}
const std::uint8_t cmf = source[0];
const std::uint8_t flg = source[1];
return (cmf & 0x0F) == 8 && ((static_cast<unsigned>(cmf) * 256u + flg) % 31u) == 0 &&
(flg & 0x20) == 0;
}
bool InflateZlib(const std::uint8_t* source, std::size_t sourceSize,
std::vector<std::uint8_t>& out, std::size_t expectedSize)
{
if (!LooksLikeZlib(source, sourceSize))
{
return false;
}
BitReader reader;
reader.data = source + 2;
reader.size = sourceSize - 6; // 末尾 4 字节是 Adler32
out.clear();
if (expectedSize > 0)
{
out.reserve(expectedSize);
}
HuffmanTable fixedLiterals;
HuffmanTable fixedDistances;
bool fixedReady = false;
for (int block = 0; block < 100000; ++block)
{
const std::uint32_t final = reader.Read(1);
const std::uint32_t type = reader.Read(2);
if (reader.error)
{
return false;
}
if (type == 0)
{
// 存储块(未压缩):对齐后 4 字节长度 + 反码 + 原始字节。
reader.Align();
if (reader.position + 4 > reader.size)
{
return false;
}
const std::uint16_t length = static_cast<std::uint16_t>(
reader.data[reader.position] | (reader.data[reader.position + 1] << 8));
reader.position += 4;
if (reader.position + length > reader.size)
{
return false;
}
out.insert(out.end(), reader.data + reader.position,
reader.data + reader.position + length);
reader.position += length;
}
else if (type == 1 || type == 2)
{
HuffmanTable literals;
HuffmanTable distances;
if (type == 1)
{
if (!fixedReady)
{
std::uint8_t lengths[288];
for (int index = 0; index < 144; ++index)
{
lengths[index] = 8;
}
for (int index = 144; index < 256; ++index)
{
lengths[index] = 9;
}
for (int index = 256; index < 280; ++index)
{
lengths[index] = 7;
}
for (int index = 280; index < 288; ++index)
{
lengths[index] = 8;
}
if (!BuildHuffman(fixedLiterals, lengths, 288))
{
return false;
}
std::uint8_t distanceLengths[30];
for (int index = 0; index < 30; ++index)
{
distanceLengths[index] = 5;
}
if (!BuildHuffman(fixedDistances, distanceLengths, 30))
{
return false;
}
fixedReady = true;
}
literals = fixedLiterals;
distances = fixedDistances;
}
else
{
const int literalCount = static_cast<int>(reader.Read(5)) + 257;
const int distanceCount = static_cast<int>(reader.Read(5)) + 1;
const int codeCount = static_cast<int>(reader.Read(4)) + 4;
if (reader.error || literalCount > 286 || distanceCount > 30)
{
return false;
}
static const std::uint8_t kOrder[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5,
11, 4, 12, 3, 13, 2, 14, 1, 15};
std::uint8_t codeLengths[19] = {};
for (int index = 0; index < codeCount; ++index)
{
codeLengths[kOrder[index]] = static_cast<std::uint8_t>(reader.Read(3));
}
HuffmanTable codeTable;
if (reader.error || !BuildHuffman(codeTable, codeLengths, 19))
{
return false;
}
std::uint8_t lengths[288 + 30] = {};
int produced = 0;
while (produced < literalCount + distanceCount)
{
const int symbol = DecodeSymbol(reader, codeTable);
if (symbol < 0)
{
return false;
}
if (symbol < 16)
{
lengths[produced++] = static_cast<std::uint8_t>(symbol);
continue;
}
int repeat = 0;
std::uint8_t value = 0;
if (symbol == 16)
{
if (produced == 0)
{
return false;
}
value = lengths[produced - 1];
repeat = 3 + static_cast<int>(reader.Read(2));
}
else if (symbol == 17)
{
repeat = 3 + static_cast<int>(reader.Read(3));
}
else
{
repeat = 11 + static_cast<int>(reader.Read(7));
}
if (reader.error || produced + repeat > literalCount + distanceCount)
{
return false;
}
for (int index = 0; index < repeat; ++index)
{
lengths[produced++] = value;
}
}
if (!BuildHuffman(literals, lengths, literalCount) ||
!BuildHuffman(distances, lengths + literalCount, distanceCount))
{
return false;
}
}
for (int symbolIndex = 0; symbolIndex < 100000000; ++symbolIndex)
{
const int symbol = DecodeSymbol(reader, literals);
if (symbol < 0)
{
return false;
}
if (symbol < 256)
{
out.push_back(static_cast<std::uint8_t>(symbol));
continue;
}
if (symbol == 256)
{
break; // 块结束
}
const int lengthCode = symbol - 257;
if (lengthCode >= 29)
{
return false;
}
const std::size_t length =
kLengthBase[lengthCode] + reader.Read(kLengthExtra[lengthCode]);
const int distanceCode = DecodeSymbol(reader, distances);
if (distanceCode < 0 || distanceCode >= 30 || reader.error)
{
return false;
}
const std::size_t distance =
kDistanceBase[distanceCode] + reader.Read(kDistanceExtra[distanceCode]);
if (reader.error || distance == 0 || distance > out.size())
{
return false;
}
const std::size_t start = out.size() - distance;
for (std::size_t index = 0; index < length; ++index)
{
out.push_back(out[start + index]);
}
}
}
else
{
return false; // 类型 3 非法
}
if (final != 0)
{
return true;
}
}
return false;
}
} // namespace jdi
+444 -587
View File
File diff suppressed because it is too large. Load diff
+196 -106
View File
@@ -8,6 +8,7 @@
#include "MatH5.hpp"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <map>
#include <set>
@@ -66,21 +67,11 @@ namespace jdi
{
return false;
}
MatHdf5::Dataset dataset;
if (!file.ReadDataset(data, dataset) || !dataset.ok)
{
return false;
}
// ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8),
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double(`#sampleSize#` 可印证)。
if (dataset.data.size() % sizeof(double) != 0)
{
return false;
}
const std::size_t count = dataset.data.size() / sizeof(double);
out.resize(count);
std::memcpy(out.data(), dataset.data.data(), count * sizeof(double));
return true;
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double。
// `ReadDoubleStream` 会逐 chunk 解压并**直接写入** `out`:
// 不生成“整个数据集的未压缩字节”,也不做第二次拷贝(GB 级文件的关键)。
return file.ReadDoubleStream(data, out) && !out.empty();
}
/** @brief 解析 `#refs#` 里的一个元素(信号 / 子数据集)。 */
@@ -260,53 +251,19 @@ namespace jdi
stack.push_back(std::move(pending));
}
std::map<std::wstring, std::size_t> groupIndex;
std::vector<std::vector<double>> streams; // 流编号 → 样本
std::vector<bool> streamCached;
// 进度:`#sigstream#` 的总流数,用来把"解码样本流"这段映射成阶段内 0-1。
std::size_t streamTotal = 0;
for (const auto& member : file.Members(sigstream))
// ⚠️ **先收集信号清单、再解码**(用户 2026-10-19 要求:边解析边填充):
// * 旧实现把**每条**信号的时间列和时间流各拷一份(`streams` 缓存 + `AppendSignal`
// 里的整份复制)⇒ GB 级文件会出现“文件 + 解压字节 + 每条信号两份 double”叠加;
// * 现在按「分组 + 时间流」合并成一个 `TableBlock`:时间列只存一份、信号列直接
// `std::move` 进数据模型,且每条数据流解码完立刻进入最终缓冲。
struct PendingSignal
{
if (!member.first.empty() && member.first[0] != '#')
{
++streamTotal;
}
}
std::size_t streamDone = 0;
const auto streamFor = [&](int index) -> const std::vector<double>*
{
if (index < 0)
{
return nullptr;
}
if (streams.size() <= static_cast<std::size_t>(index))
{
streams.resize(static_cast<std::size_t>(index) + 1);
streamCached.resize(static_cast<std::size_t>(index) + 1, false);
}
if (!streamCached[static_cast<std::size_t>(index)])
{
const std::uint64_t streamObject =
file.Member(sigstream, std::to_string(index));
if (streamObject == 0 || !ReadStream(file, streamObject, streams[static_cast<std::size_t>(index)]))
{
streams[static_cast<std::size_t>(index)].clear();
}
streamCached[static_cast<std::size_t>(index)] = true;
// ⚠️ 必须在这里回报进度:读一个 42MB 的 `.mat` 时,"解码样本流"才是真正的
// 耗时大头(每个流都要解压 + 拷贝);不报进度的话界面会停在原地等
//(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新)。
++streamDone;
reporter.Report(streamTotal > 0
? static_cast<double>(streamDone) /
static_cast<double>(streamTotal)
: 1.0);
}
return streams[static_cast<std::size_t>(index)].empty()
? nullptr
: &streams[static_cast<std::size_t>(index)];
std::wstring groupName;
std::wstring signalName;
int timeStream = -1;
int dataStream = -1;
};
std::vector<PendingSignal> signals;
int unnamedSignals = 0;
while (!stack.empty())
{
@@ -335,68 +292,201 @@ namespace jdi
}
continue;
}
if (!element.isSignal)
if (!element.isSignal || element.dataStream < 0)
{
continue;
}
const std::vector<double>* time = streamFor(element.timeStream);
const std::vector<double>* values = streamFor(element.dataStream);
if (values == nullptr)
{
continue;
}
std::vector<double> timeCopy;
if (time != nullptr && time->size() == values->size())
{
timeCopy = *time;
}
else
{
// 时间流缺失/长度不符:退化为采样序号(秒),保证信号仍可显示。
timeCopy.resize(values->size());
for (std::size_t index = 0; index < values->size(); ++index)
{
timeCopy[index] = static_cast<double>(index);
}
}
std::wstring groupName;
PendingSignal signal;
for (const std::wstring& piece : pending.path)
{
if (!groupName.empty())
if (!signal.groupName.empty())
{
groupName += L"-";
signal.groupName += L"-";
}
groupName += piece;
signal.groupName += piece;
}
if (groupName.empty())
if (signal.groupName.empty())
{
groupName = stem;
signal.groupName = stem;
}
std::wstring signalName = element.name;
if (signalName.empty())
signal.signalName = element.name;
if (signal.signalName.empty())
{
signalName = L"信号" + std::to_wstring(++unnamedSignals);
signal.signalName = L"信号" + std::to_wstring(++unnamedSignals);
}
signal.timeStream = element.timeStream;
signal.dataStream = element.dataStream;
signals.push_back(std::move(signal));
}
std::size_t index = 0;
const auto existing = groupIndex.find(groupName);
// 2) 解码:每个(分组,时间流)一个时间轴块;每条数据流只解码一次并直接进列。
std::map<std::wstring, std::size_t> groupIndex;
std::map<std::pair<std::wstring, int>, std::size_t> blockIndex;
std::size_t decoded = 0;
const auto reportStream = [&]()
{
++decoded;
const double fraction = (signals.empty())
? 1.0
: static_cast<double>(decoded) /
static_cast<double>(signals.size());
return reporter.Report(std::min(1.0, fraction));
};
for (PendingSignal& signal : signals)
{
// 分组(每个分组名对应一个 `LoadedSourceGroup`)。
std::size_t groupSlot = 0;
const auto existing = groupIndex.find(signal.groupName);
if (existing == groupIndex.end())
{
LoadedSourceGroup group;
group.name = groupName;
group.source.sourceName = groupName;
group.name = signal.groupName;
group.source.sourceName = signal.groupName;
groups.push_back(std::move(group));
index = groups.size() - 1;
groupIndex.emplace(groupName, index);
groupSlot = groups.size() - 1;
groupIndex.emplace(signal.groupName, groupSlot);
}
else
{
index = existing->second;
groupSlot = existing->second;
}
LoadedSource& source = groups[groupSlot].source;
// 数据列:直接解码到最终 vector(不再经过 `Dataset.data` 的整块字节)。
std::vector<double> values;
const std::uint64_t dataObject = file.Member(sigstream, std::to_string(signal.dataStream));
if (dataObject == 0 || !ReadStream(file, dataObject, values) || values.empty())
{
if (!reportStream())
{
return false;
}
continue;
}
// 找(或建)该信号所属的时间轴块。
const std::pair<std::wstring, int> key{signal.groupName, signal.timeStream};
std::size_t blockSlot = 0;
const auto blockFound = blockIndex.find(key);
if (blockFound == blockIndex.end())
{
TableBlock block;
block.timeHeader = L"time";
std::vector<double> time;
bool haveTime = false;
if (signal.timeStream >= 0)
{
const std::uint64_t timeObject =
file.Member(sigstream, std::to_string(signal.timeStream));
haveTime = timeObject != 0 && ReadStream(file, timeObject, time) &&
!time.empty();
}
if (!haveTime)
{
// 时间流缺失:退化为采样序号(秒),保证信号仍可显示(长度在下面裁剪)。
time.resize(values.size());
for (std::size_t index = 0; index < values.size(); ++index)
{
time[index] = static_cast<double>(index);
}
}
// ⚠️ **时间轴按内容去重**:SDI 里每条 timeseries 都有自己的时间流,
// 但内容通常完全一致。若同一分组里已经有一模一样的时间列,就直接复用它
//(否则每条信号各存一份时间轴:1.29GB / 5 信号的文件会多占 ~500MB)。
std::size_t reused = SIZE_MAX;
for (std::size_t index = 0; index < source.blocks.size(); ++index)
{
const std::vector<double>& existingTime = source.blocks[index].table.columns[0];
if (existingTime.size() != time.size())
{
continue;
}
if (time.empty() ||
std::memcmp(existingTime.data(), time.data(),
time.size() * sizeof(double)) == 0)
{
reused = index;
break;
}
}
if (reused != SIZE_MAX)
{
blockIndex.emplace(key, reused);
blockSlot = reused;
}
else
{
block.table.headers.push_back(block.timeHeader);
block.table.columns.push_back(std::move(time));
source.blocks.push_back(std::move(block));
blockSlot = source.blocks.size() - 1;
blockIndex.emplace(key, blockSlot);
}
}
else
{
blockSlot = blockFound->second;
}
TableBlock& block = source.blocks[blockSlot];
// 时间列长度与信号不一致(损坏 / 异构)⇒ 单独成块,用采样序号当时间,
// 保持“每条信号都能显示”的既有语义,而不是把数据截掉或错位。
if (block.table.columns[0].size() != values.size())
{
TableBlock own;
own.timeHeader = L"time";
std::vector<double> time(values.size());
for (std::size_t index = 0; index < values.size(); ++index)
{
time[index] = static_cast<double>(index);
}
own.table.headers.push_back(own.timeHeader);
own.table.columns.push_back(std::move(time));
own.table.headers.push_back(signal.signalName);
own.table.columns.push_back(std::move(values));
source.blocks.push_back(std::move(own));
if (!reportStream())
{
return false;
}
continue;
}
block.table.headers.push_back(signal.signalName);
block.table.columns.push_back(std::move(values));
if (!reportStream())
{
return false;
}
}
// 3) 统一行数:同一块内取各列最小长度(时间流与数据流长度不一致时按最小裁剪)。
for (LoadedSourceGroup& group : groups)
{
for (TableBlock& block : group.source.blocks)
{
std::size_t rows = SIZE_MAX;
for (const std::vector<double>& column : block.table.columns)
{
rows = std::min(rows, column.size());
}
if (rows == SIZE_MAX)
{
rows = 0;
}
for (std::vector<double>& column : block.table.columns)
{
if (column.size() > rows)
{
column.resize(rows);
}
}
block.table.rowCount = rows;
}
AppendSignal(groups[index].source, signalName, std::move(timeCopy),
std::vector<double>(*values));
}
if (groups.empty())
@@ -475,17 +565,17 @@ namespace jdi
MatHdf5 file;
// ------------------------------------------------------------------
// 进度拆成三段(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新,
// 界面只是原地等待):
// 0.00 – 0.55 读文件字节(遮罩层显示"已加载 / 总大小")
// 0.55 – 0.98 解码样本流(每个流解码后推进,含解压与拷贝)
// 进度拆成三段:
// 0.00 – 0.02 映射文件(内存映射,几乎瞬时;遮罩层仍能看到“已加载 = 总大小”)
// 0.02 – 0.98 逐条解码样本流(每个流解压后直接填进数据模型,含解压开销)
// 0.98 – 1.00 收尾
// 读文件阶段把 `hasBytes` 置真;之后字节数已满,只推进百分比。
// ⚠️ 旧实现把 0–0.55 全给了“读文件字节”(因为旧实现真的整文件读入);
// 现在改成内存映射后那一段几乎没有耗时,若不调整权重,进度条会“瞬间跳到 55% 再慢慢走”。
// ------------------------------------------------------------------
ProgressReporter readReporter;
readReporter.sink = &progress;
readReporter.low = 0.0;
readReporter.high = 0.55;
readReporter.high = 0.02;
const MatHdf5::OpenProgress openProgress =
[&readReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
{
@@ -501,7 +591,7 @@ namespace jdi
}
ProgressReporter parseReporter;
parseReporter.sink = &progress;
parseReporter.low = 0.55;
parseReporter.low = 0.02;
parseReporter.high = 0.98;
parseReporter.read = readReporter.total;
parseReporter.total = 0; // 字节已读完:本阶段只报百分比
+252 -95
View File
@@ -1,5 +1,6 @@
#include "Mf4Loader.hpp"
#include "Deflate.hpp"
#include "Utf8Util.hpp"
#include <windows.h>
@@ -8,6 +9,7 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <functional>
#include <limits>
#include <string>
#include <vector>
@@ -499,6 +501,171 @@ namespace jdi
}
}
/**
* @brief 一段原始记录数据(MDF4 的 `##DT` / `##DZ` / `##DV` 块的数据区)。
*
* MDF4 允许把记录拆到多段里:
* * `##DT` 普通数据块(记录连续存放);
* * `##DZ` 压缩数据块(zlib 流,解压后是普通记录);
* * `##DV` 数据值块(`##DL` 列表指向的原始值);
* * `##DL` 数据列表(链接到多个 `##DT` / `##DZ`);
* * `##HL` 头部列表(链接到多个数据块,含压缩的)。
*/
struct DataSegment
{
std::uint64_t offset = 0; /**< 数据区起始绝对偏移。 */
std::uint64_t bytes = 0; /**< 数据区字节数(压缩段为压缩后长度)。 */
bool compressed = false; /**< 是否 need inflate。 */
};
/**
* @brief 递归展开数据块,收集所有数据段。
*
* ⚠️ 用户 2026-10-19 报告:Simulink SDI 导出的 MF4 把记录写在 `##DL`(数据列表)
* 里,而旧实现只认 `##DT` ⇒ 整个数据组被跳过(8 个信号只能读出 4 个)。
*/
bool CollectDataSegments(const MdfFile& file, std::uint64_t block,
std::vector<DataSegment>& out, int depth)
{
if (block == 0 || depth > 4 || !file.IsBlock(block, ""))
{
return false;
}
char id[5] = {};
file.BlockId(block, id);
const std::uint64_t data = file.DataOffset(block);
const std::uint64_t payload = file.BlockLength(block) - (data - block);
if (payload == 0)
{
return false;
}
if (std::memcmp(id, "##DT", 4) == 0 || std::memcmp(id, "##DV", 4) == 0)
{
out.push_back(DataSegment{data, payload, false});
return true;
}
if (std::memcmp(id, "##DZ", 4) == 0)
{
out.push_back(DataSegment{data, payload, true});
return true;
}
if (std::memcmp(id, "##DL", 4) == 0 || std::memcmp(id, "##HL", 4) == 0)
{
const std::uint64_t links = file.LinkCount(block);
bool any = false;
for (std::uint64_t index = 0; index < links && index < 1000000; ++index)
{
any = CollectDataSegments(file, file.Link(block, index), out, depth + 1) || any;
}
return any;
}
// `##SD`(已排序数据)、`##SI` 等不在支持范围内:跳过该数据组。
return false;
}
/**
* @brief 依次从若干数据段中读取连续记录,并对每条完整记录调用 `visit`。
*
* * 记录可能**跨段**(`##DT` 载荷不是记录长度的整数倍时)⇒ 用 `carry` 拼接;
* * 压缩段先用内置 inflate 展开(`##DZ`),再按普通字节流消费。
*
* @return 成功读取到至少 `cycles` 条记录返回 true。
*/
bool ReadRecordSegments(
const MdfFile& file, const std::vector<DataSegment>& segments, std::uint64_t cycles,
std::size_t recordBytes,
const std::function<void(const unsigned char* record, std::uint64_t row)>& visit)
{
if (recordBytes == 0 || cycles == 0)
{
return false;
}
std::vector<unsigned char> carry(recordBytes);
std::size_t carryBytes = 0;
std::uint64_t row = 0;
std::vector<std::uint8_t> inflated;
for (const DataSegment& segment : segments)
{
if (row >= cycles)
{
break;
}
const unsigned char* data = nullptr;
std::size_t bytes = 0;
if (segment.compressed)
{
std::vector<std::uint8_t> compressed(static_cast<std::size_t>(segment.bytes));
if (!file.ReadAt(segment.offset, compressed.data(), compressed.size()))
{
return false;
}
if (!InflateZlib(compressed.data(), compressed.size(), inflated))
{
return false;
}
data = inflated.data();
bytes = inflated.size();
}
else
{
if (segment.offset + segment.bytes > file.Size())
{
return false;
}
// 非压缩段同样分块读,避免一次分配整段(巨型数据组可能是 GB 级)。
std::vector<unsigned char> buffer;
std::uint64_t consumed = 0;
while (consumed < segment.bytes && row < cycles)
{
const std::size_t want = static_cast<std::size_t>(
std::min<std::uint64_t>(kReadChunkBytes, segment.bytes - consumed));
buffer.resize(std::max<std::size_t>(want, recordBytes));
if (!file.ReadAt(segment.offset + consumed, buffer.data(), want))
{
return false;
}
data = buffer.data();
bytes = want;
std::size_t position = 0;
while (position < bytes && row < cycles)
{
const std::size_t need = recordBytes - carryBytes;
const std::size_t take = std::min(need, bytes - position);
std::memcpy(carry.data() + carryBytes, data + position, take);
carryBytes += take;
position += take;
if (carryBytes == recordBytes)
{
visit(carry.data(), row++);
carryBytes = 0;
}
}
consumed += want;
}
continue;
}
// 压缩段:整段已在内存里,直接消费。
std::size_t position = 0;
while (position < bytes && row < cycles)
{
const std::size_t need = recordBytes - carryBytes;
const std::size_t take = std::min(need, bytes - position);
std::memcpy(carry.data() + carryBytes, data + position, take);
carryBytes += take;
position += take;
if (carryBytes == recordBytes)
{
visit(carry.data(), row++);
carryBytes = 0;
}
}
}
return row > 0;
}
/** @brief 一个通道组(CG)解析出的时间轴 + 信号列。 */
struct ChannelGroupData
{
@@ -517,103 +684,77 @@ namespace jdi
* @param [IN] recordBytes 单条记录字节数。
* @param [IN,OUT] group 输出(`timeName`/`channels` 已填好,函数填充 `time`/`values`)。
*/
bool ReadChannelGroupRecords(const MdfFile& file, std::uint64_t dt, std::uint64_t cycles,
std::size_t recordBytes, std::size_t masterIndex,
ChannelGroupData& group)
bool ReadChannelGroupRecords(const MdfFile& file, const std::vector<DataSegment>& segments,
std::uint64_t cycles, std::size_t recordBytes,
std::size_t masterIndex, ChannelGroupData& group)
{
if (cycles == 0 || recordBytes == 0)
if (cycles == 0 || recordBytes == 0 || segments.empty())
{
return false;
}
const std::uint64_t dataStart = file.DataOffset(dt);
const std::uint64_t payload = file.BlockLength(dt) - (dataStart - dt);
if (payload < cycles * recordBytes)
{
return false; // 数据不足(损坏或带压缩)
}
group.time.assign(static_cast<std::size_t>(cycles), kMissingValue);
group.values.assign(group.channels.size(),
std::vector<double>(static_cast<std::size_t>(cycles), kMissingValue));
std::vector<unsigned char> buffer(kReadChunkBytes);
std::uint64_t done = 0;
while (done < cycles)
{
const std::uint64_t remainingRecords = cycles - done;
const std::uint64_t wantBytes = std::min<std::uint64_t>(
remainingRecords * recordBytes, static_cast<std::uint64_t>(buffer.size()));
const std::uint64_t wantRecords = wantBytes / recordBytes;
if (wantRecords == 0)
{
break;
}
const std::size_t bytes = static_cast<std::size_t>(wantRecords * recordBytes);
if (!file.ReadAt(dataStart + done * recordBytes, buffer.data(), bytes))
{
return false;
}
for (std::uint64_t r = 0; r < wantRecords; ++r)
{
const unsigned char* record = buffer.data() + r * recordBytes;
const std::size_t row = static_cast<std::size_t>(done + r);
// ⚠️ 记录可能分散在多个 `##DT` / `##DZ`(经 `##DL` / `##HL` 列表)中,
// 由 `ReadRecordSegments` 负责跨段拼接与解压(见该函数的说明)。
return ReadRecordSegments(
file, segments, cycles, recordBytes,
[&group, masterIndex, recordBytes](const unsigned char* record, std::uint64_t row)
{
// 时间主通道。
{
const ChannelSpec& master = group.channels[masterIndex];
double value = DecodeRawValue(record, recordBytes, master);
if (master.allInvalid)
{
value = kMissingValue;
}
else if (master.hasInvalidBit)
{
const std::size_t byteIndex = master.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (master.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = master.linearConversion ? (master.offset + master.factor * value) : value;
}
group.time[static_cast<std::size_t>(row)] = value;
}
// 时间主通道。
{
const ChannelSpec& master = group.channels[masterIndex];
double value = DecodeRawValue(record, recordBytes, master);
if (master.allInvalid)
{
value = kMissingValue;
}
else if (master.hasInvalidBit)
{
const std::size_t byteIndex = master.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (master.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = master.linearConversion ? (master.offset + master.factor * value) : value;
}
group.time[row] = value;
}
// 各信号通道。
for (std::size_t c = 0; c < group.channels.size(); ++c)
{
if (c == masterIndex)
{
continue;
}
const ChannelSpec& spec = group.channels[c];
double value = DecodeRawValue(record, recordBytes, spec);
if (spec.allInvalid)
{
value = kMissingValue;
}
else if (spec.hasInvalidBit)
{
const std::size_t byteIndex = spec.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (spec.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = spec.linearConversion ? (spec.offset + spec.factor * value) : value;
}
group.values[c][row] = value;
}
}
done += wantRecords;
}
return true;
// 各信号通道。
for (std::size_t c = 0; c < group.channels.size(); ++c)
{
if (c == masterIndex)
{
continue;
}
const ChannelSpec& spec = group.channels[c];
double value = DecodeRawValue(record, recordBytes, spec);
if (spec.allInvalid)
{
value = kMissingValue;
}
else if (spec.hasInvalidBit)
{
const std::size_t byteIndex = spec.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (spec.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = spec.linearConversion ? (spec.offset + spec.factor * value) : value;
}
group.values[c][static_cast<std::size_t>(row)] = value;
}
});
}
/**
@@ -692,19 +833,35 @@ namespace jdi
{
++groupCount;
const std::uint64_t cg = file.Link(dg, 1);
const std::uint64_t dt = file.Link(dg, 2);
if (!file.IsBlock(cg, "##CG") || !file.IsBlock(dt, "##DT"))
const std::uint64_t dataBlock = file.Link(dg, 2);
if (!file.IsBlock(cg, "##CG") || dataBlock == 0)
{
return true; // 跳过不完整的数据组
}
// 记录可能写在 `##DT` / `##DL`(数据列表)/ `##HL`(头部列表)/ `##DZ`(压缩)里:
// 旧实现只认 `##DT`,于是用 `##DL` 存放记录的文件会**整组被跳过**
//(用户 2026-10-19 报告:Simulink SDI 导出的 8 个信号只读出 4 个)。
std::vector<DataSegment> segments;
if (!CollectDataSegments(file, dataBlock, segments, 0) || segments.empty())
{
return true; // 不支持的数据布局(如 ##SD)⇒ 跳过该组
}
const std::uint64_t cgData = file.DataOffset(cg);
const std::uint64_t cycles = file.U64(cgData + 8);
if (cycles == 0)
{
return true;
}
const std::uint64_t payload = file.BlockLength(dt) - (file.DataOffset(dt) - dt);
const std::uint64_t recordIdBytes = file.U8(cgData); // `cg_record_id_len`
std::uint64_t payload = 0;
bool anyCompressed = false;
for (const DataSegment& segment : segments)
{
payload += segment.bytes;
anyCompressed = anyCompressed || segment.compressed;
}
// 解析通道链。
std::vector<ChannelSpec> channels;
@@ -724,15 +881,15 @@ namespace jdi
}
// 记录长度:优先用「数据块大小 / 记录数」反推(最可靠),
// 否则按通道覆盖的位宽向上取整到 4 字节。
// ⚠️ 压缩段(`##DZ`)的载荷是压缩后长度,不能用来反推 ⇒ 改为按通道位宽推算。
std::size_t recordBytes = 0;
if (payload % cycles == 0 && payload / cycles > 0)
if (!anyCompressed && payload % cycles == 0 && payload / cycles > 0)
{
recordBytes = static_cast<std::size_t>(payload / cycles);
}
else
{
std::size_t bits = 0;
std::size_t bits = static_cast<std::size_t>(recordIdBytes) * 8u;
for (const ChannelSpec& spec : channels)
{
bits = std::max(bits, spec.bitOffset + spec.bitCount);
@@ -757,7 +914,7 @@ namespace jdi
group.timeName = L"time";
}
group.channels = std::move(channels);
if (!ReadChannelGroupRecords(file, dt, cycles, recordBytes, masterIndex, group))
if (!ReadChannelGroupRecords(file, segments, cycles, recordBytes, masterIndex, group))
{
return true;
}