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 正在被用户运行的程序占用,链接步骤未覆盖它。)
165 lines
5.9 KiB
C++
165 lines
5.9 KiB
C++
#ifndef __DATA_TABLE_HPP__
|
|
#define __DATA_TABLE_HPP__
|
|
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace jdi
|
|
{
|
|
|
|
/**
|
|
* @brief 一列数值的「值 → 文本」枚举表(如 ASAM MDF v4 的 `cc_type = 7` 换算)。
|
|
*
|
|
* 它描述的是**信号本身的取值含义**(例如 `0 = Off`、`1 = On`),
|
|
* 与单位、注释、物理量纲无关,因此这里刻意命名为 `enumText` 而不是 `unit` / `label`。
|
|
*
|
|
* 空表(`texts.empty()`)表示该列没有枚举信息,界面必须保持与以前完全一致的显示。
|
|
*/
|
|
struct SignalEnumText
|
|
{
|
|
/** @brief 枚举取值(与采样列同一量纲,通常是整数)。 */
|
|
std::vector<double> values;
|
|
|
|
/** @brief 与 `values` 一一对应的显示文本。 */
|
|
std::vector<std::wstring> texts;
|
|
|
|
/** @brief 是否没有任何枚举信息。 */
|
|
bool Empty() const { return values.empty() || texts.size() != values.size(); }
|
|
|
|
/**
|
|
* @brief 按取值查找枚举文本。
|
|
* @param [IN] value 取值(物理值,未叠加界面「调整」变换)。
|
|
* @return 命中返回对应文本指针,未命中返回 nullptr。
|
|
*/
|
|
const std::wstring* Find(double value) const
|
|
{
|
|
if (Empty() || !(value == value)) // NaN 永远不命中
|
|
{
|
|
return nullptr;
|
|
}
|
|
for (std::size_t index = 0; index < values.size(); ++index)
|
|
{
|
|
if (values[index] == value)
|
|
{
|
|
return &texts[index];
|
|
}
|
|
}
|
|
// 采样值按 double 保存,整数枚举在任意位宽下都是精确可表示的,
|
|
// 但个别工具会写 `1.0000001` 这类带噪声的换算表:再做一次四舍五入匹配。
|
|
const double rounded = std::nearbyint(value);
|
|
if (rounded == value)
|
|
{
|
|
return nullptr;
|
|
}
|
|
for (std::size_t index = 0; index < values.size(); ++index)
|
|
{
|
|
if (std::nearbyint(values[index]) == rounded)
|
|
{
|
|
return &texts[index];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @brief 内存中的**列优先**数据表(与具体文件格式无关)。
|
|
*
|
|
* 这是「从任意格式读出来的原始格子」:一行是表头,其余是各列的数值。
|
|
* 它**不是**应用的数据模型(不含时间轴语义、不含信号名规范化),
|
|
* 只是各格式加载器(CSV / MF4 / 以后更多格式)与数据模型之间的中转结构。
|
|
*
|
|
* 采用列优先(每列一整块连续内存)而不是行优先的二维数组,原因有二:
|
|
* 1. 大型文件(百万行级)若按行存储会产生上百万次独立堆分配,
|
|
* 每行还要额外承担 vector 的控制块与容量对齐开销,实测内存放大接近 1.5 倍;
|
|
* 2. 构建信号时可以直接把整列**移动**进 `SignalData`,不再逐元素拷贝。
|
|
*/
|
|
struct DataTable
|
|
{
|
|
std::vector<std::wstring> headers; /**< 列名(已去除引号与首尾空白)。 */
|
|
std::vector<std::vector<double>> columns; /**< 列优先数值矩阵:columns[列][行],缺失处为 NaN。 */
|
|
/**
|
|
* @brief 与 `columns` 同下标的「值 → 文本」枚举表(没有枚举信息的列为空)。
|
|
*
|
|
* 加载器只在源格式自带枚举文本(如 MDF4 `cc_type = 7`)时填充;
|
|
* 它不是单位、不是注释,界面只在光标读数处以尾缀形式展示。
|
|
*/
|
|
std::vector<SignalEnumText> columnEnumText;
|
|
std::size_t rowCount = 0; /**< 有效数据行数。 */
|
|
wchar_t delimiter = L','; /**< 实际使用的分隔符(仅文本格式有意义)。 */
|
|
unsigned long long sourceBytes = 0; /**< 源文件字节数(用于按比例预留容量)。 */
|
|
|
|
/**
|
|
* @brief 判断解析结果是否包含可用的表格数据。
|
|
* @return 存在列名且存在数据行时返回 true。
|
|
*/
|
|
bool HasData() const { return !headers.empty() && (rowCount > 0) && (columns.size() >= headers.size()); }
|
|
};
|
|
|
|
/**
|
|
* @brief 加载进度信息。
|
|
*
|
|
* 解析阶段会携带真实的"已读 / 总字节数";列重组与统计阶段没有字节可计,
|
|
* 此时 `hasBytes` 为 false,界面应只展示百分比。
|
|
*/
|
|
struct LoadProgressInfo
|
|
{
|
|
double ratio = 0.0; /**< 0-1 总体完成比例。 */
|
|
unsigned long long bytesRead = 0; /**< 已读取字节数。 */
|
|
unsigned long long totalBytes = 0; /**< 文件总字节数。 */
|
|
bool hasBytes = false; /**< 上述字节数字段是否有效。 */
|
|
};
|
|
|
|
/**
|
|
* @brief 加载进度回调。
|
|
*
|
|
* 在解析过程中按块回调;返回 false 表示请求取消,解析会尽快停止并返回失败。
|
|
*/
|
|
using LoadProgress = std::function<bool(const LoadProgressInfo& info)>;
|
|
|
|
/**
|
|
* @brief 一组**共享同一时间轴**的数据列(一个「时间轴块」)。
|
|
*
|
|
* 一个源文件可以包含多个这样的块:CSV 里每个 `time` 列都会开启一个新块,
|
|
* MF4 里每个测量通道组(CG)是一个块。块内的各列长度一致(都等于本块的行数)。
|
|
*/
|
|
struct TableBlock
|
|
{
|
|
std::wstring timeHeader; /**< 本块时间列的列名(原样保留,如 `time`)。 */
|
|
DataTable table; /**< 本块的列集合(**含**时间列本身)。 */
|
|
};
|
|
|
|
/**
|
|
* @brief 一个源文件加载后的全部内容(可能包含多个时间轴块)。
|
|
*
|
|
* ⚠️ 这是「加载器 → 数据模型」的唯一接口:加载器只负责把文件读成
|
|
* `LoadedSource`(纯格子,无应用语义),`BuildSignalGroup` 再把它规范化为
|
|
* `SignalGroup`(含时间轴去重、信号命名、调色板等应用语义)。
|
|
* 新增文件格式时**只需新增一个加载器**,数据模型与界面代码完全不用改。
|
|
*/
|
|
struct LoadedSource
|
|
{
|
|
std::wstring sourceName; /**< 源文件名(不含目录,含扩展名)。 */
|
|
std::vector<TableBlock> blocks; /**< 时间轴块集合(至少一个)。 */
|
|
|
|
/** @brief 是否包含可用的数据。 */
|
|
bool HasData() const
|
|
{
|
|
for (const TableBlock& block : blocks)
|
|
{
|
|
if (block.table.HasData())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
} // namespace jdi
|
|
|
|
#endif // __DATA_TABLE_HPP__
|