# 1.2.1 Mac 适配与测试记录(2026-10-19) 1.2.1 是玩家最多的版本。它只改了主模块 `main` 和 `main.npdm`;`sdk`、`subsdk0`、`rtld` 和 1.0.0 / 1.1.2 逐字节相同,所以走 1.1.2 那条路(同一套系统服务,不需要 1.4.3 那样另写补丁文件)。本轮只测 Mac(Apple M4、16 GB、macOS 26.5.2),存档一律用副本。 | 范围 | 结果 | 边界 | |---|---|---| | 识别与资源 | 按模块哈希认出 1.2.1;合并 RomFS 304,473 个文件,六层 IVFC 全部校验 | 只认精确哈希的 1.2.1 | | 代码改写 | 三个模块结构检查与逐点等价全部通过,600 个反向对照全抓到 | 等价检查用上游的独占访问模型,不证明并发正确 | | 补丁地址 | 32 个地址全部从 1.1.2 映射过来,原指令字都没变 | 用工具映射,再由模拟和实机验证 | | 实际游玩 | 读档(神庙、野外、1.0.0 和 1.1.2 两种旧存档)、移动 / 跳跃、菜单 / 地图 / 快捷菜单 / 自动组装、手动存档、新进程读档、摔死重试、3 键画质三档、巡检移动、35 分钟长局 | 没有做全剧情或全地图 | ## 识别 - 更新包:`UPD/1.2.1.nsp`(355,645,252 字节)。CNMT `Patch_0100f2c0115b6800.cnmt` 的标题版本 393216,Control NCA 里 NACP 的版本字段(0x3060)是 `1.2.1`。 - ExeFS(SHA-256):`main` e5aa8d48…efcb6、`main.npdm` 7df836b8…35af39;`sdk` 91573d44…、`subsdk0` 9d04c989…、`rtld` 28317605… 与 1.1.2 相同。完整哈希在 `games/totk/assets/expected-v1.2.1.json`。 - `build_app.py` 按这些哈希选版本,不看文件名。 ## 代码改写与验证 - 改写:主模块 31,475 个改写点;`sdk` 和 `subsdk0` 的改写结果与 1.1.2 逐字节相同。 - `verify_rewrites.py --version v121`:结构检查 54,640 个改写点,逐点等价 54,540 个(sdk 的 100 个系统调用 / 计时器门只做结构检查),168,149 次试验中 145,616 次有效,分歧 0;600 个故意破坏的对照全部抓到。 - 原子窗口:测试会扫描主模块里所有“`LDXR w17` / 运算 / `STXR` 状态 w18 / `CBZ w18` 跳出窗口”的窗口,并和列表核对。1.0.0、1.1.2 各两个(就是原来写死的那两个),1.2.1 只有一个(`0x7ff694`):1.1.2 `0x74cb68` 那个函数在 1.2.1 里换了寄存器分配,不再用 x18。128 个场景通过,改错出口的反向对照被抓到。 - 调用窗口差分测试和真机调用探针(4 线程 20,000 次)通过。 ## 补丁地址 `games/totk/scripts/map_version_sites.py`(新工具): - 代码地址:取 1.1.2 里补丁点前后的指令,去掉 PC 相对字段(B / BL、条件分支、CBZ / TBZ、ADR / ADRP、字面量 LDR)和 ADRP 之后的页内偏移,在 1.2.1 主模块里搜索;窗口从 ±6 个字逐步加大,直到只剩一处匹配。 - 数据地址:通过访问它的代码(ADRP + LDR / ADD)映射;导入函数的 GOT 和 PLT 按 JUMP_SLOT 的符号名找。 - 函数入口:如果入口附近的指令找不到唯一匹配,就看它的调用者,所有调用点映射过去后 BL 必须指向同一处。 - 自检:从 1.0.0 映射到 1.1.2,32 个地址和 `game_version.h` 里已有的 1.1.2 表全部一致。 - 1.1.2 → 1.2.1:32 / 32 找到,每个补丁点的原指令字都没变(输出尺寸那条 BL 也是 `0x94000083`)。`forceSetMatrix` 由 49 个调用点中的 31 个一致定位,函数体除跳转目标外和 1.1.2 完全相同,所以角色字段沿用 0x218 / 0x2b4 / 0x2c0(实机巡检读写位置验证过)。 - `verify_fps60_patch.py`、`verify_realtime_patch.py` 用 1.2.1 的改写映像模拟出图间隔控制器和真实时间步长,正例和反向对照都通过。 ## 资源与覆盖文件 - 合并后的 RomFS:17,052,047,360 字节,SHA-256 `c2beb1c2…937c11`,304,473 个文件,用 301 秒。 - 快捷菜单背景:1.2.1 只有 `UI/LayoutArchive/Common.Product.110.Nin_NX_NVN.blarc.zs`,四个可见性关键帧的原值和 1.1.2 一样,照样改成第 0 帧截图。实机武器快捷菜单背景正常。 - 2K:`HeapDef` 的两个堆仍是出厂值(113,664 / 201,728 KiB),2560×1440 加 89,088 KiB。实机 3 键切到原生 2K 时 WindowCrop 变为 2560×1440,界面按 1440p 重排。 - GAME OVER:1.2.1 的 `EventFlowFileEntry.Product.121` 写的是 `DmF_SY_GameOver` 版本 110,执行的是 `.110` 那份(EventWaitUpdate,不会被暂停冻住)。1.0.0 那份的覆盖文件照常生成;林克死亡时游戏也会打开它(日志里有覆盖记录),但不执行。关掉覆盖文件(`TOTK_ROMFS_PATCHES=0`)摔死后选“重试”,照样能读档。 - 可视计数着色器:1.2.1 在野外用的就是 1.1.2 的那个(键 `b1e4d090…-641742ac59e3`,代码哈希 `EABE9F…`),翻译器的降级照常生效。`verify_vertex_counter.py --version v121`:96 个 GPU 场景通过,反向对照被拒绝,重新翻译的结果和游戏里翻出来的逐字节相同。 - 存档:1.2.1 写出的存档和 1.1.2 同一种格式(2,307,656 字节,文件头 `0x47E0F4`)。 - 过场影片:`Event/Movie` 下的 28 个 WebM 和 1.0.0 逐字节相同(1.0.0 的 28 个都过过逐字节的解码探针),影片宿主接管的 `subsdk0` 也相同。 ## 运行时检查 - `verify_loader.py --version v121`:主模块 PLT → sdk → 宿主计时器,4 线程共 4,000 次调用;重定位、内存权限正确,坏映像被拒绝。 - `verify_romfs.py --version v121`:304,473 个文件抽查 65 个,与独立读取一致;3 份覆盖文件校验通过;坏索引被拒绝。 - `verify_boot.py --version v121`:两次启动,三个模块的构造函数全部跑完(main 10,673 项、subsdk0 45 项、sdk 116 项),nnMain 的返回点是 `0xe7a954`。 ## 实际游玩(工作目录编出的程序,存档副本) 测试程序放在和桌面 App 游戏目录路径一样长的文件夹里(102 个字符),这样 Metal 的着色器缓存能用上桌面 App 已经编好的库,也不会挤掉它的条目(见原项目 claude.md 经验 105)。 1. 神庙存档(伊黑恩阿神庙,selda 存档 slot_00):3.9 秒出第一帧,41 秒进世界(每帧约 5,400 次绘制)。巡检 `survey pos` 读到玩家角色;`survey move` 把林克挪到 Z=88,位置读数跟着变。走路、跳跃、暂停菜单(武器页、系统页)、地图、武器快捷菜单正常。3 键三档:MetalFX 放大(1600×900 → 1920×1080)、原生 2K(2560×1440)、关闭。手动存档写进 slot_01。音频欠载 0、溢出 0。关窗口正常退出。 2. 新进程读刚才那份 1.2.1 存档:读到 slot_01。在神庙里存的档,读档后从神庙起点开始(这是游戏本身的规则;存档里记录的是存档时的位置)。 3. 野外存档(米法公园,slot_05):走路、手动存档(2026/10/19 01:22);把林克挪到上方 375 米摔死,GAME OVER 选“重试”后读档回到摔之前的位置。 4. 新进程读野外存档:林克正好出现在存档位置 (3808.51, 590.20, -570.94)。同一局关掉覆盖文件做摔死重试的反向对照:照样读档(见上面 GAME OVER 一条)。 每局红字只有 3 行,都是以前就有的:`SetPointSize`、`SetLineWidth`、一处预排布贴图回退到 CPU。第 4 局边玩边翻译了 800 多个新着色器(玩家缓存来自 1.4.3 / 1.0.0,1.2.1 有自己的变体),没有一个失败;打包时的首启种子有 8,286 个着色器(0 个跳过)。 ## 打包(`build_app.py`) - 命令:`python3.14 build_app.py <本体 XCI> -k --update <1.2.1.nsp> -o `,工作目录里已有的结果直接复用:共 10 分钟。其中签名约 5 分钟,macOS 第一次检查新 App 208 秒,在 App 的游戏目录里预编 8,286 个启动着色器 98 秒(新进程抽查 64 / 64 命中)。 - App:签名校验通过;`CFBundleShortVersionString` 1.2.1,`CFBundleVersion` 393217;同卷克隆,大文件不额外占空间。 - 直接运行 App 里的游戏程序(存档、缓存用副本):带着 1.4.3 的着色器缓存第一次开,会把 1.2.1 的种子(24,858 个文件)导入缓存,一次性约 31 秒;标题管线等 128 ms;Metal 缓存热时,后台预热 17,780 个着色器只要 6.9 秒,第一次进世界不用等。 有一局预热用了 237 秒、第一次进世界等满了 15 秒:当时 Metal 的共用缓存被别的程序挤冷了(同一时间另一个会话在测 iOS),再开一局就恢复正常。 - 通过启动器打开 App(`open`,存档副本放在系统盘的 `TOTK/tests/` 下):启动器先把存档备份成 `profile-before-1.2.1`,游戏停在标题,按 A 读档进世界,关窗口正常退出。 - 注意:重新打包的临时签名和以前授权过的不一样,App 第一次访问外接卷上、包以外的文件时,macOS 会弹“想访问可移除宗卷上的文件”。玩家的存档和缓存都在 `~/Library`,平时玩不会碰到;只有像我第一次那样把测试存档放在外接盘上才会弹。 ### 接到统一清单之后重新打包(10-19 早上) 同一个命令,21 分钟(工作目录里的解包、改写结果复用): - 首启种子:扫描整个 RomFS 和游戏程序,找到 27,663 个着色器。启动用的 12,415 个各做默认变体,清单里属于 1.2.1 的 12,233 个绑定状态各做一个,共 16,024 个着色器、46,933 个文件(准备 183 秒)。 69 个默认变体翻译不出来,跳过了(1.4.3 跳过 84 个,是同一类);游戏里真用到时照常现场翻译。 - 预编着色器库:去掉内容相同的,剩 15,425 个,用 Metal 工具链编了 4 分 46 秒,没有一个失败。游戏按 MSL 的 SHA-256 直接加载这些库,不靠 Metal 的缓存。 - macOS 第一次检查新 App 215 秒。 - 实测(存档副本): | 怎么开 | 结果 | |---|---| | 直接运行 App 里的游戏程序,带着玩家 1.4.3 时期的缓存 | 导入种子 38 秒(一次性);第一帧前等 642 ms;后台预热 25,462 个着色器 54.7 秒,约 2.2 万个直接加载预编的库,约 3,150 个从 MSL 现编(缓存里别的版本的着色器);读档进世界正常,红字只有那 3 行老的,音频 0 欠载 | | 通过启动器第一次打开 | RomFS 校验 19.7 秒,核对种子 16 秒,导入 25 秒,64.6 秒出第一帧;后台预热 6.6 秒 | | 通过启动器第二次打开 | 6.8 秒出第一帧(RomFS 记录复用,种子不再导入),第一帧前等 346 ms;按 A 读档进世界正常。标题画面后台预热的那几秒有 1 次音频欠载(16 帧) | - 启动器会去掉 `TOTK_AUTOLOAD`(游戏从标题开始,和主机一样),测试时要自己按 A 读档(命令通道 `key 38 200`)。 - 通过启动器打开时,游戏进程属于 App:命令通道里 `shot` 的路径也不能放在外接盘上,不然同样弹“可移除宗卷”的权限框,游戏线程卡在写文件上 (`shot` → `CGImageDestinationCreateWithURL` → `open`),画面停住。截图写到 `~/Library/Application Support/MacNativeGames/TOTK/tests/` 下。 ## 长局(打包版) App 里的游戏程序、野外存档副本,每 60 秒传送到一个城镇(卡卡利科、哈特诺、利特、鼓隆、卓拉领地、格鲁德、监视堡垒),5 圈共 35 站、35 分钟: - 每站都落在地面上,同一个城镇每圈落点一样;截图画面正常。利特村的落点在一间木屋里,有两圈镜头被墙挡住、整屏一块平面,转动镜头后看清是室内,不是渲染错误。 - 红字一直是那 3 行老的;音频欠载 0、溢出 0;最后正常关闭。 - 显卡已分配 2,722 → 3,573 MB(峰值 3,639 MB)。这是所有版本都有的“显卡已分配随时间增长”(原项目待办),不是 1.2.1 引入的;照这个速度连续传送约 80 分钟会碰到 5,000 MB 的预算上限。 ## 自动组装界面 selda 的后期存档有自动组装:按住 L 打开能力轮盘,选右下方的“蓝图”,松开后是设计图列表,背后显示游戏画面,不是黑屏。`AutoBuilder_00_In.bflan` 的覆盖文件(截取背景改在第 0 帧)以前一直没有在游戏里看过,这是第一次验证。 ## 统一着色器清单(2026-10-19 早上) - 1.2.1 的着色器字节和 1.4.3 大多不同(键是控制段加代码的哈希):旧种子 8,286 个着色器里只有 1,043 个已在清单里,而 1.4.3 的 8,290 个早就全在。 - 把 9 局 1.2.1 测试的翻译缓存(`shader_list.py merge`)并进清单:这些局有 8,298 个清单里没有的变体,含旧种子的默认变体和边玩边翻译的约 1,250 个。其中 14 个同时也被 iPhone / iPad 的补译加进了清单(`08937a0`),所以接到 main 上新增 8,284 个(18,548 → 26,832 个变体,16,591 → 23,867 个着色器)。 - `locate_shaders.py --romfs extracted/v121/romfs --modules rewritten/v121`:不用加定位行(打包程序自己就能在 BNSH、`.sharcb` 和游戏程序里找到这些着色器)。 - 管线(`S` / `C` / `R` 预热行)没并:这些测试局多记了 11,052 行,但清单的预热行只收整理过的标题和预热清单,玩家玩出的管线留在各自存档的 `pso_user.tsv` 里。 - 打包时的种子按新流程做:扫描整个 RomFS 和游戏程序,启动用的着色器做默认变体,清单里有的着色器按清单的绑定状态各做一个。 ## 1.4.3 写过的存档 1.4.3 打开过的存档里 `option.sav` 是 1.4.3 的格式(文件头 `0x49E946`,880 字节)。1.2.1 照样能读:开局标题无提示,选项界面里显示的是存档里的设置(语音语言、小地图向北固定等不是默认值)。`progress.sav` 要是被 1.4.3 写过(同一文件头),1.2.1 读不了;回到 1.2.1 之前要用 `profile-before-1.4.3` 之类的备份,或用 1.2.1 时期的存档。 ## 未做 - 全剧情、全地图和帧率对比;1.2.1 新增内容(例如新的事件流)没有专门走到;比 35 分钟更长的局。 - iPhone / iPad 上的 1.2.1:IPA 构建里和版本有关的步骤做过,`build/totk-native-v121 rewritten/v121 export-ios` 导出时主模块打了 17 个字(真实时间 10、60 帧 7;iOS 只出 720p,不打输出尺寸补丁),`ios/tools/gen_embed.py` 通过;Xcode 构建、安装和真机都没做。