Skip to content

Add Windows ARM64 support (under x64 emulation) - #93

Open
ndabas wants to merge 2 commits into
ModOrganizer2:masterfrom
ndabas:master
Open

ndabas wants to merge 2 commits into
ModOrganizer2:masterfrom
ndabas:master

Conversation

@ndabas

@ndabas ndabas commented Sep 24, 2026 •

Copy link
Copy Markdown

I was trying to use MO2 with Skyrim SE + SKSE on my Windows ARM64 laptop, which crashes silently.

Further testing and analyzing crash dumps has shown that usvfs currently fails on x64 targets on Windows ARM64. The DLL injects successfully, then crashes in DllMain before it can write anything to the log. It looks like the cause is an assumption in the x64 injection stub which doesn't hold true under emulation.

I have added RSP normalization to fix this issue, and have also added a windows-11-arm runner to the test matrix so we can verify that it works.

Root cause

addStub() emits a bootstrap that inherits the hijacked thread's RSP and never normalises it:

  • push return address (8 bytes)
  • pushAll (pushf + 15 registers) (128 bytes)
  • shadow space (sub rsp, 32) (32 bytes)
  • Total: 168 bytes

168 mod 16 == 8, and the x64 ABI requires 0, so the stub is only correct when the thread is hijacked at RSP mod 16 = 8.

MO2 creates targets with CREATE_SUSPENDED, so on native x64 the thread is frozen at RtlUserThreadStart and that assumption holds. Under x64 emulation on ARM64 the initial context is synthesised by the emulator (as it's really an ARM64 thread not x64) and it seems with the opposite parity, consistently, so every frame below the stub is skewed by 8 bytes and the first function to spill an SSE register with MOVAPS faults.

Evidence

Crash is deterministic with the same fault offset across Skyrim reinstalls and MO2 versions.

usvfs_x64!spdlog::details::registry::registry+0x19     <-- movaps [rsp+90h], xmm6
usvfs_x64!spdlog::details::registry::instance+0x5a
usvfs_x64!spdlog::drop                                  (inlined)
usvfs_x64!InitLoggingInternal+0xa6
usvfs_x64!InitHooks+0x3b                                <-- returns into the stub (non-module page)

At the fault: rsp = 0xb60815f2f8, target rsp+0x90 = 0xb60815f388, mod 16 == 8 where MOVAPS requires 0. RBP is likewise 8 short of its compile-time-known residue, confirming the whole frame is displaced rather than a single bad address.

Environment: Windows 11 ARM64 build 28000, xtajit64se.dll loaded, MO2 2.5.2, Skyrim SE via SKSE.

Interestingly, the trampolines and inline hooks work fine under emulation, which I initially thought might be a problem, but apparently not!

CI

Failing run on my fork, without the change.
Passing run on my fork, with the change.

Test binaries

I have added a release to my fork of usvfs with compiled DLLs that can be dropped in to an existing MO2 2.5.2 install: https://github.com/ndabas/modorganizer/releases -- another build with the latest master is also there.

A complete MO2 2.5.2 release with the patch is also available: https://github.com/ndabas/modorganizer/releases

The injected bootstrap inherits the hijacked thread's RSP alignment. pushAll only moves RSP by a multiple of 16, so a misaligned incoming stack propagates through LoadLibrary, GetProcAddress and the init function. Callees then fault on their first aligned SSE spill (movaps), which happens consistently when injecting into x64 processes under ARM64 emulation.

Normalise RSP after pushAll and restore it from rbp on both exit paths.
@ndabas

ndabas commented Sep 24, 2026

Copy link
Copy Markdown
Author

On a side note, I added a small change to my branch to get the CI to run (otherwise PR runs in all forks will always fail), but I have removed that commit from this PR branch. Let me know if you want to keep it instead, possibly with some hardening so it doesn't silently ignore missing secrets when run in the main repo.

@ndabas
ndabas force-pushed the master branch 2 times, most recently from d8e4209 to dc7db6a Compare September 24, 2026 15:33
@skjiisa

skjiisa commented Oct 3, 2026

Copy link
Copy Markdown

I've been working on getting MO2 running on the Steam Frame and hit this same issue, and this fix worked. For context, the Steam Frame has an ARM CPU and runs Windows applications through Proton and the FEX emulator. Using the usvfs_Release_x64 from run 35981022682 in @ndabas's fork worked on the Steam Frame as long as I installed the VC++ 2015–2022 x64 redistributable.

@ndabas

ndabas commented Oct 3, 2026 •

Copy link
Copy Markdown
Author

@skjiisa thanks, that's really interesting because the bug and fix are now validated on an entirely different emulator as well now -- which confirms my hypothesis that this isn't an emulation bug, it was an issue in the code not adhering to the x64 ABI, which was running fine on real x64 hardware by coincidence, not by design.

For anyone wanting to try this out, I have added links to the the usvfs patched DLLs and a complete MO2 2.5.2 package with the patch to the PR description.

skjiisa added a commit to skjiisa/ModSync that referenced this pull request Oct 3, 2026
Programs started through MO2 2.5.2 die on ARM64 because USVFS's injection
stub calls InitHooks with a misaligned stack. This downloads ndabas's
v0.5.6.1-woa.1 backport of ModOrganizer2/usvfs#93, pinned by archive and
per-file SHA-256, replaces only the four stock MO2 2.5.2 USVFS files, and
keeps verified originals in <instance>/.modsync-usvfs/backup. Available from
the dashboard, the Steam launch hub and `modsync mo2 usvfs
status|apply|restore`; `doctor` reports it. Changes are refused while any
process maps the instance's USVFS files.

Validated on a Steam Frame: modded Skyrim through Play and MO2, the in-use
check under Proton ARM64 through the Flatpak, hub Apply/Restore, and offline
reapply. Fixes a doctor crash found there.

Closes #62.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@flopku2026-ux

flopku2026-ux commented Oct 6, 2026 •

Copy link
Copy Markdown

Hello! I've recently tried using your fixed version of MO2 ( https://github.com/ndabas/modorganizer/releases ) and while it seems to work smoothly, I've run into a problem with Pandora acting up when launched.

Screenshot 2026-10-05 204234 Screenshot 2026-10-05 230140

For whatever reason, it loads up as a transparent window (not including the visible purple outline). I've attempted this both on the current version of Pandora and an older version (4.1.2). Neither avenue seemed to work, and now I'm thinking the fact that I'm running this all on an ARM system (Surface Pro x) may have something to do with it. Do you have any insight on this sort of thing? That, or would you be willing to test Pandora on an ARM system with the version of MO2 you put together?

Any and all help is appreciated!

@ndabas

ndabas commented Oct 14, 2026

Copy link
Copy Markdown
Author

@flopku2026-ux unfortunately that appears to be a different issue from the one fixed in this PR. The one that I've fixed here prevents games from launching at all; if it's launching but then behaving oddly, it's a different problem somewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants