Files
Just-DataInspector/Utilities/Src/MatLoader.cpp
T
DeepSeek-V4.1-Flash f44f8021e3 Release 产物 1.74MB → 796KB(去掉 <filesystem> 与 zlib)+ .mat 加载进度不再停住
一、体积膨胀的真因(用 -Wl,-Map 逐对象/逐节对比上一提交定位,只有两个)
1) <filesystem>(致命,约 +900KB):MatH5::Open 用 std::filesystem::file_size 取文件大小,
   MinGW 的 libstdc++ 因此链入 fs_path/fs_dir/fs_ops/c++locale/codecvt/collate_members/
   cow-wstring-inst/cxx11-wlocale-inst/fstream-inst/cp-demangle 整套 locale/iostream 机器
   (归档成员 415→538);还把 64KB 静态表从 .bss 挤成 .data(1.8KB→73.8KB)。
   修法:改用 _wfopen_s + _fseeki64/_ftelli64 取大小。
2) -lz(约 46KB):只需要"解开 HDF5 分块"。修法:MatH5 内置紧凑 inflate
   (RFC1950/1951:固定/动态 Huffman、存储块、长度-距离回抄),Makefile 与探针脚本移除 -lz。
顺带把 UploadHaloLayer 里 64KB 的预乘 alpha 查表换成 (v*a+128)*257>>16(无除法、无表)。

二、压到 800KB 以下的两项无损措施
1) 图标资源无损重编码:ico 里 64/48/32/16 四帧原为未压缩 32bpp DIB(合计 32KB),
   用自写工具(GDI+ 编码)改为 PNG 帧(ICO 的 PNG 条目 Vista+ 全支持,本文件本就有 256 PNG 帧):
   43,334 → 17,404 字节。像素逐点校验:差异 ≤85 像素、最大分量差 ≤3(编码取整),肉眼不可辨;
   并从**真实产物** LoadImageW 逐尺寸(16/32/48/64)验证可加载。
2) RELEASE_OPT 由 -Os 改为 -Oz(省约 6KB);实测动画帧 p50 7.73ms(-Os) vs 7.77ms(-Oz),
   无可辨识差异。
放弃的手段:-ffunction-sections/-fdata-sections + --gc-sections(反而 +9KB)、LTO(工具链 ICE)、
-fno-asynchronous-unwind-tables(链接失败)、-fno-exceptions(会破坏 catch (bad_alloc))。
结果:Build/Release/Target.exe = 795,648 字节。

三、.mat 加载进度不更新(已加载大小 / 总大小 / 百分比都停在 0)
真因:LoadMatSourceGroups 只在结束时回调一次 ratio=1.0,且从不设置 hasBytes;
读 42MB 文件与解码 19 个样本流(解压+拷贝,占大头)期间进度恒为 0。
修法:
- MatHdf5::Open 新增 OpenProgress(bytesRead,totalBytes),按 4MB 分块读取并回报;
- MatLoader 用 ProgressReporter 把进度拆成 0.00–0.55 读文件(hasBytes=true)与
  0.55–0.98 解码样本流(每解完一个流推进 done/total),末段 1.0;
- 取消与出错分开:取消时清空 error,不弹错误框。
实测:回调 29 次、字节数变化 11 次、比例单调、终值 1.000、bytes=42,386,344/42,386,344
(= 文件大小);Application 侧断言 m_loadingBytesRead == m_loadingTotalBytes == 文件大小。

四、验证(全部离屏,不占用鼠标/键盘/屏幕)
- matcheck 21/21(新增进度断言)、matui 15/15(新增图标 + 加载字节断言);
- 回归 verify 49/49、panelanim 158/158、gridanim 93/93、hoverdrag 20/20、lanescroll 23/23、
  datacheck 69/69、edgecheck 46/46、errcheck 19/19、halocheck 16/16、panflash 19/19、
  panelstutter 8/8、stress 6/6;
- make release / make debug 零警告。

文档:Documents/agent_mem.md 追加本轮小节(含 5 条新规则:MinGW 下 <filesystem> 是体积炸弹、
链一整条库前先算账、膨胀要看节表而非 .o 大小、图标可无损重编码并用产物校验、
长解析必须按阶段回报进度且 hasBytes 只在有字节时置真)。
2026-10-19 02:37:52 +08:00

559 lines
16 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.
// MATLAB `.mat`(v7.3 / HDF5)加载器。
//
// 支持两种内容(详见 MatLoader.hpp 的说明):
// * Simulink SDI / `SimulationData.Dataset` 导出:`#refs#` 里的元素图 + `#sigstream#` 的样本流;
// * 普通 v7.3 变量:数值矩阵按"列 = 信号、行 = 采样"读取(时间轴 = 采样序号)。
#include "MatLoader.hpp"
#include "MatH5.hpp"
#include <algorithm>
#include <cstring>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace jdi
{
namespace
{
/** @brief 一个 SDI 信号(或子数据集)的解析结果。 */
struct SdiElement
{
bool isSignal = false;
bool isDataset = false;
std::wstring name;
std::vector<std::uint64_t> children; /**< 子数据集元素的引用目标。 */
int dataStream = -1; /**< 数据流下标。 */
int timeStream = -1; /**< 时间流下标。 */
};
std::wstring TrimNumber(double value)
{
wchar_t buffer[64] = {};
if (value == static_cast<double>(static_cast<long long>(value)))
{
std::swprintf(buffer, 64, L"%lld", static_cast<long long>(value));
}
else
{
std::swprintf(buffer, 64, L"%g", value);
}
return buffer;
}
/** @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#/<n>/#data#`)。 */
bool ReadStream(const MatHdf5& file, std::uint64_t streamObject, std::vector<double>& out)
{
out.clear();
const std::uint64_t data = file.Member(streamObject, "#data#");
if (data == 0)
{
return false;
}
MatHdf5::Dataset dataset;
if (!file.ReadDataset(data, dataset) || !dataset.ok)
{
return false;
}
// ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8),
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double(`#sampleSize#` 可印证)。
if (dataset.data.size() % sizeof(double) != 0)
{
return false;
}
const std::size_t count = dataset.data.size() / sizeof(double);
out.resize(count);
std::memcpy(out.data(), dataset.data.data(), count * sizeof(double));
return true;
}
/** @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<double> numbers;
if (file.ReadNumbers(file.Member(values, "DataR2"), numbers) && !numbers.empty())
{
element.dataStream = static_cast<int>(numbers.front());
}
numbers.clear();
if (file.ReadNumbers(file.Member(values, "TimeR2"), numbers) && !numbers.empty())
{
element.timeStream = static_cast<int>(numbers.front());
}
}
/** @brief 追加一条信号(时间 + 数值)到某个分组的时间轴块集合。 */
void AppendSignal(LoadedSource& source, const std::wstring& name, std::vector<double>&& time,
std::vector<double>&& 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; /**< 总字节。 */
/** @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;
}
LoadProgressInfo info;
info.ratio = low + (high - low) * fraction;
// 总字节已知时一并上报(遮罩层据此显示“已加载 / 总大小”)。
info.hasBytes = (total > 0);
info.bytesRead = (total > 0) ? read : 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<LoadedSourceGroup>& groups,
std::wstring& error, const ProgressReporter& reporter)
{
// 1) 枚举 `#refs#` 里的全部元素。
std::map<std::uint64_t, SdiElement> 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<std::uint64_t> childAddresses;
for (const auto& entry : elements)
{
for (const std::uint64_t child : entry.second.children)
{
childAddresses.insert(child);
}
}
std::vector<std::uint64_t> 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<std::wstring> path;
int depth = 0;
};
std::vector<Pending> 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));
}
std::map<std::wstring, std::size_t> groupIndex;
std::vector<std::vector<double>> streams; // 流编号 → 样本
std::vector<bool> streamCached;
// 进度:`#sigstream#` 的总流数,用来把"解码样本流"这段映射成阶段内 0-1。
std::size_t streamTotal = 0;
for (const auto& member : file.Members(sigstream))
{
if (!member.first.empty() && member.first[0] != '#')
{
++streamTotal;
}
}
std::size_t streamDone = 0;
const auto streamFor = [&](int index) -> const std::vector<double>*
{
if (index < 0)
{
return nullptr;
}
if (streams.size() <= static_cast<std::size_t>(index))
{
streams.resize(static_cast<std::size_t>(index) + 1);
streamCached.resize(static_cast<std::size_t>(index) + 1, false);
}
if (!streamCached[static_cast<std::size_t>(index)])
{
const std::uint64_t streamObject =
file.Member(sigstream, std::to_string(index));
if (streamObject == 0 || !ReadStream(file, streamObject, streams[static_cast<std::size_t>(index)]))
{
streams[static_cast<std::size_t>(index)].clear();
}
streamCached[static_cast<std::size_t>(index)] = true;
// ⚠️ 必须在这里回报进度:读一个 42MB 的 `.mat` 时,"解码样本流"才是真正的
// 耗时大头(每个流都要解压 + 拷贝);不报进度的话界面会停在原地等
//(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新)。
++streamDone;
reporter.Report(streamTotal > 0
? static_cast<double>(streamDone) /
static_cast<double>(streamTotal)
: 1.0);
}
return streams[static_cast<std::size_t>(index)].empty()
? nullptr
: &streams[static_cast<std::size_t>(index)];
};
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)
{
continue;
}
const std::vector<double>* time = streamFor(element.timeStream);
const std::vector<double>* values = streamFor(element.dataStream);
if (values == nullptr)
{
continue;
}
std::vector<double> timeCopy;
if (time != nullptr && time->size() == values->size())
{
timeCopy = *time;
}
else
{
// 时间流缺失/长度不符:退化为采样序号(秒),保证信号仍可显示。
timeCopy.resize(values->size());
for (std::size_t index = 0; index < values->size(); ++index)
{
timeCopy[index] = static_cast<double>(index);
}
}
std::wstring groupName;
for (const std::wstring& piece : pending.path)
{
if (!groupName.empty())
{
groupName += L"-";
}
groupName += piece;
}
if (groupName.empty())
{
groupName = stem;
}
std::wstring signalName = element.name;
if (signalName.empty())
{
signalName = L"信号" + std::to_wstring(++unnamedSignals);
}
std::size_t index = 0;
const auto existing = groupIndex.find(groupName);
if (existing == groupIndex.end())
{
LoadedSourceGroup group;
group.name = groupName;
group.source.sourceName = groupName;
groups.push_back(std::move(group));
index = groups.size() - 1;
groupIndex.emplace(groupName, index);
}
else
{
index = existing->second;
}
AppendSignal(groups[index].source, signalName, std::move(timeCopy),
std::vector<double>(*values));
}
if (groups.empty())
{
error = L"文件中没有可用的信号数据。";
return false;
}
return true;
}
/** @brief 普通 v7.3 变量:数值矩阵 / 结构体。 */
void LoadPlainObject(const MatHdf5& file, std::uint64_t address, const std::wstring& name,
LoadedSource& source)
{
if (file.IsDataset(address))
{
std::vector<double> numbers;
if (!file.ReadNumbers(address, numbers) || numbers.empty())
{
return;
}
MatHdf5::Dataset dataset;
file.ReadDataset(address, dataset);
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<double> values(rows);
for (std::size_t row = 0; row < rows; ++row)
{
values[row] = numbers[column * rows + row];
}
std::vector<double> time(rows);
for (std::size_t row = 0; row < rows; ++row)
{
time[row] = static_cast<double>(row);
}
const std::wstring signalName =
(columns == 1) ? name : (name + L"(" + TrimNumber(static_cast<double>(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);
}
}
} // namespace
bool LoadMatSourceGroups(const std::wstring& filePath, std::vector<LoadedSourceGroup>& groups,
std::wstring& error, const LoadProgress& progress)
{
error.clear();
groups.clear();
MatHdf5 file;
// ------------------------------------------------------------------
// 进度拆成三段(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新,
// 界面只是原地等待):
// 0.00 – 0.55 读文件字节(遮罩层显示"已加载 / 总大小")
// 0.55 – 0.98 解码样本流(每个流解码后推进,含解压与拷贝)
// 0.98 – 1.00 收尾
// 读文件阶段把 `hasBytes` 置真;之后字节数已满,只推进百分比。
// ------------------------------------------------------------------
ProgressReporter readReporter;
readReporter.sink = &progress;
readReporter.low = 0.0;
readReporter.high = 0.55;
const MatHdf5::OpenProgress openProgress =
[&readReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
{
readReporter.read = bytesRead;
readReporter.total = totalBytes;
return readReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
static_cast<double>(totalBytes)
: 0.0);
};
if (!file.Open(filePath, error, openProgress))
{
return false; // 用户取消时 `error` 为空
}
ProgressReporter parseReporter;
parseReporter.sink = &progress;
parseReporter.low = 0.55;
parseReporter.high = 0.98;
parseReporter.read = readReporter.total;
parseReporter.total = 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))
{
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 = (readReporter.total > 0);
info.bytesRead = readReporter.total;
info.totalBytes = readReporter.total;
progress(info);
}
return true;
}
} // namespace jdi