按「性能 / BUG / 错误 / 过期注释 / 命名与实际含义不符」四项对全仓做了一次审查, 四个只读子审查交叉核对后逐条回到源码确认再改。 一、数据层真 BUG - MatH5:U8/U16/U32/U64 用逐字节 Cached() 拼装,而 Cached 只服务「完全落在同一个 64KB 块内」的请求 => 跨块时静默读成 0(U32 整段为 0、U64 高 32 位为 0),分块 B-tree 的 chunk 偏移/子节点地址被破坏且无任何报错。新增 ScalarLE():命中缓存用缓存, 否则退回一次 ReadAt;U64 顺带从 8 次缓存查找降到 1 次。 - Mf4Loader::IsBlock 未校验链接表是否落在块内 => DataOffset 越界,随后 block_len - (data - offset) 在 uint64 上下溢,得到一个约 2^64 的"可用字节数", 下游 vector(usable+1) 直接 bad_alloc。同时把 offset + bytes > size 改成不产生回绕的写法。 - DecodeRawValue 的整数路径:bit_offset%8 != 0 且位宽 64 时需 9 个字节,先拼成 64 位 再整体右移会把最高字节静默丢弃 => 改为逐字节带位偏移累加。 - cg_cycle_count 加「数据段总字节 / 记录字节」上限(压缩段按 deflate 最大膨胀比估算)。 - CsvLoader 表头分支同样跳过 #/% 注释行(否则整份文件按 1 列解析并被丢弃)。 - MatLoader:double -> int 的越界转换加了范围检查;普通对象递归加深度上限与成环检测。 - Utf8Util::BuildTicks:double -> long long 越界转换前先判商是否落在 ±9e18 内。 二、界面 BUG - 工具栏按钮永远不显示悬停高亮:Paint 读的 Button::hover 全仓从未赋值,改为按 m_hoverIndex 判断。 - 菜单打开时"取消底层拖拽"对每个消息都执行,把菜单自己的 Target::EDIT_FIELD 也清掉 => 菜单内联输入框无法拖拽框选。排除 EDIT_FIELD。 - GdiplusShutdown 之前未释放 PlotView 的 GDI+ 位图(Application 是进程单例,成员析构在 WinMain 之后)=> 新增 PlotView::ReleaseGraphicsResources() 并在 Shutdown 里调用。 - WM_KILLFOCUS 现在会提交标签页/绘图区编辑并复位捕获与拖拽状态。 - WM_LBUTTONUP 不再抢走中键平移的鼠标捕获。 - TextEdit:被过滤器拒绝的字符返回 false;无选区时 Ctrl+X 只复制、不再清空整段文本。 - SignalPanel::InvalidateRows 作废编辑/待确认/改名计时等行下标状态。 - EndFilterEdit 改走 ApplyFilterText(否则输入纯空格后回车,列表仍停在筛选结果)。 - 新增 SignalBadgeVisible 作为唯一判据(原来命中只看"可见",绘制还要求"已分配网格")。 - 折起动画结束后夹取滚动量;双击分组名改名时清掉该组正在播放的折起动画。 - ScrollToSignal 复用 SignalScrollTarget(含悬浮组名避让)。 - RowNameRect 与绘制使用同一最小宽度(原来退化时返回空矩形)。 - 分隔条拖拽上限改用与 Relayout 同一公式;UpdateDragCursor 补 SPLITTER/EDIT_FIELD。 - SignalPanel 里几个守卫改为判断真正被解引用的指针;分组行 ✕ 也返回手型光标。 三、PlotView - FindStyleOnlyLanes 改用完整的 SceneStyleEntry::operator==(原来只比 4 个字段, 另一个网格同时改"调整"时带着旧像素被当成最新)。 - 宽基底图层不再烘焙拖拽光晕(LaneLayerHash 不含它),改由 PaintHoverHighlight 每帧叠加。 - PaintLaneLegend 提前返回前清掉本网格的命中矩形(否则能点中看不见的图例条目)。 - 面板主体改为完全不透明(消除平移后 4% 透出的旧波形,零额外代价)。 - PAN / Shift+滚轮改走 SetTimeView,使"自动点线"判据随平移重算。 - 内联输入框命中字体与绘制统一为 monoSmall。 - 派生曲线取值与窗口按 SourceIndexFor 映射(水平镜像时不再读到镜像另一端)。 - EdgePanelWidth 补 style/axis 空指针守卫;二分结果夹到 count。 四、清理 - 删除 Toolbar::ActionLabel、Theme::statusBg、PopupMenu 的 editResult 死机制、 Toolbar::Layout 的重复 clear()、HitRow 的恒假分支、OnDoubleClick 的重复判定、 PaintUtil 里孤立的注释块。 - 修正一批过期/错误注释(包络带"不做加宽"、LaneLayerHash 的图例说法、 均值滤波曲线的窗口说法、样式区行数与「调整」按钮顺序、FIT_ALL 退出缩放模式、 RemoveSignal 的 ✕ 位置、.mat/.dat 格式文档、MatH5 的"映射视图"、DataModel 的 "点线/被移除文件"、SignalPanel 的"行高连续缩小"、TextEdit 的 PaintUtil 引用等)。 五、二进制体积评估(按要求只评估,未做压缩修改) - release 809,472 字节;.text 652KB、.xdata+.pdata 约 75KB、.rdata 42KB、 .rsrc 20KB、.idata 11.5KB。 - 去掉 -ld3d11 -ldwmapi -ld3dcompiler(仓内无任何调用)实测 0 字节收益 (静态库只拉取被引用的对象)=> 删链接库不是压缩手段,只是配置整洁。 - 仓内已无 <iostream>/<sstream>/<filesystem>/<regex>/<locale> 等体积炸弹。 - 结论:已无明显的浪费可捡;要显著变小只能改变取舍(动态 CRT / UPX / 砍功能), 需要用户决策,本轮未动。 验证 - editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、convcheck 18/18。 - check 136/137:唯一失败项经 git stash 回到改动前复现同一数值,属该探针自身的既有 不稳定项(取决于前置用例留下的缓存/时序),与本轮改动无关。 - 真实数据:4 个 MF4 => 283/8/18/24 个信号;.mat 样例 8 信号、 带无障碍对照-防夹力曲线数据 8 信号、BIG_MAT.mat 5 信号(私有内存 +741MB)。 - make release / make debug 零警告。 文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 171-180 条硬约束 与文末本轮小节(含体积评估数据)。
296 lines
13 KiB
C++
296 lines
13 KiB
C++
#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;
|
||
};
|
||
|
||
/**
|
||
* @brief 读取一个小端标量(1/2/4/8 字节)。
|
||
*
|
||
* ⚠️ **不能**用 `U8` 逐字节拼:`Cached` 只服务「完全落在同一个 64KB 块内」的请求,
|
||
* 跨块时返回 nullptr,逐字节拼接就会把缺失的字节当成 0(`U32` 整段为 0、`U64` 高 32 位为 0),
|
||
* 于是 B-tree 的 chunk 偏移 / 子节点地址被静默读错。这里统一走 `ScalarLE`:
|
||
* 命中缓存就用缓存,否则退回一次 `ReadAt`。
|
||
*/
|
||
std::uint64_t ScalarLE(std::uint64_t offset, std::size_t bytes) const;
|
||
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__
|