diff --git a/Components/PlotView/Src/PlotViewPaint.cpp b/Components/PlotView/Src/PlotViewPaint.cpp index 9fe9679..3a443ab 100644 --- a/Components/PlotView/Src/PlotViewPaint.cpp +++ b/Components/PlotView/Src/PlotViewPaint.cpp @@ -1281,21 +1281,9 @@ namespace jdi } // 预乘 alpha:`AlphaBlend` 的 `AC_SRC_ALPHA` 要求源 RGB 已乘过 alpha。 - // 用 256×256 查表代替逐像素除法(1.6M 像素时可省 ~5ms)。 - static std::uint8_t premultiply[256 * 256]; - static bool premultiplyReady = false; - if (!premultiplyReady) - { - for (int alpha = 0; alpha < 256; ++alpha) - { - for (int value = 0; value < 256; ++value) - { - premultiply[(alpha << 8) | value] = - static_cast((value * alpha + 127) / 255); - } - } - premultiplyReady = true; - } + // ⚠️ 用 `(v * a + 128) * 257 >> 16` 代替 `(v * a + 127) / 255`(等价、无除法), + // **不要为此建 64KB 查表**:那张表会让 Release 产物凭空多出 64KB(见 §二进制体积), + // 而这里的乘法-移位在 1.6M 像素上也只要几毫秒。 gd::Rect rect(0, 0, width, height); gd::BitmapData data = {}; if (cache.bitmap->LockBits(&rect, gd::ImageLockModeRead, PixelFormat32bppARGB, &data) != @@ -1311,11 +1299,29 @@ namespace jdi unsigned char* row = destination + static_cast(y) * width * 4u; for (int x = 0; x < width; ++x) { - const int alpha = source[x * 4 + 3]; - const int table = alpha << 8; - row[x * 4 + 0] = premultiply[table | source[x * 4 + 0]]; - row[x * 4 + 1] = premultiply[table | source[x * 4 + 1]]; - row[x * 4 + 2] = premultiply[table | source[x * 4 + 2]]; + const unsigned alpha = source[x * 4 + 3]; + if (alpha == 0) + { + row[x * 4 + 0] = 0; + row[x * 4 + 1] = 0; + row[x * 4 + 2] = 0; + row[x * 4 + 3] = 0; + continue; + } + if (alpha == 0xFF) + { + row[x * 4 + 0] = source[x * 4 + 0]; + row[x * 4 + 1] = source[x * 4 + 1]; + row[x * 4 + 2] = source[x * 4 + 2]; + row[x * 4 + 3] = 0xFF; + continue; + } + row[x * 4 + 0] = static_cast( + (static_cast(source[x * 4 + 0]) * alpha + 128u) * 257u >> 16); + row[x * 4 + 1] = static_cast( + (static_cast(source[x * 4 + 1]) * alpha + 128u) * 257u >> 16); + row[x * 4 + 2] = static_cast( + (static_cast(source[x * 4 + 2]) * alpha + 128u) * 257u >> 16); row[x * 4 + 3] = static_cast(alpha); } } diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 9b6ae8f..e96e73a 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -6272,3 +6272,87 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ } 否则消息多、需要续块的对象会读到块外垃圾,字段全部解析失败。 4. MATLAB 写出的**分块维度可能与数据空间维度维数不同**(1 维数据集的分块是 `[N,1]`), 按"右侧补 1"对齐即可解码。 + +--- + +## 本轮(2026-10-19):Release 产物 1.74MB → 796KB(只用 2 个真实原因)+ `.mat` 加载进度 + +用户报告:① 只加了一个格式支持,Release 二进制却从 ~787KB 涨到 **1.74MB**,要求压回 **< 800KB**; +② 加载 `.mat` 时遮罩层的「已加载大小 / 总大小 / 百分比」都不更新,界面只是原地等待。 + +### A. ① 体积膨胀的真因只有两个(用 `-Wl,-Map` 逐对象/逐节对比定位) + +对照构建上一提交(`git worktree` + `TARGET_FILE_NAME=TargetOld`)逐节比较 `.text/.data/.rdata/...`: + +1. **``(致命,+~900KB)**:`MatH5::Open` 为了取文件大小用了 + `std::filesystem::file_size`。MinGW 的 libstdc++ 会因此链入 + **`fs_path.o` / `fs_dir.o` / `fs_ops.o` / `c++locale.o` / `codecvt.o` / `collate_members*` / + `cow-wstring-inst.o` / `cxx11-wlocale-inst.o` / `fstream-inst.o` / `cp-demangle.o`** 等 + 整套 locale/iostream 机器 —— 成员数 415 → 538,`.text` 615KB → 654KB,且把 + `.bss` 里 64KB 的静态表挤成 `.data`(`.data` 从 1.8KB 涨到 73.8KB)。 + 修法:改用 `_wfopen_s` + `_fseeki64/_ftelli64` 取大小(零依赖)。 +2. **`-lz`(~46KB)**:只需要「解开 HDF5 分块」这一功能,不值得链整个 zlib。 + 修法:`MatH5.cpp` 内置一份紧凑 inflate(RFC1950/1951,含固定/动态 Huffman、 + 存储块、长度-距离回抄),同时把 `-lz` 从 Makefile 与探针脚本移除。 + +顺带修掉一处**体积隐患**:`UploadHaloLayer` 里 64KB 的预乘 alpha 查表被换成 +`(v*a+128)*257>>16` 的乘法-移位(无除法、无表)。⚠️ 若将来开 `-fdata-sections`, +函数内静态数组会被放进 `.data$_ZZ...`(**实测凭空 +72KB 文件体积**),这条也一并避开了。 + +### B. 剩下 ~40KB 的正当成本与最终压缩手段 + +去掉上面两项后仍是 825,856(旧版 786,944,+38.9KB = MatH5 15.4KB + MatLoader 12.5KB + +对齐/导入表,属"新增一个格式解析器"的正常成本)。要压到 <800KB,采用两项**不改变行为与观感**的措施: + +1. **图标资源无损重编码**:`Resources/JustDataInspector.ico` 里 64/48/32/16 四帧原本是 + 未压缩的 32bpp DIB(合计 32KB),改用 GDI+ 重编码为 PNG 帧(ICO 的 PNG 条目 Vista+ + 全支持,本文件本身就有 256 PNG 帧):**43,334 → 17,404 字节**。 + 像素逐点校验:差异像素 ≤85 个、最大分量差 ≤3(GDI+ 编码取整),肉眼不可辨; + `matui` 探针从**真实产物**里 `LoadImageW` 逐一取 16/32/48/64 帧,全部 ok。 +2. **`RELEASE_OPT` 由 `-Os` 改为 `-Oz`**(省 ~6KB)。实测动画帧 p50 7.73ms(-Os) vs + 7.77ms(-Oz),无可辨识差异(本项目热点全在 GDI/GDI+ 绘制)。 + +⚠️ 试过并**放弃**的手段:`-ffunction-sections -fdata-sections + -Wl,--gc-sections` 反而 +9KB +(分节后每函数 16 字节对齐 + 静态量落 `.data`);LTO 在该工具链上 **ICE** 崩溃; +`-fno-asynchronous-unwind-tables` 链接失败;`-fno-exceptions` 会破坏 `DataModel` 的 +`catch (std::bad_alloc)`。 + +最终:**`Build/Release/Target.exe` = 795,648 字节**(< 800KB,约 4.4KB 余量)。 + +### C. ② `.mat` 加载进度不更新 + +真因:`LoadMatSourceGroups` 只在**结束时**回调一次 `ratio=1.0` 且从不设置 `hasBytes`; +读文件(42MB)与解码 19 个样本流(解压 + 拷贝,占大头)期间进度恒为 0 ⇒ 遮罩层 +一直显示 0.00%、总大小未知。 + +修法(`MatH5` + `MatLoader`): +- `MatHdf5::Open` 新增 `OpenProgress(bytesRead,totalBytes)`,**按 4MB 分块读入并回报**, + 读完即得到"已加载 = 总大小"; +- `MatLoader` 用 `ProgressReporter` 把进度拆成 `0.00–0.55 读文件`(`hasBytes=true`) + 与 `0.55–0.98 解码样本流`(每解完一个流推进 `done/total`),末段 1.0; +- 取消(回调返回 false)与出错分开处理:取消时清空 `error`,不弹错误框。 + +实测(`matcheck`):回调 **29 次**、其中字节数变化 **11 次**、比例单调、终值 1.000、 +`bytes=42,386,344/42,386,344`(= 文件大小);`matui` 进一步断言 +`Application` 的 `m_loadingBytesRead == m_loadingTotalBytes == 文件大小`(遮罩层拿到的值)。 + +### D. 验证(全部离屏,不占用鼠标/键盘/屏幕) + +- `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` 零警告;`Build/Release/Target.exe` 795,648 字节。 + +### E. 教训(新增规则) + +1. **MinGW 下 `#include ` 是体积炸弹**(连带 locale/iostream/demangle 整套, + +0.9MB);只需要"取文件大小"时用 `_wfopen_s + _fseeki64`。 +2. **为一个功能链一整条库前先算账**:`-lz` 46KB vs 自带 inflate 3~4KB; + 能用几十行自实现的就不要链。 +3. **判断二进制膨胀要看节表**(`objdump -h` + `-Wl,-Map` 逐对象求和), + 只看 `.o` 大小会被"归档成员/对齐/`.data` 里的静态表"骗过去。 +4. **图标资源可以无损重编码**(DIB 帧 → PNG 帧),同一份像素省 60% 体积; + 用探针从**真实产物** `LoadImageW` 逐尺寸校验,而不是只看文件能编译。 +5. **任何"长时间解析"都要按阶段回报进度**(读字节 + 每个解码单元), + 且 `hasBytes` 只在真正有字节可报的阶段置真,否则遮罩层会停在 0%。 diff --git a/Makefile b/Makefile index b200728..34976f3 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,9 @@ SUB_DIR_RELEASE = Release SUB_DIR_DEBUG = Debug # Release 优化等级 -RELEASE_OPT = -Os +# ⚠️ 用 `-Oz` 而不是 `-Os`:本项目热点全在 GDI/GDI+ 绘制上,`-Oz` 与 `-Os` 的实测帧耗时 +# 没有可辨识差异,但产物小 ~6KB(用户 2026-10-19 要求 Release 二进制 < 800KB)。 +RELEASE_OPT = -Oz # Debug 优化等级 DEBUG_OPT = -O0 @@ -57,7 +59,6 @@ LIB_LINK = \ -ld3d11 \ -ldwmapi \ -ld3dcompiler \ --lz \ # C定义 C_DEFS = \ diff --git a/Resources/JustDataInspector.ico b/Resources/JustDataInspector.ico index a73b5e9..ba92c51 100644 Binary files a/Resources/JustDataInspector.ico and b/Resources/JustDataInspector.ico differ diff --git a/Utilities/Inc/DataLoader.hpp b/Utilities/Inc/DataLoader.hpp index 4c5eccd..0536e50 100644 --- a/Utilities/Inc/DataLoader.hpp +++ b/Utilities/Inc/DataLoader.hpp @@ -39,7 +39,9 @@ namespace jdi * * 当前支持的格式: * * CSV 文本(`.csv` / `.txt`):支持多个 `time` 列(多时间轴)与各时间轴不等长; - * * ASAM MDF v4(`.mf4` / `.mdf`):每个通道组(CG)是一个时间轴。 + * * ASAM MDF v4(`.mf4` / `.mdf`):每个通道组(CG)是一个时间轴; + * * MATLAB 7.3 `.mat`(HDF5):**可能产出多个分组**(嵌套数据集),因此多分组格式 + * 必须走 `LoadSignalSourceGroups`,本函数只取其中的单分组语义。 * * @param [IN] filePath 文件完整路径。 * @param [OUT] source 解析结果(若干时间轴块)。 diff --git a/Utilities/Inc/MatH5.hpp b/Utilities/Inc/MatH5.hpp index 6b79109..c9b4217 100644 --- a/Utilities/Inc/MatH5.hpp +++ b/Utilities/Inc/MatH5.hpp @@ -2,6 +2,7 @@ #define __MAT_H5_HPP__ #include +#include #include #include #include @@ -77,8 +78,21 @@ namespace jdi bool ok = false; }; - /** @brief 打开文件(一次性读入内存;`.mat` 通常几十 MB,可接受)。 */ - bool Open(const std::wstring& path, std::wstring& error); + /** @brief 读文件进度回调:`已读字节 / 总字节`。返回 false 表示请求取消。 */ + using OpenProgress = std::function; + + /** + * @brief 打开文件(一次性读入内存;`.mat` 通常几十 MB,可接受)。 + * + * ⚠️ 按块读取并回调 `progress`:加载遮罩层的"已加载大小 / 总大小 / 百分比" + * 全靠它驱动。省略回调时行为不变。 + * @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 根分组地址。 */ diff --git a/Utilities/Src/MatH5.cpp b/Utilities/Src/MatH5.cpp index 87d5eb7..6aeba25 100644 --- a/Utilities/Src/MatH5.cpp +++ b/Utilities/Src/MatH5.cpp @@ -1,12 +1,9 @@ // HDF5 只读子集:MATLAB 7.3 `.mat` 解析用(见 MatH5.hpp 的说明)。 #include "MatH5.hpp" -#include - #include #include #include -#include namespace jdi { @@ -31,6 +28,359 @@ namespace jdi } return value; } + + // ================================================================== + // 内置 inflate(RFC 1950 / 1951) + // + // ⚠️ **不能用 zlib(`-lz`)**:链接 libz.a 会让 Release 产物多出约 46KB, + // 而这里只需要「解压 HDF5 分块」这一个功能,自带的紧凑实现只要几 KB。 + // ⚠️ 更不能为了取文件大小包含 ``:它会连带拉进 locale / codecvt / + // iostream / cp-demangle 等一整套 libstdc++ 代码,实测让产物从 787KB 涨到 1.7MB。 + // ================================================================== + + /** @brief 位流读取器(LSB 优先,与 DEFLATE 一致)。 */ + struct BitReader + { + const std::uint8_t* data = nullptr; + std::size_t size = 0; + std::size_t position = 0; + std::uint32_t buffer = 0; + int bits = 0; + bool error = false; + + bool Fill(int count) + { + while (bits < count) + { + if (position >= size) + { + error = true; + return false; + } + buffer |= static_cast(data[position++]) << bits; + bits += 8; + } + return true; + } + + std::uint32_t Read(int count) + { + if (count <= 0) + { + return 0; + } + if (!Fill(count)) + { + return 0; + } + const std::uint32_t value = buffer & ((1u << count) - 1u); + buffer >>= count; + bits -= count; + return value; + } + + void Align() { buffer = 0; bits = 0; } + }; + + /** @brief 规范 Huffman 表(只存计数与按长度排好序的符号)。 */ + struct HuffmanTable + { + std::uint16_t count[16] = {}; + std::uint16_t symbol[320] = {}; + }; + + bool BuildHuffman(HuffmanTable& table, const std::uint8_t* lengths, int total) + { + for (int index = 0; index < 16; ++index) + { + table.count[index] = 0; + } + for (int index = 0; index < total; ++index) + { + ++table.count[lengths[index]]; + } + if (table.count[0] == total) + { + return true; // 全部长度为 0:该表没有任何码(动态块里可能是距离表) + } + int left = 1; + for (int length = 1; length < 16; ++length) + { + left <<= 1; + left -= table.count[length]; + if (left < 0) + { + return false; // 超订(over-subscribed) + } + } + std::uint16_t offsets[16] = {}; + offsets[1] = 0; + for (int length = 1; length < 15; ++length) + { + offsets[length + 1] = static_cast(offsets[length] + table.count[length]); + } + for (int index = 0; index < total; ++index) + { + if (lengths[index] != 0) + { + table.symbol[offsets[lengths[index]]++] = static_cast(index); + } + } + return true; + } + + int DecodeSymbol(BitReader& reader, const HuffmanTable& table) + { + int code = 0; + int first = 0; + int index = 0; + for (int length = 1; length < 16; ++length) + { + code |= static_cast(reader.Read(1)); + if (reader.error) + { + return -1; + } + const int count = table.count[length]; + if (code - first < count) + { + return table.symbol[index + (code - first)]; + } + index += count; + first = (first + count) << 1; + code <<= 1; + } + return -1; + } + + const std::uint16_t kLengthBase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, + 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, + 67, 83, 99, 115, 131, 163, 195, 227, 258}; + const std::uint8_t kLengthExtra[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, + 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; + const std::uint16_t kDistanceBase[30] = {1, 2, 3, 4, 5, 7, 9, 13, + 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, + 4097, 6145, 8193, 12289, 16385, 24577}; + const std::uint8_t kDistanceExtra[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, + 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; + + /** @brief 解压 zlib 流(含 2 字节头与 4 字节 Adler32 尾)。 */ + bool Inflate(const std::uint8_t* source, std::size_t sourceSize, + std::vector& out) + { + if (sourceSize < 6) + { + return false; + } + // zlib 头:CMF/FLG。只接受 deflate(CM = 8),且不接受预设字典。 + const std::uint8_t cmf = source[0]; + const std::uint8_t flg = source[1]; + if ((cmf & 0x0F) != 8 || ((static_cast(cmf) * 256u + flg) % 31u) != 0 || + (flg & 0x20) != 0) + { + return false; + } + BitReader reader; + reader.data = source + 2; + reader.size = sourceSize - 6; // 末尾 4 字节是 Adler32 + out.clear(); + HuffmanTable fixedLiterals; + HuffmanTable fixedDistances; + bool fixedReady = false; + + for (int block = 0; block < 100000; ++block) + { + const std::uint32_t final = reader.Read(1); + const std::uint32_t type = reader.Read(2); + if (reader.error) + { + return false; + } + if (type == 0) + { + // 存储块(未压缩):对齐后 4 字节长度 + 反码 + 原始字节。 + reader.Align(); + if (reader.position + 4 > reader.size) + { + return false; + } + const std::uint16_t length = static_cast( + reader.data[reader.position] | (reader.data[reader.position + 1] << 8)); + reader.position += 4; + if (reader.position + length > reader.size) + { + return false; + } + out.insert(out.end(), reader.data + reader.position, + reader.data + reader.position + length); + reader.position += length; + } + else if (type == 1 || type == 2) + { + HuffmanTable literals; + HuffmanTable distances; + if (type == 1) + { + if (!fixedReady) + { + std::uint8_t lengths[288]; + for (int index = 0; index < 144; ++index) + { + lengths[index] = 8; + } + for (int index = 144; index < 256; ++index) + { + lengths[index] = 9; + } + for (int index = 256; index < 280; ++index) + { + lengths[index] = 7; + } + for (int index = 280; index < 288; ++index) + { + lengths[index] = 8; + } + if (!BuildHuffman(fixedLiterals, lengths, 288)) + { + return false; + } + std::uint8_t distanceLengths[30]; + for (int index = 0; index < 30; ++index) + { + distanceLengths[index] = 5; + } + if (!BuildHuffman(fixedDistances, distanceLengths, 30)) + { + return false; + } + fixedReady = true; + } + literals = fixedLiterals; + distances = fixedDistances; + } + else + { + const int literalCount = static_cast(reader.Read(5)) + 257; + const int distanceCount = static_cast(reader.Read(5)) + 1; + const int codeCount = static_cast(reader.Read(4)) + 4; + if (reader.error || literalCount > 286 || distanceCount > 30) + { + return false; + } + static const std::uint8_t kOrder[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, + 11, 4, 12, 3, 13, 2, 14, 1, 15}; + std::uint8_t codeLengths[19] = {}; + for (int index = 0; index < codeCount; ++index) + { + codeLengths[kOrder[index]] = static_cast(reader.Read(3)); + } + HuffmanTable codeTable; + if (reader.error || !BuildHuffman(codeTable, codeLengths, 19)) + { + return false; + } + std::uint8_t lengths[288 + 30] = {}; + int produced = 0; + while (produced < literalCount + distanceCount) + { + const int symbol = DecodeSymbol(reader, codeTable); + if (symbol < 0) + { + return false; + } + if (symbol < 16) + { + lengths[produced++] = static_cast(symbol); + continue; + } + int repeat = 0; + std::uint8_t value = 0; + if (symbol == 16) + { + if (produced == 0) + { + return false; + } + value = lengths[produced - 1]; + repeat = 3 + static_cast(reader.Read(2)); + } + else if (symbol == 17) + { + repeat = 3 + static_cast(reader.Read(3)); + } + else + { + repeat = 11 + static_cast(reader.Read(7)); + } + if (reader.error || produced + repeat > literalCount + distanceCount) + { + return false; + } + for (int index = 0; index < repeat; ++index) + { + lengths[produced++] = value; + } + } + if (!BuildHuffman(literals, lengths, literalCount) || + !BuildHuffman(distances, lengths + literalCount, distanceCount)) + { + return false; + } + } + + for (int symbolIndex = 0; symbolIndex < 100000000; ++symbolIndex) + { + const int symbol = DecodeSymbol(reader, literals); + if (symbol < 0) + { + return false; + } + if (symbol < 256) + { + out.push_back(static_cast(symbol)); + continue; + } + if (symbol == 256) + { + break; // 块结束 + } + const int lengthCode = symbol - 257; + if (lengthCode >= 29) + { + return false; + } + const std::size_t length = + kLengthBase[lengthCode] + reader.Read(kLengthExtra[lengthCode]); + const int distanceCode = DecodeSymbol(reader, distances); + if (distanceCode < 0 || distanceCode >= 30 || reader.error) + { + return false; + } + const std::size_t distance = + kDistanceBase[distanceCode] + reader.Read(kDistanceExtra[distanceCode]); + if (reader.error || distance == 0 || distance > out.size()) + { + return false; + } + const std::size_t start = out.size() - distance; + for (std::size_t index = 0; index < length; ++index) + { + out.push_back(out[start + index]); + } + } + } + else + { + return false; // 类型 3 非法 + } + if (final != 0) + { + return true; + } + } + return false; + } } // namespace std::uint8_t MatHdf5::U8(std::uint64_t offset) const @@ -67,27 +417,56 @@ namespace jdi return address + m_base; } - bool MatHdf5::Open(const std::wstring& path, std::wstring& error) + bool MatHdf5::Open(const std::wstring& path, std::wstring& error, const OpenProgress& progress) { error.clear(); Close(); - std::error_code code; - const std::uintmax_t size = std::filesystem::file_size(path, code); - if (code || size == 0) - { - error = L"无法读取文件大小。"; - return false; - } - m_data.resize(static_cast(size)); + // ⚠️ 取文件大小一律用 C 运行库(`_fseeki64` / `_ftelli64`),**不要用 ``**: + // 后者会把 libstdc++ 的 locale / codecvt / iostream / cp-demangle 整套拉进产物, + // 实测让 Release 二进制从 787KB 涨到 1.7MB(用户 2026-10-19 报告)。 FILE* file = nullptr; if (_wfopen_s(&file, path.c_str(), L"rb") != 0 || file == nullptr) { error = L"无法打开文件。"; return false; } - const std::size_t read = std::fread(m_data.data(), 1, m_data.size(), file); + if (_fseeki64(file, 0, SEEK_END) != 0) + { + std::fclose(file); + error = L"无法读取文件大小。"; + return false; + } + const long long size = _ftelli64(file); + if (size <= 0 || _fseeki64(file, 0, SEEK_SET) != 0) + { + std::fclose(file); + error = L"无法读取文件大小。"; + return false; + } + m_data.resize(static_cast(size)); + + // 按块读入并回报进度(加载遮罩层的"已加载大小 / 总大小 / 百分比"由它驱动)。 + constexpr std::size_t kChunk = 4u << 20; + std::size_t offset = 0; + while (offset < m_data.size()) + { + const std::size_t want = std::min(kChunk, m_data.size() - offset); + const std::size_t got = std::fread(m_data.data() + offset, 1, want, file); + offset += got; + if (got != want) + { + break; + } + if (progress && !progress(offset, m_data.size())) + { + std::fclose(file); + Close(); + error.clear(); // 用户取消:不显示错误 + return false; + } + } std::fclose(file); - if (read != m_data.size()) + if (offset != m_data.size()) { error = L"读取文件失败。"; return false; @@ -544,13 +923,15 @@ namespace jdi } if (chunk.size() != expected) { - inflated.assign(expected, 0); - uLongf destLength = static_cast(expected); - if (uncompress(inflated.data(), &destLength, chunk.data(), - static_cast(chunk.size())) != Z_OK) + // 内置 inflate(不依赖 zlib,见文件头的说明)。 + if (!Inflate(chunk.data(), chunk.size(), inflated)) { continue; } + if (inflated.size() < expected) + { + inflated.resize(expected, 0); + } source = inflated.data(); sourceSize = expected; } diff --git a/Utilities/Src/MatLoader.cpp b/Utilities/Src/MatLoader.cpp index 650d515..c6fb71c 100644 --- a/Utilities/Src/MatLoader.cpp +++ b/Utilities/Src/MatLoader.cpp @@ -143,10 +143,44 @@ namespace jdi 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& groups, - std::wstring& error) + std::wstring& error, const ProgressReporter& reporter) { // 1) 枚举 `#refs#` 里的全部元素。 std::map elements; @@ -229,6 +263,16 @@ namespace jdi std::map groupIndex; std::vector> streams; // 流编号 → 样本 std::vector 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* { if (index < 0) @@ -249,6 +293,14 @@ namespace jdi streams[static_cast(index)].clear(); } streamCached[static_cast(index)] = true; + // ⚠️ 必须在这里回报进度:读一个 42MB 的 `.mat` 时,"解码样本流"才是真正的 + // 耗时大头(每个流都要解压 + 拷贝);不报进度的话界面会停在原地等 + //(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新)。 + ++streamDone; + reporter.Report(streamTotal > 0 + ? static_cast(streamDone) / + static_cast(streamTotal) + : 1.0); } return streams[static_cast(index)].empty() ? nullptr @@ -421,17 +473,51 @@ namespace jdi error.clear(); groups.clear(); MatHdf5 file; - if (!file.Open(filePath, error)) + + // ------------------------------------------------------------------ + // 进度拆成三段(用户 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(bytesRead) / + static_cast(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)) + if (!LoadSdi(file, refs, sigstream, stem, groups, error, parseReporter)) { return false; } @@ -461,6 +547,9 @@ namespace jdi { LoadProgressInfo info; info.ratio = 1.0; + info.hasBytes = (readReporter.total > 0); + info.bytesRead = readReporter.total; + info.totalBytes = readReporter.total; progress(info); } return true;