.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:
1 parent
be76f523f3
commit
75f0fafd41
10 files changed
+1677
-795
No files matched your search
@@ -1984,6 +1984,33 @@ namespace jdi
|
|||||||
void PlaceCursorLabel(gd::RectF& rect, float preferredTop, float textHeight, float clipTop,
|
void PlaceCursorLabel(gd::RectF& rect, float preferredTop, float textHeight, float clipTop,
|
||||||
float clipBottom);
|
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 统计某信号穿越横向光标幅值的上/下边沿数量,结果带缓存。
|
* @brief 统计某信号穿越横向光标幅值的上/下边沿数量,结果带缓存。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2802,6 +2802,51 @@ namespace jdi
|
|||||||
return marker;
|
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,
|
void PlotView::PlaceCursorLabel(gd::RectF& rect, float preferredTop, float textHeight,
|
||||||
float clipTop, float clipBottom)
|
float clipTop, float clipBottom)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4989,6 +4989,10 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
marker.y = labelRect.Y + labelRect.Height * 0.5f;
|
marker.y = labelRect.Y + labelRect.Height * 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⚠️ 数据点圆点被底部的「时间读数 / Δt 面板」盖住时,圆点根本看不见
|
||||||
|
//(时间读数在取值标注之后绘制、直接盖在上面)。此时改成**向下的三角**。
|
||||||
|
CoverHiddenMarker(marker, radius, clipTop);
|
||||||
PaintDataMarker(g, marker, radius, signalColor, m_theme->cardBg);
|
PaintDataMarker(g, marker, radius, signalColor, m_theme->cardBg);
|
||||||
|
|
||||||
const UiInk ink = UiContrastInk(signalColor);
|
const UiInk ink = UiContrastInk(signalColor);
|
||||||
@@ -5113,6 +5117,8 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
centerMarker.y = centerRect.Y + centerRect.Height * 0.5f;
|
centerMarker.y = centerRect.Y + centerRect.Height * 0.5f;
|
||||||
}
|
}
|
||||||
|
// 同原始信号:圆点被底部时间读数面板盖住时改为向下三角。
|
||||||
|
CoverHiddenMarker(centerMarker, radius, clipTop);
|
||||||
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
|
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
|
||||||
// 颜色样式与原始信号一致(同底色/同描边/同文字配色)。
|
// 颜色样式与原始信号一致(同底色/同描边/同文字配色)。
|
||||||
const UiInk centerInk = UiContrastInk(centerColor);
|
const UiInk centerInk = UiContrastInk(centerColor);
|
||||||
|
|||||||
@@ -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(**本机**) | **组名/信号名编辑态点击穿透 + 浮动面板 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. 跨设备协作须知(起源设备视角)
|
## 11. 跨设备协作须知(起源设备视角)
|
||||||
|
|
||||||
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
|
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
|
||||||
@@ -3739,6 +3741,38 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
|
|||||||
`54 × 1.5625`,且与「是否需要滚动条」的判定(只乘一次)不一致 ⇒ 可能出现“网格溢出却没有滚动条”。
|
`54 × 1.5625`,且与「是否需要滚动条」的判定(只乘一次)不一致 ⇒ 可能出现“网格溢出却没有滚动条”。
|
||||||
当前值 = **81 逻辑像素**(用户 2026-10-19 要求:原 54 的 1.5 倍,保证最小网格下也看得清跳变)。
|
当前值 = **81 逻辑像素**(用户 2026-10-19 要求:原 54 的 1.5 倍,保证最小网格下也看得清跳变)。
|
||||||
新增任何“按最小网格高度推导”的逻辑(滚动条判定、`FitLaneHeight`、内容高度)都必须用同一个换算。
|
新增任何“按最小网格高度推导”的逻辑(滚动条判定、`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 离屏验证方法学(本机偏好,强烈建议沿用)
|
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
|
||||||
|
|
||||||
@@ -6489,3 +6523,127 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
|
|||||||
滚动条判定改 `laneCount * m_minLaneHeight * m_scale`;其余两处保持 `* m_scale`。
|
滚动条判定改 `laneCount * m_minLaneHeight * m_scale`;其余两处保持 `* m_scale`。
|
||||||
- 实测(本机 125%、窗 1400×900 逻辑、MF4 全部 128 信号各自一格):
|
- 实测(本机 125%、窗 1400×900 逻辑、MF4 全部 128 信号各自一格):
|
||||||
`lane height = 101.25px = 81 × 1.25`,`content = 12960px`,滚动条出现且可滚动。
|
`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[]` 一一对应,
|
||||||
|
再随信号传递到界面(本轮按要求**未做代码改动**)。
|
||||||
@@ -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
@@ -75,17 +75,37 @@ namespace jdi
|
|||||||
std::uint64_t chunkAddress = 0; /**< 分块数据集的 B-tree 地址。 */
|
std::uint64_t chunkAddress = 0; /**< 分块数据集的 B-tree 地址。 */
|
||||||
std::vector<std::size_t> chunkDims;
|
std::vector<std::size_t> chunkDims;
|
||||||
bool deflate = false; /**< 是否使用了 deflate(zlib)过滤器。 */
|
bool deflate = false; /**< 是否使用了 deflate(zlib)过滤器。 */
|
||||||
|
std::uint64_t dataAddress = 0; /**< 连续布局的数据起始地址(已加基地址)。 */
|
||||||
|
std::uint64_t dataBytes = 0; /**< 连续布局的数据字节数。 */
|
||||||
|
/** 紧凑布局的字节(在布局消息里,数据量很小)。 */
|
||||||
|
std::vector<std::uint8_t> compact;
|
||||||
bool ok = false;
|
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 表示请求取消。 */
|
/** @brief 读文件进度回调:`已读字节 / 总字节`。返回 false 表示请求取消。 */
|
||||||
using OpenProgress = std::function<bool(std::uint64_t bytesRead, std::uint64_t totalBytes)>;
|
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 [IN] path 文件路径。
|
||||||
* @param [OUT] error 失败原因。
|
* @param [OUT] error 失败原因。
|
||||||
* @param [IN] progress 读取进度回调(可为空)。
|
* @param [IN] progress 读取进度回调(可为空)。
|
||||||
@@ -95,6 +115,12 @@ namespace jdi
|
|||||||
const OpenProgress& progress = OpenProgress());
|
const OpenProgress& progress = OpenProgress());
|
||||||
void Close();
|
void Close();
|
||||||
|
|
||||||
|
/** @brief 析构时释放映射(否则栈上对象的映射会留到进程退出)。 */
|
||||||
|
~MatHdf5() { Close(); }
|
||||||
|
|
||||||
|
/** @brief 文件字节数(映射长度)。 */
|
||||||
|
std::uint64_t FileBytes() const { return static_cast<std::uint64_t>(m_data.size()); }
|
||||||
|
|
||||||
/** @brief 根分组地址。 */
|
/** @brief 根分组地址。 */
|
||||||
std::uint64_t Root() const { return m_root; }
|
std::uint64_t Root() const { return m_root; }
|
||||||
|
|
||||||
@@ -110,15 +136,45 @@ namespace jdi
|
|||||||
/** @brief 读取数据集定义(含解码后的原始数据)。 */
|
/** @brief 读取数据集定义(含解码后的原始数据)。 */
|
||||||
bool ReadDataset(std::uint64_t address, Dataset& out) const;
|
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`)。 */
|
/** @brief 读取字符串属性(如 `MATLAB_class`、`MATLAB_fields`)。 */
|
||||||
std::string AttributeString(std::uint64_t address, const char* name) const;
|
std::string AttributeString(std::uint64_t address, const char* name) const;
|
||||||
|
|
||||||
/** @brief 读取 `char` 数据集为宽字符串(MATLAB 的 char 是 uint16)。 */
|
/** @brief 读取 `char` 数据集为宽字符串(MATLAB 的 char 是 uint16)。 */
|
||||||
bool ReadText(std::uint64_t address, std::wstring& out) const;
|
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;
|
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 对象引用 = 对象头地址,相对基地址)。 */
|
/** @brief 读取引用数据集(HDF5 对象引用 = 对象头地址,相对基地址)。 */
|
||||||
bool ReadReferences(std::uint64_t address, std::vector<std::uint64_t>& out) const;
|
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;
|
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,
|
bool ParseType(const std::uint8_t* data, std::size_t size, std::size_t offset, Type& out,
|
||||||
std::size_t& consumed) const;
|
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,
|
void WalkBtree(std::uint64_t node, std::uint64_t heapData,
|
||||||
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
|
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
|
||||||
void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
|
void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
|
||||||
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
|
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_base = 0;
|
||||||
std::uint64_t m_root = 0;
|
std::uint64_t m_root = 0;
|
||||||
int m_offsetSize = 8;
|
int m_offsetSize = 8;
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large.
Load diff
+196
-106
@@ -8,6 +8,7 @@
|
|||||||
#include "MatH5.hpp"
|
#include "MatH5.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -66,21 +67,11 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
MatHdf5::Dataset dataset;
|
|
||||||
if (!file.ReadDataset(data, dataset) || !dataset.ok)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8),
|
// ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8),
|
||||||
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double(`#sampleSize#` 可印证)。
|
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double。
|
||||||
if (dataset.data.size() % sizeof(double) != 0)
|
// `ReadDoubleStream` 会逐 chunk 解压并**直接写入** `out`:
|
||||||
{
|
// 不生成“整个数据集的未压缩字节”,也不做第二次拷贝(GB 级文件的关键)。
|
||||||
return false;
|
return file.ReadDoubleStream(data, out) && !out.empty();
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief 解析 `#refs#` 里的一个元素(信号 / 子数据集)。 */
|
/** @brief 解析 `#refs#` 里的一个元素(信号 / 子数据集)。 */
|
||||||
@@ -260,53 +251,19 @@ namespace jdi
|
|||||||
stack.push_back(std::move(pending));
|
stack.push_back(std::move(pending));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::map<std::wstring, std::size_t> groupIndex;
|
// ⚠️ **先收集信号清单、再解码**(用户 2026-10-19 要求:边解析边填充):
|
||||||
std::vector<std::vector<double>> streams; // 流编号 → 样本
|
// * 旧实现把**每条**信号的时间列和时间流各拷一份(`streams` 缓存 + `AppendSignal`
|
||||||
std::vector<bool> streamCached;
|
// 里的整份复制)⇒ GB 级文件会出现“文件 + 解压字节 + 每条信号两份 double”叠加;
|
||||||
// 进度:`#sigstream#` 的总流数,用来把"解码样本流"这段映射成阶段内 0-1。
|
// * 现在按「分组 + 时间流」合并成一个 `TableBlock`:时间列只存一份、信号列直接
|
||||||
std::size_t streamTotal = 0;
|
// `std::move` 进数据模型,且每条数据流解码完立刻进入最终缓冲。
|
||||||
for (const auto& member : file.Members(sigstream))
|
struct PendingSignal
|
||||||
{
|
{
|
||||||
if (!member.first.empty() && member.first[0] != '#')
|
std::wstring groupName;
|
||||||
{
|
std::wstring signalName;
|
||||||
++streamTotal;
|
int timeStream = -1;
|
||||||
}
|
int dataStream = -1;
|
||||||
}
|
|
||||||
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::vector<PendingSignal> signals;
|
||||||
int unnamedSignals = 0;
|
int unnamedSignals = 0;
|
||||||
while (!stack.empty())
|
while (!stack.empty())
|
||||||
{
|
{
|
||||||
@@ -335,68 +292,201 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!element.isSignal)
|
if (!element.isSignal || element.dataStream < 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<double>* time = streamFor(element.timeStream);
|
PendingSignal signal;
|
||||||
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;
|
|
||||||
for (const std::wstring& piece : pending.path)
|
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;
|
signal.signalName = element.name;
|
||||||
if (signalName.empty())
|
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;
|
// 2) 解码:每个(分组,时间流)一个时间轴块;每条数据流只解码一次并直接进列。
|
||||||
const auto existing = groupIndex.find(groupName);
|
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())
|
if (existing == groupIndex.end())
|
||||||
{
|
{
|
||||||
LoadedSourceGroup group;
|
LoadedSourceGroup group;
|
||||||
group.name = groupName;
|
group.name = signal.groupName;
|
||||||
group.source.sourceName = groupName;
|
group.source.sourceName = signal.groupName;
|
||||||
groups.push_back(std::move(group));
|
groups.push_back(std::move(group));
|
||||||
index = groups.size() - 1;
|
groupSlot = groups.size() - 1;
|
||||||
groupIndex.emplace(groupName, index);
|
groupIndex.emplace(signal.groupName, groupSlot);
|
||||||
}
|
}
|
||||||
else
|
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())
|
if (groups.empty())
|
||||||
@@ -475,17 +565,17 @@ namespace jdi
|
|||||||
MatHdf5 file;
|
MatHdf5 file;
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 进度拆成三段(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新,
|
// 进度拆成三段:
|
||||||
// 界面只是原地等待):
|
// 0.00 – 0.02 映射文件(内存映射,几乎瞬时;遮罩层仍能看到“已加载 = 总大小”)
|
||||||
// 0.00 – 0.55 读文件字节(遮罩层显示"已加载 / 总大小")
|
// 0.02 – 0.98 逐条解码样本流(每个流解压后直接填进数据模型,含解压开销)
|
||||||
// 0.55 – 0.98 解码样本流(每个流解码后推进,含解压与拷贝)
|
|
||||||
// 0.98 – 1.00 收尾
|
// 0.98 – 1.00 收尾
|
||||||
// 读文件阶段把 `hasBytes` 置真;之后字节数已满,只推进百分比。
|
// ⚠️ 旧实现把 0–0.55 全给了“读文件字节”(因为旧实现真的整文件读入);
|
||||||
|
// 现在改成内存映射后那一段几乎没有耗时,若不调整权重,进度条会“瞬间跳到 55% 再慢慢走”。
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
ProgressReporter readReporter;
|
ProgressReporter readReporter;
|
||||||
readReporter.sink = &progress;
|
readReporter.sink = &progress;
|
||||||
readReporter.low = 0.0;
|
readReporter.low = 0.0;
|
||||||
readReporter.high = 0.55;
|
readReporter.high = 0.02;
|
||||||
const MatHdf5::OpenProgress openProgress =
|
const MatHdf5::OpenProgress openProgress =
|
||||||
[&readReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
|
[&readReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
|
||||||
{
|
{
|
||||||
@@ -501,7 +591,7 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
ProgressReporter parseReporter;
|
ProgressReporter parseReporter;
|
||||||
parseReporter.sink = &progress;
|
parseReporter.sink = &progress;
|
||||||
parseReporter.low = 0.55;
|
parseReporter.low = 0.02;
|
||||||
parseReporter.high = 0.98;
|
parseReporter.high = 0.98;
|
||||||
parseReporter.read = readReporter.total;
|
parseReporter.read = readReporter.total;
|
||||||
parseReporter.total = 0; // 字节已读完:本阶段只报百分比
|
parseReporter.total = 0; // 字节已读完:本阶段只报百分比
|
||||||
|
|||||||
+252
-95
@@ -1,5 +1,6 @@
|
|||||||
#include "Mf4Loader.hpp"
|
#include "Mf4Loader.hpp"
|
||||||
|
|
||||||
|
#include "Deflate.hpp"
|
||||||
#include "Utf8Util.hpp"
|
#include "Utf8Util.hpp"
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <functional>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#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)解析出的时间轴 + 信号列。 */
|
/** @brief 一个通道组(CG)解析出的时间轴 + 信号列。 */
|
||||||
struct ChannelGroupData
|
struct ChannelGroupData
|
||||||
{
|
{
|
||||||
@@ -517,103 +684,77 @@ namespace jdi
|
|||||||
* @param [IN] recordBytes 单条记录字节数。
|
* @param [IN] recordBytes 单条记录字节数。
|
||||||
* @param [IN,OUT] group 输出(`timeName`/`channels` 已填好,函数填充 `time`/`values`)。
|
* @param [IN,OUT] group 输出(`timeName`/`channels` 已填好,函数填充 `time`/`values`)。
|
||||||
*/
|
*/
|
||||||
bool ReadChannelGroupRecords(const MdfFile& file, std::uint64_t dt, std::uint64_t cycles,
|
bool ReadChannelGroupRecords(const MdfFile& file, const std::vector<DataSegment>& segments,
|
||||||
std::size_t recordBytes, std::size_t masterIndex,
|
std::uint64_t cycles, std::size_t recordBytes,
|
||||||
ChannelGroupData& group)
|
std::size_t masterIndex, ChannelGroupData& group)
|
||||||
{
|
{
|
||||||
if (cycles == 0 || recordBytes == 0)
|
if (cycles == 0 || recordBytes == 0 || segments.empty())
|
||||||
{
|
{
|
||||||
return false;
|
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.time.assign(static_cast<std::size_t>(cycles), kMissingValue);
|
||||||
group.values.assign(group.channels.size(),
|
group.values.assign(group.channels.size(),
|
||||||
std::vector<double>(static_cast<std::size_t>(cycles), kMissingValue));
|
std::vector<double>(static_cast<std::size_t>(cycles), kMissingValue));
|
||||||
|
|
||||||
std::vector<unsigned char> buffer(kReadChunkBytes);
|
// ⚠️ 记录可能分散在多个 `##DT` / `##DZ`(经 `##DL` / `##HL` 列表)中,
|
||||||
std::uint64_t done = 0;
|
// 由 `ReadRecordSegments` 负责跨段拼接与解压(见该函数的说明)。
|
||||||
while (done < cycles)
|
return ReadRecordSegments(
|
||||||
{
|
file, segments, cycles, recordBytes,
|
||||||
const std::uint64_t remainingRecords = cycles - done;
|
[&group, masterIndex, recordBytes](const unsigned char* record, std::uint64_t row)
|
||||||
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)
|
const ChannelSpec& master = group.channels[masterIndex];
|
||||||
{
|
double value = DecodeRawValue(record, recordBytes, master);
|
||||||
break;
|
if (master.allInvalid)
|
||||||
}
|
{
|
||||||
const std::size_t bytes = static_cast<std::size_t>(wantRecords * recordBytes);
|
value = kMissingValue;
|
||||||
if (!file.ReadAt(dataStart + done * recordBytes, buffer.data(), bytes))
|
}
|
||||||
{
|
else if (master.hasInvalidBit)
|
||||||
return false;
|
{
|
||||||
}
|
const std::size_t byteIndex = master.invalidBitPos / 8u;
|
||||||
for (std::uint64_t r = 0; r < wantRecords; ++r)
|
if (byteIndex < recordBytes &&
|
||||||
{
|
((record[byteIndex] >> (master.invalidBitPos % 8u)) & 1u) != 0u)
|
||||||
const unsigned char* record = buffer.data() + r * recordBytes;
|
{
|
||||||
const std::size_t row = static_cast<std::size_t>(done + r);
|
value = kMissingValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (std::isfinite(value))
|
||||||
|
{
|
||||||
|
value = master.linearConversion ? (master.offset + master.factor * value) : value;
|
||||||
|
}
|
||||||
|
group.time[static_cast<std::size_t>(row)] = value;
|
||||||
|
}
|
||||||
|
|
||||||
// 时间主通道。
|
// 各信号通道。
|
||||||
{
|
for (std::size_t c = 0; c < group.channels.size(); ++c)
|
||||||
const ChannelSpec& master = group.channels[masterIndex];
|
{
|
||||||
double value = DecodeRawValue(record, recordBytes, master);
|
if (c == masterIndex)
|
||||||
if (master.allInvalid)
|
{
|
||||||
{
|
continue;
|
||||||
value = kMissingValue;
|
}
|
||||||
}
|
const ChannelSpec& spec = group.channels[c];
|
||||||
else if (master.hasInvalidBit)
|
double value = DecodeRawValue(record, recordBytes, spec);
|
||||||
{
|
if (spec.allInvalid)
|
||||||
const std::size_t byteIndex = master.invalidBitPos / 8u;
|
{
|
||||||
if (byteIndex < recordBytes &&
|
value = kMissingValue;
|
||||||
((record[byteIndex] >> (master.invalidBitPos % 8u)) & 1u) != 0u)
|
}
|
||||||
{
|
else if (spec.hasInvalidBit)
|
||||||
value = kMissingValue;
|
{
|
||||||
}
|
const std::size_t byteIndex = spec.invalidBitPos / 8u;
|
||||||
}
|
if (byteIndex < recordBytes &&
|
||||||
if (std::isfinite(value))
|
((record[byteIndex] >> (spec.invalidBitPos % 8u)) & 1u) != 0u)
|
||||||
{
|
{
|
||||||
value = master.linearConversion ? (master.offset + master.factor * value) : value;
|
value = kMissingValue;
|
||||||
}
|
}
|
||||||
group.time[row] = value;
|
}
|
||||||
}
|
if (std::isfinite(value))
|
||||||
|
{
|
||||||
// 各信号通道。
|
value = spec.linearConversion ? (spec.offset + spec.factor * value) : value;
|
||||||
for (std::size_t c = 0; c < group.channels.size(); ++c)
|
}
|
||||||
{
|
group.values[c][static_cast<std::size_t>(row)] = value;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -692,19 +833,35 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
++groupCount;
|
++groupCount;
|
||||||
const std::uint64_t cg = file.Link(dg, 1);
|
const std::uint64_t cg = file.Link(dg, 1);
|
||||||
const std::uint64_t dt = file.Link(dg, 2);
|
const std::uint64_t dataBlock = file.Link(dg, 2);
|
||||||
if (!file.IsBlock(cg, "##CG") || !file.IsBlock(dt, "##DT"))
|
if (!file.IsBlock(cg, "##CG") || dataBlock == 0)
|
||||||
{
|
{
|
||||||
return true; // 跳过不完整的数据组
|
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 cgData = file.DataOffset(cg);
|
||||||
const std::uint64_t cycles = file.U64(cgData + 8);
|
const std::uint64_t cycles = file.U64(cgData + 8);
|
||||||
if (cycles == 0)
|
if (cycles == 0)
|
||||||
{
|
{
|
||||||
return true;
|
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;
|
std::vector<ChannelSpec> channels;
|
||||||
@@ -724,15 +881,15 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 记录长度:优先用「数据块大小 / 记录数」反推(最可靠),
|
// 记录长度:优先用「数据块大小 / 记录数」反推(最可靠),
|
||||||
// 否则按通道覆盖的位宽向上取整到 4 字节。
|
// ⚠️ 压缩段(`##DZ`)的载荷是压缩后长度,不能用来反推 ⇒ 改为按通道位宽推算。
|
||||||
std::size_t recordBytes = 0;
|
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);
|
recordBytes = static_cast<std::size_t>(payload / cycles);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
std::size_t bits = 0;
|
std::size_t bits = static_cast<std::size_t>(recordIdBytes) * 8u;
|
||||||
for (const ChannelSpec& spec : channels)
|
for (const ChannelSpec& spec : channels)
|
||||||
{
|
{
|
||||||
bits = std::max(bits, spec.bitOffset + spec.bitCount);
|
bits = std::max(bits, spec.bitOffset + spec.bitCount);
|
||||||
@@ -757,7 +914,7 @@ namespace jdi
|
|||||||
group.timeName = L"time";
|
group.timeName = L"time";
|
||||||
}
|
}
|
||||||
group.channels = std::move(channels);
|
group.channels = std::move(channels);
|
||||||
if (!ReadChannelGroupRecords(file, dt, cycles, recordBytes, masterIndex, group))
|
if (!ReadChannelGroupRecords(file, segments, cycles, recordBytes, masterIndex, group))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user