Files
Just-DataInspector/Utilities/Inc/MatH5.hpp
T
DeepSeek-V4.1-Flash 181f95cc3a MF4 补 ##SD/##SI/VLSD + 信号枚举文本 enumText + .mat 改随机读(流式进度)
1) 修复上轮记录的两个已知问题
   - MatLoader: LoadPlainObject 不再为拿 dims 调 ReadDataset(整块解码第二次),
     改用 ReadDatasetInfo;普通 v7.3 大矩阵的时间/内存不再翻倍。
   - MdfFile::TextBlock 注释与实现对齐:真的支持 ##MD(UTF-8 XML,无长度前缀)。

2) 信号枚举信息(以 TestData/NEW-MF4.mf4 为目标)
   - 新增 SignalEnumText(DataTable.hpp):values[] + texts[] + Empty()/Find(),
     命名用 enumText —— 它描述"取值代表哪个状态",不是单位。
   - DataTable::columnEnumText 与 columns 同下标;SignalData::enumText 由
     BuildSignalGroup 移动赋值;其它格式(CSV/.mat)保持为空。
   - MF4 cc_type = 7:cc_val[i](F64,数据段 +24)↔ cc_ref[4 + i](##TX/##MD 文本),
     最后一个引用是默认文本,不使用。
   - 界面只在**光标取值标注**追加 " = <枚举文本>"(等号两侧各一个空格、无尖括号),
     查表用采样原值(ApplyDrawTransform 之前);常规状态显示一个像素都不变。
   - 实测:NEW-MF4.mf4 8 信号 / 4 条带枚举(32+5+32+5),MF4-Record.MF4 另有 2 种(12/32)。

3) MF4 支持范围
   - ##SI 采样索引:只有索引 ≥2 条且全部 < 周期数才生效(逐记录有效性位图)。
     实测部分工具给每个通道挂只含常量 513/4 的 ##SI,必须忽略,否则整列变 NaN。
   - ##SD + VLSD(cn_type == 1):##SD 数据段是「UINT32 长度 + 字节」样本序列,
     记录里放 64 位偏移;AppendSignalData 递归展开 SD/DZ/DL/HL(单通道上限 512MB),
     DecodeVlsdValue 先按直接偏移、失败再按 >>1 回退,取样本第一个元素。
   - CN 的 cn_data / md_unit / md_comment 下标按链接总数反推:8 链接(MDF 4.0 风格)
     = 换算[4]/数据[5]/单位[6]/注释[7],9 链接(4.1)= [5]/[6]/[7]/[8]。
     实测 Simulink 导出的“4.10”仍写 8 链接,不能写死 Link(cn,4/5)。
   - 主通道:cn_sync_type 1/2/3(时间/角度/距离)→ 名字 t/time/timestamp/zeit/时间
     (含 t[...])→ 第一个数值通道兜底;cg_flags 第 2 位(远程主通道)跳过 sync 判定。
   - 记录长度优先 cg_data_bytes(CG 数据段 +24);旧实现把 U8(cgData) 当
     cg_record_id_len,其实是 cg_flags 低字节(总线事件位)。
   - 进度:先走 DG 链得到真实组数(不再写死 64),hasBytes = true,bytesRead 取
     MdfFile::ReadAt 累计(夹到文件大小);4MB 分块读取检查取消。
   - ##DZ:数据段前有 28 字节头(original_type/zip_type/param/original_size/zip_size),
     旧实现整段当 zlib 流是错的;只支持纯 deflate(zip_type = 0)。
   - 一个信号都没有时,错误信息附带「布局无法解析 / 缺少时间通道 / 空数据组」计数。

4) .mat:内存映射 → 随机读 + 元数据块缓存(消除 SEH 风险)
   - MatHdf5::Open 改为 CreateFileW(FILE_FLAG_SEQUENTIAL_SCAN) + ReadFile;
     删除 MappedBytes,改为 ReadAt / Cached(16 × 64KB LRU + 上次命中槽)/ Tag /
     ReadData / ReadCString。I/O 失败只会变成 false,不再触发页访问异常。
   - 数据读取:连续布局按 4MB 切片;分块布局按需 + 64KB 对齐。
     固定 4MB 预读在交错存放的分块数据集上会把读盘量放大到 2.2 倍(2874MB/1293MB),
     改后 1301MB。
   - 关键提速:dims=[N]、chunk=[32768,1] 归一化后 rank=2、每行 1 字节,逐行算坐标
     是 12.9 亿次迭代;新增「rank == 2 且 chunkDims[1] == dims[1]」快路径(整块一次
     sink)。BIG_MAT.mat(1.29GB / 5 × 16,142,471):21.5s → 7.9s,私有内存 +741MB。
   - 进度:Open 立刻上报「0 / 总大小」,之后每读一块回调 ReadProgress(bytesRead,total)
     → MatLoader 映射到 0.02–0.98 并单调夹紧(字节进度与逐流解码取较大值),收尾 1.0。
     遮罩层百分比不再一上来就 100%;取消会中止(StreamDataset 返回 false),不留 NaN 空洞。
   - 连续布局数据不足(文件被截断)现在明确失败,不再静默截短。

验证(全部离屏:无可见窗口、不注入鼠标键盘屏幕)
- 新增 .preflight/perf/loadercheck(数据层)32/32:
  NEW-MF4 8 信号 / 4 条枚举 / 枚举文本逐条核对 / 9396 与 94 行;
  MF4-Record 124 信号 / 3 轴 / 4 条枚举;data-sample.mat 4 组 8 信号 / 行数;
  BIG_MAT.mat 5 信号 × 16,142,471 + 进度(total 已知、ratio 单调、bytes 递增、
  起点 ≤0.05、终值 1.0、更新 ≥10 次)。
- 新增 .preflight/perf/enumui(真实 Application + SW_HIDE)12/12:
  只显示 gVR_WinOpenReq[0],光标落在枚举采样点上 —— 标签宽度 265px;
  临时清空 enumText 后 25px;差值 240px 与 " = WinOpenReqAtDrvr_Idle" 的
  UiMeasureTextWidth 完全一致;恢复后宽度回到 265px。
- 回归:datacheck 69/69、matcheck 21/21、matui 15/15(loading bytes 达到总大小)、
  verify 49/49。
- make release / make debug 零警告。Build/Release/TargetCheck.exe = 820,224 字节
  (上轮 785,920,+34KB)。(Target.exe 正在被用户运行的程序占用,链接步骤未覆盖它。)
2026-10-20 01:56:47 +08:00

287 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#ifndef __MAT_H5_HPP__
#define __MAT_H5_HPP__
#include <cstdint>
#include <functional>
#include <string>
#include <utility>
#include <vector>
namespace jdi
{
/**
* @brief 只读的 HDF5 子集解析器(用于 MATLAB 7.3 的 `.mat` 文件)。
*
* MATLAB 7.3(`-v7.3`)的 `.mat` 其实是 **HDF5** 文件:数值矩阵是数据集,
* 结构体是分组(字段 = 成员),元信息放在属性里(如 `MATLAB_class`)。
* 本项目不引入 HDF5 库,因此这里实现一个**刚好够用**的只读子集:
*
* * 超级块 v0/v1/v2/v3;
* * 对象头 v1(含续块消息),v2 仅识别并跳过;
* * 分组成员:符号表(B-tree v1 + 本地堆)与 link 消息两种方式;
* * 数据空间 v1/v2、数据类型 v1(整数 / 浮点 / 定长字符串 / 引用 / 复合 / vlen);
* * 数据布局 v3(紧凑 / 连续 / 分块),分块数据集支持 deflate 压缩(zlib)。
*
* ⚠️ 不支持:外部数据文件、共享消息表、非 deflate 过滤器、极老的 v1 布局。
* 这些在 MATLAB 保存的文件里都不出现。
*/
class MatHdf5
{
public:
/** @brief 数据类型类别(对应 HDF5 的 class 编号)。 */
enum class TypeClass : int
{
Integer = 0,
Float = 1,
Time = 2,
String = 3,
Bitfield = 4,
Opaque = 5,
Compound = 6,
Reference = 7,
Enum = 8,
VariableLength = 9,
Array = 10
};
/** @brief 数据类型(只保留读取 MATLAB 数据所需的字段)。 */
struct Type
{
TypeClass cls = TypeClass::Opaque;
int version = 1;
std::size_t size = 0; /**< 单个元素占用的字节数。 */
bool isSigned = false; /**< 整数是否有符号。 */
bool bigEndian = false; /**< 字节序(MATLAB 一律小端)。 */
bool isVariableString = false; /**< vlen 字符串。 */
std::vector<std::pair<std::string, Type>> members; /**< 复合类型成员。 */
};
/** @brief 数据布局。 */
enum class LayoutClass
{
Compact = 0,
Contiguous = 1,
Chunked = 2
};
/** @brief 一个数据集的定义与数据。 */
struct Dataset
{
std::vector<std::size_t> dims; /**< 各维长度。 */
Type type; /**< 元素类型。 */
LayoutClass layout = LayoutClass::Compact;
std::vector<std::uint8_t> data; /**< 解码后的原始字节(行优先,按 `dims`)。 */
std::uint64_t chunkAddress = 0; /**< 分块数据集的 B-tree 地址。 */
std::vector<std::size_t> chunkDims;
bool deflate = false; /**< 是否使用了 deflate(zlib)过滤器。 */
std::uint64_t dataAddress = 0; /**< 连续布局的数据起始地址(已加基地址)。 */
std::uint64_t dataBytes = 0; /**< 连续布局的数据字节数。 */
/** 紧凑布局的字节(在布局消息里,数据量很小)。 */
std::vector<std::uint8_t> compact;
bool ok = false;
/** @brief 元素总数(各维长度相乘)。 */
std::uint64_t ElementCount() const
{
std::uint64_t count = 1;
for (const std::size_t dim : dims)
{
count *= dim;
}
return dims.empty() ? 0 : count;
}
};
/** @brief 读文件进度回调:`已读字节 / 总字节`。返回 false 表示请求取消。 */
using OpenProgress = std::function<bool(std::uint64_t bytesRead, std::uint64_t totalBytes)>;
/**
* @brief 打开文件(**随机读取**,不映射、不把整个文件读进堆内存)。
*
* 早期版本把整个文件一次读入 `std::vector`,后来改成整文件内存映射;
* 但映射在「文件被截断 / 网络共享掉线 / 移动介质拔出」时会触发页访问异常,
* 而这类异常属于 SEH,GCC/MinGW 下的 `try/catch` **捕获不到**,只能让进程崩溃。
*
* 因此这里改为 SEC2 式的「`ReadFile` 随机读 + 元数据块缓存」:
* * 所有读取都经过 `ReadAt`,I/O 失败只会变成 `false`(上层给出错误提示);
* * 对象头 / B-tree / 本地堆等热点元数据走 64KB 块缓存(LRU);
* * 数据段按 4MB 切片顺序读,并对小 chunk 做顺序预读。
* 解析代码本身与映射版完全一致。
*
* @param [IN] path 文件路径。
* @param [OUT] error 失败原因。
* @param [IN] progress 打开阶段的进度回调(可为空,只在上报总大小时调用一次)。
* @return 成功返回 true;用户取消时也返回 false。
*/
bool Open(const std::wstring& path, std::wstring& error,
const OpenProgress& progress = OpenProgress());
void Close();
/** @brief 析构时关闭文件句柄。 */
~MatHdf5() { Close(); }
/** @brief 文件字节数。 */
std::uint64_t FileBytes() const { return m_size; }
/** @brief 已从文件实际读取的字节数(用于「已读 / 总大小」进度)。 */
std::uint64_t BytesRead() const { return m_bytesRead; }
/**
* @brief 流式解码期间的字节进度回调。
*
* 每读一个块(或每 4MB 切片)调用一次;返回 false 表示请求取消。
*/
using ReadProgress = std::function<bool(std::uint64_t bytesRead, std::uint64_t totalBytes)>;
/** @brief 设置流式解码期间的字节进度回调(可为空)。 */
void SetReadProgress(const ReadProgress& progress) const { m_readProgress = progress; }
/** @brief 流式解码是否已被进度回调取消。 */
bool Cancelled() const { return m_readCancelled; }
/** @brief 根分组地址。 */
std::uint64_t Root() const { return m_root; }
/** @brief 该对象是否为数据集(有数据空间消息)。 */
bool IsDataset(std::uint64_t address) const;
/** @brief 分组(或结构体)的成员:`名字 → 对象头地址`。 */
std::vector<std::pair<std::string, std::uint64_t>> Members(std::uint64_t address) const;
/** @brief 按名字取成员(不存在返回 0)。 */
std::uint64_t Member(std::uint64_t address, const std::string& name) const;
/** @brief 读取数据集定义(含解码后的原始数据)。 */
bool ReadDataset(std::uint64_t address, Dataset& out) const;
/** @brief 只读取数据集定义(不解码数据;大数据用 `StreamDataset`)。 */
bool ReadDatasetInfo(std::uint64_t address, Dataset& out) const;
/**
* @brief 字节流回调:把数据集内容按块吐出(**不生成整块缓冲**)。
* @param [IN] bytes 块起始指针(仅在回调期间有效)。
* @param [IN] byteCount 块字节数。
* @param [IN] elementOffset 该块在数据集中的起始**元素**下标(行优先)。
* @return 返回 false 中止遍历。
*/
using ByteSink = std::function<bool(const std::uint8_t* bytes, std::size_t byteCount,
std::uint64_t elementOffset)>;
/**
* @brief 流式遍历数据集字节(连续布局按 4MB 切片;分块布局逐个 chunk 解压)。
* @param [IN] dataset `ReadDatasetInfo` 得到的信息。
* @param [IN] sink 字节回调。
* @return 成功返回 true。
*/
bool StreamDataset(const Dataset& dataset, const ByteSink& sink) const;
/** @brief 读取字符串属性(如 `MATLAB_class`、`MATLAB_fields`)。 */
std::string AttributeString(std::uint64_t address, const char* name) const;
/** @brief 读取 `char` 数据集为宽字符串(MATLAB 的 char 是 uint16)。 */
bool ReadText(std::uint64_t address, std::wstring& out) const;
/** @brief 读取数值数据集为 double 序列(流式转换,不生成整块字节缓冲)。 */
bool ReadNumbers(std::uint64_t address, std::vector<double>& out) const;
/**
* @brief 读取 Simulink SDI 的字节流数据集(`#data#`)为 double 序列。
*
* SDI 的 `#data#` 是 `uint8` 数据集,长度 = 采样数 × 8:按 8 字节一组重新解释为 double。
* 实现上逐 chunk 解压并直接写入输出缓冲(不产生整块字节数组,也不二次拷贝),
* 因此 GB 级文件也不会出现“文件 + 未压缩字节 + double”三份内存叠加。
*/
bool ReadDoubleStream(std::uint64_t address, std::vector<double>& out) const;
/** @brief 读取引用数据集(HDF5 对象引用 = 对象头地址,相对基地址)。 */
bool ReadReferences(std::uint64_t address, std::vector<std::uint64_t>& out) const;
private:
struct Message
{
std::uint16_t type = 0;
std::vector<std::uint8_t> data;
};
std::uint8_t U8(std::uint64_t offset) const;
std::uint16_t U16(std::uint64_t offset) const;
std::uint32_t U32(std::uint64_t offset) const;
std::uint64_t U64(std::uint64_t offset) const;
std::uint64_t Addr(std::uint64_t offset) const;
std::uint64_t Resolve(std::uint64_t address) const;
/** @brief 随机读取一段字节(不经过缓存;累加已读字节数)。 */
bool ReadAt(std::uint64_t offset, void* dst, std::size_t bytes) const;
/**
* @brief 取一段**元数据**的只读指针(经 64KB 块缓存)。
*
* ⚠️ 返回的指针只在下次调用前有效(缓存块可能被替换),调用方必须立即用完。
* @return 成功返回指针;越界或读失败返回 nullptr。
*/
const std::uint8_t* Cached(std::uint64_t offset, std::size_t bytes) const;
/** @brief 判断 `offset` 处的 4 字节块标识是否等于 `id`(`id` 为 4 字符)。 */
bool Tag(std::uint64_t offset, const char* id) const;
/**
* @brief 读取一个 NUL 结尾的短字符串(本地堆里的成员名)。
*
* 名字很短但可能跨越 64KB 缓存块边界,因此按块边界截断、必要时继续向后读。
*/
void ReadCString(std::uint64_t offset, std::string& out) const;
/** @brief 丢失顺序预读缓冲(随机跳转后调用)。 */
void DropReadAhead() const;
/**
* @brief 从文件读一段**数据**(带顺序预读,供小 chunk 连续读取)。
*
* 若请求落在当前预读窗口内直接返回指针;否则从 `offset` 起预读最多 4MB。
* @return 成功返回指针(仅在下次调用前有效),否则 nullptr。
*/
const std::uint8_t* ReadData(std::uint64_t offset, std::size_t bytes) 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,
std::size_t& consumed) const;
void WalkBtree(std::uint64_t node, std::uint64_t heapData,
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
/** @brief 元数据缓存槽(固定槽数,最近最少使用优先淘汰)。 */
struct CacheSlot
{
std::uint64_t base = 0;
std::uint32_t length = 0;
std::uint64_t stamp = 0;
std::vector<std::uint8_t> bytes;
};
void* m_fileHandle = nullptr; /**< `HANDLE`(避免头文件引入 windows.h)。 */
std::uint64_t m_size = 0; /**< 文件字节数。 */
mutable std::uint64_t m_bytesRead = 0; /**< 已实际读取的字节数(进度上报用)。 */
mutable std::vector<CacheSlot> m_cache; /**< 元数据块缓存。 */
mutable std::uint64_t m_stamp = 0; /**< 缓存访问计数。 */
mutable std::size_t m_lastSlot = 0; /**< 上次命中的槽(连续标量读取的快路径)。 */
mutable std::vector<std::uint8_t> m_readAhead; /**< 顺序预读缓冲。 */
mutable std::uint64_t m_readAheadBase = 0;
mutable std::uint32_t m_readAheadLength = 0;
mutable bool m_readAheadValid = false;
mutable std::vector<std::uint8_t> m_slice; /**< 连续布局切片缓冲(复用容量)。 */
mutable std::vector<std::uint8_t> m_chunk; /**< 大于预读窗口的分块缓冲(复用容量)。 */
mutable ReadProgress m_readProgress; /**< 流式解码进度回调(可空)。 */
mutable bool m_readCancelled = false; /**< 进度回调请求取消。 */
std::uint64_t m_base = 0;
std::uint64_t m_root = 0;
int m_offsetSize = 8;
int m_lengthSize = 8;
mutable std::vector<std::pair<std::uint64_t, std::uint64_t>> m_heaps; /**< 堆地址 → 数据地址。 */
};
} // namespace jdi
#endif // __MAT_H5_HPP__