Files
Just-DataInspector/Utilities/Inc/MatH5.hpp
T
DeepSeek V4.1 Flash 53b981a2a9 全项目大检查:修复数据层/UI 真 BUG、清理死代码与过期注释、并评估二进制体积
按「性能 / 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 条硬约束
与文末本轮小节(含体积评估数据)。
2026-10-20 16:10:29 +08:00

296 lines
13 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;
};
/**
* @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__