diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 7d7ba15..6ddc196 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -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 统计某信号穿越横向光标幅值的上/下边沿数量,结果带缓存。 * diff --git a/Components/PlotView/Src/PlotViewInteraction.cpp b/Components/PlotView/Src/PlotViewInteraction.cpp index fe03271..a4894eb 100644 --- a/Components/PlotView/Src/PlotViewInteraction.cpp +++ b/Components/PlotView/Src/PlotViewInteraction.cpp @@ -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) { diff --git a/Components/PlotView/Src/PlotViewPaint.cpp b/Components/PlotView/Src/PlotViewPaint.cpp index 5575987..1308730 100644 --- a/Components/PlotView/Src/PlotViewPaint.cpp +++ b/Components/PlotView/Src/PlotViewPaint.cpp @@ -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); diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index e42b307..fc3aafc 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -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 m_data`; +2. `DecodeChunked` 把**整个数据集**解压成 `out.data`(未压缩字节,与 double 输出同样大); +3. `MatLoader::ReadStream` 再 `memcpy` 成 `std::vector`;`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[]` 一一对应, +再随信号传递到界面(本轮按要求**未做代码改动**)。 \ No newline at end of file diff --git a/Utilities/Inc/Deflate.hpp b/Utilities/Inc/Deflate.hpp new file mode 100644 index 0000000..31badfd --- /dev/null +++ b/Utilities/Inc/Deflate.hpp @@ -0,0 +1,37 @@ +#ifndef __DEFLATE_HPP__ +#define __DEFLATE_HPP__ + +#include +#include +#include + +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& 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 diff --git a/Utilities/Inc/MatH5.hpp b/Utilities/Inc/MatH5.hpp index c9b4217..1af1b88 100644 --- a/Utilities/Inc/MatH5.hpp +++ b/Utilities/Inc/MatH5.hpp @@ -75,17 +75,37 @@ namespace jdi std::uint64_t chunkAddress = 0; /**< 分块数据集的 B-tree 地址。 */ std::vector chunkDims; bool deflate = false; /**< 是否使用了 deflate(zlib)过滤器。 */ + std::uint64_t dataAddress = 0; /**< 连续布局的数据起始地址(已加基地址)。 */ + std::uint64_t dataBytes = 0; /**< 连续布局的数据字节数。 */ + /** 紧凑布局的字节(在布局消息里,数据量很小)。 */ + std::vector 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; /** - * @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(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; + + /** + * @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& out) const; + /** + * @brief 读取 Simulink SDI 的字节流数据集(`#data#`)为 double 序列。 + * + * SDI 的 `#data#` 是 `uint8` 数据集,长度 = 采样数 × 8:按 8 字节一组重新解释为 double。 + * 实现上逐 chunk 解压并直接写入输出缓冲(不产生整块字节数组,也不二次拷贝), + * 因此 GB 级文件也不会出现“文件 + 未压缩字节 + double”三份内存叠加。 + */ + bool ReadDoubleStream(std::uint64_t address, std::vector& out) const; + /** @brief 读取引用数据集(HDF5 对象引用 = 对象头地址,相对基地址)。 */ bool ReadReferences(std::uint64_t address, std::vector& out) const; @@ -139,14 +195,36 @@ namespace jdi void ReadMessages(std::uint64_t address, std::vector& 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& out) const; void WalkBtree(std::uint64_t node, std::uint64_t heapData, std::vector>& out) const; void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData, std::vector>& out) const; - std::vector m_data; + /** + * @brief 内存映射的文件视图(代替“整个文件读进 `vector`”)。 + * + * ⚠️ 行为与 `std::vector` 的**只读子集**一致(`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; diff --git a/Utilities/Src/Deflate.cpp b/Utilities/Src/Deflate.cpp new file mode 100644 index 0000000..6f0c740 --- /dev/null +++ b/Utilities/Src/Deflate.cpp @@ -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(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(offsets[length] + table.count[length]); + } + for (int index = 0; index < total; ++index) + { + if (lengths[index] != 0) + { + table.symbol[offsets[lengths[index]]++] = static_cast(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(symbol); + table.fastLength[fill] = static_cast(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(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(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(cmf) * 256u + flg) % 31u) == 0 && + (flg & 0x20) == 0; + } + + bool InflateZlib(const std::uint8_t* source, std::size_t sourceSize, + std::vector& 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( + 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(reader.Read(5)) + 257; + const int distanceCount = static_cast(reader.Read(5)) + 1; + const int codeCount = static_cast(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(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(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(reader.Read(2)); + } + else if (symbol == 17) + { + repeat = 3 + static_cast(reader.Read(3)); + } + else + { + repeat = 11 + static_cast(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(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 diff --git a/Utilities/Src/MatH5.cpp b/Utilities/Src/MatH5.cpp index 6aeba25..7c23ad9 100644 --- a/Utilities/Src/MatH5.cpp +++ b/Utilities/Src/MatH5.cpp @@ -1,6 +1,10 @@ // HDF5 只读子集:MATLAB 7.3 `.mat` 解析用(见 MatH5.hpp 的说明)。 #include "MatH5.hpp" +#include "Deflate.hpp" + +#include + #include #include #include @@ -29,357 +33,52 @@ namespace jdi return value; } - // ================================================================== - // 内置 inflate(RFC 1950 / 1951) - // - // ⚠️ **不能用 zlib(`-lz`)**:链接 libz.a 会让 Release 产物多出约 46KB, - // 而这里只需要「解压 HDF5 分块」这一个功能,自带的紧凑实现只要几 KB。 - // ⚠️ 更不能为了取文件大小包含 ``:它会连带拉进 locale / codecvt / - // iostream / cp-demangle 等一整套 libstdc++ 代码,实测让产物从 787KB 涨到 1.7MB。 - // ================================================================== + /** @brief 连续布局的读取切片大小(映射视图下只是 memcpy,不会产生额外峰值)。 */ + constexpr std::size_t kStreamSliceBytes = 4u << 20; - /** @brief 位流读取器(LSB 优先,与 DEFLATE 一致)。 */ - struct BitReader + /** @brief 把一段原始字节按元素类型转换成 double(供流式解码使用)。 */ + void ConvertNumbers(const std::uint8_t* source, std::size_t count, std::size_t elementSize, + bool integer, bool floating, bool isSigned, double* out) { - 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) + for (std::size_t index = 0; index < count; ++index) { - while (bits < count) + const std::uint8_t* bytes = source + index * elementSize; + if (floating && elementSize == 8) { - if (position >= size) + double value = 0.0; + std::memcpy(&value, bytes, sizeof(value)); + out[index] = value; + } + else if (floating && elementSize == 4) + { + float value = 0.0f; + std::memcpy(&value, bytes, sizeof(value)); + out[index] = static_cast(value); + } + else if (integer && isSigned) + { + std::int64_t value = 0; + for (std::size_t byte = 0; byte < elementSize; ++byte) { - error = true; - return false; + value |= static_cast(bytes[byte]) << (8 * byte); } - buffer |= static_cast(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 表(只存计数与按长度排好序的符号)。 */ - struct HuffmanTable - { - std::uint16_t count[16] = {}; - std::uint16_t symbol[320] = {}; - }; - - 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(offsets[length] + table.count[length]); - } - for (int index = 0; index < total; ++index) - { - if (lengths[index] != 0) - { - table.symbol[offsets[lengths[index]]++] = static_cast(index); - } - } - return true; - } - - int DecodeSymbol(BitReader& reader, const HuffmanTable& table) - { - int code = 0; - int first = 0; - int index = 0; - for (int length = 1; length < 16; ++length) - { - code |= static_cast(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}; - - /** @brief 解压 zlib 流(含 2 字节头与 4 字节 Adler32 尾)。 */ - bool Inflate(const std::uint8_t* source, std::size_t sourceSize, - std::vector& out) - { - if (sourceSize < 6) - { - return false; - } - // zlib 头:CMF/FLG。只接受 deflate(CM = 8),且不接受预设字典。 - const std::uint8_t cmf = source[0]; - const std::uint8_t flg = source[1]; - if ((cmf & 0x0F) != 8 || ((static_cast(cmf) * 256u + flg) % 31u) != 0 || - (flg & 0x20) != 0) - { - return false; - } - BitReader reader; - reader.data = source + 2; - reader.size = sourceSize - 6; // 末尾 4 字节是 Adler32 - out.clear(); - 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) + const int bits = static_cast(elementSize) * 8; + if (bits < 64 && (value & (1ll << (bits - 1))) != 0) { - return false; - } - const std::uint16_t length = static_cast( - 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(reader.Read(5)) + 257; - const int distanceCount = static_cast(reader.Read(5)) + 1; - const int codeCount = static_cast(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(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(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(reader.Read(2)); - } - else if (symbol == 17) - { - repeat = 3 + static_cast(reader.Read(3)); - } - else - { - repeat = 11 + static_cast(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(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]); - } + value |= ~((1ll << bits) - 1); } + out[index] = static_cast(value); } else { - return false; // 类型 3 非法 - } - if (final != 0) - { - return true; + std::uint64_t value = 0; + for (std::size_t byte = 0; byte < elementSize; ++byte) + { + value |= static_cast(bytes[byte]) << (8 * byte); + } + out[index] = static_cast(value); } } - return false; } } // namespace @@ -421,54 +120,51 @@ namespace jdi { error.clear(); Close(); - // ⚠️ 取文件大小一律用 C 运行库(`_fseeki64` / `_ftelli64`),**不要用 ``**: - // 后者会把 libstdc++ 的 locale / codecvt / iostream / cp-demangle 整套拉进产物, - // 实测让 Release 二进制从 787KB 涨到 1.7MB(用户 2026-10-19 报告)。 - FILE* file = nullptr; - if (_wfopen_s(&file, path.c_str(), L"rb") != 0 || file == nullptr) + + // ⚠️ **内存映射而不是整文件读入**:SDI 导出的 `.mat` 可能是 GB 级, + // “整文件读入 + 分块解码成字节数组 + 再拷成 double”会同时驻留三份内存 + //(用户 2026-10-19 报告,并在本轮改为流式填充)。 + // 映射只占虚拟地址空间:未被解析到的页不会驻留,系统也可以在内存压力下回收。 + const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) { error = L"无法打开文件。"; return false; } - if (_fseeki64(file, 0, SEEK_END) != 0) + LARGE_INTEGER fileSize = {}; + if (!GetFileSizeEx(file, &fileSize) || fileSize.QuadPart <= 0) { - std::fclose(file); + CloseHandle(file); error = L"无法读取文件大小。"; return false; } - const long long size = _ftelli64(file); - if (size <= 0 || _fseeki64(file, 0, SEEK_SET) != 0) + const HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr); + if (mapping == nullptr) { - std::fclose(file); - error = L"无法读取文件大小。"; + CloseHandle(file); + error = L"无法映射文件(请确认文件可读且未被独占)。"; return false; } - m_data.resize(static_cast(size)); + const void* view = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0); + if (view == nullptr) + { + CloseHandle(mapping); + CloseHandle(file); + error = L"无法映射文件视图。"; + return false; + } + m_fileHandle = file; + m_mapHandle = mapping; + m_data.pointer = static_cast(view); + m_data.length = static_cast(fileSize.QuadPart); - // 按块读入并回报进度(加载遮罩层的"已加载大小 / 总大小 / 百分比"由它驱动)。 - constexpr std::size_t kChunk = 4u << 20; - std::size_t offset = 0; - while (offset < m_data.size()) + // 字节已全部可访问(映射完成):把“已加载 = 总大小”报给遮罩层, + // 之后的时间主要在“解码样本流”阶段(由 MatLoader 逐流推进)。 + if (progress && !progress(m_data.size(), m_data.size())) { - const std::size_t want = std::min(kChunk, m_data.size() - offset); - const std::size_t got = std::fread(m_data.data() + offset, 1, want, file); - offset += got; - if (got != want) - { - break; - } - if (progress && !progress(offset, m_data.size())) - { - std::fclose(file); - Close(); - error.clear(); // 用户取消:不显示错误 - return false; - } - } - std::fclose(file); - if (offset != m_data.size()) - { - error = L"读取文件失败。"; + Close(); + error.clear(); // 用户取消:不显示错误 return false; } @@ -514,6 +210,21 @@ namespace jdi void MatHdf5::Close() { + // 先解除视图,再关映射与文件句柄(顺序不能反)。 + if (m_data.pointer != nullptr) + { + UnmapViewOfFile(m_data.pointer); + } + if (m_mapHandle != nullptr) + { + CloseHandle(static_cast(m_mapHandle)); + m_mapHandle = nullptr; + } + if (m_fileHandle != nullptr) + { + CloseHandle(static_cast(m_fileHandle)); + m_fileHandle = nullptr; + } m_data.clear(); m_base = 0; m_root = 0; @@ -840,154 +551,7 @@ namespace jdi return true; } - bool MatHdf5::DecodeChunked(const Dataset& dataset, std::size_t elementSize, - std::vector& out) const - { - if (dataset.chunkAddress == 0 || dataset.chunkDims.empty() || elementSize == 0 || - dataset.dims.empty()) - { - return false; - } - // ⚠️ MATLAB 写出的分块维度可能与数据空间维度**维数不一致**(实测:1 维数据集的 - // 分块维度是 `[N, 1]`)。把维度数组在**右侧补 1**(行优先的最快轴)对齐即可, - // 二者的元素总数与遍历顺序都不变。 - std::vector dims = dataset.dims; - std::vector chunkDims = dataset.chunkDims; - while (dims.size() < chunkDims.size()) - { - dims.push_back(1); - } - while (chunkDims.size() < dims.size()) - { - chunkDims.push_back(1); - } - const std::size_t rank = dims.size(); - const std::uint64_t total = [&]() -> std::uint64_t - { - std::uint64_t value = 1; - for (const std::size_t dim : dims) - { - value *= dim; - } - return value; - }(); - out.assign(static_cast(total) * elementSize, 0); - std::vector chunk; - std::vector inflated; - // 遍历分块 B-tree:key = 分块大小 + 过滤器掩码 + 各维偏移。 - std::vector stack{dataset.chunkAddress}; - int guard = 0; - while (!stack.empty() && guard++ < 1000000) - { - const std::uint64_t node = stack.back(); - stack.pop_back(); - if (node == 0 || node + 24 > m_data.size() || - std::memcmp(&m_data[static_cast(node)], "TREE", 4) != 0) - { - continue; - } - const std::uint8_t level = U8(node + 5); - const std::uint16_t entries = U16(node + 6); - const std::size_t keySize = 8 + 8 * rank; - std::uint64_t offset = node + 8 + 8 + 8; - for (std::uint16_t index = 0; index < static_cast(entries + 1u); ++index) - { - const std::uint32_t chunkSize = (offset + 4 <= m_data.size()) ? U32(offset) : 0; - std::vector chunkOffset(rank, 0); - for (std::size_t axis = 0; axis < rank; ++axis) - { - chunkOffset[axis] = U64(offset + 8 + axis * 8); - } - offset += keySize; - if (index < entries) - { - const std::uint64_t child = Resolve(U64(offset)); - offset += 8; - if (level > 0) - { - stack.push_back(child); - } - else if (child != 0 && chunkSize > 0) - { - chunk.assign(m_data.begin() + static_cast(child), - m_data.begin() + static_cast(child + chunkSize)); - const std::uint8_t* source = chunk.data(); - std::size_t sourceSize = chunk.size(); - if (dataset.deflate) - { - // 分块未压缩时大小 = 分块元素数 × 元素大小:此时不需要解压。 - std::size_t expected = elementSize; - for (const std::size_t dim : dataset.chunkDims) - { - expected *= dim; - } - if (chunk.size() != expected) - { - // 内置 inflate(不依赖 zlib,见文件头的说明)。 - if (!Inflate(chunk.data(), chunk.size(), inflated)) - { - continue; - } - if (inflated.size() < expected) - { - inflated.resize(expected, 0); - } - source = inflated.data(); - sourceSize = expected; - } - } - // 把该块拷进输出缓冲区(按行优先展开)。 - const std::size_t rowBytes = chunkDims[rank - 1] * elementSize; - std::size_t rows = 1; - for (std::size_t axis = 0; axis + 1 < rank; ++axis) - { - rows *= chunkDims[axis]; - } - for (std::size_t row = 0; row < rows; ++row) - { - std::vector coordinate(rank - 1, 0); - std::size_t remainder = row; - for (std::size_t axis = rank - 1; axis-- > 0;) - { - coordinate[axis] = remainder % chunkDims[axis]; - remainder /= chunkDims[axis]; - } - std::uint64_t targetRow = 0; - bool inside = true; - std::uint64_t stride = 1; - for (std::size_t axis = rank - 1; axis-- > 0;) - { - const std::uint64_t index = chunkOffset[axis] + coordinate[axis]; - if (index >= dims[axis]) - { - inside = false; - break; - } - targetRow += index * stride; - stride *= dims[axis]; - } - if (!inside) - { - continue; - } - const std::size_t sourceOffset = row * rowBytes; - const std::size_t targetOffset = - static_cast(targetRow) * dims[rank - 1] * elementSize; - if (sourceOffset + rowBytes > sourceSize || - targetOffset + rowBytes > out.size()) - { - continue; - } - std::memcpy(out.data() + targetOffset, source + sourceOffset, rowBytes); - } - } - } - } - } - return true; - } - - bool MatHdf5::ReadDataset(std::uint64_t address, Dataset& out) const + bool MatHdf5::ReadDatasetInfo(std::uint64_t address, Dataset& out) const { out = Dataset(); std::vector messages; @@ -1091,10 +655,11 @@ namespace jdi { return false; } - const std::size_t elementSize = out.type.size; + out.dataAddress = dataAddress; + out.dataBytes = dataSize; if (out.layout == LayoutClass::Compact) { - // 紧凑数据在消息里,重新扫描一次取出字节。 + // 紧凑数据就在布局消息里(极小),保留一份以便流式遍历时吐出。 for (const Message& message : messages) { if (message.type == kMessageLayout) @@ -1102,32 +667,250 @@ namespace jdi const std::size_t offset = (message.data[0] == 3) ? 4 : 8; if (offset + compactSize <= message.data.size()) { - out.data.assign(message.data.begin() + static_cast(offset), - message.data.begin() + - static_cast(offset + compactSize)); + out.compact.assign(message.data.begin() + static_cast(offset), + message.data.begin() + + static_cast(offset + compactSize)); } } } } - else if (out.layout == LayoutClass::Contiguous) - { - if (dataAddress != 0 && dataAddress + dataSize <= m_data.size()) - { - out.data.assign(m_data.begin() + static_cast(dataAddress), - m_data.begin() + static_cast(dataAddress + dataSize)); - } - } - else if (out.layout == LayoutClass::Chunked) - { - if (!DecodeChunked(out, elementSize, out.data)) - { - return false; - } - } out.ok = true; return true; } + bool MatHdf5::ReadDataset(std::uint64_t address, Dataset& out) const + { + if (!ReadDatasetInfo(address, out)) + { + return false; + } + const std::size_t elementSize = out.type.size; + const std::uint64_t total = out.ElementCount(); + if (total == 0 || total > static_cast(SIZE_MAX) / elementSize) + { + return false; + } + out.data.assign(static_cast(total) * elementSize, 0); + const bool ok = StreamDataset( + out, [&out, elementSize](const std::uint8_t* bytes, std::size_t byteCount, + std::uint64_t elementOffset) -> bool + { + const std::size_t target = static_cast(elementOffset) * elementSize; + if (target + byteCount > out.data.size()) + { + return false; + } + std::memcpy(out.data.data() + target, bytes, byteCount); + return true; + }); + return ok && out.ok; + } + + bool MatHdf5::StreamDataset(const Dataset& dataset, const ByteSink& sink) const + { + if (!dataset.ok || !sink) + { + return false; + } + const std::size_t elementSize = dataset.type.size; + if (elementSize == 0) + { + return false; + } + + // 紧凑布局:数据就在消息里(很小)。 + if (dataset.layout == LayoutClass::Compact) + { + return sink(dataset.compact.data(), dataset.compact.size(), 0); + } + + // 连续布局:按 4MB 切片直接吐出(文件是映射的,这里只是指针运算,不产生额外缓冲)。 + if (dataset.layout == LayoutClass::Contiguous) + { + const std::uint64_t total = dataset.ElementCount(); + const std::uint64_t totalBytes = total * elementSize; + const std::uint64_t available = + std::min(dataset.dataBytes, + (dataset.dataAddress < m_data.size()) + ? (m_data.size() - dataset.dataAddress) + : 0); + const std::uint64_t bytes = std::min(totalBytes, available); + std::size_t slice = kStreamSliceBytes - (kStreamSliceBytes % elementSize); + if (slice == 0) + { + slice = elementSize; + } + for (std::uint64_t offset = 0; offset < bytes; offset += slice) + { + const std::size_t count = static_cast(std::min(slice, bytes - offset)); + if (!sink(m_data.data() + dataset.dataAddress + offset, count, offset / elementSize)) + { + return false; + } + } + return true; + } + + // 分块布局:逐 chunk 解压,立刻把每一行交给回调(不保留整块字节数组)。 + if (dataset.chunkAddress == 0 || dataset.chunkDims.empty() || dataset.dims.empty()) + { + return false; + } + // ⚠️ MATLAB 写出的分块维度可能与数据空间维度**维数不一致**(实测:1 维数据集的 + // 分块维度是 `[N, 1]`)。把维度数组在**右侧补 1**(行优先的最快轴)对齐即可。 + std::vector dims = dataset.dims; + std::vector chunkDims = dataset.chunkDims; + while (dims.size() < chunkDims.size()) + { + dims.push_back(1); + } + while (chunkDims.size() < dims.size()) + { + chunkDims.push_back(1); + } + const std::size_t rank = dims.size(); + std::size_t expected = elementSize; + for (const std::size_t dim : dataset.chunkDims) + { + expected *= dim; + } + const std::size_t rowBytes = chunkDims[rank - 1] * elementSize; + std::size_t rowsPerChunk = 1; + for (std::size_t axis = 0; axis + 1 < rank; ++axis) + { + rowsPerChunk *= chunkDims[axis]; + } + + std::vector inflated; + std::vector stack{dataset.chunkAddress}; + int guard = 0; + while (!stack.empty() && guard++ < 1000000) + { + const std::uint64_t node = stack.back(); + stack.pop_back(); + if (node == 0 || node + 24 > m_data.size() || + std::memcmp(&m_data[static_cast(node)], "TREE", 4) != 0) + { + continue; + } + const std::uint8_t level = U8(node + 5); + const std::uint16_t entries = U16(node + 6); + const std::size_t keySize = 8 + 8 * rank; + std::uint64_t offset = node + 8 + 8 + 8; + for (std::uint16_t index = 0; index < static_cast(entries + 1u); ++index) + { + const std::uint32_t chunkSize = (offset + 4 <= m_data.size()) ? U32(offset) : 0; + std::vector chunkOffset(rank, 0); + for (std::size_t axis = 0; axis < rank; ++axis) + { + chunkOffset[axis] = U64(offset + 8 + axis * 8); + } + offset += keySize; + if (index >= entries) + { + continue; + } + const std::uint64_t child = Resolve(U64(offset)); + offset += 8; + if (level > 0) + { + stack.push_back(child); + continue; + } + if (child == 0 || chunkSize == 0 || child + chunkSize > m_data.size()) + { + continue; + } + + const std::uint8_t* source = m_data.data() + child; + std::size_t sourceSize = chunkSize; + if (dataset.deflate && chunkSize != expected) + { + // 分块未压缩时大小 = 分块元素数 × 元素大小:此时不需要解压。 + if (!InflateZlib(source, sourceSize, inflated, expected)) + { + continue; + } + if (inflated.size() < expected) + { + inflated.resize(expected, 0); + } + source = inflated.data(); + sourceSize = expected; + } + + // 块内按行(最快轴一整行连续)散射到目标位置。 + // ⚠️ 行数可能很大(例如 SDI 的 `#data#` 是 `dims=[N]`、chunk 为 `[N,1]`, + // 归一化后 rank=2 ⇒「每行只有 1 字节」)。若逐行回调,就是上千万次 + // 小 memcpy + 上千万次 vector 分配,实测把解压吞吐拖到 ~11MB/s。 + // 因此这里把**目标连续的行合并成一次回调**(典型情况整块只需 1 次)。 + std::vector coordinate(rank > 0 ? rank - 1 : 0, 0); + std::uint64_t runElement = 0; // 合并段的起始元素下标 + std::size_t runSourceOffset = 0; // 合并段的源偏移 + std::size_t runBytes = 0; // 合并段字节数 + bool runOpen = false; + for (std::size_t row = 0; row < rowsPerChunk; ++row) + { + std::size_t remainder = row; + for (std::size_t axis = rank - 1; axis-- > 0;) + { + coordinate[axis] = remainder % chunkDims[axis]; + remainder /= chunkDims[axis]; + } + std::uint64_t targetRow = 0; + bool inside = true; + std::uint64_t stride = 1; + for (std::size_t axis = rank - 1; axis-- > 0;) + { + const std::uint64_t index = chunkOffset[axis] + coordinate[axis]; + if (index >= dims[axis]) + { + inside = false; + break; + } + targetRow += index * stride; + stride *= dims[axis]; + } + if (!inside) + { + continue; + } + const std::size_t sourceOffset = row * rowBytes; + if (sourceOffset + rowBytes > sourceSize) + { + continue; + } + const std::uint64_t targetElement = targetRow * chunkDims[rank - 1]; + if (runOpen && targetElement == runElement + (runBytes / elementSize)) + { + runBytes += rowBytes; + continue; + } + if (runOpen) + { + if (!sink(source + runSourceOffset, runBytes, runElement)) + { + return false; + } + } + runOpen = true; + runElement = targetElement; + runSourceOffset = sourceOffset; + runBytes = rowBytes; + } + if (runOpen) + { + if (!sink(source + runSourceOffset, runBytes, runElement)) + { + return false; + } + } + } + } + return true; + } + + std::string MatHdf5::AttributeString(std::uint64_t address, const char* name) const { std::vector messages; @@ -1253,62 +1036,136 @@ namespace jdi { out.clear(); Dataset dataset; - if (!ReadDataset(address, dataset) || !dataset.ok) + if (!ReadDatasetInfo(address, dataset) || !dataset.ok) { return false; } const std::size_t elementSize = dataset.type.size; - if (elementSize == 0 || dataset.data.size() % elementSize != 0) - { - return false; - } - const std::size_t count = dataset.data.size() / elementSize; - out.resize(count); const bool integer = (dataset.type.cls == TypeClass::Integer); const bool floating = (dataset.type.cls == TypeClass::Float); - if (!integer && !floating) + if (elementSize == 0 || (!integer && !floating)) { return false; } - for (std::size_t index = 0; index < count; ++index) + const std::uint64_t total = dataset.ElementCount(); + if (total == 0 || total > static_cast(SIZE_MAX) / elementSize) { - const std::uint8_t* source = dataset.data.data() + index * elementSize; - if (floating && elementSize == 8) - { - double value = 0.0; - std::memcpy(&value, source, sizeof(value)); - out[index] = value; - } - else if (floating && elementSize == 4) - { - float value = 0.0f; - std::memcpy(&value, source, sizeof(value)); - out[index] = static_cast(value); - } - else if (integer && dataset.type.isSigned) - { - std::int64_t value = 0; - for (std::size_t byte = 0; byte < elementSize; ++byte) - { - value |= static_cast(source[byte]) << (8 * byte); - } - // 符号扩展 - const int bits = static_cast(elementSize) * 8; - if (bits < 64 && (value & (1ll << (bits - 1))) != 0) - { - value |= ~((1ll << bits) - 1); - } - out[index] = static_cast(value); - } - else if (integer) - { - std::uint64_t value = 0; - for (std::size_t byte = 0; byte < elementSize; ++byte) - { - value |= static_cast(source[byte]) << (8 * byte); - } - out[index] = static_cast(value); - } + return false; + } + out.assign(static_cast(total), 0.0); + + // ⚠️ 流式转换:分块数据集逐 chunk 解压后**直接**写进 `out`, + // 不再先生成一份“整个数据集的未压缩字节”(GB 级数据下那份缓冲与 double 输出同样大)。 + return StreamDataset( + dataset, [&out, elementSize, integer, floating, &dataset](const std::uint8_t* bytes, + std::size_t byteCount, + std::uint64_t elementOffset) -> bool + { + if (elementSize == 0 || byteCount % elementSize != 0) + { + return false; + } + const std::size_t count = byteCount / elementSize; + if (elementOffset + count > out.size()) + { + return false; + } + ConvertNumbers(bytes, count, elementSize, integer, floating, dataset.type.isSigned, + out.data() + elementOffset); + return true; + }); + } + + bool MatHdf5::ReadDoubleStream(std::uint64_t address, std::vector& out) const + { + out.clear(); + Dataset dataset; + if (!ReadDatasetInfo(address, dataset) || !dataset.ok) + { + return false; + } + // SDI 的 `#data#` 必须是单字节元素(uint8 字节流),每 8 字节一组是一个 double。 + if (dataset.type.size != 1) + { + return false; + } + const std::uint64_t totalBytes = dataset.ElementCount(); + if (totalBytes == 0 || totalBytes % sizeof(double) != 0) + { + return false; + } + const std::size_t sampleCount = static_cast(totalBytes / sizeof(double)); + out.assign(sampleCount, 0.0); + + // ⚠️ 分块边界不一定与 8 字节对齐(chunk 维度是任意整数)⇒ 用最多 7 字节的尾巴接续。 + // 关键:**不做整块字节缓冲**,解压出来的字节立即被解释成 double 写进 `out`。 + std::uint8_t tail[8] = {}; + std::size_t tailBytes = 0; + std::uint64_t tailStart = 0; // 尾巴首字节在数据集中的字节偏移 + bool failed = false; + const bool ok = StreamDataset( + dataset, [&](const std::uint8_t* bytes, std::size_t byteCount, + std::uint64_t elementOffset) -> bool + { + // 数据必须连续吐出(分块遍历按坐标升序;中间有空洞说明表里缺块)。 + if (tailBytes > 0 && elementOffset != tailStart + tailBytes) + { + failed = true; + return false; + } + std::size_t position = 0; + if (tailBytes == 0) + { + tailStart = elementOffset; + } + // 先把尾巴补满一个 double。 + while (tailBytes > 0 && tailBytes < sizeof(double) && position < byteCount) + { + tail[tailBytes++] = bytes[position++]; + } + if (tailBytes == sizeof(double)) + { + const std::size_t index = static_cast(tailStart / sizeof(double)); + if (index >= out.size()) + { + failed = true; + return false; + } + std::memcpy(&out[index], tail, sizeof(double)); + tailStart += sizeof(double); + tailBytes = 0; + } + // 剩余部分按 8 字节整组直接 memcpy(容量已按总字节数算好)。 + const std::size_t groups = (byteCount - position) / sizeof(double); + if (groups > 0) + { + const std::size_t index = static_cast(tailStart / sizeof(double)); + if (index + groups > out.size()) + { + failed = true; + return false; + } + std::memcpy(out.data() + index, bytes + position, groups * sizeof(double)); + position += groups * sizeof(double); + tailStart += groups * sizeof(double); + } + // 末尾零头留作下一块的尾巴。 + while (position < byteCount) + { + tail[tailBytes++] = bytes[position++]; + } + return true; + }); + if (!ok || failed) + { + out.clear(); + return false; + } + // 尾部不足 8 字节(损坏)视为失败,与旧实现(要求整除)保持一致。 + if (tailBytes != 0) + { + out.clear(); + return false; } return true; } diff --git a/Utilities/Src/MatLoader.cpp b/Utilities/Src/MatLoader.cpp index c6fb71c..d038bbf 100644 --- a/Utilities/Src/MatLoader.cpp +++ b/Utilities/Src/MatLoader.cpp @@ -8,6 +8,7 @@ #include "MatH5.hpp" #include +#include #include #include #include @@ -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 groupIndex; - std::vector> streams; // 流编号 → 样本 - std::vector 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* - { - if (index < 0) - { - return nullptr; - } - if (streams.size() <= static_cast(index)) - { - streams.resize(static_cast(index) + 1); - streamCached.resize(static_cast(index) + 1, false); - } - if (!streamCached[static_cast(index)]) - { - const std::uint64_t streamObject = - file.Member(sigstream, std::to_string(index)); - if (streamObject == 0 || !ReadStream(file, streamObject, streams[static_cast(index)])) - { - streams[static_cast(index)].clear(); - } - streamCached[static_cast(index)] = true; - // ⚠️ 必须在这里回报进度:读一个 42MB 的 `.mat` 时,"解码样本流"才是真正的 - // 耗时大头(每个流都要解压 + 拷贝);不报进度的话界面会停在原地等 - //(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新)。 - ++streamDone; - reporter.Report(streamTotal > 0 - ? static_cast(streamDone) / - static_cast(streamTotal) - : 1.0); - } - return streams[static_cast(index)].empty() - ? nullptr - : &streams[static_cast(index)]; + std::wstring groupName; + std::wstring signalName; + int timeStream = -1; + int dataStream = -1; }; - + std::vector 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* time = streamFor(element.timeStream); - const std::vector* values = streamFor(element.dataStream); - if (values == nullptr) - { - continue; - } - std::vector 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(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 groupIndex; + std::map, std::size_t> blockIndex; + std::size_t decoded = 0; + + const auto reportStream = [&]() + { + ++decoded; + const double fraction = (signals.empty()) + ? 1.0 + : static_cast(decoded) / + static_cast(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 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 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 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(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& 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 time(values.size()); + for (std::size_t index = 0; index < values.size(); ++index) + { + time[index] = static_cast(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& column : block.table.columns) + { + rows = std::min(rows, column.size()); + } + if (rows == SIZE_MAX) + { + rows = 0; + } + for (std::vector& 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(*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; // 字节已读完:本阶段只报百分比 diff --git a/Utilities/Src/Mf4Loader.cpp b/Utilities/Src/Mf4Loader.cpp index 9c462d7..cf5b274 100644 --- a/Utilities/Src/Mf4Loader.cpp +++ b/Utilities/Src/Mf4Loader.cpp @@ -1,5 +1,6 @@ #include "Mf4Loader.hpp" +#include "Deflate.hpp" #include "Utf8Util.hpp" #include @@ -8,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -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& 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& segments, std::uint64_t cycles, + std::size_t recordBytes, + const std::function& visit) + { + if (recordBytes == 0 || cycles == 0) + { + return false; + } + std::vector carry(recordBytes); + std::size_t carryBytes = 0; + std::uint64_t row = 0; + std::vector 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 compressed(static_cast(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 buffer; + std::uint64_t consumed = 0; + while (consumed < segment.bytes && row < cycles) + { + const std::size_t want = static_cast( + std::min(kReadChunkBytes, segment.bytes - consumed)); + buffer.resize(std::max(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& 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(cycles), kMissingValue); group.values.assign(group.channels.size(), std::vector(static_cast(cycles), kMissingValue)); - std::vector buffer(kReadChunkBytes); - std::uint64_t done = 0; - while (done < cycles) - { - const std::uint64_t remainingRecords = cycles - done; - const std::uint64_t wantBytes = std::min( - remainingRecords * recordBytes, static_cast(buffer.size())); - const std::uint64_t wantRecords = wantBytes / recordBytes; - if (wantRecords == 0) - { - break; - } - const std::size_t bytes = static_cast(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(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(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(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 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 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(payload / cycles); } else { - std::size_t bits = 0; + std::size_t bits = static_cast(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; }