Tears of the Kingdom reaches gameplay on the Mac (3D scene still black)

New profile dm/games/0100F2C0115B6000 (update 1.4.3 = v720896; main,
subsdk0 and sdk, the last two the same builds as Breath of the Wild 1.9.0).
It plays the opening and reaches the first gameplay at 30 fps with the HUD,
stable over a 5-minute run. The 3D scene is still black: the half-resolution
deferred-lighting pass leaves its outputs at zero (being investigated).

- Memory: the NPDM declares a system resource (alias memory) and the game
  also calls SetHeapSize(0x86000000). Mac partition 8 GB = 256 MB in front +
  5 GB alias + 256 MB stack + 2.5 GB heap; the alias high-water mark is
  1.75 GB. Too large for an iPhone; to be measured and shrunk.
- SDK 20 calls that reached a service, one abort per boot: oe
  GetPreviousProgramIndex (-1); the hardware Opus decoder's C interface,
  forwarded to the sdk's software OpusDecoder; cache storage (list reports
  none, create / mount use a host folder beside the temporary storage);
  fs::IsSaveDataExisting for another title (it asks for Breath of the Wild's
  save: none) and MountSaveDataReadOnly; time::ClockSnapshot::
  CreateWithStandardSystemClock (0xD0 bytes filled on the host); web::
  ShowWebPage (unavailable, as the lobby page).
- shader_split.py reads zstd frames that name a dictionary, from
  Pack/ZsDic.pack.zs (Python 3.14+ compression.zstd): 28,669 shaders;
  28,636 compile (chain 29bb51ae3ce7).
- New profile key tex_flush_upload, off by default and on only here: TOTK
  writes its streamed textures from the CPU straight into pool memory and
  publishes them with nvnMemoryPoolFlushMappedRange (9,436 calls at boot);
  hle_nvn.c registers such textures by range and marks the ones a flush
  covers CPU-written, so they upload from guest memory. Its visual effect is
  not confirmed yet (the scene stays black for the lighting reason above).
- docs/agents/03-bringup.md: the new rewriter cases and the SDK 20 calls.

The Hades session's smoke runs of the seven playable games used a host built
with these hooks: only their known red lines.
This commit is contained in:
seedds committed 2026-10-18 13:32:48 +08:00
1 parent 495963cfa3
commit faae153b06
19 files changed
+49157 -8

No files matched your search

+1
View File
@@ -0,0 +1 @@
# generated
1 # generated
+6
View File
@@ -0,0 +1,6 @@
# chain b-29bb51ae3ce7 2026-10-18T11:41:59+08:00
35cbc686dd8461b8
4872bec9a5567f8b
759c14c6d029f531
9a5e3335514e4cbb
abbbf48f9b5e6598
1 # chain b-29bb51ae3ce7 2026-10-18T11:41:59+08:00
2 35cbc686dd8461b8
3 4872bec9a5567f8b
4 759c14c6d029f531
5 9a5e3335514e4cbb
6 abbbf48f9b5e6598
+2
View File
@@ -0,0 +1,2 @@
// 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 0100F2C0115B6000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-<V>/
#define DMSH_CHAIN "29bb51ae3ce7"
+52
View File
@@ -0,0 +1,52 @@
# The Legend of Zelda: Tears of the Kingdom (Switch, base game v0 + update 1.4.3 = v720896). In bring-up
# Nintendo EPD's engine (sead / agl / gsys, NintendoWare G3d2 / Ui2d / Vfx2, Havok), SDK 20.5.22, NVN; modules main + subsdk0 (NintendoSDK_movie) + sdk.
# subsdk0 and sdk are the same builds as Breath of the Wild 1.9.0's. Most RomFS files are zstd with dictionaries (*.zs; the dictionaries are Pack/ZsDic.pack.zs).
# Shaders: Shader/*.bfsha.zs (BNSH; material.bfsha holds most) and the layout shaders in Shader/ApplicationPackage.Nin_NX_NVN.release.sarc.zs.
# main imports nn::ro (loads NRO modules at run time). Values marked "Breath of the Wild's" are not measured for this game yet
# ---- Identity ----
title_id 0100F2C0115B6000
program_id 0100F2C0115B6000
# ---- App ----
name The Legend of Zelda: Tears of the Kingdom
ipa ZeldaTearsOfTheKingdom.ipa
app_category public.app-category.adventure-games
# the virtual gamepad starts in the 3D layout (D-pad, right stick, L3 / R3): the right stick turns the camera
pad_layout full
# ---- Versions ----
build v720896 1.4.3 main=277178b7dba1b6d4949a84778d4abc58 subsdk0=f39f4c431234c72eb520cfdd1001bbcf sdk=574284b40c06cf1821a44857939083be
package v720896
shader_versions v720896
# ---- Files ----
modules main subsdk0 sdk
romfs_marker Pack/ZsDic.pack.zs
zip_store .zs:.bwav:.mc:.webm:.metallib
# ---- Memory ----
# The NPDM declares 24 MB of system resource, so IsVirtualAddressMemoryEnabled is true and the game's memory lives in the alias region (as Engage's, see 0100A6301214E000/game.tsv).
# The game also calls SetHeapSize(0x86000000) in nninitStartup (2.1 GB of the pool as its heap; a 2 GB heap region aborts there with 0xd401).
# Until measured: the Switch's full application pool (3285 MiB) and 8 GB = 256 MB in front + 5 GB alias region + 256 MB stack region + 2.5 GB heap region.
# Too large for an iPhone (one reservation of at most 6 GB without the extended virtual addressing entitlement): measure, then shrink
mem_reserve 0x200000000
mem_alias 0x140000000
mem_stack 0x10000000
app_pool 0xCD500000
# ---- Graphics ----
# Streamed textures are written by the CPU straight into pool memory (already in GPU layout) and published with nvnMemoryPoolFlushMappedRange (hle_nvn.c ft_add)
tex_flush_upload on
# ---- NVN driver constant segments (Breath of the Wild's values, the same NVN build) ----
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 (shader_split.py) ----
shader_groups /Shader/ApplicationPackage=layout /Shader/=shader /Effect/=effect /UI/=layout
shader_note Tears of the Kingdom shaders
Can't render this file because it contains an unexpected character in line 5 and column 70.
@@ -0,0 +1 @@
# 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.
1 # 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.
+258
View File
@@ -0,0 +1,258 @@
# 翻译查询的作答表(dm/gfx/tools/shader_split.py 生成,勿手改):键 <TAB> 事实 JSON。本作没有 PC 版可配对:只有计算着色器的线程组 / 共享内存(取自控制段),其余查询按翻译器缺省
00326ff37aed3099 {"local":[32,1,1],"shared":0}
019e4e1b9c9553ca {"local":[32,1,1],"shared":0}
01c0d1ea17e642a6 {"local":[32,1,1],"shared":0}
02338175eea7af9e {"local":[8,8,1],"shared":0}
036026ceba1c401c {"local":[32,1,1],"shared":0}
0361b332f3ce94da {"local":[32,1,1],"shared":0}
05d82c91c9aec39c {"local":[32,1,1],"shared":0}
0940f61b9f334fde {"local":[32,1,1],"shared":0}
0c9caba9fa7d33ad {"local":[32,1,1],"shared":384}
0cf23fdf0c0c32fc {"local":[32,1,1],"shared":640}
0d72acd2b8cb1d6a {"local":[32,1,1],"shared":0}
0e387987e6f4992c {"local":[32,1,1],"shared":896}
0f6f591041bc86de {"local":[32,1,1],"shared":0}
0fd391ee524c8c8d {"local":[32,1,1],"shared":0}
11151f3e946c78ab {"local":[32,1,1],"shared":0}
121371769c5b1119 {"local":[32,1,1],"shared":896}
13f1d51caa486b05 {"local":[32,1,1],"shared":0}
153b608043081764 {"local":[32,1,1],"shared":0}
183bab467cb19b35 {"local":[64,1,1],"shared":0}
196b056c3a640309 {"local":[32,1,1],"shared":0}
1a5c45f3943ac4ee {"local":[32,1,1],"shared":896}
1aaa5c83d776e776 {"local":[64,1,1],"shared":0}
1aade0eba6fd2576 {"local":[32,1,1],"shared":0}
1cd3e43e1f759030 {"local":[32,1,1],"shared":896}
1d706e5738c451cd {"local":[32,1,1],"shared":896}
1dc04ea73aee8414 {"local":[32,1,1],"shared":0}
1df991d98c87f43f {"local":[32,1,1],"shared":0}
1f1635fbb2f26648 {"local":[32,1,1],"shared":0}
1faa66a926900dc9 {"local":[32,1,1],"shared":1152}
235b57c3b0928fb8 {"local":[64,1,1],"shared":0}
25c44c9858bde833 {"local":[32,1,1],"shared":0}
2611d4d70189c9d3 {"local":[32,1,1],"shared":640}
28395c0eea6145d7 {"local":[32,1,1],"shared":896}
28b17a4facd01829 {"local":[64,1,1],"shared":0}
28d499d05873486f {"local":[32,1,1],"shared":0}
2a66228d48a647f7 {"local":[32,1,1],"shared":0}
2b7ef0a5be96cd0f {"local":[32,1,1],"shared":0}
2dbc25056c9ecd02 {"local":[32,1,1],"shared":384}
3105e8a1dd643ba7 {"local":[32,1,1],"shared":0}
32c256cd71e35a22 {"local":[64,1,1],"shared":0}
3334272d65b1682f {"local":[32,1,1],"shared":0}
3393138b6765af32 {"local":[16,16,1],"shared":0}
341a0021aa713960 {"local":[32,1,1],"shared":0}
3488f02fdb5f712a {"local":[32,1,1],"shared":896}
377df4102ccfa58f {"local":[32,1,1],"shared":0}
383c22b1b2c78340 {"local":[32,1,1],"shared":640}
39747c4f0461465b {"local":[32,1,1],"shared":0}
3c838dcd9cb6fdb2 {"local":[32,1,1],"shared":640}
3c8e9ba061067c07 {"local":[32,1,1],"shared":0}
3d5516ca71d4679a {"local":[64,1,1],"shared":0}
3eeed98ae676dff6 {"local":[32,1,1],"shared":640}
3efd916bfd38a433 {"local":[1,1,1],"shared":0}
4280f9ad6b910e69 {"local":[32,1,1],"shared":896}
432f415838ca2a54 {"local":[32,1,1],"shared":384}
434f8942541f7ca2 {"local":[64,1,1],"shared":0}
44c0007ef91245f0 {"local":[32,1,1],"shared":0}
455a4c670b32d9bb {"local":[64,1,1],"shared":0}
4799c39ddbf3330c {"local":[16,16,1],"shared":0}
47de8038d0890fb9 {"local":[32,1,1],"shared":0}
4855380f8bbab1de {"local":[32,1,1],"shared":384}
48e7cb41a927e17b {"local":[32,1,1],"shared":0}
4aa361f976086111 {"local":[32,1,1],"shared":896}
4acfa5bc7cbce897 {"local":[32,1,1],"shared":0}
4b162155c379236c {"local":[32,1,1],"shared":0}
4b9937cfe8c7c3f7 {"local":[32,1,1],"shared":0}
4ba18f6d9b55ab0e {"local":[32,1,1],"shared":896}
4bfe0838ecc4ac32 {"local":[32,1,1],"shared":0}
4c4fb955dc1fb09e {"local":[32,1,1],"shared":0}
4ec31daef028346c {"local":[32,1,1],"shared":640}
5091d8fdc5873bf0 {"local":[32,1,1],"shared":0}
52009ac3e8dc3d8a {"local":[32,1,1],"shared":0}
52fe86139d69346d {"local":[1,1,1],"shared":0}
547ce3b13d8b0c43 {"local":[32,1,1],"shared":0}
55abfa7d088943ec {"local":[16,16,1],"shared":128}
5830726ffb8e1e9c {"local":[32,1,1],"shared":0}
583d02f7e39c31a6 {"local":[32,1,1],"shared":384}
598a87ad813d96b6 {"local":[32,1,1],"shared":384}
5a612265220a4b29 {"local":[32,1,1],"shared":0}
5ad57ab5825c6f89 {"local":[32,1,1],"shared":0}
5afab70d888523cd {"local":[16,16,1],"shared":0}
5b134642b7c789b2 {"local":[1,1,1],"shared":0}
5bb7a0b9726a5468 {"local":[1,1,1],"shared":0}
5d2250291e4ee772 {"local":[32,1,1],"shared":0}
5de789ac4bf41f70 {"local":[64,1,1],"shared":0}
5f7882f060ff5a8d {"local":[32,1,1],"shared":0}
60714ba157cf4462 {"local":[32,1,1],"shared":0}
619b2efd2dcc0096 {"local":[32,1,1],"shared":0}
6258b3e6a2a6dbf2 {"local":[1,1,1],"shared":0}
64746596a60238e4 {"local":[32,1,1],"shared":0}
656501157bee9c0d {"local":[8,8,1],"shared":0}
65d5662baba025d8 {"local":[32,1,1],"shared":0}
679d01768e616022 {"local":[32,1,1],"shared":0}
67cbe2ed5523bb41 {"local":[64,1,1],"shared":0}
68d7cbf2b02afbe8 {"local":[32,1,1],"shared":0}
69cd3fb80e528114 {"local":[32,1,1],"shared":0}
6aa5c68168ad2593 {"local":[32,1,1],"shared":0}
6af568977b2a06f9 {"local":[32,1,1],"shared":0}
6b8215ab56fd3050 {"local":[1,1,1],"shared":0}
6c4c0b9d5f9dfe96 {"local":[32,1,1],"shared":896}
70fb1c9bee41c752 {"local":[32,1,1],"shared":0}
71817f072a390463 {"local":[16,16,1],"shared":0}
727341b9b66dc3f4 {"local":[32,1,1],"shared":0}
73e65f7ef16d1e60 {"local":[32,1,1],"shared":0}
7494aa52a5cf0b5b {"local":[32,1,1],"shared":0}
74d0431a4191db30 {"local":[32,1,1],"shared":0}
74d8f1a80c6ce1a8 {"local":[32,1,1],"shared":640}
769e2c69318915e2 {"local":[32,1,1],"shared":640}
7771bf658a7b0b3f {"local":[32,1,1],"shared":0}
77aee28cfb05820a {"local":[32,1,1],"shared":0}
792cba0e8d509e11 {"local":[32,1,1],"shared":896}
79ea6c97166f9153 {"local":[32,1,1],"shared":0}
7aa3e2269be711e2 {"local":[32,1,1],"shared":1408}
7d8d52d30c4c21d1 {"local":[64,1,1],"shared":0}
7e137c126b35909a {"local":[32,1,1],"shared":0}
7f99b98d5f70b731 {"local":[32,1,1],"shared":0}
7fd76b579b9c7622 {"local":[32,1,1],"shared":0}
80ff876ee44991ba {"local":[32,1,1],"shared":896}
82487a00492e064c {"local":[32,1,1],"shared":0}
85501247a0c60d10 {"local":[32,1,1],"shared":0}
877aade9c11db906 {"local":[16,16,1],"shared":0}
88ec80ca0fb86ec0 {"local":[32,1,1],"shared":0}
89b928a298d2652a {"local":[32,1,1],"shared":0}
8ad8310268cdf825 {"local":[32,1,1],"shared":896}
8b24a0aeb629dd78 {"local":[32,1,1],"shared":0}
8b265ef54c9d7b22 {"local":[32,1,1],"shared":0}
8b45432c6beb4b5f {"local":[32,1,1],"shared":896}
8cfea3fae99b89fe {"local":[32,1,1],"shared":0}
8e5222cc0ece4a59 {"local":[32,1,1],"shared":0}
8ec5ae4747b1e0d5 {"local":[32,1,1],"shared":896}
8f2111c893dd7bb7 {"local":[32,1,1],"shared":0}
8f954e655dadd232 {"local":[32,1,1],"shared":0}
8fe6a13446e77131 {"local":[16,16,1],"shared":0}
9226904a3a6d65a5 {"local":[32,1,1],"shared":0}
92f8538cca5de053 {"local":[32,1,1],"shared":1152}
93b6acbd248cc6cb {"local":[32,1,1],"shared":0}
954266041bc12bbc {"local":[32,1,1],"shared":0}
95edf3bf5af7100b {"local":[32,1,1],"shared":896}
97492cf3e147f5f5 {"local":[32,1,1],"shared":0}
9757bf657536bcf9 {"local":[32,1,1],"shared":0}
994cc348e2de772a {"local":[32,1,1],"shared":0}
998c096406d3f062 {"local":[32,1,1],"shared":896}
9a4c2f4276362474 {"local":[32,1,1],"shared":0}
9bf51390dfcb420d {"local":[32,1,1],"shared":0}
9d4e8a0867163485 {"local":[32,1,1],"shared":1152}
9e68d0b3b72814e8 {"local":[32,1,1],"shared":0}
9fc322999f7e6c75 {"local":[32,1,1],"shared":0}
a0c5a5ea0a678614 {"local":[64,1,1],"shared":0}
a0d2f4fac133133f {"local":[32,1,1],"shared":0}
a1c40510bb04b882 {"local":[32,1,1],"shared":0}
a1e7b4c840d4e235 {"local":[32,1,1],"shared":896}
a31e18a0cd3a27ad {"local":[32,1,1],"shared":0}
a6d2255c7ee613b8 {"local":[32,1,1],"shared":0}
a86b8ada6b88a4e9 {"local":[32,1,1],"shared":0}
a94bdb493ac331ff {"local":[32,1,1],"shared":0}
aa9c287f8fb2a150 {"local":[64,1,1],"shared":0}
ac493944e3a2b278 {"local":[32,1,1],"shared":0}
ac4c9b72cbb6c916 {"local":[64,1,1],"shared":0}
add593bcc0e7017c {"local":[64,1,1],"shared":0}
ae6345d168ef518c {"local":[32,1,1],"shared":384}
b0689ef5aac7d0db {"local":[32,1,1],"shared":0}
b08e92c3143f9a12 {"local":[32,1,1],"shared":0}
b18f5fc229489396 {"local":[32,1,1],"shared":0}
b1e7defea210535b {"local":[32,1,1],"shared":0}
b24a334f2d4539d8 {"local":[32,1,1],"shared":0}
b28f94a2cd79ccec {"local":[16,16,1],"shared":0}
b338d7e8d7b63bec {"local":[32,1,1],"shared":896}
b361bb3308a1de1a {"local":[32,1,1],"shared":384}
b4b80f03d4741448 {"local":[32,1,1],"shared":0}
b568c65c2cfbd651 {"local":[32,1,1],"shared":0}
b5b6c1db416e29a1 {"local":[32,1,1],"shared":0}
b5d3654ef41c60eb {"local":[32,1,1],"shared":0}
b623ffd983721868 {"local":[32,1,1],"shared":640}
b6aeee10c4af21a6 {"local":[32,1,1],"shared":640}
b7d387fdda6d64ef {"local":[32,1,1],"shared":0}
b84a3b43bb16e437 {"local":[32,1,1],"shared":0}
b88fb53a6722f87a {"local":[32,1,1],"shared":0}
b9078968005b6a2c {"local":[32,1,1],"shared":0}
b940aa86d8939c47 {"local":[16,16,1],"shared":800}
ba1cd1e48c5e0cff {"local":[32,1,1],"shared":0}
ba393a90bb3049e8 {"local":[64,1,1],"shared":0}
bc291d0e41a4ad0b {"local":[32,1,1],"shared":0}
bc58d5dec860c367 {"local":[32,1,1],"shared":0}
bc8c99a054fd277d {"local":[32,1,1],"shared":0}
bd4bf756dcb82c78 {"local":[32,1,1],"shared":0}
be1dfd32b44fcd6c {"local":[16,16,1],"shared":0}
be7b289c560f8c56 {"local":[32,1,1],"shared":0}
bf9893f70ab4b0de {"local":[32,1,1],"shared":896}
c199422cb8abb29d {"local":[16,16,1],"shared":128}
c223a938382abd73 {"local":[32,1,1],"shared":0}
c40f74e59f902a5e {"local":[32,1,1],"shared":0}
c4d0ceb521cae81c {"local":[1,1,1],"shared":0}
c5e65d5570fee294 {"local":[32,1,1],"shared":896}
c7830fac6bbcb5b7 {"local":[8,8,1],"shared":0}
c7f3c9cdfab3bae5 {"local":[16,16,1],"shared":0}
c82b9f17651879fb {"local":[32,1,1],"shared":896}
c95a9af347592c7a {"local":[32,1,1],"shared":0}
cab0974145bf2280 {"local":[32,1,1],"shared":896}
cb3303f3218d6411 {"local":[32,1,1],"shared":0}
cc22a15555dba058 {"local":[16,16,1],"shared":2320}
cc2906e5d03e6795 {"local":[64,1,1],"shared":0}
cc974529dc6b66e4 {"local":[32,1,1],"shared":640}
cce690f7813e12c2 {"local":[32,1,1],"shared":0}
ccf0a230f0c11fb5 {"local":[32,1,1],"shared":640}
ce4400a7c3042dac {"local":[64,1,1],"shared":0}
ce801399b5dce911 {"local":[32,1,1],"shared":0}
d0dac711cbbca395 {"local":[64,1,1],"shared":0}
d2f4bcba3b4225dd {"local":[32,1,1],"shared":0}
d4ba338399085a96 {"local":[32,1,1],"shared":640}
d59ce6746a6e0836 {"local":[16,16,1],"shared":0}
d7242030f1969f2b {"local":[8,8,1],"shared":0}
d7ac080f46b69ebb {"local":[32,1,1],"shared":0}
d7d2b692f1100b1f {"local":[32,1,1],"shared":640}
d8f3b2a9bfdc0f93 {"local":[1,1,1],"shared":0}
d946082d551ed0a7 {"local":[32,1,1],"shared":896}
da9f92feb4fefcd4 {"local":[32,1,1],"shared":0}
db0984679ece41d3 {"local":[1,1,1],"shared":0}
db4700bbf64b308b {"local":[32,1,1],"shared":640}
dce62f1e86537e2a {"local":[32,1,1],"shared":0}
ddd612c6bf4f1eb6 {"local":[32,1,1],"shared":0}
de4ba9f12c93daf8 {"local":[32,1,1],"shared":896}
de4e7a4915b5d3c7 {"local":[32,1,1],"shared":640}
deb3122040b29516 {"local":[32,1,1],"shared":0}
dfe7662fd03a0e42 {"local":[16,16,1],"shared":800}
e27a51a0d74010a9 {"local":[64,1,1],"shared":0}
e2d4755cd4a6025c {"local":[32,1,1],"shared":0}
e3395bc35410e5e5 {"local":[32,1,1],"shared":0}
e421d3fcf09e357d {"local":[32,1,1],"shared":0}
e44948b02df80ada {"local":[32,1,1],"shared":0}
e730a8a9b3bd3d4e {"local":[32,1,1],"shared":640}
e796e09666d69ca7 {"local":[32,1,1],"shared":0}
e88cbec52e511e65 {"local":[32,1,1],"shared":0}
ed0164cef84d6b22 {"local":[32,1,1],"shared":0}
ef74d1db55e6ab1f {"local":[32,1,1],"shared":0}
ef85d3f43c2df9ca {"local":[32,1,1],"shared":0}
f0bfc813b82e905c {"local":[32,1,1],"shared":0}
f2676f5dd083c073 {"local":[32,1,1],"shared":0}
f2c3d7e3e26d273e {"local":[1,1,1],"shared":0}
f3a37d2b7aba6108 {"local":[32,1,1],"shared":1152}
f405e86dfc2e6ccc {"local":[64,1,1],"shared":0}
f42c4a062df73346 {"local":[32,1,1],"shared":0}
f518e8fe310675d1 {"local":[32,1,1],"shared":0}
f53ce57253941ed3 {"local":[32,1,1],"shared":0}
f5483dd06e51ccda {"local":[32,1,1],"shared":0}
f6330acb24a19a78 {"local":[32,1,1],"shared":0}
f6c7a36c55e68d89 {"local":[16,16,1],"shared":0}
f913fcdcf69d92db {"local":[32,1,1],"shared":0}
f9b3bf9142ac14bf {"local":[32,1,1],"shared":0}
f9d2cca448261501 {"local":[32,1,1],"shared":0}
fa85494825cdc7ae {"local":[32,1,1],"shared":0}
faba9066cc7efdbf {"local":[32,1,1],"shared":0}
fb28c2d3a900219a {"local":[32,1,1],"shared":0}
fba18fbec53f3c0f {"local":[64,1,1],"shared":0}
fc7ac155afd3ecd4 {"local":[32,1,1],"shared":0}
fd06fe9fe642921a {"local":[32,1,1],"shared":0}
fdb2b336b0e2bca9 {"local":[16,16,1],"shared":0}
fdff23daac8f4ce0 {"local":[32,1,1],"shared":0}
fe3d8ebc5ee0137f {"local":[32,1,1],"shared":0}
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
+1
View File
@@ -45,6 +45,7 @@ None of them has a default game: without one they stop and list the profiles. To
| `pad_layout` | The iPhone virtual gamepad's layout on first launch: `simple` (the default) = 2D (left stick, A/B/X/Y, shoulders, − / +), `full` = 3D (adds a D-pad, a right stick and L3 / R3). The layout button at the top centre switches between them in game and the app remembers the player's choice (`DmGamepad.m`) |
| `feedback_snap` | Fragment shader keys (prefixes) whose draws sample a colour attachment of their own render pass and must read a snapshot instead (`fb_prepare` in `nvn_mtl.m`). Writing and sampling one attachment in a render pass is undefined in Metal: the Mac happens to read the old content, the iPhone sometimes reads whole 16×16 blocks as magenta. Each such draw costs a pass split and a copy, so only the shaders seen to break are listed (Super Mario 3D World + Bowser's Fury: its final composite). `DM_NVN_FB_SNAP=0` / `all` / `<keys>` overrides, also from `Documents/env.txt` |
| `alpha_from_red` | Fragment shader keys (prefixes, up to 48) that read a single-channel render target whose channel mapping is exactly (R, 0, 0, 1) as (R, 0, 0, R) (`tex_alpha_red` in `nvn_mtl.m`). Breath of the Wild's layout captures (panes tagged `A8`) keep a blurred alpha mask in the red channel of such an R8 texture, and every layout pane that samples one treats it as alpha; read literally, HUD prompts get hard-edged shadow boxes and cloud-filled glow rectangles. BotW lists all 26 pixel-shader variants of its layout library. `DM_NVN_ALPHA_RED=0` / `<keys>` overrides, also from `Documents/env.txt` |
| `tex_flush_upload` | `on` / `off` (the default): a texture the game fills from the CPU in place (no packaged layout, no `WriteTexels`) and publishes with `nvnMemoryPoolFlushMappedRange` is uploaded from guest memory like a packaged one, and re-uploaded when its pages change (`ft_add` in `hle_nvn.c`). Tears of the Kingdom streams all its textures this way; without it the 3D scene is black. Off by default because a flushed range in another game could cover an image a shader writes, whose GPU content the upload would replace |
| `nvn_tex_base`, `nvn_tex_slots`, `nvn_samp_base`, `nvn_img_base`, `nvn_img_slots`, `nvn_ssbo_bases` | NVN driver constant segments, measured over all shaders |
| `shader_groups`, `shader_note` | `shader_split.py` source groups and the map header note (Engage: `engine` for `data.unity3d` and `Resources/`, `assets` for the Addressables bundles) |
| `shader_main_glslc` | `on` / `off` (the default): `shader_split.py` also reads GLSLC compiler outputs (magic `0x19866891`) in `main`. 13 Sentinels keeps all its shaders there; Engage's `main` has 11 that it does not use, so the key stays off for other games |
+3
View File
@@ -99,6 +99,8 @@ def header(g):
if any(not re.fullmatch(r'[0-9a-f]{8,16}', k) for k in fbs): sys.exit(f'{g.id}: feedback_snap 要写片元着色器键(8–16 位小写十六进制)| feedback_snap takes fragment shader keys (8-16 lowercase hex digits): {fbs}')
afr = g.words('alpha_from_red', '') # fragment shader key prefixes that read single-channel textures mapped (R, 0, 0, 1) with alpha = red (nvn_mtl.m tex_alpha_red); none by default
if any(not re.fullmatch(r'[0-9a-f]{8,16}', k) for k in afr): sys.exit(f'{g.id}: alpha_from_red 要写片元着色器键(8–16 位小写十六进制)| alpha_from_red takes fragment shader keys (8-16 lowercase hex digits): {afr}')
tfu = g.get('tex_flush_upload', 'off') # textures written by the CPU into a pool and published with nvnMemoryPoolFlushMappedRange are uploaded like packaged ones (hle_nvn.c): on / off
if tfu not in ('on', 'off'): sys.exit(f'{g.id}: tex_flush_upload 只能是 on / off | tex_flush_upload must be on or off: {tfu}')
api = g.get('device_api', '55.15') # the NVN API major.minor version nvnDeviceGetInteger reports (hle_nvn.c)
if not re.fullmatch(r'\d+\.\d+', api): sys.exit(f'{g.id}: device_api 要写成 主.次 | device_api must be major.minor: {api}')
src = f'dm/games/{g.id}' if g.dir.parent == HERE else str(g.dir)
@@ -127,6 +129,7 @@ def header(g):
f'#define DM_GAME_PAD_FULL {int(pl == "full")}',
f'#define DM_GAME_FEEDBACK_SNAP {q(" ".join(fbs))}',
f'#define DM_GAME_ALPHA_FROM_RED {q(" ".join(afr))}',
f'#define DM_GAME_TEX_FLUSH_UPLOAD {int(tfu == "on")}',
f'#define DM_GAME_NVN_API_MAJOR {api.split(".")[0]}',
f'#define DM_GAME_NVN_API_MINOR {api.split(".")[1]}',
'']
+32 -2
View File
@@ -175,7 +175,8 @@ NVN(nvnMemoryPoolInitialize, NVNboolean, NVNmemoryPool *p, const NVNmemoryPoolBu
}
NVN(nvnMemoryPoolFinalize, void, NVNmemoryPool *p) { if (((pool_t *)p)->mem && !(((pool_t *)p)->flags & 0x200)) { // A virtual pool's storage is borrowed from a physical pool; do not unregister it
nvn_dirty_remove_range(((pool_t *)p)->mem); mtl_pool_del(((pool_t *)p)->mem); } mtl_alias_drop_pool(p); ((pool_t *)p)->magic = 0; }
NVN(nvnMemoryPoolFlushMappedRange, void, const NVNmemoryPool *p, ptrdiff_t off, size_t n) { (void)p; (void)off; (void)n; }
static void ft_flush(const uint8_t *a, uint64_t n);
NVN(nvnMemoryPoolFlushMappedRange, void, const NVNmemoryPool *p, ptrdiff_t off, size_t n) { const pool_t *o = (const pool_t *)p; if (o->mem) ft_flush(o->mem + off, n); } // see ft_add
NVN(nvnMemoryPoolGetBufferAddress, NVNbufferAddress, const NVNmemoryPool *p) { return (NVNbufferAddress)((const pool_t *)p)->mem; }
// ---------- Descriptor pools ----------
@@ -231,6 +232,34 @@ NVN(nvnTextureBuilderGetStorageAlignment, size_t, const NVNtextureBuilder *b) {
}
#define TX(t) ((tex_t *)(t))
#define CTX(t) ((const tex_t *)(t))
// Textures the CPU fills in place (profile tex_flush_upload): Tears of the Kingdom streams its textures (TexToGo, already in the GPU's block-linear layout) straight into
// CPU-cached memory pools and publishes each with nvnMemoryPoolFlushMappedRange (9,436 calls at boot), without a packaged layout or WriteTexels. They stayed GPU-only
// textures with content 0, and the G-buffer's base colour came out black. Textures that can hold such data (not render targets, not linear, no packaged layout, in a pool
// the CPU can write) are registered by address range; a flush that covers part of one marks it CPU-written (mtl_tex_cpu_written: uploaded from guest memory, re-uploaded
// when its pages change). Off by default: in another game a flushed range could cover an image a shader writes, whose GPU content the upload would replace
typedef struct { const uint8_t *mem; uint64_t size; tex_t t; } flush_tex;
static flush_tex *g_ft; static size_t g_nft, g_cft; static pthread_mutex_t g_ft_mu = PTHREAD_MUTEX_INITIALIZER;
static void ft_add(const tex_t *t) {
if (!DM_GAME_TEX_FLUSH_UPLOAD || !t->mem || !t->pool || t->has_layout || (t->pool->flags & NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS) ||
(t->d.flags & (NVN_TEXTURE_FLAGS_LINEAR | NVN_TEXTURE_FLAGS_LINEAR_RENDER_TARGET | NVN_TEXTURE_FLAGS_COMPRESSIBLE))) return;
pthread_mutex_lock(&g_ft_mu);
if (g_nft == g_cft && !(g_ft = realloc(g_ft, (g_cft = g_cft ? 2 * g_cft : 1024) * sizeof *g_ft))) die("flush_tex 分配失败");
g_ft[g_nft++] = (flush_tex){t->mem, t->L.size, *t};
pthread_mutex_unlock(&g_ft_mu);
}
static void ft_del(uint64_t serial) {
if (!DM_GAME_TEX_FLUSH_UPLOAD || !serial) return;
pthread_mutex_lock(&g_ft_mu);
for (size_t i = 0; i < g_nft; i++) if (g_ft[i].t.serial == serial) { g_ft[i] = g_ft[--g_nft]; break; }
pthread_mutex_unlock(&g_ft_mu);
}
static void ft_flush(const uint8_t *a, uint64_t n) {
if (!DM_GAME_TEX_FLUSH_UPLOAD || !a || !n) return;
pthread_mutex_lock(&g_ft_mu);
for (size_t i = 0; i < g_nft;)
if (g_ft[i].mem < a + n && a < g_ft[i].mem + g_ft[i].size) { mtl_tex_cpu_written(&g_ft[i].t); g_ft[i] = g_ft[--g_nft]; } else i++;
pthread_mutex_unlock(&g_ft_mu);
}
NVN(nvnTextureInitialize, NVNboolean, NVNtexture *t, const NVNtextureBuilder *b) {
const tex_builder_t *s = (const tex_builder_t *)b; tex_t *o = TX(t);
// Re-initializing a texture object that is still live (without a Finalize first): release the host texture of the old serial first. SMBW re-initializes its transient render targets on the same object every frame and never Finalizes them;
@@ -250,9 +279,10 @@ NVN(nvnTextureInitialize, NVNboolean, NVNtexture *t, const NVNtextureBuilder *b)
}
OLOG("Texture %p %ux%ux%u lv%u fmt=%#x tgt=%d flags=%#x size=%#llx", (void *)t, o->d.width, o->d.height, o->d.depth,
o->d.levels, o->d.format, o->d.target, o->d.flags, o->L.size);
ft_add(o);
return 1;
}
NVN(nvnTextureFinalize, void, NVNtexture *t) { mtl_tex_release(TX(t)); TX(t)->magic = 0; }
NVN(nvnTextureFinalize, void, NVNtexture *t) { ft_del(TX(t)->serial); mtl_tex_release(TX(t)); TX(t)->magic = 0; }
NVN(nvnTextureGetFlags, NVNtextureFlags, const NVNtexture *t) { return (NVNtextureFlags)CTX(t)->d.flags; }
NVN(nvnTextureGetWidth, int, const NVNtexture *t) { return (int)CTX(t)->d.width; }
NVN(nvnTextureGetHeight, int, const NVNtexture *t) { return (int)CTX(t)->d.height; }
+29 -6
View File
@@ -155,10 +155,32 @@ def sarc_members(d):
NV_STAGES = ['vs', 'ps', 'gs', 'hs', 'ds', 'cs'] # NVN stage order
COMMON, TREES = None, {} # Unity common strings and borrowed type trees (given when the pool starts)
ZDICTS, _ZD = {}, {} # zstd dictionaries by ID (given when the pool starts) and their digested form in this process
def _pool_init(common, trees):
global COMMON, TREES
COMMON, TREES = common, trees
def _pool_init(common, trees, zdicts={}):
global COMMON, TREES, ZDICTS
COMMON, TREES, ZDICTS = common, trees, zdicts
def zs_dicts(romfs):
"""Tears of the Kingdom compresses its RomFS files with zstd dictionaries (the frame header names one, ID 1 for the shader packages). They ship as Pack/ZsDic.pack.zs:
a SARC (itself plain zstd) of *.zsdic files, each a zstd dictionary (magic 0xEC30A437, then its ID). → {ID: dictionary}; {} without that file (Mario's .zs are plain zstd)"""
p = os.path.join(romfs, 'Pack/ZsDic.pack.zs')
if not os.path.exists(p): return {}
from compression import zstd # Python 3.14+
d = zstd.decompress(open(p, 'rb').read())
return {struct.unpack_from('<I', m, 4)[0]: m for _, m in sarc_members(d) if m[:4] == bytes.fromhex('37a430ec')}
def unzs(path):
"""A .zs file decompressed: frames without a dictionary through the zstd command as before, frames that name one with that dictionary from ZDICTS"""
d = open(path, 'rb').read()
fhd = d[4] if len(d) > 5 and d[:4] == bytes.fromhex('28b52ffd') else 0
n, o = (0, 1, 2, 4)[fhd & 3], 6 - (fhd >> 5 & 1) # dictionary ID size; it follows the window descriptor, which single-segment frames omit
did = int.from_bytes(d[o:o + n], 'little')
if not did: return subprocess.run(['zstd', '-d', '-c', '-q', path], capture_output=True, check=True).stdout
from compression import zstd
if did not in ZDICTS: red(f'{path}:帧要 zstd 字典 {did},Pack/ZsDic.pack.zs 里没有')
if did not in _ZD: _ZD[did] = zstd.ZstdDict(ZDICTS[did])
return zstd.decompress(d, zstd_dict=_ZD[did])
def unity_program(code, where, stage=None):
"""Unity's Switch program code → [(stage, control, code)]; stage = the subprogram's stage from m_ParsedForm, which only the pre-2019.3 layout needs"""
@@ -247,7 +269,7 @@ def var_pairs(found):
def scan_file(args):
"""→ (relative path, {key: …}, pairs, extra); extra = this file's Unity type trees, or 'defer' (no type tree, scanned again in the second pass)"""
path, rel = args
d = subprocess.run(['zstd', '-d', '-c', '-q', path], capture_output=True, check=True).stdout if path.endswith('.zs') else open(path, 'rb').read()
d = unzs(path) if path.endswith('.zs') else open(path, 'rb').read()
if d[:4] == b'Yaz0': d = yaz0(d)
found = []; trees = {}
if d[:8] == b'UnityFS\0' or (d[8:11] == b'\0\0\0' and __import__('unityfs').is_serialized(d)):
@@ -292,17 +314,18 @@ def main():
found = []; scan_blob(img, 'main' + ('@' + tag if tag else ''), found)
if GLSLC: found += glslc_shaders(img, 'main' + ('@' + tag if tag else ''))
results.append(('main', {skey(c, k): (w, s, c, k) for w, s, c, k in found}, var_pairs(found), {}))
zdicts = {k: v for r in romfs_v for k, v in zs_dicts(r).items()} # Tears of the Kingdom's zstd dictionaries (zs_dicts)
keys = {}
# First pass: every file; Unity bundles carry type trees, and the Shader trees are collected on the way. Second pass: Unity serialized files without type trees are scanned again with the borrowed trees
J = int(os.environ.get('J', '8'))
with cf.ProcessPoolExecutor(J, initializer=_pool_init, initargs=(common, {})) as ex: results += list(ex.map(scan_file, files, chunksize=16))
with cf.ProcessPoolExecutor(J, initializer=_pool_init, initargs=(common, {}, zdicts)) as ex: results += list(ex.map(scan_file, files, chunksize=16))
trees, defer = {}, []
for rel, uniq, pr, extra in results:
if extra == 'defer': defer.append(rel)
elif extra: trees.update(extra)
if defer:
byrel = {r: p for p, r in files}
with cf.ProcessPoolExecutor(J, initializer=_pool_init, initargs=(common, trees)) as ex: again = list(ex.map(scan_file, [(byrel[r], r) for r in defer]))
with cf.ProcessPoolExecutor(J, initializer=_pool_init, initargs=(common, trees, zdicts)) as ex: again = list(ex.map(scan_file, [(byrel[r], r) for r in defer]))
for rel, uniq, pr, extra in again:
if extra == 'defer': red(f'{rel}:Unity 序列化文件没有类型树,资源包里也没有同哈希的 Shader 类型树可借')
results = [r for r in results if r[3] != 'defer'] + again
+34
View File
@@ -11,3 +11,37 @@ HLE_FORCE("_ZN2nn5codec19HardwareOpusDecoderC1Ei", "hwopus 服务;换成 SDK
if (!soft_ctor && !(soft_ctor = guest_sym("_ZN2nn5codec11OpusDecoderC1Ev"))) die("缺 OpusDecoder 构造");
soft_ctor(self);
}
// The C interface to the same hardware decoder (Tears of the Kingdom, SDK 20.5.22: GetHardwareOpusDecoderWorkBufferSize opens hwopus and aborts in sm::Initialize).
// Each call goes to the sdk's software C interface instead. The software decoder state lives in a host block kept per game decoder object (objmap), so the size of the
// game's HardwareOpusDecoderType does not matter; the game's work buffer is sized by GetOpusDecoderWorkBufferSize and handed to the software decoder as is.
#define SOFT_OPUS_STATE 0x200 // bytes for the software OpusDecoderType (larger than the SDK's)
static void *opus_fn(const char *name) { void *f = guest_sym(name); if (!f) die("sdk 缺 %s", name); return f; }
static void *opus_soft(const void *hw) { void *s = objmap_get(hw); if (!s) die("Opus 解码器 %p 没经宿主初始化", hw); return s; }
HLE_FORCE("_ZN2nn5codec36GetHardwareOpusDecoderWorkBufferSizeEiii", "hwopus 服务;按软件解码器算工作区", size_t, h_codec_HwOpusWorkSize, int rate, int ch, int option) {
(void)option; size_t (*f)(int, int) = opus_fn("_ZN2nn5codec28GetOpusDecoderWorkBufferSizeEii"); return f(rate, ch);
}
HLE_FORCE("_ZN2nn5codec29InitializeHardwareOpusDecoderEPNS0_23HardwareOpusDecoderTypeEiiPvmi", "hwopus 服务;换成软件 OpusDecoder", int, h_codec_HwOpusInit,
void *hw, int rate, int ch, void *buf, size_t size, int option) {
(void)option; int (*f)(void *, int, int, void *, size_t) = opus_fn("_ZN2nn5codec21InitializeOpusDecoderEPNS0_15OpusDecoderTypeEiiPvm");
void *s = objmap_get(hw); if (!s) { s = calloc(1, SOFT_OPUS_STATE); objmap_put(hw, s); }
return f(s, rate, ch, buf, size);
}
HLE_FORCE("_ZN2nn5codec27FinalizeHardwareOpusDecoderEPNS0_23HardwareOpusDecoderTypeE", "hwopus 服务;软件 OpusDecoder", void, h_codec_HwOpusFini, void *hw) {
void (*f)(void *) = opus_fn("_ZN2nn5codec19FinalizeOpusDecoderEPNS0_15OpusDecoderTypeE");
void *s = objmap_del(hw); if (s) { f(s); free(s); }
}
HLE_FORCE("_ZN2nn5codec32GetHardwareOpusDecoderSampleRateEPKNS0_23HardwareOpusDecoderTypeE", "hwopus 服务;软件 OpusDecoder", int, h_codec_HwOpusRate, const void *hw) {
int (*f)(const void *) = opus_fn("_ZN2nn5codec24GetOpusDecoderSampleRateEPKNS0_15OpusDecoderTypeE"); return f(opus_soft(hw));
}
HLE_FORCE("_ZN2nn5codec34GetHardwareOpusDecoderChannelCountEPKNS0_23HardwareOpusDecoderTypeE", "hwopus 服务;软件 OpusDecoder", int, h_codec_HwOpusCh, const void *hw) {
int (*f)(const void *) = opus_fn("_ZN2nn5codec26GetOpusDecoderChannelCountEPKNS0_15OpusDecoderTypeE"); return f(opus_soft(hw));
}
HLE_FORCE("_ZN2nn5codec24ResetHardwareOpusDecoderEPNS0_23HardwareOpusDecoderTypeE", "hwopus 服务;软件 OpusDecoder", void, h_codec_HwOpusReset, void *hw) {
void (*f)(void *) = opus_fn("_ZN2nn5codec16ResetOpusDecoderEPNS0_15OpusDecoderTypeE"); f(opus_soft(hw));
}
HLE_FORCE("_ZN2nn5codec33DecodeOpusInterleavedWithHardwareEPNS0_23HardwareOpusDecoderTypeEPmPiPsmPKvm", "hwopus 服务;软件 OpusDecoder", int, h_codec_HwOpusDecode,
void *hw, size_t *consumed, int *samples, int16_t *out, size_t out_size, const void *in, size_t in_size) {
int (*f)(void *, size_t *, int *, int16_t *, size_t, const void *, size_t) = opus_fn("_ZN2nn5codec21DecodeOpusInterleavedEPNS0_15OpusDecoderTypeEPmPiPsmPKvm");
return f(opus_soft(hw), consumed, samples, out, out_size, in, in_size);
}
+23
View File
@@ -351,6 +351,22 @@ HLE_FORCE("_ZN2nn2fs9IsMountedEPKc", "fsp-srv;查宿主挂载表", int, h_fs_I
HLE_FORCE("_ZN2nn2fs21MountTemporaryStorageEPKc", "fsp-srv;临时区 → 宿主目录", Result, h_fs_MountTemp, const char *name) {
char b[1024]; return mount(name, envdir("DM_TEMP", "run/platform/temp", b, sizeof b), 0);
}
// Cache storage (Tears of the Kingdom; its first list call opened fsp-srv and aborted in sm::Initialize): storage <index> is the host folder <temporary storage>_cache<index>,
// created when it is created or mounted. The list reports none (a fresh console), so the game creates its storage at every boot; creating an existing one succeeds
static const char *cache_dir(int index, char *b, size_t n) { char t[1024]; snprintf(b, n, "%s_cache%d", envdir("DM_TEMP", "run/platform/temp", t, sizeof t), index); return b; }
HLE_FORCE("_ZN2nn2fs20OpenCacheStorageListEPNS0_22CacheStorageListHandleE", "fsp-srv;缓存存储列表:报没有", Result, h_fs_OpenCacheList, void *h) { (void)h; return RES_OK; }
HLE_FORCE("_ZN2nn2fs20ReadCacheStorageListEPiPNS0_16CacheStorageInfoENS0_22CacheStorageListHandleEi", "fsp-srv;缓存存储列表:报没有", Result, h_fs_ReadCacheList,
int *n, void *info, uint64_t h, int max) { (void)info; (void)h; (void)max; *n = 0; return RES_OK; }
HLE_FORCE("_ZN2nn2fs21CloseCacheStorageListENS0_22CacheStorageListHandleE", "fsp-srv;缓存存储列表", void, h_fs_CloseCacheList, uint64_t h) { (void)h; }
HLE_FORCE("_ZN2nn2fs18CreateCacheStorageEill", "fsp-srv;缓存存储 → 宿主目录", Result, h_fs_CreateCache, int index, int64_t size, int64_t journal) {
char b[1100]; LOG("fs 建缓存存储 %d(%lld 字节,日志 %lld)→ %s", index, (long long)size, (long long)journal, cache_dir(index, b, sizeof b)); mkdir(b, 0755); return RES_OK;
}
HLE_FORCE("_ZN2nn2fs17MountCacheStorageEPKci", "fsp-srv;缓存存储 → 宿主目录", Result, h_fs_MountCache, const char *name, int index) {
char b[1100]; cache_dir(index, b, sizeof b); mkdir(b, 0755); return mount(name, b, 0);
}
HLE_FORCE("_ZN2nn2fs18DeleteCacheStorageEi", "fsp-srv;缓存存储 → 删宿主目录", Result, h_fs_DeleteCache, int index) {
char b[1100]; rm_tree(cache_dir(index, b, sizeof b)); return RES_OK;
}
HLE_FORCE("_ZN2nn2fs14EnsureSaveDataERKNS_7account3UidE", "fsp-srv;存档目录在挂载时建", Result, h_fs_EnsureSave, const void *uid) { (void)uid; return RES_OK; }
// Settings defaults for the touch product (dropped item names shown permanently by default, can be turned off in settings): Settings/Settings.json in the save only records keys the player has changed, unrecorded ones use the game default.
// Before mounting the save, fill in the touch product defaults for the two keys "the player has not touched yet"; existing keys are never changed (if the player changed it in the options page, the player wins):
@@ -446,6 +462,13 @@ const char *dm_save_workdir(void) { for (int i = 0; i < g_nmnt; i++) if (g_mnt[i
// The committed directory of the transactional save (the copy on disk, the version from the last Commit). Returns NULL when no save is mounted
const char *dm_save_commitdir(void) { for (int i = 0; i < g_nmnt; i++) if (g_mnt[i].commit[0]) return g_mnt[i].commit; return NULL; }
void (*g_dm_mount_seed_hook)(const char *save_dir); // called before mounting the save (save dir): adds the files the host needs to the save
// Another application's save data (Tears of the Kingdom asks by application ID and would mount it read-only): the host keeps only this game's own save, which exists once mounted
HLE_FORCE("_ZN2nn2fs18IsSaveDataExistingENS_13ApplicationIdERKNS_7account3UidE", "fsp-srv;只有本作自己的存档", int, h_fs_SaveExisting, uint64_t app, const void *uid) {
(void)uid; LOG("fs 问存档是否存在:程序 %016llx → %s", (unsigned long long)app, app == DM_GAME_PROGRAM_ID ? "本作,有" : "别的程序,没有"); return app == DM_GAME_PROGRAM_ID;
}
HLE_FORCE("_ZN2nn2fs21MountSaveDataReadOnlyEPKcNS_13ApplicationIdERKNS_7account3UidE", "fsp-srv;别的程序的存档:没有", Result, h_fs_MountSaveRO, const char *name, uint64_t app, const void *uid) {
(void)uid; LOG("【红】fs 只读挂别的程序的存档 %s:程序 %016llx,宿主没有", name, (unsigned long long)app); return 0x7d402u; // fs ResultTargetNotFound
}
HLE_FORCE("_ZN2nn2fs13MountSaveDataEPKcRKNS_7account3UidE", "fsp-srv;存档 → 宿主目录(单用户)", Result, h_fs_MountSave, const char *name, const void *uid) {
(void)uid;
// No default directory: silently landing on a directory with no characters distorts the "character select" measurement (this once caused a misjudgment). Runs always use a fresh copy of save_ref
+3
View File
@@ -83,6 +83,9 @@ HLE_FORCE("_ZN2nn2oe15EnableRecordingEv", "录像开关;宿主无系统录像"
HLE_FORCE("_ZN2nn6applet23GetAppletResourceUserIdEv", "小程序资源用户号;宿主固定非 0", uint64_t, h_applet_Aruid, void) { return 0x80; }
// 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; }
// Tears of the Kingdom (SDK 20.5.22): the index of the program that launched this one in a multi-program application. The SDK asks the applet proxy, which the host never opens
// (null read in applet::GetPreviousProgramIndex right after the window setup); -1 = launched from the home menu
HLE_FORCE("_ZN2nn2oe23GetPreviousProgramIndexEv", "前一个程序的序号:从主页启动,-1", int, h_oe_PrevProgram, void) { return -1; }
HLE_FORCE("_ZN2nn2oe16DisableRecordingEv", "录像开关;宿主无系统录像", void, h_oe_DisableRec, void) {}
HLE_FORCE("_ZN2nn2oe16EnableScreenShotEv", "截图开关;宿主无系统截图", void, h_oe_EnableShot, void) {}
HLE_FORCE("_ZN2nn2oe17DisableScreenShotEv", "截图开关;宿主无系统截图", void, h_oe_DisableShot, void) {}
+2
View File
@@ -105,6 +105,8 @@ HLE_FORCE("_ZN2nn3err39ShowUnacceptableApplicationVersionErrorEv", "游戏版本
// ---------- web: web page applet (online lobby). Unavailable ----------
HLE_FORCE("_ZN2nn3web13ShowLobbyPageEPNS0_20LobbyPageReturnValueERKNS0_16ShowLobbyPageArgE", "联机大厅网页小程序(剪)", Result, h_web_Lobby, void *rv, const void *arg) { (void)rv; (void)arg; LOG("web::ShowLobbyPage(剪)"); return RES_FAIL; }
// Tears of the Kingdom imports the general web page applet too (the game constructs the return value itself, so it stays as constructed)
HLE_FORCE("_ZN2nn3web11ShowWebPageEPNS0_18WebPageReturnValueERKNS0_14ShowWebPageArgE", "网页小程序(剪)", Result, h_web_Page, void *rv, const void *arg) { (void)rv; (void)arg; LOG("web::ShowWebPage(剪)"); return RES_FAIL; }
// ---------- C interface (nnsocket* / nnssl*): offline as well. The arguments are all in registers and the host does not look at them ----------
HLE_FORCE("nnsocketAccept", "离线(剪)", int, h_c_sk_Accept, void) { return -1; }
+15
View File
@@ -78,6 +78,21 @@ HLE_FORCE("_ZN2nn4time14ToCalendarTimeEPNS0_12CalendarTimeEPNS0_22CalendarAdditi
ai->dst = tm.tm_isdst > 0; ai->offset = (int32_t)tm.tm_gmtoff; }
return RES_OK;
}
// Clock snapshot (Tears of the Kingdom: CreateWithStandardSystemClock asks the time service, which the host never opens → abort in sm::Initialize). 0xD0 bytes, laid out
// as Ryujinx lays out the service's ClockSnapshot and as this SDK's constructor clears it (its last store is the byte at +0xcd): +0x00 user clock context {offset,
// steady time point}, +0x20 network clock context, +0x40 user time, +0x48 network time, +0x50 / +0x58 user / network calendar, +0x60 / +0x78 their additional info,
// +0x90 steady time point, +0xa8 location name (0x24), +0xcc automatic correction, +0xcd type. Offline, the network fields stay 0, so the SDK's CalculateSpanBetween
// uses the steady time points (same source ID)
HLE_FORCE("_ZN2nn4time13ClockSnapshot29CreateWithStandardSystemClockEPS1_", "连 time 服务取快照;宿主现填(离线:网络时钟为 0)", Result, h_time_Snapshot, uint8_t *s) {
memset(s, 0, 0xd0);
uint8_t tp[0x18]; h_time_SteadyNow(tp);
int64_t now, off; h_time_GetCurrentTime(&now); off = now - *(const int64_t *)tp;
memcpy(s, &off, 8); memcpy(s + 0x08, tp, 0x18); memcpy(s + 0x40, &now, 8);
h_time_ToCalendarTime((CalendarTime *)(s + 0x50), (CalendarAdditionalInfo *)(s + 0x60), &now);
memcpy(s + 0x90, tp, 0x18);
tzset(); strlcpy((char *)s + 0xa8, tzname[0] && *tzname[0] ? tzname[0] : "UTC", 0x24);
return RES_OK;
}
// The reverse: local calendar → absolute seconds (13 Sentinels). Around a daylight-saving switch one local time maps to 0 or 2 absolute seconds; the host's mktime gives one
HLE_FORCE("_ZN2nn4time11ToPosixTimeEPiPNS0_9PosixTimeEiRKNS0_12CalendarTimeE",
"连 time 服务的时区规则;改用宿主 mktime", Result, h_time_ToPosixTime, int *count, int64_t *out, int n, const CalendarTime *ct) {
+4
View File
@@ -34,6 +34,9 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
- **`jump table <addr>` assertion in `patch_nso.py`** (Unity / IL2CPP `main`) → the compiler used `x18` for other jump-table shapes: an offset table (`ldrsw x18, [xB, x18, lsl #2]; add x18, x18, xB; br x18`), an indirect call (`blr x18`) and an `alloca` (`mov sp, x18`). The rewriter reads the offset table's targets at rewrite time (`site_br_tab`), calls through `x16` with the original return address so C++ unwinding still finds its frames (`site_blr`), and restores its scratch registers relative to the old `sp` (`site_sp`). Engage `main`: 7 jump tables, 1 call, 1 `sp` write; Mario's output stays byte-identical.
- **`jump table <addr>` assertion in a `subsdk`** (Link's Awakening's GLSLC and movie modules) → the offset-table form `ldrsw x18, [xB, xI, lsl #2]; add x18, x18, xB; br x18` with the bounds check on another register: the index in `w0` (`sub w0, w18, #0xd1; cmp w0, #0x31`), a copy (`cmp w21, #7; …; mov w18, w21`) or a masked copy (`and w16, w18, #0x3f; cmp w16, #0xe; …; and x18, x18, #0x3f`). `jump_offsets` follows `mov` copies back to the `cmp` and accepts a check on the same `and` of the same, unchanged source. Engage's output stayed byte-identical. The entry may also be loaded into another register, with only the `add` writing `x18` (Cuphead, Unity 2017.4: `cmp w17, #7; ldrsw x17, [x15, x17, lsl #2]; add x18, x17, x15`).
- **Job system hangs; a queue claims thousands of tasks but every slot is empty** → in an `ldxr … cmp … b.ne out … stxr … cbnz` window, the rewritten stub emitted its exits in the wrong order, so successful stores took the failure path → emit the fall-through exit first (`patch_nso.py`). D2R had no such windows; Mario had 82.
- **`branch into window <addr>` assertion** (The Legend of Zelda: Tears of the Kingdom 1.4.3 `main`, 8 sites) → the compiler moved an atomic update's retry path out of line: the failed `stxr` branches to a block `ldxr; cmp; b.eq <the stxr>` that re-enters the window at its store. That block cannot jump into the window's patch copy (the `x18` stand-in would be loaded between its exclusive load and the store), so it gets a copy of its own: the retry block and the window's tail from the store on, under one prologue (`retry_block` / `site_retry_window`). A window also never spans a second exclusive load: before this rule, a retry block's `ldxr` opened a window that ran on into the next, unrelated `ldxr … stxr` loop (`main+0x5a4674`), and branches to that loop's head landed mid-window.
- **`jump table <addr>` assertion, entries relative to an `adr`** (Tears of the Kingdom `main+0x393bb0` and others) → `cmp wI, #N; b.hi; …; adr x18, #L; ldrsw xE, [xT, xI, lsl #2]; add x18, x18, xE; br x18`: target = L + entry, not table + entry (`jump_adr_offsets`, tried only where the table-relative form is not found). The bounds check can be on a register copied into the index (`mov w16, w15`), and the table base can be built far from the dispatch; then every `adrp` + `add` pair in the function that builds that register is tried, and a table is accepted only if exactly one makes every target an instruction of the function.
- **Checking a rewriter change**: rewrite every extracted module of every title with the old and the new `patch_nso.py` and compare all outputs; only `build_src.sha256` (the rewriter's own hash) may differ for titles the change is not meant for.
- **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.
@@ -102,6 +105,7 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
- **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`.
- **More SDK 20 calls that reach a service** (Tears of the Kingdom 1.4.3, the same `sdk` build as Breath of the Wild 1.9.0; each aborted in `sm::Initialize` or read a null applet proxy, one per boot): `oe::GetPreviousProgramIndex` (−1: launched from the home menu); the hardware Opus decoder's C interface (`GetHardwareOpusDecoderWorkBufferSize`, `InitializeHardwareOpusDecoder`, `DecodeOpusInterleavedWithHardware`, …), forwarded to the `sdk`'s software `OpusDecoder` C interface with a host-held decoder state per game object; cache storage (`OpenCacheStorageList` / `ReadCacheStorageList` report none, `CreateCacheStorage` / `MountCacheStorage` use a host folder beside the temporary storage); `fs::IsSaveDataExisting(ApplicationId, Uid)` for another title (it asks for Breath of the Wild's save: none) and `MountSaveDataReadOnly`; `time::ClockSnapshot::CreateWithStandardSystemClock` (0xD0 bytes, filled on the host from the steady clock, the user clock and the calendar; the network fields stay 0). The import list itself says where to look: diff the undefined symbols against the closest title the host already runs, drop what the host hooks, and the rest are the candidates. To find which `sdk` function aborted, map the backtrace offsets to the nearest exported symbol.
- **SDK 8 (Link's Awakening, 8.2.0)**:
- **Abort in `os::WaitSystemEvent` under `oe::PopNotificationMessage`** → the blocking pop waits on the system event `oe::Initialize` would have created, and the host's `Initialize` is a no-op. The host now implements it on its notification queue (a condition variable, then the `TryPop` path).
- **SIGSEGV in `vi::SetLayerCrop`** → the SDK's version dereferences vi service objects the host never made; `SetLayerCrop`, `DestroyLayer`, `CloseDisplay` and `vi::Finalize` are host no-ops.