Skip to content

feat(relinker): add --module-dir for modules loaded at run time - #846

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
merwie:feat/relinker-module-dir
Oct 11, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
merwie:feat/relinker-module-dir

Conversation

@merwie

@merwie merwie commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds --module-dir <dir> to the relinker, for modules a title keeps outside sce_module/sce_modules/prx. Since #2708 the relinker looks for guest modules only in those directories.

Unity titles keep two kinds of modules elsewhere:

  • Media/Modules: modules the executable needs at startup (Il2cppUserAssemblies.prx, PS5Util.prx).
  • Media/Plugins: native plugins, loaded at run time with sceKernelLoadStartModule. In PPSA12544, an unrelinked plugin shows up as DllNotFoundException: Unable to load DLL 'PSN' for /app0/Media/Plugins/PSN.prx.

How the option works:

  • --module-dir <dir> (repeatable) relinks the ELF modules of a directory under the module parent directory (--sce-module-path, by default the input's directory). Outputs keep the relative path, and feat(libkernel): load the relinked module for a guest module path #717 then finds them as <path>.guest.prx. The directory must exist inside the parent directory and differ from the other module directories. The option conflicts with --skip-sce-module.
  • Needed modules: a module there that the executable or another module needs is initialized at startup, like the bundled modules.
  • Other modules are initialized when the title loads them. The Windows writer exports their DT_INIT entry and init array as __aps5_guest_initialize, and dlopen calls it once, with the start arguments sceKernelLoadStartModule records for the Linux DT_INIT stub (36c979b), so *res gets the entry's result. On Linux these modules are left out of DT_NEEDED, and that stub passes the arguments. The plugins check them: PPSA12544's PSN plugin is loaded with sceKernelLoadStartModule(path, 16, {16, 0x200, ...}) and refuses to start without them (PSN is an old version that cannot be used by the current player runtime).
  • USAGE.md documents the option. TechnicalDebt covers modules that another module needs being initialized at startup, and macOS not passing start arguments.

Design question open for the maintainers: an explicit option vs. auto-discovery (see the comments).

Tested

  • Windows 11, PPSA12544, relinked with --windows --module-dir Media/Modules --module-dir Media/Plugins on main 4a747c4 plus feat(agc): depth planes sampled as 2D arrays, cubes and integer views #425. All 12 guest modules are converted. The Media/Modules modules start at launch and the plugins on load. The title boots without the DllNotFoundException, the SaveData plugin mounts its save data, and the title menu renders at about 60 fps (bring-up: Hollow Knight: Silksong (PPSA12544) #726).

  • test_guest_module_dir.py covers:

    • the option and the relative outputs;
    • the deferred initialization export, and its absence for bundled and needed modules;
    • DT_NEEDED on Linux;
    • the rejected directories.

    It fails on main (unknown option), and the needed-module case fails without the startup rule.

  • Non-GPU ctest passes (460), and the conventions check passes.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

🤖 Generated with Claude Code

@merwie

merwie commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

On the overlap report with #684: the two changes are independent. Both add a field after UsePlatformTlsResolver in GuestRuntime (#684 Names, this PR DeferInitialization) and a line in the same per-image runtime setup in GuestModuleBuilder::Build. This PR does not use the exports map that #684 changes. Whichever lands second keeps both lines; I will merge main here if #684 lands first.

@011010

011010 commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Independent verification (relinker-only, builds anywhere).

Built the relinker from pr846 standalone on macOS 26.6.2 (arm64), Apple Clang 21, C++20.

  • test_guest_module_dir.py passes; the other 21 relinker Python tests in the tree pass too.
  • Against main (5895ea7) the same test fails at the first case with FAIL: unknown option: --module-dir, so it exercises the new flag rather than passing vacuously.

One thing to make explicit before merge: this branch carries test_guest_needed_modules.py, which is added by #716 (still open), so it is stacked on #716. The PR lists no Depends on, but merging it before #716 would pull #716's changes in with it. Worth stating the order or rebasing once #716 lands.

No real Unity title was tested here (no dump).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks. #684 has landed, so as you expected this branch now conflicts with main in core/relinker/domain/include/domain/GuestRuntime.hpp and core/relinker/relinker/src/guest/GuestModuleBuilder.cpp (the Names / DeferInitialization fields and the per-image setup line). Could you rebase onto main, keeping both sides? A force push is fine.

While you're there, please add Depends on: #716 to the description if the branch still carries test_guest_needed_modules.py from #716, so the merge order is clear.

@merwie

merwie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Merged main and kept both sides: GuestRuntime has #684's Names followed by DeferInitialization, and GuestModuleBuilder::Build sets both per image. Non-GPU tests pass (233). I merged rather than force-pushed so the review history stays, and Auto-fix on my side is set up for merges. Happy to squash at merge time if you prefer. The description now starts with Depends on: #716: the branch still carries #716's commit, including test_guest_needed_modules.py.

@adriadam10

Copy link
Copy Markdown
Collaborator

@boykopovar a design question before this goes further (cc @oneandonlydean). The code itself checks out on Linux: we rebased it onto current main, and build + full ctest pass (399/399). guest_module_dir fails with the non-test changes reverted.

  • Flag or automatic discovery? The relinker already finds sce_module/sce_modules/prx on its own, and the other module flags are deprecated. With --module-dir, whoever relinks a title has to know where it keeps its runtime-loaded modules (Media/Plugins for Unity). An alternative with no new option: treat every .prx/.sprx with an ELF header under the input directory, outside the standard module directories, as a module loaded at run time (relinked, deferred initialization, kept out of DT_NEEDED). Only files with those extensions get opened, so the title's data isn't scanned. Which one do you want?
  • Linux (@merwie): there the host loader runs these modules' initializers at dlopen with the host process arguments, and *res is 0. So the case that motivates the PR (PSN started with {16, 0x200, ...}) is still broken there, and nothing reports it. Until deferred initialization exists on Linux, sceKernelLoadStartModule on such a module should throw instead.

@merwie

merwie commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for running it on Linux.

  • Linux: agreed. On Linux, sceKernelLoadStartModule with start arguments now throws (NotImplemented_nid_no_patch, before the dlopen), so the PSN case is reported instead of starting the module without its arguments. TechnicalDebt has a line for it. I can't build the Linux path here; it is one guarded call.

  • Flag or automatic discovery: that's for @boykopovar to decide. A correction to what I first wrote here: Silksong's app0 has more .prx/.sprx files outside sce_module than the runtime-loaded plugins in Media/Plugins:

    So automatic discovery would have to exclude anything reachable through DT_NEEDED, and skip or reject the fakelib and sce_sys files, which may not be ELF modules at all. That's doable. If it's the preferred direction, I'll switch the PR to it and drop the option.

  • Also merged main again (conflicts in core/relinker/CMakeLists.txt and GuestModuleBuilder.cpp, keeping this PR's lines). Non-GPU tests pass (235).

@merwie

merwie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (df16c4c) as @oneandonlydean asked; the merge commits are gone. The #716 commit dropped out because it is already upstream, so the branch is now the --module-dir commit and the Linux start-arguments commit. One correction: one of my earlier merges had accidentally removed the to_intel_linux_stubs test registration from core/relinker/CMakeLists.txt; the rebased branch keeps it. Otherwise the changes are the same. Non-GPU tests pass (239).

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 8, 2026
@orbz22

orbz22 commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

A data point for the auto-discovery question, now that #2708 looks for guest modules only in sce_module, sce_modules and prx.

Hollow Knight: Silksong (PPSA12544) keeps two of the modules its eboot lists as DT_NEEDED in Media/Modules: Il2cppUserAssemblies.prx and PS5Util.prx. They load at start-up, not through sceKernelLoadStartModule.

  • --module-dir Media/Plugins alone: on main de5e541 plus this PR, the relinker converts 10 guest modules instead of 12. It treats the missing two as host libraries, and the game stops at start-up (exit 0xC0000135):
    Failed to load module: ...\out\libs\Il2cppUserAssemblies.prx
    GetLastError: 126 - The specified module could not be found.
    
  • With --module-dir Media/Modules added: all 12 are converted, and the game runs through the opening into Moss Grotto (Windows 11, RTX 3060 Laptop). These two modules get the deferred initialization --module-dir gives, and on Windows that did not stop them from working.

So for Unity titles, the flag or auto-discovery has to cover start-up modules outside sce_module as well as the plugins loaded at run time.

🤖 Generated with Claude Code

@rm-NoobInCoding

Copy link
Copy Markdown

Data point for the auto-discovery question: Unity (PPSA07117) on Windows 11.

  • The eboot's DT_NEEDED includes Il2CppUserAssemblies.prx and PS5Util.prx from Media/Modules, as orbz22 found for PPSA12544. Il2CppUserAssemblies.prx in turn needs AkSoundEngine.prx, which lives in Media/Plugins. So one module in the plugin directory is a start-up dependency, while the other 10 are loaded by sceKernelLoadStartModule.
  • Loading all plugins at start-up with (0, NULL) fails as described here: PSNCore is an old version that cannot be used by the current player runtime.
  • With a local change along the same lines (the start-up closure converted normally; the other plugins converted with deferred initialization and started with the pending sceKernelLoadStartModule arguments), PSNCore, PSNCommon, SaveData, CommonDialog and the Wwise plugins load and start, and the title is playable.

Investigated with AI assistance (Claude Code).

@merwie

merwie commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean @boykopovar, before I switch this to auto-discovery, one conflict to settle. #2708 landed after the request above and removed recursive module search on purpose ("I want the behavior to be consistent every time"). Auto-discovery would bring back a walk of the whole input tree for .prx/.sprx files. That walk is what stopped on the malformed sce_sys/about/right.sprx in #2663, and in a real dump it also meets the dump tools' fakelib/*.sprx, which are SELF containers. An explicit directory option fits #2708's model, like --sce-module-path. So for now I've kept --module-dir and ported it onto current main.

Which do you want? If it's auto-discovery, I'll switch: walk the module parent directory, skip sce_sys and non-ELF files, and treat what's left as below.

What changed in this push (merge of main 4a747c4, plus 558c53e):

  • @orbz22's point: since feat(relinker): strict sce module search #2708, the modules Silksong's executable needs from Media/Modules are no longer found. A module in a --module-dir directory that the executable or another module needs is now initialized at startup, and on Linux it stays in DT_NEEDED. Only modules that nothing needs are deferred to sceKernelLoadStartModule. So --module-dir Media/Modules --module-dir Media/Plugins covers Unity titles. test_guest_module_dir.py has a new case for this, which fails without the change (the needed module is missing from DT_NEEDED).
  • Start arguments: main now passes them to Linux modules through the DT_INIT stub (36c979b). So I dropped my commit that made sceKernelLoadStartModule throw on Linux. The Windows deferred initializer now reads the same handoff (__aps5_get_pending_module_args), and sceKernelLoadStartModule is the same as on main.
  • TechnicalDebt: modules that another module needs are initialized at startup rather than when the module that needs them is loaded. On macOS, sceKernelLoadStartModule doesn't pass start arguments yet.

Tested: Windows 11. Non-GPU ctest passes (460), and the conventions check passes. PPSA12544 was relinked with --windows --module-dir Media/Modules --module-dir Media/Plugins on main plus this branch and #425. All 12 guest modules are converted. The two Media/Modules modules have no deferred initializer, the five plugins do. The title boots past the point where it used to fail with DllNotFoundException: Unable to load DLL 'PSN', and the SaveData plugin mounts its save data.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 10, 2026
@merwie

merwie commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

On the overlap report with #2119: the two changes are independent. #2119 adds --to-rosetta and this PR adds --module-dir, so both add a branch to ParseArgs, an entry to the usage string, and a parameter to GuestModuleBuilder::Build (and its call in main.cpp). Neither uses the other's option. Whichever lands second keeps both; I'll merge main here if #2119 lands first.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 10, 2026
@xXPBSXx

xXPBSXx commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Data point for run-time loaded modules: Hotline Miami (PPSA09682, Unity), relinked from the decrypted dump with Media/Plugins/PSNCore.prx among the extra modules. On Windows the title faults at start-up inside PSNCore.prx (read of 0x4, rip at +0x118f7 of the relinked module).

What I could read from the relinked module: the function at +0x1891 is reached from the module entry at +0x10010 (it runs the init array). It checks its second argument as a parameter block, *(u32*)p == 0x10 and *(u32*)(p+4) == 0x200, and if that fails it prints a "please update the PSNCore module" message and then reads p+4 anyway, which faults because p is null. The call into the module entry comes from the title code (return address app.exe+0x2358d26) as one of a run of calls that all pass edi = esi = edx = 0.

Not established: whether the title is meant to pass a block here and something earlier in our emulation leaves it out, or whether that call sequence is something the relinker or loader should not trigger for a module loaded at run time. A gdb breakpoint on sceKernelLoadStartModule was never reported as hit before the fault, but I did not confirm that the breakpoint had resolved, so I can not say whether #918's fix is involved. I'm leaving this as a report.

AI-assisted (Claude Code).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Keep --module-dir. #2708 settled on explicit module directories, and a tree walk would bring back the right.sprx and fakelib problems. Please rebase onto main without the merge commit (a24d523); it conflicts in CliArgs.cpp, main.cpp, GuestImage.hpp and GuestModuleBuilder.cpp.

@xXPBSXx

xXPBSXx commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

A second Unity title stops the same way: PPSA02801 faults at PSNCore.prx.guest.prx+0x11624 (read of 0x4, rbx = 0), which is the same function as the one I described for Hotline Miami above (the failed parameter-block check, then p+4 is read with p null). Both titles ship Media/Plugins/PSNCore.prx and PSNCommon.prx. I did not check whether sceKernelLoadStartModule is involved for this title (for Hotline Miami my gdb breakpoint on it was never reported as hit, but I did not confirm that the breakpoint had resolved).

AI-assisted (Claude Code).

@GustavoGamarra95

Copy link
Copy Markdown
Contributor

A third Unity title with run-time plugins: God of War Sons of Sparta (PPSA28997). Its Media/Plugins contains SaveData, PSN, CommonDialog, libfmod, libfmodstudio, lib_burst_generated, VoiceInputPlugin and libresonanceaudio. Two problems showed up, which may matter for how --module-dir lays out its output:

  1. Converted as fixed dependencies. Putting the plugins in prx/ makes them DT_NEEDED of the executable, so they are started at load without start arguments. SaveData.prx's start routine then reads the null argument pointer: SIGSEGV at mov (%rsi),%eax with rsi = 0.
  2. Converted in a separate relinker run (excluded from the executable with --exclude-sce-module, with each .guest.prx placed beside its original). The plugins got NEEDED $ORIGIN/../sce_module/libc.prx.guest.prx and RUNPATH $ORIGIN/../../libs. Those paths assume a module one directory below app0, but $ORIGIN is app0/Media/Plugins. Every sceKernelLoadStartModule failed, and the failure was only visible through dlerror (LD_DEBUG=files showed it). The title then retried PSN.prx thousands of times and stayed on a black screen after its logos. Adding the symlinks app0/Media/sce_module -> ../sce_module and app0/libs -> ../libs makes all of them load, and the title then reaches its menu and gameplay.

So the relative dependency paths would need to be computed from the directory the module is loaded from (Media/Plugins here), not assumed to be one level below app0.

@LightStorm72

Copy link
Copy Markdown
Contributor

Windows result for --module-dir: tested on Windows 11 with Road of Death (PPSA07286), merged on top of
01e65cfb (the only conflict was in GuestRuntime.hpp, where I kept both fields).

Command: relinker --windows --module-dir games <input>\eboot.elf app.exe

Before: the title's runtime-loaded plugin in app0\games was an unconverted PS5 module, and Windows rejected it with
the "Bad Image" error 0xc000012f.
After: the runner loads the converted rgpluginsgm_ps5.prx.guest.prx (it shows up in a backtrace) and prints
Starting Ratalaika Games plugin system for PS5. The title then reaches its main loop and runs.

No game files shared. AI-assisted (Claude).

Relinks the ELF modules of extra directories under the module parent directory. Modules the executable or another module needs start with the executable; the others are initialized when the title loads them with sceKernelLoadStartModule, with its start arguments (Windows: __aps5_guest_initialize export; Linux: left out of DT_NEEDED).

Squashed onto main by the reviewer (history had a merge commit).
@adriadam10
adriadam10 force-pushed the feat/relinker-module-dir branch from 558c53e to 1c6eac4 Compare October 11, 2026 08:19
@adriadam10

Copy link
Copy Markdown
Collaborator

Rebased onto current main as a single commit (the history had a merge commit; conflicts in CliArgs.cpp, GuestRuntime.hpp, main.cpp, GuestImage.hpp, GuestModuleBuilder.cpp resolved keeping both sides, including --to-rosetta).

Checked: relinker + libkernel build and 183 scoped tests pass (guest_module_dir, guest_needed_modules, guest_module_directories, sce_module_path); guest_module_dir fails with the non-test changes reverted. Windows runs by @LightStorm72 and the Unity reports above cover the runtime side. Keeping --module-dir as @oneandonlydean asked; merging once CI is green.

@merwie

merwie commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @adriadam10. Your rebase has the same tree as the one I had prepared on main f60923b (no file differs), so there's nothing more to push from my side. On Windows the non-GPU suite passes on it (516) and the conventions check passes.

@xXPBSXx

xXPBSXx commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Windows re-test of the Unity titles on current main a11c8f7, relinked with --windows --module-dir Media/Modules --module-dir Media/Plugins (all modules converted; the import audit shows no named absent imports):

  • Hotline Miami (PPSA09682): the PSNCore start-up fault and the earlier null-function-pointer stop are gone (the unnamed-import loading fix is in main as 2faa5dd). With only --module-dir Media/Plugins the run gets further but the title logs unable to load /app0/Media/Modules/PS5Util.prx, error:0x80020002 and the same for Il2CppUserAssemblies.prx: it opens those two by path at run time, so Media/Modules has to be a module directory too (as already noted above). With both directories the load errors disappear and the title reaches level 1, then faults deterministically (3 of 3 runs) on Loading.PreloadManager: rip app.exe+0x7a6730, write of 0x24. The instruction is in a Unity deserialization routine (app.exe+0x7a6690, caller chain +0x8f382e, called while /app0/Media/sharedassets1.assets is open) that stores 40-byte elements through an array whose data pointer is null while its capacity field says it is big enough, so no allocation happens. Which object that is I could not establish. Not established either: whether this is the title's data, a wrong value returned by one of our imports, or a memory-contents difference.
  • Cult of the Lamb (PPSA06465), DREDGE (PPSA09076), Harmony's Odyssey (PPSA16827): relink cleanly with both directories (12 to 19 modules) and run 60 s with no fault, no failed module load, and a swapchain created. Only that was checked; I did not look at the window.
  • New Joe & Mac (PPSA02801): relinks cleanly (10 modules) and no longer stops in PSNCore. It now aborts in the video-out path: multisampling or coverage conversion is unsupported (register 0x2f8 = 0x00108001) (State.cpp, the check on context register 0x2f8). That is multisampling, so it belongs with feat(agc): multisampled color and depth targets with CB resolves #419.

So the PSNCore fault that #846 comments reported for these titles is not reproduced on current main when both module directories are given. Evidence kept locally (logs); no game data shared.

AI-assisted (Claude Code).

@xXPBSXx

xXPBSXx commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Follow-up on the Hotline Miami stop (PPSA09682, main a11c8f7, --module-dir Media/Modules --module-dir Media/Plugins, Windows). It is deterministic (6 of 6 runs) and I can narrow it, not explain it:

  • The faulting routine (app.exe+0x7a6690, thread Loading.PreloadManager, sharedassets1.assets open) reads an element count from the stream and stores 40-byte elements into an array object. With a healthy empty array the object holds {data=0, label|0x2b, size=0, capacity=1} (logged at 7 earlier calls of the same routine). The array it faults on has data=0 but a large capacity field, so the routine skips its allocation and writes through null. The object's other fields hold values such as 1.0f and 0x7f000000, i.e. the object around that array was not constructed as expected. Which Unity class it is, I did not find out.
  • It is not a failed allocation inside our kernel calls: a local trace of sceKernelMprotect, sceKernelMapNamedFlexibleMemory and sceKernelMapDirectMemory (every call in 0x1000000000-0x10ffffffff plus every non-zero result) printed nothing. The last memory call before the fault is sceKernelReserveVirtualRange(len=0x9ec040000, flags=0) which we answer with 0 at 0x1000000000 (a 39.7 GiB reservation; the title also reads sceKernelGetDirectMemorySize = 0x360000000 several times). I do not know whether that reservation matters.
  • Running under gdb with many breakpoints changes the timing and the fault is not reached, so this needs a lighter probe than gdb.

Cult of the Lamb, DREDGE and Harmony's Odyssey each ran 180 s with the same two module directories: no fault, no abort, no error line, shader cache hits and texture creation continuing; I did not look at the window.

AI-assisted (Claude Code).

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants