在 iPhone 上運行 Dark Souls:作者記錄 Madeira 的修復工作
Running Dark Souls on an iPhone: what I had to fix in Madeira
作者記錄在 iPhone Air 上透過 Madeira 運行 PC 版 Dark Souls 的修復過程,並公開工具庫 madeira-kit(https://github.com/meshoklv/madeira-kit)。
Since late September I have been running PC builds of Dark Souls on an iPhone Air through the Madeira emulator. Remastered is now playable with an on-screen gamepad. In heavy scenes of Dark Souls III the median went from roughly 20 to 36.7 FPS on an experimental build. Along the way I collected 5 merged and 11 open PRs against Madeira, Wine, FEX and DXMT, plus a repository of scripts, which I'm publishing now.
A caveat up front: I have one phone (an iPhone Air on iOS 27.2 beta) and one tester, me, so every number below comes from that single device. The games are my own, bought legitimately. The repository contains no game files, no save editors and no copy-protection circumvention, and I won't accept any.
What Madeira is
Madeira runs x64 Windows programs directly on an iPhone. It has three layers:
- FEX-Emu translates x86-64 to ARM64 on the fly (JIT);
- Wine provides the Windows API;
- DXMT turns Direct3D 11 calls into Metal.
For JIT on a non-jailbroken phone I use StikDebug. Madeira's built-in helper needs a separate App ID, and my signing script strips it.
The project is strong, but "install it and run a heavy game" doesn't work out of the box. You need to know which entitlements to re-sign the app with, which of the dozens of config keys actually change anything, and how to tell from a log what went wrong. I wrote that unwritten knowledge down and automated the boring parts.
Dark Souls Remastered: eight problems in a row
I worked on it from September 27 to October 19, 2026. Every step looked the same: one run, one log, one change. A small script, tools/analyze-log.py, reads the log and reports what Madeira actually accepted from the environment, the module load order, exceptions with register dumps, whether graphics, window and audio came up, and how the run ended.
The full table of causes is in docs/bugs-and-fixes.md. Here are the ones I remember best.
Crash at startup, c0000018. The exe has a fixed base below 64 MB, but it needs a window at 0x140000000, and that range was not being handed out. This was already fixed upstream (Madeira #111), so I only had to pull it in.
Crash after the first writes to code. The game is protected by a layer that rewrites its own code at runtime, and that made everything fall over. The cause turned out to be a leak in Wine: on the [exec-req] return path in ARM64EC ntdll, leave_syscall_callback() was never called, so the "we are inside a syscall" flag stayed set. A one-line fix, wine #18, merged.
Crash in steamclient64, c0000005. This one is more interesting. The Steam client truncates an HMODULE to 32 bits, and iOS can't map memory below the 4 GB line, so the module lives at a high address and the truncated pointer points nowhere. I made an image with a "narrowed" base get a low-address name, and made VirtualQuery report as free only the memory that can actually be taken. That is Madeira #385, still open. Because of it, my phone build is upstream plus one unmerged commit, and without that commit DSR crashes at startup.
Crash while looking up the settings folder. The Wine profile C:\users\<user> simply had no AppData. A script creates the folders (device.py profile fix), and apply-config.sh calls it too.
Gamepad: three bugs in one chain
This took the longest, because the symptoms differed and three independent places were at fault.
- The game doesn't see the gamepad. The game folder contains its own
xinput1_3.dll, and the loader picked it over the built-in one. Fix: the loader prefers the built-in XInput (wine #19, merged). - The game sees the gamepad but ignores it. The game window wasn't active, and on iOS there is nobody to activate it. Now
win32umakes the process's main window active while it polls XInput (Madeira #149, merged). - A tap that missed an on-screen button turned into a mouse click. While the gamepad layout is shown, such taps no longer count as mouse input (Madeira #150, merged).
Two things helped: return addresses in the diagnostics, and logging of the key-press chain. That is how the gap was found. I now keep such diagnostic patches around.
A crash 35 seconds into the settings menu
This was the emulator's own watchdog. It counted handled self-modifying-code writes cumulatively, and in the settings menu the game rewrites code every frame, cycling through seven keys. It reached the threshold of 2000 in about half a minute, and the process was killed. Now only consecutive identical failures on one thread are counted (Madeira #148, merged).
After these fixes I was listed in Madeira's own Settings > Credits: "Controller fixes ... and a crash-guard fix".
Two lessons from this stretch. First: check what is actually loaded, not what the config says. Image size and base address in the log lie less often than the settings file. Second: a clean CI build beats a build assembled from whatever was lying around. One of my builds, made from a mix of old and new parts, crashed in signal_set_full_context, while the clean CI one did not.
Dark Souls III: from 20 to 36.7 FPS
DS3 is heavier. Steam is installed from inside Madeira (Madeira Dock) with your own account. Cutscenes run at 60 FPS; in combat on default settings it was 17-20.
From there it was a measurement exercise. I broke logs down by phase: FPS, GPU load, core frequencies, draw calls per frame. The parsers are in tools/ds3perf/, and the conclusions, each tagged "fact" or "hypothesis", are in docs/perf/. There are 22 notes there, in Russian.
The phone heats up and clamps its clocks
The least pleasant finding, from DS3-POWER.md. In one long session, the P-cores ran at 2.8 GHz for the first seconds and gave about 34 FPS. Around the 59th second a clamp kicked in: frequency dropped toward 1.8 GHz and median FPS fell to 20.6. At 175 seconds iOS reported the serious thermal state, which changed nothing further. So any optimization has to be measured on a hot phone, otherwise the numbers are pretty and useless.
What worked
I named the change sets with letters. This is the table from docs/compatibility.md, median in heavy scenes:
| Step | FPS | What changed |
|---|---|---|
| base set C2 | ~17-20 | pool separation, no image copying, orphaned locks get flagged |
| G1 | 21.5 | lighter diagnostics, in-process synchronization, fast path for x64 critical sections, write windows for hot code pages |
| H2 | 36.7 | buffer and command-list reuse in DXMT, fast TLS, lighter SRW locks, thread QoS by name |
For H2 in heavy scenes: median 36.7 FPS, p10 28.8 over the first minutes of a 12-minute run. After warm-up, with the P-cores sitting at 1.31 GHz, it holds 32-34 FPS. The GPU is about 88% busy, so now it is the bottleneck rather than the CPU.
Now the honest part. The 36.7 figure is the sum of several changes. I did not measure the contribution of each one separately, and every PR says so explicitly. G1 had a 26-minute run with no hangs and a median of 21.5.
What I tried and dropped:
- MetalFX: got worse;
-
pool-low: got worse; - skipping self-modifying-code writes that have the same bytes: pointless, since 93% of the writes change the bytes.
A separate note about DXMT. It turned out to create about a thousand Metal buffers per frame, and roughly 50 times per frame it allocates and frees 2 MB for command lists. Two keys (DXMT_DYN_KEEP and DXMT_CMDLIST_KEEP_BLOCK) remove that churn, and both are off by default (dxmt #42, #43).
Black screen and a hang in DS3
Two separate DXMT bugs, both described in DS3-CRASH.md and DS3-FREEZE.md.
- Black screen: when asked for an SRV description covering "all mip levels", DXMT returned
MipLevels = -1. The game takes that number as the level count and allocates an array from it, then crashes. Now the resolved value is returned (dxmt #40). - DS3 hang: a backport of a fix from 3Shain/dxmt, where the draw-argument marshaling loops for GS/TS did not advance (dxmt #41).
I have not fully tested the gamepad or saves in DS3; the repository lists this under "not tested".
What's in the repository
madeira-kit is tooling around the loop of build, sign, install, configure, run, read the log, change one thing. It is not a fork of Madeira: I sent fixes upstream, and patches/ only holds reference copies.
-
unpack.sh,swap-dll.sh,sign.sh,install.sh: unpack a build, drop in patched DLLs, re-sign with your own certificate, install while keeping app data; -
doctor.sh: a pre-flight check (phone visible, app installed, at least 3 GB free, game copy present); -
apply-config.sh: uploadmadeira.cfg, read it back and compare, create any missing profile folders; -
pull-log.shandanalyze-log.py: fetch the log and analyze it right away; - ready-made profiles for DS1 and DS3 in
configs/.
The minimal path:
git clone https://github.com/meshoklv/madeira-kit && cd madeira-kit
cp scripts/config.example.env scripts/config.env && $EDITOR scripts/config.env
scripts/unpack.sh ~/Downloads/Madeira-build.zip && scripts/sign.sh && scripts/install.sh
scripts/doctor.sh
scripts/apply-config.sh configs/ds1-play.cfg
# swipe Madeira away, enable JIT in StikDebug, start the game
scripts/pull-log.sh first-run
You need a Mac with Xcode command line tools, an iPhone on USB, a certificate and provisioning profile for your bundle id, and pip3 install pymobiledevice3. The entitlements for extended memory and extended virtual address space require a paid developer account.
If you build Madeira on GitHub Actions yourself, the pitfalls are described in docs/guide.md: LLVM as static libraries only, TUNE_CPU=cortex-a78 for FEX, and a stub for ri_page_wait_time_mach. The Windows part of DXMT is not rebuilt by this CI.
What I don't know
- The scripts are my working pipeline with personal identifiers removed. After that I syntax-checked them, but I did not re-run them end to end on a device.
- Other iPhone models and iOS versions are untested, as is a fresh clone of the repo on a second machine.
- I did not measure FPS or heat in Dark Souls Remastered itself, only "it's playable".
- All DS3 numbers come from one phone and a handful of runs, mostly a single run per configuration.
- A free developer account may not grant the required entitlements.
How to help
If you have a different iPhone or iPad, send a compatibility report: model, iOS version, game, config and result. Profiles with measured numbers are welcome. Cracks, game files and anything that bypasses protection are not.
Part of the code and patches was written together with an AI assistant (Claude); in the commits of the upstream PRs this shows as a Co-Authored-By line.
Repository: github.com/meshoklv/madeira-kit, MIT license for the scripts and docs; the patches follow the licenses of the projects they target. Issues and PRs are open. I post notes and new builds on Telegram at t.me/meshoklv. If the article was useful, a star on the repo helps others find it.
Mike Meshok - github.com/meshoklv - meshoklv.github.io
來源:DEV Community · dev.to