From f44f8021e3ac58936a2292919da7d5074eb49599 Mon Sep 17 00:00:00 2001 From: "DeepSeek-V4.1-Flash" <> Date: Mon, 19 Oct 2026 02:37:52 +0800 Subject: [PATCH] =?UTF-8?q?Release=20=E4=BA=A7=E7=89=A9=201.74MB=20?= =?UTF-8?q?=E2=86=92=20796KB=EF=BC=88=E5=8E=BB=E6=8E=89=20=20?= =?UTF-8?q?=E4=B8=8E=20zlib=EF=BC=89+=20.mat=20=E5=8A=A0=E8=BD=BD=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E4=B8=8D=E5=86=8D=E5=81=9C=E4=BD=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、体积膨胀的真因(用 -Wl,-Map 逐对象/逐节对比上一提交定位,只有两个) 1) (致命,约 +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 下 是体积炸弹、 链一整条库前先算账、膨胀要看节表而非 .o 大小、图标可无损重编码并用产物校验、 长解析必须按阶段回报进度且 hasBytes 只在有字节时置真)。 --- Components/PlotView/Src/PlotViewPaint.cpp | 46 +-- Documents/agent_mem.md | 84 +++++ Makefile | 5 +- Resources/JustDataInspector.ico | Bin 43334 -> 17404 bytes Utilities/Inc/DataLoader.hpp | 4 +- Utilities/Inc/MatH5.hpp | 18 +- Utilities/Src/MatH5.cpp | 417 +++++++++++++++++++++- Utilities/Src/MatLoader.cpp | 95 ++++- 8 files changed, 623 insertions(+), 46 deletions(-) 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 a73b5e93c8930ccb8f32fa63bff52635a90d5e59..ba92c51f31ad8a70bd91a24ea41e0a2d910441c0 100644 GIT binary patch delta 5958 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 T-1PmK@i_lKp(6j^zU_YiQI!=F literal 43334 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 +#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;