The Legend of Zelda: Breath of the Wild (1.9.0) reaches the Shrine of Resurrection on the Mac, with a 60 fps cut; deferred texture binds read at execution

- Profile 01007EF00011E000 (SDK 20.5.22, NVN 55, main + subsdk0 + sdk): 19,592 shaders, all translated (chain bd3979ea9b3d).
- Loader applies DT_RELR relative relocations (SDK 20; main has 223,994); nvn_slots_got.py and DM_LOADHASH read them too.
- 60 fps: five rewriter cuts lower the GameFrameworkNx dynamic-interval floor from 2 to 1; ksys::VFR scales the
  logic time step by the interval (ratio 0.5 at 60 fps, falls back to 30 with ratio 1 when frames run long).
- Black 3D scene: nvnCommandBufferBindTexturesDeferred reads the handles at execution (bh_fresh); the engine
  records once and fills the handles later, so the lighting composite sampled texture 0.
- A texture attached at two colour slots is attached once (TBDR tile stores raced, albedo kept scattered tiles).
- Cube textures are 6-layer 2D arrays so tex_fit can bind cube-array views (1,512 shaders sample cube arrays).
- NVN functions other than command recording run on the per-thread host stack (Metal compiled on a small guest stack).
- Address-to-pool lookups use the last hit plus a sorted index (1,760 pools): submit time 21 ms -> 7 ms per frame.
- SDK 20: ARUID hook, audio renderer layout (Span GenerateCommand, new offsets, DeviceSink ID 0x14), nfp events,
  VR mode, seven-six-axis sensor, steady clock, launch parameter, stereo vision permission.
- Compressed render targets (BC4 via an RGBA16UI view) are skipped with a red line instead of a Metal abort.
- DM_KEYS takes frame numbers (f2500:A); TEXDUMP saves 2D-array layers; PASSDUMP prints skipped passes.
- NSP script skips empty NSP files and keeps one copy per DLC.
This commit is contained in:
seedds committed 2026-10-16 16:07:15 +08:00
1 parent 68cfe6c47e
commit abcfdf9215
30 files changed
+32839 -62

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Notes for agents
- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it. Super Mario Bros. Wonder is supported (base game v0 and update 1.2.1, profile `010015100B514000`); Fire Emblem Engage (update 2.0.0, profile `0100A6301214E000`) is in bring-up and already packs into an IPA. Metroid Dread (update 2.1.0, profile `010093801237C000`) is in bring-up: on the Mac a new game reaches gameplay. 13 Sentinels: Aegis Rim (update 1.10, profile `01003FC01670C000`) is in bring-up: on the Mac a new game reaches the first battle. Super Mario 3D World + Bowser's Fury (update 1.2.2, profile `010028600EBDA000`) is in bring-up: on the Mac 3D World reaches World 1-1 and Bowser's Fury its open world. Start with [README.md](README.md) for status and build steps.
- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it. Super Mario Bros. Wonder is supported (base game v0 and update 1.2.1, profile `010015100B514000`); Fire Emblem Engage (update 2.0.0, profile `0100A6301214E000`) is in bring-up and already packs into an IPA. Metroid Dread (update 2.1.0, profile `010093801237C000`) is in bring-up: on the Mac a new game reaches gameplay. 13 Sentinels: Aegis Rim (update 1.10, profile `01003FC01670C000`) is in bring-up: on the Mac a new game reaches the first battle. Super Mario 3D World + Bowser's Fury (update 1.2.2, profile `010028600EBDA000`) is in bring-up: on the Mac 3D World reaches World 1-1 and Bowser's Fury its open world. The Legend of Zelda: Breath of the Wild (update 1.9.0, SDK 20, profile `01007EF00011E000`) is in bring-up: on the Mac a new game reaches the Shrine of Resurrection, with a 60 fps cut. Start with [README.md](README.md) for status and build steps.
- Each game's constants and shader tables form a game profile in `dm/games/<id>/`, where `<id>` is the base game's title ID: `game.tsv` plus the tables and `dmsh_chain.h`. Mario's profile is `010015100B514000`. Tools pick the profile with `DM_GAME`; `make -C dm/host` and `build_ipa.sh` pick it with `GAME`. There is no default game: set it every time (the tools stop and list the profiles otherwise). Put per-game values in the profile, not in code ([dm/games/README.md](dm/games/README.md)).
- Per-build knowledge is keyed by the NSO build ID: rewriter cuts in `dm/tools/patch_nso.py`, the profile's `build` rows (the builds the NSP script accepts, and their display versions), and host behaviour that uses fixed module addresses checks `module_t.build_id` (from `fixups.tsv`; `dm_main_build()`, `DM_D2R_BUILD` for code inherited from D2R). When a new game build appears, carry addresses over with `dm/tools/nso_fnmap.py` and check them by disassembly; never let fixed addresses run on a build they were not found in.
- To analyse or port **another** Switch title, first run `scripts/build_ipa_from_nsp.py` on its NSP folder. With no matching profile, it writes a survey to `<NSP folder>/<title ID>_survey/survey.md`. Then follow [docs/agents](docs/agents/README.md) (extraction, binary survey, host bring-up failure catalogue, graphics specifics, debugging recipes) and add a profile.
+1
View File
@@ -30,6 +30,7 @@ This project is built on other people's work:
| Metroid Dread | `010093801237C000` | the base game and update 2.1.0 | **In bring-up, Mac only so far.** A new game runs on the Mac through the title, the story recap and the opening cutscene into the first area of ZDR, at 60 fps with movies and music. No IPA has been tested yet |
| 13 Sentinels: Aegis Rim | `01003FC01670C000` | the base game and update 1.10 | **In bring-up, Mac only so far.** A new game runs on the Mac through the logos, the title, the prologue and its cutscenes into the first battle, at 60 fps with movies and sound. No IPA has been tested yet |
| Super Mario 3D World + Bowser's Fury | `010028600EBDA000` | the base game and update 1.2.2 | **In bring-up, Mac only so far.** On the Mac, Super Mario 3D World runs from the game select through its title, the player join screen and the opening cutscene into World 1-1, at 60 fps with sound; a 5-minute session in the level ran with no crash and flat memory. Bowser's Fury runs too: its title, the opening cutscene and the open world of Lake Lapcat. No IPA has been tested yet |
| The Legend of Zelda: Breath of the Wild | `01007EF00011E000` | the base game and update 1.9.0; DLC NSPs are packed too when present | **In bring-up, Mac only so far.** On the Mac it runs through the title menu and a new game into the Shrine of Resurrection with sound and lighting. A patch to the game's frame-interval logic lifts its 30 fps limit to 60 without changing the game speed (it falls back to 30 by itself when a frame takes too long). In the shrine the Mac holds 60 fps at the handheld resolution (`DM_OPMODE=0`, 1280×720) and averages about 48 at the docked 1600×900. No IPA has been tested yet |
For any other game, the script writes a survey instead of an IPA (see [Port another game](#port-another-game)).
+1
View File
@@ -0,0 +1 @@
# generated
1 # generated
+3
View File
@@ -0,0 +1,3 @@
# chain b-bd3979ea9b3d 2026-10-16T13:54:31+08:00
21fe759b8114941c
60e2041ed375cae1
1 # chain b-bd3979ea9b3d 2026-10-16T13:54:31+08:00
2 21fe759b8114941c
3 60e2041ed375cae1
+2
View File
@@ -0,0 +1,2 @@
// 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 01007EF00011E000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-<V>/
#define DMSH_CHAIN "bd3979ea9b3d"
+50
View File
@@ -0,0 +1,50 @@
# 塞尔达传说 旷野之息(Switch,基础版 v0 + 更新 1.9.0 = v1114112)的游戏档案。移植中
# Game profile of The Legend of Zelda: Breath of the Wild (Switch, base game v0 + update 1.9.0 = v1114112). In bring-up
# 格式见 dm/games/README.md | Format: see dm/games/README.md
# 任天堂 EPD 引擎(sead / agl / gsys / ksys,NintendoWare G3d / Ui2d / Vfx / Atk,Havok),SDK 20.5.22,NVN;模块 main + subsdk0(NintendoSDK_movie)+ sdk。
# RomFS 几乎全是 Yaz0(.s* 扩展名)与 SARC(Pack/*.pack)。着色器:Pack/Bootup_Graphics.pack 里的 Shader/*.sbfsha(BNSH)与 gsys / ksys 资源包,TitleBG.pack 的地形资源包,Effect/*.sesetlist 粒子特效,布局 .sblarc
# Nintendo EPD's engine (sead / agl / gsys / ksys, NintendoWare G3d / Ui2d / Vfx / Atk, Havok), SDK 20.5.22, NVN; modules main + subsdk0 (NintendoSDK_movie) + sdk.
# RomFS is almost all Yaz0 (.s* extensions) and SARC (Pack/*.pack). Shaders: Shader/*.sbfsha (BNSH) and the gsys / ksys resource packs in Pack/Bootup_Graphics.pack, the terrain resource pack in TitleBG.pack, particle effects in Effect/*.sesetlist, layouts in .sblarc
# ---- 身份 | Identity ----
title_id 01007EF00011E000
program_id 01007EF00011E000
# ---- App ----
# 游戏名:只给档案列表与 Mac 窗口标题当标签;iOS App 名取基础版 NSP 里的游戏名,包名按基础版标题号(build_ipa.sh / NSP 脚本)
# Game name: only a label for profile listings and the Mac window title; the iOS app name is the game name in the base NSP, and the bundle ID comes from the base game title ID (build_ipa.sh / NSP script)
name The Legend of Zelda: Breath of the Wild
ipa ZeldaBreathOfTheWild.ipa
app_category public.app-category.adventure-games
# ---- 版本 | Versions ----
# 只解了合并 1.9.0 的那份;基础版单独的构建号用到时再补 | only the merged 1.9.0 copy is extracted; the base game's own build IDs are added when needed
build v1114112 1.9.0 main=cd57b23fa4bbad65803d9788c01821ee subsdk0=f39f4c431234c72eb520cfdd1001bbcf sdk=574284b40c06cf1821a44857939083be
package v1114112
shader_versions v1114112
# ---- 文件 | Files ----
modules main subsdk0 sdk
romfs_marker Pack/Bootup_Graphics.pack
zip_store .sstera:.sbactorpack:.bfstm:.sbfres:.sbmaptex:.bars:.sbitemico:.shknm2:.shktmrb:.sbeventpack:.pack:.sblwp:.sstats:.shksc:.smubin:.ssarc:.sesetlist:.webm:.jpg:.metallib
# ---- 内存 | Memory ----
# NPDM 不声明系统资源(META +0x14 = 0):走 SetHeapSize。先用十三机兵防卫圈的分区(块 4 GB = 前置 256 MB + 别名区 256 MB + 栈区 256 MB + 堆区 3.25 GB),实测后再定
# The NPDM declares no system resource (META +0x14 = 0): the game uses SetHeapSize. Starts from 13 Sentinels' partition (4 GB block = 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region) until measured
mem_reserve 0x100000000
mem_alias 0x10000000
mem_stack 0x10000000
app_pool 0xCD500000
# ---- NVN 驱动常量段(dmsh_build.py 出包、nvn_mtl.m 运行时现填) | NVN driver constant segments (dmsh_build.py at packaging, nvn_mtl.m filled at run time) ----
# 先沿用马力欧的,按全库着色器实测后再定 | Mario's values until measured over all shaders
nvn_tex_base 0x20
nvn_tex_slots 32
nvn_samp_base 0x568
nvn_img_base 0x120
nvn_img_slots 8
nvn_ssbo_bases vs=0x110 ps=0x510 cs=0x310
# ---- 着色器拆分(shader_split.py) | Shader split (shader_split.py) ----
shader_groups /Shader/=shader /Effect/=effect /Layout/=layout /System/=gsys /Terrain/=terrain
shader_note 塞尔达传说 旷野之息着色器
1 # 塞尔达传说 旷野之息(Switch,基础版 v0 + 更新 1.9.0 = v1114112)的游戏档案。移植中
2 # Game profile of The Legend of Zelda: Breath of the Wild (Switch, base game v0 + update 1.9.0 = v1114112). In bring-up
3 # 格式见 dm/games/README.md | Format: see dm/games/README.md
4 # 任天堂 EPD 引擎(sead / agl / gsys / ksys,NintendoWare G3d / Ui2d / Vfx / Atk,Havok),SDK 20.5.22,NVN;模块 main + subsdk0(NintendoSDK_movie)+ sdk。
5 # RomFS 几乎全是 Yaz0(.s* 扩展名)与 SARC(Pack/*.pack)。着色器:Pack/Bootup_Graphics.pack 里的 Shader/*.sbfsha(BNSH)与 gsys / ksys 资源包,TitleBG.pack 的地形资源包,Effect/*.sesetlist 粒子特效,布局 .sblarc
6 # Nintendo EPD's engine (sead / agl / gsys / ksys, NintendoWare G3d / Ui2d / Vfx / Atk, Havok), SDK 20.5.22, NVN; modules main + subsdk0 (NintendoSDK_movie) + sdk.
7 # RomFS is almost all Yaz0 (.s* extensions) and SARC (Pack/*.pack). Shaders: Shader/*.sbfsha (BNSH) and the gsys / ksys resource packs in Pack/Bootup_Graphics.pack, the terrain resource pack in TitleBG.pack, particle effects in Effect/*.sesetlist, layouts in .sblarc
8 # ---- 身份 | Identity ----
9 title_id 01007EF00011E000
10 program_id 01007EF00011E000
11 # ---- App ----
12 # 游戏名:只给档案列表与 Mac 窗口标题当标签;iOS App 名取基础版 NSP 里的游戏名,包名按基础版标题号(build_ipa.sh / NSP 脚本)
13 # Game name: only a label for profile listings and the Mac window title; the iOS app name is the game name in the base NSP, and the bundle ID comes from the base game title ID (build_ipa.sh / NSP script)
14 name The Legend of Zelda: Breath of the Wild
15 ipa ZeldaBreathOfTheWild.ipa
16 app_category public.app-category.adventure-games
17 # ---- 版本 | Versions ----
18 # 只解了合并 1.9.0 的那份;基础版单独的构建号用到时再补 | only the merged 1.9.0 copy is extracted; the base game's own build IDs are added when needed
19 build v1114112 1.9.0 main=cd57b23fa4bbad65803d9788c01821ee subsdk0=f39f4c431234c72eb520cfdd1001bbcf sdk=574284b40c06cf1821a44857939083be
20 package v1114112
21 shader_versions v1114112
22 # ---- 文件 | Files ----
23 modules main subsdk0 sdk
24 romfs_marker Pack/Bootup_Graphics.pack
25 zip_store .sstera:.sbactorpack:.bfstm:.sbfres:.sbmaptex:.bars:.sbitemico:.shknm2:.shktmrb:.sbeventpack:.pack:.sblwp:.sstats:.shksc:.smubin:.ssarc:.sesetlist:.webm:.jpg:.metallib
26 # ---- 内存 | Memory ----
27 # NPDM 不声明系统资源(META +0x14 = 0):走 SetHeapSize。先用十三机兵防卫圈的分区(块 4 GB = 前置 256 MB + 别名区 256 MB + 栈区 256 MB + 堆区 3.25 GB),实测后再定
28 # The NPDM declares no system resource (META +0x14 = 0): the game uses SetHeapSize. Starts from 13 Sentinels' partition (4 GB block = 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region) until measured
29 mem_reserve 0x100000000
30 mem_alias 0x10000000
31 mem_stack 0x10000000
32 app_pool 0xCD500000
33 # ---- NVN 驱动常量段(dmsh_build.py 出包、nvn_mtl.m 运行时现填) | NVN driver constant segments (dmsh_build.py at packaging, nvn_mtl.m filled at run time) ----
34 # 先沿用马力欧的,按全库着色器实测后再定 | Mario's values until measured over all shaders
35 nvn_tex_base 0x20
36 nvn_tex_slots 32
37 nvn_samp_base 0x568
38 nvn_img_base 0x120
39 nvn_img_slots 8
40 nvn_ssbo_bases vs=0x110 ps=0x510 cs=0x310
41 # ---- 着色器拆分(shader_split.py) | Shader split (shader_split.py) ----
42 shader_groups /Shader/=shader /Effect/=effect /Layout/=layout /System/=gsys /Terrain/=terrain
43 shader_note 塞尔达传说 旷野之息着色器
@@ -0,0 +1,4 @@
# 预热种子清单(随包,与 shader_map.tsv 同目录)。行:S<TAB>着色器键<TAB>阶段 / R<TAB>渲染管线键(pso_skey 十六进制)/ C<TAB>计算着色器键。
# Prewarm seed list (shipped in the app, next to shader_map.tsv). Rows: S<TAB>shader key<TAB>stage / R<TAB>render pipeline key (pso_skey hex) / C<TAB>compute shader key.
# 本作尚未录制(DM_PSO_RECORD):空表 = 不预热。行序 = 首次使用顺序,不要排序。
# Not recorded yet for this game (DM_PSO_RECORD): an empty list = no prewarm. Row order = first-use order; do not sort.
1 # 预热种子清单(随包,与 shader_map.tsv 同目录)。行:S<TAB>着色器键<TAB>阶段 / R<TAB>渲染管线键(pso_skey 十六进制)/ C<TAB>计算着色器键。
2 # Prewarm seed list (shipped in the app, next to shader_map.tsv). Rows: S<TAB>shader key<TAB>stage / R<TAB>render pipeline key (pso_skey hex) / C<TAB>compute shader key.
3 # 本作尚未录制(DM_PSO_RECORD):空表 = 不预热。行序 = 首次使用顺序,不要排序。
4 # Not recorded yet for this game (DM_PSO_RECORD): an empty list = no prewarm. Row order = first-use order; do not sort.
+102
View File
@@ -0,0 +1,102 @@
# 翻译查询的作答表(dm/gfx/tools/shader_split.py 生成,勿手改):键 <TAB> 事实 JSON。本作没有 PC 版可配对:只有计算着色器的线程组 / 共享内存(取自控制段),其余查询按翻译器缺省
001b98741aa774e4 {"local":[32,1,1],"shared":0}
062451f1f13eae17 {"local":[32,1,1],"shared":0}
1124a38a1f191344 {"local":[32,1,1],"shared":0}
13373680680393f0 {"local":[32,1,1],"shared":0}
16c462782a646283 {"local":[32,1,1],"shared":0}
173336ea299d58cd {"local":[32,1,1],"shared":0}
17f505f389a43c02 {"local":[32,1,1],"shared":0}
182564512706efcd {"local":[32,1,1],"shared":384}
1934321e2fbfcda0 {"local":[32,1,1],"shared":896}
1bb935252d92e196 {"local":[32,1,1],"shared":0}
214d7556782a21b3 {"local":[32,1,1],"shared":0}
218bb262d04661cf {"local":[32,1,1],"shared":0}
23dc462223b380a1 {"local":[32,1,1],"shared":0}
25e4eb98710b4fa0 {"local":[32,1,1],"shared":640}
27d03285db65b33e {"local":[32,1,1],"shared":896}
2837619a7b23fc7a {"local":[32,1,1],"shared":640}
289d1073af7c3180 {"local":[32,1,1],"shared":384}
2a9e49bfe72c1a0f {"local":[32,1,1],"shared":0}
2b5fa4afa73daa09 {"local":[32,1,1],"shared":0}
2bd9f808704ee1c8 {"local":[32,1,1],"shared":384}
2e8c68947f99e2e4 {"local":[32,1,1],"shared":0}
2ea577df6ef36adc {"local":[32,1,1],"shared":896}
313bfa0cd490ad8e {"local":[32,1,1],"shared":0}
31e1884af7bd3a8b {"local":[32,1,1],"shared":384}
33fff1fa395dfde4 {"local":[32,1,1],"shared":0}
3671b6eb169f3f90 {"local":[32,1,1],"shared":384}
384eed1970193bcf {"local":[32,1,1],"shared":384}
3aa5d4c8a495c230 {"local":[32,1,1],"shared":0}
3e6589bbfedc7abc {"local":[32,1,1],"shared":1408}
423336a84aca13de {"local":[32,1,1],"shared":0}
434604dc1ebc0915 {"local":[32,1,1],"shared":384}
4478648c2f5c4bf5 {"local":[32,1,1],"shared":1920}
48780eb476b6ff18 {"local":[32,1,1],"shared":1408}
4c2b6ad70e54af96 {"local":[32,1,1],"shared":384}
4d2e72f7a3f06f71 {"local":[32,1,1],"shared":384}
50c31a02d6d4703a {"local":[32,1,1],"shared":0}
515833da9e76da6b {"local":[32,1,1],"shared":0}
525782948f1a6120 {"local":[32,1,1],"shared":384}
59a9744d89d39d85 {"local":[32,1,1],"shared":640}
59f1985690ba229d {"local":[32,1,1],"shared":0}
5a63068dfc323e56 {"local":[32,1,1],"shared":0}
5a859b483c02c1ea {"local":[32,1,1],"shared":384}
5ace6deefb3af1f5 {"local":[32,1,1],"shared":0}
5b0fb434ddf9550d {"local":[32,1,1],"shared":896}
5bae2a469127cb9a {"local":[32,1,1],"shared":0}
5ea21e9e5ff8630f {"local":[32,1,1],"shared":0}
5ef78ed93f60c8c3 {"local":[32,1,1],"shared":0}
6500e4192a7de36c {"local":[32,1,1],"shared":1408}
668a5bcb03d89bf4 {"local":[32,1,1],"shared":0}
677244fdf3942835 {"local":[32,1,1],"shared":0}
6c7aad2cf74e3ee4 {"local":[32,1,1],"shared":384}
6e8c303fff2b7a5d {"local":[32,1,1],"shared":0}
705d9fcaec6315e9 {"local":[32,1,1],"shared":0}
71a62b18ac0c6989 {"local":[32,1,1],"shared":0}
7415e59e2dcbbdf3 {"local":[32,1,1],"shared":640}
75321be6323c99a4 {"local":[32,1,1],"shared":0}
779cb21bafb28a37 {"local":[32,1,1],"shared":0}
78a26246bbde4160 {"local":[32,1,1],"shared":384}
78e80cf67d011b87 {"local":[32,1,1],"shared":384}
7af523907249c780 {"local":[32,1,1],"shared":0}
80a980b8013d6e24 {"local":[32,1,1],"shared":0}
8487d5afefc7d653 {"local":[32,1,1],"shared":1152}
86efe4e49fbcc123 {"local":[32,1,1],"shared":0}
8e20a8168bbce06d {"local":[32,1,1],"shared":896}
8e61f68132df7a66 {"local":[32,1,1],"shared":0}
958eabd8395dd82c {"local":[32,1,1],"shared":0}
9aed8577b5dc6b59 {"local":[32,1,1],"shared":0}
9b305fb5093e2b96 {"local":[32,1,1],"shared":0}
9c895d28ca39a571 {"local":[32,1,1],"shared":1408}
9cc42175a6f73090 {"local":[32,1,1],"shared":0}
9e19470bdde740fa {"local":[32,1,1],"shared":0}
9f8116baffddaf81 {"local":[32,1,1],"shared":384}
a7565e756a3c003a {"local":[32,1,1],"shared":0}
a7582a857a72f949 {"local":[32,1,1],"shared":384}
a8a99eac92a13b27 {"local":[32,1,1],"shared":896}
a8aba96c0231df1b {"local":[32,1,1],"shared":384}
adba86f76e526f5a {"local":[32,1,1],"shared":384}
add70ec31c59abcf {"local":[32,1,1],"shared":0}
b0719bc4bd2823f7 {"local":[32,1,1],"shared":0}
b29c9c4d9fdadf81 {"local":[32,1,1],"shared":0}
b67514d3cc980f63 {"local":[32,1,1],"shared":0}
b6ca080a499ddc67 {"local":[32,1,1],"shared":0}
b7675de24c6e09f8 {"local":[32,1,1],"shared":1920}
b94b8f3ea12cbc46 {"local":[32,1,1],"shared":0}
b98505657eeb076e {"local":[32,1,1],"shared":0}
c6824782f3c41ec7 {"local":[32,1,1],"shared":0}
cc4551e280f5137e {"local":[32,1,1],"shared":0}
cd3ab6c1126944f1 {"local":[32,1,1],"shared":384}
d272673fa0dc7d90 {"local":[32,1,1],"shared":0}
d5f9e3f394c9a3c6 {"local":[32,1,1],"shared":1920}
da3ac3ff6f8f7b08 {"local":[32,1,1],"shared":384}
dafe0bd6219e4567 {"local":[32,1,1],"shared":1152}
ddef79f5d7e245ed {"local":[32,1,1],"shared":384}
dffb4f29bf819600 {"local":[32,1,1],"shared":384}
e02a92d52422c29e {"local":[32,1,1],"shared":640}
ec8808d84232a8f2 {"local":[32,1,1],"shared":384}
f7671c9a7bd53f09 {"local":[32,1,1],"shared":384}
f8ec429f312c6bbd {"local":[32,1,1],"shared":0}
fb6fe2d5fd2ec2e3 {"local":[32,1,1],"shared":640}
ff1edbae22336cb7 {"local":[32,1,1],"shared":0}
ff56d41aff7c6e80 {"local":[32,1,1],"shared":640}
Can't render this file because it contains an unexpected character in line 2 and column 19.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,2 @@
# kind: evidence 控制段键冲突裁定:表空 | control-section key conflict rulings: empty
dm_ctl_sha16 chosen_pc class candidates records
1 # kind: evidence 控制段键冲突裁定:表空 | control-section key conflict rulings: empty
2 dm_ctl_sha16 chosen_pc class candidates records
File diff suppressed because it is too large. Load diff
+25 -6
View File
@@ -54,6 +54,8 @@ extern void *nvn_thunk[NVN_NFUNC];
extern const char *nvn_names[NVN_NFUNC];
extern void nvn_unimpl_entry(void);
extern void nvn_prof_entry(void);
extern void nvn_big_entry(void);
void *nvn_big_real[NVN_NFUNC]; // 走宿主大栈的函数的真实现(nvn_entry.S nvn_big_entry) | the real implementation of functions that run on the big host stack (nvn_entry.S nvn_big_entry)
void *nvn_real[NVN_NFUNC]; // DM_NVN_PROF:计时包装调用的真实现 | DM_NVN_PROF: the real implementation called by the timing wrapper
_Atomic uint64_t nvn_prof_ticks[NVN_NFUNC]; // DM_NVN_PROF:各函数累计耗时(cntvct 计数) | DM_NVN_PROF: cumulative time of each function (cntvct counts)
static uint8_t g_unimpl_seen[NVN_NFUNC];
@@ -746,9 +748,10 @@ NVN(nvnCommandBufferBindStorageBuffer, void, NVNcommandBuffer *c, NVNshaderStage
NVN(nvnCommandBufferBindStorageBuffers, void, NVNcommandBuffer *c, NVNshaderStage st, int first, int n, const NVNbufferRange *rg) {
for (int i = 0; i < n; i++) *REC(c, OP_BIND_SSBO, p_bind_buf) = (p_bind_buf){st, first + i, rg[i].address, rg[i].size};
}
static void bind_handles(NVNcommandBuffer *c, int op, int st, int first, int n, const uint64_t *h) {
if (n < 0 || n > 32) { LOG("【红】绑定句柄数 %d", n); return; }
p_bind_handles *r = RECN(c, op, p_bind_handles, h, n); r->stage = st; r->first = first; r->n = n; memcpy(r->h, h, 8 * n);
static p_bind_handles *bind_handles(NVNcommandBuffer *c, int op, int st, int first, int n, const uint64_t *h) {
if (n < 0 || n > 32) { LOG("【红】绑定句柄数 %d", n); return NULL; }
p_bind_handles *r = RECN(c, op, p_bind_handles, h, n); r->stage = st; r->first = first; r->n = n; r->defer = 0; r->src = 0; memcpy(r->h, h, 8 * n);
return r;
}
// 图像(可写贴图):句柄 = 贴图池号(GetImageHandle 原样返回;游戏不调 RegisterImage,图像与贴图共用登记)
// Image (writable texture): handle = texture pool index (GetImageHandle returns it as-is; the game does not call RegisterImage, images and textures share registration)
@@ -956,13 +959,20 @@ __attribute__((constructor)) static void nvn_nops_register(void) {
nvn_slot[i].fn = (void *)nvn_nop;
}
unsigned nnop = sizeof g_nops / sizeof *g_nops;
// 录制命令(nvnCommandBuffer*,每帧上万次,宿主只追加记录)以外的已实现函数走宿主大栈:提交 / 出图 / 建对象会做 Metal 工作,游戏线程的栈可能太小(见 nvn_entry.S nvn_big_entry)
// Implemented functions other than command recording (nvnCommandBuffer*, tens of thousands per frame, where the host only appends records) run on the big host stack: submit / present / object creation do Metal work, and a game thread's stack may be too small (see nvn_entry.S nvn_big_entry)
unsigned nbig = 0;
for (unsigned i = 0; i < NVN_NFUNC; i++)
if (nvn_slot[i].fn != (void *)nvn_unimpl_entry && nvn_slot[i].fn != (void *)nvn_nop && strncmp(nvn_names[i], "nvnCommandBuffer", 16)) {
nvn_big_real[i] = nvn_slot[i].fn; nvn_slot[i].fn = (void *)nvn_big_entry; nbig++;
}
// DM_NVN_PROF=1:已实现的函数(未实现桩与出图除外:出图含节拍等待,单独计)套计时包装,用于拆「翻译层录制期」耗时
// DM_NVN_PROF=1: implemented functions (except unimplemented stubs and present: present includes the display-link wait and is counted separately) get a timing wrapper, used to break down the time spent in the "translation layer recording phase"
if (getenv("DM_NVN_PROF"))
for (unsigned i = 0; i < NVN_NFUNC; i++)
if (nvn_slot[i].fn != (void *)nvn_unimpl_entry && strcmp(nvn_names[i], "nvnQueuePresentTexture")) { nvn_real[i] = nvn_slot[i].fn; nvn_slot[i].fn = (void *)nvn_prof_entry; }
LOG("[nvn] 空设备:名字 %u,已实现 %u(语义 %u + 空实现 %u),其余 %u 个接报红桩",
NVN_NFUNC, g_nsem + nnop, g_nsem, nnop, NVN_NFUNC - g_nsem - nnop);
LOG("[nvn] 空设备:名字 %u,已实现 %u(语义 %u + 空实现 %u,其中 %u 个走宿主大栈),其余 %u 个接报红桩",
NVN_NFUNC, g_nsem + nnop, g_nsem, nnop, nbig, NVN_NFUNC - g_nsem - nnop);
}
// ---------- 窗口 / 出图 ----------
@@ -1563,7 +1573,16 @@ NVN(nvnCommandBufferCallCommands, void, NVNcommandBuffer *c, int n, const NVNcom
// 组合贴图句柄(贴图号 | 采样器号 << 20)原样记进贴图槽:回放层按低 20 位取贴图、高位取采样器(nvn_mtl.m NVN_RB_SAMPT)
// Combined texture handle (texture id | sampler id << 20) is recorded into the texture slot as is: the replay layer takes the texture from the low 20 bits and the sampler from the high bits (nvn_mtl.m NVN_RB_SAMPT)
NVN(nvnCommandBufferBindTexture, void, NVNcommandBuffer *c, NVNshaderStage st, int i, NVNtextureHandle h) { uint64_t v = (uint64_t)h; bind_handles(c, OP_BIND_TEX, st, i, 1, &v); }
NVN_NOPROTO(nvnCommandBufferBindTexturesDeferred, void, NVNcommandBuffer *c, NVNshaderStage st, int first, int n, const NVNtextureHandle *h) { bind_handles(c, OP_BIND_TEX, st, first, n, (const uint64_t *)h); }
// 「延迟」绑定:GPU 执行这条命令时才去读 h 指向的句柄,不是录制时。塞尔达传说 旷野之息引擎的逐张贴图绑定(main+0xec2e64)全走它,
// 传的是贴图采样器对象里句柄字段的地址、个数 1;命令块录一次反复提交,句柄之后才写进去 / 换掉。原先录制时就拷走句柄(多是 0),
// 延迟光照合成通道的贴图全成了 0 号哑贴图,整个三维画面黑。记下地址,回放层读记录时现取(bh_fresh)
// "Deferred" binding: the GPU reads the handles h points to when it executes this command, not at recording. The per-texture binding of The Legend of Zelda: Breath of the Wild's engine (main+0xec2e64) always uses it,
// passing the address of the handle field in the texture sampler object with a count of 1; command blocks are recorded once and submitted repeatedly, and the handle is written / replaced afterwards. The host used to copy the handles at recording (mostly 0),
// so every texture of the deferred lighting composite became the dummy texture 0 and the whole 3D picture was black. The address is recorded and the replay layer reads it when it reads the record (bh_fresh)
NVN_NOPROTO(nvnCommandBufferBindTexturesDeferred, void, NVNcommandBuffer *c, NVNshaderStage st, int first, int n, const NVNtextureHandle *h) {
p_bind_handles *r = bind_handles(c, OP_BIND_TEX, st, first, n, (const uint64_t *)h);
if (r && h) { r->defer = 1; r->src = (uint64_t)(uintptr_t)h; }
}
NVN(nvnCommandBufferBindImage, void, NVNcommandBuffer *c, NVNshaderStage st, int i, NVNimageHandle h) { uint64_t v = (uint64_t)h; bind_handles(c, OP_BIND_IMG, st, i, 1, &v); }
NVN(nvnCommandBufferBindVertexBuffer, void, NVNcommandBuffer *c, int i, NVNbufferAddress a, size_t n) { NVNbufferRange r = {a, n}; h_nvnCommandBufferBindVertexBuffers(c, i, 1, &r); }
+21
View File
@@ -50,6 +50,9 @@ _nvn_prof_entry:
adrp x9, _nvn_real@PAGE
add x9, x9, _nvn_real@PAGEOFF
ldr x17, [x9, x16, lsl #3]
adrp x9, _nvn_slot@PAGE // x16 换回槽地址:真实现若是大栈入口(nvn_big_entry)要靠它找实现 | x16 back to the slot address: when the real implementation is the big-stack entry (nvn_big_entry) it finds the implementation through it
add x9, x9, _nvn_slot@PAGEOFF
add x16, x9, x16, lsl #4
ldp x0, x1, [sp, #16]
ldp x2, x3, [sp, #32]
ldp x4, x5, [sp, #48]
@@ -77,3 +80,21 @@ _nvn_prof_entry:
ldp x29, x30, [sp]
add sp, sp, #240
ret
// 大栈入口:跳板进来时 x16 = 本函数槽地址。取 nvn_big_real[i] 进 x16,交 dm_bigstack_enter(dm/host/hle_bigstack.c):
// 在客体线程栈上就换到本线程的宿主大栈再调实现。宿主的 NVN 实现在提交、出图、建贴图时做 Metal 工作,Metal 驱动编内部程序(快速清除的 blit 程序等)要很深的栈,
// 塞尔达传说 旷野之息的渲染线程栈装不下(SIGBUS 在 AGX 编译器里,地址在客体栈区)。NVN 函数不在栈上传参(生成器已核),x0–x8 / d0–d7 够用
// Big-stack entry: when the thunk comes in, x16 = this function's slot address. Load nvn_big_real[i] into x16 and hand over to dm_bigstack_enter (dm/host/hle_bigstack.c):
// on a guest thread stack it switches to this thread's big host stack before calling the implementation. The host's NVN implementations do Metal work when submitting, presenting and creating textures, and the Metal driver compiles internal programs (the fast-clear blit program, ...) on a deep stack,
// which The Legend of Zelda: Breath of the Wild's render thread stack cannot hold (SIGBUS in the AGX compiler, at an address in the guest stack region). NVN functions pass no arguments on the stack (verified by the generator); x0-x8 / d0-d7 are enough
.globl _nvn_big_entry
.p2align 2
_nvn_big_entry:
adrp x17, _nvn_slot@PAGE
add x17, x17, _nvn_slot@PAGEOFF
sub x16, x16, x17
lsr x16, x16, #4 // 槽号 i | Slot index i
adrp x17, _nvn_big_real@PAGE
add x17, x17, _nvn_big_real@PAGEOFF
ldr x16, [x17, x16, lsl #3]
b _dm_bigstack_enter
+101 -29
View File
@@ -213,6 +213,7 @@ void bf_want_note(const char *key) {
// ---------- Memory pools ----------
typedef struct { uintptr_t lo, hi; uintptr_t base; __unsafe_unretained id<MTLBuffer> buf; uint64_t gpu; const void *key; int res; } pool_ent; // res:已放进驻留集(按需,见 pool_res) | res: already in the residency set (on demand, see pool_res)
static pool_ent *g_pools; static int g_npool, g_cap;
static int g_pidx_ok, g_plast = -1; // 地址→池下标是否有效、上次命中(pool_find)| whether the address→pool index is valid, the last hit (pool_find)
static NSMutableArray *g_keep; // 持有缓冲引用(pool_ent 里是不持有的指针) | Holds the buffer references (pool_ent has non-owning pointers)
static uint64_t g_pool_adds;
static int g_pool_res_n; // 在册池里已放进驻留集的个数(内存读数行用;持 g_mu) | registered pools already in the residency set (for the memory readout; under g_mu)
@@ -365,6 +366,7 @@ void mtl_pool_add(const void *mem, uint64_t size) {
if (!buf) MDIE("内存池 %p+%#llx 建零拷贝缓冲失败", mem, (unsigned long long)size);
pthread_mutex_lock(&g_mu);
if (g_npool == g_cap) { g_cap = g_cap ? g_cap * 2 : 64; g_pools = realloc(g_pools, g_cap * sizeof *g_pools); }
g_pidx_ok = 0; g_plast = -1; // 地址下标作废(pool_find)| the address index is invalid (pool_find)
g_pools[g_npool++] = (pool_ent){lo, hi, a, buf, buf.gpuAddress, mem, 0}; // 先不进驻留集:第一次被取址时才放(pool_res) | not in the residency set yet: put in the first time it is addressed (pool_res)
g_pool_adds++;
[g_keep addObject:buf];
@@ -381,27 +383,42 @@ void mtl_pool_del(const void *mem) {
for (int i = 0; i < g_npool; i++)
if (g_pools[i].key == mem) { id<MTLBuffer> b = g_pools[i].buf; [g_keep removeObject:b]; defer_obj_release(b); if (g_pools[i].res) g_pool_res_n--;
if (!g_dead_pools) g_dead_pools = [NSHashTable weakObjectsHashTable];
[g_dead_pools addObject:b]; g_pools[i] = g_pools[--g_npool]; break; }
[g_dead_pools addObject:b]; g_pools[i] = g_pools[--g_npool]; g_pidx_ok = 0; g_plast = -1; break; }
pthread_mutex_unlock(&g_mu);
}
// 地址 → 池:上次命中优先,其次按起址排好的下标二分,找不到(池互相重叠时)再整表顺扫。塞尔达传说 旷野之息常驻约 1760 个内存池,
// 每次绘制的每个缓冲绑定都要取址,原先整表顺扫占提交线程约 4% 的样本(mtl_gpu_addr 222 / 6 s)。池增删(mtl_pool_add / del)时下标作废,用到时重排。调用方持 g_mu
// Address → pool: the last hit first, then a binary search over indices sorted by start address, and a full linear scan only when that fails (pools overlapping).
// The Legend of Zelda: Breath of the Wild keeps about 1760 memory pools alive and every buffer binding of every draw looks one up; the old full scan took about 4% of the submit thread's samples (mtl_gpu_addr 222 / 6 s). Adding / removing pools invalidates the index, which is re-sorted when next used. Caller holds g_mu
static int *g_pidx; static int g_pidx_cap;
static int pidx_cmp(const void *a, const void *b) { uintptr_t x = g_pools[*(const int *)a].lo, y = g_pools[*(const int *)b].lo; return x < y ? -1 : x > y; }
static int pool_find(uint64_t addr, uint64_t len) {
if (g_plast >= 0 && g_plast < g_npool && addr >= g_pools[g_plast].lo && addr + len <= g_pools[g_plast].hi) return g_plast;
if (!g_pidx_ok) {
if (g_pidx_cap < g_npool) { g_pidx_cap = g_npool * 2; g_pidx = realloc(g_pidx, (size_t)g_pidx_cap * sizeof *g_pidx); }
for (int i = 0; i < g_npool; i++) g_pidx[i] = i;
qsort(g_pidx, (size_t)g_npool, sizeof *g_pidx, pidx_cmp); g_pidx_ok = 1;
}
int lo = 0, hi = g_npool - 1, hit = -1; // 起址 ≤ addr 的最后一个 | the last one whose start ≤ addr
while (lo <= hi) { int m = (lo + hi) / 2; if (g_pools[g_pidx[m]].lo <= addr) { hit = m; lo = m + 1; } else hi = m - 1; }
if (hit >= 0 && addr + len <= g_pools[g_pidx[hit]].hi) return g_plast = g_pidx[hit];
for (int i = 0; i < g_npool; i++) if (addr >= g_pools[i].lo && addr + len <= g_pools[i].hi) return g_plast = i;
return -1;
}
int mtl_addr(uint64_t addr, uint64_t len, void **buf, uint64_t *off) {
int ok = 0;
pthread_mutex_lock(&g_mu);
for (int i = 0; i < g_npool; i++)
if (addr >= g_pools[i].lo && addr + len <= g_pools[i].hi) {
pool_res(i); *buf = (__bridge void *)g_pools[i].buf; *off = addr - g_pools[i].base; ok = 1; break;
}
int i = pool_find(addr, len);
if (i >= 0) { pool_res(i); *buf = (__bridge void *)g_pools[i].buf; *off = addr - g_pools[i].base; ok = 1; }
pthread_mutex_unlock(&g_mu);
return ok;
}
int mtl_gpu_addr(uint64_t addr, uint64_t len, void **buf, uint64_t *off, uint64_t *gpu) {
int ok = 0;
pthread_mutex_lock(&g_mu);
for (int i = 0; i < g_npool; i++)
if (addr >= g_pools[i].lo && addr + len <= g_pools[i].hi) {
pool_res(i); *buf = (__bridge void *)g_pools[i].buf; *off = addr - g_pools[i].base; *gpu = g_pools[i].gpu + *off; ok = 1; break;
}
int i = pool_find(addr, len);
if (i >= 0) { pool_res(i); *buf = (__bridge void *)g_pools[i].buf; *off = addr - g_pools[i].base; *gpu = g_pools[i].gpu + *off; ok = 1; }
pthread_mutex_unlock(&g_mu);
return ok;
}
@@ -977,7 +994,11 @@ static MTLTextureDescriptor *tex_desc(const tex_t *t, int packaged) {
case 4: d.textureType = MTLTextureType2DArray; d.arrayLength = t->d.depth; break;
case 5: d.textureType = MTLTextureType2DMultisample; d.sampleCount = t->d.samples; d.mipmapLevelCount = 1; break;
case 6: d.textureType = MTLTextureType2DMultisampleArray; d.sampleCount = t->d.samples; d.arrayLength = t->d.depth; d.mipmapLevelCount = 1; break;
case 8: d.textureType = MTLTextureTypeCube; break;
// 立方贴图建成 6 层的二维数组,采样时由 tex_fit 换成立方 / 立方数组视图:Metal 只许从二维数组底图建立方数组视图(立方底图建不出)。
// 塞尔达传说 旷野之息有 1512 个着色器按立方数组采样环境光探针,绑的却是单张立方贴图(贴图号 1064 起):类型不符时读值未定义,室内外受光面一片黑
// Cube textures are created as 6-layer 2D arrays and sampled through cube / cube array views made by tex_fit: Metal only allows cube array views of a 2D array base texture (not of a cube base).
// The Legend of Zelda: Breath of the Wild has 1512 shaders that sample the environment light probes as a cube array while a single cube texture is bound (texture ID 1064 onwards): with mismatched types the reads are undefined, and every lit surface came out black
case 8: d.textureType = MTLTextureType2DArray; d.arrayLength = 6; break;
case 9: d.textureType = MTLTextureTypeCubeArray; d.arrayLength = t->d.depth; break;
case 10: RED_ONCE(1, "缓冲贴图(TARGET_BUFFER)未实现"); return nil;
default: d.textureType = MTLTextureType2D; break;
@@ -1953,15 +1974,46 @@ void pg_commit_ext(id<MTLCommandBuffer> cb) { pg_commit(cb); }
// 原先预测按底图格式,经 sRGB 视图写的通道(马力欧末道)预测出的管线格式不对,真管线到首次绘制才现编(取自 d2r-mobile e2a0c9a9b)
// The format an attachment is actually attached with: the view's format if the view changes the format and can be attached, otherwise the base texture's. Shared by attaching (attach) and pipeline prediction (pso_predict_rest) so both agree --
// prediction used the base format, so for a pass written through an sRGB view (Mario's last pass) the predicted pipeline had the wrong format and the real one was compiled at the first draw (from d2r-mobile e2a0c9a9b)
// 压缩格式贴图当渲染目标(塞尔达传说 旷野之息:32x32 的 RGTC1(BC4)贴图经 RGBA16UI 视图写,一个纹素 = 一个 8 字节块,即 GPU 上自己编 BC4):
// Metal 既不能把压缩格式挂成颜色附件,也建不出压缩 ↔ 非压缩的视图(管线描述符校验直接中止进程)。暂不挂这个附件(附件全无时整通道的绘制跳过),报红;
// 要画对得另开同块大小的非压缩代理贴图,通道结束后经缓冲拷进压缩贴图
// A compressed texture as a render target (The Legend of Zelda: Breath of the Wild: a 32x32 RGTC1 (BC4) texture written through an RGBA16UI view, one texel = one 8-byte block, i.e. BC4 encoded on the GPU):
// Metal can neither attach a compressed format as a colour attachment nor create compressed <-> uncompressed views (the pipeline descriptor validation aborts the process). For now the attachment is left out (a pass with no attachment at all skips its draws), with a red line;
// drawing it correctly needs an uncompressed proxy texture with the same block size, copied into the compressed texture through a buffer after the pass
static int mtl_fmt_block(MTLPixelFormat f) { return (f >= MTLPixelFormatBC1_RGBA && f <= MTLPixelFormatBC7_RGBAUnorm_sRGB) || (f >= MTLPixelFormatPVRTC_RGB_2BPP && f < MTLPixelFormatGBGR422); }
static MTLPixelFormat rt_view_fmt(id<MTLTexture> base, const view_t *v, int has_v) {
if (base && mtl_fmt_block(base.pixelFormat)) {
RED_ONCE(78, "渲染目标是压缩格式贴图(Metal 格式 %lu %lux%lu 层 %lu,视图换格式 %d → NVN %#x):不挂,该附件的绘制落空", (unsigned long)base.pixelFormat, (unsigned long)base.width, (unsigned long)base.height,
(unsigned long)base.arrayLength, has_v ? v->set_format : 0, has_v ? v->format : 0);
return MTLPixelFormatInvalid;
}
if (!base || !has_v || !v->set_format) return base.pixelFormat;
uint32_t pf = 0; uint8_t swz[4];
if (!nvn_fmt_to_mtl(v->format, &pf, swz) || !pf || pf == (uint32_t)base.pixelFormat) return base.pixelFormat;
if (tex_need_pfv(base.pixelFormat, (MTLPixelFormat)pf) && !(base.usage & MTLTextureUsagePixelFormatView)) return base.pixelFormat;
return (MTLPixelFormat)pf;
}
// 同一张贴图的同一级 / 同一层挂在两个以上颜色槽:只挂第一个,后面的不挂(写入落空)。塞尔达传说 旷野之息的 G 缓冲通道把反照率与另一张缓冲各挂两次
// (c1 = c2、c3 = c4),c2 / c4 的通道写掩码恒为 0(cmask 0x00f0f0ff)。Switch 的 ROP 按掩码只写该写的通道,两个槽互不干扰;
// Apple 的 TBDR 每个附件各有一份块内存,通道结束时整块存回,后存的那份(只读入、没写过)盖掉先存那份的写入——先后按 32x32 块各不相同,
// 反照率只剩零散的方块,整个三维画面发黑。用到被丢的槽且写掩码非 0 时报红(那样的写入真丢了)
// The same texture level / layer attached at more than one colour slot: only the first is attached, later ones are not (their writes are dropped). The G-buffer passes of The Legend of Zelda: Breath of the Wild attach the albedo and another buffer twice each
// (c1 = c2, c3 = c4), and the channel write masks of c2 / c4 are always 0 (cmask 0x00f0f0ff). The Switch's ROP writes only the masked channels and the two slots do not interfere;
// on Apple's TBDR each attachment has its own tile memory and the whole tile is stored back at the end of the pass, so the copy stored later (loaded, never written) overwrites the writes of the earlier one -- the order differs per 32x32 tile,
// the albedo kept only scattered squares, and the whole 3D picture went black. A red line is printed when a dropped slot is used with a non-zero write mask (those writes are really lost)
static int rt_dup_slot(const p_set_rt *r, int i) {
if (!r->color[i]) return 0;
uint8_t lv = r->has_cview[i] && r->cview[i].set_levels ? r->cview[i].base_level : 0; uint16_t ly = r->has_cview[i] && r->cview[i].set_layers ? r->cview[i].min_layer : 0;
for (int j = 0; j < i; j++) {
if (!r->color[j] || r->color[j]->serial != r->color[i]->serial) continue; // 按序号比:记录里存的是贴图对象的副本(tex_snap),指针各不相同 | compare serials: records hold copies of the texture objects (tex_snap), so the pointers differ
uint8_t lj = r->has_cview[j] && r->cview[j].set_levels ? r->cview[j].base_level : 0; uint16_t yj = r->has_cview[j] && r->cview[j].set_layers ? r->cview[j].min_layer : 0;
if (lj == lv && yj == ly) return 1;
}
return 0;
}
static void attach(MTLRenderPassAttachmentDescriptor *a, const tex_t *t, const view_t *v, int has_v) {
id<MTLTexture> base = tex_mtl(t); a.texture = base;
if (base && mtl_fmt_block(base.pixelFormat)) { rt_view_fmt(base, v, has_v); a.texture = nil; return; } // 压缩格式挂不上(见 rt_view_fmt)| compressed formats cannot be attached (see rt_view_fmt)
if (has_v && v->set_levels) a.level = v->base_level;
if (has_v && v->set_layers) { if (t->d.target == 2) a.depthPlane = v->min_layer; else a.slice = v->min_layer; }
// 换格式的视图挂同格式的 Metal 视图(管线格式跟着附件走)。SMBW 末道经 RGBA8 交换链贴图的 RGBA8_SRGB 视图写:写入时由硬件编成 sRGB;
@@ -2004,6 +2056,7 @@ static id<MTL4RenderCommandEncoder> rp_begin_ex_(int clear_only) {
int any = 0;
for (int i = 0; i < R.rt.n; i++) {
if (!R.rt.color[i] || (clear_only == 1 && !R.clear_c[i])) continue;
if (rt_dup_slot(&R.rt, i)) continue; // 重复挂的槽不挂(见 rt_dup_slot)| a slot attached twice is not attached (see rt_dup_slot)
attach(pd.colorAttachments[i], R.rt.color[i], &R.rt.cview[i], R.rt.has_cview[i]);
if (!pd.colorAttachments[i].texture) continue;
any = 1;
@@ -2051,7 +2104,16 @@ static id<MTL4RenderCommandEncoder> rp_begin_ex_(int clear_only) {
}
}
}
if (!any) { RED_ONCE(4, "渲染目标都建不出 MTLTexture"); return nil; }
if (!any) {
RED_ONCE(4, "渲染目标都建不出 MTLTexture");
if (passdump()) { // PASSDUMP:整通道被跳过时列出它的目标(序号、格式、是否压缩)| PASSDUMP: when a whole pass is skipped, list its targets (serial, format, compressed or not)
char b[256]; size_t o = 0;
for (int i = 0; i < R.rt.n && i < 8; i++) if (R.rt.color[i]) o += snprintf(b + o, sizeof b - o, " c%d=%llu/f%#x%s", i, (unsigned long long)R.rt.color[i]->serial, R.rt.color[i]->d.format, fmt_compressed(R.rt.color[i]->d.format) ? "(压缩)" : "");
if (R.rt.depth) o += snprintf(b + o, sizeof b - o, " d=%llu", (unsigned long long)R.rt.depth->serial);
PD("N %llu 通道跳过(附件都挂不上)mode=%d%s", g_stat_pass + 1, clear_only, b);
}
return nil;
}
for (int i = 0; i < R.rt.n && i < 8; i++) if (R.rt.color[i] && zskip_zero_clear(i)) {
g_zskip_cand_ok[i] = 1; g_zskip_cand[i] = (zskip_zero){R.rt.color[i]->serial, (uint16_t)(R.rt.has_cview[i] && R.rt.cview[i].set_levels ? R.rt.cview[i].base_level : 0), (uint16_t)(R.rt.has_cview[i] && R.rt.cview[i].set_layers ? R.rt.cview[i].min_layer : 0)};
}
@@ -2516,6 +2578,11 @@ void mtl_reg_samp(int sid, const samp_par *sp) {
g_sreg[id_] = *sp; g_sreg_ok[id_] = 1;
pthread_mutex_unlock(&g_rmu);
}
// 延迟绑定(BindTexturesDeferred)的句柄执行时现取:录制记下的是句柄所在地址(见 hle_nvn.c);各处读绑定记录前先刷新。命令内存可写,就地改
// Handles of deferred binds (BindTexturesDeferred) are read at execution: the record keeps the address of the handles (see hle_nvn.c); every reader of a bind record refreshes it first. Command memory is writable, so it is updated in place
static inline void bh_fresh(const p_bind_handles *b) {
if (b->defer && b->src && b->n > 0 && b->n <= 32) memcpy((void *)b->h, (const void *)(uintptr_t)b->src, 8 * (size_t)b->n);
}
static MTLTextureType ttype(int target, int fallback) {
switch (target) { case 0: return MTLTextureType2D; case 1: case 7: return MTLTextureType2D; case 2: return MTLTextureType3D; // 一维按一行高的二维(见 tex 建贴图处) | 1D as one-row 2D (see where textures are created)
case 3: return MTLTextureType2DArray; case 4: return MTLTextureType2DArray; case 5: return MTLTextureType2DMultisample;
@@ -2589,6 +2656,7 @@ static id<MTLTexture> tex_fit(int tid, id<MTLTexture> t, MTLTextureType want, ui
else if (want == MTLTextureType2DArray && have == MTLTextureType2D) ns = 1;
else if (want == MTLTextureTypeCube && have == MTLTextureType2DArray && t.arrayLength >= 6) ns = 6;
else if (want == MTLTextureTypeCubeArray && have == MTLTextureType2DArray && t.arrayLength >= 6) ns = t.arrayLength / 6 * 6;
else if (want == MTLTextureTypeCube && have == MTLTextureTypeCubeArray) ns = 6; // 立方数组按立方读:第 0 个立方 | a cube array read as a cube: the first cube
if (!ns || tid <= 0 || tid >= g_ntreg) { RED_ONCE(76, "贴图类型不符:着色器声明 %lu,贴图 %lu(贴图号 %d),照原样绑", (unsigned long)want, (unsigned long)have, tid); return nil; }
treg_t *r = &g_treg[tid];
if (r->alt && r->alt_of == (__bridge void *)t && r->alt_type == (uint8_t)want) { *rid = r->alt_rid; return (__bridge id<MTLTexture>)r->alt; }
@@ -2677,6 +2745,9 @@ static void udc_note_draw(NvProg *pr) {
for (int i = 0; i < sh->rf.nb; i++) if (sh->rf.b[i].kind == NVN_RB_IMG && sh->rf.b[i].rw) { image_write = 1; break; }
if (image_write) break;
}
if (R.has_rt) for (int i = 1; i < R.rt.n && i < 8; i++) // 重复挂的槽(没挂上)真被写:写入丢了(见 rt_dup_slot)| a slot attached twice (left unattached) really written: the writes are lost (see rt_dup_slot)
if ((shader_color_mask >> i & 1) && ((S.chmask.mask >> (4 * i)) & 0xf) && rt_dup_slot(&R.rt, i))
RED_ONCE(79, "颜色槽 %d 与前面的槽挂同一张贴图(ser=%llu)且本次绘制写它(掩码 %#x):这些写入丢了", i, (unsigned long long)R.rt.color[i]->serial, (S.chmask.mask >> (4 * i)) & 0xf);
for (int i = 0; i < g_nsd; i++) {
sd_att *t = &g_sd[i];
if (image_write) { t->wrote = 1; t->wwhy = 1; }
@@ -3524,9 +3595,9 @@ static int state_op(uint32_t op, const void *a) {
case OP_BIND_VBUF: { const p_bind_vbuf *b = a; memcpy(&S.vbuf[b->first], b->r, sizeof *b->r * b->n); break; }
case OP_BIND_UBO: { const p_bind_buf *b = a; if (b->stage < 6 && b->index < 16 && memcmp(&S.ubo[b->stage][b->index], b, sizeof *b)) { S.ubo[b->stage][b->index] = *b; g_args_dirty[b->stage] = 1; } break; }
case OP_BIND_SSBO: { const p_bind_buf *b = a; if (b->stage < 6 && b->index < 16 && memcmp(&S.ssbo[b->stage][b->index], b, sizeof *b)) { S.ssbo[b->stage][b->index] = *b; g_args_dirty[b->stage] = 1; } break; }
case OP_BIND_TEX: { const p_bind_handles *b = a; if (b->stage < 6 && b->first + b->n <= 128 && memcmp(&S.tex[b->stage][b->first], b->h, 8 * b->n)) { memcpy(&S.tex[b->stage][b->first], b->h, 8 * b->n); g_args_dirty[b->stage] = 1; } break; }
case OP_BIND_SAMP: { const p_bind_handles *b = a; if (b->stage < 6 && b->first + b->n <= 128 && memcmp(&S.samp[b->stage][b->first], b->h, 8 * b->n)) { memcpy(&S.samp[b->stage][b->first], b->h, 8 * b->n); g_args_dirty[b->stage] = 1; } break; }
case OP_BIND_IMG: { const p_bind_handles *b = a; if (b->stage < 6 && b->first >= 0 && b->first + b->n <= 32) { if (memcmp(&S.img[b->stage][b->first], b->h, 8 * b->n)) { memcpy(&S.img[b->stage][b->first], b->h, 8 * b->n); g_args_dirty[b->stage] = 1; } } else RED_ONCE(31, "图像槽越界 阶段 %d 起 %d 数 %d", b->stage, b->first, b->n); break; }
case OP_BIND_TEX: { const p_bind_handles *b = a; bh_fresh(b); if (b->stage < 6 && b->first + b->n <= 128 && memcmp(&S.tex[b->stage][b->first], b->h, 8 * b->n)) { memcpy(&S.tex[b->stage][b->first], b->h, 8 * b->n); g_args_dirty[b->stage] = 1; } break; }
case OP_BIND_SAMP: { const p_bind_handles *b = a; bh_fresh(b); if (b->stage < 6 && b->first + b->n <= 128 && memcmp(&S.samp[b->stage][b->first], b->h, 8 * b->n)) { memcpy(&S.samp[b->stage][b->first], b->h, 8 * b->n); g_args_dirty[b->stage] = 1; } break; }
case OP_BIND_IMG: { const p_bind_handles *b = a; bh_fresh(b); if (b->stage < 6 && b->first >= 0 && b->first + b->n <= 32) { if (memcmp(&S.img[b->stage][b->first], b->h, 8 * b->n)) { memcpy(&S.img[b->stage][b->first], b->h, 8 * b->n); g_args_dirty[b->stage] = 1; } } else RED_ONCE(31, "图像槽越界 阶段 %d 起 %d 数 %d", b->stage, b->first, b->n); break; }
case OP_STENCIL: {
const p_stencil *b = a;
if (b->which != 2) g_dss_dirty = 1;
@@ -3563,7 +3634,7 @@ static void pso_predict_rest(void) {
if (!fz_next(&cur, &q, &hq, &aq)) break;
if (hq.op == OP_SET_RT) {
const p_set_rt *r = aq; memset(cf, 0, sizeof cf); dfull = sfull = 0; samples = 1;
for (int i = 0; i < r->n && i < 8; i++) { id<MTLTexture> t = r->color[i] ? tex_mtl(r->color[i]) : nil; if (t) { cf[i] = (uint32_t)rt_view_fmt(t, &r->cview[i], r->has_cview[i]); samples = (uint32_t)t.sampleCount; } }
for (int i = 0; i < r->n && i < 8; i++) { id<MTLTexture> t = r->color[i] && !rt_dup_slot(r, i) ? tex_mtl(r->color[i]) : nil; if (t) { cf[i] = (uint32_t)rt_view_fmt(t, &r->cview[i], r->has_cview[i]); samples = (uint32_t)t.sampleCount; } }
id<MTLTexture> dt = r->depth ? tex_mtl(r->depth) : nil;
if (dt) { MTLPixelFormat f = dt.pixelFormat; samples = (uint32_t)dt.sampleCount;
dfull = f != MTLPixelFormatStencil8 ? (uint32_t)f : 0; sfull = (f == MTLPixelFormatStencil8 || f == MTLPixelFormatDepth32Float_Stencil8) ? (uint32_t)f : 0; }
@@ -3810,7 +3881,7 @@ static void sdx_walk(const uint8_t *p, int depth) {
if (c->depth_mask) sdx_ev_add((sdx_ev){seq, g_pp_rtseq, r->depth->serial, p, lv, sl, SDX_CLEAR, 1, !r->has_dview, 0});
if ((c->stencil_mask & 0xff) == 0xff) sdx_ev_add((sdx_ev){seq, g_pp_rtseq, r->depth->serial, p, lv, sl, SDX_CLEAR, 2, !r->has_dview, 0});
break; }
case OP_BIND_TEX: case OP_BIND_IMG: { const p_bind_handles *b = a; int base = h.op == OP_BIND_IMG ? 128 : 0, lim = h.op == OP_BIND_IMG ? 32 : 128;
case OP_BIND_TEX: case OP_BIND_IMG: { const p_bind_handles *b = a; bh_fresh(b); int base = h.op == OP_BIND_IMG ? 128 : 0, lim = h.op == OP_BIND_IMG ? 32 : 128;
if (b->stage < 6 && b->first >= 0 && b->first + b->n <= lim) for (int k = 0; k < b->n; k++) g_pp_slot[b->stage][base + b->first + k] = h_ser(b->h[k]);
break; }
case OP_BIND_PROGRAM: g_pp_prog = ((const p_bind_program *)a)->prog; break;
@@ -3917,7 +3988,7 @@ static void hoist_try(void) {
case OP_JUMP: { const uint8_t *t; memcpy(&t, a, 8); p = t; continue; }
case OP_DRAW: case OP_DRAW_IDX: if (in_grp) return; break;
case OP_SCISSOR: sc = *(const p_scissor *)a; has_sc = 1; break;
case OP_BIND_TEX: case OP_BIND_IMG: { const p_bind_handles *b = a; for (int k = 0; k < b->n && k < 32 && nbh < 512; k++) if (b->h[k]) bh[nbh++] = b->h[k]; break; }
case OP_BIND_TEX: case OP_BIND_IMG: { const p_bind_handles *b = a; bh_fresh(b); for (int k = 0; k < b->n && k < 32 && nbh < 512; k++) if (b->h[k]) bh[nbh++] = b->h[k]; break; }
case OP_CLEAR_COLOR: case OP_CLEAR_DS: {
if (!in_grp || G[ng - 1].nops >= 8) return;
grp *g = &G[ng - 1];
@@ -4215,7 +4286,7 @@ static void defer_try(const p_draw *dw, const p_draw_idx *di) {
case OP_CALL: g_defer.reason = 2; goto fail;
case OP_JUMP: memcpy(&p, a, 8); continue;
case OP_BIND_PROGRAM: pg = ((const p_bind_program *)a)->prog; break;
case OP_BIND_TEX: case OP_BIND_IMG: { const p_bind_handles *b = a; int lim = h.op == OP_BIND_IMG ? 32 : 128;
case OP_BIND_TEX: case OP_BIND_IMG: { const p_bind_handles *b = a; bh_fresh(b); int lim = h.op == OP_BIND_IMG ? 32 : 128;
if (b->stage >= 0 && b->stage < 6 && b->first >= 0 && b->first + b->n <= lim) for (int k = 0; k < b->n; k++) { if (h.op == OP_BIND_IMG) img[b->stage][b->first + k] = b->h[k]; else tex[b->stage][b->first + k] = b->h[k]; } break; }
case OP_BIND_STATE: { const p_bind_state *b = a; if (b->kind == 5) memcpy(&ds, b->bytes, sizeof ds); break; }
case OP_DRAW: case OP_DRAW_IDX:
@@ -4272,7 +4343,7 @@ q_found:
if (!q_body && h.op == OP_SET_RT && !q_clears) { const p_set_rt *r2 = a; if (!defer_q_superset(r2)) { g_defer.reason = 15; goto fail; } q = r2; qop = p; p += h.size; continue; }
if (h.op == OP_CLEAR_COLOR || h.op == OP_CLEAR_DS) q_clears = 1;
if (!q_body && (h.op == OP_END || h.op == OP_SET_RT || h.op == OP_UPDATE_UB || h.op == OP_COPY_B2B || h.op == OP_CLEAR_BUF || h.op == OP_DISPATCH)) { g_defer.reason = 9; g_defer.reason_op = h.op; goto fail; }
if (h.op == OP_BIND_PROGRAM) pg = ((const p_bind_program *)a)->prog; else if (h.op == OP_BIND_TEX || h.op == OP_BIND_IMG) { const p_bind_handles *b = a; int lim = h.op == OP_BIND_IMG ? 32 : 128; if (b->stage >= 0 && b->stage < 6 && b->first >= 0 && b->first + b->n <= lim) for (int k = 0; k < b->n; k++) { if (h.op == OP_BIND_IMG) img[b->stage][b->first + k] = b->h[k]; else tex[b->stage][b->first + k] = b->h[k]; } }
if (h.op == OP_BIND_PROGRAM) pg = ((const p_bind_program *)a)->prog; else if (h.op == OP_BIND_TEX || h.op == OP_BIND_IMG) { const p_bind_handles *b = a; bh_fresh(b); int lim = h.op == OP_BIND_IMG ? 32 : 128; if (b->stage >= 0 && b->stage < 6 && b->first >= 0 && b->first + b->n <= lim) for (int k = 0; k < b->n; k++) { if (h.op == OP_BIND_IMG) img[b->stage][b->first + k] = b->h[k]; else tex[b->stage][b->first + k] = b->h[k]; } }
else if (h.op == OP_CLEAR_COLOR) { const p_clear_color *c = a; if (c->index >= 0 && c->index < q->n && q->color[c->index]) for (int i = 0; i < g_defer.p_rt.n; i++) if (g_defer.p_rt.color[i] && sub_eq(q, c->index, &g_defer.p_rt, i)) { g_defer.reason = 10; goto fail; } }
p += h.size;
}
@@ -4390,7 +4461,7 @@ static void fuse_try(void) {
if (q == r2op) goto planned;
if (hq.op == OP_BIND_PROGRAM) pg = ((const p_bind_program *)aq)->prog;
else if (hq.op == OP_BIND_STATE) { const p_bind_state *b = aq; if (b->kind == 5) memcpy(&ds, b->bytes, sizeof ds); }
else if (hq.op == OP_BIND_TEX || hq.op == OP_BIND_IMG) { const p_bind_handles *b = aq; int lim = hq.op == OP_BIND_IMG ? 32 : 128;
else if (hq.op == OP_BIND_TEX || hq.op == OP_BIND_IMG) { const p_bind_handles *b = aq; bh_fresh(b); int lim = hq.op == OP_BIND_IMG ? 32 : 128;
if (b->stage >= 0 && b->stage < 6 && b->first >= 0 && b->first + b->n <= lim) for (int k = 0; k < b->n; k++) { if (hq.op == OP_BIND_IMG) img[b->stage][b->first + k] = b->h[k]; else tex[b->stage][b->first + k] = b->h[k]; } }
else if (hq.op == OP_DRAW || hq.op == OP_DRAW_IDX) break;
}
@@ -4401,7 +4472,7 @@ planned:
cur = (fz_cursor){.p = g_exec_next};
for (;;) { const uint8_t *q; op_hdr hq; const void *aq; if (!fz_next(&cur, &q, &hq, &aq)) return; if (q == p) break;
if (hq.op == OP_BIND_PROGRAM) pg2 = ((const p_bind_program *)aq)->prog;
else if (hq.op == OP_BIND_TEX || hq.op == OP_BIND_IMG) { const p_bind_handles *b = aq; int lim = hq.op == OP_BIND_IMG ? 32 : 128;
else if (hq.op == OP_BIND_TEX || hq.op == OP_BIND_IMG) { const p_bind_handles *b = aq; bh_fresh(b); int lim = hq.op == OP_BIND_IMG ? 32 : 128;
if (b->stage >= 0 && b->stage < 6 && b->first >= 0 && b->first + b->n <= lim) for (int k = 0; k < b->n; k++) { if (hq.op == OP_BIND_IMG) i2[b->stage][b->first + k] = b->h[k]; else t2[b->stage][b->first + k] = b->h[k]; } }
}
NvProg *pr2 = pg2 ? prog_get(pg2) : nil; NvShader *vs2 = pr2 ? pr2->sh[0] : nil, *fs2 = pr2 ? pr2->sh[1] : nil;
@@ -5863,9 +5934,9 @@ void td_note(MTL4RenderPassDescriptor *pd, NSUInteger nc) {
for (NSUInteger i = 0; i < nc && i < 8; i++) {
id<MTLTexture> t = pd.colorAttachments[i].texture; if (!t) continue;
uint64_t ser = i < (NSUInteger)R.rt.n && R.rt.color[i] ? R.rt.color[i]->serial : 0;
NSUInteger lv = pd.colorAttachments[i].level;
BOOL seen = NO; for (NSArray *e in g_td_list) if ([e[1] unsignedLongLongValue] == ser && [e[2] unsignedIntegerValue] == lv) seen = YES;
if (!seen) [g_td_list addObject:@[t, @(ser), @(lv)]];
NSUInteger lv = pd.colorAttachments[i].level, sl = t.textureType == MTLTextureType2DArray ? pd.colorAttachments[i].slice : 0;
BOOL seen = NO; for (NSArray *e in g_td_list) if ([e[1] unsignedLongLongValue] == ser && [e[2] unsignedIntegerValue] == lv && [e[3] unsignedIntegerValue] == sl) seen = YES;
if (!seen) [g_td_list addObject:@[t, @(ser), @(lv), @(sl)]];
}
}
static void td_dump(void) {
@@ -5910,8 +5981,9 @@ static void td_dump(void) {
MLOG("[texraw] ser=%llu %lux%lux%lu 格式 %lu → %s", ser, (unsigned long)w, (unsigned long)h, (unsigned long)dz, (unsigned long)t.pixelFormat, path);
}
for (NSArray *e in g_td_list) {
id<MTLTexture> t = e[0]; uint64_t ser = [e[1] unsignedLongLongValue]; NSUInteger lv = [e[2] unsignedIntegerValue];
if (t.textureType != MTLTextureType2D) { MLOG("[texdump] ser=%llu 类型 %lu 跳过", ser, (unsigned long)t.textureType); continue; }
id<MTLTexture> t = e[0]; uint64_t ser = [e[1] unsignedLongLongValue]; NSUInteger lv = [e[2] unsignedIntegerValue], sl = [e[3] unsignedIntegerValue];
// 二维数组按挂上的那一层存(文件序号 序号*100 + 层*10 + 级;第 0 层与二维同名)| 2D arrays are saved by the attached layer (file index ser*100 + layer*10 + level; layer 0 is named like 2D)
if (t.textureType != MTLTextureType2D && t.textureType != MTLTextureType2DArray) { MLOG("[texdump] ser=%llu 类型 %lu 跳过", ser, (unsigned long)t.textureType); continue; }
if (getenv("DM_NVN_SNAPRAW")) { // 同时存原始字节(HDR 目标转 8 位图会截断;首行文本头:宽 高 格式 每行字节) | Also save the raw bytes (converting an HDR target to an 8-bit image truncates; first-line text header: width height format bytes-per-row)
NSUInteger rw = MAX(t.width >> lv, 1u), rh = MAX(t.height >> lv, 1u);
size_t bpp = t.pixelFormat == MTLPixelFormatRGBA16Float ? 8 : t.pixelFormat == MTLPixelFormatRGBA32Float ? 16 : t.pixelFormat == MTLPixelFormatR16Float || t.pixelFormat == MTLPixelFormatR16Unorm ? 2 : 4;
@@ -5922,7 +5994,7 @@ static void td_dump(void) {
char rp_[1024]; snprintf(rp_, sizeof rp_, g_td_path, (int)(ser * 100 + lv)); strlcat(rp_, ".raw", sizeof rp_);
FILE *f = fopen(rp_, "wb"); if (f) { fprintf(f, "%lu %lu %lu %zu\n", (unsigned long)rw, (unsigned long)rh, (unsigned long)t.pixelFormat, rw * bpp); fwrite(rb.contents, 1, rw * rh * bpp, f); fclose(f); }
}
id<MTLTexture> v = [t newTextureViewWithPixelFormat:t.pixelFormat textureType:MTLTextureType2D levels:NSMakeRange(lv, 1) slices:NSMakeRange(0, 1)];
id<MTLTexture> v = [t newTextureViewWithPixelFormat:t.pixelFormat textureType:MTLTextureType2D levels:NSMakeRange(lv, 1) slices:NSMakeRange(sl, 1)];
NSUInteger w = MAX(t.width >> lv, 1u), h = MAX(t.height >> lv, 1u);
MTLTextureDescriptor *od = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:w height:h mipmapped:NO];
od.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; od.storageMode = MTLStorageModePrivate;
@@ -5933,8 +6005,8 @@ static void td_dump(void) {
id<MTLRenderCommandEncoder> en = [cb renderCommandEncoderWithDescriptor:rp];
[en setRenderPipelineState:ps]; [en setFragmentTexture:v atIndex:0]; [en setFragmentSamplerState:sp atIndex:0];
[en drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; [en endEncoding]; [cb commit]; [cb waitUntilCompleted];
char path[1024]; snprintf(path, sizeof path, g_td_path, (int)(ser * 100 + lv));
MLOG("[texdump] ser=%llu lv=%lu 格式 %lu %lux%lu", ser, (unsigned long)lv, (unsigned long)t.pixelFormat, (unsigned long)w, (unsigned long)h);
char path[1024]; snprintf(path, sizeof path, g_td_path, (int)(ser * 100 + sl * 10 + lv));
MLOG("[texdump] ser=%llu lv=%lu 层 %lu 格式 %lu %lux%lu", ser, (unsigned long)lv, (unsigned long)sl, (unsigned long)t.pixelFormat, (unsigned long)w, (unsigned long)h);
shot(o, path, 0, NULL);
}
[g_td_list removeAllObjects];
+3 -1
View File
@@ -130,7 +130,9 @@ typedef struct { int32_t first, n; uint32_t st[16]; } p_bind_vattr;
typedef struct { int32_t first, n; vs_st st[16]; } p_bind_vstream;
typedef struct { int32_t first, n; NVNbufferRange r[16]; } p_bind_vbuf;
typedef struct { int32_t stage, index; uint64_t addr, size; } p_bind_buf; // UBO / SSBO
typedef struct { int32_t stage, first, n; uint64_t h[32]; } p_bind_handles; // 分离贴图 / 采样器句柄 | Separate texture / sampler handles
// defer / src:BindTexturesDeferred 的句柄在执行时才从 src 读(录制时的 h 只是兜底),回放层读记录前用 bh_fresh 刷新(nvn_mtl.m)
// defer / src: the handles of BindTexturesDeferred are read from src at execution time (h at recording time is only a fallback); the replay layer refreshes them with bh_fresh before reading the record (nvn_mtl.m)
typedef struct { int32_t stage, first, n, defer; uint64_t src; uint64_t h[32]; } p_bind_handles; // 分离贴图 / 采样器句柄 | Separate texture / sampler handles
typedef struct { int32_t face, value_mask, mask, ref, which; } p_stencil; // which:0 值掩码 1 写掩码 2 参考值 | which: 0 value mask 1 write mask 2 reference value
typedef struct { float factor, units, clamp; } p_poly_offset;
typedef struct { int32_t prim, first, count, instances, base_instance; } p_draw; // instances 0 按 1 | instances 0 counts as 1
+1 -1
View File
@@ -19,7 +19,7 @@ typedef struct { uint8_t *d; size_t n; } dm_blob; // 整份读进内存的一
#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
enum { DT_INIT = 12, DT_RELA = 7, DT_RELASZ = 8, DT_JMPREL = 23, DT_PLTRELSZ = 2, DT_SYMTAB = 6, DT_STRTAB = 5,
DT_INIT_ARRAY = 25, DT_INIT_ARRAYSZ = 27 };
DT_INIT_ARRAY = 25, DT_INIT_ARRAYSZ = 27, DT_RELRSZ = 35, DT_RELR = 36 };
enum { R_AARCH64_ABS64 = 257, R_AARCH64_GLOB_DAT = 1025, R_AARCH64_JUMP_SLOT = 1026, R_AARCH64_RELATIVE = 1027 };
#define ELF64_ST_BIND(i) ((i) >> 4) // 符号绑定:0 = STB_LOCAL | symbol binding: 0 = STB_LOCAL
#define ELF64_ST_VISIBILITY(o) ((o) & 3) // 可见性:0 = STV_DEFAULT | visibility: 0 = STV_DEFAULT
+31 -10
View File
@@ -105,7 +105,11 @@ HLE_FORCE("_ZN2nn5audio18CloseAudioRendererENS0_19AudioRendererHandleE", "关渲
orig(obj, state);
}
// 取 DeviceSink 的输入混音缓冲 → int16 立体声 | DeviceSink input mix buffers → int16 stereo
// 取 DeviceSink 的输入混音缓冲 → int16 立体声。命令号 SDK 16 是 0x13;SDK 20 在它前面插了一个命令,成了 0x14(sdk+0x233f44 写命令处,布局不变:0x148 字节,
// +0x128 上采样缓冲、+0x130 输入数、+0x134 输入下标)。render_once 按布局设
// DeviceSink input mix buffers → int16 stereo. The command ID is 0x13 in SDK 16; SDK 20 inserted a command before it, making it 0x14 (written at sdk+0x233f44, same layout: 0x148 bytes,
// +0x128 upsample buffer, +0x130 input count, +0x134 input indices). render_once sets it by layout
static int g_sink_id = 0x13;
static int capture(const uint8_t *cl, int16_t *dst) {
uint32_t ncmd = *(const uint32_t *)(cl + 4), samples = *(const uint32_t *)(cl + 0x14), rate = *(const uint32_t *)(cl + 0x18);
const int32_t *mix = *(const int32_t *const *)(cl + 8);
@@ -120,7 +124,7 @@ static int capture(const uint8_t *cl, int16_t *dst) {
for (uint32_t i = 0; i < ncmd; i++) {
uint32_t magic = *(const uint32_t *)c; int16_t size = *(const int16_t *)(c + 6);
if (magic != 0xcafebabeu || size <= 0) { if (!logged++) LOG("【红】audren:命令表第 %u 条格式不对(魔数 %#x 大小 %d)", i, magic, size); return 0; }
if (c[5] == 0x13 && c[4]) {
if (c[5] == g_sink_id && c[4]) {
uint32_t nin = *(const uint32_t *)(c + 0x130); const int16_t *in = (const int16_t *)(c + 0x134);
const int32_t *up = *(const int32_t *const *)(c + 0x128);
if (!logged++) LOG("audren:DeviceSink 输入 %u 个(%d %d …)采样率 %u 每帧 %u 混音缓冲 %p 上采样缓冲 %p", nin, nin ? in[0] : -1, nin > 1 ? in[1] : -1, rate, samples, (void *)mix, (void *)up);
@@ -153,24 +157,41 @@ static int capture(const uint8_t *cl, int16_t *dst) {
// Render one frame (generate commands + CPU processing); dst = output (stereo int16, *nout samples). The two callers (render thread, catch-up) are serialized by a host lock: they share one command buffer
static pthread_mutex_t g_render_mu = PTHREAD_MUTEX_INITIALIZER;
static _Atomic uint64_t g_updates, g_renders, g_catchups;
// SDK 20(塞尔达传说 旷野之息 1.9.0)的 AudioRenderSystem 换了布局:取模式 / 设备 / 命令缓冲的成员函数都内联掉了(sdk+0x22a70c ExecuteAudioRendererRendering:
// +0x48 渲染设备、+0x4c 执行模式,都要是 1;+0xf8 / +0x100 命令缓冲起址 / 长),GenerateCommand 改收 Span<std::byte> 并回 Span(x0 起址、x1 长,就是生成出的命令表),
// UpdateImpl 持的互斥锁在 +0x2d0(SDK 16 是 +0x1f0)。按有没有 Span 版 GenerateCommand 认布局
// SDK 20's AudioRenderSystem (The Legend of Zelda: Breath of the Wild 1.9.0) has another layout: the member functions for mode / device / command buffer are inlined (sdk+0x22a70c ExecuteAudioRendererRendering:
// +0x48 rendering device and +0x4c execution mode, both must be 1; +0xf8 / +0x100 command buffer start / length), GenerateCommand takes a Span<std::byte> and returns a Span (x0 start, x1 length: the generated command list),
// and the mutex UpdateImpl holds is at +0x2d0 (+0x1f0 in SDK 16). The layout is recognized by the presence of the Span version of GenerateCommand
typedef struct { uint8_t *p; size_t n; } span_t;
static Result render_once(uint8_t *sys, int16_t *dst, int *nout) {
static int (*mode)(void *), (*device)(void *); static uint8_t *(*cbuf)(void *); static size_t (*cbsz)(void *), (*gen)(void *, void *, size_t);
static span_t (*gen20)(void *, void *, size_t); static size_t mu_off;
static void (*tmax)(void *, uint32_t), (*setup)(void *, const void *, size_t), (*process)(void *), (*lock)(void *), (*unlock)(void *);
if (!process) {
lock = gsym("_ZN2nn2os12SdkMutexType4LockEv"); unlock = gsym("_ZN2nn2os12SdkMutexType6UnlockEv");
mode = gsym("_ZNK2nn5audio6server17AudioRenderSystem16GetExecutionModeEv"); device = gsym("_ZNK2nn5audio6server17AudioRenderSystem18GetRenderingDeviceEv");
cbuf = gsym("_ZNK2nn5audio6server17AudioRenderSystem26GetCommandBufferWorkBufferEv"); cbsz = gsym("_ZNK2nn5audio6server17AudioRenderSystem30GetCommandBufferWorkBufferSizeEv");
gen = gsym("_ZN2nn5audio6server17AudioRenderSystem15GenerateCommandEPvm");
gen20 = guest_sym("_ZN2nn5audio6server17AudioRenderSystem15GenerateCommandENS_4SpanISt4byteEE");
if (!gen20) {
mode = gsym("_ZNK2nn5audio6server17AudioRenderSystem16GetExecutionModeEv"); device = gsym("_ZNK2nn5audio6server17AudioRenderSystem18GetRenderingDeviceEv");
cbuf = gsym("_ZNK2nn5audio6server17AudioRenderSystem26GetCommandBufferWorkBufferEv"); cbsz = gsym("_ZNK2nn5audio6server17AudioRenderSystem30GetCommandBufferWorkBufferSizeEv");
gen = gsym("_ZN2nn5audio6server17AudioRenderSystem15GenerateCommandEPvm");
}
mu_off = gen20 ? 0x2d0 : 0x1f0; g_sink_id = gen20 ? 0x14 : 0x13;
LOG("audren:AudioRenderSystem 按 SDK %s 布局(更新锁 +%#zx)", gen20 ? "20" : "16", mu_off);
tmax = gsym("_ZN2nn5audio3dsp20CommandListProcessor17SetProcessTimeMaxEj"); setup = gsym("_ZN2nn5audio3dsp20CommandListProcessor5SetupEPKvm");
process = gsym("_ZN2nn5audio3dsp20CommandListProcessor7ProcessEv");
}
if (mode(sys) != 1 || device(sys) != 1) return 0x40299; // 同原函数:非 CPU 手动模式不能手动渲染 | same as the original: manual rendering needs CPU + manual mode
int md = gen20 ? *(int32_t *)(sys + 0x4c) : mode(sys), dv = gen20 ? *(int32_t *)(sys + 0x48) : device(sys);
if (md != 1 || dv != 1) return 0x40299; // 同原函数:非 CPU 手动模式不能手动渲染 | same as the original: manual rendering needs CPU + manual mode
pthread_mutex_lock(&g_render_mu);
uint64_t t0 = now_ns();
// 生成命令要与游戏线程的 RequestUpdate(AudioRenderSystem::Update,持 +0x1f0 的互斥锁)互斥:原版的系统线程(SendCommandToDsp)也是持同一把锁生成。处理(CPU 版 DSP)照原版在锁外。
// Command generation must exclude the game thread's RequestUpdate (AudioRenderSystem::Update, which holds the mutex at +0x1f0): the original system thread (SendCommandToDsp) generates under the same lock. Processing (the CPU DSP) stays outside the lock, as in the original.
uint8_t *cl = cbuf(sys);
lock(sys + 0x1f0); size_t n = gen(sys, cl, cbsz(sys)); unlock(sys + 0x1f0);
// 生成命令要与游戏线程的 RequestUpdate(AudioRenderSystem::Update,持 mu_off 处的互斥锁)互斥:原版的系统线程(SendCommandToDsp)也是持同一把锁生成。处理(CPU 版 DSP)照原版在锁外。
// Command generation must exclude the game thread's RequestUpdate (AudioRenderSystem::Update, which holds the mutex at mu_off): the original system thread (SendCommandToDsp) generates under the same lock. Processing (the CPU DSP) stays outside the lock, as in the original.
uint8_t *cl; size_t n;
lock(sys + mu_off);
if (gen20) { span_t sp = gen20(sys, *(uint8_t **)(sys + 0xf8), *(size_t *)(sys + 0x100)); cl = sp.p; n = sp.n; }
else { cl = cbuf(sys); n = gen(sys, cl, cbsz(sys)); }
unlock(sys + mu_off);
atomic_fetch_add(&g_renders, 1);
uint64_t proc[16] __attribute__((aligned(16))) = {0}; // CommandListProcessor(0x58 字节)| CommandListProcessor (0x58 bytes)
tmax(proc, 0xffffffffu); setup(proc, cl, n); process(proc);
+17 -6
View File
@@ -39,17 +39,20 @@ void dm_pad_set(uint64_t buttons, int32_t lx, int32_t ly, int32_t rx, int32_t ry
}
uint64_t dm_input_samples(void) { return (uint64_t)atomic_load(&g_sampling); }
// DM_KEYS 脚本输入(测试用):"秒:键[+键][~按住秒][*次数/间隔秒],…",例 "8:A,12:A,15:Down~0.5,16:A+B,40:A*80/2"(40 s 起每 2 s 按一次 A,共 80 次)。键名 A B X Y LS RS L R ZL ZR Plus Minus Left Up Right Down,左摇杆 LLeft LUp LRight LDown;
// 缺省按住 0.15 s,最多 1024 步(重复展开后计)。走手柄那一路(dm_pad_set),与键盘 / 触屏按位或,不互相覆盖。时间从宿主启动算起
// 缺省按住 0.15 s,最多 1024 步(重复展开后计)。走手柄那一路(dm_pad_set),与键盘 / 触屏按位或,不互相覆盖。时间从宿主启动算起。
// 时间前加 f 表示出图帧号("f7600:A",间隔也按帧计):机器忙时秒数与游戏进度对不上,帧号对得上(塞尔达传说 旷野之息开局走位用)
// DM_KEYS scripted input (for tests): "seconds:key[+key][~hold seconds][*count/interval seconds],...", e.g. "8:A,12:A,15:Down~0.5,16:A+B,40:A*80/2" (A every 2 s from 40 s on, 80 times). Key names A B X Y LS RS L R ZL ZR Plus Minus Left Up Right Down, left stick LLeft LUp LRight LDown;
// held 0.15 s by default, at most 1024 steps (counted after repeats are expanded). Goes through the controller path (dm_pad_set), ORed with keyboard / touch, neither overrides the other. Times are counted from host start
typedef struct { double at, hold; uint64_t btn; int32_t lx, ly; } key_step;
// held 0.15 s by default, at most 1024 steps (counted after repeats are expanded). Goes through the controller path (dm_pad_set), ORed with keyboard / touch, neither overrides the other. Times are counted from host start.
// An f before the time means a presented frame number ("f7600:A", and the repeat interval counts frames too): when the machine is busy seconds drift from the game's progress, frame numbers do not (used for the Zelda BotW opening route)
typedef struct { double at, hold; uint64_t btn; int32_t lx, ly; int frame; } key_step;
#define KEY_STEPS 1024
static key_step g_steps[KEY_STEPS]; static int g_nsteps;
static void *keys_main(void *arg) {
(void)arg; struct timespec t0; clock_gettime(CLOCK_MONOTONIC, &t0);
for (int i = 0; i < g_nsteps; i++) {
for (;;) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); double el = (t.tv_sec - t0.tv_sec) + (t.tv_nsec - t0.tv_nsec) / 1e9; if (el >= g_steps[i].at) break; usleep(10000); }
LOG("DM_KEYS:%.2f s 按 %#llx(按住 %.2f s)", g_steps[i].at, (unsigned long long)g_steps[i].btn, g_steps[i].hold);
for (;;) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); double el = (t.tv_sec - t0.tv_sec) + (t.tv_nsec - t0.tv_nsec) / 1e9;
if (g_steps[i].frame ? (double)dm_guest_frame() >= g_steps[i].at : el >= g_steps[i].at) break; usleep(5000); }
LOG("DM_KEYS:%s%.2f 按 %#llx(按住 %.2f s)", g_steps[i].frame ? "帧 " : "", g_steps[i].at, (unsigned long long)g_steps[i].btn, g_steps[i].hold);
dm_pad_set(g_steps[i].btn, g_steps[i].lx, g_steps[i].ly, 0, 0); usleep((useconds_t)(g_steps[i].hold * 1e6)); dm_pad_set(0, 0, 0, 0, 0);
}
return NULL;
@@ -60,7 +63,7 @@ void hid_keys_init(void) {
char buf[4096]; snprintf(buf, sizeof buf, "%s", e);
for (char *tok = strtok(buf, ","); tok && g_nsteps < KEY_STEPS; tok = strtok(NULL, ",")) {
char *c = strchr(tok, ':'); if (!c) { LOG("【红】DM_KEYS 步骤缺「秒:」:%s", tok); continue; }
*c = 0; key_step st = {atof(tok), 0.15, 0};
*c = 0; int fr = (tok[0] == 'f' || tok[0] == 'F'); key_step st = {atof(tok + fr), 0.15, 0}; st.frame = fr;
int rep = 1; double every = 0;
char *r = strchr(c + 1, '*'); if (r) { *r = 0; rep = atoi(r + 1); char *sl = strchr(r + 1, '/'); every = sl ? atof(sl + 1) : 1; if (rep < 1) rep = 1; }
char *h = strchr(c + 1, '~'); if (h) { *h = 0; st.hold = atof(h + 1); }
@@ -208,6 +211,14 @@ HLE_FORCE("_ZN2nn3hid16GetGestureStatesEPNS0_12GestureStateEi", "手势状态:
HLE_FORCE("_ZN2nn3hid17StopSixAxisSensorERKNS0_19SixAxisSensorHandleE", "六轴;宿主不提供", void, h_hid_SixStop, const uint32_t *h) { (void)h; }
HLE_FORCE("_ZN2nn3hid21GetSixAxisSensorStateEPNS0_18SixAxisSensorStateERKNS0_19SixAxisSensorHandleE", "六轴当前状态:未连接", void, h_hid_SixState, void *s, const uint32_t *h) { (void)h; memset(s, 0, 0x60); }
HLE_FORCE("_ZN2nn3hid21IsSixAxisSensorAtRestERKNS0_19SixAxisSensorHandleE", "六轴静止:是", unsigned, h_hid_SixRest, const uint32_t *h) { (void)h; return 1; }
// 主机自带的六轴(SevenSixAxisSensor,掌机模式下的机身姿态;塞尔达传说 旷野之息):要工作缓冲、经 hid 服务开关。宿主不提供:开关成功、状态 0 条
// The console's own six-axis sensor (SevenSixAxisSensor, the body's orientation in handheld mode; The Legend of Zelda: Breath of the Wild): takes a work buffer and is switched through the hid service. The host provides none: switching succeeds, 0 states
HLE_FORCE("_ZN2nn3hid28InitializeSevenSixAxisSensorEPvm", "机身六轴初始化;宿主不提供", Result, h_hid_SevenInit, void *buf, size_t n) { (void)buf; (void)n; return RES_OK; }
HLE_FORCE("_ZN2nn3hid26FinalizeSevenSixAxisSensorEv", "机身六轴", Result, h_hid_SevenFini, void) { return RES_OK; }
HLE_FORCE("_ZN2nn3hid23StartSevenSixAxisSensorEv", "机身六轴;宿主不提供", Result, h_hid_SevenStart, void) { return RES_OK; }
HLE_FORCE("_ZN2nn3hid22StopSevenSixAxisSensorEv", "机身六轴", Result, h_hid_SevenStop, void) { return RES_OK; }
HLE_FORCE("_ZN2nn3hid32ResetSevenSixAxisSensorTimestampEv", "机身六轴时间戳", void, h_hid_SevenReset, void) {}
HLE_FORCE("_ZN2nn3hid27GetSevenSixAxisSensorStatesEPNS0_23SevenSixAxisSensorStateEi", "机身六轴状态:0 条", int, h_hid_SevenStates, void *out, int n) { (void)out; (void)n; return 0; }
HLE_FORCE("_ZN2nn3hid25EnableSixAxisSensorFusionERKNS0_19SixAxisSensorHandleEb", "六轴融合;宿主忽略", void, h_hid_SixFusionOn, const uint32_t *h, uint32_t on) { (void)h; (void)on; }
HLE_FORCE("_ZN2nn3hid28IsSixAxisSensorFusionEnabledERKNS0_19SixAxisSensorHandleE", "六轴融合:未开", unsigned, h_hid_SixFusionQ, const uint32_t *h) { (void)h; return 0; }
HLE_FORCE("_ZN2nn3hid25GetGyroscopeZeroDriftModeERKNS0_19SixAxisSensorHandleE", "陀螺零漂模式:标准", int, h_hid_GyroGet, const uint32_t *h) { (void)h; return 1; }
+3
View File
@@ -115,6 +115,9 @@ HLE_FORCE("_ZN2nn4pctl25TryBeginFreeCommunicationEb", "家长控制·与他人
HLE_FORCE("_ZN2nn4pctl20EndFreeCommunicationEv", "家长控制·与他人通信", void, h_pctl_End, void) {}
// Engage 进战斗地图时查询(不弹界面);不接管时 SDK 经 sm 连 pctl 服务,宿主没有,中止 | Engage asks on entering a battle map (no UI); without this the SDK connects to the pctl service through sm, which the host does not have, and aborts
HLE_FORCE("_ZN2nn4pctl28IsFreeCommunicationAvailableEv", "家长控制·与他人通信可用否,联网排除;恒否", unsigned, h_pctl_IsFree, void) { return 0; }
// 塞尔达传说 旷野之息:进 VR 相关画面前查家长控制的 3D 立体显示许可(返回 bool,参数 = 是否弹界面,游戏传 0)。宿主没有家长控制:允许
// The Legend of Zelda: Breath of the Wild: checks the parental-control permission for stereoscopic 3D before VR-related screens (returns bool; the argument = whether to show UI, the game passes 0). The host has no parental controls: permitted
HLE_FORCE("_ZN2nn4pctl27CheckStereoVisionPermissionEb", "家长控制·3D 立体显示许可;宿主无家长控制,允许", unsigned, h_pctl_Stereo, unsigned show_ui) { (void)show_ui; return 1; }
// swkbd 软键盘:见 hle_swkbd.c
// swkbd software keyboard: see hle_swkbd.c
+7
View File
@@ -80,6 +80,13 @@ static int opmode(void) { return dm_opmode(); }
HLE_FORCE("_ZN2nn2oe17FinishStartupLogoEv", "收起系统启动画面(经 am 应用代理);宿主没有启动画面", void, h_oe_FinishLogo, void) {} // 银河战士 生存恐惧在 main+0xadfed0 调 | Metroid Dread calls it at main+0xadfed0
HLE_FORCE("_ZN2nn2oe44SetUserInactivityDetectionTimeExtendedUnsafeEb", "无操作自动熄屏时长(经 am);宿主不熄屏", void, h_oe_InactivityExt, int on) { (void)on; }
HLE_FORCE("_ZN2nn2oe15EnableRecordingEv", "录像开关;宿主无系统录像", void, h_oe_EnableRec, void) {}
// 小程序资源用户号(ARUID,am 分给本进程的号):宿主没有 am,SDK 自己的取法回 0。SDK 20 的 OpenAudioRenderer(CPU 渲染 + 手动执行那条路,sdk+0x221730)
// 取到 0 即中止(塞尔达传说 旷野之息 1.9.0);SDK 16 不查。报一个固定的非 0 号,只当标识用
// Applet resource user ID (ARUID, the number am assigns to this process): the host has no am, and the SDK's own getter returns 0. SDK 20's OpenAudioRenderer (the CPU rendering + manual execution path, sdk+0x221730)
// aborts when it gets 0 (The Legend of Zelda: Breath of the Wild 1.9.0); SDK 16 did not check. Report a fixed non-zero ID, used only as an identifier
HLE_FORCE("_ZN2nn6applet23GetAppletResourceUserIdEv", "小程序资源用户号;宿主固定非 0", uint64_t, h_applet_Aruid, void) { return 0x80; }
// 启动参数(别的程序带着参数启动本作时才有;返回 bool):宿主从来不带 | Launch parameter (present only when another program launched this one with one; returns bool): the host never passes one
HLE_FORCE("_ZN2nn2oe21TryPopLaunchParameterEPmPvm", "启动参数:没有", unsigned, h_oe_PopLaunchParam, size_t *out, void *buf, size_t n) { (void)buf; (void)n; if (out) *out = 0; return 0; }
HLE_FORCE("_ZN2nn2oe16DisableRecordingEv", "录像开关;宿主无系统录像", void, h_oe_DisableRec, void) {}
HLE_FORCE("_ZN2nn2oe16EnableScreenShotEv", "截图开关;宿主无系统截图", void, h_oe_EnableShot, void) {}
HLE_FORCE("_ZN2nn2oe17DisableScreenShotEv", "截图开关;宿主无系统截图", void, h_oe_DisableShot, void) {}
+18
View File
@@ -120,6 +120,12 @@ HLE_FORCE("_ZN2nn2pl17GetSharedFontSizeENS0_14SharedFontTypeE", "共享字体大
// 是否跑在 Switch 2(代号 Ounce)上(返回 bool):宿主按 Switch 1 报假。马力欧 1.2.1 据此决定要不要数追加内容(升级包)、提示升级到 Switch 2 版
// Whether running on a Switch 2 (codename Ounce) (returns bool): the host reports false, as a Switch 1. Mario 1.2.1 uses it to decide whether to count add-on content (the upgrade pack) and suggest the Switch 2 Edition
HLE_FORCE("_ZN2nn2pl16IsRunningOnOunceEv", "是否 Switch 2:否", unsigned, h_pl_IsOunce, void) { return 0; }
// 塞尔达传说 旷野之息(SDK 20):先请求装载共享字体再查状态;宿主的替身字体随取随有。VR 模式(Labo VR 眼镜):宿主不进 VR,查询恒否,进出空操作
// The Legend of Zelda: Breath of the Wild (SDK 20): asks for the shared fonts to be loaded before querying their state; the host's stand-in fonts are there on demand. VR mode (the Labo VR goggles): the host never enters VR; the query is always false, entering / leaving is a no-op
HLE_FORCE("_ZN2nn2pl21RequestSharedFontLoadENS0_14SharedFontTypeE", "请求装载共享字体;替身字体随取随有,空操作", void, h_pl_ReqLoad, int type) { (void)type; }
HLE_FORCE("_ZN2nn2pl8IsVrModeEv", "VR 模式:否", unsigned, h_pl_IsVr, void) { return 0; }
HLE_FORCE("_ZN2nn2pl11BeginVrModeEv", "进 VR 模式(经 am);宿主不进", void, h_pl_BeginVr, void) { LOG("pl::BeginVrMode(宿主不进 VR)"); }
HLE_FORCE("_ZN2nn2pl9EndVrModeEv", "出 VR 模式(经 am)", void, h_pl_EndVr, void) {}
// ---------- ec:商店小程序(升级到 Switch 2 版的页面)----------
// 商店属排除范围:空操作。游戏不看返回值(两个重载都按无返回值处理)
@@ -161,6 +167,18 @@ HLE_FORCE("_ZN2nn3nfp14GetDeviceStateERKNS0_12DeviceHandleE", "设备状态:
HLE_FORCE("_ZN2nn3nfp10GetTagInfoEPNS0_7TagInfoERKNS0_12DeviceHandleE", "标签信息:没有标签", Result, h_nfp_TagInfo, void *out, const NfpDeviceHandle *h) { (void)out; (void)h; return nfp_no_tag("GetTagInfo"); }
HLE_FORCE("_ZN2nn3nfp12GetModelInfoEPNS0_9ModelInfoERKNS0_12DeviceHandleE", "型号信息:没有标签", Result, h_nfp_ModelInfo, void *out, const NfpDeviceHandle *h) { (void)out; (void)h; return nfp_no_tag("GetModelInfo"); }
HLE_FORCE("_ZN2nn3nfp5MountERKNS0_12DeviceHandleENS0_9ModelTypeENS0_11MountTargetE", "挂载标签:没有标签", Result, h_nfp_Mount, const NfpDeviceHandle *h, int model, int target) { (void)h; (void)model; (void)target; return nfp_no_tag("Mount"); }
// 塞尔达传说 旷野之息(SDK 20)另用的一组:标签进出事件(绑内核事件、恒不触发;不接管时 SDK 的实现在 sdk+0x3f2a78 读空指针)、两参数的 Mount、
// Unmount、读通用 / 登记信息(都按没有标签答)、库状态(1 = 已初始化)与 Finalize
// Another group The Legend of Zelda: Breath of the Wild (SDK 20) uses: tag activate / deactivate events (bound to a kernel event that never fires; without the take-over the SDK's code reads a null pointer at sdk+0x3f2a78), the two-argument Mount,
// Unmount, reading common / register info (all answered as no tag), the library state (1 = initialized) and Finalize
HLE_FORCE("_ZN2nn3nfp19AttachActivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE", "标签放上事件;绑内核事件、恒不触发", Result, h_nfp_AttachAct, void *ev, const NfpDeviceHandle *h) { (void)h; hle_bind_system_event(ev, 0); return RES_OK; }
HLE_FORCE("_ZN2nn3nfp21AttachDeactivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE", "标签拿走事件;绑内核事件、恒不触发", Result, h_nfp_AttachDeact, void *ev, const NfpDeviceHandle *h) { (void)h; hle_bind_system_event(ev, 0); return RES_OK; }
HLE_FORCE("_ZN2nn3nfp5MountERKNS0_12DeviceHandleENS0_9ModelTypeE", "挂载标签(两参数):没有标签", Result, h_nfp_Mount2, const NfpDeviceHandle *h, int model) { (void)h; (void)model; return nfp_no_tag("Mount"); }
HLE_FORCE("_ZN2nn3nfp7UnmountERKNS0_12DeviceHandleE", "卸载标签:没有标签", Result, h_nfp_Unmount, const NfpDeviceHandle *h) { (void)h; return nfp_no_tag("Unmount"); }
HLE_FORCE("_ZN2nn3nfp13GetCommonInfoEPNS0_10CommonInfoERKNS0_12DeviceHandleE", "通用信息:没有标签", Result, h_nfp_CommonInfo, void *out, const NfpDeviceHandle *h) { (void)out; (void)h; return nfp_no_tag("GetCommonInfo"); }
HLE_FORCE("_ZN2nn3nfp15GetRegisterInfoEPNS0_12RegisterInfoERKNS0_12DeviceHandleE", "登记信息:没有标签", Result, h_nfp_RegInfo, void *out, const NfpDeviceHandle *h) { (void)out; (void)h; return nfp_no_tag("GetRegisterInfo"); }
HLE_FORCE("_ZN2nn3nfp8GetStateEv", "amiibo 库状态:已初始化(1)", int, h_nfp_LibState, void) { return 1; }
HLE_FORCE("_ZN2nn3nfp8FinalizeEv", "amiibo 库收尾;宿主无会话", void, h_nfp_Fini, void) {}
HLE_FORCE("_ZN2nn3nfp5FlushERKNS0_12DeviceHandleE", "写回标签:没有标签", Result, h_nfp_Flush, const NfpDeviceHandle *h) { (void)h; return nfp_no_tag("Flush"); }
HLE_FORCE("_ZN2nn3nfp19OpenApplicationAreaERKNS0_12DeviceHandleEj", "打开应用区:没有标签", Result, h_nfp_OpenArea, const NfpDeviceHandle *h, uint32_t id) { (void)h; (void)id; return nfp_no_tag("OpenApplicationArea"); }
HLE_FORCE("_ZN2nn3nfp18GetApplicationAreaEPvPmRKNS0_12DeviceHandleEm", "读应用区:没有标签", Result, h_nfp_GetArea, void *buf, uint64_t *n, const NfpDeviceHandle *h, uint64_t sz) { (void)buf; (void)h; (void)sz; if (n) *n = 0; return nfp_no_tag("GetApplicationArea"); }
+6
View File
@@ -49,6 +49,12 @@ HLE_FORCE("time", "按返回点认出建局种子那一处(DM_FIXED_TIME);
HLE_FORCE("_ZN2nn4time10InitializeEv", "连 time 服务;时钟改取宿主系统时间", Result, h_time_Initialize, void) { return RES_OK; }
HLE_FORCE("_ZN2nn4time13IsInitializedEv", "time 服务已被接管,恒为已初始化", int, h_time_IsInitialized, void) { return 1; }
// 稳定时钟(开机起的秒数 + 时钟源号,16 字节 UUID):宿主单调时钟的秒数,源号固定。GetSpanBetween 在 sdk 里原样跑:源号相同才相减
// Steady clock (seconds since boot + a clock source ID, a 16-byte UUID): the host's monotonic seconds with a fixed source ID. GetSpanBetween runs as is in sdk: it subtracts only when the source IDs match
HLE_FORCE("_ZN2nn4time19StandardSteadyClock19GetCurrentTimePointEPNS0_20SteadyClockTimePointE", "稳定时钟 → 宿主单调时钟(秒),源号固定", Result, h_time_SteadyNow, uint8_t *out) {
int64_t s = (int64_t)(clock_gettime_nsec_np(CLOCK_MONOTONIC) / 1000000000ull);
memcpy(out, &s, 8); memcpy(out + 8, "dmhost-steady-01", 16); return RES_OK;
}
HLE_FORCE("_ZN2nn4time23StandardUserSystemClock14GetCurrentTimeEPNS0_9PosixTimeE", "用户时钟 → 宿主 time()", Result, h_time_GetCurrentTime, int64_t *out) {
int64_t ft = fixed_time(); *out = ft >= 0 ? ft : time(NULL); return RES_OK;
}
+27 -3
View File
@@ -211,15 +211,36 @@ static void register_gates(module_t *m, const char *dir) {
LOG("%s: gates %d patch_off %#llx keys %lu/%lu/%lu/%lu", m->name, ng, m->patch_off, g_key18, g_keytls, g_keytlr, g_keygate);
}
// DT_RELR(SDK 20 起,塞尔达传说 旷野之息 1.9.0 的三个模块都有):相对重定位的紧凑表,加数就在槽里(*槽 += 基址)。
// 每项 8 字节:偶数 = 新起点偏移(重定位它,指针移到下一槽);奇数 = 位图,第 1–63 位依次对应起点后的 63 个槽,用完指针前进 63 槽。
// 不处理时 main 的 GOT 与虚表全是模块内偏移(main 的 RELA 里只剩符号重定位)
// DT_RELR (from SDK 20; all three modules of The Legend of Zelda: Breath of the Wild 1.9.0 have it): a compact table of relative relocations with the addend in the slot (*slot += base).
// Each 8-byte entry: even = a new start offset (relocate it, the pointer moves to the next slot); odd = a bitmap whose bits 1-63 stand for the 63 slots after the pointer, which then moves on by 63 slots.
// Without it main's GOT and vtables hold module offsets (main's RELA keeps only symbol relocations)
static unsigned relr_apply(module_t *m, uint64_t relr, uint64_t relrsz, uint64_t delta) {
const uint64_t *e = (const uint64_t *)(m->base + relr); uint64_t *where = NULL; unsigned n = 0;
for (uint64_t i = 0; i < relrsz / 8; i++) {
uint64_t v = e[i];
if (!(v & 1)) { where = (uint64_t *)(m->base + v); *where++ += delta; n++; continue; }
if (!where) die("%s: DT_RELR 位图项前没有起点", m->name);
for (unsigned b = 0; (v >>= 1); b++) if (v & 1) { where[b] += delta; n++; }
where += 63;
}
return n;
}
static void relocate(module_t *m) {
uint64_t rela = 0, relasz = 0, jmprel = 0, pltsz = 0;
uint64_t rela = 0, relasz = 0, jmprel = 0, pltsz = 0, relr = 0, relrsz = 0;
for (const Elf64_Dyn *d = m->dyn; d->d_tag; d++) switch (d->d_tag) {
case DT_RELA: rela = d->d_un.d_ptr; break;
case DT_RELASZ: relasz = d->d_un.d_val; break;
case DT_JMPREL: jmprel = d->d_un.d_ptr; break;
case DT_PLTRELSZ: pltsz = d->d_un.d_val; break;
case DT_RELR: relr = d->d_un.d_ptr; break;
case DT_RELRSZ: relrsz = d->d_un.d_val; break;
}
int cnt[3] = {0}, nint = 0;
unsigned nrelr = relr ? relr_apply(m, relr, relrsz, (uint64_t)m->base) : 0;
const Elf64_Rela *tabs[2] = {(const Elf64_Rela *)(m->base + rela), (const Elf64_Rela *)(m->base + jmprel)};
uint64_t sizes[2] = {relasz, pltsz};
for (int t = 0; t < 2; t++)
@@ -246,7 +267,7 @@ static void relocate(module_t *m) {
if (type == R_AARCH64_JUMP_SLOT) { *where = v; cnt[2]++; }
else { *where = v + r->r_addend; cnt[1]++; } // ABS64 / GLOB_DAT
}
LOG("%s: reloc relative=%d abs/glob=%d jump=%d%s", m->name, cnt[0], cnt[1], cnt[2], nint ? "(含绑到先装模块的定义) | (some bound to an earlier module's definition)" : "");
LOG("%s: reloc relative=%d relr=%u abs/glob=%d jump=%d%s", m->name, cnt[0], nrelr, cnt[1], cnt[2], nint ? "(含绑到先装模块的定义) | (some bound to an earlier module's definition)" : "");
if (nint > 8) LOG("%s: 共 %d 处绑到先装模块的定义 | %d bound to an earlier module's definition in all", m->name, nint, nint);
}
@@ -262,11 +283,14 @@ void loader_hash_log(void) {
for (int k = 0; k < g_nmod; k++) {
module_t *m = &g_mod[k];
uint8_t *cp = malloc(m->size); memcpy(cp, m->base, m->size);
uint64_t rela = 0, relasz = 0, jmprel = 0, pltsz = 0;
uint64_t rela = 0, relasz = 0, jmprel = 0, pltsz = 0, relr = 0, relrsz = 0;
for (const Elf64_Dyn *d = m->dyn; d->d_tag; d++) switch (d->d_tag) {
case DT_RELA: rela = d->d_un.d_ptr; break; case DT_RELASZ: relasz = d->d_un.d_val; break;
case DT_JMPREL: jmprel = d->d_un.d_ptr; break; case DT_PLTRELSZ: pltsz = d->d_un.d_val; break;
case DT_RELR: relr = d->d_un.d_ptr; break; case DT_RELRSZ: relrsz = d->d_un.d_val; break;
}
// DT_RELR 槽:减回基址即与地址无关(在副本上做,复用 relr_apply 的走法)| DT_RELR slots: subtracting the base makes them address-independent (done on the copy, reusing relr_apply's walk)
if (relr) { uint8_t *b = m->base; m->base = cp; relr_apply(m, relr, relrsz, -(uint64_t)b); m->base = b; }
const Elf64_Rela *tabs[2] = {(const Elf64_Rela *)(m->base + rela), (const Elf64_Rela *)(m->base + jmprel)};
uint64_t sizes[2] = {relasz, pltsz}; unsigned nslot = 0;
for (int t = 0; t < 2; t++)
+9
View File
@@ -28,6 +28,15 @@ rel = {}
for k in range(tags[8] // 24): # DT_RELA = 7, DT_RELASZ = 8
off, info, add = struct.unpack_from('<QQq', d, tags[7] + k * 24)
if info & 0xffffffff == 1027: rel[off] = add # R_AARCH64_RELATIVE
# DT_RELR(36 / 35,SDK 20 起):加数在槽里;偶数项 = 起点,奇数项 = 其后 63 槽的位图 | DT_RELR (36 / 35, from SDK 20): the addend is in the slot; even entry = start, odd entry = bitmap of the next 63 slots
where = None
for k in range(tags.get(35, 0) // 8):
e = struct.unpack_from('<Q', d, tags[36] + k * 8)[0]
if not e & 1:
rel[e] = struct.unpack_from('<Q', d, e)[0]; where = e + 8; continue
for b in range(63):
if e >> (b + 1) & 1: rel[where + b * 8] = struct.unpack_from('<Q', d, where + b * 8)[0]
where += 63 * 8
# 1) loader: name -> GOT entry -> slot
i, reg, got_of_reg, cur, fetched, slots, order = LOADER // 4, {}, {}, None, None, {}, []
+21
View File
@@ -205,6 +205,27 @@ CUTS = {
'8c08b9719e085f91847b5e0f935d9488': [
(0x32f92d4, 'f303002a', '33008052', 'QualitySettings.set_vSyncCount 包装:参数 mov w19,w0 → mov w19,#1(游戏自己的 60 帧路径)', '仅宿主'),
],
# 塞尔达传说 旷野之息 1.9.0(v1114112)main:60 帧。sead::GameFrameworkNx 的主循环(main+0x112660c)开着动态间隔(框架 +0x267 = 1):
# 每帧按上一帧耗时算要几个垂直同步(向上取整),夹在 [2, 最大 3](最大值取自间隔控制器 +0xc,构造 main+0x1126c04),超时连续 3 帧才升档,
# 降档每 6 帧降一级、降到 2 为止,结果经 setWaitVsyncInterval(vtable +0x130,main+0x11269b8)交给出图(nvnWindowSetPresentInterval)。
# 游戏逻辑的时间步跟着走:ksys::VFR 以基础间隔 2 初始化(main+0x127e740 常数 2 → VFR::init main+0x151d2d0),每帧 VFR::updateInterval(main+0x151d3d0,
# 两处调用 main+0xcf9ad4 / main+0x1281044)取框架当前间隔(vtable +0x138),帧比 = 当前间隔 / 2——间隔 1 时每帧推进半个 30 帧拍,游戏速度不变;
# 卡了升回 2 / 3 时帧比 1 / 1.5,同样不变速。所以只把下限从 2 改成 1:候选间隔的下限(两处)、升档时记的降档目标 max(候选, 3) − 1 → max(候选, 2) − 1、
# 降档停在 < 3 → < 2。真机上同样逻辑按 1 起步即 60 帧(GPU 跟不上时自己退回 30)
# The Legend of Zelda: Breath of the Wild 1.9.0 (v1114112) main: 60 fps. The main loop of sead::GameFrameworkNx (main+0x112660c) runs a dynamic interval (framework +0x267 = 1):
# every frame it computes how many vsyncs the last frame needed (rounded up), clamped to [2, max 3] (the max comes from the interval controller +0xc, constructed at main+0x1126c04); it steps up after 3 overrunning frames in a row,
# steps down one level every 6 frames until 2, and hands the result through setWaitVsyncInterval (vtable +0x130, main+0x11269b8) to presentation (nvnWindowSetPresentInterval).
# The game logic time step follows it: ksys::VFR is initialized with a base interval of 2 (constant 2 at main+0x127e740 → VFR::init main+0x151d2d0), and every frame VFR::updateInterval (main+0x151d3d0,
# called at main+0xcf9ad4 / main+0x1281044) takes the framework's current interval (vtable +0x138); the frame ratio = current interval / 2 -- at interval 1 each frame advances half a 30 fps tick and the game speed stays the same;
# when it struggles and steps back to 2 / 3 the ratio is 1 / 1.5, again at the same speed. So only the floor changes from 2 to 1: the lower bound of the candidate interval (two places), the step-down target recorded when stepping up, max(candidate, 3) - 1 → max(candidate, 2) - 1,
# and stepping down stops at < 3 → < 2. On hardware the same logic starting from 1 would also be 60 fps (falling back to 30 by itself when the GPU cannot keep up)
'cd57b23fa4bbad65803d9788c01821ee': [
(0x112670c, '6041201e', '4041201e', '动态间隔候选下限:fmov s0,s11(2.0)→ fmov s0,s10(1.0)', '仅宿主'),
(0x1126710, '7f0b0071', '7f070071', '动态间隔候选下限:cmp w27,#2 → cmp w27,#1', '仅宿主'),
(0x112665c, '78008052', '58008052', '升档时记的降档目标 max(候选, w24) − 1:mov w24,#3 → mov w24,#2', '仅宿主'),
(0x1126748, '1f0d0071', '1f090071', '升档时记的降档目标:cmp w8,#3 → cmp w8,#2', '仅宿主'),
(0x1126794, '3f0d0071', '3f090071', '降档到 < 3 停 → 到 < 2 停:cmp w9,#3 → cmp w9,#2', '仅宿主'),
],
'56ec24747ec9e7f19b3efb062483a19d': [
(0x346430, '08fca752', '087ca052', 'AddressRegionManager 构造:节点区上限 mov w8,#0x3fe00000 → mov w8,#0x3e00000', '仅宿主'),
(0x3464cc, '09fca752', '097ca052', 'AddressRegionManager 构造:节点区登记为已占区域的长度 mov w9,#0x3fe00000 → mov w9,#0x3e00000', '仅宿主'),
+9
View File
@@ -36,6 +36,8 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
- **SIGILL / trap on a system register read** → newer SDK reads a register macOS traps (`CTR_EL0` in SDK 16's `FlushDataCache`) → rewrite to a constant.
- **`B()` range assertion in `patch_nso.py` (`site_plain`), only for one module** (Metroid Dread `main`) → its bss is 1.8 GB, the engine's static heap, so a patch area appended after the image starts 1.9 GB from the code and no `B` (±128 MB) reaches it. When an appended patch area would start beyond 112 MB, the rewriter puts it 32 MB *before* the module base (negative `PATCH_OFF`). `patched.bin` then holds only text + ro + data (`FILE_IMAGE_LEN`) followed by the patch bytes, and the loader maps the bss and the key page as zero pages (`IMAGE_SIZE`). The Mac loader, the iOS embedding (`gen_embed.py`: no bss in the executable) and `QueryMemory` handle both layouts; Mario's and Engage's output is byte-identical to before.
- **Every pointer in `.data` / the GOT is a module offset, crashes everywhere** (SDK 20: The Legend of Zelda: Breath of the Wild 1.9.0) → the modules pack their relative relocations as `DT_RELR` (tag 36, size tag 35): `main` has 223,994 of them and its `DT_RELA` only symbol relocations. Each 8-byte entry is either an even offset (relocate it, move to the next slot) or an odd bitmap whose bits 1–63 stand for the following 63 slots; the addend is the slot's own value (`relr_apply` in `loader.c`; `DM_LOADHASH` and `nvn_slots_got.py` read it too). The loader's log line counts them as `relr=`.
### Memory and address space
- **`StandardAllocator::Initialize` aborts in `nninitStartup`** → the NPDM declares a system resource (offset 0x14 of `META`), so `IsVirtualAddressMemoryEnabled` is true and the allocator reserves all free memory (about 3.2 GB for Engage) as one region in the alias region, after the ~1 GB node area → the profile's `mem_alias` / `mem_reserve` must hold it (Engage: 5.5 / 8 GB on the Mac).
@@ -49,6 +51,7 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
- **Some textures are re-uploaded every frame (several MB per frame)** → the dirty-page tracker registers each memory pool rounded to 16 KB host pages, but NVN pools are only 4 KB aligned, so neighbouring pools share their first / last host page and one address can lie in several registered ranges. Taking the first range that contains an address judged textures in the second pool untracked (always dirty), released only one range on a write fault, and could unregister the neighbour when a pool was destroyed. `nvn_dirty.c` now uses the range that holds the whole span, releases every range covering a faulting page, and removes a range by the pool's exact start (`t_dirty_slots` covers it).
- **A null write as soon as the main thread starts recording a command buffer** (13 Sentinels: a constructor at `main+0x2fa9a0` runs on `this = 0` when the prologue enters its first battle) → the object came from one of the engine's ring heaps (`main+0x2d83ec`), and that heap had base 0. At startup the engine takes one GPU memory pool, sized as exactly the sum of three ring heaps, and cuts the heaps out at 64 KB alignment *relative to the pool's GPU address* (`main+0x30c118`). On the Switch a pool's GPU address is big-page aligned; the host's GPU address is the CPU address, and the storage came from the game's `malloc` rounded only to 4 KB. Up to 60 KB was lost at the front, the third heap (16 MB) did not fit, and the sub-allocator returned 0. The game's `malloc` is `nn::mem::StandardAllocator::Allocate(size)`; on this build the host routes allocations of 64 KB or more to `Allocate(size, 0x10000)` (`hle_os.c`). Probing the heap object at the allocation (`DM_PROBE=main:2bec60@x0+8`) showed the zero base directly.
- **SIGBUS inside a host hook (`snprintf` under `mount_ex` / `h_fs_MountSave`), then the process hangs** (Super Mario 3D World + Bowser's Fury) → the game's save thread has an 8 KB stack (CreateThread stack top `0x…9fe0`, fault `0x21a0` below it). Host hooks run on the calling guest thread's stack, and the file system hooks size their path buffers by PATH_MAX: one file open takes 6–7 KB (`-fstack-usage`: `OpenFile` 1.3 KB → `host_path_ex` 1.9 KB → `path_component` 3.2 KB); on hardware the same call is one IPC. Every hook in `hle_fs.c` now registers through `HLE_FORCE_BIG` (`hle.h`): a compile-time stub loads the implementation into x16 and jumps to `dm_bigstack_enter` (`hle_bigstack.c`), which moves the call to a per-thread 512 KB host stack (with a guard page) when sp lies in the guest address block, passing x0–x8 and d0–d7 through. Only for functions with register arguments only (no variadic or stack arguments). iOS forbids generating code at run time, so the stubs are static.
- **SIGBUS inside Metal (`AGCDeserializedReply`, an AGX compiler block) at an address in the guest stack region** (Breath of the Wild) → a host NVN call did Metal work on a game thread with a small stack: `nvnQueueSubmitCommands` replays the frame, and ending a render pass made the driver compile its fast-clear blit program on the calling thread. NVN functions are not registered through `HLE_FORCE`, so they got their own big-stack entry: every implemented NVN function except the `nvnCommandBuffer*` recording calls goes through `nvn_big_entry` (`nvn_entry.S`), which finds the implementation by slot and hands it to `dm_bigstack_enter`.
- **Write through a null pointer at +0x1c0 during startup** → `GetInfo(UserExceptionContextAddress)` must return a real buffer.
- **Heap allocations fail late in a busy scene** → the reported application pool (3285 MB, `APP_POOL`) and used memory decide the game's heap size; report Switch-accurate numbers.
@@ -94,6 +97,7 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
- **Abort in `nn::sm::Initialize` under `nn::pctl::IsFreeCommunicationAvailable` (Engage, entering its first battle map)** → the SDK connects to the parental-control service through `sm`. The host answers it like the other free-communication calls: not available (`hle_misc.c`).
- **Abort inside `nn::sm::Initialize`** → an SDK path reached a system service the host doesn't replace → hook the public SDK entry point named in the backtrace (`HLE_FORCE` in the relevant `hle_*.c`). The log shows it as `SVC 0x1f` (ConnectToNamedPort, port name `sm:`) and `SVC 0x21` answered by placeholders, then `nn::diag` abort.
- **A game update aborts at boot where the base game ran** → the update imports SDK functions the base game didn't. Diff the undefined dynamic symbols of the two `main` modules before the first run. Mario 1.2.1 added 28, among them `nn::nfp` (amiibo, initialized at boot), `pl::IsRunningOnOunce` (is this a Switch 2), `ec::ShowShopApplicationInformationForUpgrade`, `account::PopOpenUsers` and `aoc::CountAddOnContent`. Read the call sites before choosing answers: Mario only reads the count from `PopOpenUsers` and falls back to `TryOpenPreselectedUser` on 0, and only counts add-on content when running on a Switch 2. The host answers as a Switch 1 with no amiibo and no add-on content (`hle_sdk16.c`, `hle_account.c`, `hle_misc.c`).
- **SDK 20 calls that reach a service** (Breath of the Wild 1.9.0, SDK 20.5.22): `applet::GetAppletResourceUserId` (SDK 20's `OpenAudioRenderer` aborts on an ARUID of 0, which the SDK's own getter returns without am; the host reports 0x80), `pl::RequestSharedFontLoad`, the VR-mode calls (`pl::IsVrMode` / `BeginVrMode` / `EndVrMode`), `pctl::CheckStereoVisionPermission` (bool, permitted), `oe::TryPopLaunchParameter` (none), `time::StandardSteadyClock::GetCurrentTimePoint` (host monotonic seconds, fixed source ID; `GetSpanBetween` runs in `sdk`), the console six-axis sensor (`hid::*SevenSixAxisSensor*`: zero states) and more `nfp` calls (`AttachActivateEvent` / `AttachDeactivateEvent` crashed in the SDK at `sdk+0x3f2a78`; also two-argument `Mount`, `Unmount`, `GetCommonInfo`, `GetRegisterInfo`, `GetState`, `Finalize`). Two new imports need nothing: `nvnPrivate0` is a flag in `sdk`'s bss that enables an inlined command-buffer fast path when the NVN driver sets it (it stays 0), and `nn::ro::ProtectRelro` (called from `main`'s DSO init) runs natively on the host's no-op `SetMemoryPermission` / `SetMemoryAttribute`.
- **Code inherited from another title calls fixed addresses** → harmless on the build it was written for, silent memory corruption on any other. d2r-mobile's 3D render scale read a renderer pointer at `main+0x31e0848` and called `main+0x8582f0`; in Mario v0 that is the middle of a static initializer and only did nothing because the pointer happened to read 0. Such code checks `module_t.build_id` now (written into `fixups.tsv` by the rewriter) and stays off on other builds.
- **Device / subsystem setup returns quietly and the next stage never starts** → a version or capability check failed (NVN API version, a feature query) → compare what the game queries with what the host answers.
- **Font engine runs on garbage** → the system shared font must look like BFTTF: the SDK calls `ScalableFontEngineHelper::Decode(ptr, size)`, which XOR-decodes in place unless the first word is the "already decoded" marker (LE `0x18029a7f`) → serve `marker + bswap(size ^ 0x49621806) + TTF` (`hle_sdk16.c`).
@@ -118,9 +122,14 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
- **Controller-button icons in text show as `?`** → those glyphs come from the system shared font's Nintendo-extension set (private-use code points; Mario's `GameMsg/Key.msbt` uses U+E0E0–E0F0 for A/B/X/Y/L/R/ZL/ZR/SL/SR/D-pad/+/−, U+E0AB–E0AE for D-pad directions, U+E090–E097 for stick arrows, U+E100/E101 for sticks). `dm/tools/make_ext_font.py` generates a stand-in `nintendo_ext.ttf` with those icons; the host serves it for shared-font type 5 (`DM_FONT_DIR` or `DM_SHARED_FONT_EXT`) and a desktop font for the other types. To find which code points a title uses, scan the `TXT2` sections of its MSBT files for U+E000–F8FF.
- **Shared fonts on iOS**: the macOS stand-in path (`/System/Library/Fonts/Supplemental/Arial Unicode.ttf`) does not exist on a phone, and `GetSharedFontLoadState` would report "not loaded". The iOS app ships `dm/font/standard.ttf` and `dm/font/nintendo_ext.ttf` (copied / generated by `build_ipa.sh`) and sets `DM_FONT_DIR`.
### Frame rate
- **Running a 30 fps game at 60** (Breath of the Wild): find where the present interval comes from and how the logic time step follows it before patching anything. BotW's `sead::GameFrameworkNx` main loop (`main+0x112660c`) runs a dynamic interval: from the last frame's time it computes how many vertical syncs were needed, clamps that to [2, 3] (the maximum from the interval controller built at `main+0x1126c04`), steps up after three overrunning frames and down one level every six frames, and sets the result through `setWaitVsyncInterval` (vtable +0x130, `main+0x11269b8` → `nvnWindowSetPresentInterval`). The game logic follows on its own: `ksys::VFR` is initialized with a constant base interval of 2 (`main+0x127e740`) and every frame `VFR::updateInterval` (`main+0x151d3d0`) takes the framework's current interval, giving a frame ratio of interval / 2. Five rewriter cuts lower the floor from 2 to 1 (`patch_nso.py`, build `cd57b23f…`): the candidate floor (two places) and the hysteresis targets. Probes confirmed the result: the framework presents at interval 1, VFR reports interval 1 with ratio 0.5, and when a frame runs long the game steps back to 2 with ratio 1, so the game speed does not change. The zeldaret/botw decompilation (`VFR.cpp`) gave the semantics; 1.9.0's addresses were found by the constants (`60 / interval`, the `fmov s11, #2.0` floor).
### Audio
- **Check whether the SDK carries the renderer.** List the `sdk` module's exports for `nn::audio::server::` and `nn::audio::dsp::CommandListProcessor`. If they are there (Mario, SDK 16), do not write a mixer: switch `AudioRendererParameter` to rendering device = CPU (+0x2c = 1) and execution mode = manual (+0x30 = 1) in `GetAudioRendererWorkBufferSize` and both `OpenAudioRenderer` overloads, call the SDK originals (`guest_sym` returns the module's own code even for a forced symbol), and drive `ExecuteAudioRendererRendering` from a guest thread (`dm_guest_thread_spawn`). The open then goes through `CreateAudioRendererManagerByDfc`; no IPC.
- **SDK 20's `AudioRenderSystem` has another layout** (Breath of the Wild): the getters the host used (`GetExecutionMode`, `GetRenderingDevice`, `GetCommandBufferWorkBufferSize`) are inlined away, `GenerateCommand` takes and returns a `Span<std::byte>` (the generated list comes back in x0 / x1), the device and mode are words at +0x48 / +0x4c, the command buffer is at +0xf8 / +0x100, and `UpdateImpl` locks the mutex at +0x2d0 instead of +0x1f0. The DeviceSink command became ID 0x14 (SDK 20 inserted a command before it) with the same layout. `render_once` picks the layout by whether the Span version of `GenerateCommand` exists (`hle_audren.c`).
- **The CPU processor drops the final mix.** `dsp::SendCommandBuffer`, `dsp::Wait` and `dsp::Signal` are stubs in the application build, and DeviceSink (command 0x13) is a no-op in `CommandListProcessor::Process`. Take the output after `Process` from the mix buffers the DeviceSink command names (layout in `hle_audren.c`).
- **Generation needs the renderer's lock.** `AudioRenderSystem::Update` holds the SDK mutex at `AudioRenderSystem + 0x1f0`; `GenerateCommand` does not take it, so the host does (the auto-mode system thread does the same in `SendCommandToDsp`).
- **At least one rendered frame between two updates.** An effect's "needs init" parameter state is set only in the update where the game flags it new; the next update copies the game's value over it. Mario updates twice in a row at start-up; with a real audio device the first frame came after both, and the light limiter later crashed on a null state pointer (`ApplyLightLimiterEffect`, `sdk+0x228494`). The `AudioRenderSystem::Update` hook renders a catch-up frame first when nothing was rendered since the last update. `DM_AUDREN_TRACE=1` logs every update with the number of frames rendered since the previous one.
+14 -2
View File
@@ -108,13 +108,25 @@ Consequence: any optimization that infers "which textures a draw reads" from bou
- **Targets the fragment shader does not write**: volume draws after the G-buffer (`50d813761ca3f1f9` and others) write only outputs 0 and 2, with blending and all channels enabled on targets 1 and 3. Maxwell leaves targets outside the shader's output map untouched; on Metal such an attachment gets undefined values blended in. Pipelines now get a zero write mask and no blending for colour outputs the shader does not write (reflection `colors`, `color_known` in `nvn_refl.m`). All five games' regressions were unchanged by it.
- **Open: one scaled texture copy** (1920×1080 → 1280×720, RGB10A2, flags `ENGINE_2D`) when leaving the title: Metal blits cannot scale, so it is skipped with a red line; it is probably the captured frame for the transition.
### Seen in The Legend of Zelda: Breath of the Wild
- **Shader containers**: 19,592 shaders: `Shader/*.sbfsha` (BNSH) and the gsys / ksys / terrain resource packs inside `Pack/Bootup_Graphics.pack` and `Pack/TitleBG.pack`, the particle effects in `Effect/*.sesetlist` (11,373 shaders, 101 compute), layouts (`.sblarc`) and 37 in `main`. All bind with Mario's texture handle layout; the full build had no failure and no unresolved binding.
- **Fixed: the 3D scene black, only particles and the HUD drawn.** The engine binds almost every texture with `nvnCommandBufferBindTexturesDeferred` (`main+0xec2e64`, one handle, the pointer being the handle field inside the texture-sampler object). "Deferred" means the GPU reads the handle from that address *when the command executes*; the game records its command blocks once, submits them every frame and writes the handles afterwards. The host copied the handles at recording, mostly zeros, so the deferred-lighting composite (one full-screen triangle per material class, blending into the HDR target) read the dummy texture 0 for all of its inputs. The bind record now keeps the source address (`p_bind_handles.defer / src`) and every replay-time reader refreshes the handles first (`bh_fresh` in `nvn_mtl.m`). How it was found: a frame data-flow table built from `DM_NVN_PASSDUMP_ALL` (per pass: attachments, shader keys, sampled serials) showed the half-resolution lighting outputs never being read, and the composite pass's `B` lines were all `tid=0 哑`. Without `_ALL` the dump lists only the first draw of each pass, which hid the composite.
- **Fixed: the same texture attached at two colour slots.** The G-buffer passes attach the albedo and another buffer twice each (c1 = c2, c3 = c4); the duplicate slots have write mask 0. On Maxwell the ROP writes only masked channels and the slots do not interfere. On Apple's TBDR each attachment has its own tile memory and the whole tile is stored at the end of the pass, so the copy stored later (loaded, never written) overwrote the other one's writes, in a different order for each 32×32 tile: the albedo held only scattered squares. A slot whose texture, level and layer repeat an earlier slot is now left unattached (`rt_dup_slot`), also in pipeline prediction; a draw that really writes such a slot is reported once (`颜色槽 … 与前面的槽挂同一张贴图`). Records hold copies of texture objects (`tex_snap`), so slots are compared by serial, not by pointer.
- **Cube textures sampled as cube arrays.** 1,512 shaders sample the environment probes as `texturecube_array` while a single cube texture is bound. Metal can make a cube-array view only of a 2D-array base texture, so NVN cube textures are now created as 6-layer 2D arrays and bound through cube / cube-array views by `tex_fit` (which also maps a cube array read as a cube to its first cube).
- **Open: a compressed texture as a render target.** A 32×32 RGTC1 (BC4) texture is written through an `RGBA16UI` view (one texel per 8-byte block: BC4 encoded on the GPU). Metal can neither attach a compressed format nor make an uncompressed view of one, and the pipeline-descriptor validation aborts the process, so the attachment is left out and its draws do nothing (`渲染目标是压缩格式贴图 …`). Doing it properly needs an uncompressed proxy target copied into the BC4 texture through a buffer after the pass.
- **Big-stack NVN entry points**: the render thread's stack is small, and Metal's driver compiles internal programs (a fast-clear blit) on the calling thread while the host ends a render pass inside `nvnQueueSubmitCommands`. That overflowed the guest stack (SIGBUS in `AGCDeserializedReply`, address in the guest stack region). All implemented NVN functions except the `nvnCommandBuffer*` recording calls now run on the per-thread 512 KB host stack ([03](03-bringup.md), big-stack entry; `nvn_big_entry` in `nvn_entry.S`).
- **60 fps** is a rewriter cut on the game's frame-interval logic ([03](03-bringup.md)); the engine's variable frame rate (`ksys::VFR`) scales its time step by the present interval itself, so the frame rate the player gets is whatever host and GPU can sustain, with the game speed unchanged.
- **Address → memory pool lookups**: BotW keeps about 1,760 memory pools alive, and `mtl_addr` / `mtl_gpu_addr` scanned them all for every buffer binding of every draw: a CPU sample of the submit thread had about 1,000 of its samples under those scans (vertex buffers in `draw`, uniform buffers in `encode_args`). They now try the last hit, then a binary search over pools sorted by start address (`pool_find`, rebuilt when pools are added or removed). Host submit time in the shrine went from 21 ms to 7 ms per frame (p50), the median frame from 38 ms to 21 ms.
- **Frame rate in the Shrine of Resurrection on the Mac (M4 Pro)**, after the above: docked (1600×900) GPU p50 13–14 ms, frame p50 about 21 ms; handheld (`DM_OPMODE=0`, the game renders 1280×720) GPU p50 9.3 ms and frame p50 16.7 ms, a steady 60. The per-pass GPU timer (`DM_NVN_PASSGPU`) wrote no samples under Metal 4 in this run.
## 7. Diagnostic switches (host)
| Switch | Output |
|---|---|
| `DM_NVN_SHOT=f1,f2:path_%d.png` | Window screenshots at those presented frames |
| `DM_NVN_PASSDUMP=f` or `f1-f2` (+ `DM_NVN_PASSDUMP_ALL=1`) | Every render pass of those frames: `P` attachments (with `mtl=` the Metal texture behind them), `C` clear colours, `K` shader keys, `B` each binding's actual source (also with `mtl=`), `d`/`D` draw state (`b0`–`b3` the blend states of targets 0–3), `Y` inline updates, `X` texture clears |
| `DM_NVN_TEXDUMP=f:path_%d.png` (+ `DM_NVN_TEXSNAP=i,…`) | Every colour render target of frame f, as PNG. `TEXSNAP` copies the attachments of in-frame pass i (2D, or slice 0 of a 2D array) in a separate command buffer, which does not run at the pass's position in the GPU timeline: a snapshot taken right after a clearing pass showed a complete image, so read it as "what this target holds around the frame", not "right after pass i" |
| `DM_NVN_PASSDUMP=f` or `f1-f2` (+ `DM_NVN_PASSDUMP_ALL=1`) | Every render pass of those frames: `P` attachments (with `mtl=` the Metal texture behind them), `C` clear colours, `K` shader keys, `B` each binding's actual source (also with `mtl=`), `d`/`D` draw state (`b0`–`b3` the blend states of targets 0–3), `Y` inline updates, `X` texture clears, `T` texture copies, `N` passes skipped because no attachment could be attached. Without `_ALL` the `K` / `B` / `R` lines cover only the first draw of each pass |
| `DM_NVN_TEXDUMP=f:path_%d.png` (+ `DM_NVN_TEXSNAP=i,…`) | Every colour render target of frame f, as PNG (`%d` = serial × 100 + layer × 10 + level; 2D arrays by the attached layer). `TEXSNAP` copies the attachments of in-frame pass i (2D, or slice 0 of a 2D array) in a separate command buffer, which does not run at the pass's position in the GPU timeline: a snapshot taken right after a clearing pass showed a complete image, so read it as "what this target holds around the frame", not "right after pass i" |
| `DM_NVN_CBDUMP=dir` (with PASSDUMP) | Raw uniform buffer contents per binding |
| `DM_NVN_HILITE=fskey,…` | Replace those fragment shaders with solid colours: tells "geometry is wrong" from "shading is wrong" |
| `DM_NVN_SHDUMP=dir` | Control + code of programs missing from the shader map |
+1 -1
View File
@@ -77,7 +77,7 @@ Patterns that worked: print every call of one function with its arguments; stop
## Scripted input and thread scheduling
- `DM_KEYS="<seconds>:<button>[+button][~hold],…"` presses buttons from host start (`hle_hid.c`). `<seconds>:<button>*<count>/<interval>` repeats a press, e.g. `42:A*170/2` presses A every 2 s from 42 s on, 170 times; at most 1,024 steps after expansion. A long run through a game's story is then a single switch, such as Metroid Dread's title, new game and story recap. `LLeft` / `LUp` / `LRight` / `LDown` push the left stick fully (Super Mario 3D World moves on its map by stick only); combos such as `L+R~0.5` press buttons together.
- `DM_KEYS="<seconds>:<button>[+button][~hold],…"` presses buttons from host start (`hle_hid.c`). `<seconds>:<button>*<count>/<interval>` repeats a press, e.g. `42:A*170/2` presses A every 2 s from 42 s on, 170 times; at most 1,024 steps after expansion. A long run through a game's story is then a single switch, such as Metroid Dread's title, new game and story recap. `LLeft` / `LUp` / `LRight` / `LDown` push the left stick fully (Super Mario 3D World moves on its map by stick only); combos such as `L+R~0.5` press buttons together. An `f` before the time counts presented frames instead of seconds (`f2500:A`, repeat intervals in frames too): with another heavy job running, seconds drift away from the game's progress and a route stops lining up; frame numbers do not. Breath of the Wild from its title menu into the autosave: `DM_KEYS=f2500:A,f2800:A,f3100:A,f3400:A` (control at frame ~3900).
- Frame numbers are a stable way to aim dumps (`DM_NVN_PASSDUMP=<frame>`, `DM_NVN_SHOT=…`) at the same scene in another run: the per-frame NVN call counts (`[nvn] 帧 N:… 次调用`) matched between runs of the same script. Times in seconds drift with load.
- `DM_PREEMPT=0` / `1` turns the same-core preemption approximation off / on whatever the profile's `core_preempt` says (`hle_thread.c`). The thread dump at exit reports `同核抢占让步 N 次,共 T ms,到上限 K 次` (yields, total wait, 5 ms cap hits). A higher-priority thread that keeps a core busy shows up in the first eight `同核抢占让步到上限` lines, with its last SVC and call site.
+1 -1
View File
@@ -2,7 +2,7 @@
These pages are for an agent asked to analyse a Switch NSP and port it with the native-execution approach of this repository and [d2r-mobile](https://github.com/jhaizhou-ops/d2r-mobile): the game's own aarch64 code runs natively on Apple hardware, a rewriter patches only the instructions Apple's kernel cannot run, the host replaces Horizon system services, and NVN is reimplemented on Metal.
Everything here was learned on Diablo II: Resurrected (SDK ~12, NVN 53, the original target of d2r-mobile) and Super Mario Bros. Wonder (SDK 16.2.0, NVN 55, this repository; its update 1.2.1 uses SDK 16.2.12), then extended by Fire Emblem Engage (Unity, SDK 12.3.7), Metroid Dread (MercurySteam's engine, SDK 11.4.3) 13 Sentinels: Aegis Rim (Vanillaware's engine, SDK 11.4.3, the same `sdk` and movie module as Dread) and Super Mario 3D World + Bowser's Fury (Nintendo EPD's sead / agl engine, SDK 10.4.0, NVN API 53). Where titles differed, each is given; the next title will differ again, so treat every number below as "measure it, don't assume it".
Everything here was learned on Diablo II: Resurrected (SDK ~12, NVN 53, the original target of d2r-mobile) and Super Mario Bros. Wonder (SDK 16.2.0, NVN 55, this repository; its update 1.2.1 uses SDK 16.2.12), then extended by Fire Emblem Engage (Unity, SDK 12.3.7), Metroid Dread (MercurySteam's engine, SDK 11.4.3) 13 Sentinels: Aegis Rim (Vanillaware's engine, SDK 11.4.3, the same `sdk` and movie module as Dread) Super Mario 3D World + Bowser's Fury (Nintendo EPD's sead / agl engine, SDK 10.4.0, NVN API 53) and The Legend of Zelda: Breath of the Wild (EPD's sead / ksys engine, SDK 20.5.22, `DT_RELR` relocations, update 1.9.0). Where titles differed, each is given; the next title will differ again, so treat every number below as "measure it, don't assume it".
| Page | Covers |
|---|---|
+7 -1
View File
@@ -141,6 +141,8 @@ def scan_nsps(nsp_dir):
Group by game: {application title ID: {'base': path, 'updates': [(version, path)], 'dlc': [(title ID, version, path)]}}"""
fams, unknown = {}, []
for p in sorted(nsp_dir.glob('*.nsp')):
if p.stat().st_size == 0: # 下载不全留下的空文件(塞尔达传说 旷野之息的文件夹里有一个 0 字节的 DLC NSP,旁边是完整的 (1).nsp)| an empty file left by an incomplete download (the Zelda BotW folder holds a 0-byte DLC NSP next to the complete (1).nsp)
say(f'跳过空文件 | skipping an empty file: {p.name}'); continue
tid = title_of(p)
if tid is None: unknown.append(p); continue
low = tid & 0xFFF # 基础版 …000,更新 …800,DLC = 基础版 + 0x1000 + 序号 | base …000, update …800, DLC = base + 0x1000 + index
@@ -322,7 +324,11 @@ def extract_aoc(nx, keys, work, app, fam, logs):
Returns None without DLC. DLC packages are small (121 KB each for Engage, the content itself is in the update), so they are extracted again every time"""
if not fam['dlc']: return None
tmp = work / 'aoc.partial'; rmtree(tmp)
for tid, _, p in sorted(fam['dlc']):
best = {} # 同一 DLC 有几份时取版本最高、同版本取最大的那份 | when one DLC comes in several copies, take the highest version, and the largest of equal versions
for tid, ver, p in fam['dlc']:
k = (ver or 0, p.stat().st_size)
if tid not in best or k > best[tid][0]: best[tid] = (k, p)
for tid, (_, p) in sorted(best.items()):
idx = tid - app - 0x1000
say(f'DLC 序号 {idx} | DLC index {idx}: {p.name}')
run([nx, keys, '--aoc', p, tmp / str(idx)], logs / f'aoc_{idx}.log')