Files
Just-DataInspector/Utilities/Inc/MatH5.hpp
T
DeepSeek V4.1 Flash 75f0fafd41 .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)。
2026-10-19 20:13:17 +08:00

238 lines
9.6 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`(注释里假设“`.mat` 通常几十 MB”),
* 但 SDI 导出的录波文件可能是 **GB 级**:整文件读入 + 分块解码 + 再拷成 double 会同时
* 驻留“文件 + 未压缩字节 + double”三份内存。现改为:
* * 映射文件(只占虚拟地址空间,按页面需求驻留);
* * 分块数据集逐 chunk 解压并**直接写入**自持有的 double 列(见 `ReadDoubleStream`);
* * 进度回调在映射完成后立即报告一次(字节已全部可访问),后续解码阶段由 `MatLoader` 驱动。
*
* @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 static_cast<std::uint64_t>(m_data.size()); }
/** @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;
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 内存映射的文件视图(代替“整个文件读进 `vector`”)。
*
* ⚠️ 行为与 `std::vector<std::uint8_t>` 的**只读子集**一致(`size/data/begin/end/[]`),
* 因此所有“按偏移取字节”的解析代码一行都不用改;但**虚拟地址不等于物理内存**:
* 只有真正被解析到的页才驻留,GB 级文件不会再先吃掉一整个文件大小的私有内存。
*/
struct MappedBytes
{
const std::uint8_t* pointer = nullptr;
std::size_t length = 0;
const std::uint8_t* data() const { return pointer; }
std::size_t size() const { return length; }
const std::uint8_t& operator[](std::size_t index) const { return pointer[index]; }
const std::uint8_t* begin() const { return pointer; }
const std::uint8_t* end() const { return pointer + length; }
void clear()
{
pointer = nullptr;
length = 0;
}
};
MappedBytes m_data; /**< 文件映射视图(只读;语义同字节数组)。 */
void* m_fileHandle = nullptr; /**< `HANDLE`(避免头文件引入 windows.h)。 */
void* m_mapHandle = nullptr;
std::uint64_t m_base = 0;
std::uint64_t m_root = 0;
int m_offsetSize = 8;
int m_lengthSize = 8;
mutable std::vector<std::pair<std::uint64_t, std::uint64_t>> m_heaps; /**< 堆地址 → 数据地址。 */
};
} // namespace jdi
#endif // __MAT_H5_HPP__