Skip to content

fix(relinker): keep moved instructions off a function entry after padding - #11413

Open
atinseau wants to merge 1 commit into
boykopovar:mainfrom
atinseau:codex/relinker-island-function-entry
Open

atinseau wants to merge 1 commit into
boykopovar:mainfrom
atinseau:codex/relinker-island-function-entry

Conversation

@atinseau

@atinseau atinseau commented Oct 26, 2026 •

Copy link
Copy Markdown

What

Converting a title with --to-rosetta could overwrite the first bytes of a function that only a pointer reaches. A 256-bit store too short for a 5-byte jump is lowered through a nearby island: the converter moves a run of neighbouring instructions into a stub to make room for the jump. The window search refused only windows that contain a known branch target (direct branches and jump tables), so a window could start in the int3 padding after a ret and run into the next function's prologue. When that function is entered through a virtual call, the call lands inside the jump written over its entry.

An instruction that follows a jump, a return, a trap, or int3/nop padding after one of them can now only start a window. The check sits in the window placement shared by every lowering that needs room for a jump.

Reanimal (PPSA23372, 01.000.006), eossdk-ps5-shipping.prx, bytes from 0x6ef980 after conversion with --macos --to-rosetta:

input   00 c5 fc 11 00 31 c0 48 83 c4 08 5b 5d c3 cc cc 55 48 89 e5 41 57 41 56
main    00 eb 11 cc cc 31 c0 48 83 c4 08 5b 5d c3 e9 6d 46 2d 01 90 e9 87 46 2d
change  00 eb 13 cc cc 31 c0 48 83 c4 08 5b 5d c3 cc cc e9 6b 46 2d 01 90 e9 85

vmovups [rax], ymm0 at 0x6ef981 jumps to its island. On main the island starts at 0x6ef98e, in the padding, so the function at 0x6ef990, which the title calls through call [rax+0x10], begins with 46 2d 01 90, the tail of a jump, and the title hung while the EOS SDK initialized. With the change the island starts at the function's entry with a jump to its moved prologue, and the padding stays.

Tested

macOS 27.0.1 on an Apple M3 Max, relinker-only Release build (AppleClang, arm64) of this branch on main e2e27a3ba:

  • amd64_only_converter: the new converterIslandIndirectEntries case (int3, 2-byte and 3-byte nop padding, and no padding as the control) fails with main's converter ("An island covered the entry of a function that follows a jump and its padding") and passes with the change.
  • ctest --test-dir build-relinker: 370 of 370 pass. On arm64 the x86 execution cases of amd64_only_converter (wideStoreExecution, rdseedExecution and the like) are empty functions; the new case only checks converted bytes.
  • The bytes above come from this branch's relinker and from main's, each converting Reanimal's eboot.bin with that module in prx/. The two converted modules differ in 42 bytes.
  • python3 tools/check_conventions.py --base origin/main passes, and tools/pr_overlap.py reports no conflicting lines with other open pull requests. It could not compare fix(relinker): lower scalar RCPSS and RSQRTSS exactly #11192, which conflicts with main; the one file they share, Amd64OnlyConverterTests.cpp, merges without conflict, since fix(relinker): lower scalar RCPSS and RSQRTSS exactly #11192 extends the reciprocal tests and leaves the call list alone. fix(relinker): place 256-bit store islands across 2^63 without signed overflow #11232, merged on 2026-10-26, also changed the island placement of a 256-bit store (signed overflow across 2^63). This branch is rebased on it: Amd64OnlyConverter.cpp merged without conflict, and main() of Amd64OnlyConverterTests.cpp calls both cases after converterWideStoreWindows().
  • Titles converted with --macos --to-rosetta on the local macOS branch described below, with this change since 2026-10-22: Reanimal now gets past EOS initialization, where it hung before; Call of Duty: Black Ops II (PPSA34502) and Hollow Knight: Voidheart Edition (PPSA34971) convert and run as before.

Not tested: --to-intel output, Linux and Windows hosts.

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 under Silent stubs in their library's technical debt file
  • 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
  • Fixes: none
  • Unblocks: PPSA23372 (no Game bring-up discussion yet)
  • GPUs tested: n/a
  • AI-assisted: yes

Context: the macOS bring-up this comes from

This is the first change we propose from a bring-up of Call of Duty: Black Ops II and Reanimal on an Apple M3 Max (36 GB, macOS 27.0.1, Rosetta 2, MoltenVK 1.4.2 from Vulkan SDK 1.4.363.0, 1920×1080). This PR does not depend on that work; the rest of this section explains how the change was found and what else the bring-up needed.

Approach. We work on a local branch made of main, the macOS port (#8658, closed; its author is splitting it: #10419, #11284 and #11303 are merged, others are open), the open pull requests below integrated before their merge, and our own commits. Each integration is recorded with the head we took, its conflicts and its validation: the same focused tests before and after with VK_LAYER_KHRONOS_validation, ANYPS5_REQUIRE_VULKAN=1 and ANYPS5_NO_SHADER_CACHE=1, compared per test for status and VUID- counts; a red-first test for each of our own changes; the full suite; and title runs. When one of those PRs is merged we drop our copy at the next rebase on main. Our own generic fixes go upstream one topic at a time, starting with this one, which needs nothing else.

Where the titles are (runs of 2026-10-26 on the local branch rebased on main 4e43321bd):

Call of Duty: Black Ops II (PPSA34502). It boots, plays its videos and reached gameplay in Campaign (Angola), Zombies (TranZit) and local Multiplayer (Team Deathmatch on Aftermath); today's rebased branch was checked on the campaign. With warm caches the war-room menu runs at about 10 FPS and the Angola opening at 11.7 FPS averaged over 60 s (10 s windows from 6.8 to 19.2 FPS). The first macOS runs drew that menu at 0.6 to 0.8 FPS and the first campaign runs reached 4.7 to 5.1 FPS in Angola. The draw committer sets the pace: recording draws, MoltenVK's command encoding and its per-barrier resource walk take 75 to 85 % of a frame, so the console's 60 FPS is not reached.

Reanimal (PPSA23372), Unreal Engine 5. It boots and shows the THQ Nordic logo, the photosensitivity warning, the main menu with its 3D scene and the single-player menu, and starts a new game. That load does not finish yet. Before this rebase the title's footprint reached about 13 GB during it and the wired GPU memory neared the limit of this 36 GB Mac. On today's rebased branch the shader cache starts cold (new request format), and the long compiles of the largest compute pipelines trip Unreal's 120 s render-thread watchdog during the load. Getting this far took the 64-bit atomics emulation, the mesh fallback (#9356, #9387), depth bounds without the feature (#9737 and a follow-up of ours), deferred destruction on MoltenVK (#10062) and this relinker fix.

Black Ops II, Angola, gameplayBlack Ops II, Angola, cutscene
Black Ops II, Angola (11.7 FPS over 60 s; the title shows the instantaneous rate)Black Ops II, Angola, opening cutscene
Black Ops II, Angola, convoyBlack Ops II, war-room menu
Black Ops II, Angola, convoyBlack Ops II, war-room menu (about 10 FPS)
Reanimal, main menuReanimal, single-player menu
Reanimal, main menu (frame dumped by the driver, 960×540)Reanimal, single-player menu
Open pull requests the bring-up carries, and our confidence that each lands soon

Confidence reads the PR's state on 2026-10-28: High when a maintainer has accepted it and it merges cleanly, Medium when it merges cleanly but has no review yet, Low when it conflicts with main or has unanswered change requests.

PR What it does Why the bring-up needs it State Confidence
#7048 keeps guest bytes of pending storage results over memory that left the write watch Hollow Knight's save load stopped on this refusal open, conflicting, rebase requested Low
#7256 layered depth draws into a depth view of several slices BO2 Angola renders a two-slice depth array open, conflicting, rebase requested Low
#9356 mesh-path draws through a compute pass without mesh shaders Reanimal stopped at 24 s on MoltenVK open, conflicting, changes requested Low
#9387 mesh-fallback barycentrics through tessellation Reanimal, same stop open, conflicting, changes requested Low
#9445 no CPU waits for GPU-ordered labels BO2 frame time open, clean, no review Medium
#6501 occlusion counter dumps queued behind pipelined draws BO2 frame time open, clean, no review Medium
#5609 reuses cached draw stages when only buffer bases moved BO2 draw-cache hit rate open, conflicting, changes requested Low
#6970 keeps a binding's element flags inline Hollow Knight allocation profile open, conflicting, changes requested Low
#7457 remembers structural host import misses evaluated, no speed gain shown open, conflicting, changes requested Low
#6921 crosses uniform runs of cached guest pages 64 pages per load CPU sweep open, clean, maintainer accepted the Windows evidence Medium
#6108 keeps a fast clear's guest bytes while they match unwatched memory CPU sweep open, clean, maintainer said it can merge once green Medium
#9132 IrValue's trivial accessors in its header CPU sweep, shader compile time open, clean, maintainer accepted the CPU evidence High
#8963 BDA fault buffers in a per-device arena CPU sweep, fault-buffer memory open, clean, no review Medium
#11580 loads and stores 16-bit data of converted packed images Reanimal stopped 50 s after launch once #8898 sent 11_11_10_FLOAT storage images through the in-shader conversion open, clean, approved by MarcZX10 Medium

Merged upstream since we took them, thanks to their authors: #9115, #6951, #6946, #6848, #8612, #7968, #7882, #8034, #8928, #9177, #8756, #8911, #9737, #10062, #9701 and #9702 (the 4,750-block structurizer fix that Unreal titles such as Reanimal need; we dropped our copy at the rebase that followed its merge). Merged on 2026-10-26 and 27, after this PR was opened: #9263, #9193, #8913, #10659, #10885, #11358 and #8888. Closed: #6539, by a maintainer, for an exit-time RelocationPool crash, which the local fix listed below addresses (we keep it), and #9049, by its author, as main's memo patching left it no gain (dropped).

Our other changes, proposed later one topic at a time

Each has a red-first test or a measurement recorded; the ones that claim speed still need tools/trace_digest.py summaries of main and of the change, which we can only take once main runs titles on macOS or on another host.

  • fix(libSceSystemService): find a converted executable by the main executable's extension. BO2 aborted when Zombies started Multiplayer (codmp.elf) beside an extensionless main output.
  • feat(agc): emulate the depth bounds test beside a depth test with a stencil mask, a follow-up to feat(agc): emulate a one-sided depth bounds test without the depthBounds feature #9737 for Reanimal's deferred lights (GREATER_OR_EQUAL, bounds [0.00287, 1], D32S8).
  • feat(shader): 64-bit image and buffer atomics with device lock words where the device has none (MoltenVK), for Reanimal's Unreal Engine 5 Nanite passes.
  • perf(agc): keep the staging pool's device tier small on an integrated GPU whose heap also has a device-only type; it held 2.5 GiB of idle buffers during Reanimal's load.
  • fix(agc): keep relocation pools alive through outstanding leases, the exit-time crash perf(agc): relocate cached draws across user words that only reach the shader as values #6539 was closed for.
  • fix(libkernel): leave the signal mask alone when scePthreadExit jumps back; on macOS, longjmp restores the mask through sigprocmask, which XNU applies to every thread.
  • fix(agc): map a host import window split inside a 16 KiB host page again as one region on macOS; BO2's labels stopped forcing CPU waits (Angola 7.6–8.1 to 10.5–10.7 FPS).
  • BO2 committer work (submission-end batching, a 256-draw batch cap, scoped host barriers, shared binding plans) and a CPU sweep of the per-frame paths (guest mutex fast path, pipeline store keys, register writes, IR allocation), measured with instruction counts and fixtures so far.

@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 26, 2026

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested only for the branch being behind current main: this head merges from cbaacd2, while current main is 4b1a9a3. CONTRIBUTING requires branches to be kept current; please sync main and rerun the checks on the updated head. The intervening main changes do not touch the three files in this PR, so I found no file-level conflict in that scope.\n\nIndependent validation on exact head b17fb98: the new converterIslandIndirectEntries case passed when run alone in a local Linux relinker-only build, and diff/convention checks against current main passed. The full amd64_only_converter test binary raises SIGILL on this Xeon E5-1620 v2 host because the suite executes CPU-specific instruction tests; it does not establish a failure in this change. The PR reports the complete 303-test suite passing on macOS. No source correctness blocker found in the reviewed window placement logic.

@atinseau
atinseau force-pushed the codex/relinker-island-function-entry branch from b17fb98 to d81da62 Compare October 26, 2026 12:21
@MarcZX10

Copy link
Copy Markdown
Contributor

I manually compared the current #11232 and #11413 patches. Their source changes address separate cases (_distance arithmetic across 2^63 versus preserving entries after jump/padding) and auto-merge in Amd64OnlyConverter.cpp. The merge conflict is limited to the test driver call list, where both branches add an invocation after converterWideStoreWindows(). Preserve both calls and rerun amd64_only_converter after syncing; I did not find a behavioral dependency that requires changing Depends on: none.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact refreshed head d81da62fdd6269c591943245ff6b14f8a3dcedcd. The guard treats a candidate instruction after an unconditional transfer and int3/nop padding as a possible function entry; the regression covers int3, 2-byte and 3-byte NOP padding, and no padding, while retaining the control case. The author reports the new case fails on the pre-change converter and the full macOS relinker suite passes 303/303; my local Linux run of the focused case passed on the same source change. Current hosted conventions/labels pass; no hosted build matrix ran on this external branch. Current main is three commits newer than the 5e90e7569 base used for the reported local run, but those commits do not touch the PR files and the merge preview is clean. Manual comparison with #11232 found that both source changes auto-merge; the test driver needs both calls retained when the shared call-list hunk is resolved. AI-assisted review; the macOS suite and title run are author-reported.

@atinseau

Copy link
Copy Markdown
Author

Thanks for the second pass and the approval!

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The source review remains positive: the entry-after-padding guard and focused fixture look sound, and the earlier overlap check with #11232 found only the shared test-call-list hunk, with both calls needed. I am updating the review state solely because the branch is no longer current: its merge base is 2db37cf1de, while current main is 0e5d5a54098c. I found no intervening edits to the three files changed here, but CONTRIBUTING requires keeping the branch up to date. Please sync to current main, preserve both #11232 test invocations if the overlap remains, and rerun conventions plus the focused converter test on the resulting head. Existing macOS full-suite/title evidence is author-reported; current hosted checks show only conventions and labels, not a build matrix.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 26, 2026
@adriadam10 adriadam10 self-assigned this Oct 27, 2026
@adriadam10
adriadam10 force-pushed the codex/relinker-island-function-entry branch from d81da62 to 0b35764 Compare October 27, 2026 10:00
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 27, 2026
@atinseau
atinseau force-pushed the codex/relinker-island-function-entry branch from 0b35764 to 0209cdd Compare October 27, 2026 15:54
@atinseau

Copy link
Copy Markdown
Author

Rebased on current main (e0cd125) as 0209cdd, on top of @adriadam10's rebase after #11232 (thanks!). Both test calls stay after converterWideStoreWindows(); the conventions check, amd64_only_converter and the 361 relinker-only tests pass (macOS, arm64). @MarcZX10, this is the sync you asked for.

@github-actions

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 11192 core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp

@adriadam10
adriadam10 dismissed MarcZX10’s stale review October 28, 2026 08:32

Dismissing a stale review: the requested changes were addressed after it (new head) and the merge queue blocks on it. Please request changes again if anything is still open.

…ding

An island or relocation window could start in the int3 or nop padding after a jump or return and run into the next function. A function that only a pointer reaches, such as a virtual method, is not a known branch target, so its first bytes became the middle of a jump to a stub. An instruction that follows a jump, a return, a trap or padding after one of them can now only start a window.
@atinseau
atinseau force-pushed the codex/relinker-island-function-entry branch from 0209cdd to fb3c5df Compare October 28, 2026 08:57

This branch has not been deployed

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

Labels

relinker unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants