一、体积膨胀的真因(用 -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 只在有字节时置真)。
559 lines
16 KiB
C++
559 lines
16 KiB
C++
// 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
|