Skip to content

fix(libc): adopt the guest SDK allocator only when the title replaces the heap - #7897

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
babyhuey:fix/libc-adopt-guest-heap-only-when-replaced
Oct 20, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
babyhuey:fix/libc-adopt-guest-heap-only-when-replaced

Conversation

@babyhuey

@babyhuey babyhuey commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What

Since #6610, ApplicationHeapInitialize_nid_no_patch adopts a loaded guest SDK's ten allocator exports on Linux before it looks at the title's replacement table. Many catalog titles bundle a libc.prx that exports those NIDs but has no allocator of its own: per @root-over on #6610, the ps5-native-app-boilerplate libc.prx imports only malloc, free and posix_memalign and hands them to _sceKernelRtldSetApplicationHeapAPI. For these titles, HLE allocations now go through exports that lead back into HLE, and they abort, segfault or fail to allocate at start-up.

The guest exports are now adopted only when the title's replacement table has an allocator entry. With an empty table, the title keeps the default heap, as before #6610. A title that does replace the allocator keeps #6610's path, and non-Linux hosts are unchanged.

  • New Linux test application_heap_guest_sdk_default: it loads the same guest SDK fixture, clears the replacement table, and checks that an allocation comes from the default heap without calling the fixture's exports. With main's ApplicationHeap.cpp it segfaults (the fixture's malloc reads a null table entry).
  • The ApplicationGuestHeapApi debt entry states the new condition.

Tested

On 4a5ce8a (based on main b106f6f):

  • Linux (Ubuntu 24.04 container, GCC 13, Release): the 13 tests matching application_heap|allocator pass 5/5 (--repeat until-fail:5), including the new one and fix(libc): give the scripting heap hooks blocks from libc's own heap #6842's application_heap_scripting. No warnings in the touched files.
  • Windows 11 (RTX 3080, WinLibs GCC 15.2, Release): the 11 tests matching application_heap|allocator pass 5/5 (the guest SDK tests are Linux-only). No warnings in the touched files. Eternal Darkness (PPSA99314) runs as on main (413 frames in 30 s). The guarded path is Linux-only, so nothing changes on Windows.
  • Linux titles, headless with SDL's offscreen driver, where the titles normally stop at the DisplayWindow Vulkan check. On main these ten abort, segfault or hit an illegal instruction at start-up, or Mesa reports xmlconfig.c: 389: out of memory.. With this change all ten reach the DisplayWindow check again: ccleste (PPSA59322), Mario 64 (PPSA99310), Eternal Darkness (PPSA99314), Dusklight (PPSA99305), ProsperoTV (PPSA99003), PSXS5 (PPSA97510), Régie5 (PPSA99520), Media Player Classic (PPSA99015), DriftCheck (PPSA98766) and DOOM (PPSA99666). In the full catalog sweep, 41 of the 61 titles that reached that check stopped earlier after fix(libc): share the guest SDK application allocator #6610 (fix(libc): share the guest SDK application allocator #6610 has the table).
  • Two catalog titles added since, Pulse (PPSA99993) and PayLoader (PPSA67670), segfault at start-up on main 02084228a and reach the DisplayWindow check with this change merged onto it. So does Sahin (PPSA99404), added later, on main d50ec76da.
  • @root-over checked the same condition on a real GPU on fix(libc): share the guest SDK application allocator #6610: DriftCheck and DOOM render their UIs, and ccleste gets past start-up.

check_conventions.py 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 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. Follows up fix(libc): share the guest SDK application allocator #6610.
  • Fixes: none
  • Unblocks: PPSA59322 PPSA99310 PPSA99314 PPSA99305 PPSA99003 PPSA97510 PPSA99520 PPSA99015 PPSA98766 PPSA99666 PPSA99993 PPSA67670 PPSA99403 PPSA99404 PPSA68091
  • GPUs tested: n/a
  • AI-assisted: yes

… the heap

A bundled libc.prx can export the ten allocator NIDs while forwarding
them to HLE, so adopting its exports for a title whose replacement
table is empty sends HLE allocations back into themselves. Keep the
default heap in that case, as before the guest SDK adoption.
@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 8137 docs/dev/debt/libc.md L7-8

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 5021 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 6044 core/libs/prx/libc/CMakeLists.txt
core/libs/tests/ApplicationHeap.cpp
PR 6226 docs/dev/debt/libc.md
PR 6469 docs/dev/debt/libc.md
PR 6819 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 7081 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 7201 core/libs/prx/libc/CMakeLists.txt
core/libs/tests/ApplicationHeap.cpp
PR 7411 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 7473 docs/dev/debt/libc.md
PR 7648 docs/dev/debt/libc.md
PR 7715 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 8150 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 8352 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md

@root-over

Copy link
Copy Markdown
Contributor

Real-GPU run of this head (4a5ce8a09), past the DisplayWindow check: Linux (Ubuntu, kernel 7.0, Wayland), Ryzen 5 5625U iGPU (Vega, RADV RENOIR, Mesa 26.0.8), GCC 15.2.0, Release. Catalog releases checked against their sha256, 30 s each.

The behaviour matches #6610 reverted, or the guard I tried locally, on every title I have.

@DavidSamir

Copy link
Copy Markdown
Contributor

Two more Linux titles that this PR brings back, both new in the catalog and not in the description's list. Ubuntu 24.04 in WSL2, GCC 13.3, Release, lavapipe, headless with SDL's offscreen driver, 60 s each. main 500cc640b against main plus head 4a5ce8a094.

Title main With this PR
NFS Black Box (PPSA99403) aborts at 1 s: application heap: incomplete or mixed guest allocator API still running at 60 s
PayLoader (PPSA67670) segmentation fault at 2 s still running at 60 s

Both run 60 s on Windows on main, where the guarded path is not built.

AI-assisted: yes

@milescoviello

Copy link
Copy Markdown
Contributor

One more title this brings back: Ruffle (PPSA68091, 1.2.0), which bundles sce_module/libc.prx and has an empty replacement table. Linux, RTX 4070 Ti (NVIDIA 595.99.02), on the display:

Build Result
main 15da289 and 1d41645 (2 runs each) EXIT-134 at 0 s, before the first frame. The guest's RADV prints xmlconfig.c: 389: out of memory.
main e25c975 + 4a5ce8a094 (2 runs) starts and presents frames. The demo then stops at 10 s on a separate FMASK refusal (#8429).
main a9c93a0 + 4a5ce8a094 + #8429 (2 runs) the demo runs to the 120 s limit

A temporary print on main showed the adopted API coming from app0/sce_module/libc.prx.guest.prx. The first HLE allocation is a 32-byte strndup from RADV's config parser, and it got NULL from that libc.prx's allocator. Ruffle 1.2.0 last ran on main 98c251f (10-17), before #6610.

AI-assisted: yes (Claude Code)

@milescoviello

Copy link
Copy Markdown
Contributor

A performance data point for this PR: Super Meat Boy 3D (PPSA31162), which bundles sce_module/libc.prx. Level 1-1, Linux, RTX 4070 Ti (NVIDIA 595.99.02), Release.

main 30fda0a against the same main with 4a5ce8a094 merged (clean). Each run starts from a fresh boot. A script waits for the game's own Load map complete lines (menu, slot 1, world map, level 1), settles for 30 s in the level and counts presents for 60 s with APS5_PROFILE_DRAW=1.

Build Presents per 10 s in the level Draw commits on the committer GPU busy / frame time
main (2 runs) 16–19 63–75 µs each 135 / 590 ms
main + this PR (2 runs) 43–52 16.4 µs each 103 / 199 ms

That's about 3×. Both builds run the same draws: about 150,000 occlusion dumps per 10 s, each draining the draw pipeline. So the per-draw cost on the committer is what changes. I haven't traced why each commit costs about 4.5 times as much while the bundled libc.prx's allocator serves the HLE allocations. The title runs either way, so on main this shows up only as speed.

main a9c93a0 measures the same as 30fda0a (18–20), so the gap isn't from a change in main between those two commits.

AI-assisted: yes (Claude Code)

@oneandonlydean
oneandonlydean merged commit b85d972 into boykopovar:main Oct 20, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build docs libc unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants