// MATLAB `.mat`(v7.3 / HDF5)加载器。 // // 支持两种内容(详见 MatLoader.hpp 的说明): // * Simulink SDI / `SimulationData.Dataset` 导出:`#refs#` 里的元素图 + `#sigstream#` 的样本流; // * 普通 v7.3 变量:数值矩阵按"列 = 信号、行 = 采样"读取(时间轴 = 采样序号)。 #include "MatLoader.hpp" #include "MatH5.hpp" #include #include #include #include #include #include #include #include namespace jdi { namespace { /** @brief 一个 SDI 信号(或子数据集)的解析结果。 */ struct SdiElement { bool isSignal = false; bool isDataset = false; std::wstring name; std::vector children; /**< 子数据集元素的引用目标。 */ int dataStream = -1; /**< 数据流下标。 */ int timeStream = -1; /**< 时间流下标。 */ }; std::wstring TrimNumber(double value) { wchar_t buffer[64] = {}; if (value == static_cast(static_cast(value))) { std::swprintf(buffer, 64, L"%lld", static_cast(value)); } else { std::swprintf(buffer, 64, L"%g", value); } return buffer; } /** * @brief 把 SDI 里读到的 `DataR2` / `TimeR2` 数值转成流下标。 * * ⚠️ 不能直接 `static_cast(double)`:文件里那个数可能是 NaN、无穷或超出 `int` 范围, * 而浮点 → 整型的越界转换在 C++ 里是**未定义行为**(x86-64 上会得到 `INT_MIN` 之类的 * 垃圾下标,随后拿去拼数据集名)。非法值统一返回 -1(= 没有流)。 */ int StreamIndexFromDouble(double value) { if (!std::isfinite(value) || value < 0.0 || value > 2147483647.0) { return -1; } return static_cast(value); } /** @brief 去掉文件名中的扩展名。 */ std::wstring StemOf(const std::wstring& path) { const std::size_t slash = path.find_last_of(L"\\/"); std::wstring name = (slash == std::wstring::npos) ? path : path.substr(slash + 1); const std::size_t dot = name.find_last_of(L'.'); if (dot != std::wstring::npos && dot > 0) { name.resize(dot); } return name; } /** @brief 读取一个「流」的全部 double 样本(`#sigstream#//#data#`)。 */ bool ReadStream(const MatHdf5& file, std::uint64_t streamObject, std::vector& out) { out.clear(); const std::uint64_t data = file.Member(streamObject, "#data#"); if (data == 0) { return false; } // ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8), // 而不是 double 数据集:必须按 8 字节一组重新解释成 double。 // `ReadDoubleStream` 会逐 chunk 解压并**直接写入** `out`: // 不生成“整个数据集的未压缩字节”,也不做第二次拷贝(GB 级文件的关键)。 return file.ReadDoubleStream(data, out) && !out.empty(); } /** @brief 解析 `#refs#` 里的一个元素(信号 / 子数据集)。 */ void ParseElement(const MatHdf5& file, std::uint64_t address, SdiElement& element) { element = SdiElement(); std::wstring elementType; file.ReadText(file.Member(address, "ElementType"), elementType); file.ReadText(file.Member(address, "Name"), element.name); const std::uint64_t values = file.Member(address, "Values"); if (elementType == L"dataset") { element.isDataset = true; if (values != 0) { file.ReadReferences(values, element.children); } return; } element.isSignal = true; if (values == 0) { return; } // 名字为空时退回 timeseries 自己的 Name。 if (element.name.empty()) { file.ReadText(file.Member(values, "Name"), element.name); } std::vector numbers; if (file.ReadNumbers(file.Member(values, "DataR2"), numbers) && !numbers.empty()) { element.dataStream = StreamIndexFromDouble(numbers.front()); } numbers.clear(); if (file.ReadNumbers(file.Member(values, "TimeR2"), numbers) && !numbers.empty()) { element.timeStream = StreamIndexFromDouble(numbers.front()); } } /** @brief 追加一条信号(时间 + 数值)到某个分组的时间轴块集合。 */ void AppendSignal(LoadedSource& source, const std::wstring& name, std::vector&& time, std::vector&& values) { if (time.empty() || values.empty()) { return; } TableBlock block; block.timeHeader = L"time"; DataTable& table = block.table; table.headers.push_back(L"time"); table.headers.push_back(name); table.rowCount = std::min(time.size(), values.size()); time.resize(table.rowCount); values.resize(table.rowCount); table.columns.push_back(std::move(time)); table.columns.push_back(std::move(values)); source.blocks.push_back(std::move(block)); } /** @brief 加载进度上报器(`.mat` 解析拆成“读文件”与“解码每个样本流”两段)。 */ struct ProgressReporter { const LoadProgress* sink = nullptr; /**< 目标回调(可空)。 */ double low = 0.0; /**< 本阶段占的进度区间下界。 */ double high = 1.0; /**< 本阶段占的进度区间上界。 */ std::uint64_t read = 0; /**< 已读字节。 */ std::uint64_t total = 0; /**< 总字节。 */ mutable double lastRatio = -1.0; /**< 已上报的最大比例(保证单调不回退)。 */ /** @brief 上报一个 0-1 的阶段内进度(向宿主传递字节数与百分比)。 */ bool Report(double fraction) const { if (sink == nullptr || !(*sink)) { return true; } if (fraction < 0.0) { fraction = 0.0; } if (fraction > 1.0) { fraction = 1.0; } double ratio = low + (high - low) * fraction; // 「已读字节」与「逐流解码」是两条独立进度来源:取较大值,避免进度条回退。 if (ratio < lastRatio) { ratio = lastRatio; } else { lastRatio = ratio; } LoadProgressInfo info; info.ratio = ratio; // 总字节已知时一并上报(遮罩层据此显示“已加载 / 总大小”)。 info.hasBytes = (total > 0); info.bytesRead = (total > 0) ? std::min(read, total) : 0; info.totalBytes = total; return (*sink)(info); } }; /** @brief SDI(`SimulationData.Dataset`)结构的分组读取。 */ bool LoadSdi(const MatHdf5& file, std::uint64_t refs, std::uint64_t sigstream, const std::wstring& stem, std::vector& groups, std::wstring& error, const ProgressReporter& reporter) { // 1) 枚举 `#refs#` 里的全部元素。 std::map elements; for (const auto& member : file.Members(refs)) { if (file.IsDataset(member.second)) { continue; } SdiElement element; ParseElement(file, member.second, element); if (element.isSignal || element.isDataset) { elements[member.second] = std::move(element); } } if (elements.empty()) { error = L"文件中没有 Simulink 数据元素。"; return false; } // 2) 找出"根"数据集:没有被任何其它数据集当作子元素引用的那些。 std::set childAddresses; for (const auto& entry : elements) { for (const std::uint64_t child : entry.second.children) { childAddresses.insert(child); } } std::vector roots; for (const auto& entry : elements) { if (!entry.second.isDataset) { continue; } if (childAddresses.count(entry.first) == 0) { roots.push_back(entry.first); } } if (roots.empty()) { // 没有任何子数据集关系:所有信号归入同一组。 for (const auto& entry : elements) { if (entry.second.isSignal) { roots.push_back(entry.first); } } } // 3) 深度优先遍历:路径上的数据集名字用 `-` 连接成分组名。 struct Pending { std::uint64_t address = 0; std::vector path; int depth = 0; }; std::vector stack; for (const std::uint64_t root : roots) { const auto found = elements.find(root); if (found == elements.end()) { continue; } Pending pending; pending.address = root; if (found->second.isDataset && !found->second.name.empty()) { pending.path.push_back(found->second.name); } stack.push_back(std::move(pending)); } // ⚠️ **先收集信号清单、再解码**(用户 2026-10-19 要求:边解析边填充): // * 旧实现把**每条**信号的时间列和时间流各拷一份(`streams` 缓存 + `AppendSignal` // 里的整份复制)⇒ GB 级文件会出现“文件 + 解压字节 + 每条信号两份 double”叠加; // * 现在按「分组 + 时间流」合并成一个 `TableBlock`:时间列只存一份、信号列直接 // `std::move` 进数据模型,且每条数据流解码完立刻进入最终缓冲。 struct PendingSignal { std::wstring groupName; std::wstring signalName; int timeStream = -1; int dataStream = -1; }; std::vector signals; int unnamedSignals = 0; while (!stack.empty()) { Pending pending = std::move(stack.back()); stack.pop_back(); const auto found = elements.find(pending.address); if (found == elements.end()) { continue; } const SdiElement& element = found->second; if (element.isDataset) { for (const std::uint64_t child : element.children) { Pending next; next.address = child; next.path = pending.path; const auto childElement = elements.find(child); if (childElement != elements.end() && childElement->second.isDataset && !childElement->second.name.empty()) { next.path.push_back(childElement->second.name); } stack.push_back(std::move(next)); } continue; } if (!element.isSignal || element.dataStream < 0) { continue; } PendingSignal signal; for (const std::wstring& piece : pending.path) { if (!signal.groupName.empty()) { signal.groupName += L"-"; } signal.groupName += piece; } if (signal.groupName.empty()) { signal.groupName = stem; } signal.signalName = element.name; if (signal.signalName.empty()) { signal.signalName = L"信号" + std::to_wstring(++unnamedSignals); } signal.timeStream = element.timeStream; signal.dataStream = element.dataStream; signals.push_back(std::move(signal)); } // 2) 解码:每个(分组,时间流)一个时间轴块;每条数据流只解码一次并直接进列。 std::map groupIndex; std::map, std::size_t> blockIndex; std::size_t decoded = 0; const auto reportStream = [&]() { ++decoded; const double fraction = (signals.empty()) ? 1.0 : static_cast(decoded) / static_cast(signals.size()); return reporter.Report(std::min(1.0, fraction)); }; for (PendingSignal& signal : signals) { // 分组(每个分组名对应一个 `LoadedSourceGroup`)。 std::size_t groupSlot = 0; const auto existing = groupIndex.find(signal.groupName); if (existing == groupIndex.end()) { LoadedSourceGroup group; group.name = signal.groupName; group.source.sourceName = signal.groupName; groups.push_back(std::move(group)); groupSlot = groups.size() - 1; groupIndex.emplace(signal.groupName, groupSlot); } else { groupSlot = existing->second; } LoadedSource& source = groups[groupSlot].source; // 数据列:直接解码到最终 vector(不再经过 `Dataset.data` 的整块字节)。 std::vector values; const std::uint64_t dataObject = file.Member(sigstream, std::to_string(signal.dataStream)); if (dataObject == 0 || !ReadStream(file, dataObject, values) || values.empty()) { if (!reportStream()) { return false; } continue; } // 找(或建)该信号所属的时间轴块。 const std::pair key{signal.groupName, signal.timeStream}; std::size_t blockSlot = 0; const auto blockFound = blockIndex.find(key); if (blockFound == blockIndex.end()) { TableBlock block; block.timeHeader = L"time"; std::vector time; bool haveTime = false; if (signal.timeStream >= 0) { const std::uint64_t timeObject = file.Member(sigstream, std::to_string(signal.timeStream)); haveTime = timeObject != 0 && ReadStream(file, timeObject, time) && !time.empty(); } if (!haveTime) { // 时间流缺失:退化为采样序号(秒),保证信号仍可显示(长度在下面裁剪)。 time.resize(values.size()); for (std::size_t index = 0; index < values.size(); ++index) { time[index] = static_cast(index); } } // ⚠️ **时间轴按内容去重**:SDI 里每条 timeseries 都有自己的时间流, // 但内容通常完全一致。若同一分组里已经有一模一样的时间列,就直接复用它 //(否则每条信号各存一份时间轴:1.29GB / 5 信号的文件会多占 ~500MB)。 std::size_t reused = SIZE_MAX; for (std::size_t index = 0; index < source.blocks.size(); ++index) { const std::vector& existingTime = source.blocks[index].table.columns[0]; if (existingTime.size() != time.size()) { continue; } if (time.empty() || std::memcmp(existingTime.data(), time.data(), time.size() * sizeof(double)) == 0) { reused = index; break; } } if (reused != SIZE_MAX) { blockIndex.emplace(key, reused); blockSlot = reused; } else { block.table.headers.push_back(block.timeHeader); block.table.columns.push_back(std::move(time)); source.blocks.push_back(std::move(block)); blockSlot = source.blocks.size() - 1; blockIndex.emplace(key, blockSlot); } } else { blockSlot = blockFound->second; } TableBlock& block = source.blocks[blockSlot]; // 时间列长度与信号不一致(损坏 / 异构)⇒ 单独成块,用采样序号当时间, // 保持“每条信号都能显示”的既有语义,而不是把数据截掉或错位。 if (block.table.columns[0].size() != values.size()) { TableBlock own; own.timeHeader = L"time"; std::vector time(values.size()); for (std::size_t index = 0; index < values.size(); ++index) { time[index] = static_cast(index); } own.table.headers.push_back(own.timeHeader); own.table.columns.push_back(std::move(time)); own.table.headers.push_back(signal.signalName); own.table.columns.push_back(std::move(values)); source.blocks.push_back(std::move(own)); if (!reportStream()) { return false; } continue; } block.table.headers.push_back(signal.signalName); block.table.columns.push_back(std::move(values)); if (!reportStream()) { return false; } } // 3) 统一行数:同一块内取各列最小长度(时间流与数据流长度不一致时按最小裁剪)。 for (LoadedSourceGroup& group : groups) { for (TableBlock& block : group.source.blocks) { std::size_t rows = SIZE_MAX; for (const std::vector& column : block.table.columns) { rows = std::min(rows, column.size()); } if (rows == SIZE_MAX) { rows = 0; } for (std::vector& column : block.table.columns) { if (column.size() > rows) { column.resize(rows); } } block.table.rowCount = rows; } } if (groups.empty()) { error = L"文件中没有可用的信号数据。"; return false; } return true; } /** * @brief 普通 v7.3 变量:数值矩阵 / 结构体。 * * ⚠️ 必须带**深度上限**与**已访问集合**:HDF5 允许硬链接成环,损坏文件也可能构造出 * 极深嵌套,无保护的成员递归会直接栈溢出(崩溃,无错误提示)。 */ void LoadPlainObject(const MatHdf5& file, std::uint64_t address, const std::wstring& name, LoadedSource& source, int depth = 0, std::set* visited = nullptr) { constexpr int kMaxDepth = 32; if (depth > kMaxDepth) { return; } std::set localVisited; std::set& seen = (visited != nullptr) ? *visited : localVisited; if (address != 0 && !seen.insert(address).second) { return; // 成环:已经展开过这个对象 } if (file.IsDataset(address)) { // ⚠️ 只解析元信息(维度),**不要**用 `ReadDataset`:那会把整块数据再解码一遍。 // 旧实现先 `ReadNumbers` 解码一次、再 `ReadDataset` 只为拿 `dims`, // 大矩阵的时间与内存都翻倍。 MatHdf5::Dataset dataset; if (!file.ReadDatasetInfo(address, dataset) || dataset.dims.empty()) { return; } std::vector numbers; if (!file.ReadNumbers(address, numbers) || numbers.empty()) { return; } std::size_t rows = 1; std::size_t columns = 1; if (dataset.dims.size() >= 2) { rows = dataset.dims[0]; columns = dataset.dims[1]; } else if (dataset.dims.size() == 1) { rows = dataset.dims[0]; } if (rows == 0 || columns == 0 || rows * columns != numbers.size()) { return; } // 列 = 信号、行 = 采样(与 MATLAB 的列优先存储一致)。 for (std::size_t column = 0; column < columns; ++column) { std::vector values(rows); for (std::size_t row = 0; row < rows; ++row) { values[row] = numbers[column * rows + row]; } std::vector time(rows); for (std::size_t row = 0; row < rows; ++row) { time[row] = static_cast(row); } const std::wstring signalName = (columns == 1) ? name : (name + L"(" + TrimNumber(static_cast(column + 1)) + L")"); AppendSignal(source, signalName, std::move(time), std::move(values)); } return; } // 结构体:字段各自作为信号(子结构体递归)。 for (const auto& member : file.Members(address)) { if (member.first.empty()) { continue; } std::wstring childName(member.first.begin(), member.first.end()); LoadPlainObject(file, member.second, childName, source, depth + 1, &seen); } } } // namespace bool LoadMatSourceGroups(const std::wstring& filePath, std::vector& groups, std::wstring& error, const LoadProgress& progress) { error.clear(); groups.clear(); MatHdf5 file; // ------------------------------------------------------------------ // 进度拆成三段: // 0.00 – 0.02 打开文件(立刻知道总大小;遮罩层显示「0 / 总大小」) // 0.02 – 0.98 随机读取 + 逐条解码样本流(进度按**实际已读字节**推进) // 0.98 – 1.00 收尾 // ⚠️ 旧实现是内存映射:字节瞬间「全部可访问」⇒ 文件读取百分比立刻 100%, // 而真正的解析还要跑很久(用户 2026-10-20 报告“这部分不太对劲”)。 // 现在每次读盘都累计到 `MatHdf5::BytesRead()`,百分比随真实 I/O 推进。 // ------------------------------------------------------------------ ProgressReporter parseReporter; parseReporter.sink = &progress; parseReporter.low = 0.02; parseReporter.high = 0.98; const MatHdf5::OpenProgress openProgress = [&parseReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool { parseReporter.read = bytesRead; parseReporter.total = totalBytes; return parseReporter.Report(totalBytes > 0 ? static_cast(bytesRead) / static_cast(totalBytes) : 0.0); }; if (!file.Open(filePath, error, openProgress)) { return false; // 用户取消时 `error` 为空 } // 流式解码期间的字节进度(每读一块回调一次);返回 false 即取消。 file.SetReadProgress( [&parseReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool { parseReporter.read = bytesRead; parseReporter.total = totalBytes; return parseReporter.Report(totalBytes > 0 ? static_cast(bytesRead) / static_cast(totalBytes) : 0.0); }); if (!parseReporter.Report(0.0)) { error.clear(); return false; } const std::wstring stem = StemOf(filePath); const std::uint64_t refs = file.Member(file.Root(), "#refs#"); const std::uint64_t sigstream = file.Member(file.Root(), "#sigstream#"); if (refs != 0 && sigstream != 0) { if (!LoadSdi(file, refs, sigstream, stem, groups, error, parseReporter)) { if (file.Cancelled()) { error.clear(); // 用户取消:不显示错误 } return false; } } else { LoadedSourceGroup group; group.source.sourceName = stem; for (const auto& member : file.Members(file.Root())) { if (member.first.empty() || member.first[0] == '#') { continue; } std::wstring name(member.first.begin(), member.first.end()); LoadPlainObject(file, member.second, name, group.source); } if (!group.source.HasData()) { error = L".mat 文件中没有可用的数值数据。"; return false; } groups.push_back(std::move(group)); } if (progress) { LoadProgressInfo info; info.ratio = 1.0; info.hasBytes = (file.FileBytes() > 0); info.bytesRead = file.FileBytes(); info.totalBytes = file.FileBytes(); progress(info); } return true; } } // namespace jdi