Remove the D2R code and the legacy frame step
D2R (Diablo II Resurrected) has no profile and its code could not run here: the host-call svcs its cuts emitted only died, the touch-run-v4 rule set needed a missing dm/PRODUCT, and the host files its hooks called were never in this repository. Removed with it: - rewriter: D2R cuts, observation gates and parameter slots, the touch-run-v4 rule set and private gates, the boot list, the cut scope column and the KRES / KASPECT key slots (fixups.tsv keeps the four thread slots; no params.tsv any more); dm/tools/product.sh - host: D2R save edits, add-on content, render-scale controller, time() takeover and Terror Zone clock, crash decoding at D2R offsets, host-call registry and stubs, i18n, UI-screen, shell-app loader, zero-stripped archives, boot profiling hooks with no implementation; D2R headers, tests and scripts; dm/ios/Sources/dm_touch_controls; dm/third_party/libdeflate - graphics: runtime extrapolation, world freeze, iPad UI shift, shop drag image, brightness LUT, visual ruler, bloom / FXAA shader hashes, DM_GFXUSE, the eco-fps setting, the loading-screen gate; shader_map.py, nometal_run.py, nvn_slots.py, nvn_slots.tsv and the PC-texture samples The legacy frame step (frame_step legacy, DM_FRAME_STEP) is retired: the Breath of the Wild and Tears of the Kingdom stubs lose their mode test. Breath of the Wild's interval-controller cuts stay: the framework keeps the controller's interval while the game's VFR override holds. patch_nso.py output is byte-identical for all 15 games on this machine except the two frame-step stubs; all 18 profiles build; the host tests pass (their Makefile now builds the general tests only).
This commit is contained in:
1 parent
dcd6a52c00
commit
79d82f7b64
158 files changed
+237
-21167
No files matched your search
@@ -1,8 +1,8 @@
|
||||
# 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. Nineteen games are playable on the iPhone: Super Mario Bros. Wonder (update 1.2.1, profile `010015100B514000`), Fire Emblem Engage (update 2.0.0, `0100A6301214E000`), Metroid Dread (update 2.1.0, `010093801237C000`), 13 Sentinels: Aegis Rim (update 1.10, `01003FC01670C000`), Super Mario 3D World + Bowser's Fury (update 1.2.2, `010028600EBDA000`), The Legend of Zelda: Breath of the Wild (update 1.9.0, SDK 20, `01007EF00011E000`), The Legend of Zelda: Link's Awakening (update 1.1.0, SDK 8, `01006BB00C6F0000`) Super Mario Odyssey (update 1.4.1, SDK 7, `0100000000010000`), Hades (update 1.0.38233, SDK 9, `0100535012974000`), Hyrule Warriors: Age of Calamity (update 1.3.0, SDK 9.6, `01002B00111A2000`), Fire Emblem: Three Houses (update 1.2.0, SDK 6.5, `010055D009F78000`), Super Mario Galaxy (update 1.3.1, SDK 18, `010099C022B96000`) The Legend of Zelda: Tears of the Kingdom (update 1.4.3, SDK 20, `0100F2C0115B6000`) Monster Hunter Rise (update 16.0.2, SDK 11.4.3, `0100B04011742000`), Ace Attorney Investigations Collection (update 1.0.0.2, SDK 17.5.3, `010005501E68C000`), Hades II (base game, SDK 19.3.9, `0100A00019DE0000`), Unicorn Overlord (update 1.05, SDK 16.2.4, `010069401ADB8000`), Rhythm Heaven Groove (base game 1.0.0, SDK 21.4.0, `0100D9F01D474000`) and Octopath Traveler 0 (update 1.0.5, SDK 19.3.13, `01005270232F2000`). Hades renders with Vulkan, which the host implements on Metal (`dm/gfx/vk/hle_vk.m`, profile key `graphics vulkan`). Age of Calamity is Koei Tecmo's KTGL engine: files in RDB archives, shaders as GLSLC outputs (`shader_split.py` `idrk_shaders`). Three Houses is KTGL too; 9 of its shaders exist only as compiles of the game's own GLSLC at run time, captured once on the Mac (profile key `shader_glslc_capture`). Breath of the Wild and Tears of the Kingdom run at 60 fps through a frame step the host decides in whole vsyncs (`patch_nso.py` `FRAME_STEP`, `nvn_fstep.h`, profile keys `fps_target` and `frame_step`); Tears of the Kingdom has a 5 GB guest layout. Monster Hunter Rise is Capcom's RE Engine: files in KPKA PAK archives (`shader_split.py` `kpka_jobs`, shaders in SDF files), the base game an XCI, its GPU heap a 64 GB sparse virtual pool (`hle_nvn.c` `vpool_*`, a 4 KB page table the replay reads textures through); it runs at 60 fps (30 on the Switch). Super Mario Galaxy is the Wii game on NERD's Hagi layer (PowerPC code recompiled into `main`), renders with Vulkan, and generates its shaders at run time, which the NSP script captures on the Mac (profile key `shader_vk_capture`); its pointer is the touch screen in handheld mode (profile key `op_mode`). Ace Attorney Investigations Collection is Unity 2022 IL2CPP: its shaders keep their parameters as blob entries of their own (`shader_split.py` `unity_code_blobs`), and its C# file API goes through the SDK's C library, whose path checks the host answers (`hle_fs.c` `IsPathMounted` and the length limits). Hades II, unlike Hades, renders with NVN; its shaders bind textures at fixed slots up to 34 (profile key `nvn_tex_slots`), and the two long lines through Melinoë while the staff's Omega attack charges are the game's own aim lines, not a rendering bug. Unicorn Overlord is Vanillaware's engine like 13 Sentinels (shaders compiled into `main`); both need the profile key `alloc_big_align`, which aligns large allocations to 64 KB so the ring heaps the engine cuts from a memory pool fit. Rhythm Heaven Groove is Nintendo's own 2D engine on SDK 21, the newest SDK so far (its DSP command list has no magic, `hle_audren.c`); it plays handheld with Boost (profile keys `op_mode handheld`, `perf_mode boost`), which skips its TV latency calibration and keeps its 1920x1080 draw size. Octopath Traveler 0 is Unreal Engine 5: everything in one pak with an AES-encrypted index whose key is found in `main` (`dm/gfx/tools/uepak.py`), its shaders Oodle-compressed in the pak's shader code archives and decoded once on the Mac by the game's own Oodle (profile key `shader_ue_decode`, `DM_OODLE_JOB`), its allocator replaced by the host's because Unreal's reserves 37 GB of address space (`patch_nso.py` cut, `hle_uemalloc.c`), its address region capped by the game's own argument (profile key `game_args`), and the particle shaders that pick a texture slot at run time bound by slot (profile key `tex_dyn_slots`). [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand, prewarm lists and per-game notes.
|
||||
- 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.
|
||||
- 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)). Prefer a rule that holds for every game (Horizon / NVN semantics, or what the binary, its SDK version, its imports or its files show) to a per-game key, cut or build-ID gate, and use those only when no general rule can do it.
|
||||
- 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()`). 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 a provisional profile guessed from the game files (`dm/games/profile_guess.py`; `<NSP folder>/<title ID>_build/profile/<title ID>/`, found by the tools through `DM_GAMES_DIR`), with a short heap check on the Mac when the game has virtual address memory, and a best-effort IPA that may not boot. Then follow [docs/agents](docs/agents/README.md) (extraction, binary survey, host bring-up failure catalogue, graphics specifics, debugging recipes) and add a profile.
|
||||
- Scripts the user runs live in `scripts/` at the repository root (now `build_ipa_from_nsp.py`). Each one runs both in Spyder (F5, configured by the settings block at its top) and from a terminal (command-line options override those settings; non-zero exit on error). Tools that only agents or other scripts call stay under `dm/`.
|
||||
- Game files, keys and anything extracted from them live in the game's NSP folder, never in this repository and never in the home folder (it is nearly full): the work folder `<NSP folder>/<id>-work` for extraction, runs and analysis, beside the NSP script's `<id>_build/`. Tool venvs and other large scratch data go on that drive too. Do not print key values.
|
||||
|
||||
@@ -15,7 +15,7 @@ This project is built on other people's work:
|
||||
- **[LibHac](https://github.com/Thealexbarney/LibHac)**: reads NSPs and layers updates over the base game in `dm/tools/nx_extract`. **hactool** did the first extractions.
|
||||
- **[SPIRV-Cross](https://github.com/KhronosGroup/SPIRV-Cross)**: SPIR-V to Metal Shading Language.
|
||||
- **deko3d** (zlib): its texture layout size algorithm is ported in `dm/gfx/nvn/nvn_tex.c`.
|
||||
- **libvpx** (VP9 movies), **stb_vorbis** (movie sound), **libdeflate**, the **Vulkan headers** and **NVIDIA FLIP** ship in `dm/third_party` with their licenses ([list](dm/third_party/README.md)).
|
||||
- **libvpx** (VP9 movies), **stb_vorbis** (movie sound), the **Vulkan headers** and **NVIDIA FLIP** ship in `dm/third_party` with their licenses ([list](dm/third_party/README.md)).
|
||||
- **Capstone**, **lz4**, **fontTools** and **XcodeGen** are used by the build.
|
||||
|
||||
## Games
|
||||
|
||||
@@ -19,8 +19,8 @@ pad_layout full
|
||||
# The frame-interval controller (60 fps cuts in patch_nso.py) and dynamic resolution read the GPU time between the frame's two timestamp reports: each report takes its segment's GPU start / end
|
||||
gpu_timestamps segment
|
||||
# 60 fps (30 on the Switch): the host decides each frame's step in whole vsyncs and holds the frame that long (rewriter stubs at setWaitVsyncInterval and VFR::updateInterval,
|
||||
# patch_nso.py FRAME_STEP); 30 when 60 does not hold, game time = real time. DM_FPS_TARGET=30 (Documents/env.txt on the iPhone) saves power; DM_FRAME_STEP=legacy runs the game's own
|
||||
# interval controller with the 60 fps cuts instead
|
||||
# patch_nso.py FRAME_STEP); 30 when 60 does not hold, game time = real time. DM_FPS_TARGET=30 (Documents/env.txt on the iPhone) saves power; 0 runs the game's own
|
||||
# interval controller (with the 60 fps cuts, which also serve while the game's VFR override is set)
|
||||
fps_target 60
|
||||
|
||||
# ---- Versions ----
|
||||
|
||||
|
@@ -41,8 +41,8 @@ tex_flush_upload on
|
||||
render_cond on
|
||||
tex_dirty_upload on
|
||||
# 60 fps (30 on the Switch): the host decides each frame's step in whole vsyncs and holds the frame that long (rewriter stubs at FPSLocker's sites, patch_nso.py FRAME_STEP);
|
||||
# 30 when 60 does not hold, game time = real time. DM_FPS_TARGET=30 (Documents/env.txt on the iPhone) saves power, 0 = the game's own step; DM_FRAME_STEP=legacy steps by the measured
|
||||
# frame time instead (the method before, iPhone-tested at 60)
|
||||
# 30 when 60 does not hold, game time = real time. DM_FPS_TARGET=30 (Documents/env.txt on the iPhone) saves power, 0 = the game's own step. Until the host has decided a step
|
||||
# (the first frames) the stub steps by the measured frame time
|
||||
fps_target 60
|
||||
|
||||
# ---- NVN driver constant segments (Breath of the Wild's values, the same NVN build) ----
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 70.
|
@@ -43,7 +43,6 @@ None of them has a default game: without one they stop and list the profiles. To
|
||||
| `game_args` | Arguments the game gets when the command line gives none (the iOS app, the NSP script's Mac runs), split at spaces; `dmhost <dir> -- …` overrides them. The SDK hands them to the game before `nninitStartup`, as on hardware (`main.c`). Octopath Traveler 0 caps the address region Unreal's `nninitStartup` reserves (62 GB by default) with `-SwitchVirtualAddressSpaceSize=2048` (MB) |
|
||||
| `present_interval` | `game` (the default): frames are paced by the present interval the game sets, as on the Switch (a frame presented with interval 2 stays on screen for two display ticks; a game that keeps asking for 2 for a second gets the 30 fps target, and on iOS the display link then runs at 30 Hz; Mario 1.2.1 switches to 2 for single frames after a slow one; Engage asks for 2 on the Switch, and a rewriter cut on its 2.0.0 `main` makes it ask for 1, see `0100A6301214E000/game.tsv`). `display`: one frame per display tick whatever the game asks, which is only right for games whose logic does not count frames (D2R's runs on its own 25 Hz clock). `DM_PRESENT_INTERVAL=game` / `display` overrides it, also from `Documents/env.txt` |
|
||||
| `fps_target` | Frame rate cap of the rewriter's frame step, for builds that have one (`patch_nso.py` `FRAME_STEP`: Tears of the Kingdom 1.4.3, Breath of the Wild 1.9.0), or the engine's own frame limit the host writes into game memory (`hle_fpslock.c`: Monster Hunter Rise 16.0.2, which also needs `present_interval display`). `0` (the default): the game's own step. `30` / `60`: the host decides each frame's step in whole vsyncs and keeps the frame on screen that long (`hle_nvn.c` `fstep_tick`, `nvn_fstep.h`): 1 at a 60 cap, 2 when 60 does not hold or at a 30 cap (and while a prerendered movie plays), plus one vsync while the game is behind real time after a late frame. Presents follow that hold instead of the interval the game sets. `DM_FPS_TARGET` overrides it, also from `Documents/env.txt`; the log line `frame step: game time … of real time` checks the game's speed |
|
||||
| `frame_step` | `ticks` (the default): the frame step above. `legacy`: each game's method before it, for the iPhone A/B test: Tears of the Kingdom steps by the measured frame time capped at `fps_target`, Breath of the Wild runs its own interval controller with the 60 fps cuts. `DM_FRAME_STEP=ticks` / `legacy` overrides it, also from `Documents/env.txt` |
|
||||
| `core_preempt` | `on` / `off` (the default): approximate Horizon's per-core preemption. When an SVC wakes a higher-priority thread pinned to the caller's core, the caller waits (at most 5 ms) until such threads block again (`hle_thread.c`). Metroid Dread's movie player relies on it; Mario's loading gets slower with it (first frame 0.6 → 2.9 s), so it is off unless a game needs it. `DM_PREEMPT=0` / `1` overrides |
|
||||
| `alloc_big_align` | `on` / `off` (the default): `nn::mem::StandardAllocator::Allocate` aligns blocks of 64 KB or more to 64 KB (`hle_os.c`). Vanillaware's engine (13 Sentinels: Aegis Rim, Unicorn Overlord) takes a memory pool's storage from malloc and cuts ring heaps out of it at 64 KB alignment relative to the pool's GPU address. On the Switch that address is big-page aligned; the host's is the CPU address, only 4 KB aligned, so the last ring heap does not fit and keeps base 0, and the game writes through a null pointer (13 Sentinels entering its first battle, Unicorn Overlord past the title screen) |
|
||||
| `shared_font` | What the shared-font address points at: `bfttf` (the default) = the 8-byte BFTTF header (with the "already decoded" marker) followed by the TTF, which Mario passes to `ScalableFontEngineHelper::Decode`; `ttf` = the TTF itself, which Super Mario 3D World + Bowser's Fury (SDK 10) hands straight to `ScalableFontEngine::LoadFont` (with the header the engine fails with error 403 and the game crashes looking up a glyph) |
|
||||
|
||||
@@ -96,8 +96,6 @@ def header(g):
|
||||
if pi not in ('game', 'display'): sys.exit(f'{g.id}: present_interval must be game or display: {pi}')
|
||||
ft = g.get('fps_target', '0') # frame rate cap of the rewriter's frame step (patch_nso.py FRAME_STEP: Tears of the Kingdom 1.4.3, Breath of the Wild 1.9.0) or of the engine's own frame limit (hle_fpslock.c): 0 = the game's own step, 30 / 60
|
||||
if ft not in ('0', '30', '60'): sys.exit(f'{g.id}: fps_target must be 0, 30 or 60: {ft}')
|
||||
fs = g.get('frame_step', 'ticks') # the frame step's method: ticks = whole vsyncs decided by the host (nvn_fstep.h), legacy = each game's earlier method (for the iPhone A/B test)
|
||||
if fs not in ('ticks', 'legacy'): sys.exit(f'{g.id}: frame_step must be ticks or legacy: {fs}')
|
||||
cp = g.get('core_preempt', 'off') # same-core preemption approximation (hle_thread.c): on / off
|
||||
if cp not in ('on', 'off'): sys.exit(f'{g.id}: core_preempt must be on or off: {cp}')
|
||||
aba = g.get('alloc_big_align', 'off') # nn::mem::StandardAllocator::Allocate aligns blocks of 64 KB or more to 64 KB (hle_os.c; Vanillaware's memory pool storage): on / off
|
||||
@@ -155,7 +153,6 @@ def header(g):
|
||||
f'#define DM_GAME_CORE_PREEMPT {int(cp == "on")}',
|
||||
f'#define DM_GAME_ALLOC_BIG_ALIGN {int(aba == "on")}',
|
||||
f'#define DM_GAME_FPS_TARGET {int(ft)}',
|
||||
f'#define DM_GAME_FRAME_STEP_LEGACY {int(fs == "legacy")}',
|
||||
f'#define DM_GAME_SHARED_FONT_TTF {int(sf == "ttf")}',
|
||||
f'#define DM_GAME_PAD_FULL {int(pl == "full")}',
|
||||
f'#define DM_GAME_OPMODE {int(om == "console")}',
|
||||
|
||||
+4
-7
@@ -10,7 +10,7 @@ For the build steps of the whole project see `README.md` and `setup.sh` in the r
|
||||
|
||||
- The game's main program imports only one NVN symbol, `nvnBootstrapLoader`; all other NVN functions are obtained through it by name. The host forcibly takes over this import (`nvn/hle_nvn.c`), and not a single instruction of the original graphics driver is executed.
|
||||
- The game fetches the addresses of 523 names in total (`nvn/names.txt`). Each name has a thunk (`nvn/nvn_thunks.S`, a generated file): it first increments that function's call count and then jumps to the implementation.
|
||||
- Of these, the 216 that have call sites in the game's main program (`nvn/nvn_slots.tsv`) all have implementations: 203 are implemented according to NVN semantics, and 13 are empty implementations (`g_nops` in `hle_nvn.c`). The remaining 307 are connected to a red-reporting stub that, when called, prints `[RED] NVN unimplemented function called: <name>` in the log.
|
||||
- The functions games call have implementations, according to NVN semantics or empty (`g_nops` in `hle_nvn.c`); the rest are connected to a red-reporting stub that, when called, prints `[RED] NVN unimplemented function called: <name>` in the log.
|
||||
- Every implementation gets a compile-time signature check against `nvn/nvn_protos.h` (a generated file). The calling conventions of Apple arm64 and the Switch differ for integer parameters narrower than 32 bits, so on the host side `NVNboolean` parameters are always received as `uint32_t` and only the low 8 bits are examined.
|
||||
|
||||
### Two layers: record and replay
|
||||
@@ -36,8 +36,6 @@ Original Switch shader package
|
||||
→ xcrun metal / metallib compiles into Metal shader libraries
|
||||
```
|
||||
|
||||
Without Apple's Metal compiler (Windows, Linux, a Mac without Xcode) the last step cannot be done. `tools/nometal_run.py` lets the same chain run its first two steps as usual and puts the translated MSL source into the library files as is; a package made this way compiles the shaders once on the phone, and the system caches the compiled result. This route is used by `dm/tools/d2rm_pack.py`; see `docs/NO_XCODE.md` in the repository root.
|
||||
|
||||
The whole chain is driven by `tools/dmsh_build.py`, and `setup.sh` runs it for you. Key points:
|
||||
|
||||
- **Key**: each shader is keyed by the first 16 hexadecimal digits of the sha256 of its control section (the first 0x880 bytes, or the section's own length at control +0x20 when that is shorter, as in SDK 10's 0x850-byte sections) followed by its code area (many of Mario's shaders share a control section and differ in code). At run time the game hands both to `nvnProgramSetShaders`, and the replay layer computes the same key to find the library.
|
||||
@@ -77,7 +75,7 @@ Binding conventions followed during translation (file header of `tools/maxwell_r
|
||||
| `nvn/nvn_hmap.h`, `nvn/nvn_cover.h`, `nvn/nvn_pace.h`, `nvn/nvn_pso_skey.h` | Hash table, geometric proof of full-coverage draws, presentation pacer, pipeline manifest key |
|
||||
| `nvn/nvn_builtin.h` | Hand-written MSL for the 4 compute shaders embedded in the main program |
|
||||
| `nvn/include/nvn.h` | NVN interface declarations (third party; for the license see the file header) |
|
||||
| `nvn/names.txt`, `nvn/nvn_slots.tsv` | The NVN API name list and D2R's slot evidence. The per-game tables described above and `dmsh_chain.h` live in the game profile `dm/games/<id>/` (named after the base game's title ID; tools pick it with `DM_GAME`, make with `GAME`; there is no default game) |
|
||||
| `nvn/names.txt` | The NVN API name list. The per-game tables described above and `dmsh_chain.h` live in the game profile `dm/games/<id>/` (named after the base game's title ID; tools pick it with `DM_GAME`, make with `GAME`; there is no default game) |
|
||||
| `tex/` | Texture format mapping, deswizzling, BC decompression, see [tex/README.md](tex/README.md) |
|
||||
| `metal/` | A standalone minimal Metal offscreen example, not compiled into the host, see [metal/README.md](metal/README.md) |
|
||||
| `tools/` | Offline tools, see below |
|
||||
@@ -95,11 +93,10 @@ The scripts in `tools/`; usage is written in each file's header:
|
||||
| `dmsh_fetch.py` | Generates the "readable tile" versions of fragment shaders |
|
||||
| `c1_guard.py` | Generates `c1_full.tsv` |
|
||||
| `dmsh_dryrun.py` | Dry-run conversion of the whole library, measuring only "can it be converted" |
|
||||
| `shader_map.py`, `maxwell_refs.py` | Generates the shader map; reads resource references from the machine code |
|
||||
| `nvn_slots.py`, `gen_protos.py` | Generate `nvn_slots.tsv` / `names.txt`, and `nvn_protos.h` / the thunks |
|
||||
| `maxwell_refs.py` | Reads resource references from the machine code |
|
||||
| `gen_protos.py` | Generates `nvn_protos.h` / the thunks |
|
||||
| `tbdr_budget.py`, `tbdr_load_dead.py`, `tbdr_store_dead.py`, `tbdr_pass_merge.py` | Read the `DM_NVN_PASSDUMP` log and analyze the load / store volume of each render pass's attachments per frame |
|
||||
| `passgpu_top.py` | Reads the `DM_NVN_PASSGPU` output and lists GPU time by pass |
|
||||
| `nometal_run.py` | Runs this translation chain on a machine without Apple's Metal compiler: shaders go into the libraries as source and are compiled on the phone |
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
+5
-16
@@ -1201,7 +1201,7 @@ uint64_t dm_nvn_sub_cpu_ns(void) { return atomic_load(&g_cum_sub_cpu_ns); } //
|
||||
__attribute__((weak)) int dm_thermal_now(void) { return -1; } // Default (Mac); overridden by iOS front_ios.m // default (Mac); the strong definition in the iOS front end front_ios.m overrides it
|
||||
// Per-frame ring for the long-frame log (GPU time only arrives 8 frames late, so store first and judge later)
|
||||
#define LFN 16
|
||||
typedef struct { uint64_t f; double fr, nv, t_s, make_ms, up_mb, up_ms; unsigned pso, lib, up, rup; int ls; } lf_rec; // ls: whether the loading screen was covering the picture when this frame was presented
|
||||
typedef struct { uint64_t f; double fr, nv, t_s, make_ms, up_mb, up_ms; unsigned pso, lib, up, rup; } lf_rec;
|
||||
static lf_rec g_lf[LFN];
|
||||
__attribute__((weak)) void dm_longframe_note(const char *what) { LOG("[longframe] %s", what); } // Default (graphics unit tests); the strong definition in the host's diag.c adds scene context and writes perf.txt
|
||||
extern void mtl_create_ns(uint64_t *lib_ns, uint64_t *pso_ns); extern uint64_t mtl_upload_bytes(void); extern void mtl_pkg_stats(uint64_t *uploads, uint64_t *reuploads);
|
||||
@@ -1366,8 +1366,7 @@ static _Atomic uint64_t g_itv2_since; // start of the game's current run of in
|
||||
// The one-second hold: Mario 1.2.1 picks the interval from the previous frame's time (main+0xcfd24), setting 2 for the frame after a slow one and then 1 again (123 times in two minutes on an iPhone). Following that frame by frame would flap the display-link mode;
|
||||
// a single interval-2 frame is instead kept on screen for its own period by the display-link callback, and the next acquire waits as many ticks (mtl_link_wait_tick, nvn_mtl.m), without changing the mode
|
||||
// Frame step (patch_nso.py FRAME_STEP: Tears of the Kingdom 1.4.3, Breath of the Wild 1.9.0): the game no longer picks an interval. The host hands it each frame's step in whole vsyncs
|
||||
// (fstep_tick, nvn_fstep.h) and presents follow the hold k it decides with it; in legacy mode (state +0x28, DM_FRAME_STEP=legacy: TOTK's elapsed-time step) presents follow the cap
|
||||
// (30 while a prerendered movie plays)
|
||||
// (fstep_tick, nvn_fstep.h) and presents follow the hold k it decides with it
|
||||
__attribute__((weak)) uint8_t *dm_fps_state; // loader.c; graphics unit tests have no loader
|
||||
__attribute__((weak)) void dm_fps_lock_tick(void) {} // hle_fpslock.c: engine frame limits in game memory (Monster Hunter Rise); graphics unit tests have no host
|
||||
static int fps_step_cap(void) {
|
||||
@@ -1375,7 +1374,7 @@ static int fps_step_cap(void) {
|
||||
uint32_t b = __atomic_load_n((uint32_t *)dm_fps_state, __ATOMIC_RELAXED); float t; memcpy(&t, &b, sizeof t);
|
||||
return t <= 0 ? 0 : __atomic_load_n((uint32_t *)(dm_fps_state + 4), __ATOMIC_RELAXED) || t <= 30 ? 30 : 60;
|
||||
}
|
||||
static int fstep_on(void) { return fps_step_cap() && !__atomic_load_n((uint32_t *)(dm_fps_state + 0x28), __ATOMIC_RELAXED); } // whole vsyncs, not legacy
|
||||
static int fstep_on(void) { return fps_step_cap() != 0; } // the host decides the steps in whole vsyncs
|
||||
static nvn_fstep g_fs; // present thread only
|
||||
static _Atomic int g_fs_k; // the hold presentation follows (0 = not decided yet)
|
||||
// The display link runs at 30 Hz at a 30 cap, except while the game fixes its own step at 1 (BotW's VFR override)
|
||||
@@ -1443,20 +1442,12 @@ static void fstep_tick(const win_t *o) {
|
||||
t0 = t; v0 = vs; memset(held, 0, sizeof held);
|
||||
}
|
||||
}
|
||||
int (*g_nvn_loadscreen_up)(void); // host registration: whether the loading screen is up right now (filled in when diag.c subscribes to UI messages; null in graphics unit tests = no gate)
|
||||
NVN(nvnQueuePresentTexture, void, NVNqueue *q, NVNwindow *w, int idx) {
|
||||
(void)q; win_t *o = (win_t *)w; o->presents++;
|
||||
dm_fps_lock_tick();
|
||||
if (dm_fps_state) { static int was; int c = fps_step_cap(); if (c != was) { LOG("[nvn] frame step cap %d → %d fps at present %llu%s", was, c, (unsigned long long)o->presents, c == 30 && was == 60 ? " (prerendered movie)" : ""); was = c; } }
|
||||
if (fstep_on()) fstep_tick(o);
|
||||
else if (dm_fps_state && o->presents % 1800 == 0 && fps_step_cap()) { // legacy, the elapsed-time frame step: game time against real time (1.00 = normal speed; below 1 when frames take longer than 1/15 s)
|
||||
static uint64_t t0; static double g0; double g; memcpy(&g, dm_fps_state + 0x18, sizeof g); uint64_t t = now_ns();
|
||||
if (t0) LOG("[nvn] frame step: game time %.2f s in %.2f s (%.3f of real time), cap %d fps", g - g0, (t - t0) / 1e9, (g - g0) / ((t - t0) / 1e9), fps_step_cap());
|
||||
t0 = t; g0 = g; }
|
||||
static uint64_t prev_in, prev_out, prev_pass, prev_draw;
|
||||
// Loading screen on first entering a run: if the background pipeline warm-up has not finished, wait for it here (the loading screen stays a bit longer), so that after entering the run no pipelines are compiled on the fly on the submit thread. When warm-up finished long ago this passes with a single check
|
||||
{ static int gate;
|
||||
if (!gate && g_nvn_loadscreen_up && g_nvn_loadscreen_up()) { gate = 1; int ms = mtl_warm_wait(15000); if (ms) LOG("[nvn] the loading screen into the run waited %d ms for pipeline prewarm", ms); } }
|
||||
present_cfg(o);
|
||||
{
|
||||
// Test switch DM_TEST_PRESENT_LAG=every N presents,milliseconds: sleep that long before the Nth, 2Nth… present, imitating an occasional slow frame on a phone (Mario 1.2.1 then sets present interval 2; checks that the display link holds frames by interval). Not set in the product
|
||||
@@ -1520,7 +1511,7 @@ NVN(nvnQueuePresentTexture, void, NVNqueue *q, NVNwindow *w, int idx) {
|
||||
trow(f, "@lib_new", (double)(lib_ - plib)); trow(f, "@pso_new", (double)(pso_ - ppso));
|
||||
{ // long-frame log: this frame's items are first recorded into the ring; whether to print is decided 8 frames later, once the GPU time has arrived (see below)
|
||||
static uint64_t plns, ppns, pup, prup, pub; uint64_t lns, pns, up, rup, ub; mtl_create_ns(&lns, &pns); mtl_pkg_stats(&up, &rup); ub = mtl_upload_bytes();
|
||||
lf_rec *r = &g_lf[f % LFN]; r->f = f; r->fr = fr; r->nv = nv; r->ls = g_nvn_loadscreen_up ? g_nvn_loadscreen_up() : 0; r->t_s = (now_ns() - g_t0_ns) / 1e9; r->pso = (unsigned)(pso_ - ppso); r->lib = (unsigned)(lib_ - plib);
|
||||
lf_rec *r = &g_lf[f % LFN]; r->f = f; r->fr = fr; r->nv = nv; r->t_s = (now_ns() - g_t0_ns) / 1e9; r->pso = (unsigned)(pso_ - ppso); r->lib = (unsigned)(lib_ - plib);
|
||||
r->make_ms = ((lns - plns) + (pns - ppns)) / 1e6; r->up = (unsigned)(up - pup); r->rup = (unsigned)(rup - prup); r->up_mb = (ub - pub) / 1048576.0; r->up_ms = up_ms;
|
||||
plns = lns; ppns = pns; pup = up; prup = rup; pub = ub;
|
||||
}
|
||||
@@ -1540,8 +1531,6 @@ NVN(nvnQueuePresentTexture, void, NVNqueue *q, NVNwindow *w, int idx) {
|
||||
snprintf(b, sizeof b, "frame %llu (%.2f s) whole frame %.1f ms, submit+present %.1f ms, GPU %.1f ms; new pipelines %u + shader libraries %u, %.1f ms in all; texture uploads %u first + %u re-uploads, %.1f MB / %.1f ms",
|
||||
(unsigned long long)r->f, r->t_s, r->fr, r->nv, g, r->pso, r->lib, r->make_ms, r->up, r->rup, r->up_mb, r->up_ms);
|
||||
// Visibility: during the time a long frame "comes out late", what stays on screen is the previous frame; if the previous frame was a loading screen (or this frame itself is one), all the player sees is the loading screen staying a bit longer, with no motion interrupted
|
||||
const lf_rec *pv = &g_lf[(f - 9) % LFN]; int hidden = r->ls || (f >= 9 && pv->f == f - 9 && pv->ls);
|
||||
if (hidden) strlcat(b, "; next to a loading screen, not visible", sizeof b);
|
||||
dm_longframe_note(b); } }
|
||||
uint64_t g0, g1; if (frame_rec() && mtl_gpu_span(f - 8, &g0, &g1)) { trow(f - 8, "@gpu_beg_ms", (g0 - g_t0_ns) / 1e6); trow(f - 8, "@gpu_end_ms", (g1 - g_t0_ns) / 1e6); } }
|
||||
prev_in = t0; prev_pass = ps; prev_draw = dr;
|
||||
@@ -1568,7 +1557,7 @@ NVN(nvnQueuePresentTexture, void, NVNqueue *q, NVNwindow *w, int idx) {
|
||||
prev_out = now_ns();
|
||||
}
|
||||
|
||||
// ---------- State-only interfaces used by newer-SDK games (SMBW) that D2R did not use: buffer objects, pool / sampler / texture queries, views, color state, device and window ----------
|
||||
// ---------- State-only interfaces used by newer-SDK games (SMBW): buffer objects, pool / sampler / texture queries, views, color state, device and window ----------
|
||||
// They only read and write the object itself and record no commands. Where the host has no matching capability (texture channel swizzle, depth / stencil sampling mode, sampler reduction, virtual mapping) the first occurrence logs one line and the call returns normally.
|
||||
#define ONCE_LOG(...) do { static int once_; if (!once_++) LOG("[nvn] " __VA_ARGS__); } while (0)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include <mach/mach_time.h>
|
||||
#include <sched.h>
|
||||
|
||||
// Order of page state and mprotect (from d2r-mobile ef9b1ff13): the side changing protection first sets the state to BUSY and sets the final state only after mprotect has landed; other threads wait when they see BUSY.
|
||||
// Order of page state and mprotect: the side changing protection first sets the state to BUSY and sets the final state only after mprotect has landed; other threads wait when they see BUSY.
|
||||
// set read-only: NONE -> BUSY -> mprotect(read-only) -> PROT; unprotect: PROT -> BUSY -> mprotect(writable) -> NONE.
|
||||
// Invariant: a page whose state is NONE is always writable, one whose state is PROT is always read-only. Setting read-only used to be "record PROT, then mprotect"; in between, another thread's unprotect-before-write
|
||||
// (the movie decoder thread writing a frame into texture storage, a disk read into a guest buffer) could set the state back to NONE, then the mprotect landed => the page read-only with state NONE: a game thread writing it got only
|
||||
|
||||
@@ -44,5 +44,3 @@ void *mtl_device(void);
|
||||
uint64_t mtl_ext_tex(void *tex);
|
||||
uint64_t mtl_present_ext(uint64_t serial, double period, void *ev, uint64_t ev_val, const int crop[4]); // crop: x, y, w, h of the texture to show (NULL = all of it)
|
||||
void mtl_present_wait(uint64_t ticket); // as in nvn_obj.h: wait until that present has been handed to the GPU
|
||||
void mtl_ui_ghost(int on, float px, float py, const float grid[4], float cx, float cy); // Drag-follow image for shop items: when on=1, the icon of the item that covers screen point (px,py) and sits on the shop shelf grid `grid` (top-left x,y, width and height of one cell) is translated to center (cx,cy) and drawn once more at the end of the UI pass (normalized, top-left origin); callable from any thread
|
||||
int mtl_ui_ghost_rect(float r[4]); // Screen rectangle (normalized) of the item recognized above; returns 0 if not recognized yet
|
||||
+20
-534
@@ -71,16 +71,6 @@ static id<MTL4CommandBuffer> g_present_cb[3];
|
||||
static id<MTL4ArgumentTable> g_vt, g_ft, g_ct, g_up_ct;
|
||||
static id<MTL4ArgumentTable> g_present_args;
|
||||
static id<MTLBuffer> g_present_uv[3];
|
||||
int g_bloom_q = -1; // Bloom 1/4 starting point (non-static: real-device same-process alternation sets it to 0 / 1 directly)
|
||||
static id<MTL4ArgumentTable> g_bloomq_args;
|
||||
static id<MTLBuffer> g_bloomq_const[3];
|
||||
static id<MTLSamplerState> g_bloomq_samp;
|
||||
static id<MTLRenderPipelineState> g_bloomq_pso;
|
||||
static MTLPixelFormat g_bloomq_fmt;
|
||||
static id<MTLTexture> g_bloomq_view;
|
||||
static int g_bloomq_i;
|
||||
static struct { int state; uint64_t s_ser, b0_ser, b1_ser, s_h, samp_h, b1_h; const tex_t *s_tex; float threshold, coeff_g, coeff_b; } g_bloomq;
|
||||
static uint64_t g_bloomq_last_b0, g_bloomq_last_b1h, g_bloomq_last_b1s; // Most recent bloom 1/4 overlay read redirection: B0 serial → B1 handle (kept across frames, see the 2e2d6700a2569795 branch)
|
||||
static uint64_t g_present_alloc_fence[3];
|
||||
static pthread_mutex_t g_pq_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t g_pq_cv = PTHREAD_COND_INITIALIZER;
|
||||
@@ -323,15 +313,10 @@ void mtl_init(void) {
|
||||
pad.maxTextureBindCount = 1; pad.maxSamplerStateBindCount = 1; pad.maxBufferBindCount = 1;
|
||||
g_present_args = [g_dev newArgumentTableWithDescriptor:pad error:&ate];
|
||||
if (!g_present_args) MDIE("failed to create the present argument table: %s", ate.localizedDescription.UTF8String);
|
||||
g_bloomq_args = [g_dev newArgumentTableWithDescriptor:pad error:&ate];
|
||||
if (!g_bloomq_args) MDIE("failed to create the bloom 1/4 argument table: %s", ate.localizedDescription.UTF8String);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
g_present_uv[i] = [g_dev newBufferWithLength:sizeof(float) * 8 options:MTLResourceStorageModeShared];
|
||||
if (!g_present_uv[i]) MDIE("failed to create present UV buffer[%d]", i);
|
||||
res_add(g_present_uv[i]);
|
||||
g_bloomq_const[i] = [g_dev newBufferWithLength:sizeof(float) * 8 options:MTLResourceStorageModeShared];
|
||||
if (!g_bloomq_const[i]) MDIE("failed to create bloom 1/4 constant buffer[%d]", i);
|
||||
res_add(g_bloomq_const[i]);
|
||||
}
|
||||
g_ev = [g_dev newSharedEvent];
|
||||
g_keep = [NSMutableArray array];
|
||||
@@ -376,7 +361,7 @@ static void pc_put(int i) { // caller holds g_mu, pool i is resident
|
||||
*c = (pc_ent){g_pools[i].lo, g_pools[i].hi, g_pools[i].base, g_pools[i].buf, g_pools[i].gpu};
|
||||
}
|
||||
static _Atomic uint64_t g_reg_gen = 1; // Change generation of memory pool / texture / sampler registration: one of the invalidation conditions for argument buffer reuse
|
||||
// On-demand residency of memory pools (from d2r-mobile e2a0c9a9b): putting a zero-copy buffer into the residency set makes iOS read the whole pool into memory and pin it as wired, even a pool the game never uses
|
||||
// On-demand residency of memory pools: putting a zero-copy buffer into the residency set makes iOS read the whole pool into memory and pin it as wired, even a pool the game never uses
|
||||
// (they measured on a 17 Pro Max a 512 MB render-target pool addressed 0 times in a run, with about 530 MB more device wired memory; a 4 GB A14 was ended by the system within seconds of launch because of it).
|
||||
// So a pool is not put into the residency set when created; it is put in, and the residency set committed on the spot, the first time the GPU gets its address through mtl_addr / mtl_gpu_addr -- these are the only two ways the GPU gets pool addresses,
|
||||
// and committing on the spot is earlier than any command buffer commit that uses it. CPU-side reads of pool contents through mtl_addr trigger it too (conservative, never misses). Caller holds g_mu
|
||||
@@ -578,13 +563,11 @@ static post_op *g_post; static int g_npost, g_postcap;
|
||||
// (command buffers, encoders, pass descriptors, etc.) are never released unless put in a pool. Measured on the character select screen of a real iOS device: the host heap grows by about 47 objects per frame, 0.22 MiB/s.
|
||||
void *objc_autoreleasePoolPush(void); void objc_autoreleasePoolPop(void *);
|
||||
static void *g_sub_pool;
|
||||
static int g_ruler_truth = -1, g_ruler_pending;
|
||||
static int tbdr_on(void);
|
||||
|
||||
static uint64_t g_sub_serial; // Submission index (used by the parallel precompile's 'predict only once per submission')
|
||||
void mtl_sub_begin(void) {
|
||||
mtl_init(); g_sub_serial++; g_seg = 0; seg_clk_put(g_sclk); g_sclk = NULL; g_rc_npend = 0; g_rc_skip = 0;
|
||||
if (tbdr_on() == 0 && g_ruler_pending) { g_ruler_truth = 0; g_ruler_pending = 0; MLOG("DM_NVN_RULER: switching to the mode under test"); }
|
||||
defer_drain();
|
||||
defer_reset();
|
||||
warm_claim();
|
||||
@@ -621,7 +604,6 @@ void mtl_sub_begin(void) {
|
||||
}
|
||||
|
||||
static void rp_end(void);
|
||||
extern int g_frz_on; static int g_frz_state; // World freeze: the state latched for this frame (0 normal / 1 full frame / 2 frozen frame), see the note above frz_op
|
||||
// DM_NVN_PASSGPU_SERIAL=1 (for measurement): before creating each encoder on the main command buffer, encode 'signal n+1 → wait for signal n+1' - the signal is only raised once all prior work has completed,
|
||||
// so the following encoder starts only after the previous one has fully finished; on TBDR passes no longer overlap in the pipeline, and the per-pass start-to-end span of DM_NVN_PASSGPU is that pass's exclusive time (the whole-frame total is inflated; look only at relative cost)
|
||||
static void ser_gate(void) {
|
||||
@@ -864,7 +846,6 @@ static _Atomic uint64_t g_cur_frame;
|
||||
// @set:<global variable name>=<value> on entering the phase set this int global variable to the value, on leaving restore it (address found by symbol name; switches to be used on a real device must have a line added to the registration table below)
|
||||
// <fragment shader key prefix> or * the phase drops all draws of these fragment shaders (the picture will be wrong; it only measures 'how much this kind of draw is worth'; a nearly idle category needs a round of its own)
|
||||
// Readings: each phase discards its first 45 frames (transition + present lag); for the remaining frames, the mean of the actual present interval (same source as perf.txt) and the frame counts in the three buckets 16.7 / 25 / ≥33 ms, one log line "UB present".
|
||||
// Table output: $DM_ROOT/run/ruler/ub_table.py; real-device script: $DM_ROOT/run/ruler/ub_alt.sh.
|
||||
static int g_ub_n = -1, g_ub_N = 180; static _Atomic int g_ub_cur = -1; static char g_ub_name[16][16], g_ub_keys[16][16][48]; static int g_ub_nk[16];
|
||||
static _Atomic int g_ubp_on, g_ubp_c[4]; static _Atomic uint64_t g_ubp_sum_us;
|
||||
void mtl_ub_present(double iv) { // Called once per frame by the present thread: actual present interval in ms (−1 = dropped, 0 = no reading)
|
||||
@@ -875,8 +856,8 @@ void mtl_ub_present(double iv) { // Called once per frame by the present threa
|
||||
// Registered runtime switches: name → address. Main-program symbols of an iOS release build are not in the dynamic symbol table and dlsym cannot find them (in the first real-device round all four categories were "global variable not found" and the whole round was wasted),
|
||||
// so a switch to be measured on a real device must have a line added here; on Mac / in unit tests unregistered ones are still looked up by symbol name.
|
||||
static int *ub_var(const char *nm) {
|
||||
extern int g_bloom_q, g_dm_present_link;
|
||||
static const struct { const char *n; int *p; } t[] = { {"g_bloom_q", &g_bloom_q}, {"g_dm_present_link", &g_dm_present_link}, {"g_frz_on", &g_frz_on} };
|
||||
extern int g_dm_present_link;
|
||||
static const struct { const char *n; int *p; } t[] = { {"g_dm_present_link", &g_dm_present_link} };
|
||||
for (size_t i = 0; i < sizeof t / sizeof t[0]; i++) if (!strcmp(nm, t[i].n)) return t[i].p;
|
||||
return dlsym(RTLD_DEFAULT, nm);
|
||||
}
|
||||
@@ -1706,7 +1687,7 @@ void mtl_mem_stat(unsigned long long frame, unsigned long long rss_mb) {
|
||||
unsigned long dead_n = 0; unsigned long long dead_mb = 0;
|
||||
// Objects read from a weak table come back retained + autoreleased: the frame hook runs on a guest thread with no autorelease pool that drains, so without this wrapper every buffer counted would be pinned for good (the readout creating the leak itself)
|
||||
int pres = 0; @autoreleasepool { pthread_mutex_lock(&g_mu); pres = g_pool_res_n; for (id<MTLBuffer> b in g_dead_pools) { dead_n++; dead_mb += b.length; } pthread_mutex_unlock(&g_mu); } dead_mb >>= 20; // host objects held by serial (textures / packaged textures / programs): if it climbs along with GPU allocated, textures are not being released
|
||||
// os_proc_available_memory: how far this process is from being terminated by the system for memory, iOS only (from d2r-mobile 704147218); the Mac writes "none". On a phone, a crash with no log is most likely this number reaching 0
|
||||
// os_proc_available_memory: how far this process is from being terminated by the system for memory, iOS only; the Mac writes "none". On a phone, a crash with no log is most likely this number reaching 0
|
||||
char avail[32] = "none";
|
||||
#if TARGET_OS_IPHONE
|
||||
snprintf(avail, sizeof avail, "%llu MB", (unsigned long long)(os_proc_available_memory() >> 20));
|
||||
@@ -1737,11 +1718,10 @@ static struct {
|
||||
|
||||
// ---------- Zero-source skip ----------
|
||||
// Shader fact table (statically proven keys): 'the textures it samples are all 0 and the coefficient is finite ⇒ the output is all 0'. Coefficient = component comp of vector vec of fragment-stage constant buffer register ubo_slot.
|
||||
// 4a819818b8e489c5 ps_outline_blur_p: output = (sum of several samples of the same texture along a row / column) × fp_c7._m0[1].x (fp_c7 = cbuf reg 4), homogeneous linear in the sampled values, no constant term
|
||||
// (source of the proof: dmsh_b/<chain version>/4a819818b8e489c5_b.metal, read line by line). Add other shaders here once they are proven.
|
||||
// No shader is in the table yet: a proof comes from reading the shader's translation line by line (dmsh_b/<chain version>/<key>_b.metal); unit tests add their own (zskip_test_add).
|
||||
typedef struct { char key[17]; int ubo_slot, vec, comp; } zskip_fact;
|
||||
static zskip_fact g_zskip_facts[16] = {{"4a819818b8e489c5", 4, 1, 0}};
|
||||
static int g_zskip_nf = 1;
|
||||
static zskip_fact g_zskip_facts[16];
|
||||
static int g_zskip_nf;
|
||||
void zskip_test_add(const char *key, int ubo_slot, int vec, int comp) {
|
||||
if (!key || g_zskip_nf >= 16) return;
|
||||
snprintf(g_zskip_facts[g_zskip_nf].key, sizeof g_zskip_facts[0].key, "%s", key);
|
||||
@@ -1789,45 +1769,9 @@ static int passdump(void) {
|
||||
}
|
||||
#define PD(...) do { if (passdump()) { fprintf(stderr, "[pass] F%llu ", (unsigned long long)atomic_load(&g_cur_frame)); fprintf(stderr, __VA_ARGS__); fputc('\n', stderr); } } while (0)
|
||||
static void blit_note(void) { if (g_enc) PD("E %llu copy / buffer update ends the render pass", g_stat_pass); }
|
||||
// DM_GFXUSE=<file>: once the character select screen is stable (starting 1 second after the first OpenPanel MainMenuButtonRibbon, requires DM_BOOTPROF=1), over 30 consecutive frames,
|
||||
// the ranges in guest memory of the resources the GPU actually reads (texture storage, UBO / SSBO, vertex / index buffers), deduplicated by (kind, address, length), one line recorded on first sight.
|
||||
// For intersecting with the inflate output ranges (which data decompressed before character select is really used by the character select screen). Columns: kind guest_addr len first_frame tex_id fmt w×h
|
||||
__attribute__((weak)) uint64_t dm_prof_ribbon_ns(void) { return 0; } // diag.c provides the strong definition; when not merged it is always 0 → the time window does not open, and a failure is reported at the end
|
||||
__attribute__((weak)) void dm_shot_written(const char *path, int ok) { (void)path; (void)ok; }
|
||||
__attribute__((weak)) void dm_guest_frame_tick(void) {} // hle_os.c provides the strong definition (DM_DETCLOCK deterministic guest clock); a no-op when graphics unit tests do not link the host // diag.c provides the strong definition (DM_TICK_HOLD=E:SHOT unfreeze); a no-op when graphics unit tests do not link the host
|
||||
__attribute__((weak)) int dm_detclock_enabled(void) { return 0; } // Same as above: the host's hle_os.c provides the strong definition; when graphics unit tests do not link the host, the deterministic clock is treated as off
|
||||
__attribute__((weak)) uint64_t guest_ns(void) { return 0; } // Same as above: read only when the deterministic clock is on; never reached in graphics unit tests
|
||||
enum { USE_OFF, USE_WAIT, USE_REC, USE_DONE };
|
||||
static int g_use = -1; static FILE *g_use_f; static long g_use_f0, g_use_rows; static nvn_hmap g_use_seen = {.klen = 24};
|
||||
#define USE_FRAMES 30
|
||||
static void use_exit(void) {
|
||||
if (g_use == USE_WAIT) fprintf(stderr, "[dm] [RED] DM_GFXUSE window never opened: dm_prof_ribbon_ns() = %llu (needs DM_BOOTPROF=1 and a host with panel probes)\n", (unsigned long long)dm_prof_ribbon_ns());
|
||||
else if (g_use == USE_REC) fprintf(stderr, "[dm] [RED] DM_GFXUSE recorded only %ld frames (fewer than %d) before exit; the file is incomplete\n", (long)atomic_load(&g_cur_frame) - g_use_f0, USE_FRAMES);
|
||||
if (g_use_f) fflush(g_use_f);
|
||||
}
|
||||
static void use_tick(void) { // Called on every present
|
||||
if (g_use < 0) { const char *e = getenv("DM_GFXUSE"); g_use = USE_OFF;
|
||||
if (e) { g_use_f = fopen(e, "w"); if (!g_use_f) { MLOG("[RED] DM_GFXUSE cannot open %s", e); return; }
|
||||
fprintf(g_use_f, "kind\tguest_addr\tlen\tfirst_frame\ttex_id\tfmt\twxh\n"); fflush(g_use_f); g_use = USE_WAIT; atexit(use_exit);
|
||||
MLOG("[gfxuse] waiting for the panel event: records %d frames starting 1 s after the first OpenPanel MainMenuButtonRibbon → %s", USE_FRAMES, e); } }
|
||||
long f = (long)atomic_load(&g_cur_frame);
|
||||
if (g_use == USE_WAIT) {
|
||||
uint64_t t = dm_prof_ribbon_ns();
|
||||
if (t && now_ns() - t >= 1000000000ull) { g_use = USE_REC; g_use_f0 = f; MLOG("[gfxuse] recording started: from frame %ld, %d frames", f, USE_FRAMES); }
|
||||
} else if (g_use == USE_REC && f - g_use_f0 >= USE_FRAMES) {
|
||||
g_use = USE_DONE; fclose(g_use_f); g_use_f = NULL;
|
||||
MLOG("[gfxuse] recording done: frames %ld–%ld, %ld ranges after dedup", g_use_f0, f - 1, g_use_rows);
|
||||
}
|
||||
}
|
||||
static __attribute__((always_inline)) inline void use_rec(const char *kind, uint64_t addr, uint64_t len, int tid, const tex_t *t) {
|
||||
if (g_use != USE_REC || !addr || !len) return;
|
||||
uint64_t k[3] = {addr, len, (uint64_t)(uint8_t)kind[0]};
|
||||
if (nvn_hm_get(&g_use_seen, k)) return;
|
||||
nvn_hm_put(&g_use_seen, k, (void *)1);
|
||||
g_use_rows++;
|
||||
if (t) fprintf(g_use_f, "%s\t%#llx\t%llu\t%lld\t%d\t%#x\t%ux%u\n", kind, (unsigned long long)addr, (unsigned long long)len, (long long)atomic_load(&g_cur_frame), tid, t->d.format, t->d.width, t->d.height);
|
||||
else fprintf(g_use_f, "%s\t%#llx\t%llu\t%lld\t\t\t\n", kind, (unsigned long long)addr, (unsigned long long)len, (long long)atomic_load(&g_cur_frame));
|
||||
}
|
||||
static uint64_t att_bytes(id<MTLTexture> t, NSUInteger level) {
|
||||
if (!t) return 0;
|
||||
NSUInteger w = MAX(t.width >> level, 1), h = MAX(t.height >> level, 1);
|
||||
@@ -1985,8 +1929,7 @@ static int udc_on(void) {
|
||||
// Only for equivalence comparison (the same replay run on / off, comparing the picture byte by byte), not a product configuration
|
||||
// Mario: off by default (only DM_NVN_TBDR=1 turns it on). These optimizations infer read / write relations from "which textures are bound in texture slots", while 38% of this game's shaders use bindless textures (handles in uniform buffers, NVN_RB_TEXU),
|
||||
// so the inference would miss readers - e.g. deciding an attachment is never read afterwards and not storing it. Turn them back on once they recognize bindless readers.
|
||||
static int tbdr_on(void) { static int v = -1; if (v < 0) { const char *e = getenv("DM_NVN_TBDR"), *r = getenv("DM_NVN_RULER"); v = e && !strcmp(e, "1"); g_ruler_truth = r && !strcmp(r, "1"); if (!v) MLOG("TBDR optimizations off by default (this game uses bindless textures; DM_NVN_TBDR=1 turns them on)"); } return v && !g_ruler_truth; }
|
||||
int dm_nvn_ruler_truth(void) { tbdr_on(); return g_ruler_truth; } // Visual ruler ground-truth phase (host-side settings under test yield accordingly, e.g. the 3D render scale)
|
||||
static int tbdr_on(void) { static int v = -1; if (v < 0) { const char *e = getenv("DM_NVN_TBDR"); v = e && !strcmp(e, "1"); if (!v) MLOG("TBDR optimizations off by default (this game uses bindless textures; DM_NVN_TBDR=1 turns them on)"); } return v; }
|
||||
static int rt_range_on(void) { static int v = -1; if (v < 0) { const char *e=getenv("DM_NVN_RT_RANGE"); v=e && !strcmp(e,"1"); } return v; } // Narrow candidate, off by default
|
||||
static uint64_t g_sd_dead_bytes, g_sd_n_dead, g_sd_n_all;
|
||||
// ---------- Cross-submission attachment validity ledger ----------
|
||||
@@ -1996,7 +1939,7 @@ static uint64_t g_sd_dead_bytes, g_sd_n_dead, g_sd_n_all;
|
||||
// as an attachment with load DontCare / Clear → the pending clear is voided, with load Load → changed to a Clear load; depth-stencil is recorded per plane.
|
||||
// Violation = report a failure + that stable key (format / width / height / level) permanently disabled (in memory + persisted to DM_XV_FILE, read at the next launch) + all entries of the same texture deleted. The fallback can only detect, not undo (in that frame the attachment holds old content).
|
||||
// Its first use: color attachments 'scanned to the end undecided' at the end of a submission are not written back (sd_finish) - conditions: its first touch in this submission is a full clear / DontCare load (reset every frame before use), it is not a window texture, and the key is not disabled.
|
||||
// Switch DM_NVN_XV=0 turns it off; has no effect when tbdr_on()==0 (visual ruler ground-truth mode).
|
||||
// Switch DM_NVN_XV=0 turns it off; has no effect when tbdr_on()==0.
|
||||
// Trial period (XV_PROBE): for a key's first XV_TRIAL times it is only 'shadow-accounted' - written back as usual, but its next touch is tracked all the same; only after XV_TRIAL consecutive compliant touches does the write-back really start being skipped.
|
||||
// On a read / Load during the trial period: only disable that key (within this process, not persisted, no failure), the picture is intact - in practice there are readers as early as the launch / menu screens (upscale target 36), and not every fresh install should get one wrong frame.
|
||||
enum { XV_INVALID = 1, XV_PENDING_CLEAR = 2, XV_PROBE = 3 };
|
||||
@@ -2081,10 +2024,8 @@ static void defer_fallback(void);
|
||||
// the encoder track and the GPU interval table carry this name directly, and shader_map.tsv maps it back to the shader; off by default (one string allocation per pass)
|
||||
static int g_el_w, g_el_h, g_el_draws; static char g_el_key[12];
|
||||
static int enc_label_on(void) { static int v = -1; if (v < 0) { const char *e = getenv("DM_NVN_ENC_LABEL"); v = e && atoi(e); } return v; }
|
||||
static void ghost_flush(void);
|
||||
static int g_nbcrt; static void bcrt_flush(void); // shadows of block-compressed render targets used by the open pass (see bcrt_attach)
|
||||
static void rp_end(void) { if (g_enc) { AP_B;
|
||||
ghost_flush();
|
||||
if (enc_label_on()) { static uint64_t lf = ~0ull, base; uint64_t f = atomic_load(&g_cur_frame); if (f != lf) { lf = f; base = g_stat_pass - 1; } // Intra-frame serial: the same pass has the same name every frame, so the table can aggregate by name
|
||||
[(id)g_enc setLabel:[NSString stringWithFormat:@"p%02llu %dx%d %s d%d", (unsigned long long)(g_stat_pass - base), g_el_w, g_el_h, g_el_key[0] ? g_el_key : "-", g_el_draws]]; }
|
||||
#ifdef NVN_RT_RANGE_TEST
|
||||
@@ -2565,18 +2506,6 @@ static id<MTLLibrary> lib_from_file(NSString *path, NSError **err) {
|
||||
if (fd < 0 || fstat(fd, &st) || st.st_size <= 0) { if (fd >= 0) close(fd); if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"cannot open %@", path]}]; return nil; }
|
||||
size_t len = (size_t)st.st_size; void *m = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0); close(fd);
|
||||
if (m == MAP_FAILED) { if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"mmap failed %@", path]}]; return nil; }
|
||||
#ifdef DM_SHELL
|
||||
// Shell: when the user's computer has no Apple shader compiler (the no-compiler mode of tools/d2rm_pack.py, gfx/tools/nometal_run.py), the "library files" contain
|
||||
// shader source rather than compiled libraries (a compiled library starts with MTLB). They are compiled here on the spot; the system caches the compiled result, and from the second launch on the same shader no longer costs this time.
|
||||
// Measured on a real device (cold): about 80 ms each on average, about 450 ms for the longest single one; one session uses several hundred.
|
||||
if (len < 4 || memcmp(m, "MTLB", 4)) { NSString *src = [[NSString alloc] initWithBytes:m length:len encoding:NSUTF8StringEncoding]; munmap(m, len);
|
||||
if (!src) { if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:EILSEQ userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"neither a compiled library nor source %@", path]}]; return nil; }
|
||||
static _Atomic int n; static _Atomic long long us; CFAbsoluteTime t0 = CFAbsoluteTimeGetCurrent();
|
||||
id<MTLLibrary> l = [g_dev newLibraryWithSource:src options:nil error:err]; long long dt = (long long)((CFAbsoluteTimeGetCurrent() - t0) * 1e6);
|
||||
int k = atomic_fetch_add(&n, 1) + 1; long long tot = atomic_fetch_add(&us, dt) + dt;
|
||||
if (k == 1 || k % 100 == 0 || dt >= 300000) MLOG("runtime shader compile: #%d %.0f ms, %.1f s in total", k, dt / 1e3, tot / 1e6);
|
||||
return l; }
|
||||
#endif
|
||||
dispatch_data_t d = dispatch_data_create(m, len, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ munmap(m, len); });
|
||||
return [g_dev newLibraryWithData:d error:err];
|
||||
}
|
||||
@@ -3549,10 +3478,9 @@ static void warm_start(void) {
|
||||
atomic_store(&g_warm_running, 1);
|
||||
dispatch_async(q, ^{ warm_worker(paths); });
|
||||
}
|
||||
static int g_rt_ovr; // Stencil override for runtime extrapolation: 0 no override; 1 pass only on the unit / effect / extrapolation-hole bits (0xD0), no stencil write; 3 depth test as usual, write the effect bit 0x40 where it passes
|
||||
static id<MTLDepthStencilState> dss_get(int has_depth, int has_stencil) {
|
||||
struct { ds_st ds; uint32_t vm[2], wm[2]; int hd, hs, ovr; } k;
|
||||
memset(&k, 0, sizeof k); k.ds = S.ds; memcpy(k.vm, S.st_vmask, 8); memcpy(k.wm, S.st_wmask, 8); k.hd = has_depth; k.hs = has_stencil; k.ovr = has_stencil ? g_rt_ovr : 0;
|
||||
struct { ds_st ds; uint32_t vm[2], wm[2]; int hd, hs; } k;
|
||||
memset(&k, 0, sizeof k); k.ds = S.ds; memcpy(k.vm, S.st_vmask, 8); memcpy(k.wm, S.st_wmask, 8); k.hd = has_depth; k.hs = has_stencil;
|
||||
if (!g_dss.klen) g_dss.klen = sizeof k;
|
||||
id<MTLDepthStencilState> s = hm_get_id(&g_dss, &k);
|
||||
if (s) return s;
|
||||
@@ -3568,12 +3496,6 @@ static id<MTLDepthStencilState> dss_get(int has_depth, int has_stencil) {
|
||||
b.readMask = S.st_vmask[1]; b.writeMask = S.st_wmask[1];
|
||||
d.frontFaceStencil = f; d.backFaceStencil = b;
|
||||
}
|
||||
if (has_stencil && k.ovr) {
|
||||
MTLStencilDescriptor *f = [MTLStencilDescriptor new];
|
||||
if (k.ovr == 1) { f.stencilCompareFunction = MTLCompareFunctionNotEqual; f.readMask = 0xD0; f.writeMask = 0; } // Reference value 0: passes only if (s & 0xD0) != 0 (unit 0x10, effect 0x40, extrapolation hole / screen edge 0x80)
|
||||
else { f.stencilCompareFunction = MTLCompareFunctionAlways; f.depthStencilPassOperation = MTLStencilOperationReplace; f.readMask = 0; f.writeMask = 0x40; } // Reference value 0x40
|
||||
d.frontFaceStencil = f; d.backFaceStencil = f;
|
||||
}
|
||||
s = [g_dev newDepthStencilStateWithDescriptor:d];
|
||||
hm_put_id(&g_dss, &k, s);
|
||||
return s;
|
||||
@@ -3650,22 +3572,6 @@ static id<MTLBuffer> support_buf(void) {
|
||||
static uint64_t support_gpu(void) { static uint64_t gpu; if (!gpu) gpu = support_buf().gpuAddress; return gpu; }
|
||||
static id<MTLBuffer> zero_bank(void) { static __strong id<MTLBuffer> b; if (!b) { b = [g_dev newBufferWithLength:65536 options:MTLResourceStorageModeShared]; res_add(b); } return b; }
|
||||
static uint64_t zero_bank_gpu(void) { static uint64_t gpu; if (!gpu) gpu = zero_bank().gpuAddress; return gpu; }
|
||||
// Shift all UI draws down as a whole (iPad true fullscreen, the UI rect does not touch the top edge of the picture): add a clip-space vertical offset to the projection matrix at the start of the vertex stage's cb0, and bind a modified copy instead.
|
||||
// Moving the viewport is not used: the viewport's top edge is the edge of the clip volume, so top-anchored widgets that the host lifts back to the real top of the picture (UI coordinate y negative) would be clipped away entirely.
|
||||
// g_ui_moff is set per draw by draw() (0 = no change); the copies live in a ring buffer, and the same address in the same pass is copied only once.
|
||||
static double g_ui_moff;
|
||||
static int ui_mat_ok(void) { const p_bind_buf *u = &S.ubo[0][0]; void *mb; uint64_t mo; return u->addr && u->size >= 64 && u->size <= 4096 && !pend_hits(u->addr, u->size) && mtl_addr(u->addr, u->size, &mb, &mo); }
|
||||
static uint64_t ui_mat_patch(const p_bind_buf *u, void *mb, uint64_t mo) {
|
||||
enum { RING = 4 << 20 }; static __strong id<MTLBuffer> ring; static uint64_t ring_gpu, la, ls, lp, lg; static size_t at; static double lo;
|
||||
if (!ring) { ring = [g_dev newBufferWithLength:RING options:MTLResourceStorageModeShared]; res_add(ring); ring_gpu = ring.gpuAddress; }
|
||||
if (la == u->addr && ls == u->size && lp == g_pass.serial && lo == g_ui_moff) return lg;
|
||||
const uint8_t *src = [(__bridge id<MTLBuffer>)mb contents]; if (!src || u->size < 64 || u->size > 4096) return 0;
|
||||
size_t n = (u->size + 255) & ~(size_t)255; if (at + n > RING) at = 0;
|
||||
uint8_t *dst = (uint8_t *)ring.contents + at; memcpy(dst, src + mo, u->size);
|
||||
float m[16], o = (float)g_ui_moff; memcpy(m, dst, 64); m[1] += o * m[3]; m[5] += o * m[7]; m[9] += o * m[11]; m[13] += o * m[15]; memcpy(dst, m, 64);
|
||||
la = u->addr; ls = u->size; lp = g_pass.serial; lo = g_ui_moff; lg = ring_gpu + at; at += n;
|
||||
return lg;
|
||||
}
|
||||
// Bindless handle (TEXU / SAMPU): the 32-bit word at byte offset off of uniform slot reg = the low 32 bits of the NVN handle the game wrote there (low 20 bits texture ID, high 12 bits sampler ID).
|
||||
// Read on the CPU side at replay time (the memory at the same moment a real GPU would read it at execution); slot unbound / out of range / unreadable = 0 (empty slot: dummy texture / dummy sampler).
|
||||
// A location with a GPU-side write queued in this submit (pend_hits) is still read at its current value; the first case is reported red.
|
||||
@@ -3711,11 +3617,9 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i
|
||||
const p_bind_buf *u = reg < 16 ? &S.ubo[nvn_stage][reg] : NULL;
|
||||
void *mb; uint64_t mo, gpu;
|
||||
if (!u || !u->addr || !mtl_gpu_addr(u->addr, 1, &mb, &mo, &gpu)) { RED_ONCE(16, "stage %d cb%d has no uniform buffer bound", nvn_stage, reg); continue; }
|
||||
use_rec("buf", u->addr, u->size, 0, NULL);
|
||||
if (g_nubd && ubd_hits(u->addr, u->addr + u->size)) { // inline updates deferred in this pass (see mtl_sub_update); ubd_conflict made sure they cover the binding
|
||||
if (ubd_covers(u->addr, u->addr + u->size)) gpu = ubd_shadow(u->addr, u->addr + u->size);
|
||||
else RED_ONCE(92, "a uniform update deferred inside the pass covers only part of the binding (stage %d cb%d): the content before the update is read", nvn_stage, reg); }
|
||||
if (nvn_stage == 0 && reg == 0 && g_ui_moff != 0) { uint64_t pg = ui_mat_patch(u, mb, mo); if (pg) gpu = pg; }
|
||||
if (bl) {
|
||||
PD("B %llu st%d cb reg%d addr=%#llx size=%llu pool=%#llx+%#llx f=%g,%g,%g,%g", g_stat_pass, nvn_stage, reg, (unsigned long long)u->addr, (unsigned long long)u->size,
|
||||
(unsigned long long)(u->addr - mo), (unsigned long long)mo, ((float *)u->addr)[0], ((float *)u->addr)[1], ((float *)u->addr)[2], ((float *)u->addr)[3]);
|
||||
@@ -3742,7 +3646,6 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i
|
||||
int tid = b->kind == NVN_RB_TEXU ? (int)(ubo_handle(nvn_stage, reg, b->off) & 0xfffff) : reg < NTEX ? (int)(S.tex[nvn_stage][reg] & 0xfffff) : 0;
|
||||
void *tv = NULL; uint64_t trid = tid ? tex_fast_rid(tid, &tv) : 0; __unsafe_unretained id<MTLTexture> t = (__bridge id<MTLTexture>)tv;
|
||||
if (t && g_fb_n && g_fb_seq == g_draw_seq && nvn_stage == 1) for (int q = 0; q < g_fb_n; q++) if (t == g_fb_src[q]) { t = g_fb_use[q]; trid = g_fb_rid[q]; break; } // same-pass feedback sampling: read the snapshot instead (see fb_prepare)
|
||||
if (t && tid < g_ntreg) use_rec("tex", (uint64_t)g_treg[tid].t.mem, g_treg[tid].t.L.size, tid, &g_treg[tid].t);
|
||||
if (t && passdump() && tid < g_ntreg) PD("R %llu st%d ser=%llu", g_stat_pass, nvn_stage, g_treg[tid].t.serial);
|
||||
if (bl) PD("B %llu st%d tex reg%d tid=%d %s ser=%llu fmt=%lu %lux%lux%lu view_lv=%lu+%lu view_sl=%lu array=%lu target=%lu swz=%d%d%d%d mtl=%p", g_stat_pass, nvn_stage, reg, tid, t ? "real" : "dummy", t && tid < g_ntreg ? g_treg[tid].t.serial : 0ull,
|
||||
(unsigned long)t.pixelFormat, (unsigned long)t.width, (unsigned long)t.height, (unsigned long)t.depth,
|
||||
@@ -3795,7 +3698,6 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i
|
||||
if (t && t.textureType != (MTLTextureType)b->dtt) { uint64_t r2 = 0; id<MTLTexture> v = tex_fit(tid, t, (MTLTextureType)b->dtt, &r2); t = v; trid = v ? r2 : 0; }
|
||||
if (!t) { t = dummy_tex(b->dtt, b->dpf); trid = g_dummy_tex_rid[b->dtt & 7][b->dpf & 3]; }
|
||||
}
|
||||
if (tv && tid < g_ntreg) use_rec("tex", (uint64_t)g_treg[tid].t.mem, g_treg[tid].t.L.size, tid, &g_treg[tid].t);
|
||||
if (bl && tid) PD("B %llu st%d texd [%d] tid=%d %s", g_stat_pass, nvn_stage, k, tid, tv ? "real" : "dummy");
|
||||
if (tv && trid && trid != g_dummy_tbuf_rid[b->dpf & 3] && trid != g_dummy_tex_rid[b->dtt & 7][b->dpf & 3]) real |= 1ull << k;
|
||||
slots[aid + k] = trid;
|
||||
@@ -3814,13 +3716,13 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i
|
||||
} else if (b->kind == NVN_RB_SBUFP || b->kind == NVN_RB_UBOP) { // B: storage buffers / compute-stage uniforms are device pointers; the address is taken by NVN slot
|
||||
const p_bind_buf *u = reg < 16 ? (b->kind == NVN_RB_UBOP ? &S.ubo[nvn_stage][reg] : &S.ssbo[nvn_stage][reg]) : NULL;
|
||||
void *mb; uint64_t mo, gpu;
|
||||
if (u && u->addr && mtl_gpu_addr(u->addr, u->size ? u->size : 1, &mb, &mo, &gpu)) { use_rec("buf", u->addr, u->size, 0, NULL); slots[aid] = gpu; USE((__bridge id<MTLBuffer>)mb, b->rw ? MTLResourceUsageRead | MTLResourceUsageWrite : MTLResourceUsageRead); }
|
||||
if (u && u->addr && mtl_gpu_addr(u->addr, u->size ? u->size : 1, &mb, &mo, &gpu)) { slots[aid] = gpu; USE((__bridge id<MTLBuffer>)mb, b->rw ? MTLResourceUsageRead | MTLResourceUsageWrite : MTLResourceUsageRead); }
|
||||
else { slots[aid] = dummy_buf_gpu(); USE(dummy_buf(), MTLResourceUsageRead); }
|
||||
if (bl) PD("B %llu st%d sbufp reg%d %s addr=%#llx size=%llu %s", g_stat_pass, nvn_stage, reg, u && u->addr ? "real" : "dummy", u ? (unsigned long long)u->addr : 0ull, u ? (unsigned long long)u->size : 0ull, b->rw ? "rw" : "ro");
|
||||
} else if (b->kind == NVN_RB_SBUFU) { // B: a storage buffer whose GPU address the game keeps in its own uniform buffer (Monster Hunter Rise's compute shaders: c3 + 0x38); read like a bindless handle
|
||||
uint64_t addr; ubo_read(nvn_stage, reg, b->off, 8, &addr);
|
||||
void *mb; uint64_t mo, gpu;
|
||||
if (addr && mtl_gpu_addr(addr, 1, &mb, &mo, &gpu)) { use_rec("buf", addr, 1, 0, NULL); slots[aid] = gpu; USE((__bridge id<MTLBuffer>)mb, b->rw ? MTLResourceUsageRead | MTLResourceUsageWrite : MTLResourceUsageRead); }
|
||||
if (addr && mtl_gpu_addr(addr, 1, &mb, &mo, &gpu)) { slots[aid] = gpu; USE((__bridge id<MTLBuffer>)mb, b->rw ? MTLResourceUsageRead | MTLResourceUsageWrite : MTLResourceUsageRead); }
|
||||
else { slots[aid] = dummy_buf_gpu(); USE(dummy_buf(), MTLResourceUsageRead); }
|
||||
if (bl) PD("B %llu st%d sbufu reg%d+%#x addr=%#llx %s", g_stat_pass, nvn_stage, reg, b->off, (unsigned long long)addr, b->rw ? "rw" : "ro");
|
||||
} else if (b->kind == NVN_RB_C1) { // B: the shader's own constant segment, built when the shader is fetched
|
||||
@@ -5133,121 +5035,6 @@ static int fz_p2_draw(NvProg *pr, const p_draw *dw, const p_draw_idx *di, uint8_
|
||||
if (fz_check_on()) fz_snap_take(dw, di);
|
||||
g_fz_draws++; return 1;
|
||||
}
|
||||
static void bloomq_reset(void) { memset(&g_bloomq, 0, sizeof g_bloomq); }
|
||||
static int bloomq_enabled(void) {
|
||||
if (g_bloom_q < 0) { const char *e = getenv("DM_NVN_BLOOM_Q"); g_bloom_q = !e || strcmp(e, "0"); }
|
||||
return g_bloom_q;
|
||||
}
|
||||
static int bloomq_target(uint64_t *ser, int *lv, int *sl) {
|
||||
if (!R.has_rt || R.rt.n != 1 || !R.rt.color[0]) return 0;
|
||||
rt_sub(&R.rt, 0, ser, lv, sl); return *ser != 0;
|
||||
}
|
||||
static int bloomq_make_pipeline(MTLPixelFormat fmt) {
|
||||
if (g_bloomq_pso && g_bloomq_fmt == fmt) return 1;
|
||||
static const char *src =
|
||||
"#include <metal_stdlib>\nusing namespace metal;\n"
|
||||
"struct V { float4 p [[position]]; float2 uv; };\n"
|
||||
"vertex V vs(uint id [[vertex_id]]) { float2 uv=float2((id<<1)&2,id&2); V o; o.p=float4(uv*float2(2,-2)+float2(-1,1),0,1); o.uv=uv; return o; }\n"
|
||||
"struct P { float threshold; float coeff_g; float coeff_b; float dm; float sh; };\n"
|
||||
"fragment float4 fs(V in [[stage_in]], texture2d<float> t [[texture(0)]], sampler s [[sampler(0)]], constant P& p [[buffer(0)]]) { float2 d=float2(1.0/p.dm,1.0/p.sh); float3 c=0; for (int y=-1;y<=1;y+=2) for (int x=-1;x<=1;x+=2) { float3 v=t.sample(s,in.uv+float2(x,y)*d).rgb; float l=v.r*0.2126+v.g*p.coeff_g+v.b*p.coeff_b; if(l>=p.threshold) c+=v/(1.0+l); } return float4(c*0.25,1); }\n";
|
||||
NSError *e = nil; id<MTLLibrary> lib = [g_dev newLibraryWithSource:@(src) options:nil error:&e];
|
||||
if (!lib) { MLOG("[RED] bloom 1/4 shader compile failed: %s", e.localizedDescription.UTF8String); return 0; }
|
||||
MTLRenderPipelineDescriptor *pd = [MTLRenderPipelineDescriptor new]; pd.vertexFunction = [lib newFunctionWithName:@"vs"]; pd.fragmentFunction = [lib newFunctionWithName:@"fs"]; pd.colorAttachments[0].pixelFormat = fmt;
|
||||
g_bloomq_pso = [g_dev newRenderPipelineStateWithDescriptor:pd error:&e];
|
||||
if (!g_bloomq_pso) { MLOG("[RED] bloom 1/4 pipeline failed: %s", e.localizedDescription.UTF8String); return 0; }
|
||||
g_bloomq_fmt = fmt;
|
||||
MTLSamplerDescriptor *sd = [MTLSamplerDescriptor new]; sd.minFilter = sd.magFilter = MTLSamplerMinMagFilterLinear; sd.sAddressMode = sd.tAddressMode = MTLSamplerAddressModeClampToEdge; sd.supportArgumentBuffers = YES;
|
||||
g_bloomq_samp = [g_dev newSamplerStateWithDescriptor:sd]; return g_bloomq_samp != nil;
|
||||
}
|
||||
static int bloomq_capture(void) {
|
||||
uint64_t ser; int lv, sl; if (!bloomq_target(&ser, &lv, &sl) || !g_bloomq.s_tex) return 0;
|
||||
id<MTLTexture> base = tex_mtl(R.rt.color[0]), src = tex_mtl(g_bloomq.s_tex); if (!base || !src) return 0;
|
||||
id<MTLTexture> dst = base; if (lv || sl) { static id<MTLTexture> vb; static int vl, vs; if (vb != base || vl != lv || vs != sl) { g_bloomq_view = [base newTextureViewWithPixelFormat:base.pixelFormat textureType:base.textureType levels:NSMakeRange(lv, 1) slices:NSMakeRange(sl, 1)]; if (g_bloomq_view) res_add(g_bloomq_view); vb = base; vl = lv; vs = sl; } dst = g_bloomq_view; }
|
||||
if (!dst || !bloomq_make_pipeline(dst.pixelFormat)) return 0;
|
||||
rp_end();
|
||||
MTL4RenderPassDescriptor *rp = [MTL4RenderPassDescriptor new]; rp.colorAttachments[0].texture = dst; rp.colorAttachments[0].loadAction = MTLLoadActionDontCare; rp.colorAttachments[0].storeAction = MTLStoreActionStore;
|
||||
id<MTL4RenderCommandEncoder> en = [g_cb renderCommandEncoderWithDescriptor:rp]; if (!en) return 0;
|
||||
[en barrierAfterQueueStages:MTLStageAll beforeStages:MTLStageAll visibilityOptions:MTL4VisibilityOptionDevice]; [en setArgumentTable:g_bloomq_args atStages:MTLRenderStageFragment];
|
||||
[g_bloomq_args setTexture:src.gpuResourceID atIndex:0]; [g_bloomq_args setSamplerState:g_bloomq_samp.gpuResourceID atIndex:0];
|
||||
float *p = g_bloomq_const[g_bloomq_i].contents; p[0] = g_bloomq.threshold; p[1] = g_bloomq.coeff_g; p[2] = g_bloomq.coeff_b; p[3] = src.width; p[4] = src.height; [g_bloomq_args setAddress:g_bloomq_const[g_bloomq_i].gpuAddress atIndex:0];
|
||||
[en setViewport:(MTLViewport){0,0,dst.width,dst.height,0,1}]; [en setRenderPipelineState:g_bloomq_pso]; [en drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; m4_end(en);
|
||||
g_bloomq_i = (g_bloomq_i + 1) % 3; args_dirty_all(); g_pso_dirty = g_dss_dirty = 1; return 1;
|
||||
}
|
||||
// ---------- Drag-follow image for shop items: items on a vendor's shelf have no "picked up" state, so during a drag the replay layer takes "the draw that draws this item's icon", translates it to the finger at the end of the same pass and draws it once more ----------
|
||||
// In the UI pass each rectangle is one 4-vertex draw (20-byte vertices: position 3 floats + texture coordinates 2 floats; the position goes to clip coordinates through the matrix in vertex constant 0).
|
||||
// The front end (front_ios.m) gives the point where the finger pressed, the shelf grid and the center of the drag-follow image (normalized, top-left origin); here, in the UI pass, find the rectangle that "covers this point, has all four edges on grid lines, and has the smallest area" = that item
|
||||
// (identifying by the point alone gets it wrong: about 0.1 seconds after the press the game draws a one-cell-sized rectangle centered on the press point, not on grid lines -- on a real device it was twice taken for the item)
|
||||
// (does not depend on the game's selected state: selection has to wait about 0.1 seconds for the controller cursor to land on the item, and a quick drag that starts on press cannot wait for that). The same rectangle has two draws, cell background and icon:
|
||||
// the cell background uses a common texture (many rectangles in this pass use it), while each item has its own icon texture -- take the draw with the texture "used the fewest times in this pass". The recognized rectangle is reported back to the front end (used to cover the original shelf position with a dark color).
|
||||
// Vertical position and height of the UI rectangle in the real picture (both as a fraction of the picture height: y = 0 touches the top, h = 1 is the whole picture); registered by the front end and read fresh on every draw (changes when switching between docked / handheld mode).
|
||||
// The game draws the UI into the upper h portion of the UI texture (the viewport is the whole texture, and the projection matrix carries the scaling).
|
||||
static void (*g_ui_rect_fn)(double *y, double *h);
|
||||
void mtl_set_ui_rect_fn(void (*fn)(double *y, double *h)) { g_ui_rect_fn = fn; }
|
||||
static double ui_hfrac(void) { double y = 0, h = 1; if (g_ui_rect_fn) g_ui_rect_fn(&y, &h); return h > 0 && h < 1 ? h : 1; }
|
||||
static _Atomic int g_ghost_on; static _Atomic uint64_t g_ghost_pt, g_ghost_at, g_ghost_found, g_ghost_grid;
|
||||
void mtl_ui_ghost(int on, float px, float py, const float grid[4], float cx, float cy) {
|
||||
if (on) { uint64_t gv = 0; for (int i = 0; i < 4; i++) gv |= (uint64_t)(uint16_t)(fminf(fmaxf(grid[i], 0), 1) * 65535 + .5f) << (16 * i); atomic_store(&g_ghost_grid, gv);
|
||||
atomic_store(&g_ghost_pt, (uint64_t)(uint32_t)lroundf(fminf(fmaxf(px, 0), 1) * 65535) | (uint64_t)(uint32_t)lroundf(fminf(fmaxf(py, 0), 1) * 65535) << 32);
|
||||
atomic_store(&g_ghost_at, (uint64_t)(uint32_t)(int32_t)lroundf(cx * 65535) | (uint64_t)(uint32_t)(int32_t)lroundf(cy * 65535) << 32); }
|
||||
else atomic_store(&g_ghost_found, 0);
|
||||
atomic_store(&g_ghost_on, on);
|
||||
}
|
||||
int mtl_ui_ghost_rect(float r[4]) { uint64_t v = atomic_load(&g_ghost_found); if (!v) return 0; for (int i = 0; i < 4; i++) r[i] = (float)((v >> (16 * i)) & 0xffff) / 65535.f; return 1; }
|
||||
static struct { uint64_t pass; int n, nt, busy; float area, r[4]; struct { void *S_; p_draw dw; uint64_t tex; } c[4]; uint64_t tid[128]; uint16_t tn[128]; } g_gh;
|
||||
static uint64_t ghost_tex(void) { // The texture actually sampled by this draw's fragment shader (slot taken from shader reflection; the other slots in S.tex are left over from earlier draws)
|
||||
__unsafe_unretained NvProg *pr = S.prog ? prog_get(S.prog) : nil; NvShader *f = pr ? pr->sh[1] : nil; if (!f) return 0;
|
||||
for (int i = 0; i < f->rf.nb; i++) if ((f->rf.b[i].kind == NVN_RB_TEX || f->rf.b[i].kind == NVN_RB_SAMP) && f->rf.b[i].reg < NTEX && S.tex[1][f->rf.b[i].reg]) return S.tex[1][f->rf.b[i].reg];
|
||||
return 0; }
|
||||
// Whether the current UI draw is "untextured and exactly covering the whole UI rectangle" (the UI rectangle occupies the upper hf of the texture)
|
||||
static int ui_cover_all(double hf) {
|
||||
if (S.vbuf[0].size != 80 || ghost_tex()) return 0;
|
||||
const p_bind_buf *u = &S.ubo[0][0]; void *vb, *mb; uint64_t vo, mo; const uint8_t *vp_, *mp_;
|
||||
if (!u->addr || u->size < 64 || pend_hits(u->addr, 64) || pend_hits(S.vbuf[0].address, 80)) return 0;
|
||||
if (!mtl_addr(S.vbuf[0].address, 80, &vb, &vo) || !(vp_ = [(__bridge id<MTLBuffer>)vb contents]) || !mtl_addr(u->addr, 64, &mb, &mo) || !(mp_ = [(__bridge id<MTLBuffer>)mb contents])) return 0;
|
||||
float m[16], x0 = 9, y0 = 9, x1 = -9, y1 = -9; memcpy(m, mp_ + mo, 64);
|
||||
for (int i = 0; i < 4; i++) { float f[2]; memcpy(f, vp_ + vo + 20 * i, 8); float nx = (m[0] * f[0] + m[4] * f[1] + m[12] + 1) / 2, ny = (m[1] * f[0] + m[5] * f[1] + m[13] + 1) / 2;
|
||||
x0 = fminf(x0, nx); x1 = fmaxf(x1, nx); y0 = fminf(y0, ny); y1 = fmaxf(y1, ny); }
|
||||
return x0 < 0.002f && x1 > 0.998f && y0 < 0.002f && fabsf(y1 - (float)hf) < 0.002f;
|
||||
}
|
||||
static void ghost_note(const p_draw *dw) {
|
||||
if (g_gh.busy || dw->count != 4 || S.nattr != 2 || S.stream[0].stride != 20 || S.vbuf[0].size != 80) return;
|
||||
if (g_gh.pass != g_stat_pass) { for (int i = 0; i < g_gh.n; i++) free(g_gh.c[i].S_); g_gh.n = g_gh.nt = 0; g_gh.pass = g_stat_pass; }
|
||||
uint64_t t = ghost_tex(); int k = 0; for (; k < g_gh.nt; k++) if (g_gh.tid[k] == t) break;
|
||||
if (k == g_gh.nt && g_gh.nt < 128) { g_gh.tid[k] = t; g_gh.tn[k] = 0; g_gh.nt++; } if (k < g_gh.nt) g_gh.tn[k]++;
|
||||
const p_bind_buf *u = &S.ubo[0][0]; void *vb, *mb; uint64_t vo, mo; const uint8_t *vp_, *mp_;
|
||||
if (!u->addr || u->size < 64 || pend_hits(u->addr, 64) || pend_hits(S.vbuf[0].address, 80)) return;
|
||||
if (!mtl_addr(S.vbuf[0].address, 80, &vb, &vo) || !(vp_ = [(__bridge id<MTLBuffer>)vb contents]) || !mtl_addr(u->addr, 64, &mb, &mo) || !(mp_ = [(__bridge id<MTLBuffer>)mb contents])) return;
|
||||
float m[16], x0 = 9, y0 = 9, x1 = -9, y1 = -9; memcpy(m, mp_ + mo, 64);
|
||||
for (int i = 0; i < 4; i++) { float f[2]; memcpy(f, vp_ + vo + 20 * i, 8); float nx = (m[0] * f[0] + m[4] * f[1] + m[12] + 1) / 2, ny = (m[1] * f[0] + m[5] * f[1] + m[13] + 1) / 2;
|
||||
x0 = fminf(x0, nx); x1 = fmaxf(x1, nx); y0 = fminf(y0, ny); y1 = fmaxf(y1, ny); }
|
||||
{ float hf = (float)ui_hfrac(); y0 /= hf; y1 /= hf; } // The points given by the front end are normalized coordinates within the UI rectangle; when the UI occupies only the upper hf portion of the texture, convert to the same convention (unchanged when hf = 1)
|
||||
uint64_t pv = atomic_load(&g_ghost_pt); float qx = (float)(uint32_t)pv / 65535.f, qy = (float)(pv >> 32) / 65535.f;
|
||||
if (qx < x0 || qx > x1 || qy < y0 || qy > y1) return;
|
||||
uint64_t gv = atomic_load(&g_ghost_grid); float gx = (float)(gv & 0xffff) / 65535.f, gy = (float)((gv >> 16) & 0xffff) / 65535.f, gw = (float)((gv >> 32) & 0xffff) / 65535.f, gh = (float)(gv >> 48) / 65535.f;
|
||||
if (gw <= 0 || gh <= 0) return;
|
||||
float w = x1 - x0, h = y1 - y0, area = w * h, c0 = (x0 - gx) / gw, r0 = (y0 - gy) / gh, cn = w / gw, rn = h / gh; // Which cell the top-left corner is in, and how many cells it occupies
|
||||
if (fabsf(c0 - roundf(c0)) > 0.15f || fabsf(r0 - roundf(r0)) > 0.15f || fabsf(cn - roundf(cn)) > 0.15f || fabsf(rn - roundf(rn)) > 0.15f || cn < 0.85f || rn < 0.85f || cn > 2.15f || rn > 4.15f) return; // One item is at most 2x4 cells
|
||||
if (g_gh.n && area > g_gh.area * 1.03f) return; // A smaller one already exists
|
||||
if (g_gh.n && area < g_gh.area * 0.97f) { for (int i = 0; i < g_gh.n; i++) free(g_gh.c[i].S_); g_gh.n = 0; } // Found a smaller one: the earlier ones are invalidated
|
||||
if (g_gh.n == 4) { free(g_gh.c[0].S_); memmove(&g_gh.c[0], &g_gh.c[1], 3 * sizeof g_gh.c[0]); g_gh.n = 3; }
|
||||
g_gh.area = area; g_gh.r[0] = x0; g_gh.r[1] = y0; g_gh.r[2] = x1; g_gh.r[3] = y1;
|
||||
typeof(g_gh.c[0]) *c = &g_gh.c[g_gh.n++]; c->S_ = malloc(sizeof S); memcpy(c->S_, &S, sizeof S); c->dw = *dw; c->tex = t;
|
||||
}
|
||||
static void ghost_flush(void) { // Before the render pass ends: translate the recognized icon draw to the finger and draw it once more (moves the viewport, does not change vertices; the scissor is opened up)
|
||||
if (g_gh.busy || !g_gh.n) return;
|
||||
if (g_gh.pass != g_stat_pass || !atomic_load(&g_ghost_on)) { for (int i = 0; i < g_gh.n; i++) free(g_gh.c[i].S_); g_gh.n = 0; return; }
|
||||
int best = -1, bn = 1 << 30; for (int i = 0; i < g_gh.n; i++) { int cnt = 0; for (int k = 0; k < g_gh.nt; k++) if (g_gh.tid[k] == g_gh.c[i].tex) cnt = g_gh.tn[k]; if (cnt < bn) { bn = cnt; best = i; } }
|
||||
typeof(g_gh.c[0]) *c = &g_gh.c[best]; uint64_t at = atomic_load(&g_ghost_at); float ax = (float)(int32_t)(uint32_t)at / 65535.f, ay = (float)(int32_t)(uint32_t)(at >> 32) / 65535.f;
|
||||
float W = (float)g_pass.tw, H = (float)g_pass.th, ccx = (g_gh.r[0] + g_gh.r[2]) / 2, ccy = (g_gh.r[1] + g_gh.r[3]) / 2;
|
||||
{ uint64_t v = 0; for (int i = 0; i < 4; i++) v |= (uint64_t)(uint16_t)(fminf(fmaxf(g_gh.r[i], 0), 1) * 65535 + .5f) << (16 * i); atomic_store(&g_ghost_found, v ? v : 1); }
|
||||
g_gh.busy = 1; typeof(S) save; memcpy(&save, &S, sizeof S); p_scissor sc = R.sc; int has_sc = R.has_sc;
|
||||
memcpy(&S, c->S_, sizeof S); if (!S.has_vp) { S.vp[0][0] = S.vp[0][1] = 0; S.vp[0][2] = W; S.vp[0][3] = H; S.has_vp = 1; }
|
||||
S.vp[0][0] += (ax - ccx) * W; S.vp[0][1] += (ay - ccy) * H * (float)ui_hfrac(); R.has_sc = 0;
|
||||
args_dirty_all(); g_pso_dirty = g_dss_dirty = 1; draw(&c->dw, NULL);
|
||||
{ static int once; if (!once++) MLOG("drag ghost image: first case pass %llu, %d candidates, took the one whose texture is used %d times in this pass", g_stat_pass, g_gh.n, bn); }
|
||||
memcpy(&S, &save, sizeof S); args_dirty_all(); R.sc = sc; R.has_sc = has_sc; g_pso_dirty = g_dss_dirty = 1; g_gh.busy = 0;
|
||||
for (int i = 0; i < g_gh.n; i++) free(g_gh.c[i].S_); g_gh.n = 0;
|
||||
}
|
||||
// Diagnostic DM_NVN_KEYDUMP=<fragment shader key prefix>[:<count, default 6>[:<frame interval>]]: watch only this class of draws (for real devices; no frame number to guess). The first N matching draws force PASSDUMP's per-binding lines on (K / B / d:
|
||||
// textures with format / view / channel swizzle, samplers with LOD clamps), plus [kd] lines: vertex attribute formats, each vertex stream (first 4 vertices of per-vertex streams, all instances of per-instance streams) and every value of each uniform buffer of both stages.
|
||||
// Run the same switch once on the Mac and once on the device and compare item by item to see which input differs on the device (Super Mario 3D World + Bowser's Fury's magenta squares in 1-1: particle 5e4de7561c670fee, iPhone only, not reproducible on the Mac)
|
||||
@@ -5686,10 +5473,6 @@ static void draw(const p_draw *dw, const p_draw_idx *di) {
|
||||
if (!S.prog) { RED_ONCE(18, "no program bound before the draw"); PD("Z %llu draw skipped: no program bound", g_stat_pass + !g_enc); return; }
|
||||
__unsafe_unretained NvProg *pr = prog_get(S.prog);
|
||||
if (g_nubd && g_enc && ubd_conflict(pr, dw, di)) { g_ubd_conflicts++; rp_end(); } // land the deferred inline updates before this draw (see ubd_conflict)
|
||||
if (g_ruler_truth == 0) { // Ruler negative control (DM_NVN_RULER_NEG=<fragment key>): in the mode under test drop this shader's draws, and the ruler must report non-equivalence
|
||||
static const char *neg; static int ng = -1; if (ng < 0) { neg = getenv("DM_NVN_RULER_NEG"); ng = neg && *neg; }
|
||||
if (ng && pr) for (int st = 0; st < 5; st++) { __unsafe_unretained NvShader *sh = pr->sh[st]; if (sh && sh->stage == 1 && !strncmp(sh->key, neg, strlen(neg))) { static int once; if (!once++) MLOG("DM_NVN_RULER_NEG: the mode under test drops the draws of shader %s", sh->key); return; } }
|
||||
}
|
||||
if (zskip_can_draw(pr)) { // Zero-source skip: the output is always 0, writing 0 over 0, so this draw changes no result (see zskip_can_draw)
|
||||
g_zskip_count++; { static int once; if (!once++) MLOG("zero-source skip: first case pass %llu", g_stat_pass + 1); }
|
||||
PD("N %llu zero-source draw skipped", g_stat_pass + 1); return; }
|
||||
@@ -5711,51 +5494,6 @@ static void draw(const p_draw *dw, const p_draw_idx *di) {
|
||||
{ static int small = -1; if (small < 0) { const char *e = getenv("DM_NVN_SKIP_SMALL"); small = e ? atoi(e) : 0; if (small) MLOG("DM_NVN_SKIP_SMALL: dropping draws whose target width is ≤ %d (diagnostic; the picture is wrong)", small); }
|
||||
if (small > 0 && R.has_rt && !g_defer_replaying) { int k = R.rt.n && R.rt.color[0] ? 0 : R.rt.depth ? 8 : -1;
|
||||
if (k >= 0) { uint64_t s_; int lv, sl; rt_sub(&R.rt, k, &s_, &lv, &sl); const tex_t *t = k == 8 ? R.rt.depth : R.rt.color[0]; if ((int)MAX(t->d.width >> lv, 1u) <= small) return; } } }
|
||||
// Experimental switch DM_NVN_NOFXAA=1 (off by default; keep or drop decided by the visual ruler + looking at the picture on a real machine): skips the FXAA full-screen draw, and the later sharpening / upscale reads FXAA's input instead (the tone mapping result)
|
||||
{ static int nofxaa = -1; static uint64_t fx_from, fx_to; if (nofxaa < 0) { const char *e = getenv("DM_NVN_NOFXAA"); nofxaa = e && !strcmp(e, "1"); if (nofxaa) MLOG("DM_NVN_NOFXAA=1: skipping FXAA"); }
|
||||
if (nofxaa && tbdr_on() && !g_defer_replaying && pr->sh[1]) { NvShader *f = pr->sh[1];
|
||||
if (!strncmp(f->key, "ebcfcd1fa0a77dbc", 16) && !g_enc && R.rt.n == 1 && R.rt.color[0] && !R.clear_c[0]) {
|
||||
uint64_t h = 0; for (int i = 0; i < f->rf.nb && !h; i++) if (f->rf.b[i].kind == NVN_RB_TEX && f->rf.b[i].reg < NTEX) h = S.tex[1][f->rf.b[i].reg];
|
||||
if (h) { fx_from = R.rt.color[0]->serial; fx_to = h; PD("N %llu FXAA skipped: readers of ser=%llu read ser=%llu instead", g_stat_pass, (unsigned long long)fx_from, (unsigned long long)h_ser(h)); return; } }
|
||||
else if (fx_from && !strncmp(f->key, "52aac1f2306b8fc5", 16)) { for (int r = 0; r < NTEX; r++) if (S.tex[1][r] && h_ser(S.tex[1][r]) == fx_from) S.tex[1][r] = fx_to; fx_from = 0; } } }
|
||||
{ static uint64_t bloomq_frame; if (bloomq_frame != atomic_load(&g_cur_frame)) { bloomq_frame = atomic_load(&g_cur_frame); bloomq_reset(); }
|
||||
if (bloomq_enabled() && tbdr_on() && !g_defer_replaying && pr->sh[1]) {
|
||||
NvShader *f = pr->sh[1]; uint64_t tgt = 0; int lv, sl;
|
||||
if (!strncmp(f->key, "2938a5b71b8e21b2", 16)) {
|
||||
if (g_bloomq.state) bloomq_reset();
|
||||
if (bloomq_target(&tgt, &lv, &sl) && R.rt.color[0]) {
|
||||
uint64_t h = 0; const tex_t *st = NULL; int cbuf = 0;
|
||||
for (int i = 0; i < f->rf.nb; i++) { nvn_rbind *b = &f->rf.b[i];
|
||||
if (b->kind == NVN_RB_TEX && b->reg < NTEX && !h) { h = S.tex[1][b->reg]; int tid = (int)(h & 0xfffff); if (tid > 0 && tid < g_ntreg && g_treg[tid].ok) st = &g_treg[tid].t; }
|
||||
if (b->kind == NVN_RB_SAMPT && b->reg < NTEX) g_bloomq.samp_h = S.tex[1][b->reg] >> 20;
|
||||
if (b->kind == NVN_RB_SAMP && b->reg < 128) g_bloomq.samp_h = S.samp[1][b->reg] >> 20;
|
||||
if (b->kind == NVN_RB_CBUF && !cbuf) cbuf = ubo_float(1, (uint16_t)(b->reg << 8), &g_bloomq.threshold); // c7[0].x
|
||||
}
|
||||
if (h && st && cbuf && f->rf.c1_len >= sizeof(float) * 2) {
|
||||
float c1[2]; memcpy(c1, f->rf.c1, sizeof c1); g_bloomq.s_tex = st; g_bloomq.s_h = h; g_bloomq.s_ser = st->serial; g_bloomq.b0_ser = tgt; g_bloomq.coeff_g = c1[0]; g_bloomq.coeff_b = c1[1]; g_bloomq.state = 1;
|
||||
{ static int once; if (!once++) MLOG("bloom 1/4 start: active (S ser=%llu B0 ser=%llu)", (unsigned long long)g_bloomq.s_ser, (unsigned long long)g_bloomq.b0_ser); } PD("A %llu bloom 1/4 start S=%llu B0=%llu", g_stat_pass, (unsigned long long)g_bloomq.s_ser, (unsigned long long)g_bloomq.b0_ser); R.clear_c[0] = 0; // The whole B0 level is no longer needed: drop the pending clear too, otherwise a clear-only render pass would be opened just for it
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (g_bloomq.state == 1 && !strncmp(f->key, "019ce2f82edbda84", 16)) {
|
||||
uint64_t src = 0; for (int i = 0; i < f->rf.nb; i++) if (f->rf.b[i].kind == NVN_RB_TEX && f->rf.b[i].reg < NTEX) { src = h_ser(S.tex[1][f->rf.b[i].reg]); break; }
|
||||
if (src == g_bloomq.b0_ser && bloomq_target(&tgt, &lv, &sl) && bloomq_capture()) { R.clear_c[0] = 0; g_bloomq.b1_ser = tgt; g_bloomq.state = 2; PD("B %llu bloom 1/4 native capture B0=%llu B1=%llu", g_stat_pass, (unsigned long long)g_bloomq.b0_ser, (unsigned long long)g_bloomq.b1_ser); return; }
|
||||
RED_ONCE(62, "bloom 1/4 start: the capture was skipped but the native capture did not draw; this frame lacks bloom"); bloomq_reset();
|
||||
} else if (g_bloomq.state == 2 && !strncmp(f->key, "019ce2f82edbda84", 16)) {
|
||||
for (int i = 0; i < f->rf.nb; i++) if (f->rf.b[i].kind == NVN_RB_TEX && f->rf.b[i].reg < NTEX && h_ser(S.tex[1][f->rf.b[i].reg]) == g_bloomq.b1_ser) { g_bloomq.b1_h = S.tex[1][f->rf.b[i].reg]; break; }
|
||||
} else if (g_bloomq.state == 2 && !strncmp(f->key, "cc264583f2000c81", 16)) {
|
||||
if (bloomq_target(&tgt, &lv, &sl) && tgt == g_bloomq.b0_ser) { g_bloomq.state = 3; PD("C %llu bloom 1/4 skips the B0 upsample", g_stat_pass); return; }
|
||||
} else if (g_bloomq.state == 3 && !strncmp(f->key, "2e2d6700a2569795", 16)) {
|
||||
if (g_bloomq.b1_h) { for (int r = 0; r < NTEX; r++) if (S.tex[1][r] && h_ser(S.tex[1][r]) == g_bloomq.b0_ser) S.tex[1][r] = g_bloomq.b1_h; PD("D %llu bloom 1/4 composite reads B1 instead", g_stat_pass); g_bloomq_last_b0 = g_bloomq.b0_ser; g_bloomq_last_b1h = g_bloomq.b1_h; g_bloomq_last_b1s = g_bloomq.b1_ser; bloomq_reset(); }
|
||||
else { RED_ONCE(63, "bloom 1/4 state lacks the B1 handle"); bloomq_reset(); }
|
||||
} else if (!g_bloomq.state && g_bloomq_last_b1h && h_ser(g_bloomq_last_b1h) == g_bloomq_last_b1s && !strncmp(f->key, "2e2d6700a2569795", 16)) { // The B1 handle still points to the same texture (not recognized after a rebuild on resolution change)
|
||||
// The bloom overlay is reached without the bloom chain having run this frame (the extrapolated frame of runtime extrapolation skipped it: outside the region the input 469 is the previous frame's LDR, so it cannot run): redirect the read to B1 as last time, overlaying the previous frame's bloom.
|
||||
// Without the redirect it would read B0 -- with bloom 1/4 on, B0's upsample is always skipped and never written, so what is read is uninitialized video memory (on M4 Pro the units in the region turn entirely green; elsewhere it is mostly 0, i.e. missing bloom)
|
||||
int hit = 0; for (int r = 0; r < NTEX; r++) if (S.tex[1][r] && h_ser(S.tex[1][r]) == g_bloomq_last_b0) { S.tex[1][r] = g_bloomq_last_b1h; hit = 1; }
|
||||
if (hit) { PD("D %llu bloom chain did not run: composite reuses the previous frame's B1", g_stat_pass); static uint64_t n; if (n++ % 600 == 0) MLOG("bloom 1/4: a frame without the bloom chain; the composite reuses the previous frame's B1 (#%llu)", (unsigned long long)n); }
|
||||
} else if (g_bloomq.state) { RED_ONCE(61, "bloom 1/4 start: another draw came in the middle of the chain; this frame's bloom is incomplete"); bloomq_reset(); }
|
||||
}
|
||||
}
|
||||
if (S.poly.cull == NVN_FACE_FRONT_AND_BACK) { PD("Z %llu draw skipped (before the pass begins): both faces culled", g_stat_pass + !g_enc); return; }
|
||||
// The continued render pass may have been opened with fewer depth / stencil attachments (open modes 2 / 3 / 4): if the plane this draw needs is not attached, split before it (on reopen, attach per the new lookahead)
|
||||
if (g_rt_merged && g_enc_mode != 0 && (((S.ds.dt || S.ds.dw) && !g_pass.hd) || (S.ds.st && !g_pass.hs))) {
|
||||
@@ -5779,12 +5517,10 @@ static void draw(const p_draw *dw, const p_draw_idx *di) {
|
||||
if (!g_enc) rr_begin();
|
||||
#endif
|
||||
int defer_q_draw = !g_enc && !g_defer_replaying && g_defer_natq > 0; // The draw that opens Q's render pass: after the render pass is opened as usual through hoist_try / fuse_try, replay P first and then draw this one
|
||||
if (g_frz_state) goto frz_plain; // Full frame / frozen frame of world freeze: no hoisting, no deferral, no merging (the pass-skipping state machine must see every set-target)
|
||||
if (!g_enc && tbdr_on()) hoist_try(); // First hoist and execute the pure-clear render passes sandwiched between this render pass and its continuation (before rp_begin_ex, not occupying this render pass)
|
||||
// The deferral decision comes after hoist_try: hoisted pure-clear render passes are skipped in the lookahead, and only then can P's same-target continuation segment be recognized
|
||||
if (!g_enc && tbdr_on() && !defer_has_in_p() && !g_defer_replaying && !g_fz.active && !g_fz_checking && g_exec_next) { defer_try(dw, di); if (g_defer_cur->active && g_defer_cur->in_p) return; }
|
||||
if (!g_enc && tbdr_on() && !g_fz_checking) fuse_try(); // Readable tile render pass merge is on by default; when the structure / same-pixel proof does not hold it still falls back to a normal render pass, and it is not re-entered during the CHECK redraw
|
||||
frz_plain:;
|
||||
int need = g_enc || !tbdr_on() ? 3 : pass_ds_need(), mode = defer_q_draw ? 0 : need == 0 ? 2 : need == 2 ? 3 : need == 1 ? 4 : 0; // 2 attach neither, 3 attach stencil only, 4 attach depth only
|
||||
int skip_d = mode != 0;
|
||||
if (!g_enc) { g_fd_dw = dw; g_fd_di = di; }
|
||||
@@ -5810,26 +5546,16 @@ frz_plain:;
|
||||
unsigned df0 = (unsigned)S.ds.df; if (ovr) S.ds.df = 8; cur_ds = dss_get(g_pass.hd, g_pass.hs); S.ds.df = df0; cur_ds_pass = g_pass.serial; g_dss_dirty = 0; }
|
||||
else if (g_dss_dirty || cur_ds_pass != g_pass.serial) { cur_ds = dss_get(g_pass.hd, g_pass.hs); cur_ds_pass = g_pass.serial; g_dss_dirty = 0; }
|
||||
if (cur_ds != g_enc_dss) { enc_r_dss(en, cur_ds); g_enc_dss = cur_ds; }
|
||||
{ uint32_t sr = g_rt_ovr && g_pass.hs ? (g_rt_ovr == 3 ? 0x40 : 0) : S.st_ref[0]; // enc_r_stencil_ref sends the front value only
|
||||
{ uint32_t sr = S.st_ref[0]; // enc_r_stencil_ref sends the front value only
|
||||
if (!(g_ed.ok & ED_SREF) || g_ed.sref != sr) { enc_r_stencil_ref(en, sr, sr); g_ed.sref = sr; g_ed.ok |= ED_SREF; } }
|
||||
// Viewport / scissor (clamped within the target size)
|
||||
NSUInteger tw = g_pass.tw, th = g_pass.th;
|
||||
// When the UI rectangle does not touch the top edge of the picture (iPad true full screen: the UI is laid out at 16:9 at the bottom of the picture, see dm/host/dm_uiscreen.c), UI draws are shifted down as a whole: a vertical offset is added to the projection matrix (ui_mat_patch), and the scissor moves down with it.
|
||||
// A UI draw = 4 vertices, 2 attributes, 20-byte vertices, in a pass of its own and composited at end of frame (measured: startup loading, character select, character creation, entering-game loading, in-game, open panel, and full-screen map never mix in other draws).
|
||||
// When the matrix cannot be read this time (there are still unlanded writes in this submit), fall back to moving the viewport: same position, except that widgets lifted to the top get clipped by the viewport's top edge this one time.
|
||||
// A scissor starting from the top edge (y <= 0) does not have its top edge moved down: when the host lifts top-anchored widgets (minimap, area name etc.) back to the real top of the picture, they are drawn above the UI rectangle and must not be clipped.
|
||||
// Exception: the untextured block that exactly covers the whole UI rectangle (the dimming when a panel opens, the black curtain of a transition) is changed to cover the whole picture -- the viewport is scaled vertically by 1/h and not moved down, otherwise the 3D outside the UI rectangle is left with a light/dark dividing line.
|
||||
// When nobody registered the entry or the UI rectangle is the whole picture (iPhone, Mac default), vo = so = 0, vs = 1, the matrix is not changed, and what follows is word-for-word the same as before.
|
||||
long vo = 0, so = 0; double vs = 1, moff = 0;
|
||||
if (g_ui_rect_fn && dw && dw->count == 4 && S.nattr == 2 && S.stream[0].stride == 20) { double uy = 0, uh = 1; g_ui_rect_fn(&uy, &uh);
|
||||
if (uh > 0 && uh < 1) { if (ui_cover_all(uh)) vs = 1 / uh; else if (uy > 0) { so = lround(uy * th); if (ui_mat_ok()) moff = 2 * uy; else { vo = so; static int n; if (n++ < 4) MLOG("UI shift down: the projection matrix could not be read this time; moving the viewport instead (pass %llu)", g_stat_pass); } } } }
|
||||
if (moff != g_ui_moff) { g_ui_moff = moff; g_args_dirty[0] = 1; }
|
||||
{ MTLViewport v = S.has_vp ? (MTLViewport){S.vp[0][0], S.vp[0][1] + vo, S.vp[0][2], S.vp[0][3] * vs, S.dr[0], S.dr[1]} : (MTLViewport){0, vo, tw, th * vs, 0, 1};
|
||||
{ MTLViewport v = S.has_vp ? (MTLViewport){S.vp[0][0], S.vp[0][1], S.vp[0][2], S.vp[0][3], S.dr[0], S.dr[1]} : (MTLViewport){0, 0, tw, th, 0, 1};
|
||||
if (atomic_load_explicit(&g_origin_ll, memory_order_relaxed)) { v.originY += v.height; v.height = -v.height; } // lower-left origin (see g_origin_ll)
|
||||
if (!(g_ed.ok & ED_VP) || memcmp(&g_ed.vp, &v, sizeof v)) { enc_r_viewport(en, v); g_ed.vp = v; g_ed.ok |= ED_VP; } }
|
||||
MTLScissorRect sc = {0, 0, tw, th};
|
||||
if (R.has_sc) {
|
||||
long x0 = R.sc.x < 0 ? 0 : R.sc.x, y0 = R.sc.y <= 0 ? 0 : R.sc.y + so, x1 = (long)R.sc.x + R.sc.w, y1 = vs != 1 ? (long)th : (long)R.sc.y + R.sc.h + so;
|
||||
long x0 = R.sc.x < 0 ? 0 : R.sc.x, y0 = R.sc.y <= 0 ? 0 : R.sc.y, x1 = (long)R.sc.x + R.sc.w, y1 = (long)R.sc.y + R.sc.h;
|
||||
if (x1 > (long)tw) x1 = tw; if (y1 > (long)th) y1 = th;
|
||||
if (x1 <= x0 || y1 <= y0) { PD("Z %llu draw skipped: empty scissor (%d,%d %dx%d, target %lux%lu)", g_stat_pass, R.sc.x, R.sc.y, R.sc.w, R.sc.h, (unsigned long)tw, (unsigned long)th); return; }
|
||||
sc = (MTLScissorRect){(NSUInteger)x0, (NSUInteger)y0, (NSUInteger)(x1 - x0), (NSUInteger)(y1 - y0)};
|
||||
@@ -5850,7 +5576,7 @@ frz_plain:;
|
||||
if (!S.vbuf[s_].address) continue;
|
||||
void *mb; uint64_t mo;
|
||||
uint64_t gpu;
|
||||
if (mtl_gpu_addr(S.vbuf[s_].address, 1, &mb, &mo, &gpu)) { enc_r_vbuf(en, VB_SLOT(s_), gpu); use_rec("buf", S.vbuf[s_].address, S.vbuf[s_].size, 0, NULL); }
|
||||
if (mtl_gpu_addr(S.vbuf[s_].address, 1, &mb, &mo, &gpu)) enc_r_vbuf(en, VB_SLOT(s_), gpu);
|
||||
}
|
||||
static id<MTLBuffer> zero; if (!zero) { zero = [g_dev newBufferWithLength:64 options:MTLResourceStorageModeShared]; res_add(zero); }
|
||||
static uint64_t zero_gpu; if (!zero_gpu) zero_gpu = zero.gpuAddress;
|
||||
@@ -5881,9 +5607,6 @@ frz_plain:;
|
||||
}
|
||||
}
|
||||
g_stat_draw++;
|
||||
{ static int tg; if (!tg) { tg = 1; const char *e = getenv("DM_TEST_GHOST"); float v[8]; // Test switch DM_TEST_GHOST=px,py,gx,gy,gw,gh,cx,cy (normalized; press point, grid, center of the drag-follow image): turns on the drag-follow image directly without going through the front end (for Mac host screenshot verification)
|
||||
if (e && sscanf(e, "%f,%f,%f,%f,%f,%f,%f,%f", &v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &v[6], &v[7]) == 8) mtl_ui_ghost(1, v[0], v[1], &v[2], v[6], v[7]); } }
|
||||
if (dw && atomic_load(&g_ghost_on)) ghost_note(dw);
|
||||
if (enc_label_on() && !g_el_draws++ && pr->sh[1]) { memcpy(g_el_key, pr->sh[1]->key, 8); g_el_key[8] = 0; }
|
||||
if (passdump()) { // Characteristics of a render pass's first draw (to judge whether the load can be omitted: full screen, no blending, writes all channels, does not depend on depth); with DM_NVN_PASSDUMP_ALL every draw is recorded
|
||||
static uint64_t last_pass = ~0ull; static int all = -1; if (all < 0) all = getenv("DM_NVN_PASSDUMP_ALL") != NULL;
|
||||
@@ -5972,7 +5695,6 @@ frz_plain:;
|
||||
else { uint32_t q = (t >> 1) * 4, h = t & 1; d[3 * t] = IX(q); d[3 * t + 1] = IX(q + 1 + h); d[3 * t + 2] = IX(q + 2 + h); }
|
||||
}
|
||||
#undef IX
|
||||
use_rec("buf", di->ibuf, (uint64_t)di->count * isz, 0, NULL);
|
||||
enc_r_draw_indexed(en, mp, nt * 3, MTLIndexTypeUInt32, g_cur_arena->gpu + off, (NSUInteger)nt * 12, di->instances ? di->instances : 1, di->base_vertex, di->base_instance);
|
||||
if (g_rt_range_pending) { PD("j rt_level_range pass=%llu vc=%u", g_stat_pass, (unsigned)di->count); g_rt_range_pending = 0; }
|
||||
return;
|
||||
@@ -5982,7 +5704,6 @@ frz_plain:;
|
||||
if (!di->count) return;
|
||||
const wide_ent *w = wide_wanted(di) ? wide_find(di) : NULL;
|
||||
if (w && w->gpu) { // written earlier in this submission: widened on the GPU before this pass (wide_run)
|
||||
use_rec("buf", di->ibuf, (uint64_t)di->count, 0, NULL);
|
||||
enc_r_draw_indexed(en, mp, di->count, MTLIndexTypeUInt16, w->gpu, (NSUInteger)di->count * 2, di->instances ? di->instances : 1, di->base_vertex, di->base_instance);
|
||||
if (g_rt_range_pending) { PD("j rt_level_range pass=%llu vc=%u", g_stat_pass, (unsigned)di->count); g_rt_range_pending = 0; }
|
||||
return;
|
||||
@@ -5993,7 +5714,6 @@ frz_plain:;
|
||||
id<MTLBuffer> ab; size_t off = arena_alloc(((size_t)di->count * 2 + 15) & ~(size_t)15, &ab);
|
||||
uint16_t *d = (uint16_t *)((uint8_t *)ab.contents + off); src += mo;
|
||||
for (int32_t i = 0; i < di->count; i++) d[i] = src[i];
|
||||
use_rec("buf", di->ibuf, (uint64_t)di->count, 0, NULL);
|
||||
enc_r_draw_indexed(en, mp, di->count, MTLIndexTypeUInt16, g_cur_arena->gpu + off, (NSUInteger)di->count * 2, di->instances ? di->instances : 1, di->base_vertex, di->base_instance);
|
||||
if (g_rt_range_pending) { PD("j rt_level_range pass=%llu vc=%u", g_stat_pass, (unsigned)di->count); g_rt_range_pending = 0; }
|
||||
return;
|
||||
@@ -6001,7 +5721,6 @@ frz_plain:;
|
||||
void *mb; uint64_t mo;
|
||||
uint64_t gpu;
|
||||
if (!mtl_gpu_addr(di->ibuf, 1, &mb, &mo, &gpu)) { RED_ONCE(22, "index buffer is in no memory pool"); return; }
|
||||
use_rec("buf", di->ibuf, (uint64_t)di->count * (di->itype == 1 ? 2 : 4), 0, NULL);
|
||||
if (di->count) enc_r_draw_indexed(en, mp, di->count, di->itype == 1 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32, gpu, ((__bridge id<MTLBuffer>)mb).length - mo,
|
||||
di->instances ? di->instances : 1, di->base_vertex, di->base_instance);
|
||||
}
|
||||
@@ -6033,47 +5752,6 @@ static void warm_claim(void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// ---------- 3D picture brightening ----------
|
||||
// Tone mapping (fragment a1b1aab66933fa8f) = PQ-encode the composited HDR x0.008, then look up a 32^3 color lookup table (recomputed per region every frame by compute shader 552f45c348a61b86, read only by tone mapping).
|
||||
// Multiplying HDR by exposure k first is equivalent to replacing the table with L'(u) = L(PQ(k * PQ^-1(u))): after the dispatch in which the game finishes computing the table, make a copy and rewrite it in place accordingly (one small 32^3 dispatch); the table is recomputed every frame, so it does not get brighter with each multiplication.
|
||||
// Only the 3D world is touched: UI, text and the health orbs are composited after tone mapping; highlights are compressed by PQ and do not blur into a sheet of white as a final whole-screen multiply would. The normal / readable tile merge / freeze paths all just read this table, so it takes effect in one place.
|
||||
// k is given every frame by the host's dm_bright_k() (dm/host/dm_bright.c: 1 out of game, 4 for maze-type areas, 4 where the original game's picture does not follow day and night, the rest derived from the original game's ambient brightness, with smooth transitions); no rewrite when k ~= 1.
|
||||
__attribute__((weak)) float dm_bright_k(void) { return 1.0f; } // The host's dm_bright.c provides the strong definition; no brightening when graphics unit tests do not link the host
|
||||
static const char k_lut_msl[] =
|
||||
"#include <metal_stdlib>\nusing namespace metal;\n"
|
||||
"constant float m1 = 0.1593017578125, m2 = 78.84375, c1 = 0.8359375, c2 = 18.8515625, c3 = 18.6875;\n" // Bit-identical to the PQ constants in the tone mapping shader
|
||||
"static float3 pq(float3 v) { float3 p = pow(abs(v), m1); return pow((c1 + c2 * p) / (1.0 + c3 * p), m2); }\n"
|
||||
"static float3 pq_inv(float3 u) { float3 q = pow(u, 1.0 / m2); return pow(max(q - c1, 0.0) / max(c2 - c3 * q, 1e-6), 1.0 / m1); }\n"
|
||||
"kernel void lutk(texture3d<float> src [[texture(0)]], texture3d<float, access::write> dst [[texture(1)]], constant float &k [[buffer(0)]], uint3 g [[thread_position_in_grid]]) {\n"
|
||||
" uint3 n = uint3(dst.get_width(), dst.get_height(), dst.get_depth()); if (any(g >= n)) return;\n"
|
||||
" constexpr sampler ls(filter::linear, address::clamp_to_edge, coord::normalized);\n"
|
||||
" float3 u = (float3(g) + 0.5) / float3(n); float3 u2 = clamp(pq(k * pq_inv(u)), 0.0, 1.0);\n"
|
||||
" float4 b = src.sample(ls, u2), o = src.read(g);\n" // b: output after brightening by exposure k; o: this cell in the original game
|
||||
" float s = smoothstep(0.65, 1.0, dot(b.rgb, float3(0.2126, 0.7152, 0.0722)));\n" // Highlight protection: after brightening, the closer to white, the more it is pulled back to the original value (snow and skylight do not blur into a sheet of white); in dark areas s = 0 and brightening proceeds as usual
|
||||
" dst.write(float4(max(mix(b.rgb, o.rgb, s), o.rgb), b.a), g); }\n";
|
||||
static void bright_lut(NvShader *cs) {
|
||||
static int failed; static id<MTLComputePipelineState> ps; static id<MTL4ArgumentTable> at; static id<MTLTexture> tmp; static id<MTLBuffer> kb[3]; static int ki;
|
||||
if (failed || strncmp(cs->key, "552f45c348a61b86", 16)) return;
|
||||
float k = dm_bright_k(); if (!(k > 1.005f)) return; // k ~= 1 (out of game / noon): the table stays as in the original game
|
||||
if (k > 4.0f) k = 4.0f;
|
||||
int tid = 0; for (int i = 0; i < cs->rf.nb && !tid; i++) if (cs->rf.b[i].kind == NVN_RB_IMG && cs->rf.b[i].reg < 32) tid = (int)(S.img[5][cs->rf.b[i].reg] & 0xfffff);
|
||||
pthread_mutex_lock(&g_rmu); id<MTLTexture> lut = tid ? tex_by_id(tid) : nil; pthread_mutex_unlock(&g_rmu);
|
||||
if (!lut || lut.textureType != MTLTextureType3D || !(lut.usage & MTLTextureUsageShaderWrite)) { static int n; if (!n++) MLOG("[RED] 3D brightening: color LUT not recognized (texture ID %d); not brightening", tid); return; }
|
||||
if (!ps) { NSError *e = nil; id<MTLLibrary> lib = [g_dev newLibraryWithSource:@(k_lut_msl) options:nil error:&e]; id<MTLFunction> f = lib ? [lib newFunctionWithName:@"lutk"] : nil;
|
||||
ps = f ? [g_dev newComputePipelineStateWithFunction:f error:&e] : nil;
|
||||
MTL4ArgumentTableDescriptor *ad = [MTL4ArgumentTableDescriptor new]; ad.maxTextureBindCount = 2; ad.maxBufferBindCount = 1; at = [g_dev newArgumentTableWithDescriptor:ad error:&e];
|
||||
for (int i = 0; i < 3; i++) { kb[i] = [g_dev newBufferWithLength:16 options:MTLResourceStorageModeShared]; if (kb[i]) res_add(kb[i]); }
|
||||
if (!ps || !at || !kb[0] || !kb[1] || !kb[2]) { failed = 1; MLOG("[RED] 3D brightening: cannot create the compute pipeline %s (back to the original brightness)", e.localizedDescription.UTF8String); return; }
|
||||
MLOG("3D brightening: ready (color LUT %lux%lux%lu, exposure supplied each frame by dm_bright_k)", (unsigned long)lut.width, (unsigned long)lut.height, (unsigned long)lut.depth); }
|
||||
if (!tmp || tmp.width != lut.width || tmp.height != lut.height || tmp.depth != lut.depth || tmp.pixelFormat != lut.pixelFormat) {
|
||||
MTLTextureDescriptor *d = [MTLTextureDescriptor new]; d.textureType = MTLTextureType3D; d.pixelFormat = lut.pixelFormat; d.width = lut.width; d.height = lut.height; d.depth = lut.depth;
|
||||
d.usage = MTLTextureUsageShaderRead; d.storageMode = MTLStorageModePrivate; if (tmp) defer_obj_release(tmp); tmp = [g_dev newTextureWithDescriptor:d]; if (!tmp) { failed = 1; return; } res_add(tmp); }
|
||||
id<MTLBuffer> b0 = kb[ki]; ki = (ki + 1) % 3; *(float *)b0.contents = k; // Three copies in rotation: in-flight frames are still reading the previous copy
|
||||
id<MTL4ComputeCommandEncoder> b = blit(); enc_b_copy_texture(b, lut, 0, 0, MTLOriginMake(0, 0, 0), MTLSizeMake(lut.width, lut.height, lut.depth), tmp, 0, 0, MTLOriginMake(0, 0, 0)); enc_b_end(g_blit); g_blit = nil;
|
||||
id<MTL4ComputeCommandEncoder> ce = (id<MTL4ComputeCommandEncoder>)enc_c_begin(NULL); [ce setArgumentTable:at]; [at setTexture:tmp.gpuResourceID atIndex:0]; [at setTexture:lut.gpuResourceID atIndex:1]; [at setAddress:b0.gpuAddress atIndex:0];
|
||||
enc_c_pso(ce, ps); enc_c_dispatch(ce, MTLSizeMake((lut.width + 3) / 4, (lut.height + 3) / 4, (lut.depth + 3) / 4), MTLSizeMake(4, 4, 4)); enc_c_end(ce);
|
||||
args_dirty_all();
|
||||
}
|
||||
static void dispatch(const p_dispatch *dp) {
|
||||
zskip_clear();
|
||||
g_wr_epoch++;
|
||||
@@ -6125,7 +5803,6 @@ static void dispatch(const p_dispatch *dp) {
|
||||
enc_c_dispatch_indirect(ce, ig, MTLSizeMake(tg[0], tg[1], tg[2]));
|
||||
} else enc_c_dispatch(ce, MTLSizeMake(dp->x, dp->y, dp->z), MTLSizeMake(tg[0], tg[1], tg[2]));
|
||||
enc_c_end(ce);
|
||||
bright_lut(cs); // 3D picture brightening: rewrite after the game finishes computing the color lookup table
|
||||
}
|
||||
// GPU: texture -> buffer (row stride taken from SetCopyRowStride, layer stride from SetCopyImageStride, 0 = tightly packed)
|
||||
static void copy_t2b(const p_copy_b2t *c) {
|
||||
@@ -6419,201 +6096,15 @@ static void clear_tex(const p_clear_tex *c) {
|
||||
static const char *cctx_snap(void) {
|
||||
static char b[320]; static uint64_t p_pass, p_up, p_reup, p_ser; uint64_t up = 0, reup = 0; mtl_pkg_stats(&up, &reup);
|
||||
pthread_mutex_lock(&g_deferred_mu); unsigned long nd = (unsigned long)g_deferred.count; pthread_mutex_unlock(&g_deferred_mu);
|
||||
snprintf(b, sizeof b, "frame %llu; since the last submission: passes +%llu, first texture uploads +%llu, re-uploads +%llu, serial table changes +%llu; deferred-release queue %lu; world freeze state %d",
|
||||
snprintf(b, sizeof b, "frame %llu; since the last submission: passes +%llu, first texture uploads +%llu, re-uploads +%llu, serial table changes +%llu; deferred-release queue %lu",
|
||||
(unsigned long long)atomic_load(&g_cur_frame), (unsigned long long)(g_stat_pass - p_pass), (unsigned long long)(up - p_up), (unsigned long long)(reup - p_reup),
|
||||
(unsigned long long)(g_ser_gen - p_ser), nd, g_frz_state);
|
||||
(unsigned long long)(g_ser_gen - p_ser), nd);
|
||||
p_pass = g_stat_pass; p_up = up; p_reup = reup; p_ser = g_ser_gen; return b;
|
||||
}
|
||||
// ---------- Do not draw the world while the world is frozen (the whole game can be frozen while a panel is open) ----------
|
||||
// The "world frozen" status bit g_frz_on is set by mtl_world_frozen() (the host's freeze handshake calls it; in tests DM_NVN_TEST_FRZ=1 can stand in, or it can be switched in-process via @set:g_frz_on of DM_NVN_UB).
|
||||
// This code does not judge by itself whether a panel is open. Structure of a frame (3.3.94012 in-game, per-pass dump): UI pass -> [UI stencil pass] -> about 40 passes of shadows / depth / lighting / transparency / post-processing -> final compositing pass (writes the window texture).
|
||||
// The first frame after the bit is set = full frame: draw everything as usual, dropping only the draws of the UI stencil pass (stencil all 0 = no pixels blocked by the panel) -- post-processing normally does not write pixels blocked by the panel, so a shrinking panel would expose old content;
|
||||
// from the second frame on = frozen frame: from the first render pass without a color attachment up to before the render pass that writes the window texture, set-target / clear / draw / texture-to-texture copy / compute are all not executed, the scene result stays as the full frame's copy, and the UI pass and the final compositing pass are drawn as usual.
|
||||
// Frame boundaries are identified by render pass: the first set-target without the window texture after the window texture has been written = start of a new frame, and the status bit is latched here. Unrecognized structure (no render pass without color in a frame) = skip nothing.
|
||||
// State constants / buffer updates / events / counters / texture uploads execute as usual (the UI pass needs them, the game is waiting).
|
||||
int g_frz_on = -1;
|
||||
static int g_frz_phase = 2, g_frz_drop, g_frz_seen_ds; static uint64_t g_frz_full_n, g_frz_skip_n;
|
||||
void mtl_world_frozen(int on) { g_frz_on = on != 0; }
|
||||
static int frz_wanted(void) { if (g_frz_on < 0) { const char *e = getenv("DM_NVN_TEST_FRZ"); g_frz_on = e && atoi(e); if (g_frz_on) MLOG("DM_NVN_TEST_FRZ: the world freeze state bit is replaced by the test switch (always on)"); } tbdr_on(); return g_frz_on > 0 && tbdr_on() && g_ruler_truth <= 0; }
|
||||
// ---------- Runtime extrapolation (power-saving rendering method; no switch for the player; it caused a stutter defect in released builds, now off by default, to be re-enabled once there is evidence of no stutter on A17) ----------
|
||||
// 3D every other frame: a fully drawn frame draws as usual and, before the attachments are repurposed, copies the opaque HDR (468), the main depth-stencil, the post-tone-mapping 468 and the post-FXAA 469;
|
||||
// extrapolated frame: the UI stencil pass runs as usual; shadow map / depth prepass / main pass keep only the draws of moving units (skinned); before the first skinned draw of the depth prepass run one extrapolation pass (nvn_rtex.inc: by cb3 +464 -> +336
|
||||
// move the previous frame's HDR and depth to this frame, treat old unit positions 0x10 / UI cutout 0x2 as holes to fill, and export stencil 0x80 for holes and pixels newly exposed at the screen edge); translucents are drawn as usual and write effect bit 0x40;
|
||||
// compositing / add bloom / add fog / tone mapping / FXAA really run only in the 0xD0 region, and outside the region the previous frame's LDR result is copied in with a whole-image shift (orthographic camera, rounded); full-screen items such as the bloom chain are skipped; sharpening upscale and UI compositing run as usual.
|
||||
// Fallback to the original rendering method: version file render_safe = true (dm_update.m), the extrapolation pipeline cannot be built (whole run), test DM_RT_AB=0. When a panel is open and the world is frozen, yield to the freeze path.
|
||||
// On a camera jump (orientation change or translation over 64 pixels) and for 4 frames before and after switching the 3D render scale, whole frames are fully drawn. Unit shadows on the ground keep using the previous fully drawn frame.
|
||||
static int rt_is_skin(void) { // Moving units = the 10 whose vertex shader reads constants by bone index (the key table is taken from the Mac shader cache b-424a072084e6, identified by the dynamic vp_c5 index in the source)
|
||||
__unsafe_unretained NvProg *pr = S.prog ? prog_get(S.prog) : nil; if (!pr || !pr->sh[0]) return 0;
|
||||
static const char *k[] = { "0fa4eb7559f1637a", "1d97d33ab66abcba", "3deaf68fd65b5298", "4dffa355f2fbcdaf", "54c93fa22abff9ee",
|
||||
"64dd7d022f3fae39", "67e2ebc28c830f44", "add86ee8e20f89b9", "b3ae9be5cc670f35", "fbf6dc1873a449e9" };
|
||||
for (unsigned i = 0; i < sizeof k / sizeof *k; i++) if (!strncmp(pr->sh[0]->key, k[i], 16)) return 1;
|
||||
return 0;
|
||||
}
|
||||
int g_dm_rt_ab = 0; // Default is the original rendering method (the new method stutters in-game on A17 iPhone 15 Pro and ghosts characters on the character select page, so off for now); DM_RT_AB=1 turns on the new method for testing. The version file render_safe can only set it to 0, no effect
|
||||
static struct { int env, frame, filt, first, hdrp, mainp_seen, skipall, oit_seen, tm_seen, fx_state, comp_seen; uint64_t ldr_ser; float vpc[16], vpp[16]; id<MTLTexture> ldr, pl468, pl469; int ui_seen, main_seen, copied, ex_done, shadow, have_prev, have_ldr, sdx, sdy, cam_checked, force_full, disabled, was_on; uint64_t n_jump; uint64_t ds_ser, hdr_ser, n_ex, n_full, n_noex;
|
||||
id<MTLTexture> ds, hdr, pds, phdr; struct { uint64_t key; int skin, other; } sh[16]; int nsh, cur_sh; } g_rt = { .env = -1 };
|
||||
static int rt_want(void);
|
||||
static void rt_toggle_poll(void) { static int tg = -1; if (tg < 0) { const char *e = getenv("DM_RT_AB_TOGGLE"); tg = e ? atoi(e) : 0; (void)rt_want(); } // On first call also read DM_RT_AB (on a real device the world-freeze bit is set to 0 very early, so frz_op is not necessarily called) // test: switch back and forth between A / B every tg seconds by wall clock
|
||||
if (tg > 0) { int v = (int)((time(NULL) / tg) & 1); if (v != g_dm_rt_ab) { g_dm_rt_ab = v; MLOG("DM_RT_AB_TOGGLE: rendering method switched to %s", v ? "B" : "A"); } } }
|
||||
void mtl_rt_force_full(int n) { g_rt.force_full = n; } // Called by the host: before an overall picture change such as switching the 3D render scale, the following n frames are all fully drawn
|
||||
// ---------- Power-saving mode (30 frames) ----------
|
||||
// On: the display link object changes to 30 Hz (frame rate range 30–30, a ProMotion screen lowers its refresh rate accordingly; the frame rate hint in front_ios.m is lowered together), the game presents one frame per display-link tick; runtime extrapolation off (really draws every frame).
|
||||
// Game logic already runs at 25 ticks / second and unit positions are interpolated by time, so there are no repeated frames at 30 frames (measured on a real device: coefficient of variation of per-frame camera displacement 0.292 at 30 frames, 0.296 at 60 frames).
|
||||
// The switch is on the "Custom" page (dm_custom.c eco_fps, off by default, takes effect on apply); the test switch DM_FPS=30/60 pins it.
|
||||
__attribute__((weak)) int dm_fps_setting(void) { return 60; } // The host's dm_custom.c provides the strong definition; always 60 when graphics unit tests do not link the host
|
||||
// When the game itself asks for 30 fps (pacing by the game's present interval, interval 2, hle_nvn.c) the display link runs at 30 Hz too: games whose logic counts frames, such as Engage, are not pulled up to 60 fps by the display link on iOS
|
||||
__attribute__((weak)) int nvn_game_fps(void) { return 60; } // hle_nvn.c provides the strong definition
|
||||
int mtl_fps_target(void) { static int env = -2; if (env == -2) { const char *e = getenv("DM_FPS"); env = e ? atoi(e) : -1; if (env != 30 && env != 60) env = -1; else MLOG("DM_FPS=%d: frame rate pinned by the test switch", env); }
|
||||
return env > 0 ? env : dm_fps_setting() == 30 || nvn_game_fps() == 30 ? 30 : 60; }
|
||||
// Whenever the extrapolation enabled state changes (power-saving mode toggled, world freeze takes over / hands back): first let the "no store across frames" ledger relearn (during the trial period it stores as usual), and the extrapolation really switches one frame later --
|
||||
// guaranteeing the last submit before the switch is a full store. Otherwise attachments cleared first every frame at 30 frames are learned as no-store, and the first extrapolated frame after switching back to 60 frames reads old content and flashes for one frame (measured on a test build; only reverted after the xv self-check reported a failure)
|
||||
static void xv_relearn(const char *why) { g_xv_n = 0; g_xv_ntrial = 0; g_xv_ntouch = 0; MLOG("cross-frame store skipping: %s; the ledger is cleared and relearned (stores happen as usual during probation)", why); }
|
||||
static int rt_want_raw(void);
|
||||
static int rt_want(void) {
|
||||
static int eff = -1; static uint64_t pend; int want = rt_want_raw(); uint64_t f = atomic_load(&g_cur_frame);
|
||||
if (eff < 0) eff = want;
|
||||
if (want == eff) { pend = 0; return eff; }
|
||||
if (!pend) { pend = f + 1; xv_relearn(want ? "extrapolation will be enabled" : "extrapolation will be disabled"); return eff; }
|
||||
if (f > pend) { eff = want; pend = 0; }
|
||||
return eff;
|
||||
}
|
||||
static int rt_want_raw(void) { static int forced; if (g_rt.env < 0) { const char *e = getenv("DM_RT_AB"); g_rt.env = e ? atoi(e) : 0; forced = e != NULL; if (e) MLOG("DM_RT_AB=%d: rendering method pinned by the environment variable (for tests; the Custom page switch no longer has an effect)", g_rt.env); }
|
||||
if (forced) g_dm_rt_ab = g_rt.env;
|
||||
return g_dm_rt_ab == 1 && !g_rt.disabled && g_frz_on <= 0 && tbdr_on() && g_ruler_truth <= 0 && mtl_fps_target() == 60; } // When a panel is open and the world is frozen, yield to the freeze path (saves more); power-saving mode (30 frames) really draws every frame
|
||||
#include "nvn_rtex.inc"
|
||||
static uint64_t rt_key(const p_set_rt *r) { uint64_t s_; int lv, sl; rt_sub(r, 8, &s_, &lv, &sl); return s_ * 64 + (uint64_t)lv * 8 + (uint64_t)sl; }
|
||||
static int rt_is_shadow(const p_set_rt *r) { return !r->n && r->depth && r->depth->d.width == r->depth->d.height; }
|
||||
static id<MTLTexture> rt_cp(id<MTLTexture> src, id<MTLTexture> dst0) {
|
||||
id<MTLTexture> dst = dst0;
|
||||
if (!dst || dst.width != src.width || dst.height != src.height || dst.pixelFormat != src.pixelFormat) {
|
||||
MTLTextureDescriptor *d = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:src.pixelFormat width:src.width height:src.height mipmapped:NO];
|
||||
d.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget | MTLTextureUsagePixelFormatView; d.storageMode = MTLStorageModePrivate;
|
||||
if (dst0) defer_obj_release(dst0); // Size change: the old copy may still be read by an in-flight command buffer (MTL4 command buffers do not retain resources), so release it deferred
|
||||
dst = [g_dev newTextureWithDescriptor:d]; if (!dst) return nil; res_add(dst); }
|
||||
id<MTL4ComputeCommandEncoder> b = blit(); enc_b_copy_texture(b, src, 0, 0, MTLOriginMake(0, 0, 0), MTLSizeMake(src.width, src.height, 1), dst, 0, 0, MTLOriginMake(0, 0, 0)); return dst; }
|
||||
static void rt_blit_shift(id<MTLTexture> src, id<MTLTexture> dst, int dx, int dy) { // Integer-offset copy: dst(x + dx, y + dy) = src(x, y); the strip at the screen edge that cannot be moved is left for the region recompute (the extrapolation pass has already marked it 0x80)
|
||||
if (!src || !dst || src.width != dst.width || src.height != dst.height) return;
|
||||
int W = (int)src.width, H = (int)src.height, w = W - abs(dx), h = H - abs(dy); if (w <= 0 || h <= 0) return;
|
||||
id<MTL4ComputeCommandEncoder> b = blit(); enc_b_copy_texture(b, src, 0, 0, MTLOriginMake(MAX(0, -dx), MAX(0, -dy), 0), MTLSizeMake(w, h, 1), dst, 0, 0, MTLOriginMake(MAX(0, dx), MAX(0, dy), 0)); }
|
||||
static void rt_copy_prev(void) { // Fully drawn frame: copy the opaque HDR and the main depth-stencil once each (in order within the GPU, in this command buffer)
|
||||
if (g_rt.copied || !g_rt.main_seen || !g_rt.hdr || !g_rt.ds) return; g_rt.copied = 1; // Do not copy when the main pass was not recognized this frame (foreground / loading screen): the textures recognized last time may already have been released by the game and not be in the residency set
|
||||
if (!rx_setup(g_rt.hdr.pixelFormat, g_rt.ds.pixelFormat)) { if (!g_rt.disabled) { g_rt.disabled = 1; MLOG("[RED] runtime extrapolation: the extrapolation pipelines cannot be created on this device; the whole run falls back to the original rendering"); } return; } // Cannot be built (old GPU does not support stencil export etc.): fall back for the whole run
|
||||
g_rt.phdr = rt_cp(g_rt.hdr, g_rt.phdr); g_rt.pds = rt_cp(g_rt.ds, g_rt.pds); g_rt.have_prev = g_rt.phdr && g_rt.pds; }
|
||||
static void rt_frame_begin(void) { g_rt.filt = g_rt.ui_seen = g_rt.main_seen = g_rt.copied = g_rt.ex_done = g_rt.mainp_seen = 0; g_rt.cur_sh = -1;
|
||||
g_rt.oit_seen = g_rt.tm_seen = g_rt.fx_state = g_rt.comp_seen = g_rt.skipall = g_rt.cam_checked = 0; g_rt_ovr = 0; g_dss_dirty = 1;
|
||||
if (g_frz_state == 0) { g_rt.have_prev = g_rt.have_ldr = 0; for (int i = 0; i < g_rt.nsh; i++) g_rt.sh[i].skin = g_rt.sh[i].other = 0; } } // Fully drawn frame: the next frame may extrapolate only if this frame's copies succeeded
|
||||
static void rt_set_rt(const p_set_rt *r, int win) { // Every set-target (both fully drawn and extrapolated frames go through here): identify roles, find the copy points in fully drawn frames, and in extrapolated frames decide whether this render pass keeps only skinned draws
|
||||
g_rt.cur_sh = -1; g_rt.shadow = 0; g_rt.first = 1; g_rt.hdrp = 0;
|
||||
if (win) { if (g_frz_state == 0) { rt_copy_prev(); if (g_rt.fx_state == 2 && g_rt.ldr) { g_rt.pl469 = rt_cp(g_rt.ldr, g_rt.pl469); g_rt.have_ldr = g_rt.pl469 != nil; g_rt.fx_state = 3; } }
|
||||
g_rt.filt = 0; g_rt.skipall = 0; if (g_rt_ovr) { g_rt_ovr = 0; g_dss_dirty = 1; } return; }
|
||||
id<MTLTexture> c0 = r->n > 0 && r->color[0] ? tex_mtl(r->color[0]) : nil, dt = r->depth ? tex_mtl(r->depth) : nil;
|
||||
int hdr_here = 0; for (int i = 0; i < r->n && i < 8; i++) if (r->color[i] && g_rt.hdr_ser && r->color[i]->serial == g_rt.hdr_ser) hdr_here = 1;
|
||||
if (g_frz_state == 0) {
|
||||
if (!g_rt.main_seen && r->n == 1 && c0 && c0.pixelFormat == MTLPixelFormatRG11B10Float && dt && dt.pixelFormat == MTLPixelFormatDepth32Float_Stencil8 && dt.width >= 500 && dt.width != dt.height) {
|
||||
{ static uint64_t lh, ld; if (lh != r->color[0]->serial || ld != r->depth->serial) { lh = r->color[0]->serial; ld = r->depth->serial; MLOG("runtime extrapolation: main pass recognized: opaque HDR serial %llu %lux%lu, main depth-stencil serial %llu %lux%lu (pass %llu)", (unsigned long long)lh, (unsigned long)c0.width, (unsigned long)c0.height, (unsigned long long)ld, (unsigned long)dt.width, (unsigned long)dt.height, g_stat_pass + 1); } }
|
||||
g_rt.main_seen = 1; g_rt.hdr_ser = r->color[0]->serial; g_rt.ds_ser = r->depth->serial; g_rt.hdr = c0; g_rt.ds = dt; }
|
||||
else if (g_rt.main_seen && hdr_here && !g_rt.copied) rt_copy_prev(); // The first set-target after the main pass that attaches the opaque HDR again (the translucent pass; 468 occupies slot 0 but is not written): before it, copy the HDR and the main depth-stencil
|
||||
else if (g_rt.main_seen && hdr_here && r->n == 1 && r->depth && r->depth->serial == g_rt.ds_ser) g_rt.fx_state = 1; // Only 468 + main depth attached = tone mapping (repurposes 468 as the LDR output)
|
||||
else if (g_rt.fx_state == 1) { // The first render pass after tone mapping = FXAA (reads 468, writes 469): before it, copy the post-tone-mapping 468 (LDR) and note down 469
|
||||
if (r->n >= 1 && r->color[0] && c0) { g_rt.ldr_ser = r->color[0]->serial; g_rt.ldr = c0; g_rt.pl468 = rt_cp(g_rt.hdr, g_rt.pl468); g_rt.fx_state = 2; } else g_rt.fx_state = 0; }
|
||||
else if (g_rt.fx_state == 2 && g_rt.ldr) { g_rt.pl469 = rt_cp(g_rt.ldr, g_rt.pl469); g_rt.have_ldr = g_rt.pl468 && g_rt.pl469; g_rt.fx_state = 3; } // Before the first render pass after FXAA: copy the post-FXAA 469
|
||||
}
|
||||
if (rt_is_shadow(r)) { uint64_t k = rt_key(r); int i; for (i = 0; i < g_rt.nsh && g_rt.sh[i].key != k; i++) {}
|
||||
if (i == g_rt.nsh && i < 16) { g_rt.sh[i].key = k; g_rt.sh[i].skin = g_rt.sh[i].other = 0; g_rt.nsh++; } g_rt.cur_sh = i < 16 ? i : -1; g_rt.shadow = 1; }
|
||||
int ds_here = r->depth && g_rt.ds_ser && r->depth->serial == g_rt.ds_ser;
|
||||
if (ds_here && !r->n && !g_rt.ui_seen) { g_rt.ui_seen = 1; g_rt.filt = 0; return; } // The first render pass in this frame with the main depth and no color = the UI stencil pass: run as usual
|
||||
g_rt.hdrp = hdr_here && ds_here && r->n == 1 && !g_rt.mainp_seen; if (g_rt.hdrp) g_rt.mainp_seen = 1; // The first render pass in this frame with "only opaque HDR + main depth attached" = the main pass
|
||||
g_rt.filt = g_rt.shadow || (ds_here && !r->n) || g_rt.hdrp;
|
||||
if (g_frz_state == 2 && g_rt.mainp_seen && !g_rt.hdrp) { // Extrapolated frame, after the main pass: post-processing runs only in the unit / effect region (plan B)
|
||||
int c0ldr = r->n >= 1 && r->color[0] && g_rt.ldr_ser && r->color[0]->serial == g_rt.ldr_ser, ovr = 0; g_rt.skipall = 0;
|
||||
if (g_rt.fx_state == 2) {} // After FXAA (sharpening upscale + vignette, UI compositing): run everything
|
||||
else if (g_rt.fx_state == 1) g_rt.fx_state = 2;
|
||||
else if (!g_rt.oit_seen && ds_here && r->n >= 2) { g_rt.oit_seen = 1; ovr = 3; } // Translucent (weighted blended, two targets): draw as usual, writing effect bit 0x40 where it passes
|
||||
else if (!g_rt.comp_seen) { if (ds_here && c0ldr) { g_rt.comp_seen = 1; ovr = 1; rt_blit_shift(g_rt.pl469, g_rt.ldr, g_rt.sdx, g_rt.sdy); } else g_rt.skipall = 1; } // Before compositing: the previous frame's post-FXAA 469 is copied in with an integer shift (then recomputed in the region) // the other passes before compositing (mip chain, 511 refraction layer etc.) are skipped
|
||||
else if (ds_here && c0ldr && g_rt.tm_seen) { // The FXAA pass itself (recomputed in the region; the 469 outside the region was already shift-copied before compositing)
|
||||
g_rt.fx_state = 1; ovr = 1; }
|
||||
else if (ds_here) { if (hdr_here && r->n == 1) { g_rt.tm_seen = 1; rt_blit_shift(g_rt.pl468, g_rt.hdr, g_rt.sdx, g_rt.sdy); } ovr = 1; } // Before tone mapping: the previous frame's post-tone-mapping 468 (LDR) is copied in with an integer shift // add bloom, add fog, tone mapping: run only in the region
|
||||
else g_rt.skipall = 1; // Those without the main depth attached, such as the bloom chain: skip (the region keeps using the previous frame's bloom)
|
||||
if (ovr != g_rt_ovr) { g_rt_ovr = ovr; g_dss_dirty = 1; }
|
||||
}
|
||||
}
|
||||
static int rt_op(unsigned op) { // Non-set-target operations in an extrapolated frame: returns 1 = not executed
|
||||
if (g_frz_state == 2 && g_rt_ovr == 1 && op == OP_CLEAR_COLOR) return 1; // The region post-processing passes do not clear: outside the region is the previous frame's result copied in with a shift
|
||||
if (g_rt.skipall && (op == OP_DRAW || op == OP_DRAW_IDX || op == OP_CLEAR_COLOR || op == OP_CLEAR_DS || op == OP_COPY_T2T || op == OP_CLEAR_TEX || op == OP_DISPATCH)) return 1;
|
||||
if (op == OP_DRAW || op == OP_DRAW_IDX) {
|
||||
if (g_frz_state == 2 && !g_rt.cam_checked) { // First draw of an extrapolated frame (UI stencil pass, before shadows / prepass): check this frame's camera, and on a jump switch the whole frame to fully drawn
|
||||
g_rt.cam_checked = 1; const p_bind_buf *u = &S.ubo[0][3]; void *mb; uint64_t mo; const uint8_t *base; int ok = 0;
|
||||
if (u->addr && u->size >= 528 && mtl_addr(u->addr, u->size, &mb, &mo) && (base = [(__bridge id<MTLBuffer>)mb contents])) {
|
||||
const float *cur = (const float *)(base + mo + 336), *prev = (const float *)(base + mo + 464); int dx, dy; float md = 0;
|
||||
for (int r = 0; r < 3; r++) for (int c = 0; c < 4; c++) md = MAX(md, fabsf(cur[r * 4 + c] - prev[r * 4 + c])); // The first three rows (orientation / scale) should be unchanged: an orthographic camera only translates
|
||||
rt_shift_px(prev, cur, g_rt.hdr.width, g_rt.hdr.height, &dx, &dy);
|
||||
ok = md < 1e-4f && abs(dx) <= 64 && abs(dy) <= 64; if (!ok) { static int n; if (n++ < 20) MLOG("runtime extrapolation: camera jump (shift %d,%d pixels, orientation difference %g); this frame is fully rendered", dx, dy, md); } }
|
||||
if (!ok) { g_frz_state = 0; g_rt.n_jump++; rt_frame_begin(); g_rt.cam_checked = 1; return 0; }
|
||||
}
|
||||
if (g_frz_state == 0) { if (g_rt.cur_sh >= 0) { if (rt_is_skin()) g_rt.sh[g_rt.cur_sh].skin++; else g_rt.sh[g_rt.cur_sh].other++; } return 0; }
|
||||
if (!g_rt.filt) return 0;
|
||||
if (!rt_is_skin()) return 1;
|
||||
if (!g_rt.ex_done && !g_rt.shadow) { // Before the first skinned draw of the depth prepass: the extrapolation pass (reads the cb3 bound by this draw: +336 this frame's, +464 the previous frame's view-projection)
|
||||
g_rt.ex_done = 1; const p_bind_buf *u = &S.ubo[0][3]; void *mb; uint64_t mo; const uint8_t *base;
|
||||
if (u->addr && u->size >= 528 && mtl_addr(u->addr, u->size, &mb, &mo) && (base = [(__bridge id<MTLBuffer>)mb contents])) {
|
||||
float cur[16], prev[16]; memcpy(cur, base + mo + 336, 64); memcpy(prev, base + mo + 464, 64); memcpy(g_rt.vpc, cur, 64); memcpy(g_rt.vpp, prev, 64); rt_shift_px(prev, cur, g_rt.hdr.width, g_rt.hdr.height, &g_rt.sdx, &g_rt.sdy);
|
||||
rp_end(); if (rt_ex_encode(g_rt.hdr, g_rt.ds, g_rt.phdr, g_rt.pds, prev, cur, 0x10, 0x2)) g_rt.n_ex++; else g_rt.n_noex++;
|
||||
} else g_rt.n_noex++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (g_frz_state == 2 && g_rt.filt) {
|
||||
if (op == OP_CLEAR_COLOR || op == OP_CLEAR_DS) {
|
||||
if (g_rt.shadow && g_rt.cur_sh >= 0 && !g_rt.sh[g_rt.cur_sh].other) return 0; // A shadow level that had only skinned draws in the previous fully drawn frame (character-only level): clear and draw as usual
|
||||
return 1; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int frz_op(unsigned op, const void *a) { // Returns 1 = this operation is not executed
|
||||
if (op == OP_SET_RT) {
|
||||
const p_set_rt *r = a; int win = 0;
|
||||
for (int i = 0; i < r->n; i++) { uint64_t s_; int lv, sl; rt_sub(r, i, &s_, &lv, &sl); if (s_ && xv_present(s_)) win = 1; }
|
||||
if (g_frz_phase == 2 && !win && rt_want()) { // Runtime extrapolation: new frame, fully drawn frames and extrapolated frames alternate
|
||||
int was = g_frz_state; g_frz_phase = 0; g_rt.was_on = 1;
|
||||
{ static int dbg = -1, n; if (dbg < 0) dbg = getenv("DM_RT_DBG") != NULL; if (dbg && n < 4000 && g_rt.ds_ser) { n++; MLOG("[rtdbg] frame boundary pass %llu prev_state %d main_seen %d copied %d extrapolated %d fx %d ldr %d hoist %d fz %d defer %d/%d", g_stat_pass, was, g_rt.main_seen, g_rt.copied, g_rt.ex_done, g_rt.fx_state, g_rt.have_ldr, g_hoist_n, g_fz.active, defer_has_active(), g_defer_replaying); } }
|
||||
if (g_rt.force_full > 0) g_rt.force_full--;
|
||||
g_frz_state = !was && g_rt.have_prev && g_rt.have_ldr && !g_rt.force_full && !g_hoist_n && !g_fz.active && !defer_has_active() && !g_defer_replaying ? 2 : 0;
|
||||
if (g_frz_state == 2) g_rt.frame++; else g_rt.n_full++;
|
||||
if (g_frz_state != was && (g_rt.frame + g_rt.n_full) % 600 < 2) MLOG("runtime extrapolation: full frames %llu, extrapolated frames %d (extrapolation pass encoded %llu times, not encoded %llu times; full render after a camera jump %llu times)", g_rt.n_full, g_rt.frame, g_rt.n_ex, g_rt.n_noex, g_rt.n_jump);
|
||||
rt_frame_begin();
|
||||
}
|
||||
if (g_frz_phase != 2 && rt_want()) { if (win) g_frz_phase = 2; rt_set_rt(r, win); return g_rt.skipall; }
|
||||
if (g_frz_phase == 2 && win && rt_want()) { rt_set_rt(r, win); return 0; }
|
||||
{ int on = rt_want(); if (g_rt.was_on && !on) { // The new rendering method was just turned off (version file fallback / panel freeze takes over / fallback for the whole run): reset state to zero and hand back to the original path
|
||||
g_frz_state = 0; g_frz_phase = 2; g_rt.have_prev = g_rt.have_ldr = 0; if (g_rt_ovr) { g_rt_ovr = 0; g_dss_dirty = 1; } g_rt.skipall = 0;
|
||||
MLOG("runtime extrapolation: new rendering method disabled (method %d, world freeze %d, whole-run fallback %d)", g_dm_rt_ab, g_frz_on, g_rt.disabled); }
|
||||
g_rt.was_on = on; }
|
||||
if (g_frz_phase == 2 && !win) { // New frame
|
||||
int on = frz_wanted(), was = g_frz_state; g_frz_phase = 0; g_frz_seen_ds = 0;
|
||||
{ static uint64_t fr, p0, k0; if (was == 2 && (++fr == 4 || fr % 600 == 0)) MLOG("world freeze: frozen frame %llu opened %llu passes, skipped %llu set-target commands", fr, g_stat_pass - p0, g_frz_skip_n - k0); if (was != 2) fr = 0; p0 = g_stat_pass; k0 = g_frz_skip_n; }
|
||||
if (!on) g_frz_state = 0;
|
||||
else if (was) g_frz_state = 2;
|
||||
else if (!g_hoist_n && !g_fz.active && !defer_has_active() && !g_defer_replaying) g_frz_state = 1; // The premises / merges / deferrals left by the previous frame have not finished yet: enter next frame
|
||||
if (g_frz_state != was) MLOG("world freeze: from pass %llu, %s (full frames so far %llu, passes skipped %llu)", g_stat_pass + 1, g_frz_state == 1 ? "full frame (without the UI stencil)" : g_frz_state == 2 ? "frozen frame (only the UI and the final composite are drawn)" : "back to normal", g_frz_full_n += g_frz_state == 1, g_frz_skip_n);
|
||||
}
|
||||
g_frz_drop = 0;
|
||||
if (win) { g_frz_phase = 2; return 0; }
|
||||
if (!g_frz_state || g_frz_phase == 2) return 0;
|
||||
if (g_frz_phase == 0 && !r->n && r->depth && !g_frz_seen_ds) { // The first render pass without color in this frame
|
||||
g_frz_seen_ds = 1;
|
||||
if (g_frz_state == 2) g_frz_phase = 1;
|
||||
else { uint64_t s_; int lv, sl; rt_sub(r, 8, &s_, &lv, &sl); static int neg = -1; if (neg < 0) neg = getenv("DM_NVN_TEST_FRZ_NEG") != NULL; // Negative control: the full frame is drawn with the UI stencil as usual, and old pixels must be exposed when the panel shrinks
|
||||
g_frz_drop = !neg && MAX(r->depth->d.width >> lv, 1u) != MAX(r->depth->d.height >> lv, 1u); } // Full frame: the one that is not square (shadow map) is the UI stencil pass
|
||||
}
|
||||
if (g_frz_phase == 1) { g_frz_skip_n++; return 1; }
|
||||
return 0;
|
||||
}
|
||||
if (rt_want() && g_frz_phase != 2) return rt_op(op);
|
||||
if (g_frz_drop) return op == OP_DRAW || op == OP_DRAW_IDX;
|
||||
if (g_frz_state == 2 && g_frz_phase == 1) return op == OP_CLEAR_COLOR || op == OP_CLEAR_DS || op == OP_DRAW || op == OP_DRAW_IDX || op == OP_COPY_T2T || op == OP_CLEAR_TEX || op == OP_DISPATCH;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return env > 0 ? env : nvn_game_fps() == 30 ? 30 : 60; }
|
||||
// Hand a submission to the GPU in segments (DM_NVN_CUT=draw count, 0 = off; default 64): when the game holds back the next frame's submit until this frame's GPU work is done (The Legend of Zelda: Breath of the Wild: the main thread's nvnSyncWait on the fence of the frame just submitted),
|
||||
// the GPU sits idle for the milliseconds the host spends translating inside the submit (in the shrine each frame is GPU 13 ms + translation 7 ms back to back). Each time this many draws have accumulated, the command buffer encoded so far is committed between two passes: the GPU runs the earlier passes while the CPU translates the later ones.
|
||||
// Cut only when no encoder is open and no pass is deferred / merged / hoisted; commit this batch's uploads first (later draws use them). The fence signal and completion work stay on the last segment only: the queue executes in commit order and every encoder starts with a queue barrier
|
||||
@@ -6779,8 +6270,6 @@ void mtl_exec(const uint8_t *p) {
|
||||
g_exec_cur = p; // Scan start point of the store lookahead: the operation that ends the render pass (inclusive)
|
||||
if (p == g_defer_pend) { for (int gi = 0; gi < 4; gi++) if (g_defer_slots[gi].active && g_defer_slots[gi].p_end_op == p) g_defer_slots[gi].in_p = 0; g_defer_pend = NULL; }
|
||||
sdx_seq(p);
|
||||
if (h.op == OP_SET_RT) rt_toggle_poll();
|
||||
if ((g_frz_on || g_frz_state || g_dm_rt_ab == 1) && frz_op(h.op, p + 8)) { p += h.size; continue; }
|
||||
if (trace) PD("o cmd=%p pass=%llu op=%u size=%u", p, g_stat_pass, (unsigned)h.op, (unsigned)h.size);
|
||||
// Diagnostic DM_NVN_ALLOCPROF=1: growth of the GPU allocated size is charged to the previous operation's type (rough: allocations on other threads are counted too); the memory report line appends the top 4
|
||||
if (g_allocprof < 0) g_allocprof = getenv("DM_NVN_ALLOCPROF") != NULL;
|
||||
@@ -7015,9 +6504,7 @@ static void shot(id<MTLTexture> src, const char *path, int screen, const int cro
|
||||
if (!ctx || !img || !dst) MLOG("[RED] screenshot %s failed: %s", path, !ctx ? "cannot create the bitmap context" : !img ? "cannot get the image" : "cannot write (directory missing or not writable)");
|
||||
else { CGImageDestinationAddImage(dst, img, NULL); ok = CGImageDestinationFinalize(dst); }
|
||||
if (dst) CFRelease(dst); if (url) CFRelease(url); CGImageRelease(img); CGContextRelease(ctx); CGColorSpaceRelease(cs);
|
||||
dm_shot_written(path, ok); // Let the host write the "frozen tick" line first, then write the screenshot line ending in "ok": the screenshot script stops the host as soon as it sees "ok", and with the order reversed the log would lack the "frozen tick" line under full load (false failure in the gate check's require_log)
|
||||
MLOG("screenshot %zux%zu format %lu → %s %s", w, h, (unsigned long)read.pixelFormat, path, ok ? "ok" : "failed");
|
||||
if (g_ruler_truth && getenv("DM_NVN_RULER") && !strcmp(getenv("DM_NVN_RULER"), "1") && ok) { static int shots; if (++shots == 2) g_ruler_pending = 1; }
|
||||
if (g_defer_ab >= 0 && getenv("DM_NVN_DEFER_AB") && !strcmp(getenv("DM_NVN_DEFER_AB"), "1") && ok) { static int shots; if (++shots == 2) { g_defer_ab_on = 1; MLOG("DM_NVN_DEFER_AB: second screenshot written; deferred pass merging on"); } }
|
||||
if (g_udc_ab >= 0 && getenv("DM_NVN_UDC_AB") && !strcmp(getenv("DM_NVN_UDC_AB"), "1") && ok) {
|
||||
static int shots;
|
||||
@@ -7288,7 +6775,6 @@ static double g_link_it_tin; // Enqueue time of the frame the display-link cal
|
||||
static int present_now(const tex_t *t, double period, uint64_t frame, uint64_t ticket, const int crop[4], id<CAMetalDrawable> supplied, double target_ts) {
|
||||
if (g_present_stall_ns) { struct timespec ts = {0, (long)g_present_stall_ns}; nanosleep(&ts, NULL); }
|
||||
g_present_n++;
|
||||
if (g_use != USE_OFF && g_use != USE_DONE) use_tick();
|
||||
if (bootprof() && (g_present_n == 1 || g_present_n % 120 == 0)) {
|
||||
if (g_present_n == 1) g_bp_first_present = bp_now() - g_bp_t0;
|
||||
MLOG("[bootprof] t=%.3f s present #%llu: metallibs loaded %llu / %.1f ms; pipelines created %llu / %.1f ms (all synchronous on the submit thread); prewarmed and used: shaders %llu / render pipelines %llu / compute pipelines %llu; "
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
//
|
||||
// int rt_ex_encode(dst_hdr, dst_ds, src_hdr, src_ds, vp_prev[16], vp_cur[16], unit_bits, ui_bits)
|
||||
// nvn_rtex.inc - shaders for runtime extrapolation (#included by nvn_mtl.m after the definitions of rp_end / m4_end / res_add / args_dirty_all / blit etc.)
|
||||
// int rt_ex_encode(dst_hdr, dst_ds, src_hdr, src_ds, vp_prev[16], vp_cur[16], unit_bits, ui_bits)
|
||||
// Encodes one render pass (a single fullscreen triangle) on the current MTL4 command buffer g_cb, extrapolating the opaque HDR and depth of the previous fully drawn frame into dst_hdr / dst_ds by this frame's camera:
|
||||
// Backward mapping: destination pixel q → source pixel p, with 2 fixed-point iterations using the source depth; M = inv(vp_prev) · vp_cur is computed on the CPU in double precision (with translations on the order of 1e4 a single-precision inverse matrix would be off by 0.01 pixel).
|
||||
// Non-hole: five-tap Catmull-Rom, clamped to the min / max of the 2×2 core texels against ringing, then sharpening compensation by the fractional phase of the displacement (strongest at phase 0.5, 0 at integer phase, strength k = 0.5).
|
||||
// Hole (the stencil of the source 2×2 core contains unit_bits | ui_bits: old unit position / UI cut-out): along 8 directions search outward every 4 pixels for the first pixel that is neither a hole nor pure black (at most 24 steps),
|
||||
// weighted average by 1/distance²; depth takes the nearest of them. Holes and pixels newly exposed at the screen edge (source out of bounds) get stencil 0x80 exported by the fragment, and the later regional post-processing is recomputed accordingly.
|
||||
// Pixels covered by this frame's UI (the UI stencil pass already wrote 0x2) are skipped by the stencil test.
|
||||
// Returns 1 = encoded; 0 = size mismatch / pipeline cannot be built (the caller rt_copy_prev prechecks with rx_setup first, and if it cannot be built the whole run falls back to the original drawing method).
|
||||
// Before and after the call: this function first calls rp_end() to close the current render pass and copy encoder; after encoding it sets args_dirty_all() / g_pso_dirty / g_dss_dirty, and the game's next draw reopens its pass with 'load'.
|
||||
// Resources: src_hdr / src_ds are created and res_add'ed by the caller; src_ds must be able to open an X32_Stencil8 view (created with MTLTextureUsagePixelFormatView).
|
||||
static id<MTLRenderPipelineState> g_rx_pso1; static MTLPixelFormat g_rx_fmt_c, g_rx_fmt_ds;
|
||||
static id<MTLDepthStencilState> g_rx_dss; static id<MTL4ArgumentTable> g_rx_args;
|
||||
static id<MTLBuffer> g_rx_const[3]; static int g_rx_ci;
|
||||
static id<MTLTexture> g_rx_stv; static __unsafe_unretained id<MTLTexture> g_rx_stv_src;
|
||||
static const char *k_rx_msl =
|
||||
"#include <metal_stdlib>\n"
|
||||
"using namespace metal;\n"
|
||||
"struct VO { float4 p [[position]]; };\n"
|
||||
"vertex VO rx_vs(uint id [[vertex_id]]) { float2 uv = float2((id << 1) & 2, id & 2); VO o; o.p = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1); return o; }\n"
|
||||
"struct K { float4x4 M; uint hole; float amount; float2 pad; };\n"
|
||||
"static float4 cr_w(float t) { return float4(((-0.5 * t + 1.0) * t - 0.5) * t, (1.5 * t - 2.5) * t * t + 1.0, ((-1.5 * t + 2.0) * t + 0.5) * t, (0.5 * t - 0.5) * t * t); }\n"
|
||||
"static uint stv(texture2d<uint> s, int2 p) { uint4 v = s.read(uint2(p)); return v.r | v.g; }\n" // X32_Stencil8 view: which component holds the stencil value is implementation-dependent, take both r | g (the other one is 0)
|
||||
"static float zproj(float4x4 M, float2 p, float z, int W, int H) { float4 c = M * float4((p.x + 0.5) / W * 2 - 1, 1 - (p.y + 0.5) / H * 2, z, 1); return c.z / c.w; }\n"
|
||||
"struct O1 { float4 c [[color(0)]]; float d [[depth(any)]]; uint s [[stencil]]; };\n"
|
||||
"fragment O1 rx_fs1(VO in [[stage_in]], texture2d<float> hdr [[texture(0)]], depth2d<float> dep [[texture(1)]], texture2d<uint> stc [[texture(2)]], constant K& k [[buffer(0)]]) {\n"
|
||||
" constexpr sampler ls(filter::linear, address::clamp_to_edge, coord::pixel);\n"
|
||||
" int W = hdr.get_width(), H = hdr.get_height(); int2 hi = int2(W - 1, H - 1);\n"
|
||||
" float2 q = floor(in.p.xy); float2 p = q;\n"
|
||||
" for (int it = 0; it < 2; it++) {\n"
|
||||
" int2 pi = clamp(int2(rint(p)), int2(0), hi); float z = dep.read(uint2(pi));\n"
|
||||
" float4 c = k.M * float4((p.x + 0.5) / W * 2 - 1, 1 - (p.y + 0.5) / H * 2, z, 1);\n"
|
||||
" float2 qq = float2((c.x / c.w + 1) * 0.5 * W - 0.5, (1 - c.y / c.w) * 0.5 * H - 0.5); p += q - qq; }\n"
|
||||
" bool oob = p.x < -0.5 || p.y < -0.5 || p.x > float(hi.x) + 0.5 || p.y > float(hi.y) + 0.5;\n"
|
||||
" p = clamp(p, float2(0), float2(hi));\n"
|
||||
" int2 b0 = clamp(int2(floor(p)), int2(0), hi - 1);\n"
|
||||
" uint s4 = stv(stc, b0) | stv(stc, b0 + int2(1, 0)) | stv(stc, b0 + int2(0, 1)) | stv(stc, b0 + int2(1, 1));\n"
|
||||
" int2 cp = clamp(int2(rint(p)), int2(0), hi);\n"
|
||||
" O1 o; o.s = ((s4 & k.hole) || oob) ? 0x80u : 0u;\n"
|
||||
" if (!(s4 & k.hole)) {\n"
|
||||
" float3 t00 = hdr.read(uint2(b0)).rgb, t10 = hdr.read(uint2(b0 + int2(1, 0))).rgb, t01 = hdr.read(uint2(b0 + int2(0, 1))).rgb, t11 = hdr.read(uint2(b0 + int2(1, 1))).rgb;\n"
|
||||
" float3 lo = min(min(t00, t10), min(t01, t11)), hc = max(max(t00, t10), max(t01, t11)), avg = (t00 + t10 + t01 + t11) * 0.25;\n"
|
||||
" float2 sp = p + 0.5; float2 tp1 = floor(sp - 0.5) + 0.5; float2 f = sp - tp1;\n" // Five-tap Catmull-Rom (bilinear sampling merges the two middle taps, the four corners are dropped)
|
||||
" float2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f)), w1 = 1.0 + f * f * (-2.5 + 1.5 * f), w2 = f * (0.5 + f * (2.0 - 1.5 * f)), w3 = f * f * (-0.5 + 0.5 * f);\n"
|
||||
" float2 w12 = w1 + w2, t12 = tp1 + w2 / w12, t0 = tp1 - 1.0, t3 = tp1 + 2.0;\n"
|
||||
" float3 acc = hdr.sample(ls, float2(t12.x, t0.y)).rgb * (w12.x * w0.y) + hdr.sample(ls, float2(t0.x, t12.y)).rgb * (w0.x * w12.y) + hdr.sample(ls, t12).rgb * (w12.x * w12.y)\n"
|
||||
" + hdr.sample(ls, float2(t3.x, t12.y)).rgb * (w3.x * w12.y) + hdr.sample(ls, float2(t12.x, t3.y)).rgb * (w12.x * w3.y);\n"
|
||||
" float wt = w12.x * w0.y + w0.x * w12.y + w12.x * w12.y + w3.x * w12.y + w12.x * w3.y; float3 cr = clamp(acc / wt, lo, hc);\n"
|
||||
" o.c = float4(max(cr + k.amount * (cr - avg), 0.0), 1); o.d = zproj(k.M, p, dep.read(uint2(cp)), W, H); return o; }\n" // Sharpening compensation by fractional phase is folded into the same pass (the neighborhood takes the mean of the 2×2 core)
|
||||
" const int2 dirs[8] = { int2(1, 0), int2(-1, 0), int2(0, 1), int2(0, -1), int2(1, 1), int2(-1, 1), int2(1, -1), int2(-1, -1) };\n"
|
||||
" float3 sum = 0; float wsum = 0; int bestr = 1000; int2 bests = cp;\n"
|
||||
" for (int d = 0; d < 8; d++) for (int r = 1; r <= 24; r++) { int2 s = cp + dirs[d] * (4 * r);\n"
|
||||
" if (s.x < 0 || s.y < 0 || s.x > hi.x || s.y > hi.y) break;\n"
|
||||
" if (stv(stc, s) & k.hole) continue;\n"
|
||||
" float3 v = hdr.read(uint2(s)).rgb; if (max(v.r, max(v.g, v.b)) <= 0) continue;\n"
|
||||
" float w = 1.0 / float(r * r); sum += v * w; wsum += w; if (r < bestr) { bestr = r; bests = s; } break; }\n"
|
||||
" if (wsum > 0) { o.c = float4(sum / wsum, 1); o.d = zproj(k.M, float2(bests), dep.read(uint2(bests)), W, H); return o; }\n"
|
||||
" o.c = float4(hdr.read(uint2(cp)).rgb, 1); o.d = zproj(k.M, p, dep.read(uint2(cp)), W, H); return o;\n"
|
||||
"}\n";
|
||||
static int rx_setup(MTLPixelFormat fc, MTLPixelFormat fds) {
|
||||
if (getenv("DM_RT_TEST_NOPSO")) return 0; // Test: pretend this device cannot build the extrapolation pipeline (verifies the whole run falls back to the original drawing method)
|
||||
if (g_rx_pso1 && g_rx_fmt_c == fc && g_rx_fmt_ds == fds) return 1;
|
||||
static int failed; if (failed) return 0;
|
||||
NSError *e = nil; id<MTLLibrary> lib = [g_dev newLibraryWithSource:@(k_rx_msl) options:nil error:&e];
|
||||
if (!lib) { MLOG("[RED] runtime extrapolation: shader compile failed %s", e.localizedDescription.UTF8String); failed = 1; return 0; }
|
||||
MTLRenderPipelineDescriptor *pd = [MTLRenderPipelineDescriptor new]; pd.vertexFunction = [lib newFunctionWithName:@"rx_vs"]; pd.fragmentFunction = [lib newFunctionWithName:@"rx_fs1"];
|
||||
pd.colorAttachments[0].pixelFormat = fc; pd.depthAttachmentPixelFormat = fds;
|
||||
if (fds == MTLPixelFormatDepth32Float_Stencil8) pd.stencilAttachmentPixelFormat = fds;
|
||||
g_rx_pso1 = [g_dev newRenderPipelineStateWithDescriptor:pd error:&e];
|
||||
if (!g_rx_pso1) { MLOG("[RED] runtime extrapolation: cannot build the pipeline %s", e.localizedDescription.UTF8String); g_rx_pso1 = nil; failed = 1; return 0; }
|
||||
MTLDepthStencilDescriptor *dd = [MTLDepthStencilDescriptor new]; dd.depthCompareFunction = MTLCompareFunctionAlways; dd.depthWriteEnabled = YES;
|
||||
if (fds == MTLPixelFormatDepth32Float_Stencil8) { MTLStencilDescriptor *sd = [MTLStencilDescriptor new]; sd.stencilCompareFunction = MTLCompareFunctionEqual; sd.readMask = 0x2; sd.writeMask = 0x80; sd.depthStencilPassOperation = MTLStencilOperationReplace; // Hole pixels / pixels newly entering from off-screen get 0x80 exported by the fragment, and the later LDR region is recomputed accordingly // Pixels covered by this frame's UI (the UI stencil pass already wrote 0x2) do not count
|
||||
dd.frontFaceStencil = sd; dd.backFaceStencil = sd; }
|
||||
g_rx_dss = [g_dev newDepthStencilStateWithDescriptor:dd];
|
||||
if (!g_rx_args) { MTL4ArgumentTableDescriptor *ad = [MTL4ArgumentTableDescriptor new]; ad.maxTextureBindCount = 3; ad.maxBufferBindCount = 1;
|
||||
g_rx_args = [g_dev newArgumentTableWithDescriptor:ad error:&e];
|
||||
for (int i = 0; i < 3; i++) { g_rx_const[i] = [g_dev newBufferWithLength:256 options:MTLResourceStorageModeShared]; res_add(g_rx_const[i]); } }
|
||||
if (!g_rx_args || !g_rx_dss) { MLOG("[RED] runtime extrapolation: cannot build the argument table / depth state"); g_rx_pso1 = nil; failed = 1; return 0; }
|
||||
g_rx_fmt_c = fc; g_rx_fmt_ds = fds; MLOG("runtime extrapolation: shaders built (color format %lu, depth format %lu)", (unsigned long)fc, (unsigned long)fds);
|
||||
return 1;
|
||||
}
|
||||
static void rx_inv4(const double *m, double *o) { // 4×4 inverse (adjugate method, double precision)
|
||||
double inv[16];
|
||||
inv[0] = m[5]*m[10]*m[15] - m[5]*m[11]*m[14] - m[9]*m[6]*m[15] + m[9]*m[7]*m[14] + m[13]*m[6]*m[11] - m[13]*m[7]*m[10];
|
||||
inv[4] = -m[4]*m[10]*m[15] + m[4]*m[11]*m[14] + m[8]*m[6]*m[15] - m[8]*m[7]*m[14] - m[12]*m[6]*m[11] + m[12]*m[7]*m[10];
|
||||
inv[8] = m[4]*m[9]*m[15] - m[4]*m[11]*m[13] - m[8]*m[5]*m[15] + m[8]*m[7]*m[13] + m[12]*m[5]*m[11] - m[12]*m[7]*m[9];
|
||||
inv[12] = -m[4]*m[9]*m[14] + m[4]*m[10]*m[13] + m[8]*m[5]*m[14] - m[8]*m[6]*m[13] - m[12]*m[5]*m[10] + m[12]*m[6]*m[9];
|
||||
inv[1] = -m[1]*m[10]*m[15] + m[1]*m[11]*m[14] + m[9]*m[2]*m[15] - m[9]*m[3]*m[14] - m[13]*m[2]*m[11] + m[13]*m[3]*m[10];
|
||||
inv[5] = m[0]*m[10]*m[15] - m[0]*m[11]*m[14] - m[8]*m[2]*m[15] + m[8]*m[3]*m[14] + m[12]*m[2]*m[11] - m[12]*m[3]*m[10];
|
||||
inv[9] = -m[0]*m[9]*m[15] + m[0]*m[11]*m[13] + m[8]*m[1]*m[15] - m[8]*m[3]*m[13] - m[12]*m[1]*m[11] + m[12]*m[3]*m[9];
|
||||
inv[13] = m[0]*m[9]*m[14] - m[0]*m[10]*m[13] - m[8]*m[1]*m[14] + m[8]*m[2]*m[13] + m[12]*m[1]*m[10] - m[12]*m[2]*m[9];
|
||||
inv[2] = m[1]*m[6]*m[15] - m[1]*m[7]*m[14] - m[5]*m[2]*m[15] + m[5]*m[3]*m[14] + m[13]*m[2]*m[7] - m[13]*m[3]*m[6];
|
||||
inv[6] = -m[0]*m[6]*m[15] + m[0]*m[7]*m[14] + m[4]*m[2]*m[15] - m[4]*m[3]*m[14] - m[12]*m[2]*m[7] + m[12]*m[3]*m[6];
|
||||
inv[10] = m[0]*m[5]*m[15] - m[0]*m[7]*m[13] - m[4]*m[1]*m[15] + m[4]*m[3]*m[13] + m[12]*m[1]*m[7] - m[12]*m[3]*m[5];
|
||||
inv[14] = -m[0]*m[5]*m[14] + m[0]*m[6]*m[13] + m[4]*m[1]*m[14] - m[4]*m[2]*m[13] - m[12]*m[1]*m[6] + m[12]*m[2]*m[5];
|
||||
inv[3] = -m[1]*m[6]*m[11] + m[1]*m[7]*m[10] + m[5]*m[2]*m[11] - m[5]*m[3]*m[10] - m[9]*m[2]*m[7] + m[9]*m[3]*m[6];
|
||||
inv[7] = m[0]*m[6]*m[11] - m[0]*m[7]*m[10] - m[4]*m[2]*m[11] + m[4]*m[3]*m[10] + m[8]*m[2]*m[7] - m[8]*m[3]*m[6];
|
||||
inv[11] = -m[0]*m[5]*m[11] + m[0]*m[7]*m[9] + m[4]*m[1]*m[11] - m[4]*m[3]*m[9] - m[8]*m[1]*m[7] + m[8]*m[3]*m[5];
|
||||
inv[15] = m[0]*m[5]*m[10] - m[0]*m[6]*m[9] - m[4]*m[1]*m[10] + m[4]*m[2]*m[9] + m[8]*m[1]*m[6] - m[8]*m[2]*m[5];
|
||||
double det = m[0]*inv[0] + m[1]*inv[4] + m[2]*inv[8] + m[3]*inv[12]; det = det != 0 ? 1.0 / det : 0;
|
||||
for (int i = 0; i < 16; i++) o[i] = inv[i] * det;
|
||||
}
|
||||
int rt_ex_encode(id<MTLTexture> dst_hdr, id<MTLTexture> dst_ds, id<MTLTexture> src_hdr, id<MTLTexture> src_ds,
|
||||
const float vp_prev[16], const float vp_cur[16], uint32_t unit_bits, uint32_t ui_bits) {
|
||||
if (!dst_hdr || !dst_ds || !src_hdr || !src_ds || !vp_prev || !vp_cur || !g_cb) return 0;
|
||||
if (src_hdr.width != dst_hdr.width || src_hdr.height != dst_hdr.height || src_ds.width != src_hdr.width || src_ds.height != src_hdr.height
|
||||
|| dst_ds.width != dst_hdr.width || dst_ds.height != dst_hdr.height) return 0; // Resolution changed (loading screen / dynamic scale): no extrapolation this frame
|
||||
if (!rx_setup(dst_hdr.pixelFormat, dst_ds.pixelFormat)) return 0;
|
||||
if (g_rx_stv_src != src_ds) {
|
||||
if (g_rx_stv) defer_obj_release(g_rx_stv);
|
||||
g_rx_stv = [src_ds newTextureViewWithPixelFormat:MTLPixelFormatX32_Stencil8];
|
||||
if (!g_rx_stv) { static int n; if (!n++) MLOG("[RED] runtime extrapolation: cannot create the stencil view (src_ds needs the PixelFormatView usage)"); return 0; }
|
||||
res_add(g_rx_stv); g_rx_stv_src = src_ds;
|
||||
}
|
||||
// Constant: M = inv(vp_prev) · vp_cur (row vectors: clip_cur = (ndc_prev, z, 1) · M); Metal's float4x4 is stored by column, and row i of the row-vector matrix serves exactly as column i ⇒ write M * v in the shader
|
||||
double a[16], b[16], ia[16], m[16];
|
||||
for (int i = 0; i < 16; i++) { a[i] = vp_prev[i]; b[i] = vp_cur[i]; }
|
||||
rx_inv4(a, ia);
|
||||
for (int r = 0; r < 4; r++) for (int c = 0; c < 4; c++) { double s = 0; for (int k = 0; k < 4; k++) s += ia[r * 4 + k] * b[k * 4 + c]; m[r * 4 + c] = s; }
|
||||
// Fractional phase: pixel displacement on screen between the two frames of the world origin (any point works; orthographic + translation is uniform across the screen), d = destination − source; the source of the backward mapping = destination − d
|
||||
double W = dst_hdr.width, H = dst_hdr.height;
|
||||
double c0x = m[12] / m[15], c0y = m[13] / m[15]; // Source NDC origin (0, 0, 0, 1) projected into this frame
|
||||
double dx = c0x / 2 * W, dy = -c0y / 2 * H, sx = -dx, sy = -dy; // Offset of the source relative to the destination (pixels)
|
||||
double fx = sx - floor(sx), fy = sy - floor(sy), amount = 0.5 * 2 * (fx * (1 - fx) + fy * (1 - fy));
|
||||
float *k = g_rx_const[g_rx_ci].contents;
|
||||
for (int i = 0; i < 16; i++) k[i] = (float)m[i];
|
||||
((uint32_t *)k)[16] = unit_bits | ui_bits; k[17] = (float)amount;
|
||||
id<MTLBuffer> cbuf = g_rx_const[g_rx_ci]; g_rx_ci = (g_rx_ci + 1) % 3;
|
||||
rp_end(); if (g_blit) { enc_b_end(g_blit); g_blit = nil; }
|
||||
// Single pass: extrapolation + hole filling + sharpening compensation by fractional phase → dst_hdr; depth is written into dst_ds; pixels covered by this frame's UI are skipped by stencil (stencil not written)
|
||||
MTL4RenderPassDescriptor *rp = [MTL4RenderPassDescriptor new];
|
||||
rp.colorAttachments[0].texture = dst_hdr; rp.colorAttachments[0].loadAction = MTLLoadActionLoad; rp.colorAttachments[0].storeAction = MTLStoreActionStore;
|
||||
rp.depthAttachment.texture = dst_ds; rp.depthAttachment.loadAction = MTLLoadActionLoad; rp.depthAttachment.storeAction = MTLStoreActionStore;
|
||||
if (dst_ds.pixelFormat == MTLPixelFormatDepth32Float_Stencil8) { rp.stencilAttachment.texture = dst_ds; rp.stencilAttachment.loadAction = MTLLoadActionLoad; rp.stencilAttachment.storeAction = MTLStoreActionStore; }
|
||||
id<MTL4RenderCommandEncoder> en = [g_cb renderCommandEncoderWithDescriptor:rp]; if (!en) return 0;
|
||||
[en barrierAfterQueueStages:MTLStageAll beforeStages:MTLStageAll visibilityOptions:MTL4VisibilityOptionDevice];
|
||||
[en setArgumentTable:g_rx_args atStages:MTLRenderStageFragment];
|
||||
[g_rx_args setTexture:src_hdr.gpuResourceID atIndex:0]; [g_rx_args setTexture:src_ds.gpuResourceID atIndex:1]; [g_rx_args setTexture:g_rx_stv.gpuResourceID atIndex:2];
|
||||
[g_rx_args setAddress:cbuf.gpuAddress atIndex:0];
|
||||
[en setViewport:(MTLViewport){0, 0, W, H, 0, 1}]; [en setRenderPipelineState:g_rx_pso1]; [en setDepthStencilState:g_rx_dss]; [en setStencilReferenceValue:0];
|
||||
[en drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; m4_end(en);
|
||||
args_dirty_all(); g_pso_dirty = g_dss_dirty = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Whole-image translation in pixels between two frames under an orthographic camera (rounded): destination = source + (dx, dy)
|
||||
static void rt_shift_px(const float vp_prev[16], const float vp_cur[16], double W, double H, int *dx, int *dy) {
|
||||
double a[16], b[16], ia[16], m[16];
|
||||
for (int i = 0; i < 16; i++) { a[i] = vp_prev[i]; b[i] = vp_cur[i]; }
|
||||
rx_inv4(a, ia);
|
||||
for (int r = 0; r < 4; r++) for (int c = 0; c < 4; c++) { double s = 0; for (int k = 0; k < 4; k++) s += ia[r * 4 + k] * b[k * 4 + c]; m[r * 4 + c] = s; }
|
||||
*dx = (int)lround(m[12] / m[15] / 2 * W); *dy = (int)lround(-m[13] / m[15] / 2 * H);
|
||||
}
|
||||
@@ -1,525 +0,0 @@
|
||||
# kind: evidence main NVN fetch slots and static use sites (nvn_slots.py)
|
||||
name slot nuses first_use
|
||||
nvnDeviceBuilderSetDefaults 320e910 1 173cb6c
|
||||
nvnDeviceBuilderSetFlags 320e918 1 173cb7c
|
||||
nvnDeviceBuilderGetFlags 320e920 0 -
|
||||
nvnDeviceInitialize 320e928 1 173cb90
|
||||
nvnDeviceFinalize 320e930 1 173e31c
|
||||
nvnDeviceSetDebugLabel 320e938 0 -
|
||||
nvnDeviceGetProcAddress 320e940 1 173cba4
|
||||
nvnDeviceGetInteger 320e948 2 173cbd8
|
||||
nvnDeviceGetCurrentTimestampInNanoseconds 320e950 0 -
|
||||
nvnDeviceSetIntermediateShaderCache 320e958 0 -
|
||||
nvnDeviceGetTextureHandle 320e960 0 -
|
||||
nvnDeviceGetTexelFetchHandle 320e968 0 -
|
||||
nvnDeviceGetImageHandle 320e970 4 173ceb8
|
||||
nvnDeviceInstallDebugCallback 320e978 1 173cbb8
|
||||
nvnDeviceGenerateDebugDomainId 320e980 0 -
|
||||
nvnDeviceSetWindowOriginMode 320e988 1 173cc04
|
||||
nvnDeviceSetDepthMode 320e990 1 173cc18
|
||||
nvnDeviceRegisterFastClearColor 320e998 0 -
|
||||
nvnDeviceRegisterFastClearColori 320e9a0 0 -
|
||||
nvnDeviceRegisterFastClearColorui 320e9a8 0 -
|
||||
nvnDeviceRegisterFastClearDepth 320e9b0 0 -
|
||||
nvnDeviceGetWindowOriginMode 320e9b8 0 -
|
||||
nvnDeviceGetDepthMode 320e9c0 0 -
|
||||
nvnDeviceGetTimestampInNanoseconds 320e9c8 0 -
|
||||
nvnDeviceApplyDeferredFinalizes 320e9d0 0 -
|
||||
nvnDeviceFinalizeCommandHandle 320e9d8 1 17443fc
|
||||
nvnDeviceWalkDebugDatabase 320e9e0 0 -
|
||||
nvnDeviceGetSeparateTextureHandle 320e9e8 4 173ced0
|
||||
nvnDeviceGetSeparateSamplerHandle 320e9f0 7 173cee8
|
||||
nvnDeviceIsExternalDebuggerAttached 320e9f8 0 -
|
||||
nvnQueueGetError 320ea00 3 1744464
|
||||
nvnQueueGetTotalCommandMemoryUsed 320ea08 0 -
|
||||
nvnQueueGetTotalControlMemoryUsed 320ea10 0 -
|
||||
nvnQueueGetTotalComputeMemoryUsed 320ea18 0 -
|
||||
nvnQueueResetMemoryUsageCounts 320ea20 0 -
|
||||
nvnQueueBuilderSetDevice 320ea28 1 173d3e8
|
||||
nvnQueueBuilderSetDefaults 320ea30 1 173d3c4
|
||||
nvnQueueBuilderSetFlags 320ea38 1 173d3d4
|
||||
nvnQueueBuilderSetCommandMemorySize 320ea40 1 173d410
|
||||
nvnQueueBuilderSetComputeMemorySize 320ea48 1 173d3fc
|
||||
nvnQueueBuilderSetControlMemorySize 320ea50 1 173d424
|
||||
nvnQueueBuilderGetQueueMemorySize 320ea58 1 173d44c
|
||||
nvnQueueBuilderSetQueueMemory 320ea60 1 173d47c
|
||||
nvnQueueBuilderSetCommandFlushThreshold 320ea68 1 173d438
|
||||
nvnQueueBuilderSetQueuePriority 320ea70 0 -
|
||||
nvnQueueBuilderGetQueuePriority 320ea78 0 -
|
||||
nvnQueueBuilderGetDevice 320ea80 0 -
|
||||
nvnQueueBuilderGetFlags 320ea88 0 -
|
||||
nvnQueueBuilderGetCommandMemorySize 320ea90 0 -
|
||||
nvnQueueBuilderGetComputeMemorySize 320ea98 0 -
|
||||
nvnQueueBuilderGetControlMemorySize 320eaa0 0 -
|
||||
nvnQueueBuilderGetCommandFlushThreshold 320eaa8 0 -
|
||||
nvnQueueBuilderGetMemorySize 320eab0 0 -
|
||||
nvnQueueBuilderGetMemory 320eab8 0 -
|
||||
nvnQueueInitialize 320eac0 1 173d494
|
||||
nvnQueueFinalize 320eac8 1 1743ea4
|
||||
nvnQueueSetDebugLabel 320ead0 0 -
|
||||
nvnQueueSubmitCommands 320ead8 1 17443d4
|
||||
nvnQueueFlush 320eae0 1 1744448
|
||||
nvnQueueFinish 320eae8 1 17444e0
|
||||
nvnQueuePresentTexture 320eaf0 1 1744978
|
||||
nvnQueueAcquireTexture 320eaf8 0 -
|
||||
nvnWindowBuilderSetDevice 320eb00 1 17414e8
|
||||
nvnWindowBuilderSetDefaults 320eb08 1 17414fc
|
||||
nvnWindowBuilderSetNativeWindow 320eb10 1 1741514
|
||||
nvnWindowBuilderSetTextures 320eb18 1 1741528
|
||||
nvnWindowBuilderSetPresentInterval 320eb20 1 1741540
|
||||
nvnWindowBuilderSetNumActiveTextures 320eb28 0 -
|
||||
nvnWindowBuilderGetDevice 320eb30 0 -
|
||||
nvnWindowBuilderGetNumTextures 320eb38 0 -
|
||||
nvnWindowBuilderGetTexture 320eb40 0 -
|
||||
nvnWindowBuilderGetNativeWindow 320eb48 0 -
|
||||
nvnWindowBuilderGetPresentInterval 320eb50 0 -
|
||||
nvnWindowBuilderGetNumActiveTextures 320eb58 0 -
|
||||
nvnWindowInitialize 320eb60 1 174154c
|
||||
nvnWindowFinalize 320eb68 1 1742db4
|
||||
nvnWindowSetDebugLabel 320eb70 0 -
|
||||
nvnWindowAcquireTexture 320eb78 1 1743b38
|
||||
nvnWindowGetNativeWindow 320eb80 0 -
|
||||
nvnWindowGetPresentInterval 320eb88 0 -
|
||||
nvnWindowSetPresentInterval 320eb90 1 1743af8
|
||||
nvnWindowSetCrop 320eb98 1 1743ad0
|
||||
nvnWindowGetCrop 320eba0 0 -
|
||||
nvnWindowSetNumActiveTextures 320eba8 0 -
|
||||
nvnWindowGetNumActiveTextures 320ebb0 0 -
|
||||
nvnWindowGetNumTextures 320ebb8 0 -
|
||||
nvnProgramInitialize 320ebc0 7 173f0d8
|
||||
nvnProgramFinalize 320ebc8 7 17403c4
|
||||
nvnProgramSetDebugLabel 320ebd0 0 -
|
||||
nvnProgramSetShaders 320ebd8 7 173f100
|
||||
nvnProgramSetShadersExt 320ebe0 0 -
|
||||
nvnProgramSetSampleShading 320ebe8 0 -
|
||||
nvnMemoryPoolBuilderSetDevice 320ebf0 2 174947c
|
||||
nvnMemoryPoolBuilderSetDefaults 320ebf8 2 174946c
|
||||
nvnMemoryPoolBuilderSetStorage 320ec00 2 1749498
|
||||
nvnMemoryPoolBuilderSetFlags 320ec08 2 17494c4
|
||||
nvnMemoryPoolBuilderGetDevice 320ec10 0 -
|
||||
nvnMemoryPoolBuilderGetMemory 320ec18 0 -
|
||||
nvnMemoryPoolBuilderGetSize 320ec20 0 -
|
||||
nvnMemoryPoolBuilderGetFlags 320ec28 0 -
|
||||
nvnMemoryPoolInitialize 320ec30 2 17494d4
|
||||
nvnMemoryPoolSetDebugLabel 320ec38 0 -
|
||||
nvnMemoryPoolFinalize 320ec40 8 173e088
|
||||
nvnMemoryPoolMap 320ec48 0 -
|
||||
nvnMemoryPoolFlushMappedRange 320ec50 2 173d9a4
|
||||
nvnMemoryPoolInvalidateMappedRange 320ec58 0 -
|
||||
nvnMemoryPoolGetBufferAddress 320ec60 12 173cd40
|
||||
nvnMemoryPoolMapVirtual 320ec68 0 -
|
||||
nvnMemoryPoolGetSize 320ec70 0 -
|
||||
nvnMemoryPoolGetFlags 320ec78 0 -
|
||||
nvnTexturePoolInitialize 320ec80 1 173cdec
|
||||
nvnTexturePoolSetDebugLabel 320ec88 0 -
|
||||
nvnTexturePoolFinalize 320ec90 1 173de88
|
||||
nvnTexturePoolRegisterTexture 320ec98 5 173ec3c
|
||||
nvnTexturePoolRegisterImage 320eca0 0 -
|
||||
nvnTexturePoolGetMemoryPool 320eca8 0 -
|
||||
nvnTexturePoolGetMemoryOffset 320ecb0 0 -
|
||||
nvnTexturePoolGetSize 320ecb8 0 -
|
||||
nvnSamplerPoolInitialize 320ecc0 1 173ce88
|
||||
nvnSamplerPoolSetDebugLabel 320ecc8 0 -
|
||||
nvnSamplerPoolFinalize 320ecd0 1 173de9c
|
||||
nvnSamplerPoolRegisterSampler 320ecd8 1 1740d6c
|
||||
nvnSamplerPoolRegisterSamplerBuilder 320ece0 0 -
|
||||
nvnSamplerPoolGetMemoryPool 320ece8 0 -
|
||||
nvnSamplerPoolGetMemoryOffset 320ecf0 0 -
|
||||
nvnSamplerPoolGetSize 320ecf8 0 -
|
||||
nvnBufferBuilderSetDevice 320ed00 0 -
|
||||
nvnBufferBuilderSetDefaults 320ed08 0 -
|
||||
nvnBufferBuilderSetStorage 320ed10 0 -
|
||||
nvnBufferBuilderGetDevice 320ed18 0 -
|
||||
nvnBufferBuilderGetMemoryPool 320ed20 0 -
|
||||
nvnBufferBuilderGetMemoryOffset 320ed28 0 -
|
||||
nvnBufferBuilderGetSize 320ed30 0 -
|
||||
nvnBufferInitialize 320ed38 0 -
|
||||
nvnBufferSetDebugLabel 320ed40 0 -
|
||||
nvnBufferFinalize 320ed48 0 -
|
||||
nvnBufferMap 320ed50 0 -
|
||||
nvnBufferGetAddress 320ed58 0 -
|
||||
nvnBufferFlushMappedRange 320ed60 0 -
|
||||
nvnBufferInvalidateMappedRange 320ed68 0 -
|
||||
nvnBufferGetMemoryPool 320ed70 0 -
|
||||
nvnBufferGetMemoryOffset 320ed78 0 -
|
||||
nvnBufferGetSize 320ed80 0 -
|
||||
nvnBufferGetDebugID 320ed88 0 -
|
||||
nvnTextureBuilderSetDevice 320ed90 2 173ead8
|
||||
nvnTextureBuilderSetDefaults 320ed98 2 173eaec
|
||||
nvnTextureBuilderSetFlags 320eda0 4 173eb04
|
||||
nvnTextureBuilderSetTarget 320eda8 2 173eb48
|
||||
nvnTextureBuilderSetWidth 320edb0 1 1741be4
|
||||
nvnTextureBuilderSetHeight 320edb8 1 1741c40
|
||||
nvnTextureBuilderSetDepth 320edc0 1 1741c50
|
||||
nvnTextureBuilderSetSize1D 320edc8 1 173eb3c
|
||||
nvnTextureBuilderSetSize2D 320edd0 0 -
|
||||
nvnTextureBuilderSetSize3D 320edd8 0 -
|
||||
nvnTextureBuilderSetLevels 320ede0 1 1741c6c
|
||||
nvnTextureBuilderSetFormat 320ede8 2 173eb24
|
||||
nvnTextureBuilderSetSamples 320edf0 1 1741c98
|
||||
nvnTextureBuilderSetSwizzle 320edf8 0 -
|
||||
nvnTextureBuilderSetDepthStencilMode 320ee00 0 -
|
||||
nvnTextureBuilderGetStorageSize 320ee08 3 1741760
|
||||
nvnTextureBuilderGetStorageAlignment 320ee10 2 1741780
|
||||
nvnTextureBuilderSetStorage 320ee18 3 173eb70
|
||||
nvnTextureBuilderSetPackagedTextureData 320ee20 0 -
|
||||
nvnTextureBuilderSetPackagedTextureLayout 320ee28 1 1741b24
|
||||
nvnTextureBuilderSetStride 320ee30 1 1741b78
|
||||
nvnTextureBuilderSetGLTextureName 320ee38 0 -
|
||||
nvnTextureBuilderGetStorageClass 320ee40 0 -
|
||||
nvnTextureBuilderGetDevice 320ee48 0 -
|
||||
nvnTextureBuilderGetFlags 320ee50 2 1741728
|
||||
nvnTextureBuilderGetTarget 320ee58 0 -
|
||||
nvnTextureBuilderGetWidth 320ee60 0 -
|
||||
nvnTextureBuilderGetHeight 320ee68 0 -
|
||||
nvnTextureBuilderGetDepth 320ee70 0 -
|
||||
nvnTextureBuilderGetLevels 320ee78 0 -
|
||||
nvnTextureBuilderGetFormat 320ee80 0 -
|
||||
nvnTextureBuilderGetSamples 320ee88 0 -
|
||||
nvnTextureBuilderGetSwizzle 320ee90 0 -
|
||||
nvnTextureBuilderGetDepthStencilMode 320ee98 0 -
|
||||
nvnTextureBuilderGetPackagedTextureData 320eea0 0 -
|
||||
nvnTextureBuilderGetPackagedTextureLayout 320eea8 0 -
|
||||
nvnTextureBuilderGetStride 320eeb0 0 -
|
||||
nvnTextureBuilderGetSparseTileLayout 320eeb8 0 -
|
||||
nvnTextureBuilderGetGLTextureName 320eec0 0 -
|
||||
nvnTextureBuilderGetZCullStorageSize 320eec8 0 -
|
||||
nvnTextureBuilderGetMemoryPool 320eed0 0 -
|
||||
nvnTextureBuilderGetMemoryOffset 320eed8 0 -
|
||||
nvnTextureBuilderGetRawStorageClass 320eee0 0 -
|
||||
nvnTextureViewSetDefaults 320eee8 8 174071c
|
||||
nvnTextureViewSetLevels 320eef0 10 1740734
|
||||
nvnTextureViewSetLayers 320eef8 9 1740754
|
||||
nvnTextureViewSetFormat 320ef00 3 1740788
|
||||
nvnTextureViewSetSwizzle 320ef08 0 -
|
||||
nvnTextureViewSetDepthStencilMode 320ef10 0 -
|
||||
nvnTextureViewSetTarget 320ef18 3 17407b0
|
||||
nvnTextureViewGetLevels 320ef20 0 -
|
||||
nvnTextureViewGetLayers 320ef28 2 1745844
|
||||
nvnTextureViewGetFormat 320ef30 0 -
|
||||
nvnTextureViewGetSwizzle 320ef38 0 -
|
||||
nvnTextureViewGetDepthStencilMode 320ef40 0 -
|
||||
nvnTextureViewGetTarget 320ef48 0 -
|
||||
nvnTextureViewCompare 320ef50 0 -
|
||||
nvnTextureInitialize 320ef58 3 173eb84
|
||||
nvnTextureGetZCullStorageSize 320ef60 0 -
|
||||
nvnTextureFinalize 320ef68 4 174214c
|
||||
nvnTextureSetDebugLabel 320ef70 0 -
|
||||
nvnTextureGetStorageClass 320ef78 0 -
|
||||
nvnTextureGetViewOffset 320ef80 2 17433b4
|
||||
nvnTextureGetFlags 320ef88 2 1742ee4
|
||||
nvnTextureGetTarget 320ef90 0 -
|
||||
nvnTextureGetWidth 320ef98 7 174564c
|
||||
nvnTextureGetHeight 320efa0 7 1745668
|
||||
nvnTextureGetDepth 320efa8 7 174567c
|
||||
nvnTextureGetLevels 320efb0 0 -
|
||||
nvnTextureGetFormat 320efb8 0 -
|
||||
nvnTextureGetSamples 320efc0 0 -
|
||||
nvnTextureGetSwizzle 320efc8 0 -
|
||||
nvnTextureGetDepthStencilMode 320efd0 0 -
|
||||
nvnTextureGetStride 320efd8 0 -
|
||||
nvnTextureGetTextureAddress 320efe0 0 -
|
||||
nvnTextureGetSparseTileLayout 320efe8 0 -
|
||||
nvnTextureWriteTexels 320eff0 0 -
|
||||
nvnTextureWriteTexelsStrided 320eff8 0 -
|
||||
nvnTextureReadTexels 320f000 16 1843924
|
||||
nvnTextureReadTexelsStrided 320f008 1 18ca2d8
|
||||
nvnTextureFlushTexels 320f010 0 -
|
||||
nvnTextureInvalidateTexels 320f018 1 1883124
|
||||
nvnTextureGetMemoryPool 320f020 0 -
|
||||
nvnTextureGetMemoryOffset 320f028 0 -
|
||||
nvnTextureGetStorageSize 320f030 3 1743430
|
||||
nvnTextureCompare 320f038 2 18d6fc8
|
||||
nvnTextureGetDebugID 320f040 0 -
|
||||
nvnTextureGetRawStorageClass 320f048 8 174899c
|
||||
nvnSamplerBuilderSetDevice 320f050 1 1740bc8
|
||||
nvnSamplerBuilderSetDefaults 320f058 1 1740be8
|
||||
nvnSamplerBuilderSetMinMagFilter 320f060 2 1740c18
|
||||
nvnSamplerBuilderSetWrapMode 320f068 1 1740c80
|
||||
nvnSamplerBuilderSetLodClamp 320f070 1 1883364
|
||||
nvnSamplerBuilderSetLodBias 320f078 1 1883328
|
||||
nvnSamplerBuilderSetCompare 320f080 1 1740ca4
|
||||
nvnSamplerBuilderSetBorderColor 320f088 2 1740cdc
|
||||
nvnSamplerBuilderSetBorderColori 320f090 0 -
|
||||
nvnSamplerBuilderSetBorderColorui 320f098 0 -
|
||||
nvnSamplerBuilderSetMaxAnisotropy 320f0a0 1 1740c08
|
||||
nvnSamplerBuilderSetReductionFilter 320f0a8 0 -
|
||||
nvnSamplerBuilderSetLodSnap 320f0b0 1 1740cc0
|
||||
nvnSamplerBuilderGetDevice 320f0b8 0 -
|
||||
nvnSamplerBuilderGetMinMagFilter 320f0c0 1 18832ec
|
||||
nvnSamplerBuilderGetWrapMode 320f0c8 3 18ca2d0
|
||||
nvnSamplerBuilderGetLodClamp 320f0d0 0 -
|
||||
nvnSamplerBuilderGetLodBias 320f0d8 0 -
|
||||
nvnSamplerBuilderGetCompare 320f0e0 0 -
|
||||
nvnSamplerBuilderGetBorderColor 320f0e8 0 -
|
||||
nvnSamplerBuilderGetBorderColori 320f0f0 0 -
|
||||
nvnSamplerBuilderGetBorderColorui 320f0f8 0 -
|
||||
nvnSamplerBuilderGetMaxAnisotropy 320f100 0 -
|
||||
nvnSamplerBuilderGetReductionFilter 320f108 0 -
|
||||
nvnSamplerBuilderGetLodSnap 320f110 0 -
|
||||
nvnSamplerInitialize 320f118 1 1740cec
|
||||
nvnSamplerFinalize 320f120 2 1742920
|
||||
nvnSamplerSetDebugLabel 320f128 0 -
|
||||
nvnSamplerGetMinMagFilter 320f130 0 -
|
||||
nvnSamplerGetWrapMode 320f138 0 -
|
||||
nvnSamplerGetLodClamp 320f140 0 -
|
||||
nvnSamplerGetLodBias 320f148 0 -
|
||||
nvnSamplerGetCompare 320f150 0 -
|
||||
nvnSamplerGetBorderColor 320f158 0 -
|
||||
nvnSamplerGetBorderColori 320f160 0 -
|
||||
nvnSamplerGetBorderColorui 320f168 0 -
|
||||
nvnSamplerGetMaxAnisotropy 320f170 0 -
|
||||
nvnSamplerGetReductionFilter 320f178 0 -
|
||||
nvnSamplerCompare 320f180 0 -
|
||||
nvnSamplerGetDebugID 320f188 0 -
|
||||
nvnBlendStateSetDefaults 320f190 1 173fd44
|
||||
nvnBlendStateSetBlendTarget 320f198 1 173fd54
|
||||
nvnBlendStateSetBlendFunc 320f1a0 1 173fd8c
|
||||
nvnBlendStateSetBlendEquation 320f1a8 1 173fdac
|
||||
nvnBlendStateSetAdvancedMode 320f1b0 0 -
|
||||
nvnBlendStateSetAdvancedOverlap 320f1b8 0 -
|
||||
nvnBlendStateSetAdvancedPremultipliedSrc 320f1c0 0 -
|
||||
nvnBlendStateSetAdvancedNormalizedDst 320f1c8 0 -
|
||||
nvnBlendStateGetBlendTarget 320f1d0 0 -
|
||||
nvnBlendStateGetBlendFunc 320f1d8 0 -
|
||||
nvnBlendStateGetBlendEquation 320f1e0 0 -
|
||||
nvnBlendStateGetAdvancedMode 320f1e8 0 -
|
||||
nvnBlendStateGetAdvancedOverlap 320f1f0 0 -
|
||||
nvnBlendStateGetAdvancedPremultipliedSrc 320f1f8 0 -
|
||||
nvnBlendStateGetAdvancedNormalizedDst 320f200 0 -
|
||||
nvnColorStateSetDefaults 320f208 1 173fce4
|
||||
nvnColorStateSetBlendEnable 320f210 1 173fd2c
|
||||
nvnColorStateSetLogicOp 320f218 0 -
|
||||
nvnColorStateSetAlphaTest 320f220 0 -
|
||||
nvnColorStateGetBlendEnable 320f228 0 -
|
||||
nvnColorStateGetLogicOp 320f230 0 -
|
||||
nvnColorStateGetAlphaTest 320f238 0 -
|
||||
nvnChannelMaskStateSetDefaults 320f240 1 173fcf0
|
||||
nvnChannelMaskStateSetChannelMask 320f248 1 173fdc8
|
||||
nvnChannelMaskStateGetChannelMask 320f250 0 -
|
||||
nvnMultisampleStateSetDefaults 320f258 1 1740000
|
||||
nvnMultisampleStateSetMultisampleEnable 320f260 1 1740020
|
||||
nvnMultisampleStateSetSamples 320f268 1 1740044
|
||||
nvnMultisampleStateSetAlphaToCoverageEnable 320f270 1 1740054
|
||||
nvnMultisampleStateSetAlphaToCoverageDither 320f278 0 -
|
||||
nvnMultisampleStateGetMultisampleEnable 320f280 0 -
|
||||
nvnMultisampleStateGetSamples 320f288 0 -
|
||||
nvnMultisampleStateGetAlphaToCoverageEnable 320f290 0 -
|
||||
nvnMultisampleStateGetAlphaToCoverageDither 320f298 0 -
|
||||
nvnMultisampleStateSetRasterSamples 320f2a0 0 -
|
||||
nvnMultisampleStateGetRasterSamples 320f2a8 0 -
|
||||
nvnMultisampleStateSetCoverageModulationMode 320f2b0 0 -
|
||||
nvnMultisampleStateGetCoverageModulationMode 320f2b8 0 -
|
||||
nvnMultisampleStateSetCoverageToColorEnable 320f2c0 0 -
|
||||
nvnMultisampleStateGetCoverageToColorEnable 320f2c8 0 -
|
||||
nvnMultisampleStateSetCoverageToColorOutput 320f2d0 0 -
|
||||
nvnMultisampleStateGetCoverageToColorOutput 320f2d8 0 -
|
||||
nvnMultisampleStateSetSampleLocationsEnable 320f2e0 1 17466d8
|
||||
nvnMultisampleStateGetSampleLocationsEnable 320f2e8 0 -
|
||||
nvnMultisampleStateGetSampleLocationsGrid 320f2f0 0 -
|
||||
nvnMultisampleStateSetSampleLocationsGridEnable 320f2f8 0 -
|
||||
nvnMultisampleStateGetSampleLocationsGridEnable 320f300 0 -
|
||||
nvnMultisampleStateSetSampleLocations 320f308 1 17466c4
|
||||
nvnPolygonStateSetDefaults 320f310 1 173ff54
|
||||
nvnPolygonStateSetCullFace 320f318 1 173ff94
|
||||
nvnPolygonStateSetFrontFace 320f320 1 173ffac
|
||||
nvnPolygonStateSetPolygonMode 320f328 1 173ff70
|
||||
nvnPolygonStateSetPolygonOffsetEnables 320f330 1 173ffc8
|
||||
nvnPolygonStateGetCullFace 320f338 0 -
|
||||
nvnPolygonStateGetFrontFace 320f340 0 -
|
||||
nvnPolygonStateGetPolygonMode 320f348 0 -
|
||||
nvnPolygonStateGetPolygonOffsetEnables 320f350 0 -
|
||||
nvnDepthStencilStateSetDefaults 320f358 1 173fe28
|
||||
nvnDepthStencilStateSetDepthTestEnable 320f360 1 173fe6c
|
||||
nvnDepthStencilStateSetDepthWriteEnable 320f368 1 173fe58
|
||||
nvnDepthStencilStateSetDepthFunc 320f370 1 173fea4
|
||||
nvnDepthStencilStateSetStencilTestEnable 320f378 1 173fe80
|
||||
nvnDepthStencilStateSetStencilFunc 320f380 2 173febc
|
||||
nvnDepthStencilStateSetStencilOp 320f388 2 173ff04
|
||||
nvnDepthStencilStateGetDepthTestEnable 320f390 0 -
|
||||
nvnDepthStencilStateGetDepthWriteEnable 320f398 0 -
|
||||
nvnDepthStencilStateGetDepthFunc 320f3a0 0 -
|
||||
nvnDepthStencilStateGetStencilTestEnable 320f3a8 0 -
|
||||
nvnDepthStencilStateGetStencilFunc 320f3b0 0 -
|
||||
nvnDepthStencilStateGetStencilOp 320f3b8 0 -
|
||||
nvnVertexAttribStateSetDefaults 320f3c0 1 174027c
|
||||
nvnVertexAttribStateSetFormat 320f3c8 1 1740348
|
||||
nvnVertexAttribStateSetStreamIndex 320f3d0 1 174035c
|
||||
nvnVertexAttribStateGetFormat 320f3d8 0 -
|
||||
nvnVertexAttribStateGetStreamIndex 320f3e0 0 -
|
||||
nvnVertexStreamStateSetDefaults 320f3e8 1 1742014
|
||||
nvnVertexStreamStateSetStride 320f3f0 1 1742030
|
||||
nvnVertexStreamStateSetDivisor 320f3f8 1 1742044
|
||||
nvnVertexStreamStateGetStride 320f400 0 -
|
||||
nvnVertexStreamStateGetDivisor 320f408 0 -
|
||||
nvnCommandBufferInitialize 320f410 4 173d4c8
|
||||
nvnCommandBufferFinalize 320f418 7 17423e8
|
||||
nvnCommandBufferSetDebugLabel 320f420 0 -
|
||||
nvnCommandBufferSetMemoryCallback 320f428 3 173ef60
|
||||
nvnCommandBufferSetMemoryCallbackData 320f430 3 173ef78
|
||||
nvnCommandBufferSetCommandMemoryCallbackEnabled 320f438 0 -
|
||||
nvnCommandBufferAddCommandMemory 320f440 2 173d5ac
|
||||
nvnCommandBufferAddControlMemory 320f448 2 173d674
|
||||
nvnCommandBufferGetCommandMemorySize 320f450 0 -
|
||||
nvnCommandBufferGetCommandMemoryUsed 320f458 0 -
|
||||
nvnCommandBufferGetCommandMemoryFree 320f460 0 -
|
||||
nvnCommandBufferGetControlMemorySize 320f468 0 -
|
||||
nvnCommandBufferGetControlMemoryUsed 320f470 0 -
|
||||
nvnCommandBufferGetControlMemoryFree 320f478 0 -
|
||||
nvnCommandBufferBeginRecording 320f480 3 173d684
|
||||
nvnCommandBufferEndRecording 320f488 3 173d6ec
|
||||
nvnCommandBufferCallCommands 320f490 0 -
|
||||
nvnCommandBufferCopyCommands 320f498 1 17e2f88
|
||||
nvnCommandBufferBindBlendState 320f4a0 1 1746618
|
||||
nvnCommandBufferBindChannelMaskState 320f4a8 1 1746644
|
||||
nvnCommandBufferBindColorState 320f4b0 1 174665c
|
||||
nvnCommandBufferBindMultisampleState 320f4b8 2 17466ec
|
||||
nvnCommandBufferBindPolygonState 320f4c0 1 1746674
|
||||
nvnCommandBufferBindDepthStencilState 320f4c8 1 1746578
|
||||
nvnCommandBufferBindVertexAttribState 320f4d0 1 1746740
|
||||
nvnCommandBufferBindVertexStreamState 320f4d8 1 174675c
|
||||
nvnCommandBufferBindProgram 320f4e0 4 174671c
|
||||
nvnCommandBufferBindVertexBuffer 320f4e8 0 -
|
||||
nvnCommandBufferBindVertexBuffers 320f4f0 2 17472a8
|
||||
nvnCommandBufferBindUniformBuffer 320f4f8 9 174690c
|
||||
nvnCommandBufferBindUniformBuffers 320f500 3 1746898
|
||||
nvnCommandBufferBindTransformFeedbackBuffer 320f508 0 -
|
||||
nvnCommandBufferBindTransformFeedbackBuffers 320f510 0 -
|
||||
nvnCommandBufferBindStorageBuffer 320f518 2 174a160
|
||||
nvnCommandBufferBindStorageBuffers 320f520 3 1746868
|
||||
nvnCommandBufferBindTexture 320f528 0 -
|
||||
nvnCommandBufferBindTextures 320f530 0 -
|
||||
nvnCommandBufferBindImage 320f538 0 -
|
||||
nvnCommandBufferBindImages 320f540 3 1746814
|
||||
nvnCommandBufferSetPatchSize 320f548 1 1746780
|
||||
nvnCommandBufferSetInnerTessellationLevels 320f550 0 -
|
||||
nvnCommandBufferSetOuterTessellationLevels 320f558 0 -
|
||||
nvnCommandBufferSetPrimitiveRestart 320f560 0 -
|
||||
nvnCommandBufferBeginTransformFeedback 320f568 0 -
|
||||
nvnCommandBufferEndTransformFeedback 320f570 0 -
|
||||
nvnCommandBufferPauseTransformFeedback 320f578 0 -
|
||||
nvnCommandBufferResumeTransformFeedback 320f580 0 -
|
||||
nvnCommandBufferDrawTransformFeedback 320f588 0 -
|
||||
nvnCommandBufferDrawArrays 320f590 2 17473a0
|
||||
nvnCommandBufferDrawElements 320f598 0 -
|
||||
nvnCommandBufferDrawElementsBaseVertex 320f5a0 0 -
|
||||
nvnCommandBufferDrawArraysInstanced 320f5a8 0 -
|
||||
nvnCommandBufferDrawElementsInstanced 320f5b0 2 17474d4
|
||||
nvnCommandBufferDrawArraysIndirect 320f5b8 0 -
|
||||
nvnCommandBufferDrawElementsIndirect 320f5c0 0 -
|
||||
nvnCommandBufferMultiDrawArraysIndirectCount 320f5c8 2 1747614
|
||||
nvnCommandBufferMultiDrawElementsIndirectCount 320f5d0 2 1747784
|
||||
nvnCommandBufferClearColor 320f5d8 1 1745c98
|
||||
nvnCommandBufferClearColori 320f5e0 0 -
|
||||
nvnCommandBufferClearColorui 320f5e8 0 -
|
||||
nvnCommandBufferClearDepthStencil 320f5f0 1 1745d00
|
||||
nvnCommandBufferDispatchCompute 320f5f8 3 1747f7c
|
||||
nvnCommandBufferDispatchComputeIndirect 320f600 2 174809c
|
||||
nvnCommandBufferSetViewport 320f608 0 -
|
||||
nvnCommandBufferSetViewports 320f610 2 1747894
|
||||
nvnCommandBufferSetViewportSwizzles 320f618 0 -
|
||||
nvnCommandBufferSetScissor 320f620 4 1745c3c
|
||||
nvnCommandBufferSetScissors 320f628 0 -
|
||||
nvnCommandBufferSetDepthRange 320f630 2 17478bc
|
||||
nvnCommandBufferSetDepthBounds 320f638 3 17465ec
|
||||
nvnCommandBufferSetDepthRanges 320f640 0 -
|
||||
nvnCommandBufferSetTiledCacheAction 320f648 0 -
|
||||
nvnCommandBufferSetTiledCacheTileSize 320f650 0 -
|
||||
nvnCommandBufferBindSeparateTexture 320f658 0 -
|
||||
nvnCommandBufferBindSeparateSampler 320f660 4 1746464
|
||||
nvnCommandBufferBindSeparateTextures 320f668 3 1746880
|
||||
nvnCommandBufferBindSeparateSamplers 320f670 2 17468b0
|
||||
nvnCommandBufferSetStencilValueMask 320f678 1 17465a4
|
||||
nvnCommandBufferSetStencilMask 320f680 1 17465c0
|
||||
nvnCommandBufferSetStencilRef 320f688 2 17479fc
|
||||
nvnCommandBufferSetBlendColor 320f690 2 1747a2c
|
||||
nvnCommandBufferSetPointSize 320f698 0 -
|
||||
nvnCommandBufferSetLineWidth 320f6a0 0 -
|
||||
nvnCommandBufferSetPolygonOffsetClamp 320f6a8 1 1746694
|
||||
nvnCommandBufferSetAlphaRef 320f6b0 0 -
|
||||
nvnCommandBufferSetSampleMask 320f6b8 0 -
|
||||
nvnCommandBufferSetRasterizerDiscard 320f6c0 0 -
|
||||
nvnCommandBufferSetDepthClamp 320f6c8 0 -
|
||||
nvnCommandBufferSetConservativeRasterEnable 320f6d0 0 -
|
||||
nvnCommandBufferSetConservativeRasterDilate 320f6d8 0 -
|
||||
nvnCommandBufferSetSubpixelPrecisionBias 320f6e0 0 -
|
||||
nvnCommandBufferCopyBufferToTexture 320f6e8 1 1745390
|
||||
nvnCommandBufferCopyTextureToBuffer 320f6f0 1 17454cc
|
||||
nvnCommandBufferCopyTextureToTexture 320f6f8 3 1745238
|
||||
nvnCommandBufferCopyBufferToBuffer 320f700 4 1745050
|
||||
nvnCommandBufferClearBuffer 320f708 0 -
|
||||
nvnCommandBufferClearTexture 320f710 2 1745894
|
||||
nvnCommandBufferClearTexturei 320f718 0 -
|
||||
nvnCommandBufferClearTextureui 320f720 3 17460c0
|
||||
nvnCommandBufferUpdateUniformBuffer 320f728 1 174a140
|
||||
nvnCommandBufferReportCounter 320f730 8 1748f24
|
||||
nvnCommandBufferResetCounter 320f738 2 1744d60
|
||||
nvnCommandBufferReportValue 320f740 0 -
|
||||
nvnCommandBufferSetRenderEnable 320f748 0 -
|
||||
nvnCommandBufferSetRenderEnableConditional 320f750 0 -
|
||||
nvnCommandBufferSetRenderTargets 320f758 1 1745bdc
|
||||
nvnCommandBufferDiscardColor 320f760 0 -
|
||||
nvnCommandBufferDiscardDepthStencil 320f768 0 -
|
||||
nvnCommandBufferDownsample 320f770 0 -
|
||||
nvnCommandBufferTiledDownsample 320f778 0 -
|
||||
nvnCommandBufferDownsampleTextureView 320f780 2 1745e54
|
||||
nvnCommandBufferTiledDownsampleTextureView 320f788 0 -
|
||||
nvnCommandBufferBarrier 320f790 33 1744e74
|
||||
nvnCommandBufferWaitSync 320f798 4 1748280
|
||||
nvnCommandBufferFenceSync 320f7a0 0 -
|
||||
nvnCommandBufferSetTexturePool 320f7a8 1 173d6c0
|
||||
nvnCommandBufferSetSamplerPool 320f7b0 1 173d6dc
|
||||
nvnCommandBufferSetShaderScratchMemory 320f7b8 1 173d6a8
|
||||
nvnCommandBufferSaveZCullData 320f7c0 0 -
|
||||
nvnCommandBufferRestoreZCullData 320f7c8 0 -
|
||||
nvnCommandBufferSetCopyRowStride 320f7d0 2 1745368
|
||||
nvnCommandBufferSetCopyImageStride 320f7d8 0 -
|
||||
nvnCommandBufferGetCopyRowStride 320f7e0 0 -
|
||||
nvnCommandBufferGetCopyImageStride 320f7e8 0 -
|
||||
nvnCommandBufferDrawTexture 320f7f0 0 -
|
||||
nvnProgramSetSubroutineLinkage 320f7f8 0 -
|
||||
nvnCommandBufferSetProgramSubroutines 320f800 0 -
|
||||
nvnCommandBufferBindCoverageModulationTable 320f808 0 -
|
||||
nvnCommandBufferResolveDepthBuffer 320f810 0 -
|
||||
nvnCommandBufferSetColorReductionEnable 320f818 0 -
|
||||
nvnCommandBufferSetColorReductionThresholds 320f820 0 -
|
||||
nvnCommandBufferPushDebugGroupStatic 320f828 0 -
|
||||
nvnCommandBufferPushDebugGroupDynamic 320f830 2 1748c88
|
||||
nvnCommandBufferPushDebugGroup 320f838 0 -
|
||||
nvnCommandBufferPopDebugGroup 320f840 2 1748d68
|
||||
nvnCommandBufferPopDebugGroupId 320f848 0 -
|
||||
nvnCommandBufferInsertDebugMarkerStatic 320f850 0 -
|
||||
nvnCommandBufferInsertDebugMarkerDynamic 320f858 2 1748e18
|
||||
nvnCommandBufferInsertDebugMarker 320f860 0 -
|
||||
nvnCommandBufferGetMemoryCallback 320f868 0 -
|
||||
nvnCommandBufferGetMemoryCallbackData 320f870 0 -
|
||||
nvnCommandBufferIsRecording 320f878 2 1744f14
|
||||
nvnSyncInitialize 320f880 2 173cf14
|
||||
nvnSyncFinalize 320f888 2 173dffc
|
||||
nvnSyncSetDebugLabel 320f890 0 -
|
||||
nvnQueueFenceSync 320f898 1 1744420
|
||||
nvnSyncWait 320f8a0 2 17439fc
|
||||
nvnQueueWaitSync 320f8a8 0 -
|
||||
nvnSyncInitializeFromFencedGLSync 320f8b0 0 -
|
||||
nvnSyncCreateGLSync 320f8b8 0 -
|
||||
nvnEventBuilderSetDefaults 320f8c0 2 173f7a4
|
||||
nvnEventBuilderSetStorage 320f8c8 2 173f7b8
|
||||
nvnEventBuilderGetStorage 320f8d0 0 -
|
||||
nvnEventBuilderGetMemoryPool 320f8d8 0 -
|
||||
nvnEventBuilderGetMemoryOffset 320f8e0 0 -
|
||||
nvnEventInitialize 320f8e8 2 173f7c8
|
||||
nvnEventFinalize 320f8f0 2 17424fc
|
||||
nvnEventGetValue 320f8f8 1 1743a78
|
||||
nvnEventSignal 320f900 2 173f7e0
|
||||
nvnEventGetMemoryPool 320f908 0 -
|
||||
nvnEventGetMemoryOffset 320f910 0 -
|
||||
nvnCommandBufferWaitEvent 320f918 4 1744130
|
||||
nvnCommandBufferSignalEvent 320f920 8 17440b8
|
||||
nvnCommandBufferSetStencilCullCriteria 320f928 0 -
|
||||
nvnCommandBufferInsertCheckpointStatic 320f930 0 -
|
||||
nvnCommandBufferInsertCheckpointDynamic 320f938 0 -
|
||||
nvnDeviceSetAutomaticCheckpointsEnable 320f940 0 -
|
||||
nvnDeviceSetAutomaticCheckpointsSamplingInterval 320f948 0 -
|
||||
nvnDeviceSetAutomaticCheckpointsByDebugGroup 320f950 0 -
|
||||
nvnDeviceSetCheckpointCallStacksEnable 320f958 0 -
|
||||
nvnDeviceSetCheckpointMaxCallStacksDepth 320f960 0 -
|
||||
|
@@ -1,8 +1,8 @@
|
||||
# NVN → Metal object mapping
|
||||
|
||||
This document lists what each kind of NVN (the Switch's native graphics interface) object and command lands on in Metal within D2R Mobile's graphics layer. For the overall structure of the graphics layer see [README.md](README.md).
|
||||
This document lists what each kind of NVN (the Switch's native graphics interface) object and command lands on in Metal in the host's graphics layer. For the overall structure of the graphics layer see [README.md](README.md).
|
||||
|
||||
Scope: the 216 NVN functions that have call sites in the game's main program (listed in `nvn/nvn_slots.tsv`). The remaining 307 are connected to a red-reporting stub that, when called, prints `[RED] NVN unimplemented function called: <name>` in the log.
|
||||
Scope: the NVN functions games call. The others are connected to a red-reporting stub that, when called, prints `[RED] NVN unimplemented function called: <name>` in the log.
|
||||
|
||||
Two conventions that run through the whole table:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Ordering race between "set read-only" and "unprotect before write" in dirty-page tracking (from d2r-mobile ef9b1ff13's t_dirty_race.c, stubs adapted to this repository's tracking table).
|
||||
// Ordering race between "set read-only" and "unprotect before write" in dirty-page tracking (originally a test of the host this one started as, stubs adapted to this repository's tracking table).
|
||||
// Scene: thread A, in nvn_dirty_protect, has already recorded the page state as "protected" while its mprotect has not landed yet; thread B now arrives with prep_write (the unprotect done before the host writes into guest memory,
|
||||
// for example the movie decoder thread writing a frame into texture storage, or a disk read into a guest buffer). Old implementation: B sets the state back to "none", then A's mprotect lands => the page is read-only while its state is "none" --
|
||||
// when the host reads a file into it again: prep_write sees state "none" and skips, the system call does not go through the page-fault handler and fails with EFAULT; when a game thread writes directly: the page-fault handler only returns "re-execute" and spins forever.
|
||||
|
||||
+2
-17
@@ -26,7 +26,6 @@ Only GPUs with BC compressed textures are supported (`supportsBCTextureCompressi
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
make -C dm/gfx/tex/tests samples # fetch real-data samples (see below)
|
||||
make -C dm/gfx/tex/tests run # run t_fmt_mtl and t_tex
|
||||
```
|
||||
|
||||
@@ -38,25 +37,11 @@ make -C dm/gfx/tex/tests run # run t_fmt_mtl and t_tex
|
||||
| A round trip | Writes random data into each level and layer and reads it back out; must be byte-for-byte equal. Covers 2D, 2D array and 3D, several bytes-per-block values and non-power-of-two sizes |
|
||||
| B address formula | The result of `nvn_bl_offset()` equals the address computed by hand from the public layout rules |
|
||||
| E hand-made 3D | Places markers at hand-computed addresses, checks where they land after deswizzling, and checks block height, block depth and storage size |
|
||||
| C real data | The Switch version (block linear) of a texture, after deswizzling, is compared with the PC version (linear) of the same texture: those at the same resolution must be byte-for-byte equal; those whose Switch version was reduced in resolution were recompressed, so the comparison is instead that the PSNR after decoding (peak signal-to-noise ratio, a measure of how alike two images are) is not below 22 dB. It also checks that the per-level offsets in the file agree with `nvn_tex_level_offset` |
|
||||
| D negative control | Deliberately deswizzles with the block height off by one step; the result must no longer be byte-for-byte equal and the PSNR must be below 22 dB, proving that the criterion of group C can tell a wrong layout apart |
|
||||
|
||||
The A round trip can only prove "one-to-one and within bounds"; it cannot prove that the layout formula is right. What really backs it up is the hand-computed addresses of group B and the byte-for-byte equality of the same-resolution samples of group C.
|
||||
|
||||
**About the real-data samples**: groups C and D need one copy each of the Switch version and the PC version of the same texture. `tests/fetch_samples.py` takes samples from the game files in your workspace: the Switch version is read from `$DM_ROOT/data/switch/3.3.94012/Update/RomFS/data`, the PC version from `$DM_ROOT/data/assets-src/raw`, and the output goes to `$DM_ROOT/build/cache/dm/tex_samples`. When there are no samples, the real-data items of `t_tex` count as failed and are not skipped; groups A, B and E need no game files. The samples do not go into the repository.
|
||||
|
||||
## Layout-related fields in the texture file header
|
||||
|
||||
Facts used by `t_tex.c` when parsing the game's `.texture` files:
|
||||
|
||||
- The file begins with `<DE(`; the Switch version and the PC version use the same container.
|
||||
- In files that went through the Switch conversion, the four bytes starting at offset 0x14 are `00 00 00 07` (0 in the PC version), which indicates that the pixel area is in block-linear layout; offset 0x13 is the log2 of the level 0 block height, with the same meaning as byte 0 of the layout parameters the game passes to `SetPackagedTextureLayout` at run time.
|
||||
- Files without this marker are stored linearly and are byte-for-byte identical to the PC version.
|
||||
The A round trip can only prove "one-to-one and within bounds"; it cannot prove that the layout formula is right. What backs it up is the hand-computed addresses of groups B and E, and the games that render right.
|
||||
|
||||
## Limitations
|
||||
|
||||
- `nvn_deswizzle()` does not support multisampled textures, textures with the LINEAR flag (which are linear to begin with) or the BUFFER target; it returns -1 when it encounters them.
|
||||
- Cubes and cube arrays are handled as layered textures and have not been separately checked with real data.
|
||||
- 3D textures have only the round-trip and hand-computed address checks; there are no real-data samples.
|
||||
- Cubes and cube arrays are handled as layered textures.
|
||||
- The component bit order of the 16-bit packed formats (565 / 5551 / 4444) is mapped according to the format names and has not been checked with real data; they are marked as "substitute" in the format table.
|
||||
- The real-data sample set is small, covering the four formats RGBA8, BC1, BC3 and BC4.
|
||||
@@ -1,14 +1,9 @@
|
||||
DM_ROOT ?= $(HOME)/dm-work
|
||||
# Texture sub-module unit tests (seconds): make run. Real-data samples: make samples fetches them into $(SAMPLES) (build cache, not in the repo)
|
||||
SAMPLES ?= $(DM_ROOT)/build/cache/dm/tex_samples
|
||||
PY ?= $(DM_ROOT)/build/cache/dm/venv/bin/python
|
||||
# Texture sub-module unit tests (seconds): make run
|
||||
CFLAGS = -O2 -g -Wall -Wextra
|
||||
t_tex: t_tex.c ../nvn_deswizzle.c ../nvn_deswizzle.h ../../nvn/nvn_tex.c ../../nvn/nvn_tex.h
|
||||
clang $(CFLAGS) -o $@ t_tex.c ../nvn_deswizzle.c ../../nvn/nvn_tex.c -lm
|
||||
t_fmt_mtl: t_fmt_mtl.m ../nvn_fmt_mtl.c ../nvn_fmt_mtl.h
|
||||
clang $(CFLAGS) -fobjc-arc -o $@ t_fmt_mtl.m ../nvn_fmt_mtl.c -framework Metal -framework Foundation
|
||||
samples:
|
||||
$(PY) fetch_samples.py $(SAMPLES)
|
||||
run: t_tex t_fmt_mtl
|
||||
./t_fmt_mtl && ./t_tex $(SAMPLES)
|
||||
.PHONY: samples run
|
||||
./t_fmt_mtl && ./t_tex
|
||||
.PHONY: run
|
||||
@@ -1,26 +0,0 @@
|
||||
# Fetch real-data samples for cross-checking: one copy each of the same-named .texture from the Switch package (RomFS, block-linear) and the PC package (extracted originals, linear).
|
||||
# Usage: fetch_samples.py <output dir> → <output dir>/{dm,pc}/<index>.texture + list.tsv (index, path, note)
|
||||
# Sample selection: covers RGBA8 / BC1 / BC3 / BC4, sizes from 32² to 2048², including non-square; same-resolution ones are compared byte for byte, reduced-resolution ones by decoded PSNR.
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../tools'))
|
||||
DM_ROOT = os.environ.get('DM_ROOT') or os.path.expanduser('~/dm-work') # Workspace root (game files, build outputs and run artifacts are all under it)
|
||||
os.environ.setdefault('DMX_ROOT', os.path.join(DM_ROOT, 'data/switch/3.3.94012/Update/RomFS/data'))
|
||||
import dmx
|
||||
PC = os.path.join(DM_ROOT, 'data/assets-src/raw')
|
||||
SAMPLES = [
|
||||
('hd/vfx/textures/gradient/T_FX_perlin_wipe_03.texture', 'RGBA8 256² same resolution'),
|
||||
('hd/vfx/textures/gradient/T_FX_portal_gradient.texture', 'RGBA8 256×16 non-square same resolution'),
|
||||
('hd/env/texture/terrain/path/066_PATH.texture', 'BC4 32² same resolution'),
|
||||
('hd/debug/wasted_mip_size_2048.texture', 'BC3 sRGB 2048² 12 levels same resolution'),
|
||||
('hd/items/armor/armor/plate_mail/textures/armor_plate_mail_alb.texture', 'BC3 sRGB 256² (PC 1024²) reduced resolution'),
|
||||
('hd/env/texture/global/flow/act4/lava/lava_lavaNS_FLOW.texture', 'BC1 64² (PC 256²) reduced resolution'),
|
||||
('hd/character/enemy/fingermage1/Textures/fingermage_KTINT.texture', 'BC3 256² (PC 1024²) reduced resolution'),
|
||||
]
|
||||
out = sys.argv[1]
|
||||
os.makedirs(f'{out}/dm', exist_ok=True); os.makedirs(f'{out}/pc', exist_ok=True)
|
||||
with open(f'{out}/list.tsv', 'w') as f:
|
||||
for i, (p, why) in enumerate(SAMPLES):
|
||||
open(f'{out}/dm/{i}.texture', 'wb').write(dmx.get('data:data/' + p))
|
||||
open(f'{out}/pc/{i}.texture', 'wb').write(open(os.path.join(PC, p), 'rb').read())
|
||||
f.write(f'{i}\t{p}\t{why}\n')
|
||||
print(len(SAMPLES), 'sample pairs →', out)
|
||||
+5
-141
@@ -1,5 +1,5 @@
|
||||
// Block-linear deswizzle unit tests (seconds). A round trip; B hand-calculated addresses; C real-data cross-check (Switch block-linear vs PC linear, including per-level offset verification); D wrong-block-height negative control.
|
||||
// Usage: t_tex <sample dir> (produced by fetch_samples.py). Missing samples count as red, not skipped.
|
||||
// Block-linear deswizzle unit tests (seconds). A round trip; B hand-calculated addresses; E hand-built 3D.
|
||||
// Usage: t_tex
|
||||
#include "../nvn_deswizzle.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
@@ -45,136 +45,7 @@ static void roundtrip(const char *tag, int target, int fmt, uint32_t w, uint32_t
|
||||
free(st);
|
||||
}
|
||||
|
||||
// ---------- Real data ----------
|
||||
static uint8_t *slurp(const char *p, size_t *n) {
|
||||
FILE *f = fopen(p, "rb"); if (!f) return NULL;
|
||||
fseek(f, 0, SEEK_END); *n = (size_t)ftell(f); fseek(f, 0, SEEK_SET);
|
||||
uint8_t *b = malloc(*n); if (fread(b, 1, *n, f) != *n) { free(b); b = NULL; } fclose(f); return b;
|
||||
}
|
||||
// Files converted for Switch: the four bytes at 0x14 = 00 00 00 07 (0 on PC), 0x13 = log2 of the level-0 block height (same meaning as byte 0 of the game's SetPackagedTextureLayout).
|
||||
// Unconverted ones (e.g. those under hd/debug) are byte-identical to the PC files and stored linearly.
|
||||
typedef struct { int code, nvn, bl, th_hdr; uint32_t w, h, mips, hdr; uint32_t size[16]; uint64_t off[16]; const uint8_t *px; } tex_t;
|
||||
static int d2r_nvn(int code) { // D2R .texture format code → NVNformat
|
||||
switch (code) { case 0x1F: return 0x25; case 0x39: return 0x42; case 0x3A: return 0x46; case 0x3D: return 0x44;
|
||||
case 0x3E: return 0x48; case 0x3F: return 0x49; case 0x41: return 0x4B; }
|
||||
return 0;
|
||||
}
|
||||
static int parse(const uint8_t *b, size_t n, tex_t *t) {
|
||||
if (n < 0x24 || memcmp(b, "<DE(", 4)) return -1;
|
||||
t->code = b[4]; t->nvn = d2r_nvn(t->code); t->bl = b[0x14] == 0 && b[0x15] == 0 && b[0x16] == 0 && b[0x17] == 7; t->th_hdr = b[0x13];
|
||||
memcpy(&t->w, b + 8, 4); memcpy(&t->h, b + 12, 4); memcpy(&t->mips, b + 0x1C, 4);
|
||||
if (!t->nvn || t->mips == 0 || t->mips > 16) return -1;
|
||||
t->hdr = 36 + 8 * t->mips; uint64_t o = 0;
|
||||
for (unsigned i = 0; i < t->mips; i++) { memcpy(&t->size[i], b + 0x24 + 8 * i, 4); t->off[i] = o; o += t->size[i]; }
|
||||
if (t->hdr + o != n) return -1;
|
||||
t->px = b + t->hdr; return 0;
|
||||
}
|
||||
|
||||
// Decode to 8-bit RGB (BC1 / BC3 take the color block, BC4 takes the single channel replicated three times, RGBA8 takes RGB). Row = block row, expanded as 4×4
|
||||
static void bc1(const uint8_t *b, uint8_t out[16][3]) {
|
||||
unsigned c0 = b[0] | b[1] << 8, c1 = b[2] | b[3] << 8; uint32_t bits = b[4] | b[5] << 8 | b[6] << 16 | (uint32_t)b[7] << 24;
|
||||
int p[4][3];
|
||||
for (int k = 0; k < 2; k++) { unsigned c = k ? c1 : c0; p[k][0] = (c >> 11 & 31) * 255 / 31; p[k][1] = (c >> 5 & 63) * 255 / 63; p[k][2] = (c & 31) * 255 / 31; }
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (c0 > c1) { p[2][j] = (2 * p[0][j] + p[1][j]) / 3; p[3][j] = (p[0][j] + 2 * p[1][j]) / 3; }
|
||||
else { p[2][j] = (p[0][j] + p[1][j]) / 2; p[3][j] = 0; }
|
||||
}
|
||||
for (int i = 0; i < 16; i++) for (int j = 0; j < 3; j++) out[i][j] = (uint8_t)p[bits >> (2 * i) & 3][j];
|
||||
}
|
||||
static void bc4(const uint8_t *b, uint8_t out[16][3]) {
|
||||
int a0 = b[0], a1 = b[1], v[8] = {a0, a1};
|
||||
for (int k = 2; k < 8; k++) v[k] = a0 > a1 ? ((8 - k) * a0 + (k - 1) * a1) / 7 : k < 6 ? ((6 - k) * a0 + (k - 1) * a1) / 5 : (k == 6 ? 0 : 255);
|
||||
uint64_t bits = 0; for (int k = 0; k < 6; k++) bits |= (uint64_t)b[2 + k] << (8 * k);
|
||||
for (int i = 0; i < 16; i++) out[i][0] = out[i][1] = out[i][2] = (uint8_t)v[bits >> (3 * i) & 7];
|
||||
}
|
||||
static uint8_t *decode(int nvn, const uint8_t *lin, uint32_t w, uint32_t h) { // → w×h×3
|
||||
uint8_t *img = calloc((size_t)w * h, 3);
|
||||
if (nvn == 0x25) { for (size_t i = 0; i < (size_t)w * h; i++) memcpy(img + 3 * i, lin + 4 * i, 3); return img; }
|
||||
uint32_t bwn = (w + 3) / 4, bhn = (h + 3) / 4, bpb = (nvn == 0x42 || nvn == 0x46 || nvn == 0x49) ? 8 : 16;
|
||||
for (uint32_t by = 0; by < bhn; by++) for (uint32_t bx = 0; bx < bwn; bx++) {
|
||||
const uint8_t *b = lin + ((size_t)by * bwn + bx) * bpb; uint8_t px[16][3];
|
||||
if (nvn == 0x49) bc4(b, px); else bc1(bpb == 16 ? b + 8 : b, px);
|
||||
for (int i = 0; i < 16; i++) { uint32_t x = bx * 4 + i % 4, y = by * 4 + i / 4; if (x < w && y < h) memcpy(img + 3 * ((size_t)y * w + x), px[i], 3); }
|
||||
}
|
||||
return img;
|
||||
}
|
||||
static double psnr(const uint8_t *a, const uint8_t *b, size_t n) {
|
||||
double e = 0; for (size_t i = 0; i < n; i++) { double d = (double)a[i] - b[i]; e += d * d; }
|
||||
return e == 0 ? 99 : 10 * log10(255.0 * 255.0 / (e / n));
|
||||
}
|
||||
// Decode one level with a specified block height (for the negative control; addresses beyond the storage read as 0)
|
||||
static void deswz_tile(const uint8_t *st, uint64_t st_size, uint64_t base, uint32_t rb, uint32_t rows, unsigned th, uint8_t *out) {
|
||||
for (uint32_t y = 0; y < rows; y++) for (uint32_t x = 0; x < rb; x++) {
|
||||
uint64_t a = base + nvn_bl_offset(x, y, 0, rb, rows, th, 0);
|
||||
out[(size_t)y * rb + x] = a < st_size ? st[a] : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Criterion threshold: the correct block height must be ≥ this value and a wrong block height must be < it. Measured: correct 28.45–38.90 dB (the 3 reduced-resolution recompressed ones; same-resolution ones are byte-identical),
|
||||
// block height off by one step 6.94–16.64 dB ⇒ take the midpoint of the two sides, 22 dB, leaving about 6 dB on each side
|
||||
#define PSNR_PASS 22.0
|
||||
|
||||
static void real_pair(const char *dir, int i, const char *why) {
|
||||
char p[1024]; size_t ns, np;
|
||||
snprintf(p, sizeof p, "%s/dm/%d.texture", dir, i); uint8_t *sb = slurp(p, &ns);
|
||||
snprintf(p, sizeof p, "%s/pc/%d.texture", dir, i); uint8_t *pb = slurp(p, &np);
|
||||
tex_t s, c;
|
||||
CHECK(sb && pb && !parse(sb, ns, &s) && !parse(pb, np, &c), "sample %d read / parse failed (%s)", i, why);
|
||||
if (!sb || !pb || parse(sb, ns, &s) || parse(pb, np, &c)) return;
|
||||
if (!s.bl) { // not converted for Switch: linear, should be byte-identical to the PC original
|
||||
CHECK(ns == np && !memcmp(sb, pb, ns), "sample %d has no block-linear marker but differs from the PC original", i);
|
||||
printf(" C %d %-30s no Switch block-linear marker (0x14 = 0) → stored linearly, %s the PC original (%zu bytes)\n", i, why, ns == np && !memcmp(sb, pb, ns) ? "byte-identical to" : "differs from", ns);
|
||||
free(sb); free(pb); return;
|
||||
}
|
||||
nvn_texdesc d = {1, s.nvn, 0, 0, s.w, s.h, 1, s.mips, 0, s.th_hdr + 1}; // block height follows the file header (same as the packaged layout parameters the game passes in)
|
||||
unsigned th_auto, td_auto; { nvn_texdesc a = d; a.pkg_block_h = 0; nvn_tex_level_tiles(&a, 0, &th_auto, &td_auto); }
|
||||
// ③ Per-level offsets in the file (cumulative mip table) vs nvn_tex_level_offset
|
||||
int off_ok = 1;
|
||||
for (unsigned l = 0; l < s.mips; l++)
|
||||
if (s.off[l] != nvn_tex_level_offset(&d, l)) { off_ok = 0; printf(" ✗ sample %d level %u file offset %llu ≠ nvn_tex %llu\n", i, l, (unsigned long long)s.off[l], (unsigned long long)nvn_tex_level_offset(&d, l)); }
|
||||
nvn_texlayout L; nvn_tex_layout(&d, &L);
|
||||
uint64_t file_total = s.off[s.mips - 1] + s.size[s.mips - 1];
|
||||
checks++; if (!off_ok) fails++;
|
||||
CHECK(file_total == L.size, "sample %d file pixel area %llu ≠ nvn_tex total size %llu", i, (unsigned long long)file_total, (unsigned long long)L.size);
|
||||
// Corresponding PC level: PC side length = Switch × 2^k
|
||||
unsigned k = 0; while ((s.w << k) < c.w) k++;
|
||||
if ((s.w << k) != c.w || (s.h << k) != c.h) { CHECK(0, "sample %d size ratio is not a power of 2", i); return; }
|
||||
unsigned nl = s.mips < c.mips - k ? s.mips : c.mips - k;
|
||||
uint64_t eq = 0, tot = 0; double ps = 0, ps_bad = 99; unsigned th0, td0;
|
||||
nvn_tex_level_tiles(&d, 0, &th0, &td0);
|
||||
for (unsigned l = 0; l < nl; l++) {
|
||||
uint32_t rb, rows, sl; dims(&d, l, &rb, &rows, &sl);
|
||||
uint8_t *lin = malloc((size_t)rb * rows);
|
||||
CHECK(!nvn_deswizzle(&d, l, 0, s.px, lin, rb), "sample %d level %u deswizzle failed", i, l);
|
||||
const uint8_t *pcl = c.px + c.off[l + k];
|
||||
CHECK(c.size[l + k] >= (uint64_t)rb * rows, "sample %d level %u PC level too small", i, l);
|
||||
if (!k) { for (size_t j = 0; j < (size_t)rb * rows; j++) eq += lin[j] == pcl[j]; tot += (uint64_t)rb * rows; }
|
||||
if (l == 0) {
|
||||
uint32_t w = s.w, h = s.h;
|
||||
uint8_t *a = decode(s.nvn, lin, w, h), *b = decode(c.nvn, pcl, w, h); ps = psnr(a, b, (size_t)w * h * 3);
|
||||
for (int dt = -1; dt <= 1; dt += 2) { // D: block height off by one step (when only 1 GOB high, block height does not affect the layout; not applicable)
|
||||
if ((int)th0 + dt < 0 || rows <= 8) continue;
|
||||
uint8_t *bad = malloc((size_t)rb * rows); deswz_tile(s.px, ns - s.hdr, 0, rb, rows, th0 + dt, bad);
|
||||
uint8_t *e = decode(s.nvn, bad, w, h); double q = psnr(e, b, (size_t)w * h * 3); if (q < ps_bad) ps_bad = q;
|
||||
if (!k) CHECK(memcmp(bad, pcl, (size_t)rb * rows), "sample %d block height %+d still byte-identical (negative control ineffective)", i, dt);
|
||||
free(bad); free(e);
|
||||
}
|
||||
free(a); free(b);
|
||||
}
|
||||
free(lin);
|
||||
}
|
||||
if (!k) CHECK(eq == tot, "sample %d identical bytes %llu/%llu", i, (unsigned long long)eq, (unsigned long long)tot);
|
||||
CHECK(ps >= PSNR_PASS, "sample %d correct block height PSNR %.2f < %.0f", i, ps, PSNR_PASS);
|
||||
uint32_t rows0 = (s.h + 3) / 4; if (s.nvn == 0x25) rows0 = s.h;
|
||||
if (rows0 > 8) CHECK(ps_bad < PSNR_PASS, "sample %d wrong block height PSNR %.2f ≥ %.0f (negative control ineffective)", i, ps_bad, PSNR_PASS);
|
||||
char neg[48]; if (rows0 > 8) snprintf(neg, sizeof neg, "%.2f", ps_bad); else snprintf(neg, sizeof neg, "n/a (1 GOB high)");
|
||||
printf(" C %d %-30s %4u×%-4u→PC level %u header block height %d (auto %u) offsets %s %s PSNR correct %.2f / wrong block height %s dB\n", i, why, s.w, s.h, k,
|
||||
s.th_hdr, th_auto, off_ok ? "✓" : "✗", k ? "(reduced resolution, bytes not compared)" : (eq == tot ? "byte-identical" : "bytes differ"), ps, neg);
|
||||
if (!k) printf(" identical bytes %llu/%llu\n", (unsigned long long)eq, (unsigned long long)tot);
|
||||
free(sb); free(pb);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int main(void) {
|
||||
printf("A round trip (random data, non-power-of-2 sizes)\n");
|
||||
roundtrip("2D 100×37 R8 4 levels", 1, 0x01, 100, 37, 1, 4);
|
||||
roundtrip("2D 100×37 RG8 4 levels", 1, 0x0D, 100, 37, 1, 4);
|
||||
@@ -237,16 +108,9 @@ int main(int argc, char **argv) {
|
||||
// Shape of the real color lookup table on the character select screen: 32³ RGBA16F (ser 17 / 16). Block depth 32 + 16 = 48 ≥ 16 → 16 slices; row 256 bytes = 4 GOBs wide, 32 rows = 4 GOBs high, 2 block layers → 4·4·2·8192 = 262144
|
||||
nvn_texdesc lut = {2, 0x29, 0, 0, 32, 32, 32, 1, 0, 0}; nvn_tex_layout(&lut, &L);
|
||||
CHECK(L.tileH == 0 && L.tileD == 4 && L.size == 4 * 4 * 2 * 8192ull, "E 32³ RGBA16F block height/depth %u/%u storage %llu, expected 0/4, 262144", L.tileH, L.tileD, (unsigned long long)L.size);
|
||||
printf(" E 32³ RGBA16F (shape of the character select color lookup table) block height/depth %u/%u, storage %llu (expected 0/4, 262144)\n", L.tileH, L.tileD, (unsigned long long)L.size);
|
||||
printf(" E 32³ RGBA16F (the shape of a 32³ color lookup table) block height/depth %u/%u, storage %llu (expected 0/4, 262144)\n", L.tileH, L.tileD, (unsigned long long)L.size);
|
||||
}
|
||||
|
||||
printf("C/D real-data cross-check (Switch block-linear vs PC linear) + wrong-block-height negative control, threshold %.0f dB\n", PSNR_PASS);
|
||||
const char *dir = argc > 1 ? argv[1] : getenv("DM_TEX_SAMPLES");
|
||||
char lp[1024]; snprintf(lp, sizeof lp, "%s/list.tsv", dir ? dir : "."); FILE *f = dir ? fopen(lp, "r") : NULL;
|
||||
CHECK(f, "sample directory missing (run make samples first)"); int npair = 0;
|
||||
if (f) { char line[1024]; while (fgets(line, sizeof line, f)) { int i; char path[512], why[256];
|
||||
if (sscanf(line, "%d\t%511[^\t]\t%255[^\n]", &i, path, why) == 3) { real_pair(dir, i, why); npair++; } } fclose(f); }
|
||||
CHECK(npair >= 3, "fewer than 3 real-data sample pairs (%d)", npair);
|
||||
printf("%s: %d/%d checks passed (%d real-data pairs)\n", fails ? "RED" : "all passed", checks - fails, checks, npair);
|
||||
printf("%s: %d/%d checks passed\n", fails ? "RED" : "all passed", checks - fails, checks);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Runs the shader build chain on a machine without Apple's Metal compiler (Windows / Linux / a Mac without Xcode; for use by tools/d2rm_pack.py).
|
||||
# Usage: nometal_run.py <script.py> [args...] Example: nometal_run.py dmsh_build.py <package dir> <cache dir>
|
||||
# How: not a single file of the build chain is modified (their contents feed into the translation chain version, and any change would force a whole new generation of the beta shader libraries); only, within this process, the one entry through which they call external commands
|
||||
# (subprocess.run) is taken over: the commands below are recognized and answered here, and everything else is passed through unchanged:
|
||||
# xcrun -sdk <x> metal --version → answers "none" (the compiler is recorded as unused in the tool fingerprint → the chain version forms its own branch, and its cache never mixes with normal mode's)
|
||||
# xcrun -sdk <x> metal -c <source> -o <intermediate file> → copies the source as the intermediate file (line endings normalized to LF)
|
||||
# xcrun -sdk <x> metallib <intermediate file> -o <library> → copies the intermediate file verbatim as the library
|
||||
# which <name> → looked up by Python itself
|
||||
# cp -c <a> <b> → ordinary copy
|
||||
# Result: the "library files" (*_b.metallib / *_bf.metallib) contain shader source. When the graphics layer of the shell (DM_SHELL build) finds that a library is not a compiled one, it compiles it on the device
|
||||
# (gfx/nvn/nvn_mtl.m); the system caches the compiled result, and from the second launch on nothing is compiled again.
|
||||
# The one check that is lost: in normal mode Apple's compiler also verifies along the way that every source compiles; here that is not verified, and one that fails to compile only reports a failure when it is used on the device.
|
||||
import os, runpy, shutil, subprocess, sys
|
||||
|
||||
_real = subprocess.run
|
||||
|
||||
|
||||
def _done(args, out=''):
|
||||
return subprocess.CompletedProcess(args, 0, stdout=out, stderr='')
|
||||
|
||||
|
||||
def run(args, *a, **k):
|
||||
text = k.get('text') or k.get('universal_newlines')
|
||||
enc = (lambda s: s) if text else (lambda s: s.encode())
|
||||
if isinstance(args, (list, tuple)) and args:
|
||||
c = [str(x) for x in args]
|
||||
if c[0] == 'xcrun' and 'metal' in c and '--version' in c: return _done(args, enc('none\n'))
|
||||
if c[0] == 'xcrun' and 'metal' in c and '-c' in c and '-o' in c: # source = the first argument after metal that is neither an option nor the value of -o (the iOS command also carries a version option after -c)
|
||||
o = c.index('-o'); src = [x for i, x in enumerate(c[c.index('metal') + 1:], c.index('metal') + 1) if not x.startswith('-') and i != o + 1]
|
||||
if len(src) != 1: raise RuntimeError('nometal_run: cannot tell the source argument in %r' % c)
|
||||
with open(src[0], 'rb') as f: b = f.read()
|
||||
with open(c[o + 1], 'wb') as f: f.write(b.replace(b'\r\n', b'\n')) # On Windows spirv-cross writes sources with CRLF line endings: normalize before they go into the install package, so the same input gives the same package on every system
|
||||
return _done(args, enc(''))
|
||||
if c[0] == 'xcrun' and 'metallib' in c and '-o' in c: shutil.copyfile(c[c.index('metallib') + 1], c[c.index('-o') + 1]); return _done(args, enc(''))
|
||||
if c[0] == 'xcrun': raise RuntimeError('nometal_run: unrecognized xcrun command %r' % c)
|
||||
if c[0] == 'which' and len(c) == 2: return _done(args, enc((shutil.which(c[1]) or '') + '\n'))
|
||||
if c[0] == 'cp' and len(c) == 4 and c[1] == '-c': shutil.copyfile(c[2], c[3]); return _done(args, enc(''))
|
||||
return _real(args, *a, **k)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2: sys.exit(__doc__ or 'usage: nometal_run.py <script.py> [args...]')
|
||||
subprocess.run = run
|
||||
script = os.path.abspath(sys.argv[1]); sys.argv = sys.argv[1:]; sys.path.insert(0, os.path.dirname(script))
|
||||
runpy.run_path(script, run_name='__main__')
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# main's NVN address-lookup table and static call surface (3.3.94012).
|
||||
# 1) Parse the NVN loader function in main (looks up addresses by name and stores them one by one into global pointer slots) → name → slot;
|
||||
# 2) Scan all of .text for "ldr xT,[xN,#imm]" slot reads: search backward from the read for the previous write to xN; it only counts as a call site if that write is an adrp to the slot's page.
|
||||
# (Forward register tracking treats stale values as slot addresses, or misses an adrp hoisted out of a loop; spot checks found both kinds of error, hence the backward trace.)
|
||||
# Usage: nvn_slots.py <main raw image .bin (extracted by dm/tools/nso.py)> <output tsv> [name table output]
|
||||
# Output columns: name, slot address, call site count, first call site; the name table is ordered by slot address (= load order).
|
||||
import struct, sys
|
||||
LOADER = 0x1739290 # NVN loader function inside main (3.3.94012; re-locate it first when changing versions)
|
||||
img, out = sys.argv[1], sys.argv[2]
|
||||
d = open(img, 'rb').read()
|
||||
TEXT_END = 0x1b786a0 # end of main .text (NSO segment header)
|
||||
N = TEXT_END // 4
|
||||
w = struct.unpack_from('<%dI' % N, d, 0)
|
||||
|
||||
def adrp(x, pc):
|
||||
imm = ((x >> 29) & 3) | (((x >> 5) & 0x7ffff) << 2)
|
||||
if imm & (1 << 20): imm -= 1 << 21
|
||||
return (pc & ~0xfff) + (imm << 12)
|
||||
def cstr(a): return d[a:d.index(b'\0', a)].decode()
|
||||
|
||||
# 1) Loader function: adrp/add x1 = name, str x0,[xn,#imm] = slot
|
||||
i, reg, cur, slot = LOADER // 4, {}, None, {}
|
||||
while w[i] != 0xd65f03c0:
|
||||
x, pc = w[i], i * 4
|
||||
if x & 0x9f000000 == 0x90000000: reg[x & 31] = adrp(x, pc)
|
||||
elif x & 0xffc00000 == 0x91000000 and (x & 31) == 1: cur = cstr(reg[(x >> 5) & 31] + ((x >> 10) & 0xfff))
|
||||
elif x & 0xffc00000 == 0xf9000000 and (x & 31) == 0: slot[reg[(x >> 5) & 31] + ((x >> 10) & 0xfff) * 8] = cur
|
||||
i += 1
|
||||
offs = {a & 0xfff for a in slot}
|
||||
|
||||
def writes(x, r):
|
||||
"""Whether x writes general-purpose register r (approximate, conservative)"""
|
||||
op0 = (x >> 25) & 0xf
|
||||
if x & 0xfc000000 == 0x94000000 or x & 0xfffffc1f == 0xd63f0000: return r <= 18 # bl / blr
|
||||
if op0 in (0b1000, 0b1001, 0b0101, 0b1101): return (x & 31) == r # data processing
|
||||
if op0 & 0b0101 == 0b0100: # memory load/store
|
||||
L = (x >> 22) & 1
|
||||
if (x >> 27) & 7 == 0b101:
|
||||
wb = (x >> 23) & 3 in (1, 3)
|
||||
return (L and ((x & 31) == r or ((x >> 10) & 31) == r)) or (wb and ((x >> 5) & 31) == r)
|
||||
if (x >> 27) & 7 == 0b111 and (x >> 24) & 3 == 0:
|
||||
wb = (x >> 21) & 1 == 0 and (x >> 10) & 3 in (1, 3)
|
||||
return (L and (x & 31) == r) or (wb and ((x >> 5) & 31) == r)
|
||||
if (x >> 27) & 7 == 0b111: return bool(L) and (x & 31) == r
|
||||
if (x >> 27) & 7 == 0b011: return (x & 31) == r
|
||||
return False
|
||||
|
||||
# 2) Attribute each slot read by tracing backward
|
||||
uses = {a: [] for a in slot}
|
||||
for i in range(N):
|
||||
x = w[i]
|
||||
if x & 0xffc00000 != 0xf9400000: continue
|
||||
off = ((x >> 10) & 0xfff) * 8
|
||||
rn = (x >> 5) & 31
|
||||
if off not in offs or rn == 31: continue
|
||||
for j in range(i - 1, max(i - 4000, -1), -1):
|
||||
y = w[j]
|
||||
if y == 0xd65f03c0: break
|
||||
if writes(y, rn):
|
||||
if y & 0x9f000000 == 0x90000000 and (y & 31) == rn and adrp(y, j * 4) + off in uses:
|
||||
uses[adrp(y, j * 4) + off].append(i * 4)
|
||||
break
|
||||
with open(out, 'w') as f:
|
||||
f.write('# kind: evidence main NVN fetch slots and static use sites (nvn_slots.py)\nname\tslot\tnuses\tfirst_use\n')
|
||||
for a in sorted(slot):
|
||||
u = uses[a]; f.write('%s\t%x\t%d\t%s\n' % (slot[a], a, len(u), '%x' % u[0] if u else '-'))
|
||||
if len(sys.argv) > 3: open(sys.argv[3], 'w').write(''.join(slot[a] + '\n' for a in sorted(slot)))
|
||||
used = sum(1 for a in slot if uses[a])
|
||||
print('slots %d, with use sites %d, use sites %d' % (len(slot), used, sum(len(v) for v in uses.values())))
|
||||
@@ -1,144 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Mapping table: Switch shader → PC shader (DXBC) → PC-chain MSL (metallib).
|
||||
# Basis: data/hd/shaders on both sides holds 162 SHAD packages with the same names, one BTCD record per variant, and record counts are equal package by package (10847 in total);
|
||||
# apart from sizes, record headers differ only in the platform number (PC 3 / Switch 5), and there is no permutation key ⇒ map one-to-one by index within the package.
|
||||
# Evidence of order alignment: compute shader thread group sizes are equal record by record, 22/22; for the 22 packages with ≥16 records, code size is rank-correlated, 0.81–1.00 when aligned and lower in every case when shifted by one record.
|
||||
# Switch record: after the header = GPU code segment (starts with 0x12345678), followed by = control segment (starts with 0x98761234, length in the record header).
|
||||
# At runtime nvnProgramSetShaders receives the control segment pointer ⇒ key = sha256(control segment)[:16].
|
||||
# PC record: contains one DXBC; PC-chain cache file name = sha256(DXBC)[:16].metallib (platform/shell/mtl12/shaders/build_cache.py).
|
||||
# Binding mapping (header comment of maxwell_refs.py): NVN slot number = D3D register number; verified record by record against what the Switch machine code actually references that "used by Switch ⊆ declared by PC".
|
||||
# On a key conflict (same Switch binary, different PC DXBC) pick one: the one with the smallest difference from the output count / cb / t / s actually used by Switch;
|
||||
# on a tie take the simplest (fewer outputs, fewer resources), on a further tie take the smallest DXBC hash (deterministic). The choice and the reason are written to <output>.conflicts.tsv.
|
||||
# Shaders embedded in the executable itself (not in packages): the Switch main raw image has 10 control segments; the first 6 are graphics shaders and map one-to-one, in the same order, to the 6 DXBC embedded in PC D2R.exe
|
||||
# (each side has a pair of identical duplicates at the same position); the last 4 are compute shaders with no PC counterpart, handled separately.
|
||||
# Usage: shader_map.py <PC package dir> <Switch package dir> <PC metallib cache dir> <output tsv> [<main raw image> <PC D2R.exe>]
|
||||
import hashlib, json, os, re, struct, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from maxwell_refs import bindings, vs_inputs, sph_stage
|
||||
|
||||
pc_dir, dm_dir, cache_dir, out = sys.argv[1:5]
|
||||
|
||||
def records(d):
|
||||
r = [m.start() for m in re.finditer(b'BTCDBTCD', d)]
|
||||
return [d[x:(r[k + 1] if k + 1 < len(r) else len(d))] for k, x in enumerate(r)]
|
||||
|
||||
def pc_dxbc(rec):
|
||||
i = rec.find(b'DXBC')
|
||||
return rec[i:i + struct.unpack_from('<I', rec, i + 24)[0]]
|
||||
|
||||
def dx_outputs(dx):
|
||||
j = dx.find(b'OSGN')
|
||||
if j < 0: return -1
|
||||
n = struct.unpack_from('<I', dx, j + 8)[0]
|
||||
return sum(1 for q in range(n) if struct.unpack_from('<I', dx, j + 16 + q * 24 + 8)[0] == 0)
|
||||
|
||||
def dm_parts(rec):
|
||||
i = rec.find(bytes.fromhex('34127698')) # control segment magic (little-endian 0x98761234)
|
||||
csz = struct.unpack_from('<I', rec, 0x28)[0] # record header: platform number 5, control segment length...
|
||||
return rec[i:i + csz], i
|
||||
|
||||
STAGE = {'vs': 'vertex', 'ps': 'pixel', 'cs': 'compute', 'hs': 'hull', 'ds': 'domain'}
|
||||
cache = set(os.listdir(cache_dir))
|
||||
rows, keys, cand, dxs, locs = [], {}, {}, {}, {}
|
||||
for name in sorted(os.listdir(dm_dir)):
|
||||
A = records(open(os.path.join(pc_dir, name), 'rb').read())
|
||||
B = records(open(os.path.join(dm_dir, name), 'rb').read())
|
||||
if len(A) != len(B): sys.exit(f'[RED] {name}: record counts differ, PC {len(A)} / Switch {len(B)}')
|
||||
for k, (a, b) in enumerate(zip(A, B)):
|
||||
dx = pc_dxbc(a); ctl, _ = dm_parts(b)
|
||||
if len(ctl) < 0x100: sys.exit(f'[RED] {name}#{k}: bad control section')
|
||||
swk = hashlib.sha256(ctl).hexdigest()[:16]; pck = hashlib.sha256(dx).hexdigest()[:16]
|
||||
ml = pck + '.metallib' in cache
|
||||
dm_out = struct.unpack_from('<I', ctl, 0x734)[0] if name.startswith('ps_') else -1
|
||||
pc_out = dx_outputs(dx) if name.startswith('ps_') else -1
|
||||
refl = json.load(open(os.path.join(cache_dir, pck + '.json')))['bindings'] if ml else []
|
||||
pcb = {x['reg'] for x in refl if x['type'] == 'cbuffer'}
|
||||
pt = {x['reg'] for x in refl if x['type'].startswith('texture') or x['type'].startswith('buffer')}
|
||||
ps_ = {x['reg'] for x in refl if x['type'] == 'sampler'}
|
||||
_, ci = dm_parts(b); sb = bindings(b[0x38:ci], name[:2])
|
||||
pbuf = {x['reg'] for x in refl if x['type'] in ('buffer_raw', 'buffer_structured', 'atomic_alias')}
|
||||
ok = int(sb['ubo'] <= pcb) | int(sb['tex'] <= pt) << 1 | int(sb['samp'] <= ps_) << 2 | int(sb['ssbo'] == pbuf) << 3
|
||||
fmt = lambda x: ','.join(map(str, sorted(x))) or '-'
|
||||
rows.append((name, k, STAGE.get(name[:2], name[:2]), swk, pck, int(ml), pc_out, dm_out,
|
||||
fmt(sb['ubo']), fmt(sb['tex']), fmt(sb['samp']), fmt(sb['ssbo']), ok))
|
||||
keys.setdefault(swk, set()).add(pck)
|
||||
cand.setdefault(swk, {})[pck] = (pc_out, dm_out, pcb, pt, ps_, sb)
|
||||
dxs[pck] = dx; locs.setdefault(swk, []).append(f'{name}#{k}')
|
||||
|
||||
if len(sys.argv) > 6:
|
||||
mi = open(sys.argv[5], 'rb').read(); pe = open(sys.argv[6], 'rb').read()
|
||||
dm_ctl = [m.start() for m in re.finditer(bytes.fromhex('34127698'), mi)]
|
||||
pc_dx = []
|
||||
for m in re.finditer(b'DXBC', pe):
|
||||
o = m.start(); sz = struct.unpack_from('<I', pe, o + 24)[0] if o + 32 <= len(pe) else 0
|
||||
if 64 < sz < 1 << 20 and o + sz <= len(pe) and struct.unpack_from('<I', pe, o + 20)[0] == 1: pc_dx.append(pe[o:o + sz])
|
||||
gfx = [o for o in dm_ctl if mi.rfind(bytes.fromhex('78563412'), o - 0x1000, o) > 0]
|
||||
# Pair by features (not by order: disassembly showed the embedded order differs between the two sides): stage is taken from SPH; set difference = symmetric difference of cb/t/s and (vertex) input slots, take the unique minimum
|
||||
def pc_feat(dx):
|
||||
h = hashlib.sha256(dx).hexdigest()[:16]
|
||||
j = json.load(open(os.path.join(cache_dir, h + '.json')))
|
||||
bs = j['bindings']
|
||||
return h, j['stage'], ({x['reg'] for x in bs if x['type'] == 'cbuffer'}, {x['reg'] for x in bs if x['type'].startswith('texture')},
|
||||
{x['reg'] for x in bs if x['type'] == 'sampler'}, {x['location'] for x in j['vertex_inputs']})
|
||||
pcs = [pc_feat(dx) for dx in pc_dx]
|
||||
for k, o in enumerate(gfx):
|
||||
g = mi.rfind(bytes.fromhex('78563412'), o - 0x1000, o)
|
||||
code = mi[g:o]; stg = sph_stage(code); sb = bindings(code, stg)
|
||||
dm = (sb['ubo'], sb['tex'], sb['samp'], vs_inputs(code) if stg == 'vs' else set())
|
||||
cands = sorted((sum(len(a ^ b) for a, b in zip(dm, f)), h) for h, st, f in pcs if st == stg)
|
||||
if not cands or (len(cands) > 1 and cands[0][0] == cands[1][0] and cands[0][1] != cands[1][1]):
|
||||
sys.exit(f'[RED] embedded shader @{o:#x} ({stg}): no unique PC counterpart: {cands}')
|
||||
pck = cands[0][1]; swk = hashlib.sha256(mi[o:o + 0x880]).hexdigest()[:16]
|
||||
ml = pck + '.metallib' in cache
|
||||
refl = json.load(open(os.path.join(cache_dir, pck + '.json')))['bindings'] if ml else []
|
||||
pbuf = {x['reg'] for x in refl if x['type'] in ('buffer_raw', 'buffer_structured', 'atomic_alias')}
|
||||
f = pcs[[h for h, _, _ in pcs].index(pck)][2]
|
||||
ok = int(dm[0] <= f[0]) | int(dm[1] <= f[1]) << 1 | int(dm[2] <= f[2]) << 2 | int(sb['ssbo'] == pbuf) << 3
|
||||
fmt = lambda x: ','.join(map(str, sorted(x))) or '-'
|
||||
rows.append(('main.rodata@%x' % o, k, STAGE.get(stg, stg), swk, pck, int(ml), -1, -1, fmt(sb['ubo']), fmt(sb['tex']), fmt(sb['samp']), fmt(sb['ssbo']), ok))
|
||||
keys.setdefault(swk, set()).add(pck)
|
||||
print(f' embedded @{o:#x} {stg} uses cb{fmt(dm[0])} t{fmt(dm[1])} s{fmt(dm[2])} inputs {fmt(dm[3])} → PC {pck} (difference {cands[0][0]}, runner-up {cands[1][0] if len(cands) > 1 else "-"})')
|
||||
print(f'embedded graphics shaders {len(gfx)} (with metallib {sum(1 for r in rows[-len(gfx):] if r[5])}), embedded compute shaders not mapped {len(dm_ctl) - len(gfx)}')
|
||||
|
||||
amb = {k: v for k, v in keys.items() if len(v) > 1}
|
||||
with open(out, 'w') as f:
|
||||
f.write('# kind: evidence Switch shader variants → PC DXBC → PC-chain metallib (dm/gfx/tools/shader_map.py)\n')
|
||||
f.write('# key conflicts (one Switch control section, several PC DXBC): %d\n' % len(amb))
|
||||
f.write('package\tindex\tstage\tdm_ctl_sha16\tpc_dxbc_sha16\tmetallib\tpc_outputs\tdm_outputs\tdm_ubo\tdm_tex\tdm_samp\tdm_ssbo\tsubset_ok(cb|t<<1|s<<2|ssbo_equal<<3)\n')
|
||||
for r in rows: f.write('\t'.join(map(str, r)) + '\n')
|
||||
|
||||
# Key conflict resolution
|
||||
def dx_chunks(d):
|
||||
out = {}
|
||||
for i in range(struct.unpack_from('<I', d, 28)[0]):
|
||||
o = struct.unpack_from('<I', d, 32 + 4 * i)[0]
|
||||
out[d[o:o + 4]] = d[o + 8:o + 8 + struct.unpack_from('<I', d, o + 4)[0]]
|
||||
return out
|
||||
def klass(pcs):
|
||||
ch = [dx_chunks(dxs[p]) for p in pcs]
|
||||
code = [c.get(b'SHEX', c.get(b'SHDR')) for c in ch]
|
||||
if len(set(code)) == 1: return 'same instruction block, only the input signature differs (picked by the vertex shader outputs when the pipeline is built)'
|
||||
if len({len(x) for x in code}) == 1: return 'instruction blocks of equal length, input signatures differ (probably renumbered input registers, also picked by pairing)'
|
||||
return 'different instruction blocks (real code differences Switch compiled into one binary; needs the package and index located at run time, or a visual check)'
|
||||
def score(c):
|
||||
po, so, pcb, pt, ps_, sb = c
|
||||
diff = (po != so and po >= 0) + len(pcb ^ sb['ubo']) + len(pt ^ sb['tex']) + len(ps_ ^ sb['samp'])
|
||||
return (diff, max(po, 0) + len(pcb) + len(pt) + len(ps_))
|
||||
with open(out + '.conflicts.tsv', 'w') as f:
|
||||
f.write('# kind: evidence PC candidate rulings for Switch control section key conflicts (smallest difference → simplest → smallest hash)\n')
|
||||
f.write('dm_ctl_sha16\tchosen_pc\tclass\tcandidates(pc:diff:size)\trecords\n')
|
||||
for k in sorted(amb):
|
||||
cs = sorted(cand[k].items(), key=lambda kv: (score(kv[1]), kv[0]))
|
||||
f.write('%s\t%s\t%s\t%s\t%s\n' % (k, cs[0][0], klass([p for p, _ in cs]), ' '.join('%s:%d:%d' % (p, *score(c)) for p, c in cs), ' '.join(locs[k])))
|
||||
|
||||
n = len(rows); ml = sum(r[5] for r in rows)
|
||||
okc = [sum(1 for r in rows if r[12] >> b & 1) for b in range(4)]
|
||||
print(f'binding subset holds: cb {okc[0]}/{len(rows)}, t {okc[1]}/{len(rows)}, s {okc[2]}/{len(rows)}; storage buffers equal {okc[3]}/{len(rows)}')
|
||||
uniq_dm = len(keys); uniq_pc = len({r[4] for r in rows}); uniq_pc_ml = len({r[4] for r in rows if r[5]})
|
||||
outd = [r for r in rows if r[6] >= 0 and r[6] != r[7]]
|
||||
print(f'records {n}; distinct Switch control sections {uniq_dm}; distinct PC DXBC {uniq_pc}; key conflicts {len(amb)}')
|
||||
print(f'records with a PC metallib {ml}/{n}; distinct PC DXBC with a metallib {uniq_pc_ml}/{uniq_pc}')
|
||||
print(f'pixel shaders whose output counts differ (PC has more targets, Switch writes fewer) {len(outd)}')
|
||||
by = {}
|
||||
for r in rows: by.setdefault(r[2], [0, 0]); by[r[2]][0] += 1; by[r[2]][1] += r[5]
|
||||
for s, (t, m) in by.items(): print(f' {s}: records {t}, with metallib {m}')
|
||||
+2
-18
@@ -1,4 +1,3 @@
|
||||
DM_ROOT ?= $(HOME)/dm-work
|
||||
# Game profile (dm/games/<id>/): the host is built for it (make GAME=base game title ID, required: there is no default game). game_profile.h is generated from game.tsv into gen/<id>/, dmsh_chain.h lives in the profile folder
|
||||
# without GAME only clean and a bare make (whose default goal only builds libvpx) pass; anything else stops and lists the profiles
|
||||
ifeq ($(strip $(GAME)),)
|
||||
@@ -17,13 +16,11 @@ endif
|
||||
GAME_H = gen/$(GAME)/game_profile.h
|
||||
CFLAGS=-O1 -g -Wall -Wno-unused-function
|
||||
# Service-layer takeover hle_*.c files are included by wildcard: adding a file does not touch this line (avoids merge conflicts)
|
||||
SRC=main.c loader.c hle.c host_stubs.c crash.c probe.c thread.c memstat.m ../ios/Sources/dm_touch_controls.c $(sort $(wildcard hle_*.c)) front_mac.m entry.S dm_i18n.c
|
||||
SRC=main.c loader.c hle.c crash.c probe.c thread.c memstat.m $(sort $(wildcard hle_*.c)) front_mac.m entry.S
|
||||
# NVN null device + nn::vi + the Vulkan layer (dm/gfx/)
|
||||
# Sources under dm/gfx, dm/gfx/nvn, dm/gfx/tex and dm/gfx/vk are included by wildcard (tests/ and metal/ do not go into the host): adding a file does not touch this line
|
||||
GFX=$(sort $(wildcard ../gfx/*.c ../gfx/nvn/*.c ../gfx/nvn/*.m ../gfx/nvn/*.S ../gfx/tex/*.c ../gfx/tex/*.m ../gfx/vk/*.m))
|
||||
GFX_H=$(sort $(wildcard ../gfx/nvn/*.h ../gfx/nvn/include/*.h ../gfx/tex/*.h))
|
||||
# Third party: only the decompression-related sources of libdeflate are built (TACT BLTE zlib blocks, hle_tact.c)
|
||||
LIBDEFLATE=../third_party/libdeflate/lib/deflate_decompress.c ../third_party/libdeflate/lib/zlib_decompress.c ../third_party/libdeflate/lib/adler32.c ../third_party/libdeflate/lib/utils.c ../third_party/libdeflate/lib/arm/cpu_features.c
|
||||
# Third party: libvpx ships only the VP9 decoder's sources (movie decoding, hle_movie.c), with its configuration headers in config/ (arm64, shared by Mac and iOS). Built separately into a static library, file by file, so host changes do not recompile it
|
||||
VPX=../third_party/libvpx
|
||||
VPX_SRC=$(sort $(shell find $(VPX) -name '*.c'))
|
||||
@@ -35,19 +32,6 @@ gen/vpx/%.o: $(VPX)/%.c $(wildcard $(VPX)/config/*.h)
|
||||
$(VPX_LIB): $(VPX_OBJ)
|
||||
rm -f $@; ar rcs $@ $^
|
||||
.DEFAULT_GOAL := dmhost # The header generation rules are written first below, so the default goal must be stated explicitly (otherwise make only generates the headers and does not build the host)
|
||||
# Headers generated from the original game's tables: not checked into the repository, generated at build time from the player's own game files (Update/RomFS of Switch version 3.3.94012); the path is given by the environment variable DM_ROMFS_SRC
|
||||
ROMFS?=$(or $(DM_ROMFS_SRC),$(DM_ROOT)/data/switch/3.3.94012/Update/RomFS)
|
||||
GENH=dm_vault_tab.h dm_hud_exp.h dm_bright_levels.inc
|
||||
NEED_ROMFS=@test -f "$(ROMFS)/data/build_manifest.psv" || { echo "🔴 game files not found: $(ROMFS)/data/build_manifest.psv. Put the Update/RomFS of Switch version 3.3.94012 there, or point the environment variable DM_ROMFS_SRC at it (see dm/README.md, 'Generating resources from your game files')"; exit 1; }
|
||||
dm_vault_tab.h: ../ios/tools/gen_vault_table.py
|
||||
$(NEED_ROMFS)
|
||||
python3 ../ios/tools/gen_vault_table.py "$(ROMFS)" > $@.tmp && mv $@.tmp $@
|
||||
dm_hud_exp.h: ../ios/tools/gen_hud_exp.py ../ios/tools/romfs_art.py
|
||||
$(NEED_ROMFS)
|
||||
python3 ../ios/tools/gen_hud_exp.py "$(ROMFS)" > $@.tmp && mv $@.tmp $@
|
||||
dm_bright_levels.inc: ../tools/gen_bright_levels.py ../ios/tools/touch_layout.py
|
||||
$(NEED_ROMFS)
|
||||
python3 ../tools/gen_bright_levels.py "$(ROMFS)" > $@.tmp && mv $@.tmp $@
|
||||
.PHONY: game_profile
|
||||
game_profile: $(GAME_H)
|
||||
# Regenerated on every make (FORCE): the same GAME can come from another profile folder (DM_GAMES_FIRST), whose game.tsv may be older than the header; game.py writes it only when it changes
|
||||
@@ -59,5 +43,5 @@ $(GAME_STAMP): FORCE
|
||||
@mkdir -p gen; echo $(GAME) | cmp -s - $@ || echo $(GAME) > $@
|
||||
.PHONY: FORCE
|
||||
FORCE:
|
||||
dmhost: $(SRC) $(GFX) dm.h hle.h dm_input.h front.h touch_mac.h ../ios/Sources/dm_touch_controls.[ch] $(GFX_H) $(GAME_H) $(GAME_DIR)/dmsh_chain.h $(GAME_STAMP) $(VPX_LIB)
|
||||
dmhost: $(SRC) $(GFX) dm.h hle.h dm_input.h front.h $(GFX_H) $(GAME_H) $(GAME_DIR)/dmsh_chain.h $(GAME_STAMP) $(VPX_LIB)
|
||||
clang $(CFLAGS) -I../gfx/nvn/include -I../third_party/vulkan-headers/include -I$(dir $(GAME_H)) -I$(GAME_DIR) -I$(VPX) -I../third_party/stb_vorbis -fobjc-arc -o $@ $(SRC) $(GFX) $(VPX_LIB) -framework Cocoa -framework QuartzCore -framework Metal -framework AudioToolbox -framework VideoToolbox -framework CoreMedia -framework CoreVideo -framework ImageIO -framework UniformTypeIdentifiers
|
||||
+4
-20
@@ -14,7 +14,6 @@
|
||||
typedef struct { uint32_t st_name; uint8_t st_info, st_other; uint16_t st_shndx; uint64_t st_value, st_size; } Elf64_Sym;
|
||||
typedef struct { int64_t d_tag; union { uint64_t d_val, d_ptr; } d_un; } Elf64_Dyn;
|
||||
typedef struct { uint64_t r_offset, r_info; int64_t r_addend; } Elf64_Rela;
|
||||
typedef struct { uint8_t *d; size_t n; } dm_blob; // One save file of the original game read whole into memory (dm_pages.c dm_save_blobs)
|
||||
#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,
|
||||
@@ -31,11 +30,10 @@ typedef struct {
|
||||
size_t size;
|
||||
uint64_t text_end, ro_off, data_off, key_page_off, key_page_size; // From the rewriter fixups header
|
||||
uint64_t fps_state_off; // frame step state page (patch_nso.py FRAME_STEP: Tears of the Kingdom 1.4.3 and Breath of the Wild 1.9.0 main); 0 = none
|
||||
int fps_legacy_elapsed; // its legacy mode is an elapsed-time step (TOTK); 0 = legacy is the game's own interval logic (BotW)
|
||||
int64_t patch_off; // patch area relative to the base; negative = before the base (image too large, see patch_nso.py FAR), and size does not include it
|
||||
uint64_t image_size, file_image_len, patch_len; // only with the patch area before the base: whole image (with bss and key page), length of the image part of the file, patch area length
|
||||
char build_id[33]; // First 16 bytes of the NSO build ID (hex); empty for older build outputs
|
||||
unsigned key_idx[8];
|
||||
unsigned key_idx[4]; // key page index of each thread slot (K18, KTLS, KTLR, KGATE)
|
||||
const Elf64_Dyn *dyn;
|
||||
const Elf64_Sym *symtab;
|
||||
const char *strtab;
|
||||
@@ -46,12 +44,10 @@ typedef struct {
|
||||
|
||||
extern module_t g_mod[MAX_MOD];
|
||||
extern int g_nmod;
|
||||
// main's build ID (empty if none). DM_D2R_BUILD = the D2R build that the code inherited from d2r-mobile belongs to: code with fixed D2R addresses or that edits D2R saves runs only on it
|
||||
#define DM_D2R_BUILD "db0507f36b84200ac05f75d76ed5026f"
|
||||
// main's build ID (empty if none): code with fixed addresses in a game's module runs only on the build they were found in
|
||||
static inline const char *dm_main_build(void) { for (int i = 0; i < g_nmod; i++) if (!strcmp(g_mod[i].name, "main")) return g_mod[i].build_id; return ""; }
|
||||
// Four thread-local slots: K18 = the Switch's x18, KTLS = tpidr_el0, KTLR = tpidrro_el0 (ThreadLocalRegion), KGATE = host gate entry; the actual key numbers are written into the key page by the loader
|
||||
extern pthread_key_t g_key18, g_keytls, g_keytlr, g_keygate;
|
||||
extern pthread_key_t g_keyres[2], g_keyaspect, g_keyaspectui;
|
||||
|
||||
void load_modules(const char *dir, const char **names, int n);
|
||||
void run_module_inits(void);
|
||||
@@ -61,24 +57,18 @@ module_t *module_of(const void *p);
|
||||
// Gate dispatch: rewritten svc / timer reads come in through the common entry, ret_addr = the return point of that gate block, regs points to the saved x0..x30
|
||||
uint64_t gate_dispatch(uint64_t ret_addr, uint64_t *regs);
|
||||
// Observation gates (rewriter OBS table; numbering matches patch_nso.py): read-only callbacks, called on a gate hit once attached
|
||||
enum { OBS_UIMSG = 1, OBS_TICK = 2, OBS_C2S = 3, OBS_C2S_SYS = 4, OBS_SETLANG = 5, OBS_TARGET_RANK = 7, OBS_UI_PROJECT = 8, OBS_FREEZE_WORLD = 10, OBS_FREEZE_CLIENT = 11, OBS_FREEZE_ANIM = 12, OBS_FREEZE_RENDER = 13, OBS_TEXTBOX = 14, OBS_EVENT_MSG = 15, OBS_LEVEL_WARP = 16, OBS_QUEST_KILL = 17, OBS_NOTICE_INPUT = 18, OBS_MOVIE_RING_LINK = 19, OBS_MOVIE_RING_UNLINK = 20, OBS_MAX = 21 }; // 6 = iOS binding read (dm_touch_game_bridge.c)
|
||||
enum { OBS_MOVIE_RING_LINK = 19, OBS_MOVIE_RING_UNLINK = 20, OBS_MAX = 21 }; // Metroid Dread 2.1.0's movie request ring (hle_movie.c)
|
||||
void gate_obs_set(int id, void (*fn)(const uint64_t *regs));
|
||||
int gate_obs_present(int id);
|
||||
void dm_quiet_difficulty_msg(int on); // In a rift run, block the original game's "Game difficulty set to N" message (observation gate 15, diag.c)
|
||||
int gate_svc_present(int num); // Whether the build output has a gate for this svc number (for host call numbers introduced by cuts) // whether the build output has this observation gate (registered in gates.tsv)
|
||||
uint64_t now_ns(void);
|
||||
uint64_t guest_ns(void); // Guest clock (hle_os.c; does not advance while DM_TICK_HOLD is frozen)
|
||||
uint64_t guest_ns(void); // Guest clock (hle_os.c; does not advance while frozen, guest_clock_freeze)
|
||||
int dm_detclock_enabled(void);
|
||||
void dm_guest_clock_advanced(void);
|
||||
void guest_clock_freeze(int on);
|
||||
void guest_clock_freeze_for(int reason, int on); // Freeze by reason (1 background, 2 system keyboard); see hle_os.c
|
||||
void dm_guest_frame_tick(void); // Called on every present (hle_nvn.c): when DM_DETCLOCK is on, advances the guest clock by one fixed step
|
||||
int dm_nvn_ruler_truth(void); // 1 during the ground-truth phase of the visual ruler (the first two screenshots with DM_NVN_RULER=1)
|
||||
uint64_t dm_nvn_frame_count(void);
|
||||
uint64_t dm_nvn_sub_cpu_ns(void); // Cumulative CPU nanoseconds of submit (excluding present)
|
||||
int dm_ingame_now(void); // Whether we are in a run (diag.c)
|
||||
void dm_3d_scale_feed(double iv_ms); // The present thread reports the actual present interval every frame (−1 discard / 0 no reading), driving the dynamic 3D scale
|
||||
float dm_3d_scale_cur(void); // The 3D render scale currently in effect (hle_oe.c scale3d_tick) // current cumulative count of presented frames (for DM_TICKS alignment observation)
|
||||
void dm_backtrace(uint64_t fp);
|
||||
void crash_watch_progress(uint32_t tick, const char *stage);
|
||||
void crash_watch_frame(void);
|
||||
@@ -88,12 +78,6 @@ void crash_dump_now(const char *why); // Dump state on demand (threads + graph
|
||||
// Diagnostic switches: read once from environment variables when main starts (getenv takes the libc global environment lock, and on a hot path it would make all guest threads queue up)
|
||||
extern uint64_t g_t0_ns; // Host start time (now_ns), used for relative times in the log
|
||||
extern struct dm_flags { int thr, svc, fs, movie, dbg, imports, bootprof; } g_flag;
|
||||
// Boot timeline (DM_BOOTPROF=1, diag.c): segment marks + counters (nanoseconds / bytes, accumulated over all threads); PROF_GFX_* can be used for instrumentation
|
||||
enum { PROF_FS_READ, PROF_WAIT_LOCK, PROF_WAIT_CV, PROF_WAIT_ADDR, PROF_WAIT_SYNC, PROF_SLEEP, PROF_MAIN_WAIT,
|
||||
PROF_GFX_PRESENT, PROF_GFX_SHADER, PROF_TACT, PROF_N }; // PROF_TACT: time and output bytes of TACT block zlib decode (svc #0x80, host libdeflate)
|
||||
void dm_prof_add(int id, uint64_t ns, uint64_t bytes);
|
||||
void dm_prof_mark(const char *seg);
|
||||
uint64_t dm_prof_ribbon_ns(void); // The now_ns time at which OpenPanel MainMenuButtonRibbon first appeared (0 = not yet; needs DM_BOOTPROF=1)
|
||||
void *objmap_get(const void *guest); void objmap_put(const void *guest, void *host); void *objmap_del(const void *guest);
|
||||
void hle_signal_system_event(void *g); // hle_thread.c: signal a service-layer SystemEvent (any thread)
|
||||
void hle_bind_system_event(void *g, int autoclear); // On a guest thread, bind a guest SystemEvent to a kernel event
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// Host entry points for "select first, then confirm": vendor buying/selling, stat / skill point allocation, setting the quantity box value. For implementation and rationale see dm_actions.c, dm_textinput.c.
|
||||
// Actions: callable from any thread, executed on the game main thread (where the controller is fetched). Readings: the main thread publishes a snapshot every frame, readable from any thread.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
typedef struct {
|
||||
int open; // the vendor trade page is open
|
||||
int gold, stash_gold; // gold carried, gold in the stash (purchases can deduct from both, game rule)
|
||||
int sel_valid; // the cursor has selected a piece of goods
|
||||
uint32_t sel_id; char sel_code[4]; char sel_name[64]; int buy_price;
|
||||
int sel_gx, sel_gy, sel_gw, sel_gh; // top-left cell and footprint of the selected goods on the shelf (in cells; meaningless when sel_valid is 0). Converted to a screen rectangle for the drag-goods follow image
|
||||
int can_buy; // enough gold (buy price ≤ gold carried + gold in the stash); whether the inventory has room is judged by the game at purchase time
|
||||
int held_valid; // an item is held in hand (being dragged)
|
||||
uint32_t held_id; char held_name[64]; int sell_price; // sell_price = 0: this item cannot be sold
|
||||
} dm_shop_info;
|
||||
void dm_shop_get(dm_shop_info *out);
|
||||
int dm_shop_open(void);
|
||||
void dm_shop_buy(void); // buy the selected item (the game's own purchase path; not enough gold / no room in the inventory is rejected by the game with the original game's prompt, not blocked here)
|
||||
void dm_shop_sell_cursor(void); // sell the item held in hand
|
||||
|
||||
typedef struct {
|
||||
int stat_points, skill_points; // remaining stat points / skill points
|
||||
int tree_open; // the skill tree page is open
|
||||
int char_open; // the character page is open (and the advanced stats page is not covering it); the phone layer draws the stat "confirm +1 / +5 / all" based on this
|
||||
int sel_skill; // skill id currently selected on the skill tree page, -1 = none
|
||||
int sel_can_add; // whether a point can still be added (= sel_block == 0)
|
||||
int sel_block; // reason a point cannot be added: 0 can add, 1 no skill points, 2 level too low, 3 prerequisite skill not learned, 4 other (already at the cap, etc.)
|
||||
char sel_name[64];
|
||||
} dm_points_info;
|
||||
void dm_points_get(dm_points_info *out);
|
||||
void dm_stat_add(int stat, int n); // stat: 0 strength 1 dexterity 2 vitality 3 energy; n: 1 / 5 / -1 all (valid while the character page is open)
|
||||
void dm_skill_add_selected(void); // add 1 point to the skill currently selected on the skill tree page
|
||||
void dm_belt_put(uint32_t item_id); // put this inventory item into the belt (the game's own "move to belt" path; does nothing if it is not a potion / the belt is full). item_id uses the id from dm_cursor_item
|
||||
void dm_item_use(uint32_t item_id); // use this item immediately (the same function as the original game's "use": the endpoint of controller hold-confirm / keyboard-and-mouse right click; does not wait for the long-press threshold). Drinks potions, reads scrolls, uses tomes, opens the cube. item_id uses the id from dm_cursor_item
|
||||
int dm_ui_hover_now(int x, int y); // put the original game's controller cursor at (x, y) (the same coordinate pair dm_cursor.c writes) and have the game recompute hover / focus / selected cell on the spot; returns 1 = refreshed, 0 = not in controller cursor mode right now. May only be called on the game main thread (where the controller is fetched)
|
||||
void dm_item_compare(int on); // equipment comparison on / off: while on, the item worn in the same slot is shown side by side next to the hovered / selected equipment's tooltip (the same state bit as the original game's "hold the compare button", not going through the controller button,
|
||||
// so it also works on the vendor page and the stash page, where the compare button is taken up by tab switching). Callable from any thread; while on, the host maintains it every frame. The original game does not support comparison on the gambling page, so nothing shows there even when on
|
||||
void dm_belt_expand(int on); // expand / collapse the upper rows of the belt (the same panel toggled by the original game's keyboard-and-mouse "show belt" action, panel id 0x1a). Callable from any thread
|
||||
void dm_belt_use(int col, int row); // drink the bottle in row `row` (0 = bottom row), column `col` (0–3) of the belt; does nothing if that cell is empty. Goes through the belt branch of dm_item_use. Callable from any thread
|
||||
void dm_belt_use_merc(int col, int row); // same as above, but feeds it to the mercenary (belt double-tap); does nothing if the mercenary is absent / dead / this bottle cannot be fed
|
||||
int dm_belt_rows(int *expanded); // belt row count (1–4, depending on the belt worn); when expanded is non-null, writes 1 = currently expanded. Reads the snapshot the main thread publishes every frame
|
||||
void dm_held_equip(int bodyloc); // put the held item into this body slot (the same function as the original game's equipment slot "press confirm while holding an item", main+0x3bb24c; empty slot = equip, occupied = swap; nothing happens for what the original game rejects). Callable from any thread
|
||||
void dm_inv_sort(int page); // sort (auto-arrange): page 0 inventory, 3 Horadric Cube, 4 current page of the personal stash. The endpoint function of the original game's "hold to sort", without waiting for the long press. Callable from any thread
|
||||
void dm_name_random(void); // character creation naming page: fill the name box with a random name (the same message as the original game's "Random Name" button). Valid while the name panel is open. Callable from any thread
|
||||
int dm_name_taken(const char *name); // returns 1 if a character with this name already exists (a same-named directory exists under the save working directory)
|
||||
int dm_name_gen(char *out, int cap, int (*taken)(const char *)); // random name used when the naming page is left blank (dm_namegen.c): syllable concatenation, draws again if taken is non-zero, at most 64 times; returns 1 on success
|
||||
void dm_item_equip(uint32_t item_id); // immediately equip this item from the inventory (or stash / cube) into the corresponding slot (the endpoint of the original game's "hold confirm = equip"; the slot is chosen by the game, and the game swaps by itself when something is already equipped; when the mercenary panel is open and he can wear it, it goes to the mercenary, same as the original game).
|
||||
// When it cannot be worn (level / stats too low, unidentified, not equipment) only the original game's error sound is played and no packet is sent
|
||||
void dm_item_unequip(uint32_t item_id); // immediately unequip this worn item into the inventory (the endpoint of the original game's equipment panel "hold confirm = unequip"; when the inventory cannot hold it the game plays the error sound and does nothing). Not executed while the vendor page is open (in the vendor page that original path would turn into selling it)
|
||||
void dm_item_identify(uint32_t target_id); // in the "used an identify scroll / identify tome, waiting to pick a target" state (dm_item_targeting() non-zero), complete the use on this item (the endpoint of the original game's item cell / equipment panel click handling in that state: sends the "use item on item" packet; scroll consumption / tome charge deduction is done by the server as in the original game).
|
||||
// Not called when not in that state, when the target is not an item on oneself / in the inventory / stash / cube, or when the target has an operation pending confirmation (the log writes the reason)
|
||||
void dm_item_repair(uint32_t item_id); // repair this one item in repair mode on the vendor page (the same endpoint as clicking an item in the original game's repair mode); not called when not in repair mode / something is in hand / the original game judges no repair is needed (the log writes the reason)
|
||||
void dm_belt_fill(uint64_t belt_widget); // the belt widget in the inventory panel: fill the empty slots in one go with potions from the inventory that can go into the belt (the same code as the original controller "hold R with focus on the belt"); does nothing if the conditions are not met. Callable from any thread
|
||||
void dm_item_target_cancel(void); // leave the "waiting to pick a target" state without consuming the scroll (the same code the original game runs when clicking on empty space / an invalid target: clears the pending item, resets the cursor state). Does nothing when not in that state
|
||||
void dm_stash_take(int count); // from the stack the cursor is pointing at right now in the advanced stash (gems / materials / runes pages), take count pieces into the inventory (command 0x63, destination = inventory; 1 piece goes through the grid widget method main+0x39d824, multiple pieces go through main+0x39d9e0 with a piece count).
|
||||
// When count exceeds the stack count, takes all that remain. Does nothing when there is no item under the cursor / it is not a stacked item / the stash is not open; the log line "take multiple:" gives the reason. When the inventory cannot hold them, the server decides how many to take (not verified)
|
||||
void dm_stash_take_id(uint32_t item_id, int count); // same as above, specified by item id (for testing)
|
||||
void dm_num_set(int n); // set the quantity box value: ≥0 that number, -1 all, -2 half (dm_textinput.c)
|
||||
void dm_actions_pump(void); // called every frame where the controller is fetched (main thread)
|
||||
void dm_held_stow(void); // put the item in hand (on the cursor) into the inventory without closing the panel: the step the original game does for "press back while holding an item" (main+0x3cf4d0: finds a place in the inventory for the item and sends 0x18). Callable from any thread, executed on the game main thread next frame
|
||||
int dm_held_stow_result(void); // result of the most recent dm_held_stow: -1 no result yet (if it was not stowed it retries every frame for about 1.5 seconds: an item just swapped into the hand has to wait for the server's acknowledgement); 1 put into the inventory (packet sent); 0 not stowed, the held item is untouched
|
||||
void dm_item_identify_free(uint32_t item_id); // identify this item for free (double-tap on an unidentified item): calls the original game's identify function main+0x78acf0 in the server tick, without consuming a scroll / tome
|
||||
// One-press casting of off-hand set skills (that section of dm_actions.c)
|
||||
uint64_t dm_swapskill_filter(uint64_t buttons); // where the controller state is fetched (hle_hid.c): hold back / let through the button that needs a weapon swap first
|
||||
void dm_swapskill_srv_wset(int w); // the server tick (diag.c hud_tick) publishes the current weapon set 0 / 1
|
||||
int dm_swapskill_slot(int slot, int *charges); // skill button display: 1 = the skill is in the other set ("II" badge), 2 = charged skill (*charges = remaining charges)
|
||||
int dm_swapskill_equipped(int32_t item_id); // this item is worn on the body (including the weapon-swap set)
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
// Boss rush: the door in Harrogath → 5 stages at the Arreat Summit → a door into the Worldstone Chamber, 5 stages with the terror zone forced → 2 more stages in Hell. For implementation and rationale see dm_bossrush.c
|
||||
#include <stdint.h>
|
||||
void dm_bossrush_tick(uint64_t game, uint64_t player, uint8_t *main_base); // called every server tick (where diag.c reads the player state)
|
||||
void dm_bossrush_c2s(uint64_t game, uint64_t player, const uint8_t *pkt, uint32_t len); // called per packet at the client command packet dispatch entry (observation gate OBS_C2S)
|
||||
void dm_game_notice(uint8_t *main_base, const char *text); // original-game system notification (one chat bar line + the system notification box), called from the server thread
|
||||
int dm_bossrush_owns(uint32_t id); // this monster unit was spawned by the current boss rush stage
|
||||
void dm_kills_tick(uint64_t G, uint64_t player, uint8_t *main_base); // kill board (dm_kills.c), called in the server tick
|
||||
void dm_door_label(uint8_t *main_base, uint64_t game, int cls, const char *key, const char *zh, const char *en, int in_town); // name label of the doors we create: in town the string key of this object class is replaced with key (attached to the original game's string table), and written back on leaving town
|
||||
@@ -1,88 +0,0 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
void dm_cursor_set(int x, int y);
|
||||
// The front end registers a 'pending write to the original game's cursor' (screen point coordinates), and the host applies it when the game reads the controller (hle_hid fill):
|
||||
// it reaches the game in the same sample as the keys. If the front-end main thread writes the cursor global directly at an arbitrary time, the game writes it back from its own controller cursor state before processing A, and A lands at the old cursor
|
||||
void dm_cursor_set_screen(float x, float y, float screen_w, float screen_h);
|
||||
void dm_cursor_clear(void);
|
||||
// Stick closed loop (interim approach, used until the original game's mouse mode switch is found): on a real device the original game's per-frame mouse move path does not run, and a directly written cursor gets overwritten by the controller cursor logic.
|
||||
// Instead: on every controller read, read the original game's cursor (main+0x2d128f8/fc), and if it is > DM_CURSOR_NEAR (9) pixels from the target push the left stick toward the target (force reduced by distance within 60 pixels);
|
||||
// an A pressed before it is in position is queued and held back together with the target position at that time, and released only when the cursor reaches that position (one each for the start / end of a drag-and-drop); if not in position after 1.5 seconds in the queue, report a failure and drop it.
|
||||
// Called where the host reads the controller (hle_hid fill), reaching the game in the same sample as the keys
|
||||
void dm_cursor_apply(uint64_t *buttons, int32_t *lx, int32_t *ly);
|
||||
int dm_cursor_pending(void); // How many A presses are still held back and not delivered to the game (the cursor is not in position yet); the front end must wait for it to return to zero before ending the pulse
|
||||
// Native click (UI mouse message injection, see dm_cursor.c): the front end hands over the touch points, and the host sends them frame by frame to the UI root widget where the game reads the controller
|
||||
void dm_ui_send_queue(const char *spec); // 'category:action[:argument]'; sent to the UI root widget on the next controller read (used by the MSG step of key scripts)
|
||||
int dm_ui_fe_drag_ok(void); // Whether a vertical drag in the front end can count as a scroll right now (not while the delete confirmation / mode page / naming page / a confirmation box is open)
|
||||
void dm_ui_touch(int phase, float nx, float ny); // phase: 0 down, 1 move, 2 up, 3 cancelled (turned into a scroll); coordinates = normalized to the rendered picture [0,1]
|
||||
void dm_ui_mouse_pump(void);
|
||||
// Generic pointer events: HOVER starts a held hover (pins the cursor every frame), MOVE updates the position, END ends the hold; the rest are queued, one per frame. arg: SCROLL = wheel amount (120 per notch), KEY = VK key code
|
||||
enum { DM_UP_HOVER = 0, DM_UP_MOVE = 1, DM_UP_END = 2, DM_UP_LDOWN = 3, DM_UP_LUP = 4, DM_UP_RDOWN = 5, DM_UP_RUP = 6, DM_UP_SCROLL = 7, DM_UP_KEYDOWN = 8, DM_UP_KEYUP = 9 };
|
||||
unsigned dm_ui_item_query(float nx, float ny); // Ask 'is this point an item area'; returns the question sequence number; the main thread answers on the next frame
|
||||
int dm_ui_item_result(unsigned seq); // 1 item area / 2 tab / 0 other widget inside a panel / 3 outside the panels / 6 pressing on a button covered by an upper panel (not clicked) / 7 goods on the vendor shelf / 8 stacked cell / 10 another item while the single-item slot is occupied (left alone) / -1 not answered yet
|
||||
unsigned dm_ui_drop_query(float nx, float ny_lifted, float ny_finger); // How to handle a drag-and-drop landing point: result 5 put at the position given by dm_ui_drop_point (item area; for an equipment slot the slot center) / 3 drop / 0 put back where it was (fetched with dm_ui_item_result as well)
|
||||
int dm_ui_drop_at_now(float nx, float ny_lifted, float ny_finger); // Synchronous version, callable only from the game main thread
|
||||
int dm_ui_origin_point(float *nx, float *ny); // Center of the original position of the most recently 'picked up' item (used to put it back where it was); 0 = none
|
||||
int dm_ui_drop_last(void); // Class of the most recently answered drag-and-drop landing point (used on release if this one has not been answered yet; -1 = none)
|
||||
int dm_ui_drop_point(float *nx, float *ny); // Put-down position when the landing verdict is 5 (normalized)
|
||||
int dm_ui_drop_bag(void); // When the landing verdict is 5, whether the landing point is a player inventory cell (shelf drag-to-buy / dragging out of a stacked cell only complete here)
|
||||
int dm_fe_bliss(void); // Whether "Elysium" on the character-creation mode page is checked (1 = checked; the last value is kept after the player leaves the mode page and is read at character creation). Readable from any thread
|
||||
int dm_ui_shelf_grid(float g[4]); // Vendor shelf grid (normalized: top-left x,y, width and height of one cell); 0 = not probed yet
|
||||
int dm_ui_item_rect(unsigned seq, float r[4]); // Screen rectangle of the goods being held down on the vendor shelf (normalized; used for the finger-following image when dragging goods); 0 = none / not ready yet, ask again next frame
|
||||
int dm_ui_stack_left(void); // How many items are in the stack of the stacked cell pressed this time; -1 = not read yet
|
||||
void dm_ui_world_tap(float ux, float uy, float wx, float wy); // Tap on the world in the game state: a tap on the mercenary portrait = open the mercenary page, otherwise forwarded to dm_world_tap(wx, wy). u = normalized to the view, w = normalized to the rendered picture
|
||||
void dm_ui_stack_take_at(float nx, float ny); // A stack in the advanced stash dragged to the inventory and released: put the cursor back on that stack and take 1 item into the inventory
|
||||
void dm_ui_stack_cube_at(float nx, float ny); // A stack in the advanced stash dragged onto the Horadric Cube cell at the bottom of this page and released (landing class 11): put the cursor back on that stack and press the original game's X = 1 item into the Horadric Cube
|
||||
void dm_ui_shop_buy_at(float nx, float ny); // Goods on the vendor shelf dragged to the inventory and released: put the controller cursor back on those goods (it was moved away when the drag started) and buy them
|
||||
void dm_ui_item_tap(void); // Single tap in an item area: when in the 'identify scroll / tome used, waiting for target selection' state = identify the item under the cursor; does nothing when not in that state
|
||||
int dm_ui_targeting(void); // Whether that state is in effect right now (published by the host every frame, read from any thread)
|
||||
int dm_ui_panel_resident(const char *name); // In-run always-on layers (the HUD layers, mercenary portrait bar, save indicator, button legend...): being open does not count as 'a panel is open'
|
||||
enum { DM_PS_BANK = 1, DM_PS_VENDOR = 2, DM_PS_DEAD = 4, DM_PS_TEXT = 8, DM_PS_IMBUE = 16, DM_PS_CUBE = 32, DM_PS_VAULT = 64 }; // VAULT = the stash is open on the sixth page "Chronicle" (dm_vault_ui.c) // CUBE = the Horadric Cube page is open (HoradricCubeLayout) // IMBUE = the single-item task slot panel is open (ImbueItemsPanel, shared by imbue / socketing / personalize / tomb pedestal)
|
||||
int dm_ui_panel_state(void);
|
||||
void dm_ui_lifecycle(int background); /* Called when the front end enters / leaves the background: position guard self-check (dm_cursor.c mv_audit) */ // Panel state bits (published by reading the game's panel tree every frame; on change the synthetic UI message DM:PanelState is sent)
|
||||
int dm_ui_panel_shown(const char *name); // This panel is visible in the game's panel tree right now (called from the game main thread)
|
||||
int dm_ui_tap_box(void); // Whether the most recently single-tapped item is the Horadric Cube
|
||||
uint32_t dm_ui_cur_stack_id(void); // While the stash is open, if the cursor is pointing at an item in the inventory right now = its unit id, otherwise 0
|
||||
int dm_ui_free_cursor(void); // Whether the current page has a free cursor
|
||||
void dm_ui_use_item(void); // Double-tap in an item area: a vendor's goods under the cursor -> buy; equippable -> the original game's 'hold the confirm button' path (auto-equip); everything else -> use on the spot (drink a potion / read a scroll / open the Horadric Cube)
|
||||
void dm_ui_long_press(void); // Long-press in an item area = refill the belt: if the controller cursor points at a potion in the inventory that can go into the belt and the belt has room, put it into the belt; anything else is left alone
|
||||
void dm_ui_tab_go(void); // The most recent hit was a tab and this time it is a tap: press ZL / ZR (L / R at the top) enough times to switch to it
|
||||
int dm_ui_item_at_now(float nx, float ny); // Synchronous version of the above; may only be called from the game main thread
|
||||
void dm_ui_watch(const char *spec); // Test probe: watch the UI widget 'name[@panel name]' (NULL cancels); the main thread publishes its center every frame
|
||||
int dm_ui_watch_pos(float *nx, float *ny); // Any thread: returns 1 if the watched widget is visible and gives its coordinates normalized to the game view
|
||||
uint64_t dm_ui_taps_base(void); // Test: presented frame at which DM_UI_TAPS_AT hit (0 = not hit yet)
|
||||
void dm_ui_ptr(int kind, float nx, float ny, int arg);
|
||||
int dm_ui_touch_active(void);
|
||||
// Front-end path streamlining (see dm_cursor.c): UI message callback registration, execution where the controller is read, auto key mask
|
||||
void dm_ui_on_uimsg(const char *cat, const char *act, const char *arg);
|
||||
void dm_ui_frontend_pump(void);
|
||||
uint64_t dm_ui_auto_buttons(void);
|
||||
void dm_ui_key_pulse(uint64_t mask, int frames); // Tap a controller key on the player's behalf (held for frames guest frames)
|
||||
// Continuous UI gestures (called by the front end from any thread, executed every frame on the main thread): kind 1 = vertical drag -> scroll, 2 = horizontal drag -> slider; coordinates = normalized to the rendered picture [0,1]; tol_ny = how much vertical slack to allow when identifying a slider (finger thickness)
|
||||
void dm_ui_drag_begin(int kind, float ox, float oy, float tol_ny);
|
||||
void dm_ui_drag_move(float x, float y);
|
||||
void dm_ui_drag_end(void);
|
||||
void dm_ui_drag_lift(float ny); // Before a drag starts, report how far the item image is lifted above the finger (as a fraction of screen height): dragging a chronicle cell back to the inventory decides the landing point by the item image position (dm_vault_ui.c)
|
||||
// Rectangle of the top-right minimap (the game's AutoMap panel) in the rendered picture, normalized [0,1]: r = left, top, right, bottom. Returns 1 = has been read (the most recent one is given while the panel is closed); callable from any thread
|
||||
int dm_ui_minimap_rect(float r[4]);
|
||||
// Full-screen map ↔ minimap (switched at runtime, not written to disk): dm_ui_map_full(1) switches to full screen, (0) returns to the minimap level set by the player; dm_ui_map_is_full() = whether the game is currently on the full-screen map (callable from any thread, refreshed every frame on the main thread)
|
||||
// Touch flow for skill binding (called by the front end from any thread, advanced every frame on the main thread): dm_ui_bind_begin(key) = choose a skill for this controller key (may carry ZL = second set) -- the host opens the skill tree,
|
||||
// the player taps a skill (while the flow is in progress the front end turns the tap into 'cursor to the point + press Y') -> the original game's "Press button to rebind skill" box pops up -> the host presses the target key for the player -> the panel closes. dm_ui_bind_active() = flow in progress (0 none / 1 opening the skill tree / 2 waiting for the player to tap a skill / 3 wrapping up)
|
||||
void dm_ui_bind_begin(uint64_t key_mask);
|
||||
int dm_ui_bind_slot(void); /* Binding target slot 1..8 (F1–F8), 0 when not in the binding state */
|
||||
void dm_ui_safe_left(int units); /* Left safe area, in layout units (2160 high); the mercenary / party member portrait bar in the top-left corner is moved inward by it */
|
||||
void dm_ui_safe_right(int units); /* Right safe area, in layout units (2160 high); the minimap and info bar in the top-right corner are moved inward by it */
|
||||
uint32_t dm_ui_hit_id(void); /* The most recently hit point-spending button: attribute '+' 1..4 (strength / dexterity / vitality / energy), skill icon = an identifier with the top bit set to 1; 0 if nothing was hit */
|
||||
int dm_ui_bind_active(void); uint64_t dm_ui_auto_release(void); /* Used where the controller is read: the keys to release from the player's input right now */ int dm_ui_bind_skill_hit(float *nx, float *ny); /* The most recent hit (after snapping) landed on a point-spending button: 1 = a skill icon in the skill tree, 2 = a '+' on the attributes page; comes with normalized coordinates */
|
||||
void dm_ui_bind_cancel(void);
|
||||
void dm_ui_map_full(int on);
|
||||
int dm_ui_map_is_full(void);
|
||||
void dm_ui_text_entered(const char *text); // Called when the system keyboard returns: text = final text written back to the game (UTF-8); pass NULL for cancel / empty string
|
||||
// Latency readout (diagnostic, DM_LAT_LOG=1): at key moments print a line "LAT <millisecond timestamp> <event>", to measure the latency from finger down / release to the game taking effect
|
||||
int dm_lat_on(void);
|
||||
#define DM_LAT(fmt, ...) do { if (dm_lat_on()) LOG("LAT %llu " fmt, (unsigned long long)(now_ns() / 1000000), ##__VA_ARGS__); } while (0)
|
||||
void dm_ui_hand_idle(int idle); // Reported by the touch front end every tick: no fingers, no keys or cursor requests being sent / queued = 1 (used by the held-item guard, dm_cursor.c hand_guard)
|
||||
uint32_t dm_ui_hover_unid(void); // id of our own unidentified item the cursor is pointing at right now (inventory / stash / cube / equipped; none = 0). Published by the host every frame, read from any thread
|
||||
void dm_ui_identify_hovered(void); // Touch layer double-tap on an unidentified item: on the next frame the host calls free identify on it (dm_item_identify_free)
|
||||
int dm_ui_map_geom(float *cell); // Map geometry: returns 0 not open / 1 minimap / 2 full screen, *cell = pixels per cell (real picture pixels, any thread)
|
||||
@@ -1,22 +0,0 @@
|
||||
// 'The item the controller cursor / pointer is currently pointing at' in an item panel: taken from the game's own hover object (the function used by the bottom key hints), not back-computed from cell coordinates.
|
||||
// May only be called from the game main thread (where the controller is read / UI message callbacks): it reads client UI widgets and the client unit table, synchronously, read-only, without locking.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
enum { DM_ITEM_AT_NONE, DM_ITEM_AT_INVENTORY, DM_ITEM_AT_BELT, DM_ITEM_AT_EQUIP, DM_ITEM_AT_STASH, DM_ITEM_AT_CUBE, DM_ITEM_AT_SHOP, DM_ITEM_AT_TRADE };
|
||||
typedef struct {
|
||||
uint32_t id, cls; // Item unit id, item table row number
|
||||
char code[4]; // Item code (trailing spaces removed, e.g. "hp1", "box"), 0-terminated
|
||||
int container; // DM_ITEM_AT_*
|
||||
int potion, scroll, book; // Item type (the game's type membership function): potion / scroll / book
|
||||
int beltable; // Beltable by type (the belt flag in the item type table)
|
||||
int belt_room; // Beltable and the belt has room right now (the same check that precedes the game's "Move to Belt" hint)
|
||||
int equippable; // Can be equipped right now (the same check that precedes the game's "Equip" hint)
|
||||
int stack; // Stack count of this cell in the advanced stash (gem / material / rune pages); not a stacked item = 0 (item data +0x9c, the same byte the original game checks before a long-press takes multiple items)
|
||||
int unid; // Unidentified: bit 0x10 of item data ([item+0x10]) +0x18 is 0 (the original game's 'identified' flag; verified at both ends by flipping it in the test code dm_actions.c)
|
||||
int gx, gy, gw, gh; // Top-left cell and cells occupied of the item in its container (inventory / stash / cube / shop / trade); for the belt gives (cell number, 0, 1, 1), for an equipment slot gives (0, 0, 1, 1)
|
||||
} dm_cursor_item_info;
|
||||
int dm_cursor_item(dm_cursor_item_info *out); // 1 = pointing at an item and out has been filled; 0 = none (panel not open / pointing at an empty cell / focus not on an item cell)
|
||||
int dm_held_size(int *w, int *h); // Cells occupied by the item held in hand (width, height); returns 0 if nothing is held. The 'identify scroll used, waiting for a target' case does not count as held
|
||||
int dm_held_pending(void); // Whether the item in hand has an operation pending confirmation right now (the previous command has not been answered yet); returns 0 if nothing is held
|
||||
int dm_vendor_repair_mode(void); // Whether the vendor page is in the 'repair' cursor mode right now (repair button tapped, hammer cursor). May only be called from the game main thread
|
||||
int dm_item_targeting(void); // Whether the 'an item was used, waiting to select a target item' state is in effect right now: 0 none; non-zero = the target type of that item in the item table + 1 (identify scroll / identify tome both go through this state)
|
||||
@@ -1,15 +0,0 @@
|
||||
// "Custom" settings: on/off switches for the features we added ourselves, all read and written here. Each module only calls dm_custom_get and does not read files / environment variables as settings on its own.
|
||||
#pragma once
|
||||
|
||||
// Effective value 0 / 1: changes only after the player taps "Apply" on the options page, unchanged by "Discard", "Default" = each key's default (power-saving mode off, the rest on). Returns 1 for an unrecognized key.
|
||||
// Callable from any thread: the first call reads the file once, and after that it is just an atomic read + a few string comparisons, lock-free. On iOS it can be read already during startup (before the save is mounted).
|
||||
// Keys: auto_mana auto-drink potions (both red and blue; the key name is kept) / drop_light light beam on high-value drops / map_mark minimap markers for entrances and exits, waypoints and quest goals / item_notes item notes and old translated names / icloud iCloud save sync (cloud_ios.m, read at startup) / pot_pick potion auto-pickup (dm_potpick.c) / drop_haptic haptics on high-value drops (dm_drophaptic.c) / gem_pick auto-pickup of runes and gems into the advanced stash (pouch) / store_btn the inventory "Deposit" button (dm_pouch.c) / eco_fps power-saving mode 30 fps (nvn_mtl.m, off by default)
|
||||
int dm_custom_get(const char *key);
|
||||
int dm_custom_lang_explicit(void); // Value of the language switch (key lang_en) as written in the settings file: -1 never written (original settings left alone) / 0 Simplified Chinese / 1 English; read by hle_fs.c at save mount
|
||||
int dm_fps_setting(void); // Power-saving mode (key eco_fps, off by default): on = 30 fps, off = 60; read by mtl_fps_target in nvn_mtl.m
|
||||
|
||||
void dm_custom_init(const char *dir); // Directory holding the settings file (the iOS shell passes Documents at startup); if not called, the parent of the save commit directory is used (Mac host)
|
||||
|
||||
// The following are only for the UI wiring of the custom page
|
||||
void dm_custom_on_uimsg(const char *cat, const char *act, const char *arg); // Options page apply / default / discard (forwarded from the UI message callback in dm_cursor.c)
|
||||
void dm_custom_ui_pump(unsigned long long root); // Game main thread, every frame: while the options page is open, read the checkbox of each row (widget name TouchCustom_<key>)
|
||||
@@ -1,42 +0,0 @@
|
||||
// Landing points of the two doors at the rift completion (pure algorithm, does not touch game memory; used by dm_rift.c, tested by tests/t_doorspot.c).
|
||||
// What must be guaranteed: the doors land on cells the player "can walk to". Previously "the player walks 10 cells with their back to the guardian, offset 5 cells to the left and to the right" was handed directly to the original game's free-ground search main+0x7160d0 (mask 0x400 = objects only),
|
||||
// the wall bit (0x1) is not in the mask and there is no reachability check at all -- in narrow maps such as the Cave / the Pit the player stands right against a wall, and 10 cells away is inside the wall or outside the map (figure: completion of rift level 26 on Pit Level 1, both doors in the black area).
|
||||
// Approach: take a window centered on the player, blk[] = whether this cell blocks the player (the caller fills it by the player's own collision mask; everything outside the window counts as blocked); from the cell at the player's feet do a breadth-first search with the "player occupies a cross" way of walking,
|
||||
// and only the cells the search reaches are candidates = necessarily reachable. Among the candidates take the one nearest to the ideal point, in three tiers:
|
||||
// tier 0: the 2 cells around it (5 × 5) are all reachable cells -- the door body does not touch a wall and the player can walk around it; tier 1: the 1 cell around it (3 × 3); tier 2: reachable is enough, switched to nearest to the player (when too narrow to fit, fall back to the nearest walkable cell by the feet).
|
||||
// If none holds (the player is boxed into a single cell), land on the cell at the player's feet.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
enum { DS_R = 24, DS_W = 2 * DS_R + 1, DS_N = DS_W * DS_W }; // window radius 24 cells: the ideal point is 10 cells out and then offset by another 5 cells, leaving a margin of as much again
|
||||
enum { DS_FAR = 36 }; // doors of tier 0 / 1 are at most this many steps of walking from the player (about 11 cells in a straight line; ones reached only by going around a wall are not wanted)
|
||||
enum { DS_SELF_GAP = 3 }; // doors of tier 0 / 1 are at least 3 cells from the player (not on top of the player and the first-clear reward at the player's feet)
|
||||
struct ds_grid { uint8_t blk[DS_N]; int16_t dist[DS_N]; int reach; }; // blk is filled by the caller; dist / reach are computed by ds_flood (-1 = unreachable)
|
||||
|
||||
static inline int ds_blk(const struct ds_grid *g, int ix, int iy) { return ix < 0 || iy < 0 || ix >= DS_W || iy >= DS_W || g->blk[iy * DS_W + ix]; }
|
||||
// The player occupies a cross (center + four neighbors), same convention as ground-tap pathfinding (dm_world.c wp_free)
|
||||
static inline int ds_free(const struct ds_grid *g, int ix, int iy) { return !(ds_blk(g, ix, iy) || ds_blk(g, ix + 1, iy) || ds_blk(g, ix - 1, iy) || ds_blk(g, ix, iy + 1) || ds_blk(g, ix, iy - 1)); }
|
||||
static inline int ds_dist(const struct ds_grid *g, int ix, int iy) { return ix < 0 || iy < 0 || ix >= DS_W || iy >= DS_W ? -1 : g->dist[iy * DS_W + ix]; }
|
||||
// Breadth-first search from the window center (the player's feet). The start cell is not required to have its whole cross free: the player is standing right there at this moment
|
||||
static inline void ds_flood(struct ds_grid *g) {
|
||||
static uint16_t q[DS_N]; int h = 0, t = 0; memset(g->dist, 0xff, sizeof g->dist); g->reach = 0;
|
||||
g->dist[DS_R * DS_W + DS_R] = 0; q[t++] = (uint16_t)(DS_R * DS_W + DS_R);
|
||||
while (h < t) { int c = q[h++], cx = c % DS_W, cy = c / DS_W; g->reach++;
|
||||
static const int d4[4][2] = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } };
|
||||
for (int k = 0; k < 4; k++) { int nx = cx + d4[k][0], ny = cy + d4[k][1]; if (ds_dist(g, nx, ny) != -1 || nx < 0 || ny < 0 || nx >= DS_W || ny >= DS_W || !ds_free(g, nx, ny)) continue;
|
||||
g->dist[ny * DS_W + nx] = (int16_t)(g->dist[c] + 1); q[t++] = (uint16_t)(ny * DS_W + nx); } }
|
||||
}
|
||||
static inline int ds_clear(const struct ds_grid *g, int ix, int iy, int k) { for (int dy = -k; dy <= k; dy++) for (int dx = -k; dx <= k; dx++) if (ds_dist(g, ix + dx, iy + dy) < 0) return 0; return 1; }
|
||||
// Pick the landing point of one door. All coordinates are cell offsets relative to the player (player = 0,0). (wx, wy) is the ideal point; when have_o, (ox, oy) is the other door already decided, and the new door is at least gap cells away from it (tier 2 lowers this to 2 cells).
|
||||
// Returns which tier was used (0 / 1 / 2; 3 = landed at the player's feet); the landing point is written to *rx / *ry
|
||||
static inline int ds_pick(const struct ds_grid *g, int wx, int wy, int have_o, int ox, int oy, int gap, int *rx, int *ry) {
|
||||
for (int tier = 0; tier < 3; tier++) { long best = -1; int bx = 0, by = 0, tx = tier == 2 ? 0 : wx, ty = tier == 2 ? 0 : wy;
|
||||
for (int y = -DS_R; y <= DS_R; y++) for (int x = -DS_R; x <= DS_R; x++) { int d = ds_dist(g, x + DS_R, y + DS_R); if (d < 0) continue;
|
||||
int far = abs(x) > abs(y) ? abs(x) : abs(y), og = !have_o ? 1 << 20 : abs(x - ox) > abs(y - oy) ? abs(x - ox) : abs(y - oy);
|
||||
if (tier < 2 ? (far < DS_SELF_GAP || d > DS_FAR || og < gap || !ds_clear(g, x + DS_R, y + DS_R, 2 - tier)) : (far < 1 || og < 2)) continue;
|
||||
long s = (long)(x - tx) * (x - tx) + (long)(y - ty) * (y - ty); if (best >= 0 && s >= best) continue; best = s; bx = x; by = y; }
|
||||
if (best >= 0) { *rx = bx; *ry = by; return tier; } }
|
||||
*rx = 0; *ry = 0; return 3;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#include <stdint.h>
|
||||
// "Elysium": a per-character switch checked at character creation and unchangeable afterwards. While that character is in a run: unique drop rate ×5, sets ×3 (quality ratio table). For the implementation and its basis see dm_elysium.c
|
||||
#pragma once
|
||||
void dm_elysium_arm(int on); // Called by the checkbox on the character creation page: 1 = the character created this time is an Elysium character, 0 = it is not. Applies only to the immediately following character creation and is cleared once used
|
||||
int dm_elysium_active(void); // Whether the current run is the run of an Elysium character (tables changed)
|
||||
void dm_elysium_pump(void); // Called every frame on the game main thread (dm_actions_pump)
|
||||
void dm_elysium_stash_step(uint64_t game, uint64_t player, uint8_t *main_base); // Called every tick on the game server side (where diag.c reads the player state): tops up the shared stash after a new Elysium character is created
|
||||
@@ -1,30 +0,0 @@
|
||||
#pragma once
|
||||
// HUD numbers for bottom UI v4 (read by the host once per server tick; the front end UIKit draws 'Lv · %', badges and the difficulty number) and difficulty level requests.
|
||||
// Difficulty goes through the same chain as the original game's /players handler (main+0x415db0): main+0x3102c0 gets the players setting object -> main+0x1199c50(object, N);
|
||||
// it is applied at the next server tick entry (game thread); the original game's rules apply to monsters spawned from then on (measured: level 8 stat 100 = 8, max life floor(×4.5)).
|
||||
typedef struct {
|
||||
int valid; // In a run and the controlled player has been read
|
||||
int level; // stat 12
|
||||
int exp_permille; // Experience progress within the current level ‰ (thresholds from the original game's experience.txt)
|
||||
int belt[4]; // How many items remain in each of the four belt columns (belt item x % 4 = column)
|
||||
int portal; // Town portal count: quantity in the Tome of Town Portal tbk (stat 70) + number of Scroll of Town Portal tsc
|
||||
int players; // Current players setting (players setting object +0xc); 0 = not read
|
||||
int hp, hp_max, mp, mp_max; // Life / mana (stat 6–9 ÷ 256, rounded to an integer)
|
||||
int area, x, y; // Current area number (levels.txt Id; 0 = not read) and player coordinates (cells)
|
||||
int points; // Unassigned attribute points stat 4 + skill points stat 5 (lit state of the red square cross)
|
||||
int holding; // An item is held on the cursor (a drag-and-drop did not complete and the item is still in hand): the front end uses this to treat 'tap on empty space' as cancel
|
||||
int quest_unread; // A new quest is waiting to be read: the original game's HUD quest hint is lit (client UI state byte main+0x2d5ff58); cleared after the quest panel is opened (the other condition for the red state of the bottom bar cross)
|
||||
int dead; // Player dead (mode 0 / 17): the front end hides the UIKit bottom bar so it does not block the death screen
|
||||
} dm_hud_info;
|
||||
// Player 'dead' test = the game's own definition (main+0x6e5c48 / 0x6e5cc8 client command dispatch: a player unit whose [+0xc] mode is 0 or 0x11 is handled as dead;
|
||||
// D2 player mode 0 = DEATH, in the death animation, 17 = DEAD, already dead; 0 -> 17 when the death animation ends, and it becomes town standing 5 after reviving back in town)
|
||||
static inline int dm_hud_player_mode_dead(unsigned mode) { return mode == 0 || mode == 17; }
|
||||
// UIKit bottom bar visibility: shown only in the normal in-run state -- it gets out of the way when the player has not been read, on death, in navigation states (panel / loading screen / cutscene), and when blocked by the panel stack
|
||||
// Area numbers of the five act towns (levels.txt Id): Rogue Encampment 1, Lut Gholein 40, Kurast Docks 75, Pandemonium Fortress 103, Harrogath 109
|
||||
static inline int dm_hud_area_is_town(int area) { return area == 1 || area == 40 || area == 75 || area == 103 || area == 109; }
|
||||
static inline int dm_hud_bar_visible(const dm_hud_info *h, int navigation, int blocked) { return h->valid && !h->dead && !navigation && !blocked; }
|
||||
void dm_hud_get(dm_hud_info *out);
|
||||
void dm_hud_request_players(int n); // 1..8; callable from any thread
|
||||
// Host internal (diag.c tick entry): take the latest request, publish this tick's readings
|
||||
int dm_hud_requested_players(void);
|
||||
void dm_hud_publish(const dm_hud_info *in);
|
||||
@@ -1,71 +0,0 @@
|
||||
// String lookup for the UI text we added ourselves. Our own UI text is English only, whatever language the game is set to.
|
||||
// Table: dm_i18n_tab.h (key, English), kept by hand and compiled into this file.
|
||||
// - dm_str: text drawn inside the game (layouts write "@key") goes through the original string lookup at main+0x7f5630 first and falls back to the
|
||||
// table when the game's string tables lack the key.
|
||||
// - dm_ui: places the game cannot draw (UIKit alerts, the asset download page, shared file names, bottom-bar glyphs) read the table directly.
|
||||
// The game's own language (dm_lang_index, a separate matter from our text): once the game has set it (SetLanguage observation gate), that value wins;
|
||||
// before that (asset download page, before entering the front end) read "Language" from Settings/Settings.json in the save data;
|
||||
// no settings file = first install, use the iOS system language (dm_sys_lang).
|
||||
#include "dm_i18n.h"
|
||||
#include "dm_i18n_tab.h"
|
||||
#include <TargetConditionals.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
_Atomic int g_dm_lang = -1;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#endif
|
||||
// Language tag (BCP 47, e.g. en-US, zh-Hans-CN, zh-Hant-TW, yue-Hant-HK) → the original game's language index. Only two outcomes: a language code that
|
||||
// is Chinese (zh) or a Chinese variety
|
||||
// (yue Cantonese, wuu Wu, hak Hakka, nan Min Nan, cmn Mandarin, gan Gan, hsn Xiang, lzh Literary Chinese) → 8 Simplified Chinese, regardless of script
|
||||
// and region; everything else → 0 English, because nobody has verified the original game's other languages such as German / Japanese. Empty tag = English
|
||||
int dm_lang_from_tag(const char *t) {
|
||||
if (!t || !*t) return 0;
|
||||
char l[4] = { 0 }; int i = 0; while (i < 3 && t[i] && t[i] != '-' && t[i] != '_') { l[i] = (char)(t[i] | 0x20); i++; }
|
||||
static const char *const zh[] = { "zh", "yue", "wuu", "hak", "nan", "cmn", "gan", "hsn", "lzh" };
|
||||
for (unsigned k = 0; k < sizeof zh / sizeof *zh; k++) if (!strcmp(l, zh[k])) return 8;
|
||||
return 0;
|
||||
}
|
||||
// Language on first install (no settings file yet): by the first entry of the iOS preferred languages (CFLocaleCopyPreferredLanguages, through dm_lang_from_tag: Chinese and Sinitic varieties → Simplified Chinese, everything else → English).
|
||||
// It used to be always Simplified Chinese (back then the touch version had no way to change the language, and a Chinese player whose system was in English got the English version on a fresh install and could not change it back); the Custom page title row now has a language switch (dm_custom.c lang_en), so this is opened up.
|
||||
// DM_LANG (host / test simulation, same as hle_oe.c) takes priority; a non-iOS host has no system language to ask and uses English, the default game language
|
||||
int dm_sys_lang(void) {
|
||||
const char *e = getenv("DM_LANG"); if (e && *e) return dm_lang_from_tag(e);
|
||||
#if TARGET_OS_IPHONE
|
||||
int r = 0; CFArrayRef a = CFLocaleCopyPreferredLanguages();
|
||||
if (a) { if (CFArrayGetCount(a) > 0) { char t[64]; if (CFStringGetCString((CFStringRef)CFArrayGetValueAtIndex(a, 0), t, sizeof t, kCFStringEncodingUTF8)) r = dm_lang_from_tag(t); } CFRelease(a); }
|
||||
return r;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int settings_lang(void) { // "Language" in the settings file; file missing = first install, use the system language (hle_fs.c writes the same value when it first creates the settings file); file present but key missing = -1
|
||||
const char *sv = getenv("DM_SAVE"); if (!sv) return dm_sys_lang();
|
||||
char path[1200]; snprintf(path, sizeof path, "%s/Settings/Settings.json", sv);
|
||||
FILE *f = fopen(path, "rb"); if (!f) return dm_sys_lang();
|
||||
char buf[8192]; size_t n = fread(buf, 1, sizeof buf - 1, f); fclose(f); buf[n] = 0;
|
||||
const char *k = strstr(buf, "\"Language\""); if (!k) return -1;
|
||||
k = strchr(k + 10, ':'); return k ? atoi(k + 1) : -1;
|
||||
}
|
||||
|
||||
int dm_lang_index(void) {
|
||||
int l = atomic_load(&g_dm_lang); if (l >= 0) return l;
|
||||
static _Atomic int file = -2; int f = atomic_load(&file); if (f == -2) { f = settings_lang(); atomic_store(&file, f); }
|
||||
return f < 0 ? 0 : f; // settings file exists but has no language entry = the game default (the host reports en-US)
|
||||
}
|
||||
|
||||
const char *dm_ui(const char *key) { // our own UI text is English only; a key missing from the table comes back as it is
|
||||
for (unsigned i = 0; i < sizeof k_dm_i18n / sizeof *k_dm_i18n; i++) if (!strcmp(k_dm_i18n[i].k, key)) return k_dm_i18n[i].en;
|
||||
return key;
|
||||
}
|
||||
|
||||
const char *dm_str(uint8_t *b, const char *key) {
|
||||
if (!b) return dm_ui(key);
|
||||
struct { const char *k; uint64_t n; } s = { key, strlen(key) };
|
||||
const char *r = ((const char *(*)(void *))(b + 0x7f5630))(&s);
|
||||
return r && *r && strcmp(r, key) ? r : dm_ui(key); // if the game's string tables lack this key, fall back to the compile-time table
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// String lookup for the UI text we added ourselves (English only; table = dm_i18n_tab.h, kept by hand), and the original game's language index
|
||||
#pragma once
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
extern _Atomic int g_dm_lang; // the original game's current language index (0 enUS ... 8 zhCN), written by the SetLanguage observation gate (diag.c); -1 = the game has not set it yet
|
||||
int dm_lang_index(void); // current language index of the original game (set by the game > settings file > system language on first install)
|
||||
int dm_lang_from_tag(const char *tag); // language tag (en-US / zh-Hant-TW / es-419 ...) → the original game's language index: Chinese / Chinese varieties = 8 zhCN, everything else = 0 enUS
|
||||
int dm_sys_lang(void); // system language → the original game's language index (DM_LANG > iOS preferred language > English)
|
||||
const char *dm_str(uint8_t *main_base, const char *key); // text drawn inside the game: the original string lookup, falling back to dm_ui
|
||||
const char *dm_ui(const char *key); // text drawn outside the game (UIKit / asset download page / bottom-bar glyphs): the English text from the compile-time table
|
||||
@@ -1,302 +0,0 @@
|
||||
// UI text we added ourselves (key, English); a hand-kept table, edit it directly. Read by dm_ui() / dm_str() in dm_i18n.c
|
||||
#pragma once
|
||||
static const struct { const char *k, *en; } k_dm_i18n[] = {
|
||||
{ "dmPauseRestart", "Quick Restart" },
|
||||
{ "dmUpdBuild", "Build %d" },
|
||||
{ "dmUpdNew", "New version %d · %s" },
|
||||
{ "dmUpdNoNet", "Can't check for updates · Allow Wireless Data for this app in iOS Settings" },
|
||||
{ "dmUpdFail", "Update check failed" },
|
||||
{ "dmUpdFailSite", "Update check failed · Open %s in a browser to update" },
|
||||
{ "dmUpdNewUrgent", "New version %d · %s" },
|
||||
{ "dmUpdUrgentTitle", "Update Recommended" },
|
||||
{ "dmUpdButton", "Update" },
|
||||
{ "dmVaultStored", "Stored" },
|
||||
{ "dmVaultSeen", "Not stored" },
|
||||
{ "dmVaultNone", "Not found" },
|
||||
{ "dmVaultStoredTip", "Stored · Double-tap to withdraw" },
|
||||
{ "dmVaultStats", "%d · %d stored · %d seen" },
|
||||
{ "dmVaultSetStats", "Sets: %d · %d / %d pieces stored" },
|
||||
{ "dmVaultSetFull", "%d / %d stored · complete" },
|
||||
{ "dmVaultSetHave", "%d / %d stored" },
|
||||
{ "dmVaultSearch", "%s: %d items (%d unique · %d set)" },
|
||||
{ "dmStoreHead", "Deposited: " },
|
||||
{ "dmStoreVault", "Chronicle %d" },
|
||||
{ "dmStoreStash", "%sStash %d" },
|
||||
{ "dmStorePouch", "%sPouch %d" },
|
||||
{ "dmStoreSep", ", " },
|
||||
{ "dmStoreNone", "No items to deposit" },
|
||||
{ "dmStoreLeft", ", %d left in inventory" },
|
||||
{ "dmStoreFull", "That stack is full. Item left where it is." },
|
||||
{ "dmPouchTitle", "Pouch" },
|
||||
{ "dmPouchEmpty", "(empty)" },
|
||||
{ "dmRwTitle", "%s · Socketed (%d) · Runewords" },
|
||||
{ "dmRwLevel", "Required Level: %d" },
|
||||
{ "dmKbJoin3", "%s, %s, %s" },
|
||||
{ "dmKbKilledNth", "%s slain! (kill #%d)" },
|
||||
{ "dmKbKilledTotal", "%s slain %d times in total!" },
|
||||
{ "dmKbKills", "Kills %d" },
|
||||
{ "dmKbFirst", "First %m-%d %H:%M" },
|
||||
{ "dmKbDrops", "Unique %d · Set %d" },
|
||||
{ "dmKbTimes", "Best %s · Last %s · Avg %s" },
|
||||
{ "dmKbTotal", "Total kills %s" },
|
||||
{ "dmKbAct1", "Act I" },
|
||||
{ "dmKbAct2", "Act II" },
|
||||
{ "dmKbAct3", "Act III" },
|
||||
{ "dmKbAct4", "Act IV" },
|
||||
{ "dmKbAct5", "Act V" },
|
||||
{ "dmKbRift", "Rift" },
|
||||
{ "dmKbSortAct", "By Act" },
|
||||
{ "dmKbSortCount", "By Kills" },
|
||||
{ "dmKbSortFast", "By Best Time" },
|
||||
{ "dmBrPrefix", "Boss Rush %s" },
|
||||
{ "dmBrDoneThisGame", "Already completed this game. Try again next game" },
|
||||
{ "dmBrToChamber", "First 5 stages done. Enter the portal to the Worldstone Chamber" },
|
||||
{ "dmBrAllDone", "All stages complete" },
|
||||
{ "dmBrStage", "Stage %d / %d: %s%s" },
|
||||
{ "dmBrTerror", " (Terror Zone)" },
|
||||
{ "dmBrAndariel", "Andariel" },
|
||||
{ "dmBrDuriel", "Duriel" },
|
||||
{ "dmBrMephisto", "Mephisto" },
|
||||
{ "dmBrDiablo", "Diablo" },
|
||||
{ "dmBrBaal", "Baal" },
|
||||
{ "dmBrDiabloClone", "Diablo Clone" },
|
||||
{ "dmBrUbers", "Uber Mephisto, Uber Diablo, Uber Baal" },
|
||||
{ "dmBlissTitle", "Elysium" },
|
||||
{ "dmBlissDesc", "Unique ×5, Set ×3, Runes ×5; bosses always drop as on first kill." },
|
||||
{ "dmRwHint", "Move out of shared stash first" },
|
||||
{ "dmRwGo", "Craft" },
|
||||
{ "dmPauseShareLog", "Share Diagnostics" },
|
||||
{ "dmCustomAutoMana", "Auto-drink (Life 50%, Mana 30%)" },
|
||||
{ "dmCustomMercPot", "Auto-feed Merc at Life <50%" },
|
||||
{ "dmCustomDropLight", "Light beam on valuable drops" },
|
||||
{ "dmCustomMapMark", "Automap: exits, waypoints, quests" },
|
||||
{ "dmCustomItemNotes", "Item notes (Chinese only, restart)" },
|
||||
{ "dmCustomIcloud", "iCloud save sync (after restart)" },
|
||||
{ "dmCustomPotPick", "Auto-pick potions into belt" },
|
||||
{ "dmCustomDropHaptic", "Vibrate on valuable drops" },
|
||||
{ "dmCustomGemPick", "Auto-stash runes & gems" },
|
||||
{ "dmCustomStoreBtn", "Inventory Deposit button" },
|
||||
{ "dmCustomEcoFps", "Power saver (30 fps)" },
|
||||
{ "dmCustomLootFilter", "Loot Filter (auto, Nightmare+)" },
|
||||
{ "dmCustomAutoBuff", "Auto-recast buffs when idle" },
|
||||
{ "dmCustomLangEn", "Language: English" },
|
||||
{ "dmCustomLangHint", "Restart to apply" },
|
||||
{ "dmTabGameSettings", "Settings" },
|
||||
{ "dmVaultPart0", "Helms" },
|
||||
{ "dmVaultPart1", "Armor" },
|
||||
{ "dmVaultPart2", "Shields\n& Gloves" },
|
||||
{ "dmVaultPart3", "Boots\n& Belts" },
|
||||
{ "dmVaultPart4", "Jewelry" },
|
||||
{ "dmVaultPart5", "Swords\n& Daggers" },
|
||||
{ "dmVaultPart6", "Axes\n& Claws" },
|
||||
{ "dmVaultPart7", "Maces\n& Clubs" },
|
||||
{ "dmVaultPart8", "Polearms\n& Spears" },
|
||||
{ "dmVaultPart9", "Bows &\nCrossbows" },
|
||||
{ "dmVaultPart10", "Staves\n& Casters" },
|
||||
{ "dmVaultSearchHint", "Search name" },
|
||||
{ "dmRvBytes", "%lld bytes" },
|
||||
{ "dmRvItem", "%s%s (%s)" },
|
||||
{ "dmRvSep", ", " },
|
||||
{ "dmRvItem2", "%s%s (%s / %s)" },
|
||||
{ "dmRvHolesBad", "bad hole table" },
|
||||
{ "dmRvAllOk", "All %d files present" },
|
||||
{ "dmRvReport", "%d / %d files present. %s%s%s%s%s%s" },
|
||||
{ "dmRvMissing", "Missing: " },
|
||||
{ "dmRvPeriod", ". " },
|
||||
{ "dmRvPartial", "Incomplete: " },
|
||||
{ "dmRvNoFile", "Missing %s" },
|
||||
{ "dmRvHoles", "%s.holes has a bad hole table" },
|
||||
{ "dmRvShort", "%s is incomplete (%lld / %lld bytes)" },
|
||||
{ "dmDlAppFolder", "D2R Mobile" },
|
||||
{ "dmDlQuoted", "%@" },
|
||||
{ "dmDlIncomplete", "Resources in %s are incomplete: %s" },
|
||||
{ "dmDlNoData", "No game resources found. \"D2R Mobile\" now contains: %s%s. Copy in the whole resource folder" },
|
||||
{ "dmDlTidyFail", "Could not organize the game resources (%s)" },
|
||||
{ "dmDlUnknown", "unknown reason" },
|
||||
{ "dmDlPackBad", "The downloaded resource pack is incomplete: %s" },
|
||||
{ "dmDlNone", "No game resources yet" },
|
||||
{ "dmDlNameSep", ", " },
|
||||
{ "dmDlEtc", " etc." },
|
||||
{ "dmDlRetry", "Retry" },
|
||||
{ "dmDlNoAssets", "No game resources yet\nDownload the \"D2R Mobile Resources\" folder from \"Download resource pack\" on the beta page,\nthen in Files copy the whole folder to \"On My iPhone → D2R Mobile\" (do not rename or nest it), then tap Retry" },
|
||||
{ "dmDlTestProgress", "Downloading game resources\n0.00 / 5.50 GB (0%) 0.0 MB/s" },
|
||||
{ "dmDlConnecting", "Connecting to the resource server…" },
|
||||
{ "dmDlDownloading", "Downloading game resources…" },
|
||||
{ "dmDlSpaceMsg", "%.2f GB more space needed.\nGame resources still need %.2f GB (plus 0.2 GB headroom); this %@ has %.2f GB free.\nFree up space in Settings → General → %@ Storage, then tap Retry." },
|
||||
{ "dmDlSpaceTitle", "%@ Storage Full" },
|
||||
{ "dmOk", "OK" },
|
||||
{ "dmDlProgressNote", "Downloading game resources\n%.2f / %.2f GB (%.0f%%) %.1f MB/s%@" },
|
||||
{ "dmDlCellTitle", "Using Cellular Data" },
|
||||
{ "dmDlCellMsg", "%.2f GB of game resources left to download over cellular data. Wi-Fi is recommended." },
|
||||
{ "dmDlCellLater", "Not Now" },
|
||||
{ "dmDlCellWait", "No game resources yet\nConnect to Wi-Fi, then tap Retry" },
|
||||
{ "dmDlCellGo", "Use Cellular Data" },
|
||||
{ "dmDlProgress", "Downloading game resources\n%.2f / %.2f GB (%.0f%%) %.1f MB/s" },
|
||||
{ "dmDlPaused", "Download paused (check your network). It resumes automatically." },
|
||||
{ "dmDlPackWait", "The system is still preparing the resource pack. It continues automatically." },
|
||||
{ "dmDlNotReady", "Game resources are not ready\n%@\nFix this, then tap Retry" },
|
||||
{ "dmDlWill", "Downloading game resources: %@ remaining (%.1f GB total)" },
|
||||
{ "dmDlNotStarted", "%@The resource download could not start: %@\n%@\nYou can also download the resource pack from the beta page and put it in \"On My iPhone → D2R Mobile\"" },
|
||||
{ "dmDlNetDown", "this device is offline (on first launch the system asks whether to allow network access; if you declined, turn it on in Settings → D2R Mobile)" },
|
||||
{ "dmDlNoServer", "cannot reach the resource server" },
|
||||
{ "dmDlNoBase", "this build has no download address configured" },
|
||||
{ "dmDlNextAuto", "It resumes automatically once you are back online, or tap Retry. Anything already downloaded is kept." },
|
||||
{ "dmDlNextRetry", "Tap Retry to try again. Anything already downloaded is kept." },
|
||||
{ "dmDlStopped", "The resource download stopped: %@\nDownloaded %.2f / %.2f GB. Tap Retry to resume (it will not start over)" },
|
||||
{ "dmDlCopyIn", "No game resources yet\nCopy the game resource folder into \"On My iPhone → D2R Mobile\" in the Files app\nAny folder name or nesting works; then tap Retry" },
|
||||
{ "dmDlCopyInShort", "Copy the missing files into the same folder, then tap Retry" },
|
||||
{ "dmOldNoSupportPkg", "This build has no support for older devices (romfs_overlay_astc/pack.json is missing)" },
|
||||
{ "dmOldPackSize", "The pack in \"%s\" has the wrong size (%.2f / %.2f GB); the copy looks incomplete" },
|
||||
{ "dmOldPackBad", "The pack in \"%s\" does not match this version or is corrupted; please download it again" },
|
||||
{ "dmOldPackNone", "Supplementary pack for older devices not found" },
|
||||
{ "dmOldUnsupTitle", "Device Not Supported" },
|
||||
{ "dmOldUnsupMsg", "This device (iPhone 14 or earlier, A16 chip or older) is not supported yet: its GPU cannot read the game's texture format. iPhone 15 Pro and later can run the game." },
|
||||
{ "dmOldNeedTitle", "Supplementary Pack Required" },
|
||||
{ "dmOldNeedMsg", "This device needs the supplementary pack for older devices (about %.1f GB).\nIn the Files app, copy the whole \"Legacy Device Pack\" folder into \"On My iPhone → D2R Mobile\", then open D2R Mobile again.%@" },
|
||||
{ "dmOldWhySuffix", "\n(%s)" },
|
||||
{ "dmOldDl", "Download" },
|
||||
{ "dmOldCopied", "Already Copied" },
|
||||
{ "dmOldQuit", "Quit" },
|
||||
{ "dmOldConnecting", "Connecting to the download server…" },
|
||||
{ "dmOldDlFail", "Download failed: %s" },
|
||||
{ "dmOldProgress", "Downloading the supplementary pack\n%.2f / %.2f GB (%.0f%%)%@" },
|
||||
{ "dmA11ySurface", "Game touch area" },
|
||||
{ "dmInputTitle", "Input" },
|
||||
{ "dmNameRule", "Letters or Chinese characters: 2–15 letters, or 1–5 Chinese characters (can be mixed)" },
|
||||
{ "dmNameTitle", "Character Name" },
|
||||
{ "dmQtyTitle", "Quantity" },
|
||||
{ "dmNameBlankRandom", "Leave blank for a random name" },
|
||||
{ "dmNamePickIme", "Finish choosing characters on the keyboard first" },
|
||||
{ "dmNameTooShort", "At least 2 letters, or 1 Chinese character" },
|
||||
{ "dmNameEdge", "Cannot start or end with - or _" },
|
||||
{ "dmCancel", "Cancel" },
|
||||
{ "dmConfirm", "OK" },
|
||||
{ "dmSaveNoSpace", "Not enough storage. Progress was not saved" },
|
||||
{ "dmHudLife", "Life: %d / %d" },
|
||||
{ "dmHudMana", "Mana: %d / %d" },
|
||||
{ "dmDlVerifying", "Verifying %@" },
|
||||
{ "dmDlWriteFail", "Failed to write %@: %@" },
|
||||
{ "dmDlBad3", "%@ failed verification 3 times" },
|
||||
{ "dmDlNetRetry", "Network lost. Retrying in %d s" },
|
||||
{ "dmDlReconnect", "Unstable connection. Reconnected (attempt %d)" },
|
||||
{ "dmShareLogFile", "D2RMobile_diagnostics_%@_%@.txt" },
|
||||
{ "dmVaultTierN", "Norm" },
|
||||
{ "dmVaultTierX", "Exc" },
|
||||
{ "dmVaultTierE", "Elite" },
|
||||
{ "dmStashVaultTab", "Vault" },
|
||||
{ "dmTroveTab", "Trove" },
|
||||
{ "dmRiftSayPrefix", "Rift %s" },
|
||||
{ "dmRiftSayFirst", "Level %d first clear reward" },
|
||||
{ "dmRiftSayDoneBoth", "Rift complete! Portals opened" },
|
||||
{ "dmRiftSayLateBoth", "Not cleared. Portals opened" },
|
||||
{ "dmRiftSayLateNext", "Not cleared. Portal opened" },
|
||||
{ "dmRiftSayLateTown", "Not cleared. Town Portal open" },
|
||||
{ "dmRiftSayLate", "Level not cleared" },
|
||||
{ "dmRiftSayDoneNext", "Rift complete! Portal opened" },
|
||||
{ "dmRiftSayDoneTown", "Complete! Town Portal open" },
|
||||
{ "dmRiftSayDone", "Rift complete" },
|
||||
{ "dmRiftSayErr", "The Rift could not be opened" },
|
||||
{ "dmRiftSayStart", "Level %d: 15 minute time limit" },
|
||||
{ "dmRiftSayGift", "A Rainbow Facet has dropped" },
|
||||
{ "dmRiftSayTimeout", "Time's up: level not cleared" },
|
||||
{ "dmRiftSayGuardian", "The Rift Guardian appears!" },
|
||||
{ "dmElyToaGround", "Inventory full: Token of Absolution dropped" },
|
||||
{ "dmMercSlotLocked", "Clear Greater Rift Level 10 to unlock" },
|
||||
{ "dmRiftPtsGot", "Level %d: +%d Skill Points, +%d Stat Points" },
|
||||
{ "dmSkillCapLocked", "Clear any Rift level to unlock the skill level cap" },
|
||||
{ "dmRiftPtsHeld", "Spend your points to get the rest" },
|
||||
{ "dmRiftTierN", "Level %d" },
|
||||
{ "dmRiftCapResel", "Choose another level" },
|
||||
{ "dmRiftCapGuard", "Return to Guardian" },
|
||||
{ "dmRiftCapVoid", "Run will be lost" },
|
||||
{ "dmRiftCapVoidPad", "Hold X to quit level" },
|
||||
{ "dmRiftTitleDone", "Rift Complete" },
|
||||
{ "dmRiftTitleLate", "Time's Up" },
|
||||
{ "dmRiftTitleRun", "Rift in Progress" },
|
||||
{ "dmRiftGuardDown", "Guardian slain" },
|
||||
{ "dmRiftNextOpen", "Next portal open" },
|
||||
{ "dmRiftRetryOpen", "Portal reopened" },
|
||||
{ "dmRiftSaved", "Progress saved" },
|
||||
{ "dmRiftNotCounted", "Not cleared" },
|
||||
{ "dmRiftTimeLeft", "Time Left" },
|
||||
{ "dmRiftProgress", "Progress" },
|
||||
{ "dmRiftLvlBase", "Unchanged" },
|
||||
{ "dmRiftGamUni", "Unique" },
|
||||
{ "dmRiftGamSet", "Set" },
|
||||
{ "dmRiftGamUniAfter", "Unique after" },
|
||||
{ "dmRiftGamSetAfter", "Set after" },
|
||||
{ "dmRiftMsGot", "Claimed" },
|
||||
{ "dmRiftMsNext", "Next" },
|
||||
{ "dmRiftMsNo", "Locked" },
|
||||
{ "dmKbRiftName", "Greater Rift" },
|
||||
{ "dmKbRiftBest", "Best Level: %d" },
|
||||
{ "dmKbRiftNever", "No Greater Rifts yet" },
|
||||
{ "dmKbRiftClears", "Cleared %d · Deaths %d" },
|
||||
{ "dmKbRiftUnlock", "Max: %d" },
|
||||
{ "dmKbRiftMs", "First Clear Rewards" },
|
||||
{ "dmKbRiftMsNext", "Next at Level %d" },
|
||||
{ "dmKbRiftMsAll", "All claimed" },
|
||||
{ "dmKbRiftFast", "Best Time %d:%02d" },
|
||||
{ "dmKbRiftMsGot", "First clear reward claimed" },
|
||||
{ "dmKbRiftTotal", "Best Level: %d" },
|
||||
{ "dmCloudConflict", "iCloud shared stash mismatch: kept this device. Please contact us" },
|
||||
{ "dmRiftHudTitle", "Rift" },
|
||||
{ "dmRiftHudDone", "Level %d complete" },
|
||||
{ "dmRiftHudLate", "Level %d overtime" },
|
||||
{ "dmRiftHudGuard", "Rift Guardian" },
|
||||
{ "dmRiftHudNext", "Next: Level %d" },
|
||||
{ "dmRiftHudSame", "Next: same level" },
|
||||
{ "dmRiftHudKill", "Slay the Rift Guardian" },
|
||||
{ "dmRiftHudGoal", "Slay the monsters %d%%" },
|
||||
{ "dmRiftMsItem10", "Sunder Charm" },
|
||||
{ "dmRiftMsSub10", "One at random" },
|
||||
{ "dmRiftMsTier10", "10" },
|
||||
{ "dmRiftMsItem20", "Annihilus" },
|
||||
{ "dmRiftMsTier20", "20" },
|
||||
{ "dmRiftMsItem30", "Hellfire Torch" },
|
||||
{ "dmRiftMsTier30", "30" },
|
||||
{ "dmRiftMsItem40", "Ethereal Elite" },
|
||||
{ "dmRiftMsSub40", "Socketed item" },
|
||||
{ "dmRiftMsTier40", "40" },
|
||||
{ "dmRiftMsItem50", "Colossal Jewel" },
|
||||
{ "dmRiftMsSub50", "One at random" },
|
||||
{ "dmRiftMsTier50", "50" },
|
||||
{ "dmRiftMsItem60", "Death's Fathom" },
|
||||
{ "dmRiftMsTier60", "60" },
|
||||
{ "dmRiftMsItem70", "Griffon's Eye" },
|
||||
{ "dmRiftMsTier70", "70" },
|
||||
{ "dmRiftMsItem80", "Tyrael's Might" },
|
||||
{ "dmRiftMsTier80", "80" },
|
||||
{ "dmRiftMsItem90", "Sunder Charm" },
|
||||
{ "dmRiftMsSub90", "One you are missing" },
|
||||
{ "dmRiftMsTier90", "90" },
|
||||
{ "dmRiftMsItem100", "6 High Runes" },
|
||||
{ "dmRiftMsSub100", "Lo to Zod, one each" },
|
||||
{ "dmRiftMsTier100", "100" },
|
||||
{ "dmRiftSelT", "Select Level" },
|
||||
{ "dmRiftInfT", "This Level" },
|
||||
{ "dmRiftRange", "Unlocked" },
|
||||
{ "dmRiftBest", "Highest" },
|
||||
{ "dmRiftStr", "Monster Power" },
|
||||
{ "dmRiftLvl", "Monster Level" },
|
||||
{ "dmRiftTime", "Time Limit" },
|
||||
{ "dmRiftGamT", "Gamble at Anya" },
|
||||
{ "dmRiftBtnStart", "Enter Rift" },
|
||||
{ "dmRiftBtnAbandon", "Quit" },
|
||||
{ "dmRiftBtnResel", "Select Level" },
|
||||
{ "dmRiftCapCont", "Resume your run" },
|
||||
{ "dmRiftBtnCont", "Continue" },
|
||||
{ "dmRiftMsT", "First Clear" },
|
||||
{ "dmRiftMsN", "+Rune, Token, Socket" },
|
||||
{ "dmRiftPanelTitle", "Greater Rift" },
|
||||
{ "dmPadLegDiff", "Difficulty" },
|
||||
{ "dmPadLegSort", "Sort" },
|
||||
{ "dmPadLegView", "View / Withdraw" },
|
||||
{ "dmPadLegPrev", "Previous Page" },
|
||||
{ "dmPadLegNext", "Next Page" },
|
||||
{ "dmPadHintDeposit", "Deposit" },
|
||||
{ "dmPadLegIdentify", "Pick Up (Hold to Identify)" },
|
||||
{ "dmPadDiffLine", "Game Difficulty: %d" },
|
||||
{ "dmRiftGamAfterEn", "After This Level" },
|
||||
{ "dmRiftMsNEn", "+ Rune, Token of Absolution, Sockets" },
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
// Registry of touch-only / controller-only behaviors: the mode switch transaction (dm_padmode.c) calls the "leave" of each item on the old side before changing the layout, and the "enter" of each item on the new side after the rebuild completes.
|
||||
// Leave = the item stands down by itself: void in-flight key-press / message requests made on the player's behalf, write moved original widgets back, clear cached widget addresses (after a rebuild the old addresses are all dangling).
|
||||
// Side: 0 = acts only in touch mode, 1 = acts only in controller mode, 2 = present on both sides but holding things that become invalid with a rebuild (leaves once and enters once on every switch).
|
||||
// Register a new mode-specific behavior with one row here; do not check the mode separately elsewhere, and do not have the switch code call some module by name.
|
||||
// X(name, side, enter, leave) enter / leave: void (*)(void); write dm_mode_none (an empty function) when there is none
|
||||
#define DM_MODE_ITEMS(X) \
|
||||
X("runeword list", 2, dm_mode_none, dm_runeword_ui_rebuild) \
|
||||
X("front end path straightening and key presses for the player", 0, dm_mode_none, dm_ui_auto_mode_leave) \
|
||||
X("UI touch cache and widget move table", 2, dm_mode_none, dm_ui_cache_mode_leave) \
|
||||
X("main menu build number and new version notice", 2, dm_update_mode_enter, dm_mode_none) \
|
||||
X("death popup reopened for the current mode", 2, dm_dead_modal_mode_enter, dm_mode_none) \
|
||||
X("cube side by side", 0, dm_mode_none, dm_cube_side_mode_leave)
|
||||
@@ -1,32 +0,0 @@
|
||||
// Transaction for switching between controller / touch modes (pure state machine, does not touch game memory; used by dm_padmode.c, tested by tests/t_modetx.c).
|
||||
// One switch = freeze input → registered items of the old side stand down → change the layout prefix and report the other operation mode to the game (the game rebuilds the whole UI) → read back that the rebuild is complete → report the original operation mode again (rebuild once more) →
|
||||
// read back that the rebuild is complete → registered items of the new side take their places → release input. "Rebuild complete" does not count frames: the caller supplies a readback signal count tips (the game came to read the operation mode, the set of panels under the UI root was replaced; each counts once),
|
||||
// and it is complete when the count has moved and then stays unchanged for SETTLE consecutive frames; if that has not happened after TIMEOUT frames a timeout is recorded and it proceeds anyway (input cannot be kept frozen forever), and the caller reports a failure.
|
||||
// New requests arriving during a transaction do not interrupt it: the caller keeps only the last wanted mode and compares once more after the transaction completes and returns to idle.
|
||||
#ifndef DM_MODETX_H
|
||||
#define DM_MODETX_H
|
||||
#include <stdint.h>
|
||||
enum { DM_MODETX_SETTLE = 4, DM_MODETX_TIMEOUT = 240 };
|
||||
enum { DM_TX_IDLE, DM_TX_REBUILD1, DM_TX_REBUILD2 };
|
||||
enum { DM_TXA_NONE, DM_TXA_BEGIN, DM_TXA_REPORT_BACK, DM_TXA_DONE }; // What the caller should do this frame
|
||||
typedef struct { int st, timeouts; uint64_t at, moved_at; uint32_t tips; } dm_modetx;
|
||||
static inline int dm_modetx_step(dm_modetx *t, uint64_t frame, int want, int cur, uint32_t tips) {
|
||||
if (t->st == DM_TX_IDLE) {
|
||||
if (want < 0 || want == cur) return DM_TXA_NONE;
|
||||
t->st = DM_TX_REBUILD1; t->at = frame; t->moved_at = 0; t->tips = tips; t->timeouts = 0; return DM_TXA_BEGIN; }
|
||||
if (tips != t->tips) { t->tips = tips; t->moved_at = frame; }
|
||||
int done = t->moved_at && frame - t->moved_at >= DM_MODETX_SETTLE, late = frame - t->at >= DM_MODETX_TIMEOUT;
|
||||
if (!done && !late) return DM_TXA_NONE;
|
||||
if (!done) t->timeouts++;
|
||||
t->at = frame; t->moved_at = 0;
|
||||
if (t->st == DM_TX_REBUILD1) { t->st = DM_TX_REBUILD2; return DM_TXA_REPORT_BACK; }
|
||||
t->st = DM_TX_IDLE; return DM_TXA_DONE;
|
||||
}
|
||||
// Input gate: during the freeze all keys handed to the game are 0, and the keys held at any point during it are recorded; after release these keys must be let go once before they are handed to the game again
|
||||
// (the press that triggers the switch only does the switch: pressing A switches the UI to the controller version, and that A must not land on the freshly built UI). Returns 1 = the sticks are zeroed too
|
||||
typedef struct { uint64_t hold; } dm_modegate;
|
||||
static inline int dm_modegate_apply(dm_modegate *g, int frozen, uint64_t *buttons) {
|
||||
if (frozen) { g->hold |= *buttons; *buttons = 0; return 1; }
|
||||
g->hold &= *buttons; *buttons &= ~g->hold; return 0;
|
||||
}
|
||||
#endif
|
||||
@@ -1,27 +0,0 @@
|
||||
// The four face buttons of a physical controller -> game keys: mapped by the letter printed on the button glyph (press whichever letter the
|
||||
// button button legend shows). Pure function, used by the front end dm_gamepad.m, checked by the host unit test tests/t_padmap.c.
|
||||
// The four face buttons given by the system (GameController) are defined by position, not by the letter on the button glyph:
|
||||
// buttonA bottom, buttonB right, buttonX left, buttonY top.
|
||||
// - Nintendo controllers (Switch Pro, Joy-Con): the glyphs are right A, bottom B, top X, left Y -- identical to the Switch as is.
|
||||
// - Xbox controllers: the glyphs are bottom A, right B, left X, top Y.
|
||||
// - Controllers with no letters on the glyphs (PS etc.) and unrecognized types: laid out by the Xbox positions (bottom button = confirm).
|
||||
// The type is recognized by whether the two strings given by the system, product category and vendor name (case-insensitive), contain
|
||||
// switch / nintendo / joy-con; neither = laid out as Xbox.
|
||||
// (The vendor name of the Switch Pro is "Pro Controller", which has none of these words, so it must be recognized by the product category
|
||||
// "Switch Pro Controller"; that is why both strings are checked.)
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include "dm_input.h"
|
||||
enum { DM_PADKIND_XBOX = 0, DM_PADKIND_NINTENDO = 1 };
|
||||
static inline int dm_padmap_has(const char *s, const char *w) { size_t n = strlen(w); for (; s && *s; s++) if (!strncasecmp(s, w, n)) return 1; return 0; }
|
||||
static inline int dm_padmap_kind(const char *category, const char *vendor) {
|
||||
static const char *const k[] = { "switch", "nintendo", "joy-con" };
|
||||
for (unsigned i = 0; i < sizeof k / sizeof *k; i++) if (dm_padmap_has(category, k[i]) || dm_padmap_has(vendor, k[i])) return DM_PADKIND_NINTENDO;
|
||||
return DM_PADKIND_XBOX;
|
||||
}
|
||||
static inline uint64_t dm_padmap_face(int kind, int south, int east, int west, int north) { // The four parameters = whether the bottom / right / left / top button is held
|
||||
if (kind == DM_PADKIND_NINTENDO) return (east ? DM_BTN_A : 0) | (south ? DM_BTN_B : 0) | (north ? DM_BTN_X : 0) | (west ? DM_BTN_Y : 0);
|
||||
return (south ? DM_BTN_A : 0) | (east ? DM_BTN_B : 0) | (west ? DM_BTN_X : 0) | (north ? DM_BTN_Y : 0);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Rift tower climb (dm_rift.c)
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
typedef struct { int tier; float progress; int secs_left, done, guardian, inc; } dm_rift_hud_t;
|
||||
void dm_rift_tick(uint64_t G, uint64_t player, uint8_t *main_base); // Server tick (diag.c)
|
||||
void dm_rift_c2s(uint64_t G, const uint8_t *pkt, uint32_t len); // Client command (door tap)
|
||||
void dm_rift_client_pump(uint8_t *main_base); // At the controller sampling point (dm_world.c): recognize kills and newly spawned monsters
|
||||
int dm_rift_hud(dm_rift_hud_t *out); // Front end every frame: returns 1 while a rift is in progress / just completed
|
||||
int dm_rift_active(void);
|
||||
void dm_rift_gamble_vals(int best, uint32_t *uniq, uint32_t *set); // Gambling bonus: highest cleared level -> Hell unique / set values (in units of 1/100000); best < 1 = 0 (no bonus)
|
||||
typedef struct { int menu, best, unlock, def; unsigned ms; int clvl, tier, done, secs_left; float progress; int inc; } dm_rift_menu_t; // menu 1 = level selection page, 2 = continue / quit; inc = 0 with done set = Guardian defeated after time ran out (not passed)
|
||||
int dm_rift_cleared_any(void); // has this character cleared any Greater Rift level (only clears within the time limit count: highest cleared level ≥ 1): the unlock condition for the hard skill point cap (dm_actions.c)
|
||||
int dm_rift_merc_unlocked(void); // Whether the new mercenary equipment slots are unlocked (highest rift level cleared ≥ 10)
|
||||
void dm_rift_merc_locked_hint(void); // Hint when not unlocked: "Clear Greater Rift Level 10 to unlock"
|
||||
int dm_rift_tier_level(int tier, int clvl); // this level's monster level (below level 9, 0 = unchanged)
|
||||
int dm_rift_tier_hp_x10(int tier, int clvl); // total life multiplier of this level's monsters ×10 (relative to a native Hell /players 1 monster)
|
||||
int dm_rift_menu(dm_rift_menu_t *out); // Front end every frame: the page to show (0 = none)
|
||||
void dm_rift_menu_close(void); // Close (the X at the top right)
|
||||
void dm_rift_menu_start(int tier); // Level selection page "Enter Rift"
|
||||
void dm_rift_menu_continue(void); // "Continue"
|
||||
void dm_rift_menu_abandon(void);
|
||||
int dm_rift_menu_pending(void); int dm_rift_menu_seq(void); int dm_rift_swept(uint32_t id); // "Quit" (after abandoning, the server opens the level selection page again)
|
||||
void dm_rift_menu_peek(dm_rift_menu_t *out); // Fetch the page readout even when there is no page to open (chronicle "Rift" page)
|
||||
void dm_rift_ui_pump(void); // dm_rift_ui.c: every frame on the game main thread (dm_actions.c)
|
||||
void dm_rift_ui_on_uimsg(const char *cat, const char *act, const char *arg); // UI messages (diag.c)
|
||||
typedef struct { int best, clears, deaths, tier, unlock; unsigned ms; int fast[101]; } dm_rift_stats_t;
|
||||
int dm_rift_stats(dm_rift_stats_t *out); // Kill board "Rift" page readout (sidecar file); returns 0 if there is none
|
||||
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
// Text box color code mapping table (implementation and notes in dm_textbox_core.c)
|
||||
#include <stddef.h>
|
||||
int dm_textbox_load(const char *path); // Read the table, return the entry count; return -1 if it cannot be opened (table cleared)
|
||||
void dm_textbox_reset(void);
|
||||
int dm_textbox_count(void);
|
||||
// Rewrite by the table: 0 = unchanged (*out is NULL); 1 = whole-string hit; 2 = annotated substrings inside were replaced. *out is freed by the caller
|
||||
int dm_textbox_rewrite(const char *s, size_t n, char **out, size_t *on);
|
||||
// Read the mapping table (read only once, does not hook the gate; implemented in dm_textbox.c): returns 1 = table present, 0 = none (item annotations off / no overlay)
|
||||
int dm_textbox_ensure_loaded(void);
|
||||
@@ -1,17 +0,0 @@
|
||||
// On screens narrower than 16:9 (iPad 4:3), lay out the game UI at 16:9 while 3D still fills the whole screen. Off by default: when not turned on nothing is written, and the conversions below are all identity. Implementation and basis in dm_uiscreen.c
|
||||
#pragma once
|
||||
enum { DM_UISCREEN_OFF, DM_UISCREEN_TOP, DM_UISCREEN_CENTER, DM_UISCREEN_BOTTOM };
|
||||
void dm_uiscreen_enable(int mode); // Called by the front end when "the window is narrower than 16:9": DM_UISCREEN_BOTTOM etc. = turn on, with the UI rectangle (width = frame width, height = width÷16×9) top-aligned / centered / bottom-aligned; DM_UISCREEN_OFF = turn off and write the game's values back. Callable from any thread, takes effect next frame
|
||||
void dm_uiscreen_pump(void); // Called every frame on the game main thread: on finding that the game rewrote the "screen width / height" globals (boot, switching docked / handheld mode), record the real values and rewrite them to the values the UI uses - this one place only, regardless of occasion
|
||||
int dm_screen_real(int *w, int *h); // Real frame width and height (the one used by 3D rendering and hit boxes). Returns 0 = not read yet
|
||||
void dm_ui_rect(int *x, int *y, int *w, int *h); // Position of the UI rectangle in the real frame (pixels); when off = the whole frame. The graphics layer shifting the UI draw and the front end placing the game view both read it; it is the only source for the vertical position
|
||||
float dm_ui_ny(float ny_real); // Normalized y in the real frame → normalized y within the UI rectangle (horizontal unchanged)
|
||||
float dm_ui_ny_back(float ny_ui); // Reverse
|
||||
int dm_ui_proj_dy(void); // Number of vertical pixels subtracted from the result of the game's "world point → screen" (main+0x1237c60 / 0x90d110) (the UI rectangle's y; not subtracting = 0). Add it back when the host itself calls these two functions and needs real frame coordinates
|
||||
int dm_uiscreen_ingame(void); // 1 = in a run (true fullscreen); 0 = out of a run (front end pages and loading screen: 16:9 centered with margins). The single signal, readable from any thread
|
||||
void dm_uiscreen_set_ingame(int on); // May only be called from the per-frame UI site (dm_cursor.c)
|
||||
void dm_uiscreen_set_panel(int on); // A large panel open in a run (function pages / stash / vendor / cube..., = the touch front end's panel mode): the UI rectangle moves to the center; when closed it drops back to the placement the front end chose. Called by the touch front end in the UI message callback
|
||||
int dm_uiscreen_panel(void); // The state above: a large panel open in a run = 1
|
||||
#include <stdint.h>
|
||||
uint64_t dm_game_res(int docked); // The value the game's render resolution parameter slot should give right now (packed {u32 width, u32 height}): the front end's native value, changed to 16:9 of the screen width when true fullscreen is on and not in a run
|
||||
void dm_uiscreen_res_reread(void); // May only be called by hle_oe.c when the game takes the "performance mode changed" notification
|
||||
@@ -1,31 +0,0 @@
|
||||
// Unit → the level it is in (the host reads game memory directly; server units and client units share the same offsets):
|
||||
// unit +0x38 path → path +0x20 room (for object / item class units the "path" is a different struct, with the room at +0) → room +0x18 → +0x90 level → level +0x1f8 level id.
|
||||
// Every step can be NULL and must be null-checked one by one: during the act-change loading screen the client player has a path but no room.
|
||||
// Previously various places read four levels in one go, rd(rd(rd(rd(u + 0x38) + 0x20) + 0x18) + 0x90); rd only guards address 0, so with a NULL room it went on to read address 0x18 →
|
||||
// sig=11 addr=0x18 on both real device and Mac (in a rift, tapping "Continue" after dying in another act's map, during those few act-change loading frames). Unit test tests/t_unit_level.c.
|
||||
#ifndef DM_UNIT_H
|
||||
#define DM_UNIT_H
|
||||
#include <stdint.h>
|
||||
static inline uint64_t dm_unit_rd64(uint64_t a) { return a ? *(volatile uint64_t *)(uintptr_t)a : 0; }
|
||||
// Level struct pointer; returns 0 if any step is NULL
|
||||
static inline uint64_t dm_unit_level_ptr(uint64_t u) {
|
||||
uint64_t path = u ? dm_unit_rd64(u + 0x38) : 0, room = path ? dm_unit_rd64(path + 0x20) : 0, ex = room ? dm_unit_rd64(room + 0x18) : 0;
|
||||
return ex ? dm_unit_rd64(ex + 0x90) : 0;
|
||||
}
|
||||
// Level id; returns 0 = unknown level when it cannot be read (unit / path / room NULL at any step)
|
||||
static inline int dm_unit_level(uint64_t u) { uint64_t lv = dm_unit_level_ptr(u); return lv ? *(volatile int32_t *)(uintptr_t)(lv + 0x1f8) : 0; }
|
||||
// Head of a level's room chain (level +0x10, next room = room +0x48); returns 0 when the level pointer is NULL - anything that walks rooms from the result of dm_unit_level_ptr must go through here,
|
||||
// so that "level could not be read" never turns into reading address 0x10
|
||||
static inline uint64_t dm_level_rooms(uint64_t lv) { return lv ? dm_unit_rd64(lv + 0x10) : 0; }
|
||||
// Fixed-point teleport: put the unit in a free spot next to (target room, x, y) and send the client a relocation = the original main+0x7cc530(game,
|
||||
// unit, target room, x, y, 0, 0).
|
||||
// The target room must be given: the original change-level-by-number 0x634000 is itself "0x5596c0 gets the landing point and room → 0x7cc530(..., room,
|
||||
// x, y, 0, 0)" (0x634168–0x634188).
|
||||
// Passing 0 only works when the target is near the unit's current room; a target in another cluster of rooms returns failure (after finishing a Rift and
|
||||
// returning to town, tapping "Continue" landed on that map's waypoint).
|
||||
// A room of 0 (the target room is not loaded / not in this map) returns 0 directly without taking chances
|
||||
static inline int dm_teleport(const uint8_t *main_base, uint64_t G, uint64_t unit, uint64_t room, int x, int y) {
|
||||
if (!main_base || !G || !unit || !room) return 0;
|
||||
return ((int (*)(uint64_t, uint64_t, uint64_t, int, int, int, int))(uintptr_t)(main_base + 0x7cc530))(G, unit, room, x, y, 0, 0);
|
||||
}
|
||||
#endif
|
||||
@@ -1,31 +0,0 @@
|
||||
// Before reading or writing "class-specific fields" of a native UI widget by offset, check that it is the expected class (vtable).
|
||||
// Why: a widget found by name is not guaranteed to be the class the offsets were measured on (layout changed, a same-named widget changed class, the game swapped the slot for another widget), and writing by offset then scribbles into some other object.
|
||||
// Base-class fields shared by all widgets (name +8, visible +0x50 / +0x51, children +0x58 / +0x60, rect +0x70) need no check.
|
||||
// Vtable values are offsets within the main module (3.3.94012), read on the host:
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
enum {
|
||||
DM_VT_TEXT = 0x20ba4c8, // text box: string +0x88 / length +0x90, text color +0xbc
|
||||
DM_VT_INPUT = 0x20b8718, // input box: committed text string +0x558
|
||||
DM_VT_NUMINPUT = 0x20b91c0, // numeric input box
|
||||
DM_VT_TOGGLE = 0x20bc110, // checkbox / sub-tab (ToggleButtonWidget): checked +0xa88, four frame ids at +0x6e8.., current frame +0x610
|
||||
DM_VT_TABBAR = 0x20ba3a8, // tab bar (TabBarWidget)
|
||||
DM_VT_SLOT = 0x2089fa0, // item slot: body location +0x5d8, owner +0x5c8
|
||||
DM_VT_SLOT_B = 0x2089ab8, // item slot (another kind, recognized by dm_actions.c / dm_runeword.c)
|
||||
DM_VT_SLOT_C = 0x2088fc8, // item slot (the third kind, recognized by dm_cursor_item.c)
|
||||
DM_VT_IMAGE = 0x20b8650, // image widget: current frame +0x90 (read on the host: Img on the Chronicle page)
|
||||
DM_VT_BUTTON = 0x20b7b00, // button: four frame ids at +0x6e8.., current frame +0x610 (read on the host: TouchRF_M1 on the Greater Rift level select page)
|
||||
};
|
||||
|
||||
static inline uint64_t dm_widget_vt(const uint8_t *base, uint64_t w) { return w && base ? *(const uint64_t *)(uintptr_t)w - (uint64_t)(uintptr_t)base : 0; }
|
||||
static inline int dm_widget_is(const uint8_t *base, uint64_t w, uint64_t vt) { return w && base && dm_widget_vt(base, w) == vt; }
|
||||
static inline int dm_widget_is_slot(const uint8_t *base, uint64_t w) { uint64_t v = dm_widget_vt(base, w); return v == DM_VT_SLOT || v == DM_VT_SLOT_B || v == DM_VT_SLOT_C; }
|
||||
|
||||
// For call sites: expected class → 1; otherwise → 0 and log one failure (at most 5 per file, no log flooding). A file using it needs LOG (hle.h) and one DM_WIDGET_RED_DEF in the file
|
||||
#define DM_WIDGET_OK(base, w, vt, who) (dm_widget_is((base), (w), (vt)) ? 1 : dm_widget_red_((base), (w), (vt), (who)))
|
||||
#define DM_WIDGET_RED_DEF \
|
||||
static int dm_widget_red_(const uint8_t *base, uint64_t w, uint64_t vt, const char *who) { static int n; \
|
||||
if (w && base && n++ < 5) { const char *nm = *(const char **)(uintptr_t)(w + 8); \
|
||||
LOG("[RED] %s: widget '%s' is not of the expected class (vtable main+%#llx, should be %#llx), offsets not read or written", who, nm ? nm : "?", (unsigned long long)dm_widget_vt(base, w), (unsigned long long)vt); } \
|
||||
return 0; }
|
||||
@@ -1,11 +0,0 @@
|
||||
// Tapping a target in the game world = interacting with it (implementation and rationale in dm_world.c)
|
||||
#pragma once
|
||||
unsigned dm_world_tap(float nx, float ny); // any thread: queue one tap (coordinates normalized to the rendered picture, [0,1]); the main thread resolves the target and sends the command at the controller-poll point, and sends nothing when there is no target. Returns the sequence number
|
||||
int dm_world_tap_result(unsigned seq); // -1 not executed yet; 0 no target; 1 item (pick up) 2 NPC (talk) 3 object (use) 4 exit 5 corpse (retrieve)
|
||||
int dm_world_has_target(void); // any thread: whether a target that the primary button would interact with exists (the game's own controller-version selection)
|
||||
void dm_world_pump(void); // called every frame where the controller is fetched (main thread)
|
||||
void dm_world_watch(int type, uint32_t cls); // test probe: watch one kind of unit (type < 0 cancels); the main thread publishes the screen position of its body box center every frame
|
||||
int dm_world_watch_pos(float *nx, float *ny); // any thread: returns 1 and gives normalized coordinates when the watched unit is in the picture
|
||||
// Minimap quest goals (dm_world.c): published by the main thread every frame, fetched by the front end from any thread. x / y = world cell; art >= 64 = frame art-64 of the Tal Rasha symbols, otherwise = nameplate number (tools/gen_map_goal_art.py)
|
||||
typedef struct { int n, px, py; float sx, sy; int x[12], y[12]; unsigned char art[12]; } dm_map_goal_view; // px / py player world cell; sx / sy position of the player's feet on screen (normalized)
|
||||
int dm_map_goals(dm_map_goal_view *v); // returns the number of goals (map marking switch off, no goal in this level, scan not finished = 0)
|
||||
+1
-4
@@ -4,10 +4,7 @@
|
||||
void front_run(void (*guest)(void));
|
||||
// The CAMetalLayer the graphics backend presents frames to (NULL when there is no window)
|
||||
void *front_metal_layer(void);
|
||||
// Game output resolution (the rewriter's PARAMS resolution parameter slot): docked = 1 docked mode / 0 handheld mode, returns (u64)height << 32 | width.
|
||||
// The original game's constants are 1600x900 / 1280x720 (16:9); front ends with a different screen aspect ratio widen at the same height, and the game natively renders at the new width and re-lays out the UI (width ≤ 4096).
|
||||
#include <stdint.h>
|
||||
uint64_t front_game_res(int docked);
|
||||
// Text input box (for the software keyboard swkbd): the main thread shows a native input box and the calling thread blocks until the user confirms / cancels.
|
||||
// All text is UTF-8; max_chars is the maximum character count (0 = unlimited, only a hint for the front end; truncation is done uniformly by the caller hle_swkbd.c in UTF-16 units).
|
||||
// Confirm returns 1 and writes the result into out (including the terminating 0); cancel returns 0;
|
||||
@@ -20,7 +17,7 @@ void dm_host_preload_start(int argc, char **argv);
|
||||
|
||||
// Touchscreen (implemented by the host's hle_touch.c; called by the iOS / Mac front end): whenever the set of touch points changes (down / move / up / cancel) the whole set is pushed once; n = 0 = all lifted.
|
||||
// id = finger slot number 0–15 (the lowest free slot on down, unchanged until up; used directly as finger_id); x / y = normalized coordinates [0,1) on the output frame, origin at top left.
|
||||
// The host converts to the panel coordinates the game reads, 1280x720 (x·1280, y·720, the way main 0x1b65260 reads them), and latches short taps (a down and up between two reads is reported at least once). May be called from any thread.
|
||||
// The host converts to the panel coordinates the game reads, 1280x720 (x·1280, y·720), and latches short taps (a down and up between two reads is reported at least once). May be called from any thread.
|
||||
typedef struct { uint32_t id; float x, y; } dm_touch;
|
||||
void dm_touch_set(const dm_touch *t, int n);
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
#import <Metal/Metal.h>
|
||||
#include "dm.h"
|
||||
#include "dm_input.h"
|
||||
#include "dm_uiscreen.h"
|
||||
void mtl_set_ui_rect_fn(void (*fn)(double *y, double *h));
|
||||
static void ui_rect_frac(double *fy, double *fh) { int x, y, w, h, rw = 0, rh = 0; dm_ui_rect(&x, &y, &w, &h); if (dm_screen_real(&rw, &rh) && rh > 0) { *fy = (double)y / rh; *fh = (double)h / rh; } } // Same as front_ios.m: vertical position and height of the UI rectangle (as fractions of the frame height)
|
||||
__attribute__((constructor)) static void ui_shift_reg(void) { mtl_set_ui_rect_fn(ui_rect_frac); } // Must also take effect when running without a window (screenshots / gate checks), so it is not placed where the layer is created
|
||||
#include "front.h"
|
||||
#include "hle.h"
|
||||
#include <signal.h>
|
||||
@@ -19,13 +15,6 @@ static uint64_t g_btn;
|
||||
static int g_w, g_a, g_s, g_d;
|
||||
|
||||
void *front_metal_layer(void) { return (__bridge void *)g_layer; }
|
||||
// The original game's 16:9 constants, behavior unchanged; DM_GAME_RES=<width>x<height> (docked mode; handheld scaled by 720/900) is only for side-by-side comparison with iOS widescreen
|
||||
uint64_t front_game_res(int docked) {
|
||||
unsigned w, h; const char *e = getenv("DM_GAME_RES");
|
||||
if (e && sscanf(e, "%ux%u", &w, &h) == 2) return docked ? (uint64_t)h << 32 | w : (uint64_t)(h * 4 / 5) << 32 | (w * 4 / 5 + 3) / 4 * 4;
|
||||
return docked ? 900ull << 32 | 1600 : 720ull << 32 | 1280;
|
||||
}
|
||||
|
||||
static void push(void) {
|
||||
int32_t lx = (g_d - g_a) * 32767, ly = (g_w - g_s) * 32767;
|
||||
dm_input_set(g_btn, lx, ly, 0, 0);
|
||||
|
||||
@@ -61,8 +61,6 @@ enum { G_SVC, G_CNTPCT, G_CNTVCT, G_OBS };
|
||||
static void (*g_obs_fn[OBS_MAX])(const uint64_t *regs); static int g_obs_have[OBS_MAX];
|
||||
void gate_obs_set(int id, void (*fn)(const uint64_t *regs)) { if (id <= 0 || id >= OBS_MAX) die("observation gate number out of range %d", id); g_obs_fn[id] = fn; }
|
||||
int gate_obs_present(int id) { return id > 0 && id < OBS_MAX && g_obs_have[id]; }
|
||||
static uint8_t g_svc_have[256]; // svc numbers registered in the build output (used to tell whether the host call numbers from 0x80 up, introduced by cuts, are present)
|
||||
int gate_svc_present(int num) { return num >= 0 && num < 256 && g_svc_have[num]; }
|
||||
typedef struct { uint64_t ret; int kind, arg; uint64_t orig; } gate_t;
|
||||
#define GATE_BITS 12 // 4096 slots, gate count ≤ 1024, load factor ≤ 1/4
|
||||
static gate_t g_gate[1u << GATE_BITS];
|
||||
@@ -73,7 +71,6 @@ void gate_register(uint64_t ret_abs, const char *kind, int arg, uint64_t orig) {
|
||||
int k = !strcmp(kind, "svc") ? G_SVC : !strcmp(kind, "cntpct") ? G_CNTPCT : !strcmp(kind, "cntvct") ? G_CNTVCT : !strcmp(kind, "obs") ? G_OBS : -1;
|
||||
if (k < 0) die("unknown gate kind %s", kind);
|
||||
if (k == G_OBS) { if (arg <= 0 || arg >= OBS_MAX) die("observation gate number out of range %d", arg); g_obs_have[arg] = 1; }
|
||||
if (k == G_SVC && arg >= 0 && arg < 256) g_svc_have[arg] = 1;
|
||||
if (++g_ngate > (1 << GATE_BITS) / 4) die("gate overflow");
|
||||
unsigned i = gate_h(ret_abs);
|
||||
while (g_gate[i].ret) { if (g_gate[i].ret == ret_abs) die("gate registered twice %#llx", ret_abs); i = (i + 1) & ((1u << GATE_BITS) - 1); }
|
||||
|
||||
+1
-26
@@ -20,11 +20,10 @@ void *objmap_del(const void *guest);
|
||||
uint64_t now_ns(void);
|
||||
// This thread is doing the host's own heavy work (NVN submit / present: encoding Metal commands; on hardware that is the GPU's business and does not occupy a guest core). The same-core preemption approximation (hle_thread.c) does not count it as "running"
|
||||
extern __thread _Atomic int dm_host_work;
|
||||
uint64_t guest_ns(void); // Guest clock (hle_os.c; does not advance while DM_TICK_HOLD is frozen)
|
||||
uint64_t guest_ns(void); // Guest clock (hle_os.c; does not advance while frozen, guest_clock_freeze)
|
||||
int dm_detclock_enabled(void);
|
||||
void dm_guest_clock_advanced(void);
|
||||
uint64_t dm_guest_frame(void); // Incremented each time the host presents a frame; unaffected by machine load
|
||||
uint64_t dm_server_tick(void); // In-run server tick; returns UINT64_MAX when not in a run or while DM_TICK_HOLD is frozen
|
||||
void guest_clock_freeze(int on);
|
||||
void guest_clock_freeze_for(int reason, int on); // Freeze by reason (1 background, 2 system keyboard); see hle_os.c
|
||||
|
||||
@@ -48,10 +47,6 @@ typedef struct { const char *name; void *fn; const char *why; } hle_force_ent;
|
||||
|
||||
// Operation mode (0 handheld / 1 docked): owned by hle_oe.c, and the hid controller style follows it; the front end can switch it at runtime
|
||||
int dm_opmode(void);
|
||||
void dm_3d_scale_hot(int hot); // When overheating, lower the 3D render scale to 0.70 (UI untouched); 0 = restore
|
||||
ssize_t dm_data_pread(int fd, void *buf, size_t n, off_t off); // Resource archive reads: zero-stripped archives with a ".holes" extent table are translated through the table (hle_fs.c)
|
||||
void dm_data_note_fd(int fd, const char *hostpath);
|
||||
int dm_overlay_file(const char *rel, char *out, size_t n); // File in the overlay: DM_ROMFS_OVERLAY may have several layers (colon-separated, earlier ones take priority); look for <layer>/<rel> layer by layer, and on a hit write it to out and return 1 (hle_fs.c)
|
||||
void dm_set_opmode(int m); // On change, push notification 30 / 31 according to the switches the game has turned on
|
||||
// Focus (1 foreground / 2 focus lost / 3 background): called by the front end on lifecycle changes; a change pushes notification 15
|
||||
void dm_focus_set(int st);
|
||||
@@ -60,23 +55,3 @@ extern uint8_t *dm_fps_state; // frame step state page (loader.c install_fps_s
|
||||
void dm_audio_suspend(int on);
|
||||
uint64_t dm_audio_render_calls(void); // Cumulative sound card callback count (the silent sink is not counted)
|
||||
void dm_audio_reset(void); // iOS media services reset: recreate the AudioUnits (hle_audio.c)
|
||||
|
||||
// ---------- Host call number registry (a cut replaces a game-internal function entry with `svc #number; ret`, and the rewriter turns it into a host gate) ----------
|
||||
// Horizon's SVC numbers only go up to 0x7F; 0x80 onward is reserved for host calls. New ones must be registered here and numbers must not repeat; dispatch is in hle_svc.c → svc_host_call.
|
||||
// No. Purpose File Cut (main offset)
|
||||
// 0x80 TACT BLTE block zlib decode → libdeflate hle_tact.c 0x1b0ee60 (DecodeZlib entry)
|
||||
// 0x81 Client command packet delivery (receive pump → server receive) diag.c 0x5d3a88 (bl 0x632510 call site; forwarded as is by default, dropped under DM_C2S_REPLAY)
|
||||
// 0x82 System-class client message delivery (receive pump → 0x6310b0) diag.c 0x5d39c8 (bl 0x6310b0 call site; forwarded as is by default, dropped from the file's first tick on under DM_C2S_REPLAY)
|
||||
// 0x83 Granny section per-block decode (section read 0x11f0110) hle_granny.c 0x11f02dc (bl 0x11f4af0 call site; by default replaced with the pre-decoded candidate, forwarded as is if it does not check out)
|
||||
// 0x84 Granny fixup table per-block decode (section read 0x11f07d0) hle_granny.c 0x11f0914 (bl 0x11f4af0 call site; same as above)
|
||||
// 0x85 Private skill HUD self-draw query (read-only, writes x8) hle_tact.c 0x3e443c (branch gate: x8≠0 jumps to 0x3e4460, otherwise executes the original instruction and returns to 0x3e4440; touch-only private gate)
|
||||
// 0x86 Private skill ground direction (forwards to the original 0x2b4ef8, then redirects by the right stick) hle_private_aim.c 0x2faf04 (bl 0x2b4ef8 call site; touch-only private gate)
|
||||
// 0x87 Ground item appearance path (forwards to the original 0xa14dc4, then repoints by rule to the lit variant) dm_dropfilter.c 0x8fa590 (bl 0xa14dc4 call site; only items in mode 3 / 5 are changed, output in the x8 string)
|
||||
// 0x88 Item landing drop sound (forwards to the original 0x470c20, then swaps tier-one items to a notification sound) dm_dropfilter.c 0x468f2c (bl 0x470c20 call site; writes x0 = sound id)
|
||||
// 0x89 Rift exit rerouting (in a rift run the destination is replaced with the next map, writes x2 / x3 / x8) dm_rift.c 0x7cc1e0 (branch gate: x8≠0 jumps to 0x634000, otherwise executes the original instruction and returns to 0x7cc1e4)
|
||||
// 0x8a Spawn point for the player on run entry (forwards to the original 0x5596c0; replaced with a point next to the waypoint when entering in town) dm_townwp.c 0x5d8040 (bl 0x5596c0 call site; in the player join function 0x5d7f70)
|
||||
// 0x8b Drop point for the corpse on run entry (same as above, the same point as the player gets) dm_townwp.c 0x5d8400 (bl 0x5596c0 call site; in the same function)
|
||||
// 0x8c Current act saved while in a rift (when the character is in a rift map w9 is replaced with 4 = Act V, writes x9; x8 always 0) dm_rift.c 0x6fa9d0 (branch gate: x8≠0 jumps to 0x6fa9d4, otherwise executes the original instruction and returns to 0x6fa9d4)
|
||||
// 0x8d Extra mercenary equipment slots (mercenary + rift level ≥10 + belt / boots / gloves / ring / amulet: w0 is replaced with 1, writes x0; x8 always 0) dm_rift.c 0x64a34c (branch gate: x8≠0 jumps to 0x64a350, otherwise executes the original instruction and returns to 0x64a350)
|
||||
// 0x8e Client-side check for the extra mercenary equipment slots (rift level ≥10 + the item is a belt / boots / gloves / ring / amulet: w1 is replaced with the item's own type number, writes x1; x8 always 0) dm_rift.c 0x3beb58 (branch gate: x8≠0 jumps to 0x3beb5c, otherwise executes the original instruction and returns to 0x3beb5c)
|
||||
enum { SVC_HOST_TACT_ZLIB = 0x80, SVC_HOST_C2S_DELIVER = 0x81, SVC_HOST_C2S_SYS = 0x82, SVC_HOST_GRANNY_A = 0x83, SVC_HOST_GRANNY_B = 0x84, SVC_HOST_PRIVATE_SKILL_RENDER = 0x85, SVC_HOST_PRIVATE_SKILL_AIM = 0x86, SVC_HOST_GROUND_ASSET = 0x87, SVC_HOST_DROP_SOUND = 0x88, SVC_HOST_RIFT_EXIT = 0x89, SVC_HOST_JOIN_SPAWN = 0x8a, SVC_HOST_JOIN_CORPSE = 0x8b, SVC_HOST_SAVE_ACT = 0x8c, SVC_HOST_MERC_SLOT = 0x8d, SVC_HOST_MERC_SLOT_CL = 0x8e }; // 0x87: fetch the model path for ground items (dm_dropfilter.c drop light) // 0x83/0x84: Granny per-block decode call sites (hle_granny.c)
|
||||
@@ -21,8 +21,8 @@ static void *fdstat_main(void *arg) {
|
||||
if (S_ISSOCK(st.st_mode)) { sock++; continue; } if (S_ISFIFO(st.st_mode)) { pipe_++; continue; } if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) { dev++; continue; }
|
||||
char p[MAXPATHLEN] = ""; if (fcntl(fd, F_GETPATH, p) || !p[0]) { if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode)) other++; else kq++; continue; }
|
||||
if (S_ISDIR(st.st_mode)) dir++;
|
||||
if (strstr(p, "romfs") || strstr(p, "RomFS") || strstr(p, "/data/hd/") || strstr(p, ".casc") || strstr(p, "/Data/data/")) romfs++;
|
||||
else if (strstr(p, "/save") || strstr(p, ".d2s") || strstr(p, ".d2i") || strstr(p, "/user/")) save++;
|
||||
if (strstr(p, "romfs") || strstr(p, "RomFS")) romfs++;
|
||||
else if (strstr(p, "/save")) save++;
|
||||
else if (strstr(p, ".log") || strstr(p, "boot.txt") || strstr(p, ".jsonl") || strstr(p, ".tsv")) logf++;
|
||||
else if (strstr(p, "/Caches/") || strstr(p, "/cache/") || strstr(p, ".metallib") || strstr(p, "/tmp/") || strstr(p, "/var/folders/")) cache++;
|
||||
else { other++; if (sk < 300) { const char *b = strrchr(p, '/'); sk += snprintf(sample + sk, sizeof sample - sk, " %s", b ? b + 1 : p); } } }
|
||||
|
||||
+15
-209
@@ -1,5 +1,5 @@
|
||||
// nn::fs: on the real device it goes through the fsp-srv service; the host connects directly to the Mac file system. Mount name → host directory:
|
||||
// rom → $DM_ROMFS (default $DM_ROOT/data/switch/3.3.94012/Update/RomFS, read-only; the game parses the storage blocks in it itself)
|
||||
// rom → $DM_ROMFS (default $DM_ROOT/romfs, read-only; the game parses the storage blocks in it itself)
|
||||
// save → $DM_SAVE (must be set, pointing to this run's copy of the standard save save_ref; if unset, report a failure and abort)
|
||||
// temp → $DM_TEMP (default $DM_ROOT/run/platform/temp)
|
||||
// Paths look like "rom:/data/x". FileHandle / DirectoryHandle are 8 bytes passed by value, here they store the host fd / DIR*.
|
||||
@@ -46,12 +46,11 @@ static uint8_t g_fsave[65536]; // fd → whether it is on a transac
|
||||
static _Atomic int g_save_writes; // number of writes on the save mount (shared by the injection point and the commit log)
|
||||
static _Atomic uint64_t g_txn_first_ns; // time of this transaction's first save write (used by the commit log to report timing; cleared to 0 at commit)
|
||||
// Write failures are refused per transaction: on the real device save space is reserved when the save is created, overwrites cannot fail, and the game does not check the return values of WriteFile / SetFileSize either, it Commits as usual after a failure
|
||||
// (after injecting a failure on the 1st write, the committed joe.d2s is 0 bytes = character lost). When a host write fails (disk full etc., really possible on iOS):
|
||||
// (after injecting a failure on the 1st write, the committed save file was 0 bytes). When a host write fails (disk full etc., really possible on iOS):
|
||||
// if any save write / set-size failed between the last Commit and this Commit, this Commit is refused -- the committed version is unchanged, the working directory is discarded and re-cloned from the committed version,
|
||||
// and the front end is notified (front_save_failed); the next transaction (the game's next full save rewrite) is attempted as usual and committed if it succeeds. g_save_poison = errno of the first failure in this transaction (0 = none).
|
||||
static _Atomic int g_save_poison;
|
||||
static void save_poison(int e) { int z = 0; atomic_compare_exchange_strong(&g_save_poison, &z, e ? e : EIO); }
|
||||
void dm_fs_save_poison(int e) { save_poison(e); } // for the pre-commit hook: a side file (Chronicle / pouch) that failed to write also counts as a write failure of this transaction -- if the commit went ahead as usual, the items would no longer be in the character save and would not be in the side file either
|
||||
// The default weak implementation only logs (Mac); the iOS front end provides a strong implementation of the same name that shows a native alert. Called on the game thread, the front end switches to the main thread itself
|
||||
__attribute__((weak)) void front_save_failed(int err) { LOG("front_save_failed(errno=%d): no front end attached (default weak implementation)", err); }
|
||||
// DM_FSLOG=<file>: each ReadFile logs one TSV line "seconds since boot\tguest path\toffset\tlength\tguest buffer address" (which bytes boot read, for offline reverse lookup against the manifest)
|
||||
@@ -64,7 +63,7 @@ static const char *envdir(const char *var, const char *rel, char *buf, size_t n)
|
||||
snprintf(buf, n, "%s%s/%s", getenv("DM_ROOT") ?: getenv("HOME"), getenv("DM_ROOT") ? "" : "/dm-work", rel); return buf;
|
||||
}
|
||||
|
||||
// Steps that end in a failed write (from d2r-mobile b94670c79): test injection DM_TEST_FS_FAILAT=<point>[:<count>][,...] -- in a save transaction, mountclone (clone the work directory at mount),
|
||||
// Steps that end in a failed write: test injection DM_TEST_FS_FAILAT=<point>[:<count>][,...] -- in a save transaction, mountclone (clone the work directory at mount),
|
||||
// reclone (reclone after a refused commit), guestname (write the guest name after a rename), renameback (rename rollback) fail with ENOSPC for the first <count> times (default 1),
|
||||
// to check "no exit, committed version unchanged, next commit works" (tests/t_fs.c 6). Without the variable it costs one getenv
|
||||
static int dm_fs_test_fail(const char *pt) {
|
||||
@@ -116,7 +115,7 @@ static Result mount(const char *name, const char *dir, int ro) { return mount_ex
|
||||
// APFS is case-insensitive by default, Horizon paths are case-sensitive: every component is looked up exactly by guest name.
|
||||
// Ordinary host names are unchanged; only when a name collides on the host is a separate physical name chosen, with the guest name stored in an xattr.
|
||||
// clonefile preserves xattr, so save Commit/remount needs no separate mapping table; enumeration must not leak physical aliases.
|
||||
#define FS_GUEST_NAME "org.d2rmobile.guest-name"
|
||||
#define FS_GUEST_NAME "org.d2rmobile.guest-name" // the name this host has always used: existing saves carry it, so it is kept
|
||||
static int guest_name(const char *parent, const char *physical, char *out, size_t n) {
|
||||
// Only the physical names chosen on a case collision (.dm-case-N) carry the xattr (set_guest_name writes it only when the physical name differs from the guest name); any other physical name is the guest name,
|
||||
// so no per-entry getxattr is needed -- that was one system call per entry when listing, seconds for the 216,000 entries Engage walks in RomFS at boot
|
||||
@@ -214,20 +213,6 @@ static int root_path(const char *root, const char *rest, char *out, size_t n) {
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
// Files in the overlay that the host itself reads (marker files, sidecar tables, data archives) are all looked up here. DM_ROMFS_OVERLAY may have
|
||||
// several layers, colon-separated, earlier ones taking priority; old devices have two layers,
|
||||
// "in-bundle romfs_overlay_astc:Legacy Device Pack directory"; treating the whole string as one directory when building file names finds nothing at all
|
||||
// on old devices.
|
||||
int dm_overlay_file(const char *rel, char *out, size_t n) {
|
||||
const char *ov = getenv("DM_ROMFS_OVERLAY");
|
||||
for (const char *p = ov ? ov : ""; *p;) {
|
||||
size_t l = strcspn(p, ":");
|
||||
if (l && snprintf(out, n, "%.*s/%s", (int)l, p, rel) < (int)n && !access(out, R_OK)) return 1;
|
||||
p += l; if (*p == ':') p++;
|
||||
}
|
||||
if (n) out[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
static int rom_overlay_path(const mount_t *m, const char *rest, char *out, size_t n) {
|
||||
char *roots = strdup(m->overlay);
|
||||
if (!roots) die("rom overlay path allocation failed");
|
||||
@@ -321,7 +306,7 @@ static Result err_res(const char *op, const char *path) {
|
||||
HLE_FORCE("_ZN2nn2fs22QueryMountRomCacheSizeEPm", "fsp-srv; rom reads the host directory directly, the cache gets only the minimum size", Result, h_fs_QueryRomCache, size_t *out) { *out = 0x1000; return RES_OK; }
|
||||
HLE_FORCE("_ZN2nn2fs8MountRomEPKcPvm", "fsp-srv; rom → host RomFS directory", Result, h_fs_MountRom, const char *name, void *cache, size_t sz) {
|
||||
(void)cache; (void)sz; char b[1024];
|
||||
Result r = mount(name, envdir("DM_ROMFS", "data/switch/3.3.94012/Update/RomFS", b, sizeof b), 1);
|
||||
Result r = mount(name, envdir("DM_ROMFS", "romfs", b, sizeof b), 1);
|
||||
if (r == RES_OK && getenv("DM_ROMFS_OVERLAY")) {
|
||||
PATH_LOCK();
|
||||
for (int i = 0; i < g_nmnt; i++) if (!strcmp(g_mnt[i].name, name)) {
|
||||
@@ -410,100 +395,6 @@ HLE_FORCE("_ZN2nn2fs14ExtendSaveDataERKNS_7account3UidEll", "fsp-srv; extend sav
|
||||
(void)uid; LOG("fs extend save data to %lld bytes (journal %lld): nothing to do", (long long)size, (long long)journal); return RES_OK;
|
||||
}
|
||||
HLE_FORCE("_ZN2nn2fs14EnsureSaveDataERKNS_7account3UidE", "fsp-srv; the save directory is created at mount time", 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):
|
||||
// "Item Name Display": 1 -- options page "Show Items" = toggle style (game default 0 = hold; touch has no key that can be held). Companion: the front end taps the "Show Items" key once for the player the first time this process reaches the normal in-run state (front_ios.m),
|
||||
// in toggle style one tap keeps it shown, and it persists into the next run after exit and re-entry; a player who wants it off changes this item back to "Hold".
|
||||
// "Item Tooltip Hotkey Appender": 0 -- item tooltips carry no controller key hints (game default 1).
|
||||
// Minimap (placed top right: mini, semi-transparent):
|
||||
// "AutoMapMode": 2 = mini map on the right (0 full screen / 1 left / 2 right, the game default is also 2, stated here explicitly); opacity (AutoMapFade) is not filled in, the game default 0 = no fade is used -- with "All" (3) the minimap lines fade until almost only the text remains; the map is lines overlaid on the picture anyway, so it shows through without fading;
|
||||
// "AutoMap Open On Load": 1 = open automatically on entering a run (game default 0). Tapping the minimap = the map toggle key, see front_ios.m.
|
||||
// Done by default in the iOS app; not done by default on the Mac host (gate checks compare images against Switch ground truth), DM_TOUCH_DEFAULTS=1 on / =0 off.
|
||||
// Shared stash seed: when the save directory has no ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i, write the seed file (stash_seed.h); an existing one is not changed by a single byte.
|
||||
// Done before mounting the save and cloning the working directory, so it is what the game reads the first time. On by default on iOS, DM_STASH_SEED=0 turns it off; off by default on the Mac host, DM_STASH_SEED=1 turns it on.
|
||||
// Only covers the DLC type (standard + hardcore): SharedStash{Soft,Hard}CoreV2.d2i used by the Lord of Destruction type is not seeded -- the touch character creation page cannot create that kind of character
|
||||
// The seed file stash_seed.h holds the bytes of a save the game itself generated, and the public repository does not ship it: without the header nothing is seeded (the game then creates an empty shared stash of its own the first time one is needed, as the original does)
|
||||
#if __has_include("stash_seed.h")
|
||||
#include "stash_seed.h"
|
||||
static void stash_seed_one(const char *save_dir, const char *name) {
|
||||
char dir[1100], path[1200], tmp[1210]; snprintf(dir, sizeof dir, "%s/%s", save_dir, name); snprintf(path, sizeof path, "%s/%s.d2i", dir, name);
|
||||
struct stat st; if (!stat(path, &st)) return;
|
||||
if (mkdir(dir, 0755) && errno != EEXIST) { LOG("shared stash seed: creating directory %s failed (%s), not seeding", dir, strerror(errno)); return; }
|
||||
snprintf(tmp, sizeof tmp, "%s.tmp", path); FILE *f = fopen(tmp, "wb"); if (!f) { LOG("shared stash seed: writing %s failed (%s), not seeding", tmp, strerror(errno)); return; }
|
||||
int ok = fwrite(g_stash_seed, 1, sizeof g_stash_seed, f) == sizeof g_stash_seed; ok = fclose(f) == 0 && ok;
|
||||
if (ok && !rename(tmp, path)) LOG("shared stash seed: %s did not exist, wrote the seed file (%zu bytes: 2.5 million gold + 2 Tokens of Absolution)", path, sizeof g_stash_seed); else { remove(tmp); LOG("shared stash seed: write failed, not seeding"); }
|
||||
}
|
||||
// One copy each for standard and hardcore, each judged on its own (the game picks the file name by character type, main+0x2ce1e8: DLC type standard = ModernSharedStashSoftCoreV2, hardcore = ModernSharedStashHardCoreV2; same file format)
|
||||
static void stash_seed(const char *save_dir) {
|
||||
const char *e = getenv("DM_STASH_SEED"); int on = e ? strcmp(e, "0") != 0 : TARGET_OS_IPHONE; if (!on) return; // on by default on iOS; off by default on the Mac host (gate checks such as lockstep comparison / save transaction compare the save directory byte by byte, one extra file turns them red), set DM_STASH_SEED=1 to verify
|
||||
stash_seed_one(save_dir, "ModernSharedStashSoftCoreV2"); stash_seed_one(save_dir, "ModernSharedStashHardCoreV2");
|
||||
}
|
||||
#else
|
||||
static void stash_seed(const char *save_dir) { (void)save_dir; }
|
||||
#endif
|
||||
// Fallback for "Large Font" (settings key "Low Vision Mode"): if it is 1 in the settings file, change it back to 0. It switches the UI to the original game's low-vision layout profile, which our touch UI was never built for; it hangs after being turned on in the settings page.
|
||||
// This item in the settings page is separately hidden by the UI overlay; this here guarantees that devices where it was already turned on recover on restart. Same switch as settings_seed (on by default on iOS, on the Mac host only with DM_TOUCH_DEFAULTS=1)
|
||||
static void low_vision_off(const char *save_dir) {
|
||||
const char *e = getenv("DM_TOUCH_DEFAULTS"); int on = e ? strcmp(e, "0") != 0 : TARGET_OS_IPHONE; if (!on) return;
|
||||
char path[1200]; snprintf(path, sizeof path, "%s/Settings/Settings.json", save_dir); static char buf[65536]; FILE *f = fopen(path, "rb"); if (!f) return;
|
||||
size_t n = fread(buf, 1, sizeof buf - 1, f); fclose(f); if (n == sizeof buf - 1) return; buf[n] = 0;
|
||||
char *k = strstr(buf, "\"Low Vision Mode\""); if (!k) return; char *v = strchr(k + 17, ':'); if (!v) return; v++; while (*v == ' ' || *v == '\t') v++;
|
||||
if (*v != '1' || (v[1] >= '0' && v[1] <= '9')) return; *v = '0';
|
||||
char tmp[1210]; snprintf(tmp, sizeof tmp, "%s.tmp", path); f = fopen(tmp, "wb"); if (!f) return; int ok = fwrite(buf, 1, n, f) == n; ok = fclose(f) == 0 && ok;
|
||||
if (ok && !rename(tmp, path)) LOG("settings fallback: 'Large Font' (Low Vision Mode) was on, turned it back off"); else remove(tmp);
|
||||
}
|
||||
static void settings_seed(const char *save_dir) {
|
||||
const char *e = getenv("DM_TOUCH_DEFAULTS"); int on = e ? strcmp(e, "0") != 0 : TARGET_OS_IPHONE; if (!on) return;
|
||||
static const char *kv[][2] = {{"\"Item Name Display\"", "1"}, {"\"Item Tooltip Hotkey Appender\"", "0"},
|
||||
{"\"AutoMapMode\"", "2"}, {"\"AutoMap Open On Load\"", "1"}};
|
||||
char dir[1100], path[1200]; snprintf(dir, sizeof dir, "%s/Settings", save_dir); snprintf(path, sizeof path, "%s/Settings.json", dir);
|
||||
static char buf[65536], out[66000]; size_t n = 0; FILE *f = fopen(path, "rb"); // static: the game thread that mounts the save has a small stack (two 64 KB blocks on the stack overflow it outright, measured crash); mounting the save is done on one thread only
|
||||
int first = f == NULL; // no settings file = first install
|
||||
if (f) { n = fread(buf, 1, sizeof buf - 1, f); fclose(f); if (n == sizeof buf - 1) { LOG("settings defaults: %s is over 64 KB, left alone", path); return; } }
|
||||
buf[n] = 0;
|
||||
char *brace = strchr(buf, '{'); int had = brace != NULL; size_t o = 0; int added = 0;
|
||||
o += (size_t)snprintf(out + o, sizeof out - o, "{");
|
||||
for (size_t i = 0; i < sizeof kv / sizeof kv[0]; i++) if (!strstr(buf, kv[i][0])) { o += (size_t)snprintf(out + o, sizeof out - o, "\n %s: %s,", kv[i][0], kv[i][1]); added++; }
|
||||
// On first install the original game's text and voice languages are set from the system language (dm_sys_lang(), see dm_i18n.c; only when there is no settings file). An existing settings file does not get these two keys added (existing players stay as they are); afterwards they are changed by the language switch on the Custom page (settings_lang_apply)
|
||||
if (first) { int dm_sys_lang(void); o += (size_t)snprintf(out + o, sizeof out - o, "\n \"Language\": %d,\n \"VoiceLanguage\": %d,", dm_sys_lang(), dm_sys_lang()); added += 2; }
|
||||
if (!added) return;
|
||||
if (had) { const char *rest = brace + 1; const char *q = rest; while (*q == ' ' || *q == '\n' || *q == '\r' || *q == '\t') q++;
|
||||
if (*q == '}') { out[o - 1] = 0; o--; } // the original file is an empty object: remove the last comma that was added
|
||||
o += (size_t)snprintf(out + o, sizeof out - o, "%s", rest); }
|
||||
else { out[o - 1] = 0; o--; o += (size_t)snprintf(out + o, sizeof out - o, "\n}\n"); }
|
||||
mkdir(dir, 0755); char tmp[1300]; snprintf(tmp, sizeof tmp, "%s.seed", path);
|
||||
f = fopen(tmp, "wb"); if (!f || fwrite(out, 1, o, f) != o) { if (f) fclose(f); LOG("[RED] settings defaults: writing %s failed", tmp); return; }
|
||||
fclose(f); if (rename(tmp, path)) { LOG("[RED] settings defaults: renaming to %s failed", path); return; }
|
||||
LOG("settings defaults: %s got %d keys added (Show Items = toggle / tooltip hotkeys = off / minimap on the right, opens on entering a run%s; existing keys untouched)", path, added, first ? " / first install: Language written from the system language" : "");
|
||||
}
|
||||
// Language switch (the cell in the title row of the Custom page, lang_en in dm_custom.c): once the player has tapped "Apply" the settings file holds an explicit value, and on every save mount (before the game reads its settings) the original game's settings file has its
|
||||
// Language (text) and VoiceLanguage (voice; the voice switches along with it) changed to 0 English / 8 Simplified Chinese accordingly. It is changed only when it "does not match the switch": switch = English but text is not 0; switch = Chinese but text is 0.
|
||||
// So with the switch off, if the player has chosen another language such as Traditional Chinese in the original game, it is not changed back to Simplified Chinese. Whichever of the two keys is missing is added. Apply never tapped (-1) = nothing is touched
|
||||
static int json_int_set(char *buf, size_t cap, const char *key, int val) { // Replace the N of "key": N with val (if the key is absent, insert it after the first {); returns 1 = changed
|
||||
char *k = strstr(buf, key); char num[16]; snprintf(num, sizeof num, "%d", val);
|
||||
if (!k) { char *b = strchr(buf, '{'); char ins[96]; int m = snprintf(ins, sizeof ins, "\n %s: %s,", key, num); if (!b || strlen(buf) + (size_t)m + 1 >= cap) return 0;
|
||||
memmove(b + 1 + m, b + 1, strlen(b + 1) + 1); memcpy(b + 1, ins, (size_t)m); return 1; }
|
||||
char *v = strchr(k + strlen(key), ':'); if (!v) return 0; v++; while (*v == ' ' || *v == '\t') v++; char *e = v; if (*e == '-') e++; while (*e >= '0' && *e <= '9') e++;
|
||||
if (e == v || atoi(v) == val) return 0; size_t nl = strlen(num); if (strlen(buf) + nl + 1 >= cap) return 0;
|
||||
memmove(v + nl, e, strlen(e) + 1); memcpy(v, num, nl); return 1; }
|
||||
static void settings_lang_apply(const char *save_dir) {
|
||||
int dm_custom_lang_explicit(void); int en = dm_custom_lang_explicit(); if (en < 0) return;
|
||||
char path[1200]; snprintf(path, sizeof path, "%s/Settings/Settings.json", save_dir); static char buf[66000]; FILE *f = fopen(path, "rb"); if (!f) return;
|
||||
size_t n = fread(buf, 1, 65535, f); fclose(f); if (n == 65535) return; buf[n] = 0;
|
||||
const char *k = strstr(buf, "\"Language\""), *c = k ? strchr(k + 10, ':') : NULL; int cur = c ? atoi(c + 1) : 0; // Not written = game default (the host reports en-US)
|
||||
if ((en && cur == 0) || (!en && cur != 0)) { const char *vk = strstr(buf, "\"VoiceLanguage\""), *vc = vk ? strchr(vk + 15, ':') : NULL; int vcur = vc ? atoi(vc + 1) : cur;
|
||||
if ((en && vcur == 0) || (!en && vcur != 0)) return; } // Text and voice already match the switch
|
||||
int want = en ? 0 : 8, ch = json_int_set(buf, sizeof buf, "\"Language\"", want) | json_int_set(buf, sizeof buf, "\"VoiceLanguage\"", want); if (!ch) return;
|
||||
char tmp[1300]; snprintf(tmp, sizeof tmp, "%s.lang", path); f = fopen(tmp, "wb"); size_t m = strlen(buf);
|
||||
if (!f || fwrite(buf, 1, m, f) != m) { if (f) fclose(f); LOG("[RED] language switch: writing %s failed", tmp); return; }
|
||||
fclose(f); if (rename(tmp, path)) { LOG("[RED] language switch: renaming to %s failed", path); return; }
|
||||
LOG("language switch: set the settings file's Language / VoiceLanguage to %d (%s; previous text language %d)", want, en ? "English" : "Simplified Chinese", cur);
|
||||
}
|
||||
// The current working directory of the transactional save (the copy the game reads and writes; the host's own side files must be written here to enter the committed version with Commit). Returns NULL when no save is mounted
|
||||
const char *dm_save_workdir(void) { for (int i = 0; i < g_nmnt; i++) if (g_mnt[i].commit[0]) return g_mnt[i].dir; return NULL; }
|
||||
// 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; only this game's own save data exists", int, h_fs_SaveExisting, uint64_t app, const void *uid) {
|
||||
(void)uid; LOG("fs asks whether save data exists: program %016llx → %s", (unsigned long long)app, app == DM_GAME_PROGRAM_ID ? "this game, yes" : "another program, no"); return app == DM_GAME_PROGRAM_ID;
|
||||
@@ -517,25 +408,15 @@ HLE_FORCE("_ZN2nn2fs13MountSaveDataEPKcRKNS_7account3UidE", "fsp-srv; save data
|
||||
const char *d = getenv("DM_SAVE");
|
||||
if (!d) die("DM_SAVE is not set: point it at a writable copy of a save directory");
|
||||
char dd[1024]; strlcpy(dd, d, sizeof dd); size_t L = strlen(dd); while (L > 1 && dd[L - 1] == '/') dd[--L] = 0; // strip trailing slashes (.work / .new are appended at the same level)
|
||||
// The items below all edit D2R's save (Settings/Settings.json, shared stash): only on the D2R build, other games' files of the same name are left alone
|
||||
int d2r = !strcmp(dm_main_build(), DM_D2R_BUILD);
|
||||
if (d2r) { stash_seed(dd); low_vision_off(dd); settings_seed(dd); settings_lang_apply(dd); }
|
||||
// Rule files for the loot filter (the game loads them at startup; dm_lootfilter.c registers this hook from a constructor; it is null in unit tests that link only hle_fs.c). Same switch as settings_seed:
|
||||
// written by default on iOS; not written under the Mac host's parity gate (saves must match the device ground truth file by file)
|
||||
{ const char *e = getenv("DM_TOUCH_DEFAULTS"); if (d2r && g_dm_mount_seed_hook && (e ? strcmp(e, "0") != 0 : TARGET_OS_IPHONE)) g_dm_mount_seed_hook(dd); }
|
||||
return mount_ex(name, dd, 0, 1);
|
||||
}
|
||||
void (*g_dm_pre_commit_hook[4])(const char *workdir); // called before commit (working directory): host side files are checked against the save contents in the working directory before being written ([0] personal stash pages dm_pages.c, [2] pouch dm_pouch.c, [1] chronicle vault dm_vault.c, [3] rift progress dm_rift.c)
|
||||
void (*g_dm_save_commit_hook)(const char *commit_dir); // called after a save commit succeeds (committed directory); the iOS iCloud sync sets it, NULL on the host
|
||||
static _Atomic uint64_t g_save_commits; // number of successful commits (used by the front end when going to background to wait for "the game's current save has been committed")
|
||||
uint64_t dm_fs_commit_count(void) { return atomic_load(&g_save_commits); }
|
||||
HLE_FORCE("_ZN2nn2fs6CommitEPKc", "fsp-srv; transactional save: the whole work directory tree is cloned, then swapped atomically with the committed directory (renamex_np RENAME_SWAP)", Result, h_fs_Commit, const char *name) {
|
||||
PATH_LOCK(); pthread_mutex_lock(&g_lock);
|
||||
int i = 0; while (i < g_nmnt && strcmp(g_mnt[i].name, name)) i++;
|
||||
if (i == g_nmnt || !g_mnt[i].commit[0]) { pthread_mutex_unlock(&g_lock); return RES_OK; } // non-transactional mount: nothing to commit
|
||||
uint64_t tc0 = clock_gettime_nsec_np(CLOCK_MONOTONIC), tw0 = atomic_exchange(&g_txn_first_ns, 0);
|
||||
for (int k = 0; k < 4; k++) if (g_dm_pre_commit_hook[k]) g_dm_pre_commit_hook[k](g_mnt[i].dir);
|
||||
uint64_t tc1 = clock_gettime_nsec_np(CLOCK_MONOTONIC); // side files written into the working directory are swapped in atomically together with this commit
|
||||
uint64_t tc1 = clock_gettime_nsec_np(CLOCK_MONOTONIC), tw0 = atomic_exchange(&g_txn_first_ns, 0);
|
||||
int perr = atomic_exchange(&g_save_poison, 0);
|
||||
if (perr) { // a write failed in this transaction: refuse the commit, discard the working directory
|
||||
char torn[512] = ""; size_t tl = 0; DIR *d0 = opendir(g_mnt[i].dir); struct dirent *e0; // record files truncated to 0 bytes (the damage that would enter the save if committed as usual)
|
||||
@@ -568,11 +449,11 @@ HLE_FORCE("_ZN2nn2fs6CommitEPKc", "fsp-srv; transactional save: the whole work d
|
||||
if (clonefile(g_mnt[i].dir, neu, 0)) { LOG("[RED] save commit: clonefile %s failed errno=%d (committed version unchanged)", neu, errno); r = RES_FAIL; }
|
||||
else if (renamex_np(neu, g_mnt[i].commit, RENAME_SWAP)) { LOG("[RED] save commit: atomic swap failed errno=%d (committed version unchanged)", errno); rm_tree(neu); r = RES_FAIL; }
|
||||
else rm_tree(neu); // the old version that was swapped out
|
||||
// Timing readouts (integer microseconds, no floating point): how long the pre-commit hook and the clone + atomic swap each took; how long in total from this transaction's first save write to the end of the commit (the game writing files + the commit; 0 = no save write in this transaction)
|
||||
// Timing readouts (integer microseconds, no floating point): how long the clone + atomic swap took; how long in total from this transaction's first save write to the end of the commit (the game writing files + the commit; 0 = no save write in this transaction)
|
||||
static int n; if (r == RES_OK) { uint64_t tc2 = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
LOG("save commit #%d: %s swapped atomically into %s (%d save writes so far) (hook %llu us, clone + swap %llu us, since this transaction's first write %llu us)", ++n, g_mnt[i].dir, g_mnt[i].commit, atomic_load(&g_save_writes),
|
||||
(unsigned long long)((tc1 - tc0) / 1000), (unsigned long long)((tc2 - tc1) / 1000), (unsigned long long)(tw0 ? (tc2 - tw0) / 1000 : 0)); atomic_fetch_add(&g_save_commits, 1);
|
||||
if (g_dm_save_commit_hook) g_dm_save_commit_hook(g_mnt[i].commit); } // iCloud sync (cloud_ios.m): called with the lock held, that side only takes a clonefile snapshot
|
||||
LOG("save commit #%d: %s swapped atomically into %s (%d save writes so far) (clone + swap %llu us, since this transaction's first write %llu us)", ++n, g_mnt[i].dir, g_mnt[i].commit, atomic_load(&g_save_writes),
|
||||
(unsigned long long)((tc2 - tc1) / 1000), (unsigned long long)(tw0 ? (tc2 - tw0) / 1000 : 0)); atomic_fetch_add(&g_save_commits, 1);
|
||||
}
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return r;
|
||||
}
|
||||
@@ -581,75 +462,6 @@ HLE_FORCE("_ZN2nn2fs14CommitSaveDataEPKc", "fsp-srv; same as Commit (SDK 11's ol
|
||||
HLE_FORCE("_ZN2nn2fs7UnmountEPKc", "fsp-srv; the mount table is permanent, unmounting is a no-op", void, h_fs_Unmount, const char *name) { (void)name; }
|
||||
|
||||
// ---------- Files ----------
|
||||
// Zero-stripped archives: large zero-filled ranges in the resource pack (cutscene videos, old-version music) are not distributed with the package -- these zero-filled ranges are stripped from the archive file, and an extent table is placed beside it,
|
||||
// "<archive name>.holes" (text: first line `size <original byte count>`, then one line `<zero-filled range start> <zero-filled range length>` each, ascending by start, non-overlapping).
|
||||
// When reading such archives the host translates offsets through the table: bytes falling in a zero-filled range return 0 and the file size is reported as the original value; the game and the startup prefetch still see the original layout, and not a single word of the index needs changing.
|
||||
// There are only two read entry points: ReadFile in this file and the startup prefetch in hle_tact.c, both going through dm_data_pread. Files without .holes take the original path with zero extra overhead.
|
||||
typedef struct { int64_t size; int n; int64_t *log, *len, *phys; char name[40]; } holemap; // Kept segment: original start / length / start in the zero-stripped file
|
||||
static _Atomic uint64_t g_hole_reads; // Number of reads that hit a zero-filled range: should normally be 0 (zero-filled ranges are stripped cutscene videos / old-version music the game should not come to read); non-zero = stripped wrongly
|
||||
static holemap *g_hm[4096];
|
||||
static holemap *hm_load(const char *hostpath) {
|
||||
char hp2[1100]; if (snprintf(hp2, sizeof hp2, "%s.holes", hostpath) >= (int)sizeof hp2) return NULL;
|
||||
FILE *f = fopen(hp2, "r"); if (!f) return NULL;
|
||||
long long size = -1, o, l; if (fscanf(f, "size %lld", &size) != 1 || size < 0) die("bad hole table format: %s", hp2);
|
||||
holemap *m = calloc(1, sizeof *m); int cap = 64; m->size = size; m->log = malloc(cap * sizeof *m->log); m->len = malloc(cap * sizeof *m->len); m->phys = malloc(cap * sizeof *m->phys);
|
||||
int64_t pos = 0, phys = 0;
|
||||
while (fscanf(f, "%lld %lld", &o, &l) == 2) {
|
||||
if (o < pos || l <= 0 || o + l > size) die("hole table zero range out of bounds or out of order: %s (%lld %lld)", hp2, o, l);
|
||||
if (o > pos) { if (m->n == cap) { cap *= 2; m->log = realloc(m->log, cap * sizeof *m->log); m->len = realloc(m->len, cap * sizeof *m->len); m->phys = realloc(m->phys, cap * sizeof *m->phys); }
|
||||
m->log[m->n] = pos; m->len[m->n] = o - pos; m->phys[m->n++] = phys; phys += o - pos; }
|
||||
pos = o + l;
|
||||
}
|
||||
fclose(f);
|
||||
if (pos < size) { if (m->n == cap) { cap++; m->log = realloc(m->log, cap * sizeof *m->log); m->len = realloc(m->len, cap * sizeof *m->len); m->phys = realloc(m->phys, cap * sizeof *m->phys); }
|
||||
m->log[m->n] = pos; m->len[m->n] = size - pos; m->phys[m->n++] = phys; phys += size - pos; }
|
||||
struct stat st; if (stat(hostpath, &st) || st.st_size != phys) die("zero-stripped archive length does not match its hole table: %s is %lld bytes, the table says %lld", hostpath, (long long)st.st_size, (long long)phys);
|
||||
const char *bn = strrchr(hostpath, '/'); strlcpy(m->name, bn ? bn + 1 : hostpath, sizeof m->name);
|
||||
{ static _Atomic int said[64]; unsigned h = 0; for (const char *c = m->name; *c; c++) h = h * 31 + (unsigned char)*c; // Print a line for each archive only on first registration
|
||||
if (!atomic_exchange(&said[h % 64], 1)) LOG("zero-stripped archive %s: original %lld bytes, stored %lld bytes, %d kept segments", m->name, (long long)size, (long long)phys, m->n); }
|
||||
return m;
|
||||
}
|
||||
void dm_data_note_fd(int fd, const char *hostpath) { // Called after opening a resource archive: register it if there is an extent table
|
||||
if (fd < 0 || fd >= 4096) return;
|
||||
g_hm[fd] = hm_load(hostpath);
|
||||
}
|
||||
// Same as dm_data_logical_size, but opened relative to the directory fd: listing does not resolve the long path from the root again for every entry
|
||||
static int64_t data_logical_size_at(int dfd, const char *name, int64_t phys_size) {
|
||||
char hn[0x320]; if (snprintf(hn, sizeof hn, "%s.holes", name) >= (int)sizeof hn) return phys_size;
|
||||
int fd = openat(dfd, hn, O_RDONLY); if (fd < 0) return phys_size;
|
||||
FILE *f = fdopen(fd, "r"); if (!f) { close(fd); return phys_size; }
|
||||
long long size = -1; int ok = fscanf(f, "size %lld", &size) == 1; fclose(f); return ok && size >= 0 ? size : phys_size;
|
||||
}
|
||||
int64_t dm_data_logical_size(const char *hostpath, int64_t phys_size) { // For directory listing / size queries: report the original size when there is an extent table
|
||||
char hp2[1100]; if (snprintf(hp2, sizeof hp2, "%s.holes", hostpath) >= (int)sizeof hp2) return phys_size;
|
||||
FILE *f = fopen(hp2, "r"); if (!f) return phys_size; long long size = -1; int ok = fscanf(f, "size %lld", &size) == 1; fclose(f); return ok && size >= 0 ? size : phys_size;
|
||||
}
|
||||
ssize_t dm_data_pread(int fd, void *buf, size_t n, off_t off) { // Same semantics as pread (may short-read); zero-stripped archives are translated through the extent table
|
||||
holemap *m = fd >= 0 && fd < 4096 ? g_hm[fd] : NULL;
|
||||
if (!m) return pread(fd, buf, n, off);
|
||||
if (off >= m->size) return 0;
|
||||
if ((int64_t)n > m->size - off) n = (size_t)(m->size - off);
|
||||
int lo = 0, hi = m->n; // The first kept segment whose end point > off
|
||||
while (lo < hi) { int mid = (lo + hi) / 2; if (m->log[mid] + m->len[mid] <= off) lo = mid + 1; else hi = mid; }
|
||||
size_t done = 0;
|
||||
while (done < n) {
|
||||
int64_t cur = off + (int64_t)done;
|
||||
if (lo < m->n && cur >= m->log[lo]) { // In a kept segment: read directly into the destination buffer
|
||||
int64_t in = cur - m->log[lo]; size_t want = n - done; if ((int64_t)want > m->len[lo] - in) want = (size_t)(m->len[lo] - in);
|
||||
size_t got = 0; while (got < want) { ssize_t r = pread(fd, (uint8_t *)buf + done + got, want - got, m->phys[lo] + in + (int64_t)got); if (r < 0) { if (errno == EINTR) continue; return -1; } if (r == 0) return (ssize_t)(done + got); got += (size_t)r; }
|
||||
done += want; if (in + (int64_t)want == m->len[lo]) lo++;
|
||||
} else { // In a zero-filled range: fill 0 up to the start of the next kept segment
|
||||
int64_t end = lo < m->n ? m->log[lo] : m->size; size_t z = n - done; if ((int64_t)z > end - cur) z = (size_t)(end - cur);
|
||||
memset((uint8_t *)buf + done, 0, z); done += z;
|
||||
// The game reads whole blocks aligned to 256 KB: when what it wants is a kept file next to a zero-filled range, the whole block incidentally overlaps the edge of the zero-filled range (harmless, the kept file's bytes are returned as usual).
|
||||
// Only a read that lies entirely inside a zero-filled range is really reading stripped content → report a failure (the first 20 one by one, then one line per 1000); boundary-crossing reads are only noted the first 3 times
|
||||
if (z == n) { uint64_t hr = atomic_fetch_add(&g_hole_reads, 1);
|
||||
if (hr < 20 || hr % 1000 == 0) LOG("[RED] read inside a stripped zero range %s offset %lld length %zu (#%llu; the whole block lies in the stripped cutscene videos / old-version music: the stripping was wrong or there is a new reader)", m->name, (long long)off, n, (unsigned long long)hr + 1);
|
||||
} else { static _Atomic uint64_t edge; if (atomic_fetch_add(&edge, 1) < 3) LOG("block read overlaps the edge of a stripped zero range (harmless): %s read %lld length %zu, %zu bytes of it in the zero range", m->name, (long long)off, n, z); }
|
||||
}
|
||||
}
|
||||
return (ssize_t)done;
|
||||
}
|
||||
HLE_FORCE("_ZN2nn2fs8OpenFileEPNS0_10FileHandleEPKci", "fsp-srv → open(2)", Result, h_fs_OpenFile, uint64_t *out, const char *path, int mode) {
|
||||
HP(path);
|
||||
if (mode & (MODE_WRITE | MODE_APPEND)) RW();
|
||||
@@ -659,7 +471,6 @@ HLE_FORCE("_ZN2nn2fs8OpenFileEPNS0_10FileHandleEPKci", "fsp-srv → open(2)", Re
|
||||
if (S_ISDIR(st.st_mode)) { close(fd); return RES_PATH_NOT_FOUND; }
|
||||
if (fd >= (int)sizeof g_fmode) die("fd beyond the table");
|
||||
g_fmode[fd] = (uint8_t)mode; *out = (uint64_t)fd;
|
||||
g_hm[fd] = NULL; if (!(mode & (MODE_WRITE | MODE_APPEND))) dm_data_note_fd(fd, hp);
|
||||
{ const char *c = strchr(path, ':'); size_t ln = c ? (size_t)(c - path) : 0; g_fsave[fd] = 0;
|
||||
for (int i = 0; c && i < g_nmnt; i++) if (strlen(g_mnt[i].name) == ln && !strncmp(g_mnt[i].name, path, ln)) g_fsave[fd] = g_mnt[i].commit[0] != 0; }
|
||||
fslog_init();
|
||||
@@ -669,16 +480,15 @@ HLE_FORCE("_ZN2nn2fs8OpenFileEPNS0_10FileHandleEPKci", "fsp-srv → open(2)", Re
|
||||
}
|
||||
HLE_FORCE("_ZN2nn2fs8ReadFileEPmNS0_10FileHandleElPvm", "fsp-srv → pread(2)", Result, h_fs_ReadFile, size_t *out, uint64_t h, int64_t off, void *buf, size_t sz) {
|
||||
if (!h) { *out = 0; return RES_FAIL; } // a file handle is an fd; 0 is stdin and never a file the game opened: the null handle the game keeps using after a failed open
|
||||
size_t got = 0; uint64_t t0 = g_flag.bootprof ? now_ns() : 0;
|
||||
size_t got = 0;
|
||||
dm_guest_write_prepare(buf, sz); // The destination may be a texture pool page write-protected by the graphics layer: a kernel write to a read-only page returns EFAULT instead of page-faulting, so unprotect and mark dirty first
|
||||
while (got < sz) {
|
||||
ssize_t r = dm_data_pread((int)h, (uint8_t *)buf + got, sz - got, off + got);
|
||||
ssize_t r = pread((int)h, (uint8_t *)buf + got, sz - got, off + got);
|
||||
if (r < 0) { if (errno == EINTR) continue; return err_res("pread", "fd"); }
|
||||
if (r == 0) break;
|
||||
got += r;
|
||||
}
|
||||
*out = got;
|
||||
if (g_flag.bootprof) dm_prof_add(PROF_FS_READ, now_ns() - t0, got);
|
||||
if (g_fslog) { pthread_mutex_lock(&g_fslog_lock);
|
||||
static int hash_on = -1; if (hash_on < 0) hash_on = getenv("DM_FSLOG_HASH") != NULL; // DM_FSLOG_HASH=1: append a column with the FNV-1a 64 of the content read (to compare whether the bytes read from two resource packs match)
|
||||
uint64_t fh = 0xcbf29ce484222325ull; if (hash_on) for (size_t i = 0; i < got; i++) fh = (fh ^ ((uint8_t *)buf)[i]) * 0x100000001b3ull;
|
||||
@@ -718,7 +528,7 @@ HLE_FORCE("_ZN2nn2fs9WriteFileENS0_10FileHandleElPKvmRKNS0_11WriteOptionE", "fsp
|
||||
HLE_FORCE("_ZN2nn2fs11GetFileSizeEPlNS0_10FileHandleE", "fsp-srv → fstat(2)", Result, h_fs_GetFileSize, int64_t *out, uint64_t h) {
|
||||
if (!h) return RES_FAIL;
|
||||
struct stat st; if (fstat((int)h, &st)) return err_res("fstat", "fd");
|
||||
*out = (int)h < 4096 && g_hm[(int)h] ? g_hm[(int)h]->size : st.st_size; return RES_OK;
|
||||
*out = st.st_size; return RES_OK;
|
||||
}
|
||||
HLE_FORCE("_ZN2nn2fs11SetFileSizeENS0_10FileHandleEl", "fsp-srv → ftruncate(2)", Result, h_fs_SetFileSize, uint64_t h, int64_t sz) {
|
||||
if (!h) return RES_FAIL;
|
||||
@@ -730,7 +540,6 @@ HLE_FORCE("_ZN2nn2fs9FlushFileENS0_10FileHandleE", "fsp-srv; host writes are alr
|
||||
HLE_FORCE("_ZN2nn2fs9CloseFileENS0_10FileHandleE", "fsp-srv → close(2)", void, h_fs_CloseFile, uint64_t h) {
|
||||
if (!h) return; // the null handle after a failed open (see ReadFile): do not touch fd 0
|
||||
g_fmode[(int)h & 0xffff] = 0; g_fsave[(int)h & 0xffff] = 0;
|
||||
if ((int)h >= 0 && (int)h < 4096 && g_hm[(int)h]) { holemap *m = g_hm[(int)h]; g_hm[(int)h] = NULL; free(m->log); free(m->len); free(m->phys); free(m); }
|
||||
close((int)h); }
|
||||
HLE_FORCE("_ZN2nn2fs15GetFileOpenModeENS0_10FileHandleE", "fsp-srv; the open mode is recorded by the host", int, h_fs_GetFileOpenMode, uint64_t h) { return g_fmode[(int)h & 0xffff]; }
|
||||
|
||||
@@ -817,7 +626,6 @@ static void dir_snapshot(dirh_t *h, const mount_t *m, const char *rest) {
|
||||
struct dirent *e;
|
||||
while ((e = readdir(x))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
{ size_t dl = strlen(e->d_name); if (dl > 6 && !strcmp(e->d_name + dl - 6, ".holes")) continue; } // The extent table is not shown to the game
|
||||
if (!guest_name(parent, e->d_name, guest, 0x301)) continue;
|
||||
int dup = 0; for (int j = 0; j < nn; j++) if (!strcmp(names[j], guest)) { dup = 1; break; }
|
||||
if (!dup && nn < 2048) strlcpy(names[nn++], guest, sizeof names[0]);
|
||||
@@ -831,7 +639,7 @@ static void dir_snapshot(dirh_t *h, const mount_t *m, const char *rest) {
|
||||
snprintf(rel, 2048, "%s/%s", rest, names[j]);
|
||||
if (!rom_overlay_path(m, rel, resolved, 1024)) continue;
|
||||
struct stat st;
|
||||
if (!stat(resolved, &st)) { h->snap[j].type = S_ISDIR(st.st_mode) ? ENT_DIR : ENT_FILE; h->snap[j].size = S_ISDIR(st.st_mode) ? 0 : dm_data_logical_size(resolved, st.st_size); }
|
||||
if (!stat(resolved, &st)) { h->snap[j].type = S_ISDIR(st.st_mode) ? ENT_DIR : ENT_FILE; h->snap[j].size = S_ISDIR(st.st_mode) ? 0 : st.st_size; }
|
||||
}
|
||||
free(resolved); free(rel); free(guest); free(parent); free(all); free(names); free(roots);
|
||||
}
|
||||
@@ -852,7 +660,6 @@ HLE_FORCE("_ZN2nn2fs13ReadDirectoryEPlPNS0_14DirectoryEntryENS0_15DirectoryHandl
|
||||
int dfd = dirfd(h->d);
|
||||
while (n < max && (e = readdir(h->d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
{ size_t dl = strlen(e->d_name); if (dl > 6 && !strcmp(e->d_name + dl - 6, ".holes")) continue; } // The extent table is not shown to the game
|
||||
// a directory's type comes from d_type without stat; files need a size, taken with fstatat relative to the directory fd (following links, like the old stat)
|
||||
struct stat st; int isdir;
|
||||
if (e->d_type == DT_DIR) isdir = 1;
|
||||
@@ -860,7 +667,7 @@ HLE_FORCE("_ZN2nn2fs13ReadDirectoryEPlPNS0_14DirectoryEntryENS0_15DirectoryHandl
|
||||
if (!(h->mode & (isdir ? DIRMODE_DIR : DIRMODE_FILE))) continue;
|
||||
DirectoryEntry *o = &ents[n++]; memset(o, 0, sizeof *o);
|
||||
if (!guest_name(h->path, e->d_name, o->name, sizeof o->name)) { char c[1280]; snprintf(c, sizeof c, "%s/%s", h->path, e->d_name); *count = n - 1; return err_res("directory name", c); }
|
||||
o->type = isdir ? ENT_DIR : ENT_FILE; o->size = isdir ? 0 : data_logical_size_at(dfd, e->d_name, st.st_size);
|
||||
o->type = isdir ? ENT_DIR : ENT_FILE; o->size = isdir ? 0 : st.st_size;
|
||||
}
|
||||
*count = n; return RES_OK;
|
||||
}
|
||||
@@ -887,7 +694,6 @@ HLE_FORCE("_ZN2nn2fs22GetDirectoryEntryCountEPlNS0_15DirectoryHandleE", "fsp-srv
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
{ size_t dl = strlen(e->d_name); if (dl > 6 && !strcmp(e->d_name + dl - 6, ".holes")) continue; }
|
||||
char c[1280]; snprintf(c, sizeof c, "%s/%s", h->path, e->d_name);
|
||||
struct stat st; if (stat(c, &st)) continue;
|
||||
if (h->mode & (S_ISDIR(st.st_mode) ? DIRMODE_DIR : DIRMODE_FILE)) n++;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Host-initiated guest threads (host background work such as Granny pre-decoding needs to call the game's own allocator / locks and needs a complete SDK thread context).
|
||||
// Host-initiated guest threads (host background work that calls the game's own allocator / locks, such as the movie player's, needs a complete SDK thread context).
|
||||
// Approach: on a guest thread (e.g. before guest_main enters nnMain) call the SDK's nn::os::CreateThread / StartThread directly, with a host function as the entry --
|
||||
// the thread is created by the SDK, so ThreadType, thread handle and TLS slots are all in place, and the game's malloc / nn::os locks work as usual (the lock tag word holds this thread's guest handle).
|
||||
// Constraints: must be called on a guest thread (SDK thread creation goes through the SVC gate); not exposed to the game.
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
// Host lifecycle: graceful exit (SIGTERM; on iOS the App termination callback calls dm_host_shutdown).
|
||||
// Stop background threads (boot block prefetch), pause audio, suspend graphics (wait for in-flight NVN calls to end and drain GPU work), then _exit(0); a 1.5 s fallback goes straight to _exit(0).
|
||||
// Pause audio, suspend graphics (wait for in-flight NVN calls to end and drain GPU work), then _exit(0); a 1.5 s fallback goes straight to _exit(0).
|
||||
// The default action also exits in about 0.08 s (exit code 143); the value of this function is stopping background threads in order, exit code 0, and giving the iOS lifecycle callback an entry.
|
||||
#include "hle.h"
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <stdatomic.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
void tact_prefetch_stop(void);
|
||||
void granny_prof_dump(void);
|
||||
void hlecount_flush(void);
|
||||
void dm_host_shutdown(const char *why) {
|
||||
static _Atomic int once; if (atomic_exchange(&once, 1)) return;
|
||||
uint64_t t0 = now_ns(); LOG("received %s: graceful exit", why);
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1500 * NSEC_PER_MSEC), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
LOG("[RED] graceful exit not done in 1.5 seconds, exiting at once"); _exit(0); });
|
||||
tact_prefetch_stop();
|
||||
granny_prof_dump();
|
||||
hlecount_flush(); // DM_HLECOUNT: _exit does not run atexit, write a copy here
|
||||
dm_audio_suspend(1);
|
||||
dm_gfx_suspend(1);
|
||||
|
||||
+5
-15
@@ -3,23 +3,18 @@
|
||||
#include "hle.h"
|
||||
#include <dirent.h>
|
||||
|
||||
// ---------- aoc add-on content (D2R's Reign of the Warlock; other games by the DLC NSPs in the player's folder) / ec shop (excluded) ----------
|
||||
// Reign of the Warlock is the only add-on content in the Switch package: AOC 0100726014353001 = add-on content index 1 of the base 0100726014352000.
|
||||
// This DLC is only an unlock ticket of about 100 KB, the content itself is already in the main RomFS; main does not import MountAddOnContent and only judges by "is index 1 present".
|
||||
// The reimplementation scope requires keeping Reign of the Warlock, so index 1 is reported as owned; what is excluded is the shop (ec), not the DLC itself.
|
||||
enum { AOC_WARLOCK = 1 };
|
||||
// ---------- aoc add-on content (the DLC NSPs in the player's folder) / ec shop (excluded) ----------
|
||||
// Owned add-on content indices (ascending), fixed at the first query:
|
||||
// numeric subdirectories of DM_AOC_DIR: each is one DLC's RomFS, which the packing script extracts only from DLC NSPs really in the player's folder (title ID = base + 0x1000 + index).
|
||||
// Engage: F001 → 1 (E0), F002 → 2 (H0); the content itself is in the 2.0.0 update's RomFS and each DLC package holds one empty marker file; the game calls MountAddOnContent for each index ListAddOnContent returns (hle_fs.c).
|
||||
// No DM_AOC_DIR: D2R (main build db0507f3…) reports {1} (the Warlock expansion, content in the main RomFS, main does not import MountAddOnContent); other builds none --
|
||||
// on a Switch 2 Mario 1.2.1 uses it to tell whether the upgrade pack is owned, and the user has no Mario add-on content files.
|
||||
// DM_AOC_OWNED=0: always none (test switch, for lockstep comparison against ground truth recorded without the DLC installed); DM_AOC_OWNED=1 without DM_AOC_DIR: reports {1}.
|
||||
// No DM_AOC_DIR: none (on a Switch 2 Mario 1.2.1 uses it to tell whether the upgrade pack is owned, and the user has no Mario add-on content files).
|
||||
// DM_AOC_OWNED=0: always none (test switch, for runs without the DLC).
|
||||
static int g_aoc[64], g_naoc = -1;
|
||||
static int aoc_int_cmp(const void *a, const void *b) { return *(const int *)a - *(const int *)b; }
|
||||
static void aoc_load(void) {
|
||||
if (g_naoc >= 0) return;
|
||||
g_naoc = 0;
|
||||
const char *e = getenv("DM_AOC_OWNED"), *dir = getenv("DM_AOC_DIR"); const char *why = "by main build ID";
|
||||
const char *e = getenv("DM_AOC_OWNED"), *dir = getenv("DM_AOC_DIR"); const char *why = "no DM_AOC_DIR";
|
||||
if (e && !strcmp(e, "0")) why = "DM_AOC_OWNED=0";
|
||||
else if (dir && *dir) {
|
||||
why = dir; DIR *d = opendir(dir); struct dirent *ent;
|
||||
@@ -29,16 +24,11 @@ static void aoc_load(void) {
|
||||
}
|
||||
if (d) closedir(d);
|
||||
qsort(g_aoc, g_naoc, sizeof *g_aoc, aoc_int_cmp);
|
||||
} else {
|
||||
int d2r = 0;
|
||||
for (int i = 0; i < g_nmod; i++) if (!strcmp(g_mod[i].name, "main")) d2r = !strcmp(g_mod[i].build_id, "db0507f36b84200ac05f75d76ed5026f");
|
||||
if (e) why = "DM_AOC_OWNED";
|
||||
if (d2r || e) g_aoc[g_naoc++] = AOC_WARLOCK;
|
||||
}
|
||||
char list[256] = ""; for (int i = 0; i < g_naoc; i++) snprintf(list + strlen(list), sizeof list - strlen(list), "%s%d", i ? "," : "", g_aoc[i]);
|
||||
LOG("aoc: reporting %d add-on contents {%s} (%s)", g_naoc, list, why);
|
||||
}
|
||||
HLE_FORCE("_ZN2nn3aoc17CountAddOnContentEv", "add-on content count: length of the owned index list (D2R Reign of the Warlock / DM_AOC_DIR)", int, h_aoc_Count, void) { aoc_load(); return g_naoc; }
|
||||
HLE_FORCE("_ZN2nn3aoc17CountAddOnContentEv", "add-on content count: length of the owned index list (DM_AOC_DIR)", int, h_aoc_Count, void) { aoc_load(); return g_naoc; }
|
||||
// ListAddOnContent(out, start index, max count): writes out from the owned index list starting at the start index, returns the number written
|
||||
HLE_FORCE("_ZN2nn3aoc16ListAddOnContentEPiii", "add-on content list: the owned index list", int, h_aoc_List, int *out, int off, int n) {
|
||||
int w = 0; aoc_load();
|
||||
|
||||
+7
-125
@@ -2,7 +2,6 @@
|
||||
// oe::Initialize is in hle_sdkinit.c, not here.
|
||||
// Environment switches: DM_OPMODE=0 handheld / 1 console (default: the profile's op_mode, console unless set; console corresponds to the console-mode render profile); DM_LANG=language code (default en-US).
|
||||
#include "hle.h"
|
||||
#include "dm_uiscreen.h"
|
||||
|
||||
// The operation mode is runtime state (the front end can change it: iOS overheat downgrade switches to handheld): the single source is here, taken once from DM_OPMODE at startup, the environment variable is not read again afterwards
|
||||
#include <stdatomic.h>
|
||||
@@ -13,7 +12,7 @@ int dm_opmode(void) {
|
||||
return m;
|
||||
}
|
||||
// ---------- System notification queue (AppletMessage): front end state changes → fetched by the game every frame with TryPopNotificationMessage ----------
|
||||
// Message numbers (those actually handled by the game at main 0xe10d30): 15 focus change, 30 operation mode change, 31 performance mode change
|
||||
// Message numbers the host sends: 15 focus change, 30 operation mode change, 31 performance mode change
|
||||
enum { MSG_FOCUS = 15, MSG_OPMODE = 30, MSG_PERFMODE = 31 };
|
||||
#define MQ 32
|
||||
static pthread_mutex_t g_mq_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t g_mq_cond = PTHREAD_COND_INITIALIZER; // cond: wakes the blocking PopNotificationMessage
|
||||
@@ -38,7 +37,7 @@ static void mq_push(uint32_t m) { mq_push_(m); mq_ev_sync(); }
|
||||
static void mq_push_(uint32_t m) {
|
||||
pthread_mutex_lock(&g_mq_lock);
|
||||
// 15/30/31 are invalidation notifications meaning "query the current state", not events carrying historical values.
|
||||
// While one of the same type has not been taken yet, the first queue position is kept: the D2R consumer loop only reads the latest state, repeated notifications write back the same thing, and with the same size there is no further resize.
|
||||
// While one of the same type has not been taken yet, the first queue position is kept: a consumer loop only reads the latest state, repeated notifications write back the same thing, and with the same size there is no further resize.
|
||||
// Only these three state notifications are merged; future per-occurrence events (such as Resume/Exit) must not use this rule.
|
||||
// Legitimate producers only send these three, so at most three items are pending, and any number of focus losses/restores no longer hits the 32-slot limit; after a pop, new changes are enqueued as usual.
|
||||
if (m == MSG_FOCUS || m == MSG_OPMODE || m == MSG_PERFMODE)
|
||||
@@ -49,16 +48,6 @@ static void mq_push_(uint32_t m) {
|
||||
pthread_cond_broadcast(&g_mq_cond);
|
||||
pthread_mutex_unlock(&g_mq_lock);
|
||||
}
|
||||
void dm_oe_res_changed(void) { if (getenv("DM_TEST_OE_FOCUS_FIRST")) mq_push(MSG_FOCUS); /* Test: queue 31 behind a focus notification (the order at iOS app startup) */
|
||||
if (g_ntf_perfmode) mq_push(MSG_PERFMODE); else LOG("[RED] the resolution needs to change, but the game has not enabled performance mode change notifications"); } // the render resolution should change (dm_uiscreen.c): make the game re-read the parameter bits
|
||||
// Operation mode reported to GetOperationMode only (used to make the game reload layouts when switching between controller / touch layouts, dm_padmode.c):
|
||||
// m ≥ 0 reports m and pushes one operation-mode notification; m < 0 reports the true value again and pushes one more.
|
||||
// The controller style (hle_hid.c connected_id) and performance mode keep their true values, so no "Reconnect controller" prompt appears and the render
|
||||
// resolution does not change
|
||||
static _Atomic int g_ui_opmode = -1;
|
||||
static _Atomic int g_ui_armed; static _Atomic uint32_t g_ui_reads; // Counts once when the game first comes to read the operation mode after a notification has been pushed: the switch transaction uses it to read back "the game received it"
|
||||
uint32_t dm_oe_ui_opmode_reads(void) { return atomic_load(&g_ui_reads); }
|
||||
void dm_oe_ui_opmode(int m) { g_ui_opmode = m < 0 ? -1 : m; atomic_store(&g_ui_armed, 1); if (g_ntf_opmode) mq_push(MSG_OPMODE); }
|
||||
void dm_set_opmode(int m) { // called by the front end (overheat downgrade etc.): change the state and push 30 / 31 according to the switches the game has turned on
|
||||
int old = dm_opmode(); if (old == m) return;
|
||||
g_opmode = m;
|
||||
@@ -110,7 +99,7 @@ HLE_FORCE("_ZN2nn2oe17SetCopyrightImageEPKvmiiiiNS0_16WindowOriginModeE", "copyr
|
||||
HLE_FORCE("_ZN2nn2oe22SetCopyrightVisibilityEb", "copyright overlay visibility; the host has no system screenshot", void, h_oe_CopyrightVisible, int on) { (void)on; }
|
||||
|
||||
// OperationMode: 0 Handheld, 1 Console; PerformanceMode: 0 Normal, 1 Boost (one-to-one with the operation mode)
|
||||
HLE_FORCE("_ZN2nn2oe16GetOperationModeEv", "operation mode query; a fixed value from DM_OPMODE", int, h_oe_GetOpMode, void) { int u = g_ui_opmode; if (atomic_exchange(&g_ui_armed, 0)) atomic_fetch_add(&g_ui_reads, 1); return u >= 0 ? u : opmode(); }
|
||||
HLE_FORCE("_ZN2nn2oe16GetOperationModeEv", "operation mode query; a fixed value from DM_OPMODE", int, h_oe_GetOpMode, void) { return opmode(); }
|
||||
// Profile perf_mode boost: Boost whatever the operation mode, a combination a Switch 1 never reports, so a profile choice and not a default. Rhythm Heaven Groove keeps one flag per mode
|
||||
// (main+0xa764 / main+0xa784): console means a TV, which gets a latency calibration (main+0x20d0, scene script main+0x4c0820), and Boost alone picks its 1920x1080 draw size
|
||||
// (table main+0xada6d0, else 1280x720). Handheld with Boost: no TV calibration on a phone, full-size drawing
|
||||
@@ -132,10 +121,8 @@ HLE_FORCE("_ZN2nn2oe27SetPerformanceConfigurationENS0_15PerformanceModeEi", "set
|
||||
}
|
||||
|
||||
// Scripted state changes (verify lifecycle / overheat downgrade without a window): DM_OE_SCRIPT="focus=3@10,focus=1@20,opmode=0@30" (second N); bg=1 / bg=0 run the whole background round trip (see script_bg)
|
||||
// Time counts from the game's first notification fetch; "@ingame+N" = N seconds after the loading screen is taken down for the K-th time (K = number of preceding @ingame items + 1, the first one is run entry) (event-based, not affected by load). Coexists with front end calls.
|
||||
// Time counts from the game's first notification fetch. Coexists with front end calls.
|
||||
#include <unistd.h>
|
||||
static _Atomic int g_ls_unloads; // number of times the loading screen was taken down (the UIMSG handler in diag.c calls dm_note_loadscreen_unload)
|
||||
void dm_note_loadscreen_unload(void) { atomic_fetch_add(&g_ls_unloads, 1); }
|
||||
// bg=1 / bg=0: the whole "to background / back to foreground" sequence, in the same order as the scene lifecycle in front_ios.m (resign active -> enter background; enter foreground -> become active), minus the audio session reset iOS does on returning.
|
||||
// Used to reproduce "after going to the background and back" problems on the Mac (audio take-over and resume, guest clock freeze, graphics suspend); to imitate the process being suspended as well, send dmhost SIGSTOP / SIGCONT between the two items
|
||||
static void script_bg(int on) {
|
||||
@@ -146,15 +133,9 @@ static void *oe_script(void *arg) {
|
||||
char *buf = arg; uint64_t t0 = now_ns();
|
||||
for (char *save, *tok = strtok_r(buf, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) {
|
||||
char key[16]; int v; double at;
|
||||
if (sscanf(tok, "%15[a-z]=%d@ingame+%lf", key, &v, &at) == 3) {
|
||||
static int need;
|
||||
need++; while (atomic_load(&g_ls_unloads) < need) usleep(20000);
|
||||
usleep((useconds_t)(at * 1e6));
|
||||
} else {
|
||||
if (sscanf(tok, "%15[a-z]=%d@%lf", key, &v, &at) != 3) die("DM_OE_SCRIPT entry '%s' should be key=value@seconds or key=value@ingame+seconds", tok);
|
||||
uint64_t due = t0 + (uint64_t)(at * 1e9), now = now_ns();
|
||||
if (due > now) usleep((useconds_t)((due - now) / 1000));
|
||||
}
|
||||
if (sscanf(tok, "%15[a-z]=%d@%lf", key, &v, &at) != 3) die("DM_OE_SCRIPT entry '%s' should be key=value@seconds", tok);
|
||||
uint64_t due = t0 + (uint64_t)(at * 1e9), now = now_ns();
|
||||
if (due > now) usleep((useconds_t)((due - now) / 1000));
|
||||
if (!strcmp(key, "focus")) dm_focus_set(v);
|
||||
else if (!strcmp(key, "opmode")) dm_set_opmode(v);
|
||||
else if (!strcmp(key, "bg")) script_bg(v);
|
||||
@@ -173,111 +154,12 @@ HLE_FORCE("_ZN2nn2oe20GetCurrentFocusStateEv", "foreground / background state; d
|
||||
HLE_FORCE("_ZN2nn2oe20SetFocusHandlingModeENS0_17FocusHandlingModeE", "focus handling mode; the game sets 1 (focus notifications), the host implements the notification semantics", void, h_oe_SetFocusMode, int m) { LOG("oe::SetFocusHandlingMode %d", m); }
|
||||
|
||||
// Notification: returns bool (w0), the host has no system notifications
|
||||
// ---------- 3D internal render scale ----------
|
||||
// The game has a static 3D render scale built in: renderer object (global pointer [main+0x31e0848]) member +0x66a4 (float, always 1.0 in the original game).
|
||||
// The resolution change function main+0x8582f0(output width, output height) → main+0x1627ef0 rebuilds the 3D target as "scale × output width/height" (+0xee78 / +0xee7c),
|
||||
// the UI layer and Final Result are still built at the output size (+0xee88 / +0xee8c), and at the end the game's own upscale shader ps_upsample_p brings it back to the output size.
|
||||
// The boot flow does not call 0x8582f0, so it has to be called once actively; the call site must be where the game main thread fetches system notifications -- the same context in which the game itself handles mode switches
|
||||
// (calling it in the present HLE self-deadlocks: the render lock is already held by the present path).
|
||||
// Default: iOS 0.75, Mac host 1.0 (the screenshot gate compares against Switch ground truth); DM_3D_SCALE=0.5–1.0 overrides.
|
||||
// The visual ruler ground truth phase (A / B of DM_NVN_RULER=1) forces 1.0, it only takes effect after switching to the mode under test.
|
||||
// Dynamic scale (system throttling looks at utilization, the thermal state is only a coarse signal → adjust by actual dropped frames): the controller consumes the actual present intervals reported by the present thread
|
||||
// (same source as perf.txt), three levels configured value (0.75) → 0.72 → 0.70; lower limit 0.70 (0.65 is visibly different). For the rules see scale3d_ctl_step.
|
||||
// Switches: DM_3D_SCALE_DYN=0 off; DM_THERMAL_NOFALLBACK=1 (power / frame time measurement) also turns it off -- the scale must be pinned during measurement.
|
||||
// DM_3D_SCALE_DYN_INJECT=start frame-end frame: diagnostic, within these frames every interval fed to the controller is taken as 25 ms (the Mac has no present timestamps, this is used to verify the whole lower / raise chain).
|
||||
#include <TargetConditionals.h>
|
||||
#define SCALE3D_MIN 0.70f
|
||||
#define SCALE3D_MID 0.72f
|
||||
// Pure-function controller (unit test t_oe.c feeds sequences directly): level 0 = configured value, larger is lower. Safety valve: only acts on perceptible dropped frames.
|
||||
// Window: one window per 2 seconds of accumulated present intervals, frame rate in the window = presented frames ÷ sum of intervals (frames dropped by the system are not counted, that time falls into the interval of the next presented frame).
|
||||
// Lower: frame rate of two consecutive windows both < target × 0.92 (55.2 at 60 fps) → lower one level.
|
||||
// Raise: frame rate of 10 consecutive windows (20 seconds) all ≥ target × 0.97 (58.2 at 60 fps) → raise one level; if knocked back within 10 seconds of going up, the next raise needs 30 consecutive windows (60 seconds).
|
||||
// Judging is suspended for 2 seconds after a level change (rebuilding the 3D target itself causes a hitch). Target frame rate = 1000 ÷ period_ms (0 = 60 fps).
|
||||
// The old rule (lower as soon as slow frames > 5% in a 2-second window, raise needs 60 consecutive seconds with zero slow frames, knock-back ×3) lowered too easily and almost never came back up: on the test iPhone, of 582 run entries 33 were lowered and 0 came back up,
|
||||
// and two real-device logs were also lowered to 0.70 at thermal state 2 with frame rate 59.8–60; the slow frame count is kept only in the diagnostic log and no longer takes part in the decision.
|
||||
typedef struct { int level, max_level; double period_ms; uint64_t hold_until, last_up; double win_ms, last_fps; unsigned win_n; int bad_run, good_run, need_good; } scale3d_ctl;
|
||||
#define S3_SEC 1000000000ull
|
||||
static void scale3d_ctl_hold(scale3d_ctl *c, uint64_t now, uint64_t ns) { // suspend judging for a while (after entering a run / returning to foreground / changing level): the window and the consecutive counts are cleared
|
||||
if (now + ns > c->hold_until) c->hold_until = now + ns;
|
||||
c->win_ms = 0; c->win_n = 0; c->bad_run = c->good_run = 0;
|
||||
}
|
||||
static int scale3d_ctl_step(scale3d_ctl *c, double iv_ms, uint64_t now) { // returns the current level
|
||||
if (now < c->hold_until || iv_ms <= 0) return c->level; // judging suspended / no reading (0) / dropped (−1)
|
||||
c->win_ms += iv_ms; c->win_n++;
|
||||
if (c->win_ms < 2000) return c->level;
|
||||
double tgt = 1000.0 / (c->period_ms > 0 ? c->period_ms : 1000.0 / 60), fps = c->win_n * 1000.0 / c->win_ms;
|
||||
c->last_fps = fps; c->win_ms = 0; c->win_n = 0;
|
||||
if (fps < tgt * 0.92) {
|
||||
c->good_run = 0;
|
||||
if (++c->bad_run >= 2 && c->level < c->max_level) {
|
||||
if (c->last_up && now - c->last_up < 10 * S3_SEC) c->need_good = 30; // dropped again within 10 seconds of going up: the next raise needs 60 seconds
|
||||
c->level++; scale3d_ctl_hold(c, now, 2 * S3_SEC);
|
||||
}
|
||||
} else {
|
||||
c->bad_run = 0;
|
||||
if (fps >= tgt * 0.97) { if (++c->good_run >= (c->need_good ? c->need_good : 10) && c->level > 0) { c->level--; c->last_up = now; c->need_good = 0; scale3d_ctl_hold(c, now, 2 * S3_SEC); } }
|
||||
else c->good_run = 0;
|
||||
}
|
||||
return c->level;
|
||||
}
|
||||
static _Atomic int g_scale3d_level;
|
||||
static _Atomic int g_scale3d_resumed;
|
||||
void dm_3d_scale_resumed(void) { atomic_store(&g_scale3d_resumed, 1); } // front end: just returned to foreground. Slow frames in the first few seconds back are recovery (surface rebuild, caches gone cold), not GPU pressure: suspend judging for 3 seconds
|
||||
void dm_3d_scale_feed(double iv_ms) { // called by the present thread every frame (forwarded from diag.c dm_perf_present): actual present interval in ms, −1 = dropped, 0 = no reading
|
||||
static scale3d_ctl c = {.max_level = 2}; static int on = -1; static long inj0 = -1, inj1 = -1;
|
||||
if (on < 0) { const char *e = getenv("DM_3D_SCALE_DYN"), *j = getenv("DM_3D_SCALE_DYN_INJECT"); on = !(e && !strcmp(e, "0")) && !getenv("DM_THERMAL_NOFALLBACK");
|
||||
if (j) { inj0 = atol(j); inj1 = strchr(j, '-') ? atol(strchr(j, '-') + 1) : inj0; } }
|
||||
if (!on) return;
|
||||
if (inj0 >= 0) { long f = (long)dm_nvn_frame_count(); iv_ms = f >= inj0 && f <= inj1 ? 25.0 : 16.7; }
|
||||
uint64_t now = now_ns();
|
||||
if (atomic_exchange(&g_scale3d_resumed, 0)) scale3d_ctl_hold(&c, now, 3 * S3_SEC);
|
||||
{ static int was; int in = dm_ingame_now(); // not judged outside a run (menu / loading); not judged in the first 20 seconds of a run either (slow frames in the first ten-odd seconds of a run are loading, not GPU pressure)
|
||||
if (!in) { was = 0; scale3d_ctl_hold(&c, now, S3_SEC); return; }
|
||||
if (!was) { was = 1; if (inj0 < 0) scale3d_ctl_hold(&c, now, 20 * S3_SEC); } }
|
||||
{ int mtl_fps_target(void); double p = mtl_fps_target() == 30 ? 1000.0 / 30 : 0; if (p != c.period_ms) { c.period_ms = p; scale3d_ctl_hold(&c, now, 2 * S3_SEC); } } // power-saving mode (30 fps): the target period changes with it, judging is suspended for 2 seconds after the switch
|
||||
int was = c.level, lv = scale3d_ctl_step(&c, iv_ms, now);
|
||||
if (lv != was) LOG("3D scale control: window frame rate %.1f (target %.0f) → %s to level %d%s", c.last_fps, c.period_ms > 0 ? 1000.0 / c.period_ms : 60.0, lv > was ? "down" : "up", lv, c.need_good ? " (dropped right after going up: the next step up needs 60 s)" : "");
|
||||
atomic_store(&g_scale3d_level, lv);
|
||||
}
|
||||
void dm_3d_scale_hot(int hot) { (void)hot; } // overheating no longer lowers the level directly (now by actual dropped frames); the symbol is kept for the front end
|
||||
static _Atomic float g_scale3d_cur = 1.0f;
|
||||
float dm_3d_scale_cur(void) { return atomic_load(&g_scale3d_cur); }
|
||||
static void scale3d_tick(void) {
|
||||
static float want_cfg = -1, cur = 1.0f; static module_t *main;
|
||||
if (want_cfg < 0) {
|
||||
const char *e = getenv("DM_3D_SCALE"); want_cfg = TARGET_OS_IPHONE ? 0.75f : 1.0f;
|
||||
if (e) { char *end; float v = strtof(e, &end); if (end == e || *end || !(v >= 0.5f && v <= 1.0f)) die("DM_3D_SCALE invalid: %s (must be 0.5–1.0)", e); want_cfg = v; }
|
||||
for (int i = 0; i < g_nmod; i++) if (!strcmp(g_mod[i].name, "main")) main = &g_mod[i];
|
||||
// The addresses above (renderer global pointer, resolution change function, member offsets) all belong to D2R build db0507f3…; in other builds (every Mario version) the same addresses are unrelated data and code
|
||||
// (Mario v0's main+0x8582f0 is in the middle of a static initializer), so writing and calling them would corrupt memory. Only enabled on that build; elsewhere it says so only when DM_3D_SCALE asks for this scale
|
||||
// (Breath of the Wild and Tears of the Kingdom scale through DM_RENDER_SCALE, and the line read like a failure in their logs)
|
||||
if (main && strcmp(main->build_id, "db0507f36b84200ac05f75d76ed5026f")) { if (e) LOG("3D render scale: main build %s has no known addresses, left alone (only D2R db0507f3… is known)", main->build_id[0] ? main->build_id : "(output has no build ID)"); main = NULL; }
|
||||
}
|
||||
int lv = atomic_load(&g_scale3d_level); // dynamic level: 0 = configured value, 1 = 0.72, 2 = 0.70 (none higher than the configured value)
|
||||
float dyn = lv == 0 ? want_cfg : lv == 1 ? SCALE3D_MID : SCALE3D_MIN; if (dyn > want_cfg) dyn = want_cfg;
|
||||
float want = dm_nvn_ruler_truth() ? 1.0f : dyn;
|
||||
if (want == cur || !main || dm_nvn_frame_count() < 30) return;
|
||||
uintptr_t renderer = *(uintptr_t *)(main->base + 0x31e0848); if (!renderer) return;
|
||||
uint32_t ow = *(uint32_t *)(renderer + 0xee88), oh = *(uint32_t *)(renderer + 0xee8c); if (!ow || !oh) return;
|
||||
{ extern void mtl_rt_force_full(int) __attribute__((weak)); if (mtl_rt_force_full) mtl_rt_force_full(4); } // runtime extrapolation: the few frames before and after a scale change are all drawn in full (the frame where the main pass changes size cannot be extrapolated)
|
||||
float *scale = (float *)(renderer + 0x66a4); dm_guest_write_prepare(scale, sizeof *scale); *scale = want;
|
||||
((void (*)(uint32_t, uint32_t))(main->base + 0x8582f0))(ow, oh);
|
||||
cur = want; atomic_store(&g_scale3d_cur, want);
|
||||
LOG("3D render scale %.2f: 3D target %ux%u, output %ux%u (frame %llu)", want, *(uint32_t *)(renderer + 0xee78), *(uint32_t *)(renderer + 0xee7c),
|
||||
*(uint32_t *)(renderer + 0xee88), *(uint32_t *)(renderer + 0xee8c), (unsigned long long)dm_nvn_frame_count());
|
||||
}
|
||||
HLE_FORCE("_ZN2nn2oe25TryPopNotificationMessageEPj", "system notification queue; front end state changes are queued", unsigned, h_oe_TryPopMsg, uint32_t *msg) {
|
||||
scale3d_tick();
|
||||
pthread_once(&g_oe_once, oe_script_start);
|
||||
pthread_mutex_lock(&g_mq_lock);
|
||||
int ok = g_mqn > 0;
|
||||
if (ok) { *msg = g_mq[g_mqh]; g_mqh = (g_mqh + 1) % MQ; g_mqn--; }
|
||||
pthread_mutex_unlock(&g_mq_lock);
|
||||
// The resolution parameter bits live in the thread slot, and the game reads them on the thread that fetches notifications to change the render size (on receiving 31 "performance mode changed", from main+0xe10ddc; 30 only updates a flag).
|
||||
// When they are read: each time the handler is entered, once after the first notification is fetched (0xe10d5c / 0xe10d60), and notifications fetched later in the loop still use the values from that read. So every time one is taken, the value due at this moment is written into the current thread's slot --
|
||||
// writing only when 31 is taken is not enough: when 31 is queued behind another notification (e.g. focus 15), the parameter bits have already been read after fetching 15 (iOS app: the front end did not switch to 16:9, in the Mac host run 31 happened to be queued first).
|
||||
if (ok) for (int i = 0; i < 2; i++) if (g_keyres[i]) pthread_setspecific(g_keyres[i], (void *)dm_game_res(i));
|
||||
if (ok && *msg == MSG_PERFMODE) dm_uiscreen_res_reread();
|
||||
if (ok) LOG("oe notification popped by the game: %u", *msg); // state changes are rare, printing each one does not flood the log
|
||||
mq_ev_sync(); // signalled again while notifications remain, cleared when the queue is empty (also a signal left behind by a pop that found it empty)
|
||||
return ok;
|
||||
|
||||
+1
-14
@@ -55,26 +55,13 @@ static int detclock_cfg(void) {
|
||||
static uint64_t wall_guest_ns(void) { return atomic_load(&g_gfz) ? atomic_load(&g_gfrozen) : now_ns() - atomic_load(&g_glag); }
|
||||
uint64_t guest_ns(void) { return atomic_load(&g_gdet_on) ? atomic_load(&g_gdet) : wall_guest_ns(); }
|
||||
int dm_detclock_enabled(void) { return detclock_cfg() && atomic_load(&g_gdet_on); }
|
||||
// Run entry alignment: the number of presented frames in the front end / character select phase varies with real time (loading advances on a background thread by wall clock), so the guest clock value accumulated since the first frame differs every run --
|
||||
// pictures that depend on absolute time (such as the liquid level of the life / mana orbs) still differ at the same tick (measured: at tick E + 2 frames two passes differ by about 18,000 pixels, concentrated at the two orb positions).
|
||||
// On seeing the UI message FrontEndNavigation:ToGame (before the run is created and the server tick deadline is initialized) the guest clock is set to the fixed value 2^52 ns (forward jumps only),
|
||||
// from then on it advances purely by presents: which frame tick E lands on and the absolute time at that moment are determined by the step alone.
|
||||
#define DET_TOGAME_NS (1ull << 52)
|
||||
static void det_on_uimsg(const char *cat, const char *act, const char *arg) {
|
||||
(void)arg; if (strcmp(cat, "FrontEndNavigation") || strcmp(act, "ToGame")) return;
|
||||
uint64_t cur = atomic_load(&g_gdet);
|
||||
if (cur >= DET_TOGAME_NS) { LOG("[RED] DM_DETCLOCK: guest clock %llu ≥ alignment value 2^52 on entering a run, not turned back (alignment not applied)", (unsigned long long)cur); return; }
|
||||
atomic_store(&g_gdet, DET_TOGAME_NS);
|
||||
LOG("DM_DETCLOCK: on entering a run (ToGame) the guest clock is aligned to 2^52 ns (jumped forward %.3f s)", (DET_TOGAME_NS - cur) / 1e9);
|
||||
}
|
||||
void dm_guest_frame_tick(void) {
|
||||
crash_watch_frame();
|
||||
atomic_fetch_add(&g_guest_frame, 1);
|
||||
if (!detclock_cfg()) return;
|
||||
if (!atomic_load(&g_gdet_on)) {
|
||||
atomic_store(&g_gdet, wall_guest_ns()); atomic_store(&g_gdet_on, 1);
|
||||
void uimsg_subscribe(void (*)(const char *, const char *, const char *)); uimsg_subscribe(det_on_uimsg);
|
||||
LOG("DM_DETCLOCK: from the first present on, the guest clock advances by presents, step %llu ns; aligned to 2^52 ns on entering a run", (unsigned long long)g_gdet_step); return;
|
||||
LOG("DM_DETCLOCK: from the first present on, the guest clock advances by presents, step %llu ns", (unsigned long long)g_gdet_step); return;
|
||||
}
|
||||
if (!atomic_load(&g_gfz)) { atomic_fetch_add(&g_gdet, g_gdet_step); dm_guest_clock_advanced(); }
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// System service interfaces used by games on newer SDKs (16.x) that the d2r-mobile title did not use: oe / time / pl (shared font) / vi vsync / diag.
|
||||
// System service interfaces used by games on newer SDKs (16.x): oe / time / pl (shared font) / vi vsync / diag.
|
||||
// On the real device all of these go through Horizon system services (IPC); the host gives values directly or makes them "unavailable".
|
||||
#include "hle.h"
|
||||
#include <fcntl.h>
|
||||
|
||||
+1
-3
@@ -15,7 +15,7 @@
|
||||
// about 4.2 GB in total; on iOS without the "extended virtual addressing" entitlement a single reservation can be at most 6 GB
|
||||
// (read on a real device: 6 GB succeeds, 7 GB fails), so it cannot be made any wider --
|
||||
// with an 8 GB reservation, builds from developer accounts that cannot sign that entitlement (free accounts) would exit at this step.
|
||||
#define RESERVE DM_GAME_MEM_RESERVE // SMBW: the heap needs only 1.47 GB, so the block stays at 5 GB like D2R (the total proven on a real device; a single 6 GB block can be reserved, but Metal etc. still need address space)
|
||||
#define RESERVE DM_GAME_MEM_RESERVE // SMBW: the heap needs only 1.47 GB, so the block stays at 5 GB (the total proven on a real device; a single 6 GB block can be reserved, but Metal etc. still need address space)
|
||||
static uint8_t *g_as; // ASLR region base = the whole block
|
||||
static uint8_t *g_heap; static size_t g_heap_cur, g_heap_cap;
|
||||
// The address range lies entirely within the currently opened (readable and writable) part of the guest heap: such reads need no kernel trap to check, memcpy directly (used where guest memory is read every tick, such as diag.c tk_rd)
|
||||
@@ -120,7 +120,6 @@ uint64_t svc_dispatch(int num, uint64_t *regs, uint64_t orig_addr) {
|
||||
{ void hlecount_svc(int); hlecount_svc(num); } // DM_HLECOUNT: svc counts by number
|
||||
mem_init();
|
||||
if (svc_thread(num, regs)) return regs[0];
|
||||
{ uint64_t svc_host_call(int, uint64_t *); if (num >= 0x80 && svc_host_call(num, regs)) return regs[0]; } // from 0x80: host calls by cut convention (hle_tact.c)
|
||||
switch (num) {
|
||||
case 0x01: { // SetHeapSize(out_addr*, size) → x1 = heap base
|
||||
size_t size = regs[1];
|
||||
@@ -204,7 +203,6 @@ uint64_t svc_dispatch(int num, uint64_t *regs, uint64_t orig_addr) {
|
||||
// The address lies in "the current host thread's stack": report the whole stack (state 0xB = stack). When the SDK initializes the main thread record it queries here with the sp at that moment and takes the reported range as the main thread stack;
|
||||
// later nn::os::GetCurrentStackInfo asserts that sp is still inside that range and aborts if not. Previously every address outside the guest address space was reported as "the 4 KiB page containing it",
|
||||
// so the main thread stack was recorded as one page. Whether the sp at init and a later sp share a page depends on the absolute position of the host stack top, which moves with the total length of environment variables / launch arguments (a different environment occasionally aborts at startup).
|
||||
// This Diablo build does not call GetCurrentStackInfo (main / subsdk0 do not import it and the SDK has no internal call site; the host reaches the main menu with 12 different environment padding lengths), so it was never hit; reporting "one page" is wrong in itself, so fix it anyway.
|
||||
uint64_t top = (uint64_t)pthread_get_stackaddr_np(pthread_self()), ssz = pthread_get_stacksize_np(pthread_self());
|
||||
if (addr < top && addr >= top - ssz) { base = top - ssz; size = ssz; state = 0xB/*Stack*/; perm = 3; }
|
||||
// Other host memory: report "free, no permissions" (the 4 KiB page around it) as Horizon does for unmapped addresses. It used to report Code+RX, and GetExceptionInfo walking back from main merged on through host memory down to 0
|
||||
|
||||
+2
-3
@@ -1,13 +1,13 @@
|
||||
// swkbd software keyboard (system applet) → host native input box. MakePreset / SetInitialTextUtf8 / GetRequired*Size are left to run as is in the SDK,
|
||||
// here we only read the ShowKeyboardArg they write and write back String per the output convention of the real swkbd applet.
|
||||
// Layout sources (offsets in the original sdk 3.3.94012 image):
|
||||
// Layout sources (offsets in the sdk image the host was first written against):
|
||||
// ShowKeyboard 0x3a6770 moves KeyboardConfig field by field into the applet arguments: +0x24 title u16[65], +0xa6 subtitle u16[129], +0x1a8 hint u16[257],
|
||||
// +0x3ac maximum length, +0x3b0 minimum length, +0x3bd isUseUtf8, +0x3c0 offset of the initial text within the work buffer, +0x3c4 UTF-16 length of the initial text;
|
||||
// work buffer pointer / size at +0x4d0 / +0x4d8 (handed to the applet together with CreateLargeStorage)
|
||||
// SetInitialTextUtf8 0x3a74c0: UTF-8 → UTF-16 written to work buffer+offset (offset 0x14, at most 500 characters + terminating 0)
|
||||
// ReadCloseResultAndString 0x3a63f0: output storage = u32 CloseResult + text area; the text area is copied as is into String{+0 pointer, +8 byte count}
|
||||
// ShowKeyboard wrap-up 0x3a6acc: CloseResult == 0 (OK) → Result 0, otherwise (cancel) → 0x29F; GetRequiredStringBufferSize = 0x7D4
|
||||
// Game side (main 0xc6deac) sets isUseUtf8 = 1 after MakePreset and reads back as a C string (0x1b66640 strlen); UTF-16 output is supported as well per the original convention.
|
||||
// A game that sets isUseUtf8 = 1 after MakePreset reads the result back as a C string; UTF-16 output is supported as well per the original convention.
|
||||
// No window and DM_KBD unset: return as cancel (no popup, no hang). DM_KBD="text": no popup, return that text directly as OK (windowless testing / lockstep comparison).
|
||||
#include "hle.h"
|
||||
#include <stddef.h>
|
||||
@@ -90,7 +90,6 @@ Result swkbd_show(void *out, const void *arg_, swkbd_input_fn input) {
|
||||
uint16_t u16[1003]; size_t n = swkbd_u8_to_u16(text, maxlen > 0 ? (size_t)maxlen : 1002, u16, 1003); // truncate to the character limit (counted in UTF-16 units)
|
||||
if (arg[ARG_UTF8]) swkbd_u16_to_u8(u16, n, (char *)buf, cap);
|
||||
else memcpy(buf, u16, (n + 1) * 2 <= cap ? n * 2 : (cap / 2 - 1) * 2);
|
||||
{ char fin[64]; swkbd_u16_to_u8(u16, n, fin, sizeof fin); void dm_ui_text_entered(const char *text); dm_ui_text_entered(n > 0 ? fin : NULL); }
|
||||
{ const char *lag = getenv("DM_TEST_SWKBD_LAG_MS"); if (lag) usleep((useconds_t)atoi(lag) * 1000); } // test: slow down "keyboard handed back → the game's thread gets the result" to verify that auto-accept does not jump the gun // front end path cleanup (dm_cursor.c): auto-accept after the name is typed on the naming page
|
||||
return RES_OK;
|
||||
}
|
||||
|
||||
+5
-44
@@ -165,16 +165,7 @@ static void wake(thr_t *t, int64_t res) {
|
||||
if (self && self != t && !t->host_owned && t->prio < self->prio && t->mask == 1ull << (self->core & 63)) self->yield_pending = 1;
|
||||
}
|
||||
// Suspend until woken or timed out (ns<0 infinite, 0 immediate timeout); returns wres or R_TIMEOUT
|
||||
static int64_t block_inner(thr_t *t, int kind, int64_t ns);
|
||||
static int64_t block(thr_t *t, int kind, int64_t ns) { // Boot timeline: accumulated by wait kind (all threads) + the main thread listed separately
|
||||
if (!g_flag.bootprof) return block_inner(t, kind, ns);
|
||||
uint64_t t0 = now_ns(); int64_t r = block_inner(t, kind, ns); uint64_t d = now_ns() - t0;
|
||||
static const int id[] = {0, PROF_WAIT_LOCK, PROF_WAIT_CV, PROF_WAIT_ADDR, PROF_WAIT_SYNC};
|
||||
dm_prof_add(id[kind & 7], d, 0);
|
||||
if (t->handle == 0x1001) dm_prof_add(PROF_MAIN_WAIT, d, 0);
|
||||
return r;
|
||||
}
|
||||
static int64_t block_inner(thr_t *t, int kind, int64_t ns) {
|
||||
static int64_t block(thr_t *t, int kind, int64_t ns) {
|
||||
t->wkind = kind; t->woke = 0; t->next = g_wait; g_wait = t; t->wseq = ++g_wseq;
|
||||
if (ns == 0) { wait_unlink(t); t->wkind = 0; return R_TIMEOUT; }
|
||||
struct timespec dl;
|
||||
@@ -445,7 +436,7 @@ static void thr_bt(thr_t *t) {
|
||||
arm_thread_state64_t st; mach_msg_type_number_t n = ARM_THREAD_STATE64_COUNT;
|
||||
if (thread_get_state(t->mport, ARM_THREAD_STATE64, (thread_state_t)&st, &n) == KERN_SUCCESS) {
|
||||
// Threads not waiting in a system call (running guest code, possibly spinning on a condition): print the callee-saved registers x19–x28, plus 128 bytes of the memory pointed to by each of x19 / x20.
|
||||
// The state seen in the occasional "no frames after entering a run": the main thread spins in the job pool's "help while waiting" (main+0x14f08d8), x19 = wait object, x20 = job pool -- only with these can one tell which batch it is waiting for and what the unfinished count is
|
||||
// A thread spinning on a condition is told apart by its callee-saved registers (often the object it waits on) and the memory they point to
|
||||
if (t->wkind == 0) { char rl[400]; int q = snprintf(rl, sizeof rl, "[dm] %#x registers pc=", t->handle); uint64_t pc = __darwin_arm_thread_state64_get_pc(st); module_t *pm = module_of((void *)pc);
|
||||
q += pm ? snprintf(rl + q, sizeof rl - q, "%s+%#llx", pm->name, pc - (uint64_t)pm->base) : snprintf(rl + q, sizeof rl - q, "%#llx", pc);
|
||||
for (int i = 19; i <= 28; i++) q += snprintf(rl + q, sizeof rl - q, " x%d=%#llx", i, st.__x[i]);
|
||||
@@ -461,43 +452,15 @@ static void thr_bt(thr_t *t) {
|
||||
if (sp > 0x100000000ull && !vm_read_overwrite(mach_task_self(), sp, sizeof stk, (vm_address_t)stk, &g) && g == sizeof stk)
|
||||
for (int r = 0; r < 128; r += 8) fprintf(stderr, "[dm] %#x stack sp+%#05x: %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", t->handle, r * 8,
|
||||
stk[r], stk[r + 1], stk[r + 2], stk[r + 3], stk[r + 4], stk[r + 5], stk[r + 6], stk[r + 7]); }
|
||||
char line[1024]; int p = snprintf(line, sizeof line, "[dm] %#x backtrace:", t->handle), nf = 0; uint64_t wait_fp = 0;
|
||||
char line[1024]; int p = snprintf(line, sizeof line, "[dm] %#x backtrace:", t->handle), nf = 0;
|
||||
for (int k = 0; k < 64 && fp && nf < 20 && p < (int)sizeof line - 40; k++) {
|
||||
module_t *m = module_of((void *)lr);
|
||||
if (m) { p += snprintf(line + p, sizeof line - p, " %s+%#llx", m->name, lr - (uint64_t)m->base); nf++; }
|
||||
uint64_t fr[2]; vm_size_t g2 = 0;
|
||||
if (vm_read_overwrite(mach_task_self(), fp, 16, (vm_address_t)fr, &g2) || g2 != 16 || fr[0] <= fp) break;
|
||||
if (m && !strcmp(m->name, "main") && lr - (uint64_t)m->base == 0x132db30) wait_fp = fp; // The return point is inside the resource wait function main+0x132d988 (after it calls the job pool's "help while waiting"): the frame pointer at this moment is its own
|
||||
fp = fr[0]; lr = fr[1];
|
||||
}
|
||||
fprintf(stderr, "%s\n", line);
|
||||
// The resource wait function main+0x132d988 (prologue sub sp,#0x50 … add x29,sp,#0x40) places the wait object {vtable, pointer to the event pointer} at the bottom of its own stack (0x132db14 stp x8,x23,[sp]),
|
||||
// so the wait object = its frame pointer −0x40; what that points to, −0x10 = the resource wait object {+0 in-flight job, +8 mutex pointer, +0x10 event pointer}. It is located by the return point 0x132db30, regardless of which instruction of the wait loop the thread is stopped on right now.
|
||||
// The old code took "the previous frame" based on 0x14f0934 and was off by one level, reading the resource wait object as the wait object; "in-flight job null" and "event / lock unreadable" were both misreads
|
||||
if (wait_fp) { uint64_t wo[2] = {0, 0}, res[3] = {0, 0, 0}, wjob = 0; vm_size_t g = 0;
|
||||
#define RD(addr, buf) ((addr) > 0x100000000ull && !vm_read_overwrite(mach_task_self(), (addr), sizeof(buf), (vm_address_t)(buf), &g) && g == sizeof(buf))
|
||||
#define HEX(tag, addr, nbytes) { uint32_t raw[32]; vm_size_t gg = 0; if ((addr) > 0x100000000ull && !vm_read_overwrite(mach_task_self(), (addr), (nbytes), (vm_address_t)raw, &gg) && gg == (nbytes)) { \
|
||||
char hx[300]; int h = 0; for (unsigned q = 0; q < (nbytes) / 4; q++) h += snprintf(hx + h, sizeof hx - h, "%08x ", raw[q]); fprintf(stderr, "[dm] %#x job pool wait: %s %#llx: %s\n", t->handle, tag, (unsigned long long)(addr), hx); } \
|
||||
else fprintf(stderr, "[dm] %#x job pool wait: %s %#llx unreadable\n", t->handle, tag, (unsigned long long)(addr)); }
|
||||
if (RD(wait_fp - 0x40, wo)) { module_t *vm_ = module_of((void *)wo[0]);
|
||||
fprintf(stderr, "[dm] %#x job pool wait: wait object %#llx vtable %s+%#llx (should be main+0x20f2408), points to %#llx\n", t->handle, (unsigned long long)(wait_fp - 0x40), vm_ ? vm_->name : "?", vm_ ? wo[0] - (uint64_t)vm_->base : wo[0], wo[1]);
|
||||
if (RD(wo[1] - 0x10, res)) {
|
||||
fprintf(stderr, "[dm] %#x job pool wait: resource wait object %#llx: in-flight job %#llx (%s), mutex %#llx, event %#llx. Reading: in-flight job null = the signal was consumed by some wait; non-null and event not set = the completion signal never came; event set = a problem in the wait loop\n",
|
||||
t->handle, (unsigned long long)(wo[1] - 0x10), res[0], res[0] ? "non-null" : "null", res[1], res[2]);
|
||||
HEX("event body 64 bytes", res[2], 64);
|
||||
HEX("mutex body 32 bytes (+8 recursion count, +0x10 owner thread object, +0x18 lock word = owner handle)", res[1], 32);
|
||||
// Job object (filled by the submit function main+0x147f154): +0x10 flags (filtered by a per-thread-class mask when taking a job, 0xeda190 / 0xdf8fac), +0x20 next in the queue, dependency table from +0x30 (low 31 bits = count), +0x60 resource, +0x68 pointer to the event pointer
|
||||
if (res[0]) { HEX("in-flight job object, 128 bytes from +0", res[0], 128); HEX("in-flight job object, 128 bytes from +0x80", res[0] + 0x80, 128); } wjob = res[0]; } }
|
||||
// Job pool: when the thread is stopped on an instruction of the wait loop main+0x14f08d8 itself, x20 is the pool; when it is stopped in a function called from it, x20 cannot be trusted and is not printed
|
||||
{ uint64_t pc = __darwin_arm_thread_state64_get_pc(st); module_t *pm = module_of((void *)pc); uint64_t off = pm && !strcmp(pm->name, "main") ? pc - (uint64_t)pm->base : 0;
|
||||
// Queue (0xeda190): {+0 tail, +8 head, +0x10 count, +0x18 lock}, node +0x10 flags, +0x20 next. Walk from the head for at most 8 nodes to see whether the awaited job is in this pool
|
||||
if (off >= 0x14f0900 && off < 0x14f09b8) { uint64_t qa = st.__x[20] + 0x410, nd = 0; HEX("job queue head (pool = x20, 32 bytes from +0x410)", qa, 32); int in = 0, cnt = 0;
|
||||
if (RD(qa + 8, &nd)) for (; nd && cnt < 8; cnt++) { uint64_t nb[5] = {0}; if (!RD(nd, nb)) break; fprintf(stderr, "[dm] %#x job pool wait: queue entry %d %#llx flags %#x%s\n", t->handle, cnt, (unsigned long long)nd, (uint32_t)nb[2], nd == wjob ? " (the waited-for job)" : ""); in |= nd == wjob; nd = nb[4]; }
|
||||
fprintf(stderr, "[dm] %#x job pool wait: the waited-for job %s among the first %d in the queue of pool x20\n", t->handle, in ? "is" : "is not", cnt); }
|
||||
else fprintf(stderr, "[dm] %#x job pool wait: the thread is not on the wait loop's own instructions, job pool not read\n", t->handle); }
|
||||
#undef RD
|
||||
#undef HEX
|
||||
}
|
||||
}
|
||||
thread_resume(t->mport);
|
||||
}
|
||||
@@ -530,7 +493,7 @@ void svc_thread_dump(void) {
|
||||
fprintf(stderr, "[dm] %#x waited-for object, 64 bytes from %#llx: %s\n", t->handle, t->waddr & ~0xfull, hx); }
|
||||
thr_bt(t);
|
||||
}
|
||||
// Threads created by the host itself (prefetch, Granny background, graphics present, audio, etc.): not in the guest thread table, enumerated as mach threads, the first 12 frames symbolized with dladdr
|
||||
// Threads created by the host itself (graphics present, audio, movie decoding, etc.): not in the guest thread table, enumerated as mach threads, the first 12 frames symbolized with dladdr
|
||||
thread_act_array_t ths; mach_msg_type_number_t nth = 0;
|
||||
if (task_threads(mach_task_self(), &ths, &nth) == KERN_SUCCESS) {
|
||||
for (mach_msg_type_number_t i = 0; i < nth; i++) {
|
||||
@@ -668,14 +631,13 @@ static int svc_thread_impl(int num, uint64_t *regs) {
|
||||
case 0x0A: thread_exit(cur()); return 1;
|
||||
case 0x0B: { // SleepThread(x0 ns); 0/-1/-2 = the various kinds of yield
|
||||
int64_t ns = (int64_t)regs[0];
|
||||
uint64_t t0 = g_flag.bootprof ? now_ns() : 0;
|
||||
if (ns > 0 && dm_detclock_enabled()) {
|
||||
uint64_t deadline = guest_ns() + (uint64_t)ns;
|
||||
while (guest_ns() < deadline) { struct timespec ts = {0, 1000000}; nanosleep(&ts, 0); }
|
||||
} else if (ns >= 20000) { thr_t *me = pthread_getspecific(g_keythr); if (me) me->sleeping = 1; sleep_precise(now_ns() + (uint64_t)ns); if (me) me->sleeping = 0; }
|
||||
// Yield (0 / -1 / -2), and sleeps shorter than 20 µs, which sleep_precise would return from at once: Super Mario Galaxy's Hagi polls with SleepThread(100 ns) (main+0x517c),
|
||||
// which kept a core at 100% (1.4 billion calls in 200 s); as yields they get the back-off below.
|
||||
// Count and time of the main thread's yields are recorded: the game's render task graph "wait for everything to finish" is pure polling + yield (main+0x15ee898); measure how much of a frame it takes
|
||||
// Count and time of the main thread's yields are recorded: a render task graph's "wait for everything to finish" is often pure polling + yield; measure how much of a frame it takes
|
||||
else {
|
||||
thr_t *me = pthread_getspecific(g_keythr);
|
||||
uint64_t y0 = me ? now_ns() : 0;
|
||||
@@ -685,7 +647,6 @@ static int svc_thread_impl(int num, uint64_t *regs) {
|
||||
else sched_yield();
|
||||
if (me && me->handle == 0x1001) { g_main_yield_ns += now_ns() - y0; g_main_yield_n++; }
|
||||
}
|
||||
if (g_flag.bootprof) { uint64_t d = now_ns() - t0; dm_prof_add(PROF_SLEEP, d, 0); thr_t *me = pthread_getspecific(g_keythr); if (me && me->handle == 0x1001) dm_prof_add(PROF_MAIN_WAIT, d, 0); }
|
||||
return 1;
|
||||
}
|
||||
case 0x0C: { thr_t *t = h_get((uint32_t)regs[1], O_THREAD); regs[0] = t ? 0 : R_BAD_HANDLE; regs[1] = t ? (uint64_t)t->prio : 0; return 1; }
|
||||
|
||||
+5
-45
@@ -8,37 +8,13 @@
|
||||
typedef struct { int16_t year; int8_t month, day, hour, minute, second; int8_t pad; } CalendarTime;
|
||||
typedef struct { uint32_t wday, yday; char tzname[8]; uint32_t dst; int32_t offset; } CalendarAdditionalInfo;
|
||||
|
||||
// DM_FIXED_TIME=seconds (for deterministic comparison; the ground-truth side counterpart is dm/verify/ryu/drive.py --fix-time):
|
||||
// ① the user clock (source of the save's d2s +0x20 last-played time and .meta last_modified_time) always returns this value;
|
||||
// ② the one time() at the game-creation random seed (main 0x3509a0 bl time, return point 0x3509a4, same point as drive.py) returns this value.
|
||||
// This SDK's libc time() is uptime (clock_gettime(0) → __nnmusl_ClockGetTime → GetSystemTick), not wall clock;
|
||||
// the other 17 time() call sites in main are left alone and forwarded unchanged to the SDK's own time().
|
||||
// DM_FIXED_TIME=seconds (for deterministic comparison): the user clock always returns this value (and main.c pins the host time zone to UTC)
|
||||
static int64_t fixed_time(void) {
|
||||
static int64_t v = -2;
|
||||
if (v == -2) { const char *e = getenv("DM_FIXED_TIME"); v = e ? strtoll(e, 0, 0) : -1;
|
||||
if (v >= 0) LOG("DM_FIXED_TIME=%lld: user clock and the run-setup time() (main 0x3509a4) return a fixed value", (long long)v); }
|
||||
if (v >= 0) LOG("DM_FIXED_TIME=%lld: the user clock returns a fixed value", (long long)v); }
|
||||
return v;
|
||||
}
|
||||
HLE_FORCE("time", "recognizes the run-setup seed call by its return address (DM_FIXED_TIME); everything else passes straight to the SDK's time()", int64_t, h_time_time, int64_t *t) {
|
||||
uint64_t lr = (uint64_t)__builtin_return_address(0);
|
||||
static int64_t (*sdk_time)(int64_t *); static uint64_t seed_ret;
|
||||
if (!sdk_time) {
|
||||
module_t *ms = NULL, *mm = NULL;
|
||||
for (int i = 0; i < g_nmod; i++) { if (!strcmp(g_mod[i].name, "sdk")) ms = &g_mod[i]; if (!strcmp(g_mod[i].name, "main")) mm = &g_mod[i]; }
|
||||
void *f = ms ? module_sym(ms, "time") : NULL; if (!f) die("time not found in the SDK");
|
||||
uint32_t w = mm && !strcmp(mm->build_id, DM_D2R_BUILD) ? *(uint32_t *)(mm->base + 0x3509a0) : 0; // read only on the D2R build (elsewhere this is unrelated code, and a main smaller than 3.3 MB would be read past its end)
|
||||
if ((w & 0xfc000000) == 0x94000000) seed_ret = (uint64_t)mm->base + 0x3509a4; // bl time (build db0507f3…; the rewriter changes the branch target, so only the bl itself is matched)
|
||||
else if (fixed_time() >= 0) LOG("[RED] DM_FIXED_TIME: main 0x3509a0 is not the expected bl time (%#x), run-setup time() not fixed", w);
|
||||
sdk_time = (int64_t (*)(int64_t *))f;
|
||||
}
|
||||
int64_t ft = fixed_time();
|
||||
if (ft >= 0 && lr == seed_ret) {
|
||||
static int once; if (!once++) LOG("DM_FIXED_TIME: run-setup time() returns %lld", (long long)ft);
|
||||
if (t) *t = ft; return ft;
|
||||
}
|
||||
return sdk_time(t);
|
||||
}
|
||||
|
||||
HLE_FORCE("_ZN2nn4time10InitializeEv", "connect to the time service; clocks come from the host system time instead", Result, h_time_Initialize, void) { return RES_OK; }
|
||||
HLE_FORCE("_ZN2nn4time13IsInitializedEv", "the time service is taken over, always initialized", int, h_time_IsInitialized, void) { return 1; }
|
||||
// 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
|
||||
@@ -78,23 +54,7 @@ HLE_FORCE("_ZN2nn4time23StandardUserSystemClock21GetSystemClockContextEPNS0_18Sy
|
||||
pthread_mutex_unlock(&mu);
|
||||
return RES_OK;
|
||||
}
|
||||
// Forced Terror Zone for stages 6–10 of the boss rush (set by dm_bossrush.c): when g_dm_tz_time is non-zero and not expired, only the clock reads at the "Terror Zone" sites get it; save timestamps and all other calls are unchanged.
|
||||
// Which sites: the return point falls in the cluster starting at the zone function 0x58f1e0 (0x58f1e0–0x591440), or the zone schedule 0x58f310 is called within 8 instructions after the return (server side 0x636b38 applying the zone, 0x694b48 and so on, 9 sites).
|
||||
// Expiry: dm_bossrush renews it by 1 second every tick; once this run stops ticking (exit / crash) it no longer applies, and the next run gets the real clock when the game is created.
|
||||
_Atomic int64_t g_dm_tz_time;
|
||||
_Atomic uint64_t g_dm_tz_live_ns;
|
||||
static int tz_site(uint64_t lr) { // Checked only while forcing is active, reading the 8 instructions after the return point each time, no caching (several threads read the clock)
|
||||
static _Atomic uint64_t base; uint64_t b = atomic_load(&base);
|
||||
if (!b) { for (int i = 0; i < g_nmod; i++) if (!strcmp(g_mod[i].name, "main")) b = (uint64_t)g_mod[i].base; atomic_store(&base, b); }
|
||||
if (!b || lr < b) return 0;
|
||||
uint64_t off = lr - b; if (off > 0x58f1e0 && off < 0x591440) return 1;
|
||||
for (int k = 0; k < 8; k++) { uint32_t w = *(const uint32_t *)(uintptr_t)(lr + 4 * k);
|
||||
if ((w & 0xfc000000) == 0x94000000) { int64_t imm = w & 0x3ffffff; if (imm & 0x2000000) imm -= 1 << 26; if (off + 4 * k + imm * 4 == 0x58f310) return 1; } }
|
||||
return 0;
|
||||
}
|
||||
HLE_FORCE("_ZN2nn4time23StandardUserSystemClock3nowEv", "user clock → host time(); while the boss rush forces a zone, the Terror Zone call sites get a different value", int64_t, h_time_now, void) {
|
||||
int64_t tz = atomic_load(&g_dm_tz_time);
|
||||
if (tz && (uint64_t)clock_gettime_nsec_np(CLOCK_MONOTONIC) < atomic_load(&g_dm_tz_live_ns) && tz_site((uint64_t)__builtin_return_address(0))) return tz;
|
||||
HLE_FORCE("_ZN2nn4time23StandardUserSystemClock3nowEv", "user clock → host time()", int64_t, h_time_now, void) {
|
||||
int64_t ft = fixed_time(); return ft >= 0 ? ft : time(NULL); }
|
||||
HLE_FORCE("_ZN2nn4time23StandardUserSystemClock9to_time_tERKNSt3__16chrono10time_pointINS0_17SystemClockTraitsENS3_8durationIlNS2_5ratioILl1ELl1EEEEEEE",
|
||||
"the SDK implementation pairs with now(), taken over with it", int64_t, h_time_to_time_t, const int64_t *tp) { return *tp; }
|
||||
@@ -142,8 +102,8 @@ HLE_FORCE("_ZN2nn4time18ToPosixTimeFromUtcERKNS0_12CalendarTimeE", "UTC calendar
|
||||
struct tm tm = {.tm_year = ct->year - 1900, .tm_mon = ct->month - 1, .tm_mday = ct->day, .tm_hour = ct->hour, .tm_min = ct->minute, .tm_sec = ct->second};
|
||||
return (int64_t)timegm(&tm);
|
||||
}
|
||||
// Local time conversion is forced onto the host libc: the SDK's own localtime / localtime_r / mktime have no time zone data and always use UTC (measured on the host: the chronicle's "First Discovered" is produced by
|
||||
// main+0xb04d34 calling localtime and then strftime, and was off by 8 hours). The SDK on a real Switch converts by the system time zone, so only following the host system time zone here is consistent;
|
||||
// Local time conversion is forced onto the host libc: the SDK's own localtime / localtime_r / mktime have no time zone data and always use UTC
|
||||
// (a game's local timestamps were off by the time zone's offset). The SDK on a real Switch converts by the system time zone, so only following the host system time zone here is consistent;
|
||||
// the host TZ normally = the system time zone, and is pinned to UTC only for deterministic comparison with DM_FIXED_TIME (main.c). The struct tm layout is the same on both sides (9 ints, long offset, time zone name pointer).
|
||||
// gmtime / gmtime_r / timegm are UTC anyway and are not taken over. The same-named pass-through entries in hle_table.c only take effect when the symbol has no definition (the SDK defines them), hence HLE_FORCE
|
||||
HLE_FORCE("localtime", "local time → host localtime (follows the system time zone)", struct tm *, h_localtime, const int64_t *t) { time_t v = (time_t)*t; return localtime(&v); }
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Hook points left behind by the game-specific modules split out of d2r-mobile (diagnostic tick gates, TACT/Granny, UI screen, touch UI interaction, ...):
|
||||
// the generic host still calls them, and they get empty implementations here. When a feature is redone for this game, replace the matching stub with the real thing.
|
||||
#include "dm.h"
|
||||
#include "front.h"
|
||||
|
||||
void crash_sample_self(void) {}
|
||||
void inflate_bench_maybe(void) {}
|
||||
void ticks_install(void) {}
|
||||
void target_rank_install(void) {}
|
||||
void dm_padmode_boot(void) {}
|
||||
void dm_prof_mark(const char *seg) { (void)seg; }
|
||||
void dm_prof_add(int id, uint64_t ns, uint64_t bytes) { (void)id; (void)ns; (void)bytes; }
|
||||
void tact_chk_init(void) {}
|
||||
void tact_prefetch_start(const char *outdir) { (void)outdir; }
|
||||
void tact_prefetch_stop(void) {}
|
||||
void granny_prof_dump(void) {}
|
||||
void uimsg_subscribe(void (*fn)(const char *cat, const char *act, const char *arg)) { (void)fn; }
|
||||
void dm_ui_text_entered(const char *text) { (void)text; }
|
||||
int dm_custom_lang_explicit(void) { return 0; }
|
||||
int dm_ingame_now(void) { return 0; }
|
||||
void dm_uiscreen_res_reread(void) {}
|
||||
// Game resolution: whatever the front end gives (Mac window / iOS screen), without D2R's UI-screen rewriting
|
||||
uint64_t dm_game_res(int docked) { return front_game_res(docked); }
|
||||
int dm_screen_real(int *w, int *h) { *w = *h = 0; return 0; }
|
||||
void dm_ui_rect(int *x, int *y, int *w, int *h) { *x = *y = *w = *h = 0; }
|
||||
// svc #0x80.. host gates: only D2R's rewrite rules create them; they should not appear in this game's build output
|
||||
uint64_t svc_host_call(int num, uint64_t *regs) { (void)regs; die("svc_host_call: this game has no host gate svc #%#x", num); }
|
||||
+13
-46
@@ -13,8 +13,7 @@
|
||||
module_t g_mod[MAX_MOD];
|
||||
int g_nmod;
|
||||
pthread_key_t g_key18, g_keytls, g_keytlr, g_keygate;
|
||||
pthread_key_t g_keyres[2], g_keyaspect, g_keyaspectui; // g_keyaspect: aspect ratio upper-limit parameter slot (KASPECT) // resolution parameter slots (rewriter PARAMS, KRES0 handheld / KRES1 docked); 0 and not allocated if the build output lacks them
|
||||
enum { KEY_K18, KEY_KTLS, KEY_KTLR, KEY_KGATE, KEY_KRES0, KEY_KRES1, KEY_KASPECT, KEY_KASPECTUI, KEY_N };
|
||||
enum { KEY_K18, KEY_KTLS, KEY_KTLR, KEY_KGATE, KEY_N };
|
||||
|
||||
uint64_t resolve_import(module_t *m, const char *name);
|
||||
int hle_implements(const char *name);
|
||||
@@ -23,18 +22,13 @@ void gate_register(uint64_t ret_addr, const char *kind, int arg, uint64_t orig);
|
||||
static pthread_key_t key_value(unsigned i) {
|
||||
switch (i) {
|
||||
case KEY_K18: return g_key18; case KEY_KTLS: return g_keytls; case KEY_KTLR: return g_keytlr;
|
||||
case KEY_KGATE: return g_keygate; case KEY_KRES0: return g_keyres[0]; case KEY_KRES1: return g_keyres[1];
|
||||
case KEY_KASPECT: return g_keyaspect; default: return g_keyaspectui;
|
||||
default: return g_keygate;
|
||||
}
|
||||
}
|
||||
static void create_keys(void) {
|
||||
if (pthread_key_create(&g_key18, NULL) || pthread_key_create(&g_keytls, NULL) ||
|
||||
pthread_key_create(&g_keytlr, NULL) || pthread_key_create(&g_keygate, NULL)) die("cannot create the host key");
|
||||
// Parameter slot keys are always created (not decided by whether the build output uses them: an iOS app has no params.tsv, so deciding by file would miss creating them and the key page would be written as 0 → the game reads TSD slot 0)
|
||||
if (pthread_key_create(&g_keyres[0], NULL) || pthread_key_create(&g_keyres[1], NULL) ||
|
||||
pthread_key_create(&g_keyaspect, NULL) || pthread_key_create(&g_keyaspectui, NULL)) die("cannot create the parameter-bit key");
|
||||
LOG("key numbers K18=%lu KTLS=%lu KTLR=%lu KGATE=%lu KRES0=%lu KRES1=%lu KASPECT=%lu KASPECTUI=%lu",
|
||||
g_key18, g_keytls, g_keytlr, g_keygate, g_keyres[0], g_keyres[1], g_keyaspect, g_keyaspectui);
|
||||
LOG("key numbers K18=%lu KTLS=%lu KTLR=%lu KGATE=%lu", g_key18, g_keytls, g_keytlr, g_keygate);
|
||||
}
|
||||
|
||||
// K slot check: the pthread TSD layout is fixed, slot = tpidrro_el0 + 8*key
|
||||
@@ -150,10 +144,10 @@ static void read_layout(module_t *m, const char *dir) {
|
||||
char path[1024]; snprintf(path, sizeof path, "%s/%s.fixups.tsv", dir, m->name);
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) die("open %s", path);
|
||||
static const char *kn[8] = {"K18", "KTLS", "KTLR", "KGATE", "KRES0", "KRES1", "KASPECT", "KASPECTUI"};
|
||||
static const char *kn[KEY_N] = {"K18", "KTLS", "KTLR", "KGATE"};
|
||||
char line[256], key[32]; unsigned long v; unsigned seen = 0;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
if (sscanf(line, "# %31s %lx", key, &v) != 2) die("%s: the output comes from an old rewriter; replace it with output of the current product name (dm/PRODUCT)", path);
|
||||
if (sscanf(line, "# %31s %lx", key, &v) != 2) die("%s: the output comes from an old rewriter; rewrite the modules again with dm/tools/patch_nso.py", path);
|
||||
if (!strcmp(key, "PATCH_OFF")) m->patch_off = strtoll(line + 2 + strlen(key), NULL, 16); // may be negative (patch area before the base)
|
||||
else if (!strcmp(key, "IMAGE_SIZE")) m->image_size = v;
|
||||
else if (!strcmp(key, "FILE_IMAGE_LEN")) m->file_image_len = v;
|
||||
@@ -163,9 +157,8 @@ static void read_layout(module_t *m, const char *dir) {
|
||||
else if (!strcmp(key, "RO_OFF")) m->ro_off = v;
|
||||
else if (!strcmp(key, "DATA_OFF")) m->data_off = v;
|
||||
else if (!strcmp(key, "FPS_STATE_OFF")) m->fps_state_off = v;
|
||||
else if (!strcmp(key, "FPS_LEGACY_ELAPSED")) m->fps_legacy_elapsed = (int)v;
|
||||
else if (!strcmp(key, "BUILD_ID")) sscanf(line, "# %*s %32s", m->build_id);
|
||||
else for (int k = 0; k < 8; k++) {
|
||||
else for (int k = 0; k < KEY_N; k++) {
|
||||
size_t n = strlen(kn[k]);
|
||||
if (!strncmp(key, "KEY_", 4) && !strncmp(key + 4, kn[k], n) && !strcmp(key + 4 + n, "_IDX")) {
|
||||
m->key_idx[k] = (unsigned)strtoul(strrchr(line, '\t') + 1, 0, 10); seen |= 1u << k;
|
||||
@@ -173,44 +166,35 @@ static void read_layout(module_t *m, const char *dir) {
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
if (!m->patch_off || m->key_page_off % 0x4000 || m->key_page_size != 0x4000 || seen != 0xff)
|
||||
die("%s: the output comes from an old rewriter; replace it with output of the current product name (dm/PRODUCT)", path);
|
||||
if (!m->patch_off || m->key_page_off % 0x4000 || m->key_page_size != 0x4000 || seen != (1u << KEY_N) - 1)
|
||||
die("%s: the output comes from an old rewriter; rewrite the modules again with dm/tools/patch_nso.py", path);
|
||||
if (m->patch_off < 0 && (!m->image_size || !m->file_image_len || m->file_image_len % 0x4000 || m->patch_off % 0x4000 || m->key_page_off + m->key_page_size > m->image_size))
|
||||
die("%s: patch area before the base, but IMAGE_SIZE / FILE_IMAGE_LEN missing or wrong", path);
|
||||
}
|
||||
|
||||
// Frame step state (the page after the key page, read-write; layout in patch_nso.py FRAME_STEP): the rewriter's stubs step the game by the host's whole vsyncs capped at this
|
||||
// target, 0 = the game's own interval logic. The profile's fps_target, overridden by DM_FPS_TARGET (0, 30 or 60); the graphics layer decides the steps and paces presents
|
||||
// (hle_nvn.c fstep_tick). Mode +0x28: the profile's frame_step, overridden by DM_FRAME_STEP (ticks / legacy); legacy is each game's earlier method: Tears of the Kingdom's
|
||||
// elapsed-time step, or for a build whose legacy is not one (FPS_LEGACY_ELAPSED 0, Breath of the Wild) target 0, so its own interval controller runs
|
||||
// (hle_nvn.c fstep_tick)
|
||||
uint8_t *dm_fps_state;
|
||||
static void install_fps_state(module_t *m) {
|
||||
if (m->fps_state_off + 0x40 > m->size || m->fps_state_off < m->key_page_off + m->key_page_size) die("%s: frame step state page out of range", m->name);
|
||||
const char *e = getenv("DM_FPS_TARGET"); int t = e ? atoi(e) : DM_GAME_FPS_TARGET;
|
||||
if (t != 0 && t != 30 && t != 60) { LOG("[RED] DM_FPS_TARGET=%s: must be 0, 30 or 60; using the game's own frame step", e); t = 0; }
|
||||
const char *fe = getenv("DM_FRAME_STEP"); int legacy = DM_GAME_FRAME_STEP_LEGACY;
|
||||
if (fe) { if (!strcmp(fe, "legacy")) legacy = 1; else if (!strcmp(fe, "ticks")) legacy = 0; else LOG("[RED] DM_FRAME_STEP=%s: must be ticks or legacy; keeping the profile's frame_step", fe); }
|
||||
if (legacy && !m->fps_legacy_elapsed) t = 0;
|
||||
float f = (float)t; dm_fps_state = m->base + m->fps_state_off; memcpy(dm_fps_state, &f, sizeof f);
|
||||
uint32_t mode = (uint32_t)legacy; memcpy(dm_fps_state + 0x28, &mode, sizeof mode);
|
||||
// Render scale of the 3D views (the stubs at Tears of the Kingdom's main+0xd5cc0, Breath of the Wild's main+0x1266880): 1.0 keeps the game's own size and dynamic resolution;
|
||||
// another factor fixes the size at that fraction (DM_RENDER_SCALE, 0.5 to 1.0). Independent of the frame step: written in legacy mode and at target 0 too
|
||||
// another factor fixes the size at that fraction (DM_RENDER_SCALE, 0.5 to 1.0). Independent of the frame step: written at target 0 too
|
||||
const char *rs = getenv("DM_RENDER_SCALE"); float sc = 1.0f;
|
||||
if (rs) { char *end; sc = strtof(rs, &end); if (end == rs || *end || !(sc >= 0.5f && sc <= 1.0f)) { LOG("[RED] DM_RENDER_SCALE=%s: must be 0.5 to 1.0; keeping the game's size", rs); sc = 1.0f; } else LOG("%s: 3D views rendered at %.2f of the game's size (DM_RENDER_SCALE)", m->name, sc); }
|
||||
memcpy(dm_fps_state + 0x10, &sc, sizeof sc);
|
||||
char cap[32]; snprintf(cap, sizeof cap, ", capped at %d fps", t);
|
||||
if (legacy && !m->fps_legacy_elapsed) LOG("%s: frame step as the game sets it (legacy: %s)", m->name, fe ? "DM_FRAME_STEP" : "game profile frame_step");
|
||||
else LOG("%s: frame step %s%s (%s; %s)", m->name, !t ? "as the game sets it" : legacy ? "by elapsed time (legacy)" : "in whole vsyncs decided by the host", t ? cap : "",
|
||||
e ? "DM_FPS_TARGET" : "game profile fps_target", fe ? "DM_FRAME_STEP" : "game profile frame_step");
|
||||
LOG("%s: frame step %s%s (%s)", m->name, !t ? "as the game sets it" : "in whole vsyncs decided by the host", t ? cap : "", e ? "DM_FPS_TARGET" : "game profile fps_target");
|
||||
}
|
||||
static void install_key_page(module_t *m) {
|
||||
if (m->key_page_off + m->key_page_size > m->size) die("%s: key page out of range", m->name);
|
||||
uint64_t *page = (uint64_t *)(m->base + m->key_page_off);
|
||||
memset(page, 0, m->key_page_size);
|
||||
for (unsigned i = 0; i < 4; i++) page[m->key_idx[i]] = (uint64_t)key_value(i) * 8;
|
||||
page[m->key_idx[KEY_KRES0]] = (uint64_t)g_keyres[0] * 8; page[m->key_idx[KEY_KRES1]] = (uint64_t)g_keyres[1] * 8;
|
||||
page[m->key_idx[KEY_KASPECT]] = (uint64_t)g_keyaspect * 8; page[m->key_idx[KEY_KASPECTUI]] = (uint64_t)g_keyaspectui * 8;
|
||||
for (int k = 0; k < 8; k++) if (!page[m->key_idx[k]]) die("%s: key page entry %d is 0 (host key not created) -- the game would read TSD slot 0", m->name, k);
|
||||
for (unsigned i = 0; i < KEY_N; i++) page[m->key_idx[i]] = (uint64_t)key_value(i) * 8;
|
||||
for (int k = 0; k < KEY_N; k++) if (!page[m->key_idx[k]]) die("%s: key page entry %d is 0 (host key not created) -- the game would read TSD slot 0", m->name, k);
|
||||
if (mprotect(m->base + m->key_page_off, m->key_page_size, PROT_READ)) die("%s key page mprotect", m->name);
|
||||
if (m->fps_state_off) install_fps_state(m);
|
||||
}
|
||||
@@ -346,24 +330,7 @@ static void remap_at(module_t *m, vm_address_t to, const void *from, unsigned lo
|
||||
if (kr != KERN_SUCCESS || a != to) die("%s vm_remap %p → %#lx (%#lx bytes) failed: %d", m->name, from, (unsigned long)to, size, kr);
|
||||
if ((cur & need) != need) die("%s permissions after vm_remap %#x, missing %#x", m->name, cur, need);
|
||||
}
|
||||
#ifdef DM_SHELL
|
||||
// Shell (the App built by ios/tools/shell_build.sh, containing no game content at all): the module images are not in the main executable but in the
|
||||
// Frameworks/Guest.framework/Guest that users build themselves from their game files -- a dynamic library that holds only segments (written by dm/tools/guest_dylib.py), with the same section names as the embedded approach; below only the image header to fetch from is swapped.
|
||||
#include <dlfcn.h>
|
||||
#include <mach-o/dyld.h>
|
||||
static const struct mach_header_64 *image_mh(void) {
|
||||
static const struct mach_header_64 *mh; if (mh) return mh;
|
||||
char exe[4096]; uint32_t n = sizeof exe; if (_NSGetExecutablePath(exe, &n)) die("cannot get the executable path");
|
||||
char *sl = strrchr(exe, '/'); if (!sl || (size_t)(sl - exe) + 40 > sizeof exe) die("unexpected executable path: %s", exe); strcpy(sl, "/Frameworks/Guest.framework/Guest");
|
||||
if (!dlopen(exe, RTLD_NOW)) die("this install package has no game program yet (%s): first put your own game files into the package with dm/tools/d2rm_pack.py, then install it. The system says: %s", exe, dlerror());
|
||||
for (uint32_t i = 0; i < _dyld_image_count(); i++) if (strstr(_dyld_get_image_name(i), "/Guest.framework/Guest")) mh = (const struct mach_header_64 *)_dyld_get_image_header(i);
|
||||
if (!mh) die("Guest dynamic library is loaded but its image header cannot be found");
|
||||
fprintf(stderr, "boot: module image taken from Guest dynamic library %s\n", exe); return mh;
|
||||
}
|
||||
#define IMAGE_MH image_mh()
|
||||
#else
|
||||
#define IMAGE_MH (&_mh_execute_header)
|
||||
#endif
|
||||
static void map_image(module_t *m, const char *dir) {
|
||||
(void)dir;
|
||||
char sx0[17], sx1[17], dm[17]; snprintf(sx0, sizeof sx0, "__dm%sx0", m->name); snprintf(sx1, sizeof sx1, "__dm%sx1", m->name); snprintf(dm, sizeof dm, "__DM%sW", m->name);
|
||||
|
||||
+3
-16
@@ -6,18 +6,15 @@
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
void probe_install(void); void ticks_install(void); void hid_keys_init(void); void tact_prefetch_start(const char *outdir); void tact_chk_init(void); void stubs_report(void); void crash_install(void); void guest_thread_init(void); uint32_t svc_thread_main_init(void);
|
||||
void probe_install(void); void hid_keys_init(void); void stubs_report(void); void crash_install(void); void guest_thread_init(void); uint32_t svc_thread_main_init(void);
|
||||
typedef void (*fn0)(void); typedef void (*fn2)(uint64_t, uint64_t); typedef void (*fnp)(void *);
|
||||
static void *req(const char *n){ void *p=guest_sym(n); if(!p) die("missing symbol %s", n); return p; }
|
||||
|
||||
// Game arguments: argv[0] is fixed to "game", and whatever follows "--" is appended as is (on a real device nn::init takes them from the process argument area and hands them to the SDK through SetHostArgc/Argv)
|
||||
static char *g_argv[64] = {"game"}; static int g_argc = 1;
|
||||
|
||||
void crash_sample_self(void);
|
||||
static void guest_main(void) {
|
||||
guest_thread_init();
|
||||
{ void inflate_bench_maybe(void); inflate_bench_maybe(); } // DM_INFLATE_BENCH test mode (for diagnostics, exits when done)
|
||||
crash_sample_self(); // DM_SAMPLE sampling (for diagnostics)
|
||||
uint32_t hmain = svc_thread_main_init(); // Main thread handle: x0 of nnosInitialize (stored in an sdk global, used by ThreadManagerImplByHorizon); x1 = pointer to the argument area
|
||||
// Copy Start step by step: _init_libc1 = __nnmusl_init_locks (initializes the stdio etc. locks as recursive locks; skipping it self-deadlocks on nested locking in putc) + __init_ssp.
|
||||
// The three callbacks passed in by rtld: A only sets an rtld flag (skipped), B = each module's DT_INIT (before nnMain), C = destructors (not done)
|
||||
@@ -31,11 +28,8 @@ static void guest_main(void) {
|
||||
((fnp)req("_ZN2nn2os11SetHostArgvEPPc"))(g_argv);
|
||||
LOG(">>> nninitStartup"); ((fn0)req("nninitStartup"))();
|
||||
((fn0)req("_init_libc2"))();
|
||||
dm_prof_mark("② SDK init (nnosInitialize → nninitStartup)");
|
||||
run_module_inits(); // rtld callback B: each module's DT_INIT (global constructors)
|
||||
dm_prof_mark("③ module init_arrays");
|
||||
{ void dm_guest_spawn_selftest(void); dm_guest_spawn_selftest(); } // DM_GUEST_SPAWN_TEST self-test of host-created guest threads (hle_guestthread.c)
|
||||
{ void dm_padmode_boot(void); dm_padmode_boot(); } // Touch overlay present: repoint the layout prefixes to the touch version (dm_padmode.c); must happen before the game builds its panels
|
||||
{ void dm_oodle_job_maybe(void); dm_oodle_job_maybe(); } // DM_OODLE_JOB: decode a job file with the game's own Oodle and exit (hle_oodle.c, for the offline shader tools)
|
||||
LOG(">>> nnMain"); ((fn0)req("nnMain"))();
|
||||
LOG("nnMain returned");
|
||||
@@ -66,7 +60,6 @@ static void *keyrace_thread(void *unused);
|
||||
static void host_preload(int argc, char **argv) {
|
||||
g_t0_ns = proc_start_mono_ns();
|
||||
flags_init();
|
||||
dm_prof_mark("⓪ process creation → host main (dyld)");
|
||||
if (argc < 2) die("usage: dmhost <dir> [mod...] [-- game arguments...]");
|
||||
// Time zone: the game converts absolute time to local time along two paths, both ending in the host libc -- nn::time::ToCalendarTime (hle_time.c, localtime_r) and the guest libc's
|
||||
// localtime_r / mktime (hle_table.c wires them straight to the host). So only when the host follows the system time zone do the original game's screens (chronicle discovery time etc.) show this machine's time.
|
||||
@@ -101,16 +94,10 @@ static void host_preload(int argc, char **argv) {
|
||||
load_modules(argv[1], names, n);
|
||||
uint64_t ta = now_ns();
|
||||
probe_install(); // DM_PROBE probes (for diagnostics)
|
||||
ticks_install(); // DM_TICKS per-tick state hash (tick-entry observation gate)
|
||||
{ void target_rank_install(void); target_rank_install(); } // Touch version auto-targeting Boss > elite > normal (hle_target.c)
|
||||
hid_keys_init(); // DM_KEYS scripted input (steps driven by UI messages must subscribe before the message appears)
|
||||
hid_keys_init(); // DM_KEYS scripted input
|
||||
uint64_t tb = now_ns();
|
||||
tact_chk_init(); // DM_TACTCHK per-block log (logging stops at character select)
|
||||
tact_prefetch_start(argv[1]); // Background pre-decode of boot blocks (on by default: bootlist.tsv in the build output directory; DM_TACT_PREFETCH=0 turns it off)
|
||||
uint64_t tc = now_ns();
|
||||
stubs_report();
|
||||
if (g_flag.bootprof) LOG("① breakdown (after loading): probes / tick gates / keys %.1f ms, prefetch start (reading the list) %.1f ms, stub report %.1f ms", (tb - ta) / 1e6, (tc - tb) / 1e6, (now_ns() - tc) / 1e6);
|
||||
dm_prof_mark("① host loading (read NSO / relocate / forced takeover)");
|
||||
if (g_flag.bootprof) LOG("① breakdown (after loading): probes / keys %.1f ms, stub report %.1f ms", (tb - ta) / 1e6, (now_ns() - tb) / 1e6);
|
||||
{ void loader_hash_log(void); loader_hash_log(); } // DM_LOADHASH=1: address-independent hash of the load result (loader.c)
|
||||
{ void dm_ue_malloc_install(void); dm_ue_malloc_install(); } // Unreal's allocator on a host malloc zone, for the builds whose constructor call is cut (hle_uemalloc.c)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Probes DM_PROBE (taken from d2r-mobile dm/host/diag.c, keeping only the probe itself): when the debugger needs a system authorization prompt on the Mac, use this to print registers and a backtrace at a guest address.
|
||||
// Probes DM_PROBE: when the debugger needs a system authorization prompt on the Mac, use this to print registers and a backtrace at a guest address.
|
||||
#include "dm.h"
|
||||
#include <TargetConditionals.h>
|
||||
#include <signal.h>
|
||||
|
||||
+26
-86
@@ -1,109 +1,49 @@
|
||||
DM_ROOT ?= $(HOME)/dm-work
|
||||
# dm/host unit tests in one command: make -C dm/host/tests run (takes seconds; build output goes to build/ and is not committed)
|
||||
PY ?= $(DM_ROOT)/build/cache/dm/venv/bin/python
|
||||
EXEFS ?= $(DM_ROOT)/data/switch/3.3.94012/Update/ExeFS
|
||||
# dm/host unit tests in one command: make -C dm/host/tests GAME=<base game title ID> run (takes seconds; build output goes to build/ and is not committed)
|
||||
B = build
|
||||
# game profile header (dm.h needs game_profile.h, generated by the host Makefile into ../gen/<profile>/)
|
||||
# tests that use the game profile need GAME=<base game title ID> (there is no default game); GINC only expands when used, so tests without the profile build as before
|
||||
GINC = $(if $(strip $(GAME)),-I../gen/$(GAME) -I../../games/$(GAME),$(error GAME is not set: make GAME=<base game title ID>: $(shell python3 ../../games/game.py list | awk -F'\t' '{printf "%s (%s) ", $$1, $$3}')))
|
||||
run: $(B)/t_textbox $(B)/t_padmap $(B)/t_touch $(B)/t_hid_receipt $(B)/t_hid_script $(B)/t_touch_mac $(B)/t_oe $(B)/t_fs $(B)/t_thread_leak $(B)/t_fs_case $(B)/t_romfs_validate $(B)/t_audio $(B)/t_audio_spsc $(B)/t_swkbd $(B)/t_altstack $(B)/t_movie $(B)/t_condvar $(B)/t_condvar_old $(B)/t_target_rank $(B)/t_hud_dead $(B)/t_namegen $(B)/t_modetx $(B)/t_unit_level $(B)/t_widget $(B)/t_doorspot $(B)/t_cloudsync $(B)/t_i18n $(B)/main.orig.bin $(B)/subsdk0.orig.bin
|
||||
python3 native_write_scan.py --selftest ..
|
||||
python3 mode_scan.py ../..
|
||||
python3 ../../ios/tools/dm_i18n.py --check
|
||||
python3 ../../i18n/scan_literals.py
|
||||
$(B)/t_textbox
|
||||
GAME_H = ../gen/$(GAME)/game_profile.h
|
||||
run: $(B)/t_touch $(B)/t_oe $(B)/t_fs $(B)/t_thread_leak $(B)/t_fs_case $(B)/t_audio $(B)/t_audio_spsc $(B)/t_swkbd $(B)/t_altstack $(B)/t_condvar $(B)/t_condvar_old
|
||||
$(B)/t_touch
|
||||
$(B)/t_hid_script 2>/dev/null
|
||||
$(B)/t_hid_receipt 2>/dev/null
|
||||
$(B)/t_touch_mac
|
||||
$(B)/t_oe 2>/dev/null
|
||||
$(B)/t_fs 2>/dev/null
|
||||
$(B)/t_thread_leak
|
||||
TMPDIR="$(abspath $(B))" $(B)/t_fs_case 2>/dev/null
|
||||
DM_ROMFS="$(DM_ROOT)/data/switch/3.3.94012/Update/RomFS" $(B)/t_romfs_validate
|
||||
$(B)/t_audio
|
||||
$(B)/t_audio_spsc
|
||||
$(B)/t_swkbd
|
||||
$(B)/t_altstack
|
||||
$(B)/t_movie
|
||||
$(B)/t_condvar
|
||||
$(B)/t_condvar_old
|
||||
$(B)/t_target_rank 2>/dev/null
|
||||
$(B)/t_hud_dead
|
||||
$(B)/t_namegen
|
||||
$(B)/t_modetx
|
||||
$(B)/t_padmap
|
||||
$(B)/t_unit_level
|
||||
$(B)/t_widget
|
||||
$(B)/t_doorspot
|
||||
TMPDIR="$(abspath $(B))" $(B)/t_cloudsync 2>/dev/null
|
||||
for m in en zh none sysen systw tags game; do TMPDIR="$(abspath $(B))" $(B)/t_i18n $$m || exit 1; done
|
||||
$(PY) movie_cut.py $(B)/main.orig.bin $(B)/subsdk0.orig.bin
|
||||
$(PY) oe_notify.py $(B)/main.orig.bin
|
||||
$(B)/t_i18n: t_i18n.c ../dm_i18n.c ../dm_i18n.h ../dm_i18n_tab.h | $(B)
|
||||
clang -O1 -Wall -Werror -I.. -o $@ t_i18n.c ../dm_i18n.c
|
||||
$(B)/t_hud_dead: t_hud_dead.c ../dm_hud.h | $(B)
|
||||
clang -O1 -Wall -Werror -I.. -o $@ t_hud_dead.c
|
||||
$(B)/t_textbox: t_textbox.c ../dm_textbox_core.c ../dm_textbox_core.h | $(B)
|
||||
clang -O1 -Wall -Werror -o $@ t_textbox.c ../dm_textbox_core.c
|
||||
$(B)/t_doorspot: t_doorspot.c ../dm_doorspot.h | $(B)
|
||||
clang -O1 -Wall -Werror -I.. -o $@ t_doorspot.c
|
||||
$(B)/t_widget: t_widget.c ../dm_widget.h | $(B)
|
||||
clang -O1 -Wall -Werror -I.. -o $@ t_widget.c
|
||||
$(B)/t_unit_level: t_unit_level.c ../dm_unit.h | $(B)
|
||||
clang -O1 -Wall -Werror -I.. -o $@ t_unit_level.c
|
||||
$(B)/t_padmap: t_padmap.c ../dm_padmap.h ../dm_input.h | $(B)
|
||||
clang -O1 -Wall -Werror -o $@ t_padmap.c
|
||||
$(B)/t_modetx: t_modetx.c ../dm_modetx.h | $(B)
|
||||
clang -O1 -Wall -Werror -o $@ t_modetx.c
|
||||
$(B)/t_namegen: t_namegen.c ../dm_namegen.c | $(B)
|
||||
clang -O1 -Wall -Werror -o $@ t_namegen.c ../dm_namegen.c
|
||||
$(B)/t_cloudsync: t_cloudsync.c ../dm_cloudsync.c ../dm.h | $(B)
|
||||
clang -O1 -Wall -Werror -I.. -o $@ t_cloudsync.c ../dm_cloudsync.c
|
||||
$(B)/t_touch: t_touch.c ../hle_touch.c ../front.h ../dm.h | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. -o $@ t_touch.c ../hle_touch.c
|
||||
$(B)/t_thread_leak: t_thread_leak.c ../thread.c ../dm.h | $(B)
|
||||
$(GAME_H):
|
||||
$(MAKE) -C .. game_profile GAME=$(GAME)
|
||||
$(B)/t_touch: t_touch.c ../hle_touch.c ../front.h ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_touch.c ../hle_touch.c
|
||||
$(B)/t_thread_leak: t_thread_leak.c ../thread.c ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_thread_leak.c
|
||||
$(B)/t_fs: t_fs.c t_fs_stub.c ../hle_fs.c ../hle.h ../dm.h ../dm_i18n.c | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_fs.c t_fs_stub.c ../dm_i18n.c
|
||||
$(B)/t_audio: t_audio.c ../hle_audio.c ../hle.h ../dm.h | $(B)
|
||||
$(B)/t_fs: t_fs.c t_fs_stub.c ../hle_fs.c ../hle_bigstack.c ../hle.h ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_fs.c t_fs_stub.c ../hle_bigstack.c
|
||||
$(B)/t_fs_case: t_fs_case.c ../hle_fs.c ../hle_bigstack.c ../hle.h ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_fs_case.c ../hle_bigstack.c
|
||||
$(B)/t_audio: t_audio.c ../hle_audio.c ../hle.h ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_audio.c -framework AudioToolbox
|
||||
$(B)/t_audio_spsc: t_audio_spsc.c ../hle_audio.c ../hle.h ../dm.h | $(B)
|
||||
$(B)/t_audio_spsc: t_audio_spsc.c ../hle_audio.c ../hle.h ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_audio_spsc.c -framework AudioToolbox
|
||||
$(B)/t_audio_spsc_tsan: t_audio_spsc.c ../hle_audio.c ../hle.h ../dm.h | $(B)
|
||||
clang -O1 -g -O1 -fsanitize=thread -Wno-unused-function -I.. -o $@ t_audio_spsc.c -framework AudioToolbox
|
||||
$(B)/t_swkbd: t_swkbd.c ../hle_swkbd.c ../hle.h ../dm.h | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. -o $@ t_swkbd.c ../hle_swkbd.c
|
||||
$(B)/t_movie: t_movie.c ../hle_movie.c ../hle.h ../dm.h | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. -o $@ t_movie.c ../hle_movie.c
|
||||
$(B)/t_altstack: t_altstack.c t_altstack_stub.c ../crash.c ../dm.h | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. -o $@ t_altstack.c t_altstack_stub.c ../crash.c
|
||||
$(B)/%.orig.bin: $(EXEFS)/% | $(B)
|
||||
$(PY) ../../tools/nso.py $< $@ > /dev/null
|
||||
$(B)/t_audio_spsc_tsan: t_audio_spsc.c ../hle_audio.c ../hle.h ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -g -O1 -fsanitize=thread -Wno-unused-function -I.. $(GINC) -o $@ t_audio_spsc.c -framework AudioToolbox
|
||||
$(B)/t_swkbd: t_swkbd.c ../hle_swkbd.c ../hle.h ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_swkbd.c ../hle_swkbd.c
|
||||
$(B)/t_altstack: t_altstack.c t_altstack_stub.c ../crash.c ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_altstack.c t_altstack_stub.c ../crash.c
|
||||
$(B)/t_oe: t_oe.c ../hle_oe.c ../hle.h ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_oe.c
|
||||
$(B)/t_condvar: t_condvar.c t_condvar_stub.c ../hle_thread.c ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -DDM_TEST_HOOKS -I.. $(GINC) -o $@ t_condvar.c t_condvar_stub.c ../hle_thread.c
|
||||
$(B)/t_condvar_old: t_condvar.c t_condvar_stub.c ../hle_thread.c ../dm.h $(GAME_H) | $(B)
|
||||
clang -O1 -Wno-unused-function -DDM_TEST_HOOKS -DDM_TEST_CV_OLD_ORDER -I.. $(GINC) -o $@ t_condvar.c t_condvar_stub.c ../hle_thread.c
|
||||
$(B):
|
||||
mkdir -p $@
|
||||
.PHONY: run tsan
|
||||
tsan: $(B)/t_audio_spsc_tsan
|
||||
$(B)/t_audio_spsc_tsan
|
||||
$(B)/t_condvar: t_condvar.c t_condvar_stub.c ../hle_thread.c ../dm.h | $(B)
|
||||
clang -O1 -Wno-unused-function -DDM_TEST_HOOKS -I.. -o $@ t_condvar.c t_condvar_stub.c ../hle_thread.c
|
||||
$(B)/t_condvar_old: t_condvar.c t_condvar_stub.c ../hle_thread.c ../dm.h | $(B)
|
||||
clang -O1 -Wno-unused-function -DDM_TEST_HOOKS -DDM_TEST_CV_OLD_ORDER -I.. -o $@ t_condvar.c t_condvar_stub.c ../hle_thread.c
|
||||
|
||||
$(B)/t_fs_case: t_fs_case.c ../hle_fs.c ../hle.h ../dm.h ../dm_i18n.c | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_fs_case.c ../dm_i18n.c
|
||||
$(B)/t_romfs_validate: t_romfs_validate.c ../romfs_validate.c ../romfs_validate.h ../dm_i18n.c ../dm_i18n_tab.h | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. -o $@ t_romfs_validate.c ../romfs_validate.c ../dm_i18n.c
|
||||
|
||||
$(B)/t_hid_script: t_hid_script.c t_touch_tick_stub.c ../hle_hid.c ../dm_cursor.c ../dm_hud.c ../dm_hud.h ../touch_mac.c ../../ios/Sources/dm_touch_controls.c ../hle.h ../dm.h ../dm_input.h ../front.h | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. -I../../ios/Sources -o $@ t_hid_script.c t_touch_tick_stub.c ../dm_cursor.c ../dm_hud.c ../touch_mac.c ../../ios/Sources/dm_touch_controls.c -lm
|
||||
|
||||
$(B)/t_oe: t_oe.c ../hle_oe.c ../hle.h ../dm.h | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_oe.c
|
||||
|
||||
$(B)/t_hid_receipt: t_hid_receipt.c t_touch_tick_stub.c ../hle_hid.c ../dm_cursor.c ../dm_hud.c ../dm_hud.h ../touch_mac.c ../../ios/Sources/dm_touch_controls.c ../hle.h ../dm.h ../dm_input.h ../front.h | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. -I../../ios/Sources -o $@ t_hid_receipt.c t_touch_tick_stub.c ../dm_cursor.c ../dm_hud.c ../touch_mac.c ../../ios/Sources/dm_touch_controls.c -lm
|
||||
$(B)/t_touch_mac: t_touch_mac.c ../touch_mac.c ../dm_cursor.c ../dm_hud.c ../dm_hud.h ../../ios/Sources/dm_touch_controls.c ../../ios/Sources/dm_touch_controls.h ../../host/dm_input.h | $(B) # the test #includes touch_mac.c and dm_touch_controls.c, which must be listed as dependencies (omitting them means source changes do not trigger a rebuild)
|
||||
clang -std=c11 -Wall -Wextra -Werror -I.. -I../../ios/Sources -o $@ t_touch_mac.c ../dm_cursor.c ../dm_hud.c -lm
|
||||
$(B)/t_target_rank: t_target_rank.c ../hle_target.c ../dm.h | $(B)
|
||||
clang -O1 -Wno-unused-function -I.. -o $@ t_target_rank.c ../hle_target.c
|
||||
@@ -1,20 +0,0 @@
|
||||
# Executes only the client STA gate/rule trampoline after the original GetStat; the end uses an explicit STOP stand-in, and neither the original getter nor the real Move speed is verified.
|
||||
import sys,struct
|
||||
from pathlib import Path
|
||||
from unicorn import Uc,UC_ARCH_ARM64,UC_MODE_ARM,UC_HOOK_CODE
|
||||
from unicorn.arm64_const import *
|
||||
b=Path(sys.argv[1]).read_bytes();u=Uc(UC_ARCH_ARM64,UC_MODE_ARM);u.mem_map(0,0x4000000);u.mem_write(0,b);u.mem_map(0x7000000,0x10000)
|
||||
UNIT=0x7001000;ctx={}
|
||||
def hook(u,a,n,d):
|
||||
if a in [0x4ac36c,0x4ac3f4]:ctx['target']=a;u.emu_stop()
|
||||
u.hook_add(UC_HOOK_CODE,hook);bad=0;n=0
|
||||
for kind in [0,1,2,3,4,5,6,99]:
|
||||
for sta in [0,1,22016,0xffffffff]:
|
||||
ctx.clear();u.mem_write(UNIT,struct.pack('<I',kind));u.reg_write(UC_ARM64_REG_X19,UNIT);u.reg_write(UC_ARM64_REG_W0,sta);u.reg_write(UC_ARM64_REG_SP,0x700f000);u.reg_write(UC_ARM64_REG_NZCV,0xa0000000)
|
||||
u.emu_start(0x4ac368,0x4000000,count=100)
|
||||
target=0x4ac3f4 if kind==0 or sta else 0x4ac36c
|
||||
ok=ctx.get('target')==target and u.reg_read(UC_ARM64_REG_W0)==sta and u.reg_read(UC_ARM64_REG_X19)==UNIT and u.reg_read(UC_ARM64_REG_SP)==0x700f000 and u.reg_read(UC_ARM64_REG_NZCV)==0xa0000000
|
||||
n+=1
|
||||
if not ok:bad+=1;print('FAIL type',kind,'STA',sta,'expected',hex(target),'→ got',ctx)
|
||||
print(f'CLIENT_STA_GATE {n} cases, {bad} failed; only type0 skips the STA auto Stop, non-player keeps the original zero / non-zero branch, original W0/X19/SP/NZCV unchanged; the end is a stand-in')
|
||||
sys.exit(bool(bad))
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Registration gate for controller / touch modes (run in the host unit tests, takes seconds). Reads source only, does not run the game.
|
||||
Three rules:
|
||||
1. Every pump on the long per-frame line in dm_cursor.c either carries "if (tm) " (touch-only: not run in controller mode or while a switch is in progress) or is on the list in the comment above it headed "Runs in both modes:";
|
||||
every *_pump defined in the file must also appear on this line or on that list -- a newly added pump registered in neither fails.
|
||||
2. The number of places that check the mode ad hoc (dm_padmode_current()) and that press keys / send messages on the player's behalf (fe_key / ui_auto_key / fe_send / dm_ui_key_pulse) is recorded per file in the two tables below, and may only decrease, never increase.
|
||||
To add one: first think through whether in the other mode it should stop, carry on as usual, or work differently, register it in dm_mode_items.h (for things to be cleaned up on a switch) or on the list above, then change the number here and state the reason in the commit message.
|
||||
3. Every enter / leave function registered in dm_mode_items.h must have a definition.
|
||||
Usage: mode_scan.py <dm directory>
|
||||
"""
|
||||
import re, sys, os, glob
|
||||
root = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(__file__), '..', '..')
|
||||
red = []
|
||||
def src(rel): return open(os.path.join(root, rel), encoding='utf-8').read()
|
||||
def strip_comments(s): return re.sub(r'//[^\n]*', '', re.sub(r'/\*.*?\*/', '', s, flags=re.S))
|
||||
|
||||
# ---- 1 ----
|
||||
cur = src('host/dm_cursor.c')
|
||||
m = re.search(r'^.*int tm = ui_touch_mode\(\);.*$', cur, re.M)
|
||||
if not m: red.append('cannot find the per-frame line in dm_cursor.c (int tm = ui_touch_mode();)')
|
||||
else:
|
||||
line = m.group(0)
|
||||
both_txt = re.search(r'Runs in both modes:(.*?)\nstatic int ui_touch_mode', cur, re.S)
|
||||
both = set(re.findall(r'\b(\w+_pump|ui_map_center|ui_panel_heal)\b', both_txt.group(1))) if both_txt else set()
|
||||
if not both: red.append('cannot find the "Runs in both modes:" list in dm_cursor.c')
|
||||
on_line = {}
|
||||
for mm in re.finditer(r'(if \(tm\) )?\b(\w+_pump|ui_map_center)\(', line): on_line[mm.group(2)] = bool(mm.group(1))
|
||||
for name, gated in sorted(on_line.items()):
|
||||
if not gated and name not in both: red.append('%s on the per-frame line neither carries if (tm) nor is on the "Runs in both modes" list' % name)
|
||||
if gated and name in both: red.append('%s carries if (tm) and is also on the "Runs in both modes" list; pick one' % name)
|
||||
for name in sorted(set(re.findall(r'^static void (\w+_pump)\(', cur, re.M))):
|
||||
if name not in on_line and name not in both: red.append('dm_cursor.c defines %s, but it is neither on the per-frame line nor on the "Runs in both modes" list' % name)
|
||||
|
||||
# ---- 2 ----
|
||||
PADMODE = {'host/dm_cursor.c': 8, 'host/dm_custom.c': 2, 'host/dm_kills.c': 2, 'host/dm_rift_ui.c': 4, 'host/dm_runeword.c': 3, 'host/dm_vault_ui.c': 1, 'host/dm_update.c': 2, 'host/hle_tact.c': 1}
|
||||
AUTOKEY = {'host/dm_cursor.c': 38, 'host/dm_rift_ui.c': 6, 'host/hle_hid.c': 2, 'ios/Sources/front_ios.m': 4}
|
||||
def count(pat, skip_defs):
|
||||
out = {}
|
||||
for f in sorted(glob.glob(os.path.join(root, 'host', '*.c')) + glob.glob(os.path.join(root, 'ios', 'Sources', '*.m')) + glob.glob(os.path.join(root, 'ios', 'Sources', '*.c'))):
|
||||
rel = os.path.relpath(f, root); s = strip_comments(open(f, encoding='utf-8').read()); n = 0
|
||||
for mm in re.finditer(pat, s):
|
||||
ls = s.rfind('\n', 0, mm.start()) + 1; head = s[ls:mm.start()]
|
||||
if skip_defs and re.search(r'(^|\s)(static\s+)?(void|int|uint64_t)\s+$', head): continue # A function definition / declaration itself does not count as a call
|
||||
n += 1
|
||||
if n: out[rel] = n
|
||||
return out
|
||||
for label, table, got in (('ad-hoc mode check dm_padmode_current()', PADMODE, count(r'\bdm_padmode_current\(\)', True)),
|
||||
('key presses / messages for the player', AUTOKEY, count(r'\b(fe_key|ui_auto_key|fe_send|dm_ui_key_pulse|dm_ui_key_pulse_alone|dm_ui_send|dm_ui_send_queue)\(', True))):
|
||||
if label.startswith('ad-hoc') and 'host/dm_padmode.c' in got: got.pop('host/dm_padmode.c')
|
||||
for rel in sorted(set(got) | set(table)):
|
||||
g, t = got.get(rel, 0), table.get(rel, 0)
|
||||
if g > t: red.append('%s: %s has %d now, the table records %d -- register the new ones first (see the top of this file), then change the table' % (label, rel, g, t))
|
||||
elif g < t: red.append('%s: %s has %d now, fewer than the %d in the table -- lower the number in the table (it may only go down)' % (label, rel, g, t))
|
||||
|
||||
# ---- 3 ----
|
||||
items = src('host/dm_mode_items.h'); allsrc = ''.join(open(f, encoding='utf-8').read() for f in glob.glob(os.path.join(root, 'host', '*.c')))
|
||||
for name, side, enter, leave in re.findall(r'X\("([^"]+)",\s*(\d),\s*(\w+),\s*(\w+)\)', items):
|
||||
for fn in (enter, leave):
|
||||
if not re.search(r'\bvoid %s\(void\)\s*\{' % fn, allsrc): red.append('dm_mode_items.h entry "%s": function %s is not defined' % (name, fn))
|
||||
|
||||
if red:
|
||||
print('mode registration gate [RED]: %d' % len(red)); [print(' ' + r) for r in red]; sys.exit(1)
|
||||
print('✓ mode registration gate: all %d per-frame pumps are classified, ad-hoc mode checks and key presses for the player are within the registered counts, all entry functions are defined' % (len(on_line) if m else 0))
|
||||
@@ -1,45 +0,0 @@
|
||||
# Executes the original ARM codec; only libm calls use explicit f32 stand-ins. Not a criterion for speed or for the game stopping.
|
||||
import math, struct, hashlib, sys
|
||||
from pathlib import Path
|
||||
from unicorn import Uc, UC_ARCH_ARM64, UC_MODE_ARM, UC_HOOK_CODE
|
||||
from unicorn.arm64_const import *
|
||||
P=Path(sys.argv[1]) if len(sys.argv)>1 else Path(__file__).with_name('build')/'main.orig.bin'
|
||||
b=P.read_bytes(); u=Uc(UC_ARCH_ARM64,UC_MODE_ARM)
|
||||
u.mem_map(0,0x4000000); u.mem_write(0,b)
|
||||
u.mem_map(0x7000000,0x20000)
|
||||
STOP,VEC,OUT,AMP,FLAG=0x7000000,0x7001000,0x7002000,0x7002010,0x7002020
|
||||
bits=lambda v:struct.unpack('<I',struct.pack('<f',v))[0]
|
||||
flt=lambda v:struct.unpack('<f',struct.pack('<I',v))[0]
|
||||
def hook(u,a,n,d):
|
||||
if a==STOP: u.emu_stop(); return
|
||||
fn={0x1b77270:lambda:math.atan2(flt(u.reg_read(UC_ARM64_REG_S0)),flt(u.reg_read(UC_ARM64_REG_S1))),
|
||||
0x1b77220:lambda:math.fmod(flt(u.reg_read(UC_ARM64_REG_S0)),flt(u.reg_read(UC_ARM64_REG_S1))),
|
||||
0x1b77130:lambda:math.cos(flt(u.reg_read(UC_ARM64_REG_S0))),
|
||||
0x1b77120:lambda:math.sin(flt(u.reg_read(UC_ARM64_REG_S0)))}.get(a)
|
||||
if fn:
|
||||
u.reg_write(UC_ARM64_REG_S0,bits(fn())); u.reg_write(UC_ARM64_REG_PC,u.reg_read(UC_ARM64_REG_LR))
|
||||
u.hook_add(UC_HOOK_CODE,hook)
|
||||
def call(pc):
|
||||
u.reg_write(UC_ARM64_REG_SP,0x701fff0);u.reg_write(UC_ARM64_REG_LR,STOP)
|
||||
u.emu_start(pc,STOP+4,count=3000)
|
||||
assert u.reg_read(UC_ARM64_REG_PC)==STOP
|
||||
n=0
|
||||
# Expectations hard-coded first: four-axis direction 10bit=512/768/0/256; magnitude 5bit=0/0/0/7/15/31/31.
|
||||
for dx,dy,direction in [(1,0,512),(0,1,768),(-1,0,0),(0,-1,256)]:
|
||||
packets=[]
|
||||
for amp,mag in [(-1,0),(0,0),(1/64,0),(.25,7),(.5,15),(1,31),(2,31)]:
|
||||
pair=[]
|
||||
for run in [0,1]:
|
||||
u.mem_write(VEC,struct.pack('<ff',dx,dy));u.reg_write(UC_ARM64_REG_X0,VEC);u.reg_write(UC_ARM64_REG_X1,run);u.reg_write(UC_ARM64_REG_S0,bits(amp));call(0x4cd4b0)
|
||||
packet=u.reg_read(UC_ARM64_REG_W0);want=direction|(mag<<10)|(run<<15)
|
||||
assert packet==want,('encode',dx,dy,amp,run,hex(packet),hex(want))
|
||||
pair.append(packet)
|
||||
u.mem_write(OUT,b'\xff'*0x30);u.reg_write(UC_ARM64_REG_X0,packet);u.reg_write(UC_ARM64_REG_X1,OUT);u.reg_write(UC_ARM64_REG_X2,AMP);u.reg_write(UC_ARM64_REG_X3,FLAG);call(0x4cd5f0)
|
||||
x,y=struct.unpack('<ff',u.mem_read(OUT,8));a=struct.unpack('<f',u.mem_read(AMP,4))[0];f=u.mem_read(FLAG,1)[0]
|
||||
assert abs(x-dx)<1e-6 and abs(y-dy)<1e-6,('direction',dx,dy,x,y)
|
||||
assert abs(a-mag/31)<1e-7 and f==run,('decode',amp,mag,a,f,run)
|
||||
n+=1
|
||||
assert pair[0]^pair[1]==0x8000
|
||||
print(f'axis ({dx},{dy}) direction code {direction}: 7 magnitudes × 2 walk/run bits, encode and decode all match')
|
||||
print('original image SHA256',hashlib.sha256(b).hexdigest())
|
||||
print(f'ORIGINAL_MOVE_CODEC OK {n}/{n}; the 28 pairs differ only in bit15, zero/tiny magnitudes quantize to 0; speed / stick release / real movement not tested')
|
||||
@@ -1,89 +0,0 @@
|
||||
# Static check of cutscene no-op playback (the game-side protocol in the header comment of hle_movie.c): check the key instructions and vtable slot names one by one on the original image; any mismatch is a failure.
|
||||
# Usage: movie_cut.py <main.orig.bin> <subsdk0.orig.bin> (.orig.bin is generated from ExeFS by dm/tools/nso.py)
|
||||
import struct, sys
|
||||
from capstone import *
|
||||
main, sub = open(sys.argv[1], 'rb').read(), open(sys.argv[2], 'rb').read()
|
||||
md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
|
||||
def ins(a): i = next(md.disasm(main[a:a + 4], a)); return f'{i.mnemonic} {i.op_str}'
|
||||
|
||||
# (offset, expected disassembly, meaning of this instruction in the chain) -- the game-side protocol that cutscene "no-op playback for the real duration" (plays nothing but lasts the real duration) depends on (header comment of hle_movie.c)
|
||||
CHAIN = [
|
||||
(0x898ed0, 'mov w1, #0x10', 'Extractor object 0x10 bytes: the cut stores its state pointer in the second word'),
|
||||
(0x899960, 'mov w1, #0x10', 'Decoder object 0x10 bytes: same as above'),
|
||||
(0x899780, 'ldr x8, [x8, #0x20]', 'Setup calls Extractor slot 4 GetTrackCount'),
|
||||
(0x89979c, 'b.lt #0x899928', 'track count < 1 skips track selection (the cut reports 0 tracks)'),
|
||||
(0x898f70, 'stp x24, x23, [x19, #0x1b8]', 'DecoderEvents[0..1] = {P+0x18, P+0x40}'),
|
||||
(0x899044, 'mov w8, #1', 'wait item kind at P+0x18 is 1 = input available'),
|
||||
(0x8990b8, 'mov w8, #2', 'wait item kind at P+0x40 is 2 = output available'),
|
||||
(0x8999e4, 'ldr x8, [x8, #0x28]', 'Setup calls Decoder slot 5 Start'),
|
||||
(0x899f54, 'cmp w21, #0', 'Open returns (Start status == 0): Start in the cut succeeds ⇒ playing'),
|
||||
(0x899f5c, 'cset w0, eq', ''),
|
||||
(0xbbfe40, 'bl #0x898230', 'the per-frame update calls the player update'),
|
||||
(0x89832c, 'ldrb w8, [x8, #0x489]', 'with the frame queue empty returns !P[+0x489] (finished flag) …'),
|
||||
(0x898334, 'cset w0, eq', '… not finished counts as playing'),
|
||||
(0x8992e0, 'cmp w21, #1', 'decoder thread: kind 1 takes the input path'),
|
||||
(0x8992e8, 'cmp w21, #2', 'decoder thread: kind 2 takes the output path'),
|
||||
(0x899508, 'ldr x8, [x8, #0x48]', 'input: Decoder slot 9 AcquireInputBufferIndex'),
|
||||
(0x899534, 'ldr x8, [x8, #0x50]', 'input: Decoder slot 10 GetInputBuffer'),
|
||||
(0x8995a8, 'ldr x8, [x8, #0x50]', 'input: Extractor slot 10 Read'),
|
||||
(0x8995b8, 'cmp w23, #2, lsl #12', 'Read returning 0x2000 = end of stream …'),
|
||||
(0x8995c0, 'ldrb w8, [x19, #0x12]', '… loop flag …'),
|
||||
(0x8995d4, 'ldr x8, [x8, #0x40]', '… if looping, Extractor slot 8 SeekTo (the cut re-arms its timer)'),
|
||||
(0x899620, 'mov w26, #4', '… otherwise sends flag 4 (end of stream)'),
|
||||
(0x899634, 'ldr x8, [x8, #0x60]', 'input: Extractor slot 12 GetSampleTime'),
|
||||
(0x899670, 'ldr x8, [x8, #0x58]', 'input: Decoder slot 11 SendInputBufferForDecode'),
|
||||
(0x89968c, 'ldr x8, [x8, #0x58]', 'input: Extractor slot 11 Advance'),
|
||||
(0x89932c, 'ldr x8, [x8, #0x60]', 'output: Decoder slot 12 AcquireOutputBufferIndex'),
|
||||
(0x899338, 'tst w0, #0xffffdfff', 'only status 0 / 0x2000 enters the frame queue (the cut returns 0x1008 with no output)'),
|
||||
(0x899cb4, 'cmp x9, #2', 'frames are dropped only with a queue ≥2 (a single end-of-stream item is never dropped)'),
|
||||
(0x89a650, 'ldr x8, [x8, #0x90]', 'frame fetch: Decoder slot 18 GetOutputBufferFormat (the cut returns non-zero)'),
|
||||
(0x89a65c, 'cmp w27, #4', 'flag 4 …'),
|
||||
(0x89a670, 'strb w8, [x25, #0x489]', '… sets finished ⇒ the game takes the normal finished path'),
|
||||
(0x899fdc, 'ldr x8, [x8, #0x30]', 'teardown calls Decoder slot 6 Stop (the cut cancels its timer)'),
|
||||
(0x899ff0, 'ldr x8, [x8, #8]', 'teardown calls Decoder slot 1 deleting destructor'),
|
||||
(0x89a004, 'ldr x8, [x8, #8]', 'teardown calls Extractor slot 1 deleting destructor'),
|
||||
]
|
||||
bad = 0
|
||||
for a, want, why in CHAIN:
|
||||
got = ins(a); ok = got == want; bad += not ok
|
||||
print(f'{"✓" if ok else "✗"} {a:#x} {got:28s} {"" if ok else "expected " + want} {why}')
|
||||
|
||||
# Vtable slot names (subsdk0 dynamic symbols + RELATIVE relocations): the cut vtable is ordered by these slot numbers
|
||||
m0 = struct.unpack_from('<I', sub, 4)[0]; dyn = m0 + struct.unpack_from('<i', sub, m0 + 4)[0]
|
||||
D = {}; o = dyn
|
||||
while True:
|
||||
t, v = struct.unpack_from('<qQ', sub, o); o += 16
|
||||
if t == 0: break
|
||||
D.setdefault(t, v)
|
||||
rel = {}
|
||||
for k in range(D[8] // 24):
|
||||
off, info, add = struct.unpack_from('<QQq', sub, D[7] + 24 * k)
|
||||
rel[off] = add if info & 0xffffffff == 1027 else ('sym', info >> 32) # RELATIVE gives an address; ABS64 / GLOB_DAT give a symbol number
|
||||
names, sym, symn = {}, {}, {}
|
||||
for k in range(1, (D[5] - D[6]) // 24):
|
||||
nm, info, oth, shn, val, sz = struct.unpack_from('<IBBHQQ', sub, D[6] + 24 * k)
|
||||
s = sub[D[5] + nm:sub.index(b'\0', D[5] + nm)].decode(); symn[k] = s
|
||||
if shn and val: names.setdefault(val, s); sym[s] = val
|
||||
def slot_name(r): return symn.get(r[1], '?') if isinstance(r, tuple) else names.get(r, '?')
|
||||
VT = [('_ZTVN5movie9ExtractorE', 4, '_ZN5movie9Extractor13GetTrackCountEPi'),
|
||||
('_ZTVN5movie9ExtractorE', 15, '_ZN5movie9Extractor13SetDataSourceEPNS_18ClientStreamReaderEPKc'),
|
||||
('_ZTVN5movie7DecoderE', 2, '_ZN5movie7Decoder9ConfigureEPNS_9MediaDataE'),
|
||||
('_ZTVN5movie7DecoderE', 3, '_ZN5movie7Decoder20RegisterClientEventsEPNS_13DecoderEventsE'),
|
||||
('_ZTVN5movie7DecoderE', 5, '_ZN5movie7Decoder5StartEv'),
|
||||
('_ZTVN5movie7DecoderE', 6, '_ZN5movie7Decoder4StopEv'),
|
||||
('_ZTVN5movie9ExtractorE', 8, '_ZN5movie9Extractor6SeekToEl'),
|
||||
('_ZTVN5movie9ExtractorE', 10, '_ZN5movie9Extractor4ReadEPNS_6BufferE'),
|
||||
('_ZTVN5movie9ExtractorE', 11, '_ZN5movie9Extractor7AdvanceEv'),
|
||||
('_ZTVN5movie9ExtractorE', 12, '_ZN5movie9Extractor13GetSampleTimeEPl'),
|
||||
('_ZTVN5movie7DecoderE', 9, '_ZN5movie7Decoder23AcquireInputBufferIndexEPiS1_'),
|
||||
('_ZTVN5movie7DecoderE', 10, '_ZN5movie7Decoder14GetInputBufferEiPNS_6BufferE'),
|
||||
('_ZTVN5movie7DecoderE', 11, '_ZN5movie7Decoder24SendInputBufferForDecodeEiiilj'),
|
||||
('_ZTVN5movie7DecoderE', 12, '_ZN5movie7Decoder24AcquireOutputBufferIndexEPiPlPjS1_'),
|
||||
('_ZTVN5movie7DecoderE', 15, '_ZN5movie7Decoder24ReleaseOutputBufferIndexEi'),
|
||||
('_ZTVN5movie7DecoderE', 18, '_ZN5movie7Decoder21GetOutputBufferFormatEiPNS_9MediaDataE')]
|
||||
for vt, slot, want in VT:
|
||||
got = slot_name(rel.get(sym[vt] + 16 + 8 * slot, 0)); ok = got == want; bad += not ok
|
||||
print(f'{"✓" if ok else "✗"} {vt} slot {slot} {got}{"" if ok else " expected " + want}')
|
||||
n = len(CHAIN) + len(VT)
|
||||
print(f'{"all passed" if not bad else "[RED]"}: {n - bad}/{n}')
|
||||
sys.exit(1 if bad else 0)
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Our own features must not write the original game's stash widgets (the chronicle vault is a separate page; lost-equipment incident: the chronicle page set the original grid invisible and the game never resets it, so the personal / shared pages looked completely empty).
|
||||
# Static scan: the chronicle page (all of dm_vault_ui.c) and the personal stash page numbers (ui_pages_pump in dm_cursor.c) must not contain original stash widget names -- these two places should only touch widgets under TouchVault / TouchPgPage.
|
||||
# There is another check at run time: dm_vault_ui.c vu_guard (before writing, walk the parent chain to check whether it is our widget; if not, report a failure and do not write).
|
||||
# Second check (do not write original-game widgets): the whole host (dm/host/*.c) must not write a widget's visible flag +0x50 / +0x51 directly, only inside guarded functions --
|
||||
# dm_cursor.c ui_show_ours, dm_vault_ui.c vu_show, dm_kills.c kb_show (a name not starting with Touch is reported as a failure and not written). To change visibility elsewhere, call them
|
||||
# Usage: native_write_scan.py <dm/host directory>; --selftest first verifies with a deliberately violating piece of text that the scan itself reports a failure
|
||||
import os, re, sys
|
||||
|
||||
NATIVE = ['grid', 'gold_amount', 'gold_withdraw', 'gold_max', 'basicstash_container', 'advancedstash_gems', 'advancedstash_materials', 'advancedstash_runes',
|
||||
'advancedstash_horadriccube', 'SharedStashTabContainer', 'StashNavigation', 'CurrentSharedStashText', 'background', 'PreviousSeasonToggleDisplay']
|
||||
|
||||
def scan(text, where):
|
||||
bad = []
|
||||
for no, line in enumerate(text.split('\n'), 1):
|
||||
code = line.split('//', 1)[0]
|
||||
for nm in NATIVE:
|
||||
if re.search(r'"%s"' % re.escape(nm), code): bad.append('%s:%d uses the original-game widget name "%s": %s' % (where, no, nm, line.strip()[:120]))
|
||||
return bad
|
||||
|
||||
VIS = re.compile(r'\+\s*0x5[01]\)\s*=(?!=)')
|
||||
GUARDED = ('ui_show_ours', 'vu_show', 'kb_show')
|
||||
def vis_scan(text, where): # lines that write the visible flag directly; those inside a guarded function (its definition line or its body) do not count
|
||||
bad, inside = [], None
|
||||
for no, line in enumerate(text.split('\n'), 1):
|
||||
m = re.match(r'static\s+\w+\s+(\w+)\(', line)
|
||||
if m: inside = m.group(1) if m.group(1) in GUARDED else None
|
||||
code = line.split('//', 1)[0]
|
||||
if VIS.search(code) and not inside: bad.append('%s:%d writes the visible flag directly (go through %s): %s' % (where, no, ' / '.join(GUARDED), line.strip()[:120]))
|
||||
if line.startswith('}'): inside = None
|
||||
return bad
|
||||
|
||||
# Third check (position guard): in dm_cursor.c, writing a widget rectangle (+0x70 x, +0x74 y, +0x78 width, +0x7c height) is allowed only in mv_offset (original-game widgets, allow list + record original value + must write back)
|
||||
# and in ui_move_ours (only names starting with Touch). Two forms are recognized: a direct *(int32_t *)(... + 0x7?) = ...; or, within the same function, first taking int32_t *name = (int32_t *)(... + 0x7?) and then writing name[k] = / *name =
|
||||
RECT_OK = ('mv_offset', 'ui_move_ours')
|
||||
RECT_PTR = re.compile(r'int32_t\s*\*\s*(\w+)\s*=\s*\(int32_t\s*\*\)\s*\([^;]*\+\s*0x7[0-9a-c]\b')
|
||||
RECT_DIRECT = re.compile(r'\*\s*\(int32_t\s*\*\)\s*\([^)]*\+\s*0x7[0-9a-c]\)\s*=(?!=)')
|
||||
def rect_scan(text, where):
|
||||
bad, fn, ptrs = [], None, set()
|
||||
for no, line in enumerate(text.split('\n'), 1):
|
||||
m = re.match(r'(?:static\s+)?\w+\s+\*?(\w+)\(', line)
|
||||
if m and not line.startswith(' '): fn, ptrs = m.group(1), set()
|
||||
code = line.split('//', 1)[0]
|
||||
for pm in RECT_PTR.finditer(code): ptrs.add(pm.group(1))
|
||||
code = RECT_PTR.sub('', code) # the declaration itself (int32_t *name = ...) does not count as a write
|
||||
hit = RECT_DIRECT.search(code) or any(re.search(r'(?<![\w.>])(?:\*\s*%s|%s\s*\[\s*\d\s*\])\s*=(?!=)' % (q, q), code) for q in ptrs)
|
||||
if hit and fn not in RECT_OK: bad.append('%s:%d writes a widget rectangle directly in %s (original-game widgets go through mv_offset, ours through ui_move_ours): %s' % (where, no, fn, line.strip()[:120]))
|
||||
return bad
|
||||
|
||||
def pages_pump(src):
|
||||
i = src.find('static void ui_pages_pump(')
|
||||
if i < 0: raise SystemExit('native_write_scan: ui_pages_pump not found in dm_cursor.c (if it was renamed, update this script)')
|
||||
j = src.find('\n}\n', i)
|
||||
return src[i:j]
|
||||
|
||||
def main():
|
||||
if '--selftest' in sys.argv:
|
||||
if not scan(' vu_show(vu_find(bk, "grid", 2), 0);', 'selftest') or scan(' vu_show(vu_find(vt, "TouchV_Back", 0), 1); // "grid" only in a comment', 'selftest') \
|
||||
or not vis_scan('static void f(uint64_t w) {\n *(uint8_t *)(w + 0x50) = 0;\n}', 'selftest') or vis_scan('static void ui_show_ours(uint64_t w, int on) {\n *(uint8_t *)(w + 0x50) = 1;\n}\n if (*(uint8_t *)(w + 0x50) == 1) x();', 'selftest') \
|
||||
or not rect_scan('static void f(uint64_t w) {\n int32_t *r = (int32_t *)(w + 0x70); r[0] = 5;\n}', 'selftest') or not rect_scan('static void g(uint64_t w) {\n *(int32_t *)(w + 0x74) = 3;\n}', 'selftest') \
|
||||
or rect_scan('static void k(uint64_t w) {\n int32_t *rx = (int32_t *)(w + 0x70), ww = *(int32_t *)(w + 0x78); if (*rx) x();\n}\nstatic void mv_offset(uint64_t w) {\n int32_t *v = (int32_t *)(w + 0x70); *v = 1;\n}\nstatic int h(float *r) {\n r[0] = 1.0f; int x = *(int32_t *)(w + 0x70); return x;\n}', 'selftest'):
|
||||
print('✗ native_write_scan selftest failed: a violation was not reported, or a clean line was'); return 1
|
||||
print('✓ native_write_scan selftest: violating lines are reported, names in comments do not count')
|
||||
host = next(a for a in sys.argv[1:] if not a.startswith('--'))
|
||||
bad = scan(open(os.path.join(host, 'dm_vault_ui.c'), encoding='utf-8').read(), 'dm_vault_ui.c')
|
||||
bad += scan(pages_pump(open(os.path.join(host, 'dm_cursor.c'), encoding='utf-8').read()), 'dm_cursor.c ui_pages_pump')
|
||||
srcs = sorted(f for f in os.listdir(host) if f.endswith('.c'))
|
||||
for f in srcs: bad += vis_scan(open(os.path.join(host, f), encoding='utf-8', errors='replace').read(), f)
|
||||
bad += rect_scan(open(os.path.join(host, 'dm_cursor.c'), encoding='utf-8').read(), 'dm_cursor.c')
|
||||
if bad:
|
||||
print('✗ our features touched original-game widgets (the Chronicle page / personal stash page numbers may only write widgets under TouchVault / TouchPgPage; the visible flag only through guarded functions):'); [print(' ' + b) for b in bad]; return 1
|
||||
print('✓ the Chronicle page and personal stash page numbers do not touch the original stash widgets (%d names scanned); none of the %d host source files writes the visible flag around the guards; dm_cursor.c writes widget rectangles only in mv_offset / ui_move_ours' % (len(NATIVE), len(srcs))); return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -1,57 +0,0 @@
|
||||
# Runs the original image main+e10d30 directly; only the system queries and the resize callback are replaced, the consumer function is not rewritten.
|
||||
# Hand-written expectations: at the end of the message loop focus/op/perf each hold the current value; the target size corresponds to the performance mode; the same target is resized only once.
|
||||
import struct,itertools,hashlib,sys
|
||||
from pathlib import Path
|
||||
from unicorn import Uc,UC_ARCH_ARM64,UC_MODE_ARM,UC_HOOK_CODE
|
||||
from unicorn.arm64_const import *
|
||||
P=Path(sys.argv[1]);b=P.read_bytes()
|
||||
u=Uc(UC_ARCH_ARM64,UC_MODE_ARM);u.mem_map(0,0x4000000);u.mem_write(0,b);u.mem_map(0x7000000,0x2000);u.mem_map(0x7100000,0x10000)
|
||||
if sys.argv[2:] == ['--negative']:
|
||||
u.mem_write(0xe10e24,bytes.fromhex('1f2003d5')) # only in the negative control is the same-size skip removed, so repeated notifications resize repeatedly
|
||||
print('NEGATIVE: local copy of the original function, e10e24 b.eq→nop, the source image is unchanged')
|
||||
u.mem_write(0x3211718,struct.pack('<Q',0x7001001));u.mem_write(0x7001000,struct.pack('<Q',0x7001100));u.mem_write(0x7001110,struct.pack('<Q',0x7001200))
|
||||
state={}
|
||||
def hook(u,a,n,d):
|
||||
if a==0x7001800:u.emu_stop();return
|
||||
if a==0x1b77890:
|
||||
q=state['q'];out=u.reg_read(UC_ARM64_REG_X0)
|
||||
if q:u.mem_write(out,struct.pack('<I',q.pop(0)));u.reg_write(UC_ARM64_REG_X0,1)
|
||||
else:u.reg_write(UC_ARM64_REG_X0,0)
|
||||
elif a==0x1b778a0:u.reg_write(UC_ARM64_REG_X0,state['mode'])
|
||||
elif a==0x1b778b0:u.reg_write(UC_ARM64_REG_X0,state['focus'])
|
||||
elif a==0x1b778c0:u.reg_write(UC_ARM64_REG_X0,state['mode'])
|
||||
elif a==0x7001200:
|
||||
w=struct.unpack('<Q',u.mem_read(u.reg_read(UC_ARM64_REG_X3),8))[0];h=struct.unpack('<Q',u.mem_read(u.reg_read(UC_ARM64_REG_X4),8))[0]
|
||||
state['resize'].append((w,h));u.reg_write(UC_ARM64_REG_X0,0)
|
||||
else:return
|
||||
u.reg_write(UC_ARM64_REG_PC,u.reg_read(UC_ARM64_REG_LR))
|
||||
u.hook_add(UC_HOOK_CODE,hook)
|
||||
def run(q,mode,focus,canvas=(1600,900),target=(0,0)):
|
||||
state.update(q=list(q),mode=mode,focus=focus,resize=[])
|
||||
u.mem_write(0x211da30,struct.pack('<II',*canvas));u.mem_write(0x211da38,struct.pack('<II',*target))
|
||||
for a in [0x315e628,0x315e630,0x32137e8,0x32137ec]:u.mem_write(a,b'\x00'*8)
|
||||
u.reg_write(UC_ARM64_REG_SP,0x710fff0);u.reg_write(UC_ARM64_REG_LR,0x7001800)
|
||||
try:u.emu_start(0xe10d30,0x7001804,count=20000)
|
||||
except Exception:
|
||||
print("FAIL PC",hex(u.reg_read(UC_ARM64_REG_PC)),"LR",hex(u.reg_read(UC_ARM64_REG_LR)),"SP",hex(u.reg_read(UC_ARM64_REG_SP)));raise
|
||||
assert u.reg_read(UC_ARM64_REG_PC)==0x7001800,'did not return'
|
||||
return (struct.unpack('<I',u.mem_read(0x315e628,4))[0],u.mem_read(0x315e630,2),u.mem_read(0x32137e8,1),u.mem_read(0x32137ec,1),struct.unpack('<II',u.mem_read(0x211da38,8)),list(state['resize']))
|
||||
checks=0
|
||||
print('original image SHA256',hashlib.sha256(b).hexdigest())
|
||||
for focus,mode in [(1,0),(3,1)]:
|
||||
for order in itertools.permutations([15,30,31]):
|
||||
full=[m for m in order for _ in range(11)] # 33 messages, really exceeding the old capacity of 32; each message still carries no historical value
|
||||
a=run(full,mode,focus);c=run(order,mode,focus);checks+=1
|
||||
size=(1280,720) if mode==0 else (1600,900)
|
||||
want=(focus,bytes([mode,mode]),bytes([mode]),bytes([mode]),size,[size])
|
||||
ok=a==c==want
|
||||
print(('✓' if ok else '✗')+f' order {order} focus={focus} mode={mode}: 33 raw notifications / 3 state notifications → same write-back and exactly 1 resize {c}')
|
||||
if not ok:raise SystemExit(1)
|
||||
for mode in [0,1]:
|
||||
size=(1280,720) if mode==0 else (1600,900)
|
||||
for name,canvas,target,out,resizes in [('target already matches',(1600,900),size,size,[]),('capacity too small',(1,1),(777,333),(777,333),[]),('width matches, height differs',(1600,900),(size[0],333),size,[size])]:
|
||||
a=run([15]*11+[30]*11+[31]*11,mode,2,canvas,target);c=run([15,30,31],mode,2,canvas,target);checks+=1
|
||||
want=(2,bytes([mode,mode]),bytes([mode]),bytes([mode]),out,resizes)
|
||||
ok=a==c==want;print(('✓' if ok else '✗')+f' {name} mode={mode} focus=2: 33/3 notifications write back as hand-written expectations, resize={len(c[-1])}')
|
||||
if not ok:raise SystemExit(1)
|
||||
print(f'CONSUMER_EQUIV OK {checks}/{checks}: original ARM consumer function, 33→3 notifications, hand-written state/size/resize')
|
||||
@@ -1,51 +0,0 @@
|
||||
# Independent registration check of the new rule's build output; does not execute the game and does not trust the rewriter's own digest.
|
||||
import sys,struct,hashlib
|
||||
from pathlib import Path
|
||||
D=Path(sys.argv[1]); b=(D/'main.patched.bin').read_bytes(); orig=Path(sys.argv[2]).read_bytes()
|
||||
headers={};blocks=[]
|
||||
try:
|
||||
for line in (D/'main.rules.tsv').read_text().splitlines():
|
||||
if line.startswith('# '):k,v=line[2:].split('\t',1);headers[k]=v
|
||||
else:blocks.append(line.split('\t'))
|
||||
assert headers['RULESET']=='touch-run-v4'
|
||||
assert headers['BUILD_ID']=='db0507f36b84200ac05f75d76ed5026f'
|
||||
assert headers['ORIGINAL_SHA256']==hashlib.sha256(orig).hexdigest()=='6843ab6909f96e1558b4b73b0fa807f3873409120cf3e1ef8a86dbebb275ee27'
|
||||
assert headers['IMAGE_SHA256']==hashlib.sha256(b).hexdigest()
|
||||
expected={0x2c620c:0x52800029,0x2c6210:0xb90e5109,0x369a90:None,0x369980:0xd503201f,0x2b2c74:0xd503201f,0x2b2dc0:0xd503201f,0x2b30fc:0x52800021,0x2b3104:0x52800021,0x6dc2c4:0x52800062,0x6dce90:0xd503201f,0x6dcec0:None,0x6c6648:None,0x6c6290:None,0x4ac368:None,0x6c6580:None}
|
||||
cuts={}
|
||||
for line in (D/'main.cuts.tsv').read_text().splitlines():
|
||||
if line.startswith('#'):continue
|
||||
off,old,new,scope,why=line.split('\t',4)
|
||||
if why.startswith('touch-run-v4: '):cuts[int(off,16)]=(old,new,scope)
|
||||
assert set(cuts)==set(expected),('cut set',set(cuts)^set(expected))
|
||||
def w(at):return struct.unpack_from('<I',b,at)[0]
|
||||
def branch(at):
|
||||
v=w(at);assert v&0xfc000000==0x14000000;imm=v&0x3ffffff
|
||||
return at+4*((imm^0x2000000)-0x2000000)
|
||||
for off,newword in expected.items():
|
||||
old,new,scope=cuts[off];assert scope=='all';assert orig[off:off+4]==bytes.fromhex(old);assert b[off:off+4]==bytes.fromhex(new)
|
||||
if newword is not None:assert w(off)==newword
|
||||
assert branch(0x369a90)==0x369900 and branch(0x6dcec0)==0x6dce50
|
||||
assert len(blocks)==4
|
||||
fixheaders={}
|
||||
for line in (D/'main.fixups.tsv').read_text().splitlines():
|
||||
if line.startswith('# '):k,v=line[2:].split('\t');fixheaders[k]=int(v,0)
|
||||
po=fixheaders['PATCH_OFF'];seen=set()
|
||||
for off,raw,sha,why in blocks:
|
||||
off=int(off,16);body=bytes.fromhex(raw);assert po<=off<len(b) and off+len(body)<=len(b);assert b[off:off+len(body)]==body;assert hashlib.sha256(body).hexdigest()==sha
|
||||
src=next(s for s in [0x6c6648,0x6c6290,0x4ac368,0x6c6580] if branch(s)==off);assert src not in seen;seen.add(src)
|
||||
if src==0x6c6580:
|
||||
assert len(body)==24 and w(off)==0x35000088 and w(off+4)==0x51000849 and w(off+8)==0x35000049 and w(off+12)==0x52800062 and w(off+16)==0x51000849 and branch(off+20)==0x6c6584
|
||||
continue
|
||||
assert w(off)==0xb9400268
|
||||
assert w(off+4)==(0x34000048 if src==0x4ac368 else 0x35000048)
|
||||
if src==0x6c6648:
|
||||
assert len(body)==24 and branch(off+8)==0x6c664c and w(off+12)==0x34000040 and branch(off+16)==0x6c664c and branch(off+20)==0x6c6654
|
||||
elif src==0x6c6290:
|
||||
assert len(body)==20 and branch(off+8)==0x6c62ec and w(off+12)==0x710006df and branch(off+16)==0x6c6294
|
||||
else:
|
||||
assert len(body)==28 and branch(off+8)==off+16 and branch(off+12)==0x4ac3f4 and w(off+16)==0x34000040 and branch(off+20)==0x4ac3f4 and branch(off+24)==0x4ac36c
|
||||
assert seen=={0x6c6648,0x6c6290,0x4ac368,0x6c6580}
|
||||
print('RUN_MANIFEST OK original/image hash, buildid, 15-cut whitelist, 4 trampolines hash/range/structure/entry B')
|
||||
except (AssertionError,KeyError,OSError,ValueError,StopIteration) as e:
|
||||
print('RUN_MANIFEST FAIL',type(e).__name__,str(e));sys.exit(1)
|
||||
@@ -1,88 +0,0 @@
|
||||
# Local original-ARM contract of the new rule; does not replace the speed/real action matrix.
|
||||
import sys,struct,hashlib
|
||||
from pathlib import Path
|
||||
from unicorn import Uc,UC_ARCH_ARM64,UC_MODE_ARM,UC_HOOK_CODE
|
||||
from unicorn.arm64_const import *
|
||||
orig=Path(sys.argv[1]).read_bytes(); candidate=Path(sys.argv[2]).read_bytes()
|
||||
U,P,R,A,L=0x7001000,0x7002000,0x7003000,0x7004000,0x7005000
|
||||
fails=[]; total=0
|
||||
class Machine:
|
||||
def __init__(self,blob):
|
||||
self.u=u=Uc(UC_ARCH_ARM64,UC_MODE_ARM);u.mem_map(0,0x4000000);u.mem_write(0,blob);u.mem_map(0x7000000,0x20000)
|
||||
for addr,v in [(U+0x38,P),(P,R),(P+0x20,R),(R+0x18,A),(A+0x90,L)]:u.mem_write(addr,struct.pack('<Q',v))
|
||||
self.sta=0;self.calls=[];self.stop=0
|
||||
u.hook_add(UC_HOOK_CODE,self.hook)
|
||||
def hook(self,u,a,n,d):
|
||||
if a==self.stop:u.emu_stop();return
|
||||
if a==0x5b9290:self.calls.append(('get',10));u.reg_write(UC_ARM64_REG_X0,self.sta)
|
||||
elif a==0x5a13a0:u.reg_write(UC_ARM64_REG_X0,0)
|
||||
elif a==0x5bc5a0:
|
||||
assert u.reg_read(UC_ARM64_REG_W1)==10
|
||||
v=u.reg_read(UC_ARM64_REG_W2);v=(v^0x80000000)-0x80000000
|
||||
self.calls.append(('add',v));self.sta+=v
|
||||
elif a==0x5bc1f0:
|
||||
assert u.reg_read(UC_ARM64_REG_W1)==10
|
||||
self.calls.append(('set',u.reg_read(UC_ARM64_REG_W2)));self.sta=u.reg_read(UC_ARM64_REG_W2)
|
||||
elif a==0x6c6550 and self.stop==0x6c62ec:self.calls.append(('downgrade',u.reg_read(UC_ARM64_REG_W2)))
|
||||
elif a==0x45f9d0:u.reg_write(UC_ARM64_REG_X0,U)
|
||||
elif a==0x460fd0:
|
||||
v=u.reg_read(UC_ARM64_REG_W1);self.calls.append(('mode',v));u.mem_write(U+0xc,struct.pack('<I',v));u.reg_write(UC_ARM64_REG_X0,0)
|
||||
elif a in [0x4bbc50,0x5ca8a0]:
|
||||
v=u.reg_read(UC_ARM64_REG_W1);self.calls.append(('mode',v));u.mem_write(U+0xc,struct.pack('<I',v))
|
||||
elif a==0x2ceab0:self.calls.append(('packet',u.mem_read(u.reg_read(UC_ARM64_REG_X0),1)[0]))
|
||||
elif a==0x5a7510:pass
|
||||
else:return
|
||||
u.reg_write(UC_ARM64_REG_PC,u.reg_read(UC_ARM64_REG_LR))
|
||||
def go(self,start,stop):
|
||||
self.stop=stop;self.u.reg_write(UC_ARM64_REG_SP,0x701fff0)
|
||||
self.u.emu_start(start,stop+4,count=3000)
|
||||
assert self.u.reg_read(UC_ARM64_REG_PC)==stop,hex(self.u.reg_read(UC_ARM64_REG_PC))
|
||||
def selector(self,typ,town,sta,mode):
|
||||
u=self.u;self.sta=sta;self.calls=[];u.mem_write(U,struct.pack('<I',typ));u.mem_write(L+0x1f8,struct.pack('<I',1 if town else 2))
|
||||
u.reg_write(UC_ARM64_REG_X0,0x7006000);u.reg_write(UC_ARM64_REG_X1,U);u.reg_write(UC_ARM64_REG_X2,mode)
|
||||
self.go(0x6c6550,0x6c6678);return u.reg_read(UC_ARM64_REG_W22)
|
||||
def drain(self,typ,sta):
|
||||
self.sta=sta;self.calls=[];u=self.u;u.mem_write(U,struct.pack('<I',typ));u.reg_write(UC_ARM64_REG_X19,U);u.reg_write(UC_ARM64_REG_X20,0x7006000);u.reg_write(UC_ARM64_REG_W22,2)
|
||||
self.go(0x6c6290,0x6c62ec);return self.sta,list(self.calls)
|
||||
def check(label,actual,want):
|
||||
global total
|
||||
total+=1
|
||||
if actual!=want:fails.append((label,actual,want))
|
||||
a,b=Machine(orig),Machine(candidate)
|
||||
for typ in [0,1,2,3,4,6]:
|
||||
for town in [False,True]:
|
||||
for sta in [0,1,22272]:
|
||||
for mode in [1,2,3,6,7,10,17,19]:
|
||||
old=a.selector(typ,town,sta,mode)
|
||||
want=3 if typ==0 and mode in (2,3) else old
|
||||
check(f'select type={typ} town={town} sta={sta} mode={mode}',b.selector(typ,town,sta,mode),want)
|
||||
for typ in [0,1,2,3,4,6]:
|
||||
for sta in [0,1,22272]:
|
||||
old=a.drain(typ,sta);want=(sta,[]) if typ==0 else old
|
||||
check(f'cost type={typ} sta={sta}',b.drain(typ,sta),want)
|
||||
for pref in [0,1]:
|
||||
for hyst in [0,1]:
|
||||
for latch in [0,1]:
|
||||
u=b.u;u.reg_write(UC_ARM64_REG_W10,pref);u.reg_write(UC_ARM64_REG_W8,hyst);u.reg_write(UC_ARM64_REG_W9,latch)
|
||||
b.go(0x2b30e8,0x2b3108);check(f'client select {pref}/{hyst}/{latch}',u.reg_read(UC_ARM64_REG_W1),1)
|
||||
u=b.u;b.go(0x2c6208,0x2c6214);check('initial run preference',struct.unpack('<I',u.mem_read(0x2d20e50,4))[0],1)
|
||||
# The build cut entry redirection check does not claim full On execution.
|
||||
for start,target in [(0x369a90,0x369900),(0x6dcec0,0x6dce50)]:
|
||||
u=b.u;u.reg_write(UC_ARM64_REG_SP,0x701fff0);u.emu_start(start,0x4000000,count=1);check(f'off redirect {start:x}',u.reg_read(UC_ARM64_REG_PC),target)
|
||||
# The real On/Off functions are executed in full; lookup/stat/animation switch/packet send are explicit peripheral stand-ins.
|
||||
for typ in [0,1]:
|
||||
for mode in [0,1,2,3,4,5,6,7,10,17,19]:
|
||||
for sta in [0,1,22272]:
|
||||
for entry in [0x369900,0x369a90,0x6dce50,0x6dcec0]:
|
||||
u=b.u;b.sta=sta;b.calls=[];u.mem_write(U,struct.pack('<I',typ));u.mem_write(U+0xc,struct.pack('<I',mode))
|
||||
u.mem_write(0x2d20ba1,b'\0');u.mem_write(0x2d20e50,struct.pack('<II',0,0));u.reg_write(UC_ARM64_REG_X1,U);u.reg_write(UC_ARM64_REG_W3,1);u.reg_write(UC_ARM64_REG_LR,0x7000100)
|
||||
b.go(entry,0x7000100);want=3 if typ==0 and mode==2 else mode
|
||||
actual=struct.unpack('<I',u.mem_read(U+0xc,4))[0]
|
||||
check(f'toggle entry={entry:x} type={typ} mode={mode} sta={sta}',actual,want)
|
||||
packets=[v for k,v in b.calls if k=='packet']
|
||||
wp=[0x49] if entry<0x600000 and typ==0 and mode==2 else []
|
||||
check(f'toggle packets entry={entry:x} type={typ} mode={mode} sta={sta}',packets,wp)
|
||||
print('ORIGINAL_SHA256',hashlib.sha256(orig).hexdigest());print('CANDIDATE_SHA256',hashlib.sha256(candidate).hexdigest())
|
||||
for label,actual,want in fails[:30]:print(f'FAIL {label}: expected {want} → got {actual}')
|
||||
print(f'RUN_RULE_LOCAL {total} cases, {len(fails)} failed; selector / deduction window / encoding choice / preference / entry redirect; not a full action / speed criterion')
|
||||
sys.exit(bool(fails))
|
||||
@@ -1,28 +0,0 @@
|
||||
# disabled_bit_guard only tests in-place eligibility; a hand-made local memory fixture, which does not prove the legitimate source of the control lock of a living player.
|
||||
# Executes the input validity checks and dispatch of the original full 42 handler; the decoder/downstream movement/fatal log are explicit peripheral stand-ins.
|
||||
import sys,struct
|
||||
from pathlib import Path
|
||||
from unicorn import *
|
||||
from unicorn.arm64_const import *
|
||||
b=Path(sys.argv[1]).read_bytes();u=Uc(UC_ARCH_ARM64,UC_MODE_ARM);u.mem_map(0,0x4000000);u.mem_write(0,b);u.mem_map(0x7000000,0x20000)
|
||||
G,U,D,C,P,PK,STOP=0x7001000,0x7002000,0x7003000,0x7004000,0x7005000,0x7006000,0x701f000
|
||||
ctx={}
|
||||
def hook(u,a,n,d):
|
||||
if a==STOP:ctx['returned']=True;u.emu_stop();return
|
||||
if a==0x4cd5f0:
|
||||
v=u.reg_read(UC_ARM64_REG_W0);u.mem_write(u.reg_read(UC_ARM64_REG_X1),struct.pack('<ff',1,0));u.mem_write(u.reg_read(UC_ARM64_REG_X2),struct.pack('<f',1));u.mem_write(u.reg_read(UC_ARM64_REG_X3),bytes([v>>15]));u.reg_write(UC_ARM64_REG_PC,u.reg_read(UC_ARM64_REG_LR))
|
||||
elif a==0x6cd670:ctx['move']=u.reg_read(UC_ARM64_REG_W2);u.reg_write(UC_ARM64_REG_W0,0);u.reg_write(UC_ARM64_REG_PC,u.reg_read(UC_ARM64_REG_LR))
|
||||
elif a==0xde1850:ctx['fatal']=True;u.emu_stop()
|
||||
u.hook_add(UC_HOOK_CODE,hook);bad=0;n=0
|
||||
for bit in [0,1]:
|
||||
for case in ['valid','badlen','dead0','dead17','disabled_bit_guard','far','nonplayer']:
|
||||
ctx.clear();u.mem_write(G,b'\0'*0x1000);u.mem_write(U,b'\0'*0x400);u.mem_write(D,b'\0'*0x200);u.mem_write(C,b'\0'*0x1000);u.mem_write(P,b'\0'*0x300)
|
||||
u.mem_write(U,struct.pack('<I',int(case=='nonplayer')));u.mem_write(U+12,struct.pack('<I',0 if case=='dead0'else 17 if case=='dead17'else 1));u.mem_write(U+0x10,struct.pack('<Q',D));u.mem_write(D+0xf0,struct.pack('<Q',C));u.mem_write(U+0x38,struct.pack('<Q',P));u.mem_write(P+2,struct.pack('<H',5826));u.mem_write(P+6,struct.pack('<H',4520));u.mem_write(U+0x126,bytes([int(case=='disabled_bit_guard')]));u.mem_write(G+0x170,struct.pack('<I',421));u.mem_write(PK,struct.pack('<BHHH',0x42,512|(31<<10)|(bit<<15),5926 if case=='far'else 5826,4520))
|
||||
for r,v in [(UC_ARM64_REG_X0,G),(UC_ARM64_REG_X1,U),(UC_ARM64_REG_X2,PK),(UC_ARM64_REG_X3,6 if case=='badlen'else 7),(UC_ARM64_REG_SP,0x701fff0),(UC_ARM64_REG_LR,STOP)]:u.reg_write(r,v)
|
||||
u.emu_start(0x6dc270,STOP+4,count=5000)
|
||||
wantmove=3 if case=='valid'else None;wantstatus=3 if case=='badlen'else 1 if case in ['dead0','dead17','disabled_bit_guard','far']else 0
|
||||
ok=(ctx.get('fatal',False) or ctx.get('returned',False)) and ctx.get('move')==wantmove and (ctx.get('fatal',False)==(case=='nonplayer')) and (case=='nonplayer'or u.reg_read(UC_ARM64_REG_W0)==wantstatus)
|
||||
n+=1
|
||||
if not ok:bad+=1;print('FAIL',bit,case,'expected move',wantmove,'→ got',ctx,'status',u.reg_read(UC_ARM64_REG_W0))
|
||||
print(f'SERVER42_RULE {n} cases, {bad} failed; real RET to STOP / original fatal-halt criterion; bit0/1 legal requests Run3, length / death / disable bit / coordinates / non-player original checks; real downstream speed not included')
|
||||
sys.exit(bool(bad))
|
||||
@@ -1,21 +0,0 @@
|
||||
# Independent rule test of the generic movement dispatch index point; the entry has already passed the original null/type5 gate, the end point is an explicit stand-in, and the real path is not verified.
|
||||
import sys,struct
|
||||
from pathlib import Path
|
||||
from unicorn import Uc,UC_ARCH_ARM64,UC_MODE_ARM,UC_HOOK_CODE
|
||||
from unicorn.arm64_const import *
|
||||
u=Uc(UC_ARCH_ARM64,UC_MODE_ARM);u.mem_map(0,0x4000000);u.mem_write(0,Path(sys.argv[1]).read_bytes());u.mem_map(0x7000000,0x10000);ctx={};bad=0;n=0
|
||||
G,UNIT,SP=0x7001000,0x7002000,0x700f000
|
||||
def hook(u,a,n,d):
|
||||
if a==0x6c6584:ctx['arrived']=True;u.emu_stop()
|
||||
u.hook_add(UC_HOOK_CODE,hook)
|
||||
for kind in [0,1,2,3,4,5,6,99]:
|
||||
for mode in [0,1,2,3,5,6,10,0xffffffff]:
|
||||
ctx.clear();u.mem_write(UNIT,struct.pack('<I',kind))
|
||||
for r,v in [(UC_ARM64_REG_X0,G),(UC_ARM64_REG_X19,UNIT),(UC_ARM64_REG_W8,kind),(UC_ARM64_REG_W2,mode),(UC_ARM64_REG_SP,SP),(UC_ARM64_REG_NZCV,0xa0000000)]:u.reg_write(r,v)
|
||||
u.emu_start(0x6c6580,0x4000000,count=100)
|
||||
want=3 if kind==0 and mode==2 else mode
|
||||
ok=ctx.get('arrived',False) and u.reg_read(UC_ARM64_REG_W2)==want and u.reg_read(UC_ARM64_REG_W9)==((want-2)&0xffffffff) and u.reg_read(UC_ARM64_REG_W8)==kind and u.reg_read(UC_ARM64_REG_X0)==G and u.reg_read(UC_ARM64_REG_X19)==UNIT and u.reg_read(UC_ARM64_REG_SP)==SP and u.reg_read(UC_ARM64_REG_NZCV)==0xa0000000
|
||||
n+=1
|
||||
if not ok:bad+=1;print('FAIL type',kind,'request',mode,'expected normalized',want,'→ got',u.reg_read(UC_ARM64_REG_W2),'arrived',ctx.get('arrived',False))
|
||||
print(f'SERVER_WALK_REQUEST {n} cases, {bad} failed; only type0 request 2 is normalized to 3 and the index recomputed, other requests / types and W0/X19/W8/SP/NZCV unchanged; stand-in at the end')
|
||||
sys.exit(bool(bad))
|
||||
@@ -1,3 +1,5 @@
|
||||
#include <stdint.h>
|
||||
typedef struct module_t module_t;
|
||||
// the loader's module table (loader.c), which crash.c's report walks: the unit test has no modules
|
||||
#include "../dm.h"
|
||||
module_t g_mod[MAX_MOD];
|
||||
int g_nmod;
|
||||
module_t *module_of(const void *p) { (void)p; return 0; }
|
||||
@@ -1,181 +0,0 @@
|
||||
// iCloud sync core unit test (takes seconds): dm_cloudsync.c. Two local directories act as two devices and one local directory as the cloud; all expectations are derived by hand from "later write overwrites earlier write", not obtained by running the implementation under test.
|
||||
#include <dirent.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
int dm_cloud_sync(const char *save, const char *cloud, const char *bak, const char *statef, int allow_down, uint64_t now_ms, int (*ready)(const char *unit));
|
||||
int dm_cloud_conflict_take(void);
|
||||
#if __has_include("../stash_seed.h") // the public repository does not ship this seed (it is the bytes of a save the game generated): when it is absent, the two groups that use it, 18 and 19, are skipped
|
||||
#include "../stash_seed.h"
|
||||
#define T_HAVE_STASH_SEED 1
|
||||
#endif
|
||||
|
||||
static int fails, checks;
|
||||
#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
static char R[512];
|
||||
static void path(char *o, const char *a, const char *b) { snprintf(o, 1024, "%s/%s/%s", R, a, b); }
|
||||
static void put(const char *dev, const char *rel, const char *txt, long sec) { // write a file and set its modification time to sec seconds
|
||||
char p[1024]; path(p, dev, rel); char d[1024]; strcpy(d, p); for (char *s = d + strlen(R) + 1; (s = strchr(s, '/')); s++) { *s = 0; mkdir(d, 0755); *s = '/'; }
|
||||
FILE *f = fopen(p, "w"); fputs(txt, f); fclose(f); struct timeval tv[2] = { { sec, 0 }, { sec, 0 } }; utimes(p, tv);
|
||||
}
|
||||
static const char *get(const char *dev, const char *rel) { static char b[256]; char p[1024]; path(p, dev, rel); FILE *f = fopen(p, "r"); if (!f) return NULL; size_t n = fread(b, 1, 255, f); b[n] = 0; fclose(f); return b; }
|
||||
static long mtime(const char *dev, const char *rel) { char p[1024]; struct stat st; path(p, dev, rel); return stat(p, &st) ? -1 : st.st_mtimespec.tv_sec; }
|
||||
static int dsync(const char *dev, int down, uint64_t now) { char s[1024], c[1024], b[1024], t[1024]; path(s, dev, "save"); snprintf(c, sizeof c, "%s/cloud", R); path(b, dev, "bak"); path(t, dev, "state"); return dm_cloud_sync(s, c, b, t, down, now, NULL); }
|
||||
static int count_bak(const char *dev, const char *pre) { char d[1024]; path(d, dev, "bak"); DIR *dp = opendir(d); struct dirent *e; int n = 0; while (dp && (e = readdir(dp))) n += !strncmp(e->d_name, pre, strlen(pre)); if (dp) closedir(dp); return n; }
|
||||
static int no_skip(const char *u) { return strcmp(u, "Alice"); }
|
||||
// Shared stash file: one page of items (items pieces, only the page header and item count are written) + one chronicle section (chron bytes); side file: n pieces. Only the structure is built, as the sync side only looks at structure (dm_cloudsync.c rich_of)
|
||||
static void put_bin(const char *dev, const char *rel, const uint8_t *d, size_t n, long sec) {
|
||||
char p[1024]; path(p, dev, rel); char dd[1024]; strcpy(dd, p); for (char *x = dd + strlen(R) + 1; (x = strchr(x, '/')); x++) { *x = 0; mkdir(dd, 0755); *x = '/'; }
|
||||
FILE *f = fopen(p, "wb"); fwrite(d, 1, n, f); fclose(f); struct timeval tv[2] = { { sec, 0 }, { sec, 0 } }; utimes(p, tv);
|
||||
}
|
||||
static void put_d2i(const char *dev, int items, int chron, long sec) {
|
||||
static uint8_t b[8192]; memset(b, 0, sizeof b); uint32_t l1 = 68, l2 = (uint32_t)chron;
|
||||
memcpy(b, "\x55\xaa\x55\xaa", 4); memcpy(b + 16, &l1, 4); memcpy(b + 64, "JM", 2); b[66] = (uint8_t)items;
|
||||
memcpy(b + 68, "\x55\xaa\x55\xaa", 4); memcpy(b + 68 + 16, &l2, 4); b[68 + 64] = 0xc0;
|
||||
put_bin(dev, "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i", b, 68 + l2, sec);
|
||||
}
|
||||
static void put_vault(const char *dev, int n, long sec) { uint8_t b[8] = { 'S', 'W', 'V', 'T', 1, 0, (uint8_t)n, 0 }; put_bin(dev, "save/ModernSharedStashSoftCoreV2.swvault", b, 8, sec); }
|
||||
static int d2i_items(const char *dev, const char *rel) { char p[1024]; path(p, dev, rel); FILE *f = fopen(p, "rb"); if (!f) return -1; uint8_t b[70]; size_t n = fread(b, 1, 70, f); fclose(f); return n >= 67 ? b[66] : -1; }
|
||||
|
||||
int main(void) {
|
||||
snprintf(R, sizeof R, "%s/t_cloudsync.%d", getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp", getpid()); mkdir(R, 0755);
|
||||
char c[1024]; snprintf(c, sizeof c, "%s/cloud", R); mkdir(c, 0755);
|
||||
// (1) A has character Alice (timestamp 1000 seconds), the shared stash and Settings; B is empty -> A uploads, B gets Alice and the shared stash at startup, Settings is not synced, and modification times follow A's
|
||||
put("A", "save/Alice/Alice.d2s", "a1", 1000); put("A", "save/Alice/Alice.key", "k", 900);
|
||||
put("A", "save/SharedStashSoftCoreV2.d2i", "s1", 1100); put("A", "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i", "m1", 1100); put("A", "save/Settings/Settings.json", "{}", 1100);
|
||||
put("B", "save/Settings/Settings.json", "{b}", 1200);
|
||||
CHECK(dsync("A", 0, 0) == 2, "① A should upload 2 units (Alice, _stash)");
|
||||
CHECK(dsync("B", 1, 0) == 2, "① B should download 2 units");
|
||||
CHECK(get("B", "save/Alice/Alice.d2s") && !strcmp(get("B", "save/Alice/Alice.d2s"), "a1"), "① B's Alice should be a1");
|
||||
CHECK(mtime("B", "save/Alice/Alice.d2s") == 1000, "① modification time should be restored to 1000, got %ld", mtime("B", "save/Alice/Alice.d2s"));
|
||||
CHECK(get("B", "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i") && !strcmp(get("B", "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i"), "m1"), "① B's shared stash should be m1");
|
||||
CHECK(!strcmp(get("B", "save/Settings/Settings.json"), "{b}"), "① B's Settings should not be touched");
|
||||
CHECK(dsync("A", 1, 0) == 0 && dsync("B", 1, 0) == 0, "① syncing once more should do nothing on either side");
|
||||
// (2) Only B changed (2000) -> B uploads while running; A gets it at startup, and A's former copy goes to backup
|
||||
put("B", "save/Alice/Alice.d2s", "b2", 2000);
|
||||
CHECK(dsync("B", 0, 0) == 1, "② B should upload 1");
|
||||
CHECK(dsync("A", 0, 0) == 0 && !strcmp(get("A", "save/Alice/Alice.d2s"), "a1"), "② A should not change local files while running");
|
||||
CHECK(dsync("A", 1, 0) == 1 && !strcmp(get("A", "save/Alice/Alice.d2s"), "b2"), "② A should have b2 after startup");
|
||||
CHECK(get("A", "bak/Alice-1000000/Alice/Alice.d2s") && !strcmp(get("A", "bak/Alice-1000000/Alice/Alice.d2s"), "a1"), "② A's overwritten a1 should be in backup Alice-1000000");
|
||||
// (3) Both sides changed, B wrote later (A's 3000 uploaded first, B 3500) -> B still uploads while running (last write wins), the cloud's former a3 goes to B's backup; A gets b3 at startup
|
||||
put("A", "save/Alice/Alice.d2s", "a3", 3000); put("B", "save/Alice/Alice.d2s", "b3", 3500);
|
||||
CHECK(dsync("A", 0, 0) == 1, "③ A uploads first");
|
||||
CHECK(dsync("B", 0, 0) == 1, "③ B wrote later and uploads too");
|
||||
CHECK(get("B", "bak/Alice-3000000/Alice/Alice.d2s") && !strcmp(get("B", "bak/Alice-3000000/Alice/Alice.d2s"), "a3"), "③ the overwritten cloud a3 should be in B's backup");
|
||||
CHECK(dsync("A", 1, 0) == 1 && !strcmp(get("A", "save/Alice/Alice.d2s"), "b3"), "③ A should have b3 after startup");
|
||||
// (4) Both sides changed, A wrote later (A's 5000 uploaded; B's 4000 not uploaded before restarting) -> B gets A's a4 at startup, b4 goes to backup Alice-4000000
|
||||
put("A", "save/Alice/Alice.d2s", "a4", 5000); CHECK(dsync("A", 0, 0) == 1, "④ A uploads");
|
||||
put("B", "save/Alice/Alice.d2s", "b4", 4000);
|
||||
CHECK(dsync("B", 1, 0) == 1 && !strcmp(get("B", "save/Alice/Alice.d2s"), "a4"), "④ B should have a4 after startup (cloud 5000 > local 4000)");
|
||||
CHECK(get("B", "bak/Alice-4000000/Alice/Alice.d2s") && !strcmp(get("B", "bak/Alice-4000000/Alice/Alice.d2s"), "b4"), "④ B's overwritten b4 should be in backup");
|
||||
// (5) The cloud package has not finished downloading (ready returns 0) -> skip, local files untouched
|
||||
put("A", "save/Alice/Alice.d2s", "a5", 6000); CHECK(dsync("A", 0, 0) == 1, "⑤ A uploads");
|
||||
{ char s[1024], b[1024], t[1024]; path(s, "B", "save"); path(b, "B", "bak"); path(t, "B", "state"); CHECK(dm_cloud_sync(s, c, b, t, 1, 0, no_skip) == 0 && !strcmp(get("B", "save/Alice/Alice.d2s"), "a4"), "⑤ B should stay untouched while the download is unfinished"); }
|
||||
CHECK(dsync("B", 1, 0) == 1 && !strcmp(get("B", "save/Alice/Alice.d2s"), "a5"), "⑤ B should have a5 once downloaded");
|
||||
// (6) A deletes Alice -> deletion marker in the cloud; B also deletes at startup and B's copy goes to backup; B's backup keeps only 3 copies (before this there were 1000000 not on B, 3000000, 4000000, plus 6000000 = 3 copies)
|
||||
{ char p[1024]; path(p, "A", "save/Alice/Alice.d2s"); unlink(p); path(p, "A", "save/Alice/Alice.key"); unlink(p); path(p, "A", "save/Alice"); rmdir(p); }
|
||||
CHECK(dsync("A", 0, 7000000) == 1, "⑥ A should write a deletion marker");
|
||||
CHECK(dsync("B", 1, 0) == 1 && !get("B", "save/Alice/Alice.d2s"), "⑥ B's Alice should be deleted after startup");
|
||||
CHECK(get("B", "bak/Alice-6000000/Alice/Alice.d2s") && !strcmp(get("B", "bak/Alice-6000000/Alice/Alice.d2s"), "a5"), "⑥ B's deleted a5 should be in backup");
|
||||
CHECK(count_bak("B", "Alice-") == 3, "⑥ B should keep 3 backups of Alice, got %d", count_bak("B", "Alice-"));
|
||||
for (long t = 8; t <= 9; t++) { char v[8]; snprintf(v, sizeof v, "x%ld", t); put("B", "save/Bob/Bob.d2s", v, t * 1000); } // unrelated characters are not affected
|
||||
CHECK(dsync("B", 1, 0) == 1, "⑥ B's new character Bob uploads");
|
||||
// (7) After the deletion A created a character with the same name again (9500) -> upload (revive); B gets it at startup
|
||||
put("A", "save/Alice/Alice.d2s", "a7", 9500);
|
||||
CHECK(dsync("A", 1, 0) == 2 && !strcmp(get("A", "save/Bob/Bob.d2s"), "x9"), "⑦ A: Alice uploads + Bob downloads");
|
||||
CHECK(dsync("B", 1, 0) == 1 && get("B", "save/Alice/Alice.d2s") && !strcmp(get("B", "save/Alice/Alice.d2s"), "a7"), "⑦ B should have the recreated Alice a7 after startup");
|
||||
// (8) Shared stash: B changes it (10000) -> A gets it at startup
|
||||
put("B", "save/SharedStashSoftCoreV2.d2i", "s8", 10000);
|
||||
CHECK(dsync("B", 0, 0) == 1 && dsync("A", 1, 0) == 1 && !strcmp(get("A", "save/SharedStashSoftCoreV2.d2i"), "s8"), "⑧ A's shared stash should be s8");
|
||||
CHECK(!strcmp(get("A", "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i"), "m1"), "⑧ the shared stash's other file should keep m1");
|
||||
// (9) The save directory was emptied by something other than the game (the state file is still there) -> at startup it does not count as a deletion, everything is fetched back from the cloud, and no deletion marker appears in the cloud
|
||||
{ char p[1024]; path(p, "A", "save"); char cmd[1200]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", p); system(cmd); mkdir(p, 0755); }
|
||||
CHECK(dsync("A", 1, 0) == 3, "⑨ A should download 3 at startup (Alice, Bob, _stash)");
|
||||
CHECK(get("A", "save/Alice/Alice.d2s") && !strcmp(get("A", "save/Alice/Alice.d2s"), "a7") && get("A", "save/Bob/Bob.d2s") && !strcmp(get("A", "save/SharedStashSoftCoreV2.d2i"), "s8"), "⑨ A should get back Alice a7, Bob, shared stash s8");
|
||||
CHECK(dsync("B", 1, 0) == 0 && get("B", "save/Alice/Alice.d2s"), "⑨ B is unaffected (no deletion marker came over)");
|
||||
// (10) The backup directory cannot be written (here it is blocked with a regular file of the same name) -> neither overwrite nor delete touches local files; after unblocking it proceeds as usual (previously it still overwrote / deleted when the backup write failed)
|
||||
{ char s[1024], c2[1024], b[1024], t2[1024], bf[1024]; snprintf(c2, sizeof c2, "%s/cloud2", R); mkdir(c2, 0755);
|
||||
#define SY(dev, down, now) (path(s, dev, "save"), path(b, dev, "bak"), path(t2, dev, "state"), dm_cloud_sync(s, c2, b, t2, down, now, NULL))
|
||||
put("F", "save/Settings/Settings.json", "{}", 1); // F is a device that already has a save directory
|
||||
put("E", "save/Eve/Eve.d2s", "e1", 1000); CHECK(SY("E", 0, 0) == 1 && SY("F", 1, 0) == 1 && get("F", "save/Eve/Eve.d2s") && !strcmp(get("F", "save/Eve/Eve.d2s"), "e1"), "⑩ setup: F gets Eve e1");
|
||||
put("E", "save/Eve/Eve.d2s", "e2", 2000); CHECK(SY("E", 0, 0) == 1, "⑩ E uploads e2");
|
||||
path(bf, "F", "bak"); FILE *f = fopen(bf, "w"); fclose(f); // F's backup directory location is a regular file: the directory cannot be created
|
||||
CHECK(SY("F", 1, 0) == 0 && get("F", "save/Eve/Eve.d2s") && !strcmp(get("F", "save/Eve/Eve.d2s"), "e1"), "⑩ F should not overwrite while the backup cannot be written, still e1");
|
||||
unlink(bf);
|
||||
CHECK(SY("F", 1, 0) == 1 && get("F", "save/Eve/Eve.d2s") && !strcmp(get("F", "save/Eve/Eve.d2s"), "e2") && get("F", "bak/Eve-1000000/Eve/Eve.d2s") && !strcmp(get("F", "bak/Eve-1000000/Eve/Eve.d2s"), "e1"), "⑩ once unblocked F switches to e2, the whole e1 is in the backup");
|
||||
{ char p2[1024]; path(p2, "E", "save/Eve/Eve.d2s"); unlink(p2); path(p2, "E", "save/Eve"); rmdir(p2); }
|
||||
CHECK(SY("E", 0, 3000000) == 1, "⑩ E deleted Eve while running, writes a deletion marker");
|
||||
char bk[1100]; snprintf(bk, sizeof bk, "%s.keep", bf); rename(bf, bk); f = fopen(bf, "w"); fclose(f); // move the existing backup directory aside first, then block it with a regular file
|
||||
CHECK(SY("F", 1, 0) == 0 && get("F", "save/Eve/Eve.d2s"), "⑩ F should not delete Eve while the backup cannot be written");
|
||||
unlink(bf); rename(bk, bf);
|
||||
CHECK(SY("F", 1, 0) == 1 && !get("F", "save/Eve/Eve.d2s") && get("F", "bak/Eve-2000000/Eve/Eve.d2s"), "⑩ once unblocked F deletes Eve, the whole e2 is in the backup");
|
||||
#undef SY
|
||||
}
|
||||
// (11)-(15) Chronicle replaced wholesale by an empty one: a new device's first sync takes the cloud as authoritative; the shared stash must not be overwritten by a copy with "clearly less" (another cloud directory cloud3, devices G / H)
|
||||
{ char s3[1024], c3[1024], b3[1024], t3[1024]; snprintf(c3, sizeof c3, "%s/cloud3", R); mkdir(c3, 0755);
|
||||
#define S3(dev, down) (path(s3, dev, "save"), path(b3, dev, "bak"), path(t3, dev, "state"), dm_cloud_sync(s3, c3, b3, t3, down, 0, NULL))
|
||||
#define D2I "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i"
|
||||
put_d2i("G", 5, 300, 1000); put_vault("G", 3, 1000);
|
||||
CHECK(S3("G", 0) == 1, "⑫ cloud empty: G's first sync uploads the shared stash");
|
||||
dm_cloud_conflict_take();
|
||||
put_d2i("H", 0, 148, 5000); // H freshly installed: the empty shared stash the game just created, with a timestamp newer than the cloud's
|
||||
CHECK(S3("H", 0) == 0 && d2i_items("H", D2I) == 0, "⑪ new device's first sync while running: no upload, no change (an empty stash must not overwrite the cloud)");
|
||||
CHECK(S3("H", 1) == 1 && d2i_items("H", D2I) == 5 && get("H", "save/ModernSharedStashSoftCoreV2.swvault"), "⑪ new device's first sync at startup: the cloud wins, gets the 5 items and the Chronicle vault");
|
||||
CHECK(count_bak("H", "_stash-") == 1, "⑪ the new device's own empty stash goes to backup");
|
||||
CHECK(!dm_cloud_conflict_take(), "⑪ a normal first sync is not a conflict");
|
||||
put_d2i("G", 6, 400, 6000); CHECK(S3("G", 0) == 1, "⑭ G's Chronicle grew (400), uploads as usual");
|
||||
CHECK(S3("H", 1) == 1 && d2i_items("H", D2I) == 6, "⑭ H gets G's new version at startup");
|
||||
put_d2i("H", 0, 148, 7000); // the shared stash on H somehow became empty (chronicle 148, 0 items), with a newer timestamp
|
||||
CHECK(S3("H", 0) == 0, "⑬ H wants to upload the emptied copy while running: blocked");
|
||||
CHECK(dm_cloud_conflict_take() == 1, "⑬ the block records a conflict (a notice pops up on entering a run)");
|
||||
CHECK(S3("G", 1) == 0 && d2i_items("G", D2I) == 6, "⑬ the cloud was not overwritten: G still has 6 items");
|
||||
put_d2i("H", 7, 500, 7500); CHECK(S3("H", 0) == 1, "⑮ H plays normally: Chronicle 500 uploads");
|
||||
put_d2i("G", 6, 450, 7600); // G wrote 450 itself without having synced H's 500 (both sides changed, G wrote later)
|
||||
CHECK(S3("G", 0) == 0, "⑮ G wants to overwrite the cloud's Chronicle 500 with 450 while running: blocked");
|
||||
CHECK(S3("G", 1) == 0 && d2i_items("G", D2I) == 6, "⑮ G at startup: the cloud's 500 is older than the local 450 but its Chronicle is longer -- last write wins would upload, it is blocked; local files untouched");
|
||||
CHECK(dm_cloud_conflict_take() == 1, "⑮ conflict recorded");
|
||||
{ char p3[1024]; path(p3, "H", "save/ModernSharedStashSoftCoreV2.swvault"); unlink(p3); } put_d2i("H", 7, 500, 8000);
|
||||
CHECK(S3("H", 0) == 0 && dm_cloud_conflict_take() == 1, "⑬ the Chronicle vault side file is gone (the cloud has 3 items): blocked");
|
||||
put_vault("H", 0, 8100); put_d2i("H", 0, 500, 8100);
|
||||
CHECK(S3("H", 0) == 1 && !dm_cloud_conflict_take(), "⑯ normal play: shared stash items taken out, Chronicle vault emptied (file still there, 0 items), Chronicle not shorter -- uploads as usual");
|
||||
// (17) That rf3 shape: cloud hardcore stash has 2 items and the chronicle is still the freshly created 148; the local copy was replaced by the empty file the game freshly creates (0 items, 148), the state record is still there and the cloud is unchanged = takes the "only local changed" upload
|
||||
{ char cd[1024]; snprintf(cd, sizeof cd, "%s/cloud4", R); mkdir(cd, 0755);
|
||||
#define S4(dev, down) (path(s3, dev, "save"), path(b3, dev, "bak"), path(t3, dev, "state"), dm_cloud_sync(s3, cd, b3, t3, down, 0, NULL))
|
||||
put_d2i("J", 2, 148, 1000); CHECK(S4("J", 1) == 1, "⑰ setup: J uploads a stash with 2 items and Chronicle 148");
|
||||
put_d2i("J", 0, 148, 2000); dm_cloud_conflict_take();
|
||||
CHECK(S4("J", 0) == 0 && dm_cloud_conflict_take() == 1, "⑰ wants to overwrite the 2-item copy with a freshly created empty file (0 items, 148) while running: blocked");
|
||||
CHECK(S4("J", 1) == 0, "⑰ blocked at startup too (no upload)");
|
||||
put_d2i("J", 0, 300, 3000); CHECK(S4("J", 0) == 1, "⑰ a stash emptied with a Chronicle longer than the fresh size: uploads as usual");
|
||||
// (18) The real rf3 shape: the cloud hardcore stash is another device's preset (stash_seed.h), and the local device has no hardcore stash file yet (startup sync runs before the preset is written) -> not blocked, no prompt; uploading after the local device writes the preset is not blocked either
|
||||
#ifdef T_HAVE_STASH_SEED
|
||||
{ char cd5[1024]; snprintf(cd5, sizeof cd5, "%s/cloud5", R); mkdir(cd5, 0755);
|
||||
#define S5(dev, down) (path(s3, dev, "save"), path(b3, dev, "bak"), path(t3, dev, "state"), dm_cloud_sync(s3, cd5, b3, t3, down, 0, NULL))
|
||||
#define HC "save/ModernSharedStashHardCoreV2/ModernSharedStashHardCoreV2.d2i"
|
||||
put_bin("K", HC, g_stash_seed, sizeof g_stash_seed, 1000); put_d2i("K", 3, 400, 1000); CHECK(S5("K", 1) == 1, "⑱ setup: K uploads (the hardcore stash is the preset)");
|
||||
{ char p5[1024]; path(p5, "K", HC); unlink(p5); } put_d2i("K", 3, 400, 2000); dm_cloud_conflict_take();
|
||||
S5("K", 0); CHECK(!dm_cloud_conflict_take(), "⑱ no local hardcore stash file yet, the cloud holds only the preset: nothing is lost, no notice");
|
||||
put_bin("K", HC, g_stash_seed, sizeof g_stash_seed, 3000); S5("K", 0); CHECK(!dm_cloud_conflict_take(), "⑱ the local device wrote the preset and uploads: not blocked");
|
||||
// (19) The reverse: the cloud has real content and the local copy is the preset -> still blocked
|
||||
put_d2i("K", 5, 600, 4000); S5("K", 0); dm_cloud_conflict_take();
|
||||
{ uint8_t big[700]; memset(big, 0, sizeof big); uint32_t l1 = 68, l2 = 300; memcpy(big, "\x55\xaa\x55\xaa", 4); memcpy(big + 16, &l1, 4); memcpy(big + 64, "JM", 2); big[66] = 9; memcpy(big + 68, "\x55\xaa\x55\xaa", 4); memcpy(big + 84, &l2, 4); big[132] = 0xc0;
|
||||
put_bin("K", HC, big, 68 + 300, 5000); CHECK(S5("K", 0) == 1, "⑲ setup: hardcore stash with 9 items, Chronicle 300 uploads"); }
|
||||
put_bin("K", HC, g_stash_seed, sizeof g_stash_seed, 6000);
|
||||
CHECK(S5("K", 0) == 0 && dm_cloud_conflict_take() == 1, "⑲ the local copy reverted to the preset wants to overwrite the 9-item copy: blocked");
|
||||
#undef HC
|
||||
#undef S5
|
||||
}
|
||||
#else
|
||||
printf("skipping ⑱ ⑲: no shared stash seed stash_seed.h (the public repository does not ship it)\n");
|
||||
#endif
|
||||
#undef S4
|
||||
}
|
||||
#undef D2I
|
||||
#undef S3
|
||||
}
|
||||
printf("t_cloudsync: %d checks, %d failed\n", checks, fails);
|
||||
return fails != 0;
|
||||
}
|
||||
@@ -7,7 +7,6 @@ void guest_thread_init(void) {}
|
||||
void dm_thread_enter(uint64_t entry, uint64_t arg, uint64_t sp) { (void)entry; (void)arg; (void)sp; abort(); }
|
||||
void *guest_sym(const char *name) { (void)name; return NULL; }
|
||||
module_t *module_of(const void *p) { (void)p; return NULL; }
|
||||
void dm_prof_add(int id, uint64_t ns, uint64_t bytes) { (void)id; (void)ns; (void)bytes; }
|
||||
#include <mach/mach_time.h>
|
||||
uint64_t now_ns(void) { static mach_timebase_info_data_t tb; if (!tb.denom) mach_timebase_info(&tb); return mach_absolute_time() * tb.numer / tb.denom; }
|
||||
uint64_t guest_ns(void) { return now_ns(); }
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// Unit test of the rift completion door landing points (dm_doorspot.h, takes seconds). The expected values are all computed by hand from the maps, without running the implementation under test.
|
||||
// Situation: narrow maps of the Cave / Pit kind, the player standing against a wall, and the old rule's ideal point (10 cells with the back to the guardian, offset 5 cells left / right) lands inside a wall.
|
||||
// Fail → pass: on the same map, the old rule's landing point is a blocked cell (old_spot_blocked); the new rule's landing point must be a reachable cell.
|
||||
#include "dm_doorspot.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static int fails, checks;
|
||||
#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
static struct ds_grid g;
|
||||
static void fill(int v) { memset(g.blk, v, sizeof g.blk); }
|
||||
static void rect(int x0, int y0, int x1, int y1, int v) { for (int y = y0; y <= y1; y++) for (int x = x0; x <= x1; x++) if (x >= -DS_R && x <= DS_R && y >= -DS_R && y <= DS_R) g.blk[(y + DS_R) * DS_W + x + DS_R] = (uint8_t)v; }
|
||||
static int blk(int x, int y) { return ds_blk(&g, x + DS_R, y + DS_R); }
|
||||
static int dist(int x, int y) { return ds_dist(&g, x + DS_R, y + DS_R); }
|
||||
|
||||
int main(void) {
|
||||
int x, y, t;
|
||||
// ① Open ground: the ideal point itself qualifies → taken as is (wilderness maps do not degrade)
|
||||
fill(0); ds_flood(&g);
|
||||
t = ds_pick(&g, 12, -3, 0, 0, 0, 6, &x, &y); CHECK(t == 0 && x == 12 && y == -3, "open ground should take the ideal point 12,-3 tier 0, got %d,%d tier %d", x, y, t);
|
||||
t = ds_pick(&g, 2, 12, 1, 12, -3, 6, &x, &y); CHECK(t == 0 && x == 2 && y == 12, "open ground, second door should take the ideal point 2,12, got %d,%d tier %d", x, y, t);
|
||||
CHECK(dist(12, -3) == 15, "open ground, 12,-3 is 15 steps away, computed %d", dist(12, -3));
|
||||
|
||||
// ② Horizontal corridor (y −4..4 walkable, the rest is wall), the player in the middle, the ideal point inside the wall straight below
|
||||
fill(1); rect(-DS_R, -4, DS_R, 4, 0); ds_flood(&g);
|
||||
CHECK(blk(5, 10) && blk(-5, 10), "control: the old rule's two landing points ±5,10 are inside the wall");
|
||||
// Cross footprint → the reachable y is −3..3; the 2 cells around all reachable → y −1..1. The nearest to (5,10) is (5,1)
|
||||
t = ds_pick(&g, 5, 10, 0, 0, 0, 6, &x, &y); CHECK(t == 0 && x == 5 && y == 1, "in a corridor should take 5,1 tier 0, got %d,%d tier %d", x, y, t);
|
||||
CHECK(dist(x, y) >= 0 && !blk(x, y), "landing point must be reachable and not blocked");
|
||||
int x2, y2; t = ds_pick(&g, -5, 10, 1, x, y, 6, &x2, &y2); CHECK(t == 0 && x2 == -5 && y2 == 1, "in a corridor, second door should take -5,1, got %d,%d tier %d", x2, y2, t);
|
||||
|
||||
// ③ There is a patch of open ground beyond the wall but it is not connected to the player: it must not be chosen
|
||||
fill(1); rect(-DS_R, -4, DS_R, 4, 0); rect(-DS_R, 8, DS_R, 16, 0); ds_flood(&g);
|
||||
CHECK(!blk(5, 10) && dist(5, 10) < 0, "control: 5,10 is not blocked but cannot be reached from the player");
|
||||
t = ds_pick(&g, 5, 10, 0, 0, 0, 6, &x, &y); CHECK(dist(x, y) >= 0 && y <= 1, "open ground behind a wall must not be chosen, should stay on the player's side, got %d,%d", x, y);
|
||||
|
||||
// ④ Narrow passage (3 cells wide: y −1..1): neither 5 × 5 nor 3 × 3 fits → tier 2, take the reachable cell nearest to the player (with the cross footprint only the row y = 0 is reachable, 1 cell from the player)
|
||||
fill(1); rect(-DS_R, -1, DS_R, 1, 0); ds_flood(&g);
|
||||
t = ds_pick(&g, 5, 10, 0, 0, 0, 6, &x, &y); CHECK(t == 2 && y == 0 && abs(x) == 1, "in a narrow passage should fall back to the nearest reachable cell at the feet (±1,0) tier 2, got %d,%d tier %d", x, y, t);
|
||||
t = ds_pick(&g, -5, 10, 1, x, y, 6, &x2, &y2); CHECK(t == 2 && y2 == 0 && abs(x2 - x) >= 2 && dist(x2, y2) >= 0, "in a narrow passage, second door at least 2 cells from the first and reachable, got %d,%d tier %d", x2, y2, t);
|
||||
|
||||
// ⑤ The player is boxed into a single cell: land at the feet
|
||||
fill(1); rect(0, 0, 0, 0, 0); ds_flood(&g);
|
||||
t = ds_pick(&g, 5, 10, 0, 0, 0, 6, &x, &y); CHECK(t == 3 && x == 0 && y == 0 && g.reach == 1, "when walled in, lands at the feet, tier 3, got %d,%d tier %d (%d cells reachable)", x, y, t, g.reach);
|
||||
|
||||
// ⑥ Cells reached only by a long detour are not chosen: the ideal point is behind a long wall, and going around takes more than 36 steps
|
||||
fill(0); rect(-20, 6, 20, 7, 1); ds_flood(&g);
|
||||
CHECK(dist(0, 12) > DS_FAR, "control: 0,12 behind the wall takes %d steps (> %d)", dist(0, 12), DS_FAR);
|
||||
t = ds_pick(&g, 0, 12, 0, 0, 0, 6, &x, &y); CHECK(t == 0 && abs(x) == 3 && y == 2, "in front of a long wall should take ±3,2 on this side of the wall (y = 2 is the row nearest the wall with all cells within 2 reachable; at least 3 cells from the player, so x = ±3), got %d,%d tier %d", x, y, t);
|
||||
|
||||
printf("t_doorspot: %d checks, %d failed\n", checks, fails); return fails != 0;
|
||||
}
|
||||
+2
-46
@@ -17,9 +17,8 @@ static ssize_t fake_pwrite(int fd, const void *buf, size_t n, off_t off);
|
||||
ssize_t pwrite(int, const void *, size_t, off_t);
|
||||
|
||||
struct dm_flags g_flag; uint64_t g_t0_ns; uint64_t now_ns(void) { return 0; } // stubs for log timestamps and the read path (not used by the write path)
|
||||
void dm_prof_add(int id, uint64_t ns, uint64_t b) { (void)id; (void)ns; (void)b; } void dm_guest_write_prepare(void *a, size_t n) { (void)a; (void)n; }
|
||||
void dm_guest_write_prepare(void *a, size_t n) { (void)a; (void)n; }
|
||||
extern int g_nfail, g_lasterr; // front-end notification count (the strong implementation in t_fs_stub.c replaces the weak one in hle_fs.c)
|
||||
void dm_fs_save_poison(int e);
|
||||
static int g_mode, g_calls; // 0 write normally; 1 always return 0; 2 write only 3 bytes the first time
|
||||
static ssize_t fake_pwrite(int fd, const void *buf, size_t n, off_t off) {
|
||||
g_calls++;
|
||||
@@ -41,29 +40,7 @@ static Result write_save(const char *path, const char *s) { // open ->
|
||||
}
|
||||
static int read_fs(const char *path, char *out, size_t n) { uint64_t h; size_t got = 0; if (h_fs_OpenFile(&h, path, MODE_READ)) return 0; Result r = h_fs_ReadFile(&got, h, 0, out, n - 1); h_fs_CloseFile(h); out[got] = 0; return !r; }
|
||||
|
||||
// Offset translation for zero-stripped archives (dm_data_pread): original content = a fixed byte sequence computed from position, with 0 filled in at zero-run positions; zero-stripped file = zero runs removed.
|
||||
// Expected values are taken directly from the "original sequence", not through the implementation under test. Covers: crossing kept-run / zero-run boundaries, a whole read falling in a zero run, start and end, reading past end of file.
|
||||
static int test_holes(void) {
|
||||
enum { N = 100000 }; static uint8_t orig[N], got[N]; const long hole[3][2] = {{0, 1000}, {30000, 25000}, {99000, 1000}}; // one zero run each at the start, middle and end
|
||||
for (long i = 0; i < N; i++) orig[i] = (uint8_t)(i * 131 + (i >> 8) * 7 + 1);
|
||||
for (int h = 0; h < 3; h++) memset(orig + hole[h][0], 0, (size_t)hole[h][1]);
|
||||
char path[] = "/tmp/dm-holes.XXXXXX"; int tfd = mkstemp(path); if (tfd < 0) return 1; char hp[64]; snprintf(hp, sizeof hp, "%s.holes", path);
|
||||
long pos = 0; for (int h = 0; h <= 3; h++) { long end = h < 3 ? hole[h][0] : N; if (end > pos && write(tfd, orig + pos, (size_t)(end - pos)) != end - pos) return 1; if (h < 3) pos = hole[h][0] + hole[h][1]; }
|
||||
close(tfd); FILE *f = fopen(hp, "w"); fprintf(f, "size %d\n", N); for (int h = 0; h < 3; h++) fprintf(f, "%ld %ld\n", hole[h][0], hole[h][1]); fclose(f);
|
||||
int fd = open(path, O_RDONLY); dm_data_note_fd(fd, path); int bad = 0;
|
||||
const long q[][2] = {{0, 500}, {500, 1000}, {999, 2}, {1000, 29000}, {29990, 20}, {30000, 25000}, {40000, 100}, {54990, 20}, {55000, 44000}, {98990, 1010}, {99500, 500}, {99990, 100}, {0, N}, {100000, 10}, {123456, 10}};
|
||||
for (size_t k = 0; k < sizeof q / sizeof *q; k++) {
|
||||
long off = q[k][0], n = q[k][1], want = off >= N ? 0 : (off + n > N ? N - off : n);
|
||||
memset(got, 0xAA, sizeof got); ssize_t r = dm_data_pread(fd, got, (size_t)n, off);
|
||||
if (r != want || (want && memcmp(got, orig + off, (size_t)want))) { fprintf(stderr, "FAIL: zero-stripped read off=%ld n=%ld returned %zd (expected %ld)\n", off, n, r, want); bad++; }
|
||||
}
|
||||
if (dm_data_logical_size(path, 49000) != N) { fprintf(stderr, "FAIL: original size of the zero-stripped archive\n"); bad++; }
|
||||
close(fd); g_hm[fd] = NULL; unlink(path); unlink(hp);
|
||||
printf("%s zero-stripped archive offset translation: 15 kinds of read equal the original content byte for byte\n", bad ? "✗" : "✓");
|
||||
return bad;
|
||||
}
|
||||
int main(void) {
|
||||
if (test_holes()) return 1;
|
||||
char root[1024], save[1100], work[1200], p[1200];
|
||||
const char *sp = getenv("TMPDIR"); snprintf(root, sizeof root, "%s/t_fs.XXXXXX", sp ? sp : "/private/tmp");
|
||||
if (!mkdtemp(root)) { perror("mkdtemp"); return 1; }
|
||||
@@ -74,15 +51,6 @@ int main(void) {
|
||||
setenv("DM_ROMFS", rom, 1); setenv("DM_ROMFS_OVERLAY", overlay, 1); CHECK(h_fs_MountRom("rom", NULL, 0) == RES_OK, "mount RomFS with two layers (base + overlay)");
|
||||
CHECK(read_fs("rom:/data/same", got, sizeof got) && !strcmp(got, "overlay"), "the overlay file wins"); CHECK(read_fs("rom:/data/base", got, sizeof got) && !strcmp(got, "only-base"), "falls back to the base when the overlay lacks the file");
|
||||
uint64_t rh; CHECK(h_fs_OpenFile(&rh, "rom:/data/same", MODE_WRITE) == RES_FAIL, "the two-layer mount refuses writes");
|
||||
{ // Find a file in the overlay (dm_overlay_file): two layers joined by a colon (old devices = in-bundle overlay + Legacy Device Pack), searched layer by layer with earlier ones taking priority; treating the whole string as one directory finds nothing
|
||||
char l2[1100], two[2300], want[1300]; snprintf(l2, sizeof l2, "%s/pack", root); mkdir(l2, 0755); snprintf(two, sizeof two, "%s:%s", overlay, l2); setenv("DM_ROMFS_OVERLAY", two, 1);
|
||||
snprintf(fn, sizeof fn, "%s/mark", overlay); put(fn, "1"); snprintf(fn, sizeof fn, "%s/only2", l2); put(fn, "2"); snprintf(fn, sizeof fn, "%s/mark", l2); put(fn, "2");
|
||||
snprintf(want, sizeof want, "%s/mark", overlay); CHECK(dm_overlay_file("mark", p, sizeof p) == 1 && !strcmp(p, want), "in both layers: the first layer's is taken");
|
||||
snprintf(want, sizeof want, "%s/only2", l2); CHECK(dm_overlay_file("only2", p, sizeof p) == 1 && !strcmp(p, want), "only in the second layer: the second layer's is taken");
|
||||
snprintf(want, sizeof want, "%s/data/over", overlay); CHECK(dm_overlay_file("data/over", p, sizeof p) == 1 && !strcmp(p, want), "relative path with a subdirectory");
|
||||
CHECK(dm_overlay_file("none", p, sizeof p) == 0 && !p[0], "in no layer: returns 0, output cleared");
|
||||
setenv("DM_ROMFS_OVERLAY", overlay, 1); snprintf(want, sizeof want, "%s/mark", overlay); CHECK(dm_overlay_file("mark", p, sizeof p) == 1 && !strcmp(p, want), "a single layer (newer devices) works as before");
|
||||
unsetenv("DM_ROMFS_OVERLAY"); CHECK(dm_overlay_file("mark", p, sizeof p) == 0, "no overlay set: returns 0"); setenv("DM_ROMFS_OVERLAY", overlay, 1); }
|
||||
g_nmnt = 0;
|
||||
snprintf(save, sizeof save, "%s/save", root); snprintf(work, sizeof work, "%s.work", save);
|
||||
mkdir(save, 0755); snprintf(p, sizeof p, "%s/a", save); put(p, "old");
|
||||
@@ -138,7 +106,7 @@ int main(void) {
|
||||
// 6a Recloning after a refused commit fails: do not exit, the work directory is untouched and the committed version unchanged; the next commit is refused again and the reclone succeeds; the transaction after that commits normally
|
||||
setenv("DM_TEST_FS_FAILAT", "reclone:1", 1);
|
||||
CHECK(write_save("save:/a", "bad") == RES_OK, "write a=bad");
|
||||
dm_fs_save_poison(ENOSPC); int nf0 = g_nfail;
|
||||
save_poison(ENOSPC); int nf0 = g_nfail; // a save write of this transaction failed
|
||||
CHECK(h_fs_Commit("save") == RES_FAIL && g_nfail == nf0 + 1, "this transaction is refused (injected reclone failure), the process is still alive, front end notified once");
|
||||
snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "new"), "committed directory a is still new");
|
||||
snprintf(p, sizeof p, "%s/a", work); CHECK(same(p, "bad"), "work directory not deleted (a is still bad)");
|
||||
@@ -169,18 +137,6 @@ int main(void) {
|
||||
snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "ok3"), "committed a = ok3");
|
||||
|
||||
|
||||
// Language switch rewriting the settings file (settings_lang_apply): expected values computed by hand from the rule -- only when it "does not match the switch" are Language / VoiceLanguage changed to 0 (English) or 8 (Simplified Chinese); other keys and other languages are untouched
|
||||
{ extern int g_t_lang; char sd[1200], sf[1300], gotj[512]; snprintf(sd, sizeof sd, "%s/langt", root); mkdir(sd, 0755); snprintf(sf, sizeof sf, "%s/Settings", sd); mkdir(sf, 0755); snprintf(sf, sizeof sf, "%s/Settings/Settings.json", sd);
|
||||
#define LANGCASE(dm, in, want, what) do { g_t_lang = (dm); put(sf, (in)); settings_lang_apply(sd); FILE *lf = fopen(sf, "rb"); size_t ln_ = lf ? fread(gotj, 1, sizeof gotj - 1, lf) : 0; if (lf) fclose(lf); gotj[ln_] = 0; CHECK(!strcmp(gotj, (want)), "language switch: %s", (what)); } while (0)
|
||||
LANGCASE(-1, "{ \"Language\": 8, \"VoiceLanguage\": 8 }", "{ \"Language\": 8, \"VoiceLanguage\": 8 }", "Apply never tapped: not a byte changes");
|
||||
LANGCASE(1, "{ \"A\": 1, \"Language\": 8, \"VoiceLanguage\": 8, \"B\": 2 }", "{ \"A\": 1, \"Language\": 0, \"VoiceLanguage\": 0, \"B\": 2 }", "switch = English, file is Simplified Chinese: text and voice both become 0, other keys untouched");
|
||||
LANGCASE(0, "{ \"Language\": 0, \"VoiceLanguage\": 0 }", "{ \"Language\": 8, \"VoiceLanguage\": 8 }", "switch = Chinese, file is English: both become 8");
|
||||
LANGCASE(0, "{ \"Language\": 9, \"VoiceLanguage\": 9 }", "{ \"Language\": 9, \"VoiceLanguage\": 9 }", "switch = Chinese, the player chose Traditional Chinese in the original game: not changed back to Simplified Chinese");
|
||||
LANGCASE(1, "{ \"Language\": 0, \"VoiceLanguage\": 0 }", "{ \"Language\": 0, \"VoiceLanguage\": 0 }", "switch = English, file already English: untouched");
|
||||
LANGCASE(1, "{ \"Language\": 0, \"VoiceLanguage\": 8 }", "{ \"Language\": 0, \"VoiceLanguage\": 0 }", "switch = English, text already English but voice still Chinese: the voice follows");
|
||||
LANGCASE(1, "{ \"Language\": 12 }", "{\n \"VoiceLanguage\": 0, \"Language\": 0 }", "switch = English, two-digit language number and no voice key: rewritten and the voice key added");
|
||||
g_t_lang = -1; }
|
||||
|
||||
char cmd[1200]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", root); system(cmd);
|
||||
printf("== %d checks, %d failed ==\n", checks, fails);
|
||||
return fails != 0;
|
||||
|
||||
@@ -15,7 +15,6 @@ static int fake_setxattr(const char *p, const char *name, const void *v, size_t
|
||||
#undef setxattr
|
||||
struct dm_flags g_flag; uint64_t g_t0_ns;
|
||||
uint64_t now_ns(void) { return 0; }
|
||||
void dm_prof_add(int id, uint64_t ns, uint64_t b) { (void)id; (void)ns; (void)b; }
|
||||
void dm_guest_write_prepare(void *a, size_t n) { (void)a; (void)n; }
|
||||
static int checks, fails;
|
||||
#define CHECK(c, text) do { checks++; int ok = !!(c); fails += !ok; printf("%s %s\n", ok ? "✓" : "✗", text); } while (0)
|
||||
@@ -42,7 +41,6 @@ static int entry_count(void) {
|
||||
}
|
||||
static void *create_race(void *p) { return (void *)(uintptr_t)h_fs_CreateFile(p, 0); }
|
||||
module_t g_mod[MAX_MOD]; int g_nmod; // Module table (MountSave picks save fixes by main build ID): this unit test loads no modules
|
||||
int dm_custom_lang_explicit(void) { return -1; } // Language switch (dm_custom.c): not involved in this unit test, treated as the player never having tapped it
|
||||
int main(void) {
|
||||
char root[1024], save[1024], p[1100];
|
||||
const char *tmp = getenv("TMPDIR");
|
||||
@@ -113,3 +111,5 @@ int main(void) {
|
||||
rm_tree(root);
|
||||
printf("== case: %d checks, %d failed ==\n", checks, fails); return fails != 0;
|
||||
}
|
||||
// the guest address space (hle_svc.c): the unit test has none, so every stack is a host stack (hle_bigstack.c)
|
||||
int dm_guest_as_contains(uintptr_t a) { (void)a; return 0; }
|
||||
@@ -2,10 +2,10 @@
|
||||
int g_nfail, g_lasterr;
|
||||
void front_save_failed(int err) { g_nfail++; g_lasterr = err; }
|
||||
|
||||
int g_t_lang = -1; // Value of the language switch (lang_en in dm_custom.c) in the settings file, changed by the cases in t_fs.c: -1 never tapped / 0 Simplified Chinese / 1 English
|
||||
int dm_custom_lang_explicit(void) { return g_t_lang; }
|
||||
|
||||
// the loader's module table: h_fs_MountSave picks the save directory by main's build ID (dm.h dm_main_build); the unit test has no modules, so the table is empty
|
||||
// the loader's module table (dm.h): the unit test has no modules, so the table is empty
|
||||
#include "../dm.h"
|
||||
module_t g_mod[MAX_MOD];
|
||||
int g_nmod;
|
||||
// the guest address space (hle_svc.c): the unit test has none, so every stack is a host stack and the big-stack takeovers run on it directly (hle_bigstack.c)
|
||||
#include <stdint.h>
|
||||
int dm_guest_as_contains(uintptr_t a) { (void)a; return 0; }
|
||||
@@ -1,58 +0,0 @@
|
||||
// The actual dm_input_set -> GetNpadState return takes the sample; the fake clock only fixes the timestamp and does not replace the input/return layout.
|
||||
#ifndef HLE_HID_SOURCE
|
||||
#define HLE_HID_SOURCE "../hle_hid.c"
|
||||
#endif
|
||||
#include HLE_HID_SOURCE
|
||||
module_t g_mod[MAX_MOD]; int g_nmod;
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
static uint64_t clock_ns=1000000000ull;
|
||||
uint64_t dm_guest_frame(void) { return 0; }
|
||||
uint64_t dm_server_tick(void) { return UINT64_MAX; }
|
||||
int dm_loadscreen_up(void) { return 0; }
|
||||
void dm_hold_for_loadscreen(void) {}
|
||||
uint64_t now_ns(void) { return clock_ns; }
|
||||
uint64_t guest_ns(void) { return clock_ns; }
|
||||
static int opmode=1;
|
||||
int dm_opmode(void) { return opmode; }
|
||||
int touch_fill(void *p, uint64_t n) { (void)p;(void)n;return 0; }
|
||||
void dm_touch_set(const dm_touch *p,int n) { (void)p;(void)n; }
|
||||
void uimsg_subscribe(void (*f)(const char *,const char *,const char *)) { (void)f; }
|
||||
static int checks,fails;
|
||||
#define CK(c) do { checks++; if (!(c)) { fails++;printf("FAIL line=%d %s\n",__LINE__,#c); } } while(0)
|
||||
typedef struct { long long sampling; unsigned long long start,time,buttons,script; unsigned id,style,attrs; int lx,ly,rx,ry; } Row;
|
||||
static int load(const char *path,Row *rows,int *limit) {
|
||||
FILE *f=fopen(path,"r");if(!f)return -1;char s[512];int n=0;*limit=0;
|
||||
while(fgets(s,sizeof s,f)) { if(strstr(s,"limit_reached")){(*limit)++;continue;} Row r={0};
|
||||
int k=sscanf(s,"{\"event\":\"npad_return\",\"read_start_ns\":%llu,\"time_ns\":%llu,\"sampling\":%lld,\"id\":%u,\"style\":%u,\"buttons\":%llu,\"lx\":%d,\"ly\":%d,\"rx\":%d,\"ry\":%d,\"attributes\":%u,\"script_mask\":%llu}",&r.start,&r.time,&r.sampling,&r.id,&r.style,&r.buttons,&r.lx,&r.ly,&r.rx,&r.ry,&r.attrs,&r.script);
|
||||
CK(k==12);CK(r.start<=r.time);if(n<1024)rows[n]=r;n++;
|
||||
}fclose(f);return n;
|
||||
}
|
||||
static void exercise(int which) {
|
||||
char path[256];snprintf(path,sizeof path,"%s/hid-receipt-%d.jsonl",getenv("TMPDIR")?getenv("TMPDIR"):"/tmp",getpid());unlink(path);
|
||||
unsetenv("DM_KEYS");unsetenv("DM_HID_RECEIPT_LOG");if(which)setenv("DM_HID_RECEIPT_LOG",path,1);if(which==2)setenv("DM_KEYS","A@0+10",1);
|
||||
NpadState s;uint32_t id=0;Row *rows=calloc(1024,sizeof *rows);int limit=0,n;
|
||||
if(which==4) {
|
||||
dm_input_set(0,0,0,0,0);h_hid_GetFullKey(&s,&id);h_hid_GetFullKey(&s,&id);
|
||||
clock_ns+=1000000000ull;h_hid_GetFullKey(&s,&id);
|
||||
n=load(path,rows,&limit);CK(n==2);if(n==2){CK(rows[0].sampling==1&&rows[1].sampling==3);CK(rows[1].time-rows[0].time==1000000000ull);CK(!rows[0].buttons&&!rows[1].buttons);}CK(limit==0);
|
||||
} else if(which==3) {
|
||||
for(int i=0;i<1030;i++){dm_input_set(0,(i&1)?32767:-32767,0,0,0);h_hid_GetFullKey(&s,&id);CK(s.lx==((i&1)?32767:-32767));}
|
||||
n=load(path,rows,&limit);CK(n==1024);CK(limit==1);
|
||||
} else if(which==2) {
|
||||
dm_input_set(DM_BTN_B,123,-456,7,-8);h_hid_GetFullKey(&s,&id);CK(s.buttons==(DM_BTN_A|DM_BTN_B));CK(s.lx==123&&s.ly==-456);
|
||||
n=load(path,rows,&limit);CK(n==1);if(n==1){CK(rows[0].buttons==s.buttons);CK(rows[0].script==DM_BTN_A);CK(rows[0].lx==123&&rows[0].ly==-456&&rows[0].rx==7&&rows[0].ry==-8);}CK(limit==0);
|
||||
} else {
|
||||
dm_input_set(0,0,0,0,0);h_hid_GetFullKey(&s,&id);CK(s.sampling==1&&s.attributes==1);
|
||||
dm_input_set(DM_BTN_B,32767,-32767,11,-12);h_hid_GetFullKey(&s,&id);CK(s.buttons==DM_BTN_B&&s.lx==32767&&s.ly==-32767&&s.rx==11&&s.ry==-12);
|
||||
h_hid_GetFullKey(&s,&id);CK(s.sampling==3);
|
||||
dm_input_set(0,0,0,0,0);h_hid_GetFullKey(&s,&id);CK(!s.buttons&&!s.lx&&!s.ly);
|
||||
opmode=0;id=0x20;dm_input_set(DM_BTN_Y,-32767,0,0,0);h_hid_GetHandheld(&s,&id);CK(s.attributes==1&&s.buttons==DM_BTN_Y&&s.lx==-32767);
|
||||
id=0;h_hid_GetFullKey(&s,&id);CK(s.attributes==0&&!s.buttons&&!s.lx&&s.sampling==6);
|
||||
n=load(path,rows,&limit);
|
||||
if(!which)CK(n==-1);
|
||||
else {CK(n==5);CK(limit==0);if(n==5){CK(rows[0].sampling==1&&rows[0].time==clock_ns&&rows[0].buttons==0);CK(rows[1].sampling==2&&rows[1].buttons==DM_BTN_B&&rows[1].lx==32767&&rows[1].ry==-12);CK(rows[2].sampling==4&&rows[2].buttons==0&&rows[2].lx==0);CK(rows[3].id==0x20&&rows[3].style==STYLE_HANDHELD&&rows[3].attrs==1&&rows[3].lx==-32767);CK(rows[4].id==0&&rows[4].style==STYLE_FULLKEY&&rows[4].attrs==0&&rows[4].sampling==6);}}
|
||||
}
|
||||
free(rows);unlink(path);printf("HID_RECEIPT case=%d checks=%d failures=%d\n",which,checks,fails);fflush(stdout);_exit(fails?1:0);
|
||||
}
|
||||
int main(void){int fail=0;for(int i=0;i<5;i++){pid_t p=fork();if(!p)exercise(i);int s;waitpid(p,&s,0);fail+=!WIFEXITED(s)||WEXITSTATUS(s)!=0;}printf("HID_RECEIPT %s cases=5 failures=%d\n",fail?"FAIL":"PASS",fail);return !!fail;}
|
||||
@@ -1,144 +0,0 @@
|
||||
// HID script unit test: a fake clock advances frame by frame, with real DM_KEYS parsing and the real GetNpad path; no sleeping, no skipping steps by duration.
|
||||
// Begin is action start/repeat trigger and End marks action end: two Begins need only one End of the same action.
|
||||
#ifndef HLE_HID_SOURCE
|
||||
#define HLE_HID_SOURCE "../hle_hid.c"
|
||||
#endif
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include HLE_HID_SOURCE
|
||||
module_t g_mod[MAX_MOD]; int g_nmod;
|
||||
static uint64_t clock_ns = 1000000000ull;
|
||||
static uint64_t guest_frame;
|
||||
static uint64_t server_tick;
|
||||
static int server_available;
|
||||
uint64_t now_ns(void) { return clock_ns; }
|
||||
uint64_t guest_ns(void) { return clock_ns; }
|
||||
uint64_t dm_guest_frame(void) { return guest_frame; }
|
||||
uint64_t dm_server_tick(void) { return server_available ? server_tick : UINT64_MAX; }
|
||||
int touch_fill(void *out, uint64_t max) { (void)out; (void)max; return 0; }
|
||||
static int test_loadscreen_up, hold_requests;
|
||||
int dm_loadscreen_up(void) { return test_loadscreen_up; } // the real check is in diag.c; the unit test sets it by hand
|
||||
void dm_hold_for_loadscreen(void) { hold_requests++; } // the real implementation freezes the guest clock; the unit test only counts requests
|
||||
int dm_opmode(void) { return 1; }
|
||||
void dm_touch_set(const dm_touch *t, int n) { (void)t; (void)n; }
|
||||
static void (*ui_cb)(const char *, const char *, const char *);
|
||||
void uimsg_subscribe(void (*f)(const char *, const char *, const char *)) { ui_cb = f; }
|
||||
static int checks, fails;
|
||||
#define CHECK(c, s) do { checks++; int ok = !!(c); fails += !ok; printf("%s %s\n", ok ? "✓" : "✗", s); } while (0)
|
||||
static void setup(const char *script) {
|
||||
memset(g_steps, 0, sizeof g_steps); memset(g_keys, 0, sizeof g_keys);
|
||||
g_nsteps = g_nkeys = g_kcur = g_ktries = g_kbeg = g_kend = 0; g_kstate = KS_PRE; g_krel = 1;
|
||||
clock_ns = 1000000000ull; guest_frame = 0; server_tick = 0; server_available = 1; setenv("DM_KEYS", script, 1); keys_parse();
|
||||
}
|
||||
static void setup_pregame(const char *script) {
|
||||
memset(g_steps, 0, sizeof g_steps); memset(g_keys, 0, sizeof g_keys);
|
||||
g_nsteps = g_nkeys = g_kcur = g_ktries = g_kbeg = g_kend = 0; g_kstate = KS_PRE; g_krel = 1;
|
||||
clock_ns = 1000000000ull; guest_frame = 0; server_tick = 0; server_available = 0; setenv("DM_KEYS", script, 1); keys_parse();
|
||||
}
|
||||
static void ui(const char *act) { ui_cb("InputMessage", act, ""); }
|
||||
static NpadState read_state(void) { NpadState s; fill(&s, 0, STYLE_FULLKEY); return s; }
|
||||
static int neutral(NpadState s) { return !s.buttons && !s.lx && !s.ly; }
|
||||
static void release(void) { guest_frame += ACTION_HOLD_TICKS; server_tick += ACTION_HOLD_TICKS; read_state(); }
|
||||
static void frames(uint64_t n) { guest_frame += n; server_tick += n; read_state(); }
|
||||
int main(int argc, char **argv) {
|
||||
unsetenv("DM_KEYS"); hid_keys_init();
|
||||
setup("LSL@+0.05,LSU@+0.05");
|
||||
NpadState s = read_state(); CHECK(s.lx == -32767 && s.ly == 0 && s.buttons == 0, "first step LSL goes into the real controller layout, not into the button bits");
|
||||
ui("ControllerActionBegin"); ui("ControllerActionBegin"); release();
|
||||
CHECK(neutral(read_state()), "with two Begins and no End yet, the next step is still held back");
|
||||
ui("ControllerActionEnd"); s = read_state();
|
||||
CHECK(s.ly == 32767 && s.lx == 0, "after two repeated Begins / one End, the next step LSU goes ahead");
|
||||
|
||||
setup("A@+0.05,B@+0.05"); CHECK(read_state().buttons == DM_BTN_A, "plain A pressed");
|
||||
ui("ControllerActionBegin"); release();
|
||||
CHECK(neutral(read_state()), "a single Begin without End holds back the next key");
|
||||
ui("ControllerActionEnd"); CHECK(read_state().buttons == DM_BTN_B, "B is sent only after End, no key swallowed");
|
||||
|
||||
setup("A@+0.6"); CHECK(read_state().buttons == DM_BTN_A, "hold duration test: A pressed first");
|
||||
clock_ns += 60000000000ull; guest_frame += 15 - 1; server_tick += 15 - 1; // +0.6 seconds = 0.6*25 = 15 ticks
|
||||
CHECK(read_state().buttons == DM_BTN_A, "wall clock jumps but the guest frame is not due: A still held");
|
||||
guest_frame++; server_tick++; CHECK(neutral(read_state()), "A released only once the server tick is due");
|
||||
|
||||
setup("LSL@+0.05,LSU@+0.05"); read_state(); release();
|
||||
CHECK(read_state().ly == 32767, "no Begin in the run: idle on release, no wait for an End that never comes");
|
||||
|
||||
setup("A@+0.05,B@+0.05"); read_state(); ui("ControllerActionBegin"); ui("ControllerActionEnd"); ui("ControllerActionBegin"); release();
|
||||
CHECK(neutral(read_state()), "a new Begin after End: busy again, the old End does not release the new action");
|
||||
ui("ControllerActionEnd"); CHECK(read_state().buttons == DM_BTN_B, "goes ahead after the new action's own End");
|
||||
|
||||
setup("A@+0.05,B@+0.05"); read_state(); ui("ControllerActionEnd"); ui("ControllerActionEnd"); ui("ControllerActionBegin"); release();
|
||||
CHECK(neutral(read_state()), "surplus Ends that came first do not cancel a later Begin");
|
||||
ui("ControllerActionEnd"); CHECK(read_state().buttons == DM_BTN_B, "continues once the later Begin has ended");
|
||||
|
||||
setup("A@>OpenPanel:Done,B@+0.05"); read_state(); ui("ControllerActionBegin"); ui("ControllerActionBegin");
|
||||
ui_cb("PanelManager", "OpenPanel", "Done");
|
||||
CHECK(neutral(read_state()), "a confirm while held releases, but with the action not ended it still waits for idle");
|
||||
ui("ControllerActionEnd"); CHECK(read_state().buttons == DM_BTN_B, "repeated Begins after the confirm need only one End, then B goes");
|
||||
|
||||
setup("A@+0.05,B@%0.2+0.05"); read_state(); ui("ControllerActionBegin"); ui("ControllerActionBegin"); release(); ui("ControllerActionEnd");
|
||||
CHECK(neutral(read_state()), "End does not skip the script's own delay");
|
||||
frames(4); CHECK(neutral(read_state()), "server delay at 4 of 5 ticks: next key held back");
|
||||
frames(1); CHECK(read_state().buttons == DM_BTN_B, "next key only once the server delay reaches 5 ticks and the action has ended");
|
||||
release(); CHECK(neutral(read_state()), "no extra key press after the last step is released");
|
||||
|
||||
setup("A@>Never"); read_state(); release();
|
||||
frames(OK_WAIT_TICKS - ACTION_HOLD_TICKS - 1); CHECK(g_ktries == 1 && neutral(read_state()), "no re-press before the confirm wait reaches 150 server ticks");
|
||||
frames(1); CHECK(g_ktries == 2 && read_state().buttons == DM_BTN_A, "re-press only after a confirm wait of 150 server ticks");
|
||||
|
||||
setup_pregame("A@%0.2"); read_state(); frames(11); CHECK(neutral(read_state()), "main menu presented-frame delay at 11/12 frames: no early press");
|
||||
frames(1); CHECK(read_state().buttons == DM_BTN_A, "main menu presented-frame delay: press only after 12 frames");
|
||||
|
||||
pid_t short_hold = fork();
|
||||
if (!short_hold) { setup("A@+0.01"); _exit(read_state().buttons == 0 ? 0 : 99); }
|
||||
int short_status; waitpid(short_hold, &short_status, 0);
|
||||
CHECK(WIFEXITED(short_status) && WEXITSTATUS(short_status) == 0, "counterexample: held less than 1 server tick, the real reading counts as not sampled");
|
||||
|
||||
pid_t child = fork();
|
||||
if (!child) { setup("A@>Never"); read_state(); release(); clock_ns += 60000000000ull; read_state(); _exit(99); }
|
||||
int status; waitpid(child, &status, 0);
|
||||
CHECK(WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT, "guest frames stopped but 60 s of wall clock: only the fallback prints a [RED] line");
|
||||
|
||||
setup("A@Start>OpenPanel:Done,B@>OpenPanel:Next");
|
||||
ui_cb("PanelManager", "OpenPanel", "Done"); ui_cb("PanelManager", "OpenPanel", "Start");
|
||||
CHECK(read_state().buttons == DM_BTN_A && g_kcur == 0, "an old confirm from before the press does not complete the current step");
|
||||
ui("ControllerActionBegin"); ui("ControllerActionBegin"); ui_cb("PanelManager", "OpenPanel", "Done");
|
||||
ui_cb("PanelManager", "OpenPanel", "Next");
|
||||
CHECK(neutral(read_state()), "an old confirm before the next step is pressed cannot skip a step, still waits for the previous action's End");
|
||||
ui("ControllerActionEnd");
|
||||
CHECK(read_state().buttons == DM_BTN_B && g_kcur == 1, "after the previous End the next step really presses B, the old Next confirm does not count as completion");
|
||||
ui_cb("PanelManager", "OpenPanel", "Next");
|
||||
CHECK(neutral(read_state()) && g_kcur == 2, "only a fresh confirm after the next step is pressed completes it and releases");
|
||||
if (argc > 1) {
|
||||
FILE *f = fopen(argv[1], "r"); if (!f) { perror("trace"); return 2; }
|
||||
setup("LSL@+8,LSU@+8"); read_state(); char line[2048]; int entered = 0, nb = 0, ne = 0;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
if (strstr(line, "hid script step 3 pressed")) entered = 1;
|
||||
if (!entered) continue;
|
||||
double t;
|
||||
if (sscanf(line, "[dm] UIMSG [%lfs]", &t) != 1) continue;
|
||||
int beg = strstr(line, "InputMessage ControllerActionBegin") != NULL;
|
||||
int end = strstr(line, "InputMessage ControllerActionEnd") != NULL;
|
||||
if (!beg && !end) continue;
|
||||
clock_ns = 1000000000ull + (uint64_t)((t - 16.13) * 1e9); read_state();
|
||||
ui(beg ? "ControllerActionBegin" : "ControllerActionEnd"); nb += beg; ne += end;
|
||||
if (end) break;
|
||||
}
|
||||
fclose(f); s = read_state();
|
||||
CHECK(nb == 73 && ne == 1 && s.ly == 32767, "iOS original step 3 trace, 73 Begin / 1 End replayed, then enters LSU (fake clock advanced by message times)");
|
||||
}
|
||||
// During a tick freeze (DM_TICK_HOLD) the tick number does not advance: the precondition was seen under the in-game tick clock, then the host reports "no tick", and %1 must be finished by presented frames instead, 60 frames (host_savecmp waited 451 s dead)
|
||||
setup("A@OpenPanel:PauseLayout%1>PausePanelMessage:ExitGame"); server_tick = 401; guest_frame = 1000;
|
||||
ui_cb("PanelManager", "OpenPanel", "PauseLayout"); CHECK(neutral(read_state()), "pre seen before the freeze, delay not elapsed: no press");
|
||||
server_available = 0; read_state();
|
||||
guest_frame += 59; CHECK(neutral(read_state()), "counted in frames after the freeze: 59 frames, under 1 second, no press");
|
||||
guest_frame += 1; CHECK(read_state().buttons == DM_BTN_A, "counted in frames after the freeze: A pressed at 60 frames (1 second), no endless wait on the stopped tick number");
|
||||
// An in-game key press that comes due while the loading screen is still up: do not press, request a tick freeze; press only after it is down (screenshot gate NPC page: the A at tick 84 was swallowed by the loading screen)
|
||||
setup("A@DM:Tick:84#>OpenPanel:NpcDialogPanel"); server_tick = 84; test_loadscreen_up = 1; hold_requests = 0;
|
||||
ui_cb("DM", "Tick", "84#"); CHECK(neutral(read_state()) && hold_requests == 1, "loading screen up: no A even though the pre has arrived, and a tick freeze is requested");
|
||||
frames(30); CHECK(neutral(read_state()) && hold_requests >= 2, "loading screen still up: still no press");
|
||||
test_loadscreen_up = 0; hold_requests = 0; CHECK(read_state().buttons == DM_BTN_A && hold_requests == 0, "loading screen down: A pressed, no more freeze requests");
|
||||
setup("A@DM:Tick:84#>OpenPanel:NpcDialogPanel"); server_tick = 84; test_loadscreen_up = 0; hold_requests = 0;
|
||||
ui_cb("DM", "Tick", "84#"); CHECK(read_state().buttons == DM_BTN_A && hold_requests == 0, "loading screen already down: press when due, no freeze request (timing unchanged)");
|
||||
printf("== HID script %d checks, %d failed ==\n", checks, fails); return fails != 0;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Unit test for hiding the bottom bar on death: mode -> dead, bottom bar show/hide decision (dm_hud.h). Expected values are hand-written from the game code: main+0x6e5c48 player mode 0 / 0x11 means dead
|
||||
#include <stdio.h>
|
||||
#include "dm_hud.h"
|
||||
static int fails, cases;
|
||||
static void expect(const char *what, int got, int want) {
|
||||
cases++; if (got == want) printf("✓ %s\n", what); else { fails++; printf("✗ %s: got %d, expected %d\n", what, got, want); }
|
||||
}
|
||||
int main(void) {
|
||||
char buf[64];
|
||||
for (unsigned m = 0; m <= 19; m++) { // D2 player modes 0..19 (DEATH ... KNOCKBACK)
|
||||
snprintf(buf, sizeof buf, "mode %u %s", m, (m == 0 || m == 17) ? "→ dead" : "→ alive");
|
||||
expect(buf, dm_hud_player_mode_dead(m), m == 0 || m == 17);
|
||||
}
|
||||
expect("mode 0xffffffff (out of range) → alive", dm_hud_player_mode_dead(0xffffffffu), 0);
|
||||
dm_hud_info h = {0}; h.valid = 1;
|
||||
for (unsigned m = 0; m <= 19; m++) {
|
||||
h.dead = dm_hud_player_mode_dead(m);
|
||||
snprintf(buf, sizeof buf, "normal in-game state, mode %u → bottom bar %s", m, (m == 0 || m == 17) ? "hidden" : "shown");
|
||||
expect(buf, dm_hud_bar_visible(&h, 0, 0), !(m == 0 || m == 17));
|
||||
}
|
||||
h.dead = 0;
|
||||
expect("alive but in navigation state → hidden", dm_hud_bar_visible(&h, 1, 0), 0);
|
||||
expect("alive but covered by a panel → hidden", dm_hud_bar_visible(&h, 0, 1), 0);
|
||||
h.valid = 0; expect("player not read → hidden", dm_hud_bar_visible(&h, 0, 0), 0);
|
||||
printf("%s t_hud_dead %d/%d\n", fails ? "FAIL" : "OK", cases - fails, cases);
|
||||
return fails != 0;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Language source of dm_i18n.c: once the game has set the language (the SetLanguage observation gate writes g_dm_lang), that value
|
||||
// wins; if not, read "Language" from Settings/Settings.json in the save data;
|
||||
// no settings file = first install, use the system language (simulated in tests with DM_LANG). Our own UI text (dm_ui, for UIKit text) is English
|
||||
// in every language.
|
||||
// Expected values come from the rule itself (for the language index table see the name table at main+0x20ae478 in diag.c), without running the game.
|
||||
// Each case needs its own process (the file is read once and then cached), so this program runs one case per argv and the Makefile invokes them one
|
||||
// by one.
|
||||
#include "../dm_i18n.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
static int fails;
|
||||
static void expect(const char *what, const char *got, const char *want) { int ok = !strcmp(got, want); if (!ok) fails++; printf("%s %s: expected '%s' → got '%s'\n", ok ? "✓" : "✗", what, want, got); }
|
||||
int main(int argc, char **argv) {
|
||||
const char *mode = argc > 1 ? argv[1] : "";
|
||||
char dir[512]; snprintf(dir, sizeof dir, "%s/i18n_%s", getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp", mode);
|
||||
char sd[600], f[700]; snprintf(sd, sizeof sd, "%s/Settings", dir); snprintf(f, sizeof f, "%s/Settings.json", sd);
|
||||
mkdir(dir, 0755); mkdir(sd, 0755); remove(f); setenv("DM_SAVE", dir, 1);
|
||||
if (!strcmp(mode, "en") || !strcmp(mode, "game")) { FILE *o = fopen(f, "w"); fputs("{\n \"Item Name Display\": 1,\n \"Language\": 0,\n \"AutoMapMode\": 2\n}\n", o); fclose(o); }
|
||||
if (!strcmp(mode, "zh")) { FILE *o = fopen(f, "w"); fputs("{ \"Language\": 8 }", o); fclose(o); }
|
||||
if (!strcmp(mode, "en")) expect("settings file Language 0, game has not set it", dm_ui("dmConfirm"), "OK");
|
||||
else if (!strcmp(mode, "zh")) expect("settings file Language 8 (our text stays English)", dm_ui("dmConfirm"), "OK");
|
||||
else if (!strcmp(mode, "none")) { unsetenv("DM_LANG"); expect("no settings file, no system language given (Mac default English)", dm_ui("dmConfirm"), "OK"); }
|
||||
else if (!strcmp(mode, "sysen")) { setenv("DM_LANG", "en-US", 1); expect("no settings file (first install), system language en-US", dm_ui("dmConfirm"), "OK"); }
|
||||
else if (!strcmp(mode, "systw")) { setenv("DM_LANG", "zh-Hant-TW", 1); expect("no settings file, system language zh-Hant-TW (our text stays English)", dm_ui("dmConfirm"), "OK"); }
|
||||
else if (!strcmp(mode, "tags")) { // language tag → the original game's language index
|
||||
static const struct { const char *t; int want; } c[] = { { "en-US", 0 }, { "en-GB", 0 }, { "zh-Hans-CN", 8 }, { "zh-Hans", 8 }, { "zh", 8 }, { "zh-Hans-HK", 8 }, { "zh-Hans-TW", 8 }, { "zh-Hans-MO", 8 },
|
||||
{ "zh-Hant-TW", 8 }, { "zh-Hant-HK", 8 }, { "zh-Hant", 8 }, { "zh-HK", 8 }, { "zh_TW", 8 }, { "yue-Hant-HK", 8 }, { "yue-Hans-CN", 8 }, { "yue", 8 }, { "wuu-Hans-CN", 8 },
|
||||
{ "de-DE", 0 }, { "es-ES", 0 }, { "es-419", 0 }, { "fr-CA", 0 }, { "it-IT", 0 }, { "ko-KR", 0 }, { "pl-PL", 0 }, { "ru-RU", 0 }, { "ja-JP", 0 }, { "pt-BR", 0 }, { "nl-NL", 0 }, { "ar", 0 }, { "zu-ZA", 0 } };
|
||||
for (unsigned i = 0; i < sizeof c / sizeof *c; i++) { char w[8], g[8]; snprintf(w, sizeof w, "%d", c[i].want); snprintf(g, sizeof g, "%d", dm_lang_from_tag(c[i].t)); expect(c[i].t, g, w); } }
|
||||
else if (!strcmp(mode, "game")) { // the settings file says English, then the game switches to Simplified Chinese, zhTW (9) and deDE: our text stays English throughout
|
||||
expect("before the game sets it, the settings file decides", dm_ui("dmConfirm"), "OK");
|
||||
atomic_store(&g_dm_lang, 8); expect("game switches to zhCN (our text stays English)", dm_ui("dmConfirm"), "OK");
|
||||
atomic_store(&g_dm_lang, 9); expect("game switches to zhTW (our text stays English)", dm_ui("dmConfirm"), "OK");
|
||||
atomic_store(&g_dm_lang, 1); expect("game switches to deDE (English)", dm_ui("dmConfirm"), "OK");
|
||||
expect("a key missing from the table comes back as it is", dm_ui("dmNoSuchKey"), "dmNoSuchKey"); }
|
||||
else { fprintf(stderr, "usage: t_i18n en|zh|none|sysen|systw|tags|game\n"); return 2; }
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Unit test of the controller / touch switch transaction (takes seconds): the state machine and input gate of dm_modetx.h, without touching the game. Expected values are all computed by hand from the rules:
|
||||
// rebuild complete = SETTLE consecutive frames without movement after the hint panel reopen count has moved; if that has not happened within TIMEOUT frames, proceed as a timeout; keys that were held during the freeze are passed to the game only after they have been released once following the unfreeze.
|
||||
#include <stdio.h>
|
||||
#include "../dm_modetx.h"
|
||||
static int fails, checks;
|
||||
#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
int main(void) {
|
||||
{ dm_modetx t = {0}; uint64_t f = 100; // One normal switch: touch (0) → controller (1)
|
||||
CHECK(dm_modetx_step(&t, f, -1, 0, 7) == DM_TXA_NONE, "no request: must not move");
|
||||
CHECK(dm_modetx_step(&t, f, 0, 0, 7) == DM_TXA_NONE, "requested mode is the current one: must not move");
|
||||
CHECK(dm_modetx_step(&t, f, 1, 0, 7) == DM_TXA_BEGIN && t.st == DM_TX_REBUILD1, "request for the other mode should begin");
|
||||
for (int i = 1; i <= 10; i++) CHECK(dm_modetx_step(&t, f + i, 1, 0, 7) == DM_TXA_NONE, "must not proceed before the hint panel reopens (frame %d)", i);
|
||||
CHECK(dm_modetx_step(&t, f + 11, 1, 0, 8) == DM_TXA_NONE, "the frame of the reopen does not count as done");
|
||||
CHECK(dm_modetx_step(&t, f + 13, 1, 0, 9) == DM_TXA_NONE, "reopened again: restart the settle count");
|
||||
CHECK(dm_modetx_step(&t, f + 16, 1, 0, 9) == DM_TXA_NONE, "3 settled frames are not enough");
|
||||
CHECK(dm_modetx_step(&t, f + 17, 1, 0, 9) == DM_TXA_REPORT_BACK && t.st == DM_TX_REBUILD2, "4 settled frames → report back the original run mode");
|
||||
CHECK(dm_modetx_step(&t, f + 30, 0, 1, 9) == DM_TXA_NONE, "a reverse request during the transaction does not interrupt it");
|
||||
CHECK(dm_modetx_step(&t, f + 31, 0, 1, 10) == DM_TXA_NONE, "second rebuild just started");
|
||||
CHECK(dm_modetx_step(&t, f + 35, 0, 1, 10) == DM_TXA_DONE && t.st == DM_TX_IDLE && !t.timeouts, "second rebuild settled → done, no timeout");
|
||||
CHECK(dm_modetx_step(&t, f + 36, 0, 1, 10) == DM_TXA_BEGIN, "the frame after completion handles the queued reverse request"); }
|
||||
{ dm_modetx t = {0}; // Timeout: the hint panel never reopens
|
||||
CHECK(dm_modetx_step(&t, 0, 1, 0, 0) == DM_TXA_BEGIN, "begin");
|
||||
CHECK(dm_modetx_step(&t, DM_MODETX_TIMEOUT - 1, 1, 0, 0) == DM_TXA_NONE, "1 frame short: no timeout");
|
||||
CHECK(dm_modetx_step(&t, DM_MODETX_TIMEOUT, 1, 0, 0) == DM_TXA_REPORT_BACK && t.timeouts == 1, "at the deadline proceed as a timeout and count it");
|
||||
CHECK(dm_modetx_step(&t, 2 * DM_MODETX_TIMEOUT - 1, 1, 0, 0) == DM_TXA_NONE, "second stage restarts the timer");
|
||||
CHECK(dm_modetx_step(&t, 2 * DM_MODETX_TIMEOUT, 1, 0, 0) == DM_TXA_DONE && t.timeouts == 2 && t.st == DM_TX_IDLE, "second stage times out too → done, 2 counted"); }
|
||||
{ dm_modegate g = {0}; uint64_t b; // Input gate: A=0x1 triggers the switch and stays held, B=0x2 is also pressed along the way, and after releasing B it is pressed again
|
||||
b = 0x1; CHECK(dm_modegate_apply(&g, 1, &b) == 1 && b == 0, "frozen: keys cleared, sticks cleared too");
|
||||
b = 0x3; CHECK(dm_modegate_apply(&g, 1, &b) == 1 && b == 0, "keys newly pressed while frozen are not passed either");
|
||||
b = 0x3; CHECK(dm_modegate_apply(&g, 0, &b) == 0 && b == 0, "A and B still held after the unfreeze are not passed");
|
||||
b = 0x5; CHECK(dm_modegate_apply(&g, 0, &b) == 0 && b == 0x4, "B released, A still held: newly pressed X=0x4 is passed as usual, A still not");
|
||||
b = 0x0; CHECK(dm_modegate_apply(&g, 0, &b) == 0 && b == 0, "all released");
|
||||
b = 0x3; CHECK(dm_modegate_apply(&g, 0, &b) == 0 && b == 0x3, "pressed again after a release: passed as usual"); }
|
||||
printf("t_modetx: %d checks, %d failed\n", checks, fails); return fails != 0;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Unit test for WebM duration parsing of cutscene no-op playback (takes seconds): a few container headers are assembled by hand per EBML, and all expected values are computed by hand without calling the implementation under test.
|
||||
// Build and run: make -C dm/host/tests run
|
||||
#include "../hle.h"
|
||||
#include <stdio.h>
|
||||
|
||||
void *guest_sym(const char *n) { (void)n; return NULL; } // stub for the guest call in the file under test (not used by the parsing path)
|
||||
struct dm_flags g_flag; uint64_t g_t0_ns;
|
||||
uint64_t now_ns(void) { return 0; } // log timestamp stub (not used by the parsing path)
|
||||
typedef int64_t (*mv_read_fn)(void *ctx, int64_t off, void *buf, size_t n);
|
||||
int64_t mv_webm_duration_ns(mv_read_fn rd, void *ctx);
|
||||
|
||||
static int fails, checks;
|
||||
#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } else { printf(" ✓ "); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
typedef struct { const uint8_t *p; size_t n; } mem;
|
||||
static int64_t rd_mem(void *c_, int64_t off, void *buf, size_t n) {
|
||||
mem *c = c_; if (off < 0 || (size_t)off >= c->n) return 0;
|
||||
size_t k = c->n - off < n ? c->n - off : n; memcpy(buf, c->p + off, k); return (int64_t)k;
|
||||
}
|
||||
static int64_t dur(const uint8_t *p, size_t n) { mem m = {p, n}; return mv_webm_duration_ns(rd_mem, &m); }
|
||||
|
||||
int main(void) {
|
||||
// (1) Normal: EBML header (empty body) + Segment (known length) { SeekHead (empty) , Info { TimecodeScale=1000000, Duration=float64 6633.0 } }
|
||||
// IEEE754 double of 6633.0 = 0x40B9E90000000000; expected 6633.0 * 1000000 ns = 6,633,000,000 ns (= the 6.633 s ffprobe measures for logoloop)
|
||||
static const uint8_t a[] = {
|
||||
0x1A,0x45,0xDF,0xA3, 0x80, // EBML, length 0
|
||||
0x18,0x53,0x80,0x67, 0x9C, // Segment, length 28 = 5 + 5 + 18
|
||||
0x11,0x4D,0x9B,0x74, 0x80, // SeekHead, length 0
|
||||
0x15,0x49,0xA9,0x66, 0x92, // Info, length 18 = 7 + 11
|
||||
0x2A,0xD7,0xB1, 0x83, 0x0F,0x42,0x40, // TimecodeScale = 0x0F4240 = 1000000
|
||||
0x44,0x89, 0x88, 0x40,0xB9,0xE9,0x00,0x00,0x00,0x00,0x00 // Duration float64 6633.0
|
||||
};
|
||||
CHECK(dur(a, sizeof a) == 6633000000ll, "float64 duration 6633 × 1ms = 6.633 s (read %lld)", (long long)dur(a, sizeof a));
|
||||
// (2) float32 + non-default TimecodeScale: Duration=float32 2000.0 (0x44FA0000), Scale=500000 => 1,000,000,000 ns; Segment of unknown length (0x01FFFFFFFFFFFFFF)
|
||||
static const uint8_t b[] = {
|
||||
0x1A,0x45,0xDF,0xA3, 0x80,
|
||||
0x18,0x53,0x80,0x67, 0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
|
||||
0x15,0x49,0xA9,0x66, 0x8E, // Info, length 14 = 7 + 7
|
||||
0x2A,0xD7,0xB1, 0x83, 0x07,0xA1,0x20, // 0x07A120 = 500000
|
||||
0x44,0x89, 0x84, 0x44,0xFA,0x00,0x00 // float32 2000.0
|
||||
};
|
||||
CHECK(dur(b, sizeof b) == 1000000000ll, "float32 2000 × 0.5ms + unknown-size Segment = 1.000 s (read %lld)", (long long)dur(b, sizeof b));
|
||||
// (3) Default TimecodeScale (absent from Info): Duration=float64 10033.0 (0x40C3988000000000) => 10,033,000,000 ns
|
||||
static const uint8_t c[] = {
|
||||
0x1A,0x45,0xDF,0xA3, 0x80,
|
||||
0x18,0x53,0x80,0x67, 0x90,
|
||||
0x15,0x49,0xA9,0x66, 0x8B,
|
||||
0x44,0x89, 0x88, 0x40,0xC3,0x98,0x80,0x00,0x00,0x00,0x00
|
||||
};
|
||||
CHECK(dur(c, sizeof c) == 10033000000ll, "default TimecodeScale = 1ms, 10033 ⇒ 10.033 s (read %lld)", (long long)dur(c, sizeof c));
|
||||
// (4) Cluster encountered first, no Info => -1 (the caller reports a failure on this and gives no default)
|
||||
static const uint8_t d[] = { 0x1A,0x45,0xDF,0xA3, 0x80, 0x18,0x53,0x80,0x67, 0x85, 0x1F,0x43,0xB6,0x75, 0x80 };
|
||||
CHECK(dur(d, sizeof d) == -1, "no Info, Cluster comes first ⇒ -1 (read %lld)", (long long)dur(d, sizeof d));
|
||||
// (5) No Duration in Info => -1
|
||||
static const uint8_t e[] = { 0x1A,0x45,0xDF,0xA3, 0x80, 0x18,0x53,0x80,0x67, 0x8C, 0x15,0x49,0xA9,0x66, 0x87, 0x2A,0xD7,0xB1, 0x83, 0x0F,0x42,0x40 };
|
||||
CHECK(dur(e, sizeof e) == -1, "no Duration ⇒ -1 (read %lld)", (long long)dur(e, sizeof e));
|
||||
// (6) Truncated (no data after the Segment header) => -1
|
||||
CHECK(dur(a, 10) == -1, "truncated ⇒ -1");
|
||||
printf(fails ? "t_movie failed %d/%d\n" : "t_movie all passed: %d/%d\n", fails ? fails : checks, checks);
|
||||
return fails != 0;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Unit test for random names at character naming (takes seconds): dm_namegen.c. Criterion: create a character 20 times in a row with the name left blank, and the names are all different and all valid.
|
||||
// "Characters already created" are simulated with the taken stub: each name drawn is recorded in the existing list, and drawing it the next time counts as a duplicate. Valid = the game's naming rule (main+0x7fed78): 2–15 bytes, first and last are letters, at most one of - _; our names should contain only English letters.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
int dm_name_gen(char *out, int cap, int (*taken)(const char *));
|
||||
|
||||
static int fails, checks;
|
||||
#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
static char have[20][32]; static int nhave;
|
||||
static int taken(const char *n) { for (int i = 0; i < nhave; i++) if (!strcasecmp(have[i], n)) return 1; return 0; } // the save directory is on a case-insensitive file system
|
||||
static int all_taken(const char *n) { (void)n; return 1; }
|
||||
static int legal(const char *n) { size_t l = strlen(n); if (l < 2 || l > 15) return 0; for (size_t i = 0; i < l; i++) if (!((n[i] >= 'A' && n[i] <= 'Z') || (n[i] >= 'a' && n[i] <= 'z'))) return 0; return 1; }
|
||||
|
||||
int main(void) {
|
||||
for (int i = 0; i < 20; i++) { char nm[32]; int ok = dm_name_gen(nm, sizeof nm, taken); CHECK(ok, "draw %d: no name", i + 1); CHECK(legal(nm), "draw %d: '%s' is not valid", i + 1, nm); CHECK(!taken(nm), "draw %d: '%s' duplicates an existing character", i + 1, nm); strcpy(have[nhave++], nm); }
|
||||
printf(" 20 draws: %s %s %s … %s\n", have[0], have[1], have[2], have[19]);
|
||||
int bad = 0; for (int i = 0; i < 100000; i++) { char nm[32]; dm_name_gen(nm, sizeof nm, NULL); bad += !legal(nm); } CHECK(!bad, "%d invalid out of 100000", bad);
|
||||
char nm[32]; CHECK(!dm_name_gen(nm, sizeof nm, all_taken), "should return 0 when every name is taken");
|
||||
printf("t_namegen: %d checks, %d failed\n", checks, fails); return fails != 0;
|
||||
}
|
||||
@@ -7,16 +7,6 @@
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
uint64_t now_ns(void) { return 0; }
|
||||
// External dependencies of the 3D render scale (scale3d_tick): the unit test has no module table or graphics layer, so they are stubbed as "no main module" -> the function returns immediately
|
||||
module_t g_mod[MAX_MOD]; int g_nmod;
|
||||
void dm_guest_write_prepare(void *a, size_t n) { (void)a; (void)n; }
|
||||
uint64_t dm_nvn_frame_count(void) { return 0; }
|
||||
int dm_nvn_ruler_truth(void) { return 0; }
|
||||
int dm_ingame_now(void) { return 1; }
|
||||
int mtl_fps_target(void) { return 60; } // target frame rate (nvn_mtl.m): always 60 in the unit test, and the 30 fps mode cases pass period_ms directly
|
||||
pthread_key_t g_keyres[2]; // stub for the resolution parameter slot (loader.c): no key created, so taking 31 does not write the thread slot
|
||||
uint64_t dm_game_res(int docked) { (void)docked; return 0; } // stub for the render resolution (dm_uiscreen.c)
|
||||
void dm_uiscreen_res_reread(void) {}
|
||||
void dm_audio_suspend(int on) { (void)on; } // stubs for the bg item of DM_OE_SCRIPT (hle_audio.c / hle_os.c / hle_nvn.c)
|
||||
void guest_clock_freeze_for(int reason, int on) { (void)reason; (void)on; }
|
||||
void dm_gfx_suspend(int on) { (void)on; }
|
||||
@@ -66,70 +56,7 @@ static int child_burst(int kind) {
|
||||
int st; if (waitpid(p, &st, 0) != p) return 0;
|
||||
return WIFEXITED(st) && WEXITSTATUS(st) == 0;
|
||||
}
|
||||
// Dynamic 3D scale controller (scale3d_ctl_step, a pure function): expected values are computed by hand from the rules, not generated by the implementation (rules in hle_oe.c: frame rate over 2-second windows; two consecutive windows < target *0.92 steps down; 10 consecutive windows >= *0.97 steps up; a drop within 10 seconds of stepping up -> the next step-up needs 30 windows)
|
||||
static uint64_t s3_now; static scale3d_ctl s3; static int s3_dm; // s3_dm: cumulative number of level changes
|
||||
static int s3_one(double iv) { int b = s3.level; s3_now += (uint64_t)((iv > 0 ? iv : 16.6667) * 1e6); int lv = scale3d_ctl_step(&s3, iv, s3_now); s3_dm += lv != b; return lv; }
|
||||
static int s3_n(int n, double iv) { int lv = s3.level; for (int i = 0; i < n; i++) lv = s3_one(iv); return lv; }
|
||||
#define F60 16.6667 // 60 fps: 120 frames per window (120 * 16.6667 = 2000.004 ms)
|
||||
// Frame counts of the "25 ms bucket / >=33 ms bucket" for each 10-second window of the second section of real play on a real device (copied by hand from perf lines)
|
||||
static const int s3_rows[18][2] = {{0,1},{2,4},{3,3},{0,0},{10,12},{0,0},{0,0},{0,0},{2,1},{0,0},{6,8},{0,0},{0,0},{0,0},{2,3},{5,4},{0,0},{0,0}};
|
||||
static int s3_replay(int clustered) { // clustered = 1: the slow frames of each 10 seconds are packed consecutively at the start (worst case); 0: spread evenly. Returns the number of level changes
|
||||
s3 = (scale3d_ctl){.max_level = 2}; s3_now = 5000 * S3_SEC; s3_dm = 0;
|
||||
for (int r = 0; r < 18; r++) { int ns = s3_rows[r][0] + s3_rows[r][1], every = ns ? 600 / ns : 0;
|
||||
for (int i = 0; i < 600; i++) { int slow = clustered ? i < ns : (ns && i % every == 0 && i / every < ns); s3_one(slow ? (i % 2 ? 25.0 : 40.0) : F60); } }
|
||||
return s3_dm;
|
||||
}
|
||||
static void test_scale3d(void) {
|
||||
s3 = (scale3d_ctl){.max_level = 2}; s3_now = 1000 * S3_SEC;
|
||||
CHECK(s3_n(1200, F60) == 0, "dynamic scale: steady 60 fps for 20 s → stays at level 0");
|
||||
CHECK(s3_n(199, 20.0) == 0, "dynamic scale: 50 fps (< 55.2) in window 1 + window 2 short by 1 frame → not lowered yet");
|
||||
CHECK(s3_n(1, 20.0) == 1, "dynamic scale: two 2 s windows in a row at 50 fps → lowered to level 1");
|
||||
CHECK(s3_n(100, 20.0) == 1, "dynamic scale: frames within 2 s of a level change do not count (50 fps still does not lower)");
|
||||
CHECK(s3_n(200, 20.0) == 2, "dynamic scale: after the hold ends, two more windows in a row at 50 fps → lowered to level 2");
|
||||
CHECK(s3_n(1000, 25.0) == 2, "dynamic scale: at the floor, further drops stay at level 2");
|
||||
s3 = (scale3d_ctl){.max_level = 2, .level = 2}; s3_now = 1500 * S3_SEC; // test stepping up from a clean level 2 (without the half bad window the previous section left in the window)
|
||||
CHECK(s3_n(1199, F60) == 2, "dynamic scale: 60 fps for a little over 9 windows in a row → not raised yet");
|
||||
CHECK(s3_n(1, F60) == 1, "dynamic scale: 10 windows in a row (20 s) ≥ 58.2 → raised to level 1");
|
||||
s3 = (scale3d_ctl){.max_level = 2}; s3_now = 2000 * S3_SEC;
|
||||
for (int k = 0; k < 10; k++) { s3_n(100, 20.0); s3_n(120, F60); }
|
||||
CHECK(s3.level == 0, "dynamic scale: bad and good windows alternating (never two bad in a row) → not lowered");
|
||||
s3 = (scale3d_ctl){.max_level = 2, .level = 1}; s3_now = 2500 * S3_SEC;
|
||||
CHECK(s3_n(1200 * 3, 1000.0 / 57) == 1, "dynamic scale: 57 fps (between 55.2 and 58.2) for 60 s → neither lowered nor raised");
|
||||
s3 = (scale3d_ctl){.max_level = 2}; s3_now = 3000 * S3_SEC;
|
||||
s3_n(90, F60); s3_one(500.0); s3_n(120 * 3, F60);
|
||||
CHECK(s3.level == 0, "dynamic scale: one 500 ms stall in a window (about 45 frames there), next window normal → not lowered (needs two in a row)");
|
||||
s3 = (scale3d_ctl){.max_level = 2}; s3_now = 3500 * S3_SEC; int lv = 0;
|
||||
for (int i = 0; i < 2000; i++) lv = s3_one(i % 4 == 0 ? -1 : F60);
|
||||
CHECK(lv == 0, "dynamic scale: frames dropped by the system (−1) not counted, the rest at 60 fps → not lowered");
|
||||
for (int i = 0; i < 2000; i++) lv = s3_one(0);
|
||||
CHECK(lv == 0, "dynamic scale: all frames without a reading (0) → no change");
|
||||
// Debounce: knocked back down within 10 seconds of stepping up -> the next step-up needs 30 windows (60 seconds)
|
||||
s3 = (scale3d_ctl){.max_level = 2, .level = 1}; s3_now = 4000 * S3_SEC;
|
||||
CHECK(s3_n(1200, F60) == 0, "debounce: 20 s of good windows → raised to level 0");
|
||||
s3_n(120, F60); // judging is suspended for 2 seconds after a level change
|
||||
CHECK(s3_n(200, 20.0) == 1, "debounce: two 50 fps windows in a row about 6 s after going up → back to level 1");
|
||||
s3_n(120, F60);
|
||||
CHECK(s3_n(2400, F60) == 1, "debounce: dropped right after going up, then 40 s of good windows → not raised (needs 60 s)");
|
||||
CHECK(s3_n(1200, F60) == 0, "debounce: a full 60 s of good windows → raised to level 0");
|
||||
s3_n(120, F60); s3_n(120 * 6, F60); // drops only 12 seconds (> 10 seconds) after stepping up
|
||||
CHECK(s3_n(200, 20.0) == 1, "debounce: dropped only 12 s after going up → lowered to level 1");
|
||||
s3_n(120, F60);
|
||||
CHECK(s3_n(1200, F60) == 0, "debounce: not a drop right after going up, 20 s of good windows raise it back");
|
||||
// 30 fps mode: target 30, step-down line 27.6, step-up line 29.1
|
||||
s3 = (scale3d_ctl){.max_level = 2, .period_ms = 1000.0 / 30}; s3_now = 6000 * S3_SEC;
|
||||
CHECK(s3_n(600, 33.34) == 0, "30 fps tier: 33.34 ms (60 frames per window) steady for 20 s → stays at level 0");
|
||||
CHECK(s3_n(100, 40.0) == 1, "30 fps tier: 25 fps (< 27.6) two windows in a row → lowered to level 1");
|
||||
// Injection: the rds run treats the interval of a stretch of frames as 25 ms (40 fps)
|
||||
s3 = (scale3d_ctl){.max_level = 2}; s3_now = 7000 * S3_SEC;
|
||||
CHECK(s3_n(160, 25.0) == 1 && s3_n(80, 25.0) == 1 && s3_n(160, 25.0) == 2, "inject 25 ms: 4 s to level 1, 2 s hold, then 4 s more to level 2");
|
||||
s3_n(120, F60);
|
||||
CHECK(s3_n(1200, F60) == 1 && (s3_n(120, F60), s3_n(1200, F60)) == 0, "injection over: up one level every 20 s, back to level 0");
|
||||
int a = s3_replay(0), b = s3_replay(1);
|
||||
printf(" (replay of real play, part two: slow frames spread evenly changed level %d times, bunched together %d times; the old controller did 12 times on the device)\n", a, b);
|
||||
CHECK(a == 0 && b == 0, "dynamic scale: the slow-frame sequence from real play part two (59+ fps) changes no level, spread out or bunched together");
|
||||
}
|
||||
int main(void) {
|
||||
test_scale3d();
|
||||
unsetenv("DM_OE_SCRIPT"); reset();
|
||||
CHECK(empty(), "empty queue at start: returns false and leaves the output alone");
|
||||
dm_focus_set(1); dm_set_opmode(1); CHECK(empty(), "same state sends no notification");
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Physical controller face button mapping (dm_padmap.h): each type x four face buttons -> game keys. Expected values are hand-written from
|
||||
// the rule: whatever letter is printed is that game key; Nintendo glyphs are right A, bottom B, top X, left Y, Xbox glyphs are bottom A,
|
||||
// right B, left X, top Y; everything else is laid out as Xbox.
|
||||
#include <stdio.h>
|
||||
#include "../dm_padmap.h"
|
||||
static int checks, fails;
|
||||
#define CHECK(c, ...) do { checks++; if (c) printf(" ✓ "); else { fails++; printf(" ✗ "); } printf(__VA_ARGS__); printf("\n"); } while (0)
|
||||
int main(void) {
|
||||
struct { const char *cat, *ven; int kind; } id[] = {
|
||||
{ "Switch Pro Controller", "Pro Controller", DM_PADKIND_NINTENDO }, { "Nintendo Switch Joy-Con (L/R)", "Joy-Con (L/R)", DM_PADKIND_NINTENDO },
|
||||
{ "Nintendo Switch Joy-Con (L)", "Joy-Con (L)", DM_PADKIND_NINTENDO }, { "MFi", "NINTENDO compatible pad", DM_PADKIND_NINTENDO },
|
||||
{ "Xbox One", "Xbox Wireless Controller", DM_PADKIND_XBOX }, { "DualSense", "DualSense Wireless Controller", DM_PADKIND_XBOX },
|
||||
{ "DualShock 4", "DUALSHOCK 4 Wireless Controller", DM_PADKIND_XBOX }, { "MFi", "SteelSeries Nimbus", DM_PADKIND_XBOX }, { NULL, NULL, DM_PADKIND_XBOX }, { "", "", DM_PADKIND_XBOX } };
|
||||
for (unsigned i = 0; i < sizeof id / sizeof *id; i++) CHECK(dm_padmap_kind(id[i].cat, id[i].ven) == id[i].kind, "category '%s' vendor name '%s' → %s layout", id[i].cat ? id[i].cat : "(empty)", id[i].ven ? id[i].ven : "(empty)", id[i].kind ? "Nintendo" : "Xbox");
|
||||
// bottom right left top
|
||||
CHECK(dm_padmap_face(DM_PADKIND_NINTENDO, 1, 0, 0, 0) == DM_BTN_B, "Nintendo: bottom = B"); CHECK(dm_padmap_face(DM_PADKIND_NINTENDO, 0, 1, 0, 0) == DM_BTN_A, "Nintendo: right = A");
|
||||
CHECK(dm_padmap_face(DM_PADKIND_NINTENDO, 0, 0, 1, 0) == DM_BTN_Y, "Nintendo: left = Y"); CHECK(dm_padmap_face(DM_PADKIND_NINTENDO, 0, 0, 0, 1) == DM_BTN_X, "Nintendo: top = X");
|
||||
CHECK(dm_padmap_face(DM_PADKIND_XBOX, 1, 0, 0, 0) == DM_BTN_A, "Xbox: bottom = A"); CHECK(dm_padmap_face(DM_PADKIND_XBOX, 0, 1, 0, 0) == DM_BTN_B, "Xbox: right = B");
|
||||
CHECK(dm_padmap_face(DM_PADKIND_XBOX, 0, 0, 1, 0) == DM_BTN_X, "Xbox: left = X"); CHECK(dm_padmap_face(DM_PADKIND_XBOX, 0, 0, 0, 1) == DM_BTN_Y, "Xbox: top = Y");
|
||||
CHECK(dm_padmap_face(DM_PADKIND_XBOX, 1, 1, 0, 0) == (DM_BTN_A | DM_BTN_B), "two pressed at once: each counted on its own"); CHECK(dm_padmap_face(DM_PADKIND_NINTENDO, 0, 0, 0, 0) == 0, "none pressed = 0");
|
||||
printf("== %d checks, %d failed ==\n", checks, fails);
|
||||
return fails != 0;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#include "../romfs_validate.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
static int check(int ok, const char *s) { if (!ok) fprintf(stderr, "FAILED: %s\n", s); return ok; }
|
||||
int main(void) {
|
||||
const char *src = getenv("DM_ROMFS"); if (!src) src = getenv("HOME");
|
||||
if (!src) return 1;
|
||||
char cmd[4096], why[512], rep[2048], sd[4096]; snprintf(sd, sizeof sd, "%s/data", src);
|
||||
if (!check(dm_romfs_validate(sd, why, sizeof why), "original RomFS passes")) { fprintf(stderr, "%s\n", why); return 1; }
|
||||
if (!check(dm_romfs_report(sd, rep, sizeof rep) == 0, "full list: original RomFS has nothing missing")) return 1;
|
||||
{ // Empty directory: none of the 18 archives is present. Expected values come from the need / size tables in romfs_validate.c: 00-00000001.data 1073741824 bytes = 1.00 GB, 17-00000002.data 4496514 bytes = 4 MB
|
||||
char r1[] = "/tmp/dm-romfs-test.XXXXXX"; if (!mkdtemp(r1)) return 1;
|
||||
if (!check(!dm_romfs_validate(r1, why, sizeof why) && !strcmp(why, "Missing 00-00000001.data"), "empty directory rejected, first archive reported")) { fprintf(stderr, "%s\n", why); return 1; }
|
||||
int bad = dm_romfs_report(r1, rep, sizeof rep);
|
||||
if (!check(bad == 18 && strstr(rep, "0 / 18 files present") && strstr(rep, "00-00000001.data (1.00 GB)") && strstr(rep, "17-00000002.data (4 MB)") && !strstr(rep, "Incomplete"), "full list: all 18 missing listed one by one")) { fprintf(stderr, "%s\n", rep); return 1; }
|
||||
// Small files such as the config, key and manifests are outside the scope of the check: put in a copy with wrong contents, and neither the verdict nor
|
||||
// the list shown to the player changes, and their names do not appear in the list
|
||||
snprintf(cmd, sizeof cmd, "printf bad > '%s/.build.config' && printf x > '%s/build_key.txt' && printf x > '%s/archive_manifest.psv'", r1, r1, r1); if (system(cmd)) return 1;
|
||||
bad = dm_romfs_report(r1, rep, sizeof rep);
|
||||
if (!check(bad == 18 && !strstr(rep, "build") && !strstr(rep, "psv"), "full list: small files not mentioned")) { fprintf(stderr, "%s\n", rep); return 1; }
|
||||
snprintf(cmd, sizeof cmd, "rm -rf '%s'", r1); system(cmd);
|
||||
}
|
||||
{ // Archive files alone (no config, key or any manifest) are enough to pass; then check byte counts and zero-stripped archives: build a copy with symlinks, then truncate one small archive / replace it with "zero-stripped + extent table"
|
||||
char r2[] = "/tmp/dm-romfs-size.XXXXXX"; if (!mkdtemp(r2)) return 1;
|
||||
snprintf(cmd, sizeof cmd, "for f in '%s'/data/*.data; do ln -s \"$f\" '%s/' || exit 1; done", src, r2); if (system(cmd)) return 1;
|
||||
if (!check(dm_romfs_validate(r2, why, sizeof why), "directory with only the archive files passes")) { fprintf(stderr, "%s\n", why); return 1; }
|
||||
snprintf(cmd, sizeof cmd, "rm '%s/04-00000002.data' && head -c 1000000 '%s/data/04-00000002.data' > '%s/04-00000002.data'", r2, src, r2); if (system(cmd)) return 1;
|
||||
if (!check(!dm_romfs_validate(r2, why, sizeof why) && strstr(why, "is incomplete"), "half-copied archive rejected")) { fprintf(stderr, "%s\n", why); return 1; }
|
||||
int bad = dm_romfs_report(r2, rep, sizeof rep);
|
||||
if (!check(bad == 1 && strstr(rep, "17 / 18 files present") && strstr(rep, "Incomplete: 04-00000002.data (1 MB / 9 MB)"), "full list: incomplete archives listed separately with present / expected size (1000000 bytes = 1 MB, 8915855 bytes = 9 MB, divided by 1048576 and rounded)")) { fprintf(stderr, "%s\n", rep); return 1; }
|
||||
snprintf(cmd, sizeof cmd, "printf 'size 8915855\\n1000000 7915855\\n' > '%s/04-00000002.data.holes'", r2); if (system(cmd)) return 1;
|
||||
if (!check(dm_romfs_validate(r2, why, sizeof why), "zero-stripped archive (first 1000000 bytes + extent table) passes")) { fprintf(stderr, "%s\n", why); return 1; }
|
||||
snprintf(cmd, sizeof cmd, "printf 'size 8915855\\n1000000 100\\n' > '%s/04-00000002.data.holes'", r2); if (system(cmd)) return 1;
|
||||
if (!check(!dm_romfs_validate(r2, why, sizeof why) && strstr(why, "is incomplete"), "extent table that does not match the actual length rejected")) { fprintf(stderr, "%s\n", why); return 1; }
|
||||
snprintf(cmd, sizeof cmd, "rm -rf '%s'", r2); system(cmd);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <stdio.h>
|
||||
|
||||
Result swkbd_show(void *out, const void *arg, int (*input)(const char *, const char *, const char *, const char *, int, char *, int));
|
||||
void dm_ui_text_entered(const char *text) { (void)text; } // the real implementation is in dm_cursor.c (the naming page auto-accepts once input is finished)
|
||||
int front_text_input(const char *t, const char *s, const char *g, const char *i, int m, char *o, int c) { (void)t; (void)s; (void)g; (void)i; (void)m; (void)o; (void)c; return -1; }
|
||||
|
||||
static int fails, checks;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// Automatic monster targeting unit test: lay out fake units / fake monster data in this process's memory, call the observation gate callback, and check the score adjustment (expected values computed by hand, not by calling the implementation under test).
|
||||
// Layout: unit +0 type, +4 class number, +0x10 -> monster data (+0x1A TypeFlag: 0x02 super unique / 0x04 champion / 0x08 unique / 0x10 minion).
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
int gate_obs_present(int id) { (void)id; return 1; }
|
||||
static void (*g_fn)(const uint64_t *);
|
||||
void gate_obs_set(int id, void (*fn)(const uint64_t *)) { (void)id; g_fn = fn; }
|
||||
void target_rank_install(void);
|
||||
static int fails, cases;
|
||||
static float run(const void *unit, float score) {
|
||||
uint64_t frame[100] = {0}; frame[1] = (uint64_t)(uintptr_t)unit; frame[2] = 1;
|
||||
memcpy(&frame[32], &score, 4); g_fn(frame);
|
||||
float out; memcpy(&out, &frame[32], 4); return out;
|
||||
}
|
||||
static void expect(const char *what, float got, float want) {
|
||||
cases++; if (got == want) printf("✓ %s (%g)\n", what, got); else { fails++; printf("✗ %s: got %g, expected %g\n", what, got, want); }
|
||||
}
|
||||
int main(void) {
|
||||
target_rank_install();
|
||||
uint8_t data[0x40] = {0};
|
||||
struct { uint32_t type, cls, id, mode; uint64_t data; } unit = {1, 5, 77, 0, (uint64_t)(uintptr_t)data};
|
||||
expect("normal zombie (class 5, TypeFlag 0): score unchanged", run(&unit, 0.5f), 0.5f);
|
||||
data[0x1a] = 0x10; expect("minion (0x10) counts as normal", run(&unit, 0.5f), 0.5f);
|
||||
data[0x1a] = 0x04; expect("champion (0x04) +1000", run(&unit, 0.5f), 1000.5f);
|
||||
data[0x1a] = 0x08; expect("unique (0x08) +1000", run(&unit, 0.25f), 1000.25f);
|
||||
data[0x1a] = 0x02; expect("super unique (0x02) +1000", run(&unit, 0.75f), 1000.75f);
|
||||
data[0x1a] = 0; unit.cls = 156; expect("Andariel (class 156, boss=1) +2000", run(&unit, 0.5f), 2000.5f);
|
||||
unit.cls = 745; expect("new 3.3 Boss Colossus (class 745) +2000", run(&unit, 0.125f), 2000.125f);
|
||||
unit.cls = 5; unit.type = 0; data[0x1a] = 0x08; expect("player unit (type 0) untouched", run(&unit, 0.5f), 0.5f);
|
||||
unit.type = 1; unit.data = 0; data[0x1a] = 0; expect("null monster data pointer: nothing to read, untouched", run(&unit, 0.5f), 0.5f);
|
||||
expect("null candidate unit untouched", run(NULL, 0.5f), 0.5f);
|
||||
// Across tiers: the worst elite score (0.01+1000) is still higher than the best normal score (0.99); the worst Boss score is higher than the best elite score
|
||||
unit.data = (uint64_t)(uintptr_t)data; data[0x1a] = 0x04; float elite = run(&unit, 0.01f);
|
||||
data[0x1a] = 0; unit.cls = 211; float boss = run(&unit, 0.01f);
|
||||
cases++; if (elite > 0.99f && boss > 1000.99f) printf("✓ priority across tiers: elite %g > normal 0.99, Boss %g > elite 1000.99\n", elite, boss); else { fails++; printf("✗ priority across tiers\n"); }
|
||||
printf("%s: %d/%d\n", fails ? "[RED] auto-targeting" : "auto-targeting all green", cases - fails, cases);
|
||||
return fails != 0;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// dm_textbox_core unit test: all expectations are hand-written. U+E07E = EE 81 BE.
|
||||
// Criterion: whole strings / substrings of the annotated version found in the table are replaced by the text box version; strings outside the table (including the original game's own coded strings and strings without codes) are untouched byte for byte; with no table nothing is touched.
|
||||
#include "../dm_textbox_core.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define C "\xee\x81\xbe"
|
||||
static int fails, checks;
|
||||
static void expect(const char *what, const char *in, int want_k, const char *want) {
|
||||
char *o; size_t on; int k = dm_textbox_rewrite(in, strlen(in), &o, &on); checks++;
|
||||
int ok = k == want_k && (want ? (o && on == strlen(want) && !memcmp(o, want, on)) : !o);
|
||||
if (!ok) { fails++; printf("✗ %s: got k=%d '%.*s'\n", what, k, o ? (int)on : 0, o ? o : ""); } else printf("✓ %s\n", what);
|
||||
free(o);
|
||||
}
|
||||
int main(void) {
|
||||
// The table rows and inputs below are game text from the Chinese D2R client (Hongye mod), written as UTF-8 escapes so the bytes stay identical:
|
||||
// an "enhanced damage 60-70%" range line, two unique item names, "Found Chronicle item: ...!", "and",
|
||||
// the game's whisper line "You whisper to <someone>: hello", and a two-line text "line one" / "line two".
|
||||
char path[] = "/tmp/t_textbox_XXXXXX"; int fd = mkstemp(path); FILE *f = fdopen(fd, "w");
|
||||
fprintf(f, "# annotated version\ttext box version\n");
|
||||
fprintf(f, "%s\t%s\n", C "5[\xe5\xbc\xba\xe4\xbc\xa4" "60-70%]\\n" C "4\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2", "\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2"); // range line + name
|
||||
fprintf(f, "%s\t%s\n", C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a", "\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a"); // Hongye mod name color
|
||||
fclose(f);
|
||||
expect("no table: coded string untouched", C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a", 0, NULL);
|
||||
if (dm_textbox_load(path) != 2) { printf("✗ table entry count is not 2\n"); return 1; }
|
||||
expect("whole annotated string in the table → text box version", C "5[\xe5\xbc\xba\xe4\xbc\xa4" "60-70%]\n" C "4\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2", 1, "\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2");
|
||||
expect("whole Hongye name-color string → plain name", C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a", 1, "\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a");
|
||||
expect("annotated text inside a longer sentence: only that part is replaced", "\xe5\x8f\x91\xe7\x8e\xb0\xe7\xbc\x96\xe5\xb9\xb4\xe5\x8f\xb2\xe7\x89\xa9\xe5\x93\x81\xef\xbc\x9a" C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a" "\xef\xbc\x81", 2, "\xe5\x8f\x91\xe7\x8e\xb0\xe7\xbc\x96\xe5\xb9\xb4\xe5\x8f\xb2\xe7\x89\xa9\xe5\x93\x81\xef\xbc\x9a\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a\xef\xbc\x81");
|
||||
expect("two occurrences in one sentence: both replaced", C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a" "\xe5\x92\x8c" C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a", 2, "\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a\xe5\x92\x8c\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a");
|
||||
expect("the original game's own coded string (whisper) is not in the table: untouched", C "0\xe4\xbd\xa0\xe6\x82\x84\xe6\x82\x84\xe5\x9c\xb0\xe5\xaf\xb9" C "1\xe6\x9f\x90\xe4\xba\xba" C "0\xe8\xaf\xb4: \xe4\xbd\xa0\xe5\xa5\xbd", 0, NULL);
|
||||
expect("string without codes: untouched", "\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2", 0, NULL);
|
||||
expect("multi-line original text (no codes): untouched", "\xe7\xac\xac\xe4\xb8\x80\xe8\xa1\x8c\n\xe7\xac\xac\xe4\xba\x8c\xe8\xa1\x8c", 0, NULL);
|
||||
expect("looks annotated but is not in the table: untouched", C "5[\xe5\xbc\xba\xe4\xbc\xa4" "1-2%]\n" C "4\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2", 0, NULL);
|
||||
unlink(path); dm_textbox_reset();
|
||||
expect("after clearing the table: untouched", C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a", 0, NULL);
|
||||
printf("TEXTBOX %s checks=%d failures=%d\n", fails ? "FAIL" : "PASS", checks, fails);
|
||||
return fails != 0;
|
||||
}
|
||||
@@ -18,10 +18,8 @@ static void t_free(void *p) { if (!p) return; atomic_fetch_sub(&g_live_blocks, 1
|
||||
#undef calloc
|
||||
#undef free
|
||||
|
||||
pthread_key_t g_key18, g_keytls, g_keytlr, g_keygate, g_keyres[2], g_keyaspect, g_keyaspectui;
|
||||
pthread_key_t g_key18, g_keytls, g_keytlr, g_keygate;
|
||||
void dm_gate(void) {}
|
||||
uint64_t dm_game_res(int i) { (void)i; return 0; }
|
||||
uint64_t front_game_res(int i) { (void)i; return 0; }
|
||||
|
||||
static int fails, checks;
|
||||
#define EXPECT(name, want, got) do { long w_ = (long)(want), g_ = (long)(got); checks++; printf("%-46s expected %-8ld → got %-8ld %s\n", name, w_, g_, w_ == g_ ? "✓" : "✗"); if (w_ != g_) fails++; } while (0)
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "../dm.h"
|
||||
#include "../touch_mac.c"
|
||||
#include "../../ios/Sources/dm_touch_controls.c"
|
||||
|
||||
uint64_t now_ns(void) { return 0; }
|
||||
void (*dm_pad_hook)(uint64_t, uint64_t *, int32_t *, int32_t *, int32_t *, int32_t *);
|
||||
uint64_t guest_ns(void) { return 0; }
|
||||
uint64_t dm_guest_frame(void) { return 0; } // the UI mouse / front-end paths run on the presented-frame cadence; this unit test does not test them
|
||||
int dm_world_has_target(void) { return 1; } // stub for world tapping (dm_world.c): has a target = the primary button sends its key as usual
|
||||
unsigned dm_world_tap(float nx, float ny) { (void)nx; (void)ny; return 0; }
|
||||
int dm_shop_open(void) { return 0; } // stub for the vendor page (dm_actions.c): not open
|
||||
int dm_padmode_current(void) { return 0; } // Stub for controller / touch mode (dm_padmode.c): touch
|
||||
uint64_t dm_padmode_page_keys(uint64_t m) { (void)m; return 0; } void dm_padmode_legend_hide(const char *l, int a) { (void)l; (void)a; } int dm_padmode_busy(void) { return 0; } uint64_t dm_padmode_page_held(void) { return 0; } void dm_padmode_page_pulse(uint64_t m) { (void)m; } void dm_padmode_ident_hint(int on) { (void)on; } void dm_padmode_legend(int a, const char *t) { (void)a; (void)t; } const char *dm_str(uint8_t *b, const char *k) { (void)b; return k; } // Stubs for controller page keys / button legend / string fetch (used by controller paging of the personal stash, not reached under touch)
|
||||
int dm_swapskill_wset(void) { return -1; }
|
||||
int dm_pages_cur(void) { return 0; } int dm_pages_count(void) { return 5; } void dm_pages_request_flip(int d) { (void)d; } // stubs for the multi-page personal stash (dm_pages.c)
|
||||
void dm_vault_ui_flip(int dir) { (void)dir; }
|
||||
int dm_vault_ui_pager_hit(int x, int y) { (void)x; (void)y; return 0; }
|
||||
void dm_vault_request_deposit(uint32_t id) { (void)id; }
|
||||
uint32_t dm_vault_ui_held(void) { return 0; }
|
||||
int dm_vault_ui_over(int x, int y) { (void)x; (void)y; return 0; }
|
||||
int dm_vault_ui_tip_gate(int x, int y) { (void)x; (void)y; return 0; }
|
||||
int dm_pouch_tip_gate(int x, int y) { (void)x; (void)y; return 0; }
|
||||
int dm_vault_ui_blocks(int x, int y) { (void)x; (void)y; return 0; }
|
||||
int dm_vault_ui_tap(int x, int y) { (void)x; (void)y; return 0; }
|
||||
int dm_vault_ui_drag(int phase, int x, int y) { (void)phase; (void)x; (void)y; return 0; }
|
||||
uint32_t dm_swapskill_itemsig(void) { return 0; } // stub for the current weapon set of the swap-set skill (dm_actions.c): cannot be read
|
||||
_Atomic int g_dm_frz_want, g_dm_frozen; void dm_freeze_frame(int want) { (void)want; } // stub for the panel freeze (diag.c)
|
||||
void dm_elysium_arm(int on) { (void)on; } // stub for the Elysium checkbox (dm_elysium.c)
|
||||
void dm_custom_on_uimsg(const char *cat, const char *act, const char *arg) { (void)cat; (void)act; (void)arg; } // stub for the "Custom" settings (dm_custom.c)
|
||||
void dm_belt_put(unsigned id) { (void)id; }
|
||||
void dm_ui_rect(int *x, int *y, int *w, int *h) { *x = *y = *w = *h = 0; }
|
||||
int dm_screen_real(int *w, int *h) { (void)w; (void)h; return 0; }
|
||||
int dm_uiscreen_ingame(void) { return 0; }
|
||||
void dm_uiscreen_set_ingame(int on) { (void)on; } // stub for the UI rectangle (dm_uiscreen.c): true full screen not enabled
|
||||
void dm_inv_sort(int page) { (void)page; }
|
||||
int dm_diag_difficulty(void) { return -1; }
|
||||
uint64_t dm_fs_commit_count(void) { return 0; }
|
||||
void dm_held_equip(int bodyloc) { (void)bodyloc; }
|
||||
int dm_rift_merc_unlocked(void) { return 1; }
|
||||
int dm_rift_merc_slot_empty(int loc) { (void)loc; return 0; }
|
||||
void dm_rift_merc_locked_hint(void) {}
|
||||
void dm_stash_take(int count) { (void)count; }
|
||||
void dm_shop_buy(void) {}
|
||||
int dm_cursor_item(void *out) { (void)out; return 0; }
|
||||
void dm_shop_sell_cursor(void) {}
|
||||
module_t g_mod[MAX_MOD]; int g_nmod;
|
||||
static void (*ui_cb)(const char *, const char *, const char *);
|
||||
static uint32_t test_tick;
|
||||
uint32_t dm_cur_tick(void) { return test_tick; }
|
||||
static int test_loadscreen_down;
|
||||
int dm_loadscreen_down(void) { return test_loadscreen_down; } // the real check is in diag.c; the unit test sets it by hand following the loading screen messages
|
||||
int dm_loadscreen_up(void) { return 0; } // in the unit test the loading screen does not block starting a run: the freeze is not triggered
|
||||
void dm_hold_for_loadscreen(void) {}
|
||||
void uimsg_subscribe(void (*f)(const char *, const char *, const char *)) { ui_cb = f; }
|
||||
static int checks, fails;
|
||||
#define CHECK(c, s) do { checks++; if (!(c)) { fails++; printf("FAIL %s\n", s); } else printf("PASS %s\n", s); } while (0)
|
||||
|
||||
int main(void) {
|
||||
const char *path = "build/touch_host.script";
|
||||
FILE *f = fopen(path, "w"); if (!f) return 2;
|
||||
fputs("0 down 0 90,1220\n0.1 move 0 150,1220\n2.1 up 0\n", f); fclose(f);
|
||||
setenv("DM_TOUCH_SCRIPT", path, 1); unsetenv("DM_TOUCH_TEST_DROP");
|
||||
uint64_t b = 0; int32_t lx = 0, ly = 0, rx = 0, ry = 0;
|
||||
test_tick = 100;
|
||||
dm_touch_host_step(0, &b, &lx, &ly, &rx, &ry);
|
||||
ui_cb("FrontEndNavigation", "ToGame", "");
|
||||
ui_cb("PanelManager", "UnloadPanel", "LoadScreenPanel"); test_loadscreen_down = 1;
|
||||
dm_touch_host_step(0, &b, &lx, &ly, &rx, &ry);
|
||||
CHECK(lx == 0 && ly == 0, "down only sets the stick origin");
|
||||
for (test_tick = 101; test_tick <= 162; test_tick++) {
|
||||
b = 0; lx = ly = rx = ry = 0;
|
||||
dm_touch_host_step(0, &b, &lx, &ly, &rx, &ry);
|
||||
}
|
||||
CHECK(lx > 0 && ly == 0, "move writes the host left stick");
|
||||
test_tick = 162;
|
||||
b = 0; lx = ly = rx = ry = 0; dm_touch_host_step(0, &b, &lx, &ly, &rx, &ry);
|
||||
CHECK(lx > 0 && ly == 0, "repeated sampling in the same tick does not advance script time");
|
||||
test_tick = 212;
|
||||
b = 0; lx = ly = rx = ry = 0; dm_touch_host_step(0, &b, &lx, &ly, &rx, &ry);
|
||||
CHECK(lx == 0 && ly == 0, "up clears the host left stick");
|
||||
// Cleanup of the mode switch transaction (the "leave" registered in dm_mode_items.h): key presses on the player's behalf queued by touch are voided on a switch; ones the host sends by itself, such as the single "Show Items" press on entering a run, are not among them
|
||||
{ uint64_t dm_ui_auto_buttons(void); void dm_ui_key_pulse(uint64_t, int), dm_ui_auto_mode_leave(void), dm_ui_cache_mode_leave(void);
|
||||
dm_ui_key_pulse(0x40, 1000); CHECK(dm_ui_auto_buttons() == 0x40, "touch button key pulse in flight: the key is among the keys pressed on the player's behalf");
|
||||
dm_ui_auto_mode_leave(); CHECK(dm_ui_auto_buttons() == 0, "key pulses in flight are voided by the switch cleanup");
|
||||
dm_ui_cache_mode_leave(); CHECK(1, "UI cache cleanup does not crash without a UI root"); }
|
||||
printf("== touch host %d checks, %d failed ==\n", checks, fails);
|
||||
return fails != 0;
|
||||
}
|
||||
void dm_shop_get(dm_shop_info *out) { memset(out, 0, sizeof *out); } // vendor readout (dm_actions.c): all 0
|
||||
void dm_item_use(unsigned id) { (void)id; }
|
||||
int dm_ui_hover_now(int x, int y) { (void)x; (void)y; return 0; }
|
||||
void dm_item_equip(unsigned id) { (void)id; }
|
||||
void dm_item_unequip(unsigned id) { (void)id; }
|
||||
int dm_held_size(int *w, int *h) { (void)w; (void)h; return 0; }
|
||||
int dm_item_targeting(void) { return 0; }
|
||||
int dm_vendor_repair_mode(void) { return 0; }
|
||||
void dm_item_repair(uint32_t id) { (void)id; }
|
||||
void dm_belt_fill(uint64_t w) { (void)w; }
|
||||
void dm_item_identify(unsigned id) { (void)id; }
|
||||
void dm_item_target_cancel(void) {}
|
||||
void dm_item_compare(int on) { (void)on; }
|
||||
void dm_held_stow(void) {}
|
||||
int dm_held_stow_result(void) { return -1; }
|
||||
int dm_held_pending(void) { return 0; }
|
||||
void uimsg_emit(const char *cat, const char *act, const char *arg) { (void)cat; (void)act; (void)arg; }
|
||||
void dm_item_identify_free(unsigned id) { (void)id; } // stub for free identify (dm_actions.c)
|
||||
int dm_runeword_long_press(const void *ci) { (void)ci; return 0; } // stub for the runeword helper (dm_runeword.c)
|
||||
int dm_runeword_list_open(void) { return 0; }
|
||||
void dm_runeword_button(const char *name) { (void)name; }
|
||||
void dm_runeword_list_close(void) {}
|
||||
@@ -1,91 +0,0 @@
|
||||
#include <stdint.h>
|
||||
|
||||
uint32_t dm_cur_tick(void) { return 0; }
|
||||
int dm_loadscreen_down(void) { return 1; } // the loading screen check is in diag.c; the touch point unit test treats it as already down
|
||||
// World tapping (dm_world.c): these unit tests carry no game image, so empty stubs are given
|
||||
void dm_world_pump(void) {}
|
||||
int dm_world_has_target(void) { return 1; }
|
||||
unsigned dm_world_tap(float nx, float ny) { (void)nx; (void)ny; return 0; }
|
||||
void dm_textinput_pump(void) {} // text box hookup (dm_textinput.c): same as above, an empty stub
|
||||
void dm_update_pump(void) {} // build number and new-version notice on the character select page (dm_update.c): same as above, an empty stub
|
||||
void dm_num_set(int n) { (void)n; }
|
||||
void dm_actions_pump(void) {}
|
||||
uint64_t dm_swapskill_filter(uint64_t b) { return b; } // swap-set skill (dm_actions.c): does not suppress keys
|
||||
uint64_t dm_runeword_pad_filter(uint64_t b) { return b; } // Runeword list in controller mode (dm_runeword.c): swallow no buttons
|
||||
uint64_t dm_padmode_page_filter(uint64_t b) { return b; } // Our own pages in controller mode (dm_padmode.c): swallow no buttons
|
||||
int dm_padmode_input_gate(uint64_t *b) { (void)b; return 0; } // Input gate of the controller / touch switch transaction (dm_padmode.c): not switching
|
||||
int dm_padmode_current(void) { return 0; } // Controller / touch mode (dm_padmode.c): touch
|
||||
uint64_t dm_padmode_page_keys(uint64_t m) { (void)m; return 0; } void dm_padmode_legend_hide(const char *l, int a) { (void)l; (void)a; } int dm_padmode_busy(void) { return 0; } uint64_t dm_padmode_page_held(void) { return 0; } void dm_padmode_page_pulse(uint64_t m) { (void)m; } void dm_padmode_ident_hint(int on) { (void)on; } void dm_padmode_legend(int a, const char *t) { (void)a; (void)t; } const char *dm_str(uint8_t *b, const char *k) { (void)b; return k; } // Stubs for controller page keys / button legend / string fetch (used by controller paging of the personal stash, not reached under touch)
|
||||
int dm_swapskill_wset(void) { return -1; }
|
||||
int dm_pages_cur(void) { return 0; } int dm_pages_count(void) { return 5; } void dm_pages_request_flip(int d) { (void)d; } // stubs for the multi-page personal stash (dm_pages.c)
|
||||
void dm_vault_ui_flip(int dir) { (void)dir; }
|
||||
int dm_vault_ui_pager_hit(int x, int y) { (void)x; (void)y; return 0; }
|
||||
void dm_vault_request_deposit(uint32_t id) { (void)id; }
|
||||
uint32_t dm_vault_ui_held(void) { return 0; }
|
||||
int dm_vault_ui_over(int x, int y) { (void)x; (void)y; return 0; }
|
||||
int dm_vault_ui_tip_gate(int x, int y) { (void)x; (void)y; return 0; }
|
||||
int dm_pouch_tip_gate(int x, int y) { (void)x; (void)y; return 0; }
|
||||
int dm_vault_ui_blocks(int x, int y) { (void)x; (void)y; return 0; }
|
||||
int dm_vault_ui_tap(int x, int y) { (void)x; (void)y; return 0; }
|
||||
int dm_vault_ui_drag(int phase, int x, int y) { (void)phase; (void)x; (void)y; return 0; }
|
||||
uint32_t dm_swapskill_itemsig(void) { return 0; } // same as above: the current weapon set cannot be read
|
||||
void dm_shop_buy(void) {}
|
||||
void dm_shop_sell_cursor(void) {}
|
||||
void dm_stat_add(int s, int n) { (void)s; (void)n; }
|
||||
void dm_skill_add_selected(void) {}
|
||||
void dm_test_hold_first_sellable(void) {}
|
||||
void dm_belt_put(unsigned id) { (void)id; }
|
||||
void dm_test_sell_first(void) {}
|
||||
int dm_shop_open(void) { return 0; }
|
||||
_Atomic int g_dm_frz_want, g_dm_frozen; void dm_freeze_frame(int want) { (void)want; } // stub for the panel freeze (diag.c)
|
||||
int dm_cursor_item(void *out) { (void)out; return 0; } // item under the cursor (dm_cursor_item.c): none
|
||||
void dm_test_belt_first_potion(void) {}
|
||||
void dm_shop_get(void *out) { __builtin_memset(out, 0, 320); } // vendor readout (dm_actions.c): all 0 (dm_shop_info is under 320 bytes)
|
||||
void dm_test_use_first(int kind) { (void)kind; }
|
||||
void dm_test_hover_at(float nx, float ny) { (void)nx; (void)ny; }
|
||||
int dm_ui_hover_now(int x, int y) { (void)x; (void)y; return 0; }
|
||||
void dm_item_compare(int on) { (void)on; }
|
||||
void dm_belt_expand(int on) { (void)on; }
|
||||
void dm_inv_sort(int page) { (void)page; }
|
||||
int dm_diag_difficulty(void) { return -1; }
|
||||
uint64_t dm_fs_commit_count(void) { return 0; }
|
||||
void dm_held_equip(int bodyloc) { (void)bodyloc; }
|
||||
int dm_rift_merc_unlocked(void) { return 1; }
|
||||
int dm_rift_merc_slot_empty(int loc) { (void)loc; return 0; }
|
||||
void dm_rift_merc_locked_hint(void) {}
|
||||
void dm_name_random(void) {}
|
||||
void dm_item_use(unsigned id) { (void)id; }
|
||||
void dm_test_equip(int what) { (void)what; }
|
||||
void dm_item_equip(unsigned id) { (void)id; }
|
||||
void dm_item_unequip(unsigned id) { (void)id; }
|
||||
int dm_held_size(int *w, int *h) { (void)w; (void)h; return 0; }
|
||||
int dm_item_targeting(void) { return 0; }
|
||||
int dm_vendor_repair_mode(void) { return 0; }
|
||||
void dm_item_repair(uint32_t id) { (void)id; }
|
||||
void dm_belt_fill(uint64_t w) { (void)w; }
|
||||
void dm_test_identify(int what) { (void)what; }
|
||||
uint64_t dm_diag_srv_item(uint32_t id) { (void)id; return 0; }
|
||||
void dm_belt_use(int col, int row) { (void)col; (void)row; }
|
||||
void dm_aim_cone_pump(void) {}
|
||||
void dm_item_identify(unsigned id) { (void)id; }
|
||||
void dm_item_target_cancel(void) {}
|
||||
void dm_elysium_arm(int on) { (void)on; } // Elysium checkbox (dm_elysium.c): empty stub in the unit test
|
||||
void dm_custom_on_uimsg(const char *cat, const char *act, const char *arg) { (void)cat; (void)act; (void)arg; } // stub for the "Custom" settings (dm_custom.c)
|
||||
void dm_test_stash_take(int n) { (void)n; }
|
||||
void dm_stash_take(int n) { (void)n; }
|
||||
float dm_ui_ny(float ny) { return ny; }
|
||||
int dm_screen_real(int *w, int *h) { (void)w; (void)h; return 0; }
|
||||
void dm_uiscreen_enable(int m) { (void)m; }
|
||||
void dm_ui_rect(int *x, int *y, int *w, int *h) { *x = *y = *w = *h = 0; }
|
||||
int dm_uiscreen_ingame(void) { return 0; }
|
||||
void dm_uiscreen_set_ingame(int on) { (void)on; }
|
||||
void dm_held_stow(void) {}
|
||||
int dm_held_stow_result(void) { return -1; }
|
||||
void dm_test_swap_held(void) {}
|
||||
int dm_held_pending(void) { return 0; }
|
||||
void uimsg_emit(const char *cat, const char *act, const char *arg) { (void)cat; (void)act; (void)arg; }
|
||||
void dm_item_identify_free(unsigned id) { (void)id; } // stub for free identify (dm_actions.c)
|
||||
int dm_runeword_long_press(const void *ci) { (void)ci; return 0; } // stub for the runeword helper (dm_runeword.c)
|
||||
int dm_runeword_list_open(void) { return 0; }
|
||||
void dm_runeword_button(const char *name) { (void)name; }
|
||||
void dm_runeword_list_close(void) {}
|
||||
@@ -1,54 +0,0 @@
|
||||
// Unit test for reading "unit -> the level it is in" (dm_unit.h, takes seconds).
|
||||
// Situation: during the loading screen of an act change the client player "has a path but no room". The old code read four levels in one go, and with a null room went on to read address 0x18 -> sig=11 addr=0x18.
|
||||
// Fail -> pass: with the same set of inputs, the old code (old_level, copied verbatim from the pre-fix line) runs in a child process and must die of SIGSEGV; the new code returns 0 (unknown level).
|
||||
#include "dm_unit.h"
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int fails, checks;
|
||||
#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
static uint64_t rd(uint64_t a) { return a ? *(volatile uint64_t *)(uintptr_t)a : 0; }
|
||||
static int old_level(uint64_t u) { uint64_t r = rd(rd(rd(rd(u + 0x38) + 0x20) + 0x18) + 0x90); return r ? *(int32_t *)(uintptr_t)(r + 0x1f8) : 0; } // exactly as it was before the fix
|
||||
// Run the old code in a child process: returns which signal it died of (0 = returned normally)
|
||||
static int old_dies(uint64_t u) {
|
||||
fflush(stdout); pid_t p = fork(); if (p == 0) { signal(SIGSEGV, SIG_DFL); signal(SIGBUS, SIG_DFL); volatile int v = old_level(u); (void)v; _exit(0); }
|
||||
int st = 0; waitpid(p, &st, 0); return WIFSIGNALED(st) ? WTERMSIG(st) : 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static uint8_t unit[0x200], path[0x100], room[0x100], ex[0x100], level[0x300];
|
||||
// A complete chain: level id 59
|
||||
*(uint64_t *)(unit + 0x38) = (uint64_t)(uintptr_t)path; *(uint64_t *)(path + 0x20) = (uint64_t)(uintptr_t)room;
|
||||
*(uint64_t *)(room + 0x18) = (uint64_t)(uintptr_t)ex; *(uint64_t *)(ex + 0x90) = (uint64_t)(uintptr_t)level; *(int32_t *)(level + 0x1f8) = 59;
|
||||
uint64_t u = (uint64_t)(uintptr_t)unit;
|
||||
CHECK(dm_unit_level(u) == 59, "complete chain should read 59, read %d", dm_unit_level(u));
|
||||
CHECK(dm_unit_level_ptr(u) == (uint64_t)(uintptr_t)level, "complete chain should return the level struct pointer");
|
||||
CHECK(old_level(u) == 59, "old code should also read 59 on the complete chain (control)");
|
||||
|
||||
// (1) Has a path, room is null (the client player during an act-change loading)
|
||||
*(uint64_t *)(path + 0x20) = 0;
|
||||
int sg = old_dies(u); CHECK(sg == SIGSEGV || sg == SIGBUS, "old code should crash (red) on 'path present, room null', actual signal %d", sg);
|
||||
CHECK(dm_unit_level(u) == 0 && dm_unit_level_ptr(u) == 0, "new code should return 0 on 'path present, room null'");
|
||||
// (2) Path is null
|
||||
*(uint64_t *)(path + 0x20) = (uint64_t)(uintptr_t)room; *(uint64_t *)(unit + 0x38) = 0;
|
||||
sg = old_dies(u); CHECK(sg == SIGSEGV || sg == SIGBUS, "old code should crash (red) on 'path null', actual signal %d", sg);
|
||||
CHECK(dm_unit_level(u) == 0 && dm_unit_level_ptr(u) == 0, "new code should return 0 on 'path null'");
|
||||
// (3) Unit is null
|
||||
sg = old_dies(0); CHECK(sg == SIGSEGV || sg == SIGBUS, "old code should crash (red) on 'unit null', actual signal %d", sg);
|
||||
CHECK(dm_unit_level(0) == 0 && dm_unit_level_ptr(0) == 0, "new code should return 0 on 'unit null'");
|
||||
// (4) Room present, room +0x18 null; (5) +0x90 null (the last two steps of the chain)
|
||||
*(uint64_t *)(unit + 0x38) = (uint64_t)(uintptr_t)path; *(uint64_t *)(room + 0x18) = 0;
|
||||
CHECK(dm_unit_level(u) == 0, "room +0x18 null should return 0");
|
||||
*(uint64_t *)(room + 0x18) = (uint64_t)(uintptr_t)ex; *(uint64_t *)(ex + 0x90) = 0;
|
||||
CHECK(dm_unit_level(u) == 0, "null level pointer should return 0");
|
||||
*(uint64_t *)(ex + 0x90) = (uint64_t)(uintptr_t)level;
|
||||
CHECK(dm_unit_level(u) == 59, "should read 59 again after relinking");
|
||||
// Taking the room chain head when the level pointer is null: returns 0 (previously each place did rd(lv + 0x10) directly and read address 0x10)
|
||||
CHECK(dm_level_rooms(0) == 0, "room list head should be 0 for a null level pointer");
|
||||
*(uint64_t *)(level + 0x10) = (uint64_t)(uintptr_t)room; CHECK(dm_level_rooms((uint64_t)(uintptr_t)level) == (uint64_t)(uintptr_t)room, "non-null level should read the room at +0x10");
|
||||
printf("t_unit_level: %d checks, %d failed\n", checks, fails); return fails != 0;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Unit test for the widget type check (dm_widget.h), runs in seconds. The fake "main module" is a block of memory; a widget's first word = base + vtable offset.
|
||||
#include "dm_widget.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static int fails, checks;
|
||||
#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
int main(void) {
|
||||
static uint8_t base[16]; static uint64_t w_toggle[4], w_text[4], w_slot[4], w_slotb[4], w_slotc[4], w_other[4];
|
||||
w_toggle[0] = (uint64_t)(uintptr_t)base + DM_VT_TOGGLE; w_text[0] = (uint64_t)(uintptr_t)base + DM_VT_TEXT; w_other[0] = (uint64_t)(uintptr_t)base + 0x1234;
|
||||
w_slot[0] = (uint64_t)(uintptr_t)base + DM_VT_SLOT; w_slotb[0] = (uint64_t)(uintptr_t)base + DM_VT_SLOT_B; w_slotc[0] = (uint64_t)(uintptr_t)base + DM_VT_SLOT_C;
|
||||
uint64_t tg = (uint64_t)(uintptr_t)w_toggle, tx = (uint64_t)(uintptr_t)w_text, ot = (uint64_t)(uintptr_t)w_other;
|
||||
|
||||
CHECK(dm_widget_vt(base, tg) == DM_VT_TOGGLE, "toggle vtable offset should read %#x, read %#llx", DM_VT_TOGGLE, (unsigned long long)dm_widget_vt(base, tg));
|
||||
CHECK(dm_widget_is(base, tg, DM_VT_TOGGLE), "toggle should be recognized as a toggle");
|
||||
CHECK(!dm_widget_is(base, tx, DM_VT_TOGGLE), "text box must not be recognized as a toggle (this happens when a widget of the same name changes class)");
|
||||
CHECK(dm_widget_is(base, tx, DM_VT_TEXT), "text box should be recognized as a text box");
|
||||
CHECK(!dm_widget_is(base, ot, DM_VT_TOGGLE) && !dm_widget_is(base, ot, DM_VT_TEXT), "unknown classes are never recognized");
|
||||
CHECK(!dm_widget_is(base, 0, DM_VT_TOGGLE) && dm_widget_vt(base, 0) == 0, "null widget: not recognized, vtable reads 0");
|
||||
CHECK(!dm_widget_is(NULL, tg, DM_VT_TOGGLE) && dm_widget_vt(NULL, tg) == 0, "no base address: not recognized, vtable reads 0");
|
||||
CHECK(dm_widget_is_slot(base, (uint64_t)(uintptr_t)w_slot) && dm_widget_is_slot(base, (uint64_t)(uintptr_t)w_slotb) && dm_widget_is_slot(base, (uint64_t)(uintptr_t)w_slotc), "all three item slot kinds recognized as item slots");
|
||||
CHECK(!dm_widget_is_slot(base, tg) && !dm_widget_is_slot(base, 0), "toggle / null widget is not an item slot");
|
||||
// Vtable values of the classes are pairwise distinct (one mistyped number would make two classes match each other)
|
||||
{ const uint64_t v[] = { DM_VT_TEXT, DM_VT_INPUT, DM_VT_NUMINPUT, DM_VT_TOGGLE, DM_VT_TABBAR, DM_VT_SLOT, DM_VT_SLOT_B, DM_VT_SLOT_C, DM_VT_IMAGE, DM_VT_BUTTON }; int dup = 0;
|
||||
for (unsigned i = 0; i < sizeof v / sizeof *v; i++) for (unsigned j = i + 1; j < sizeof v / sizeof *v; j++) dup += v[i] == v[j];
|
||||
CHECK(dup == 0, "%d duplicate pairs among the vtable constants", dup); }
|
||||
|
||||
printf("t_widget: %d checks, %d failed\n", checks, fails); return fails != 0;
|
||||
}
|
||||
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user