Files
Songlx516 ba668ade50 release: v1.9.3M -- encrypted archives, plus the UI round that followed
First release under the fork-marker convention: VERSION_TAG carries a trailing
`M`, so /api/version, the PS5 start-up notification, the stdout banner, the UI
footer and the ELF file name all read "v1.9.3M" in one move -- and a fork build
can no longer collide with an upstream artifact of the same version, a mix-up
that already happened twice. The footer carries a tooltip spelling the marker
out.

Two bodies of work.

1. Encrypted archives (ZIP / RAR / 7z)

   - ZIP: ZipCrypto (traditional PKWARE) and WinZip AES-256, through
     minizip-ng plus a vendored crypto layer (mz_crypt_wfm.c,
     mz_strm_pkcrypt.c, mz_strm_wzaes.c).
   - RAR: RARSetPassword, wired after RAROpenArchiveEx and before the first
     RARReadHeaderEx. The ordering is load-bearing, not stylistic.
   - 7z: 7zAES including -mhe=on encrypted headers, via a virtual
     ISeekInStream that splices a pseudo-header + the real archive + the
     decrypted header, so no offset stored inside the archive has to move.

   A wrong password is reported as ZIPX_ERR_PASSWORD, and a failed attempt
   leaves no staging directory behind.

2. Reporting, and the UI round that on-device testing produced

   - A RAR whose dictionary exceeds what the build supports now gets its own
     extract_dict_too_large code instead of being mis-reported as "entry too
     large"; the message names both the required and the supported size. The
     behaviour is deliberately unchanged -- such archives are still refused,
     because admitting one means allocating the whole window up front, which
     is why rarlab's own CLI refuses them by default.
   - The upload entry is a menu again: one "Upload" button opening "Upload
     files / Upload folder". The previous main-button-plus-small-arrow made
     "upload folder" effectively undiscoverable.
   - A drag-and-drop hint sits in the footer (hidden on the console browser,
     where drag is not how anyone uploads).
   - The extract button is now always present and merely disabled until
     exactly one archive is selected, instead of appearing out of nowhere.
   - Upload-and-extract on an encrypted archive now prompts for the password
     directly. The retry table used to be keyed by PATH, and for a non-ASCII
     directory the string the page holds and the string the server reports
     are not the same bytes -- the lookup missed, so the user got a bare
     error box and had to press Extract by hand before the prompt appeared.
     It is now keyed by task id, which the server assigns and echoes back
     verbatim.
   - Error text passes through decodeFsText(), so a GBK entry name no longer
     surfaces as `â®…ç§.psd`.
   - Local names are encoded with encodeFsText() before being joined onto a
     server-side path. fs_path_value() declines to rewrite a path if ANY code
     point exceeds 0xFF, so concatenating a local name onto a server directory
     produced a mixed representation and a silently dead path.
   - The menu row highlight was losing the cascade to the generic button rule
     (identical specificity, later in the file) while inheriting the toolbar's
     3px focus ring, which overflowed a 46px row. Both rules are now scoped to
     the panel and the keyboard cue is an inset ring, so it cannot escape the
     row at any line height.
   - The footer status line is clamped to a single line; a long
     "uploading 3/12: some-name.zip" used to wrap out of the 46px footer.
   - A first failed password attempt now says the archive is encrypted,
     instead of blaming a password the user was never asked for.

Artifact
  web-file-mgr-v1.9.3M.elf
  903,448 B
  sha256 8ca47d5aaca75085b32641300cce30fadb7df7749cb6b53d04f129bcecc286b7
  e_machine 0x003e (x86-64 / PS5)

  The file size is identical to the four builds before it, and every one of
  them carries a different sha256: only .rodata moved, and by less than the
  16 KiB section alignment absorbs. Compare sections with `readelf -SW` --
  never infer "nothing changed" from the byte count.

Verification
  - host suites: 140 ZIP + 37 RAR = 177 checks, 0 failures
  - 7z suite: 27 cases, 0 failures. The `aeshe` entry that used to sit in
    KNOWN_GAPS is gone -- the -mhe=on fixture now passes both the folder
    decoder and the extraction facade
  - frontend: .build/ui_retry_test.mjs (40 checks), .build/ui_upload_menu_test.mjs
    (40 checks), .build/preview_check.mjs (12 assertions in headless Chromium
    against the real page and a fixture API). Two layout regressions and the
    highlight cascade bug were caught by the last one and by nothing else --
    reading the source, both CSS rules "look correct"
  - built twice from this tree: byte-identical (cmp clean). rsync refreshes
    every asset mtime, so this is a genuine recompile, not make short-circuiting
    on unchanged sources
  - embedded assets verified in place with .build/check-elf-gzip.py, because
    gen-asset-module.py gzips them and plain `strings` finds none of their text
  - exercised end to end on a real PS5; the checklist is
    docs/DEVICE-TEST-v1.9.3M.md

Docs
  - docs/USER-GUIDE-zh-CN.md (new, simplified Chinese user guide)
  - docs/DEVICE-TEST-v1.9.3M.md (new, on-device acceptance checklist)
  - docs/REAL-CONSOLE-PROFILE.md (new, measured console behaviour)
  - docs/archive/HANDOVER-v1.8-planning.md (superseded v1.8 design notes)
  - CHANGELOG / README (both languages) / HANDOVER updated with the artifact
    fingerprint, the section deltas and the new test counts
2026-09-24 21:07:12 +08:00

19 KiB
Raw Permalink Blame History

真机验证清单:解压速度(10–40 MB/s 到底卡在哪)

⚑ 本清单的终局:停在第 1 步之前(2026-09-23 18:15,用户决定)

T_copy 没做;RAR 多线程(#51)不做;生产代码一行不动、不刷机 —— 11 分钟维持现状。

理由:MT 的收益在 PC 上已实测(下面「第三件」2.45×/我们引擎 1.40–1.74×), 但能不能兑现,刚好吊在本文档第 1 步那个实验上 —— 而 MT 只并行解码、写盘恒为主线程 串行(unpack50mt.cpp 的 UnpWriteBuf() 只在主线程调,见 §六 终局块), 所以那个实验的结果很可能是「收工」。赌一次的成本(改构建 + 刷机 + 重跑 11 分钟 + 真机验证) 压在不确定收益上 ⇒ 不做。

下面全部内容保留,作为「将来重开时的现成答案」。 ⚠️ 重开的第一个动作是第 1 步的 T_copy,不是改构建。

⚠️⚠️ 2026-09-23 深夜(第二次更正):归档参数已拿到、MT 已实测、最大嫌疑已排除

口径修正:包不是 18 GB,是 3 个分卷 ≈ 11.6 GB(4 GB + 4 GB + 3.6 GB)。 下文凡写「18 GB」处,一律按「≈11.6 GB」读;核心算术已在本块重算。

① 真实归档参数(从 part3 的头实测,该文件随后被清理) RAR 5、卷 3/3、锁定、不是固实(直解主头 archive flags=0x0013,MHFL_SOLID=0x0004 未置位)、 压缩参数 -m1 -md=4g(最快档 + 4 GiB 字典)。关键条目 PPSA16608.exfat 19.49 GB → 打包 3.83 GB(ratio 5.09),并整段都在 part3 内。 详见 docs/EXTRACTION-PERF.md §六 第二次更正块。

② 最大嫌疑(scan 白解码一遍 = 2×)已排除。 那建立在「固实 skip 必须解码」 (dll.cpp:341 只在 !Arc.Solid 走廉价 SeekToNext())之上;本档非固实 ⇒ 我们的 scan 不解码。

③ MT 收益已实测 = ~2.4×(真实形状夹具:-m1 -md4g、5:1 可压缩、2 GB、-mt1 386→-mt8 946 MB/s 解压后)。 接线只需 1 个编译开关 + 1 个链接开关(-DRAR_SMP + -lpthread),不必改 vendored 源码、 不必增删源文件 —— 早期版本写的「还要加 threadmisc.cpp」是错的:threadpool.cpp:5 已经 #include "threadmisc.cpp"(实测 nm unrar7_threadpool.o 里有 T GetNumberOfThreads),再加会重符号。

④ 但矛头现在指向「PS5 写路径」——所以本清单第 1 件事(T_copy)不变,而且更该先做。

  • 单线程解码在 PC 上同形状就是 386–505 MB/s;PS5 单核按 1/3 算也 ~130 MB/s。
  • 真机:只算 .exfat 一项就是 19.49 GB ÷ 660 s = ≥29.5 MB/s 解压后。
  • 两次独立操作撞同一个数:上传(写 11.6 GB)30–40 MB/s = 解压(写 ≥19.5 GB)≈30 MB/s。 ⇒ 优先怀疑这条写路径的上限就是 30–40 MB/s。若 T_copy 也 ~10 分钟 ⇒ 收工,MT 不做 (做了会被 I/O 吃掉);若 T_copy 明显快 ⇒ 立刻上 MT,那 2.4× 是真的。

下面是原始判断(保留),优先级以本块为准。

⚠️ 2026-09-23 晚 更正 —— 本文档的起点判断已反转。

本文最初的前提是「已排除解码器」,那建立在两条现已作废的论证上: ① 拿 PS5 上解 RAR 的 28 MiB/s 去比 PC 上解 7z 的 427 MiB/s(不同格式/解码器/机器); ② UI 速度的 4× 摆动(已自行降级为 250 ms 采样噪声的弱证据)。

新增的三条事实把方向翻了过来:格式是 RAR(解码就是 rarlab 的 UnRAR 库本身); 包原本在 PC 上、经插件上传进 PS5;上传实测 30–40 MB/s。随后查代码发现: 我们的 POSIX 构建没有定义 RAR_SMP(os.hpp:43-45 的 #define RAR_SMP 在 #ifdef _WIN_ALL 内;官方 POSIX makefile 第 11 行是 DEFINES=... -DRAR_SMP), 后果是 unpack50mt.cpp(Unpack::Unpack5MT,多线程 RAR5 解压器)根本没编进来, unpack.cpp 的 MT 分支整段不参与编译 ⇒ RAR 解码在 PS5 上是单线程的。

⇒ 新的首要假设:28 MiB/s ≈ 单线程 RAR5 解码的正常量级。 详细更正与代码行号见 docs/EXTRACTION-PERF.md §六 文首更正块。 下文按「实测 → 判读」仍有效;但第 2 步(读粒度)对 RAR 不适用,已加注。

已到手的数据(2026-09-23 真机)

项 值
包大小 ≈11.6 GB 压缩包(4 GB + 4 GB + 3.6 GB 三个分卷;✅ 2026-09-23 晚更正,原写 18 GB 有误)
条目数 1252
总耗时 11 分钟 = 660 s(UI 显示)
UI 速度口径 解压后字节(src/rar_extract.c:766 在 UCM_PROCESSDATA 里 bytes_done += p2)⇒ 10–40 MB/s 是「吐出数据的速度」
平均吞吐(输入侧) ≥ 17.6 MB/s 打包字节(11.6 GB ÷ 660 s)
平均吞吐(输出侧) ≥ 29.5 MB/s 解压后——只算 PPSA16608.exfat 一项就是 19.49 GB ÷ 660 s
格式 RAR 5 ✅(头 8 字节 52 61 72 21 1A 07 01 00)
固实 不是固实 ✅(主头 MHFL_SOLID=0x0004 未置位)
压缩参数 -m1 -md=4g(最快档 + 4 GiB 字典)✅
关键条目 PPSA16608.exfat 19 493 027 840 B → 打包 3 831 295 727 B(ratio 5.09),整段在 part3 内
来源 包原本在 PC 上,经插件本身上传进 PS5,上传速度 30–40 MB/s(写 11.6 GB)
源 / 目标设备 待确认 ⬅ 现在最关键的一条(内置 SSD / 外置 USB / 是否同盘)
解压后总大小 待确认 ⬅ 决定输出侧吞吐的确切值(≥19.49 GB 已知)

「18 GB 是压缩包大小」这条把口径钉住了,而且给出一个不需要知道解压后大小的硬上界:

源盘必须在 660 s 内交出 18 GB 的归档数据
⇒ 整条流水线的平均吞吐上界 = 18 GB ÷ 660 s ≈ 28 MiB/s

解压后有多大都不影响这个上界 —— 无论解码多快,源盘就只有这个交付速度。 所以第 1 步的 T_copy 和它可以直接比大小(同一个 18 GB 文件、同一对设备)。

两个立刻可用的推论:

  1. per-entry(小文件)开销解释不了它。 1252 个文件、平均 14.7 MB,不是「几万个小文件」那种 形态;建文件 + rename 这两种 per-entry 成本加起来分摊到 0.53 s/文件 里微乎其微。
  2. UI 上那个 10–40 MB/s 是瞬时值,不是平均值。 进度回调只在累计 ≥ 1 MiB 时上报 (src/zip_extract.c:123),src/task.c:202-206 每 250 ms 采一次样。250 ms 窗口 + 写缓冲突发会让速度在「忽停忽走」之间跳,波动里含相当比例的采样噪声。 ⇒ 只有「解压后字节 ÷ 总耗时」可信;那 4× 的摆动只能说明「不是恒定负载」,不能单独定案。

★ 你要在真机上做的事(就两件)

第一件:补一个数字 T_copy(3 分钟操作 + 一次等待)

  1. 把那个 18 GB 的包,用插件自己的复制功能,复制到解压时输出所在的那块盘
  2. 记下耗时(UI 上有)
  3. 顺便记下:这个包原本在哪(内置 SSD / 外置 USB),解压输出到哪(同盘还是另一块)

判读(就在 T_copy 与 660 s 之间比):

T_copy 结论 我接下来做什么
≥ 10 分钟 存储物理极限,与代码无关 收工,解码优化全部关闭(#48 取消)
1–2 分钟 解压比同一对设备上的纯搬运慢 5–10× ⇒ 代码里有真问题 做 #48:vol_read() 加 read-ahead
介于中间 混合 按字节数扣掉 I/O 分量,差值才是能改的部分

为什么这个数字这么关键:它是同一台机器、同一对设备、同一份代码,只把「解压」换成 「纯搬运」。不需要造新包、不需要第二块盘、不需要任何假设。上面那 4 个对照实验(第 3 步) 只在它的结论模糊时才需要。

第二件:两个小事实 —— 已答一半,剩两个待确认

  1. RAR4 还是 RAR5? RAR5 ✅;是否固实? 非固实 ✅;压缩参数 -m1 -md=4g ✅ (都从 part3 的头上直接读到,见文首第二次更正块 ①)。 顺带确认:UI 的口径是解压后字节(src/rar_extract.c:766)。
  2. 还缺两个数(都很便宜):
    • 解压后总大小 —— 决定输出侧吞吐。已知 ≥19.49 GB(仅 .exfat 一项)。 插件跑解压时 UI 上的 total 就是它(task->total = p->bytes_total,src/extract.c:64)。
    • 源 / 目标设备 —— 包在哪块盘、解压输出写到哪块盘、是否同一块盘。 这一条现在比什么都重要:写路径被怀疑是墙(文首块 ④)。

第三件(我已做完,不用上真机):多线程 RAR5 的收益

用 WinRAR 自带的 UnRAR 7.23(-mt<N> 开关可解析,虽然不出现在 -? 帮助里) 在一份按真实形状造的夹具上做 A/B(-m1 -md4g、5.27:1 可压缩、2 GB、分卷):

线程 耗时 解压后吞吐 加速
1 5.18 s 386 MB/s 1.00×
4 2.39 s 835 MB/s 2.16×
8 2.11 s 946 MB/s 2.45×

⇒ 收益 2.4×,且已在真实形状上验证(非固实版本 2.42×,量级一致)。 接线只需 1 个编译开关 + 1 个链接开关(-DRAR_SMP / -lpthread),不用改 vendored 源码、 不用增删源文件(早期写的 threadmisc.cpp 那一条经实测作废,见 §六 块 ④ 行内更正)—— 原因与行号见 docs/EXTRACTION-PERF.md §六 第二次更正块 ④。

⚠️ 但结论顺序仍以第一件为准:先 T_copy。如果墙是 30–40 MB/s 的写路径, 这 2.45× 会被 I/O 完全吃掉,做了等于白做。 ⇒ 闭环(2026-09-23 18:15):T_copy 没做,用户决定不做这项了(见文首终局块)。 本节的实测数据保留 —— 将来重开时它就是收益依据;但第一步仍是 T_copy。


第 1 步(决定性实验,= 上面「第一件」):用插件自己的「复制」功能做 I/O 基线

这是目前唯一能把「存储」和「我们的代码」一刀切开的实验,而且零改动。

插件里 TASK_COPY 是已实现功能,并且对 ≥ 256 MiB 的文件自动走 copy_file_pipeline()(src/filemgr.c:836,阈值见 :37):3 个 slot × 8 MiB、4096 字节对齐、 独立读线程 + 独立写线程。也就是说,它就是「同一台 PS5、同一对设备、同一份代码, 只把解压那一步换成纯搬运」。

操作:把那个 18 GB 的包,用插件自己的复制功能,复制到解压时输出所在的那块盘上。 记下耗时 T_copy。

判读:

观察到 结论 下一步
T_copy ≈ 10 分钟或更多 存储物理极限,与我们的代码无关 收工。解码优化全部搁置
T_copy ≈ 1–2 分钟(远小于 660 s) 我们的解压比同一对设备上的纯 I/O 慢 5–10× ⇒ 代码里有真问题 进第 2、第 4 步
介于两者之间 混合 按字节数把 I/O 分量扣掉,差值才是我们能动的部分

严格比较要看总移动字节:复制读 18 GB + 写 18 GB = 36 GB;解压读 ≤ 18 GB、写 = 解压后大小。 所以「复制 36 GB 用了多久」和「解压搬了 (18 GB + 解压后大小) 用了 660 s」才是同一口径。

为什么这个实验优于第 3 步那一堆:它不需要造新包、不需要两块盘、不需要任何假设, 而且测的就是出事的那对设备。第 3 步只在第 1 步结论模糊时才需要。


第 2 步:读写粒度审计 —— 目前唯一可疑的代码级病因(⚠️ 对 RAR 不适用)

2026-09-23 晚加注:本节整段只对 7z / ZIP 有效。 它比较的是「引擎内部缓冲大小」, 而 RAR 的归档 I/O 在 UnRAR 自己的 File 类里(我们按路径打开, src/rar_extract.c:1059),我们的回调只收到解压后的数据 —— 读粒度我们改不了。 唯一还能对上 RAR 的那半句是「写入粒度」,而 RAR 的写出也在 UnRAR 内。 ⇒ 真机工作负载是 RAR 时,本节没有可操作性;留着是为了 7z/ZIP 场景。 顺带:本表里 RAR 那行写的 4 MiB 是 File::CopyBufferSize() (third_party/unrar7/file.hpp:148-153),那是文件复制的缓冲,不是解压读取缓冲 —— 原文引用错了行,这条勘误一并记在这里。

把四条路径的单次请求大小摊开看(已核对源码):

路径 源侧读粒度 目标侧写粒度
复制(pipeline,≥ 256 MiB) 8 MiB × 3 slot,4096 对齐,独立读线程 8 MiB
7z chain SZ_IN_CHUNK = 256 KiB(src/sevenz_chain.c:87);MT 路径 inBufSize_MT = 1 MiB SZ_OUT_CHUNK = 64 KiB(src/sevenz_chain.c:94)
ZIP ZIPX_IO_BUFFER = 128 KiB(src/zip_extract.c:32) 128 KiB(src/zip_extract.c:900)
RAR UnRAR 内部 File::CopyBufferSize() = 4 MiB(third_party/unrar7/file.hpp:148-153) 4 MiB

⇒ 结论一:三个引擎都不是「小读」病理,最小也有 128 KiB,不是 4 KB 那种灾难。

⇒ 结论二:但都比复制小 32–64 倍。

为什么这可能正是病根 —— 在延迟主导的设备上,吞吐 ≈ 单次请求大小 ÷ 每次请求的等效延迟:

256 KiB / 10 ms =  25 MB/s    ← 正好落在实测 10–40 MB/s 的中间
  8 MiB / 10 ms = 800 MB/s    ← 复制路径不会撞这个上限

这个算术顺带解释两件事:

  • 为什么吞吐会 4× 摆动:等效延迟随设备状态(HDD 寻道、USB 桥接、写缓存回刷)变化, 线性映射到吞吐上就是大幅波动。
  • 为什么「条目平均 14.7 MB」没能救我们:per-entry 成本的确不是主因,但读请求粒度是另一回事 —— 它由引擎内部缓冲决定,与条目大小无关。

最可能的场景是源和目标在同一块盘(外置 USB HDD 上解压到同一块盘):读流和写流同时存在, 磁头来回跑,OS read-ahead 被写回刷反复打断,于是每次请求退化成一次寻道 —— 这正好让上面那个 算术成立。第 1 步如果出现「复制明显快于解压」,就是在支持它(复制的寻道次数只有解压的 1/32)。

如果第 1 步指向这里,改动面其实很小:所有 7z 的读都只经过 src/sevenz_volstream.c 的 vol_read() 这一个函数(ZIP/RAR 同理在 src/zipx_volstream.c)。在那里加一层 read-ahead (向后 seek 时丢弃缓存)即可,不需要动解码器。但这要等第 1 步的结论,现在不写。


第 3 步:四个对照实验(零改动)—— 第 1 步结论模糊时才需要

实验 做法 若结果为 A 若结果为 B
A 换简单包 造一个「单一大文件(如 10 GB 伪随机数据)」的 zip,放内置 SSD,解到内置 SSD 吞吐跳到 150+ MiB/s ⇒ 慢在条目数 / per-entry 开销 仍是 10–40 ⇒ 慢在存储或 I/O 模式,与条目数无关
B 换源设备 同一个包,分别放内置 SSD 与外置 USB 两者差别巨大 ⇒ 就是源盘带宽 两者一样慢 ⇒ 不是源盘
C 换目标设备 同一个包,分别解到内置与外置 差别巨大 ⇒ 写入端是瓶颈 一样慢 ⇒ 不是目标盘
D 同盘 vs 异盘 包与输出在同一块盘 / 在两块盘 同盘明显更慢 ⇒ 读写争用(这也是第 2 步最看好的假设) —

参考量级:机械/低成本 USB HDD 顺序读写约 30–80 MB/s,小文件随机访问降到 1–10 MB/s。


第 4 步:只有在第 1/3 步指向「我们的代码」时才做(要改代码)

  1. 阶段计时:在四个阶段边界累加时间戳 —— scan / read+decode / write staging / publish。用 clock_gettime(CLOCK_MONOTONIC, ...)(该原语已在三个引擎里用于进度上报, 真机可用)。收尾用 printf 打进日志。
  2. 拆开 read+decode:这一段目前分不开。可用「同一归档跑两遍(第二遍吃页缓存)」或 「解到 /dev/null 类目标」来逼近 I/O 与解码的分界。
  3. SZX_MT_THREADS 运行时可配:现在是 src/sevenz_mt.h:21 的编译期 #define 8, 做线程数 A/B 必须重编四次。改成运行时读(默认仍 8)会让实验便宜很多。
  4. 读粒度 A/B:SZ_IN_CHUNK / ZIPX_IO_BUFFER 加大到 1–4 MiB 各构建一版, 看第 1 步指出的那条曲线是否真的跟着动。
  5. 埋点要用编译开关控制,关闭时零开销 —— 避免影响已发布的产物指纹。

已排除的项(不要再花时间)

⚠️ 本表 2026-09-23 晚已按「格式 = RAR」重排。原表里「解码整体就不是瓶颈」这条前提已撤回, 所以同一批项现在被分成两类:与 RAR 无关(不用看)、以及真已排除。两类都不要花时间。

A. 与 RAR 工作负载无关(它们是 7z / ZIP 的项;RAR 解码在 rarlab 库里,我们碰不到)

项 为什么无关
CRC 硬件化 对 7z/ZIP 才值 4–5%;RAR 的 CRC 由 UnRAR 自己算
MT 扩到 BCJ2 / 多 folder 纯 7z 概念
ZIP inflate 换 libdeflate ZIP 专属
AES-NI 只影响 ZIP 加密流与 7zAES;RAR 加密走 UnRAR 自己的 rijndael
读粒度 / read-ahead(#48) RAR 按路径自读(src/rar_extract.c:1059 的 od.ArcName),回调只收解压后数据,插不进去

B. 真已排除(与格式无关,或已结论)

项 为什么排除
与 7-Zip 比倍数 真机上没有可比对象(上游那个 wfm-7zip-helper.elf 单独分发,本仓刻意不走 helper 路线);而且那是 7z 的对比,与 RAR 负载无关
逐条目 fsync 已经全部移除(2026-09-16)
「条目太小」 1252 条 / 18 GB,平均 14.7 MB,不是小文件场景
「我们的 RAR 解码器写得慢」 解码就是 vendored 的 UnRAR 库本体,我们只剩一层 facade

C. 唯一还站着的、且直接针对真实负载的一项 ⬅ 优先看这个

项 状态
UnRAR RAR5 多线程解压(RAR_SMP + -lpthread) ❌ 不做(2026-09-23 用户决定,见文首终局块)。我们没打开它;收益已实测(PC 上 2.45× / 我们引擎 1.40–1.74×)但被「写路径是否为墙」卡住,而验证它的 T_copy 未执行 ⇒ 搁置