Skip to content

EA Sports UFC 5 (PPSA03541): null function pointer call in Engine.Render.Core2.PlatformPs5 about 20 s after start #3123

Open

Description

@The-IOMIOU

Summary

EA Sports UFC 5 (PPSA03541, EUR, v01.031) converts with the relinker, loads, creates the Vulkan device and swapchain, and then crashes about 20 s after start. A Job0 thread jumps to address 0 from inside the game's renderer module (Engine.Render.Core2.PlatformPs5.prx).

Not a request for support. This is a data point on a title that is not in the compatibility list, plus what I already ruled out.

Environment

  • Windows 11, RTX 3060 (Vulkan device selected and swapchain created, 2226x1252), 24 GB RAM
  • Built from main at 9206c3e (2026-10-11) with MinGW-w64 GCC 16.1.0 (not the documented 15.2.0); relinker and full build both succeeded
  • Relinker: --windows --to-intel, input is the game's decrypted eboot.bin (plain ELF). Its 4 modules (libc.prx, libSceNpCppWebApi.prx, Engine.BuildInfo.prx, Engine.Render.Core2.PlatformPs5.prx) were converted together as bundled modules
  • Engine.*.prx sit next to eboot.bin rather than in sce_module/, so I staged all four in a prx/ directory to convert them, and copied the outputs to where the game loads them (/app0/Engine.*.prx). Before that the game called a null pointer much earlier, because the unconverted module was not found

What happens

  1. Startup is normal: Vulkan device, swapchain, pad, /app0/Data/..., /app0/Patch/package.mft opened
  2. Last log line before the crash: [libSceSysmodule/Export.cpp:127 sceSysmoduleLoadModule] unknown id: 267
  3. FATAL: unhandled exception 0xc0000005 on thread 'Job0', rip 0x0000000000000000, execute of 0x0000000000000000
  4. Return address on the stack: Engine.Render.Core2.PlatformPs5.prx.guest.prx+0x47b43, so the renderer module calls a null function pointer
FATAL: unhandled exception 0xc0000005 on thread 'Job0'
  rip 0x0000000000000000
  execute of 0x0000000000000000
  rax 0000000000000000 rbx 0000000000000000 rdx 0000000000000000
  rsi 0000000100000001 r14 0000000000000000
  [rdi] 0000000000000000 0000000000000000 0000000000000000 0000000000000000
  [r13] deadbeefcafebabe 0000000000000000 0000000000000001 0000000000000001
  stack return address candidates:
    [rsp+0x0] Engine.Render.Core2.PlatformPs5.prx.guest.prx+0x47b43

Reproduced on every run, and unchanged after pulling 6 newer commits.

What I ruled out

  • Missing imports: tools/import_audit.py over the executable and all 4 bundled modules reports 984 unique imports, 0 absent, 732 implemented, 25 stubs. The stubs are voice chat (sceVoice*, sceProprietaryVoiceChatHelper*), sceAudioInOpen, sceNpEAAccess*, sceHttpWaitRequest, sceSslGetPem, sceAgcDebugRaiseException and a few kernel debug functions. They throw when called, so none explains a jump to 0
  • Failed sceKernelDlsym: I added a log on the failure path of sceKernelDlsym and it printed nothing before the crash, so the null pointer is not from a symbol lookup the game made through it
  • Sysmodule id 267 (0x10B): libSceSysmodule/ModuleTable.hpp has no entry for 0x10A or 0x10B, and sceSysmoduleLoadModule returns success for unknown ids. It is the last thing logged before the crash, but I could not tie it to the null call

Guess

A pointer the game expects a system function to fill (a callback or function table) stays zero, possibly because an "implemented" function returns an incomplete result or because module 267 is not provided. I have not disassembled the module around +0x47b43 to confirm.

Questions

  • Does anyone know which system module id 267 (0x10B) is on PS5?
  • Is a title that keeps its own modules next to eboot.bin (outside sce_module/) expected to work with the prx/ staging approach above, or is there a supported layout?

Activity

  1. LeandroSSB commented on Oct 11, 2026

    @LeandroSSB
    Contributor

    Your module-id question has an answer now: 267 (0x10b) is the Acm co-processor module — identified in #3710 (Demon's Souls start-up crash), which cites #3240/#3203 for the id.

    That thread also isolates an enabler your null-pointer crash likely shares: sceSysmoduleLoadModule returns 0 for ids it does not know, so a title that probes for Acm and has a fallback (yours: a callback/table the game expects a system function to fill stays zero) takes the Acm path believing it loaded, while libSceAcm is a no-op — the same "returns success, delivers nothing" shape you ruled imports out for. If your renderer module turns out to gate on another unknown id the same way, the fix direction is probably at the sysmodule table (fail or map unknown ids) rather than at the null call site.

  2. The-IOMIOU commented on Oct 11, 2026

    @The-IOMIOU
    Author

    Thanks @LeandroSSB, that was useful. I tried it.

    • Rebuilt from current main (a11c8f7). The module table there has no 0x10B entry, so sceSysmoduleLoadModule still logs unknown id: 267 and returns 0.
    • Local experiment: sceSysmoduleLoadModule(0x10B) returns 0x805A0001 instead of 0, so the title sees Acm as not loadable. Nothing else changed.

    Result: no difference. Same crash on a Job0 thread, rip 0, same return address Engine.Render.Core2.PlatformPs5.prx.guest.prx+0x47b43, and the same register pattern as before. The unknown id: 267 line is still the last log line before it.

    So for UFC 5 the Acm id does not look like the cause: either the renderer does not check that return value, or the null pointer comes from somewhere else. Acm may still be the cause in the Demon's Souls case.

    Earlier checks, for completeness: import_audit.py over the executable and all 4 bundled modules reports 0 absent imports, and sceKernelDlsym logged no failed lookup before the crash.

    Next I plan to disassemble the renderer module around +0x47b43 to see which pointer is null. If anyone already knows what that module expects a system function to fill in, that would save time.

  3. The-IOMIOU commented on Oct 15, 2026

    @The-IOMIOU
    Author

    Update, and a correction: the null-pointer crash I reported was caused by my own setup, not by the project. UFC 5 now gets much further.

    What was wrong

    Engine.*.prx sit next to eboot.bin, outside sce_module/. The relinker only writes bundled modules into the directory they were staged from (prx/ or sce_module/), so I also put plain copies of the relinked files at the paths the game asks for (app0/Engine.*.prx.guest.prx, app0/sce_module/*.prx.guest.prx).

    The startup stub loads and binds the instances under app0/prx/. The game then opens /app0/Engine.Render.Core2.PlatformPs5.prx, RelinkedModulePath_nid_no_patch returns my copy, and Windows loads it as a second instance whose imports nothing ever bound. The first call through an unbound slot was __cxa_guard_acquire (3GPpjQdAMTw), which is the call just before the crash address +0x47b43: a function-local static guard, jumping through an empty import slot to 0.

    Fix on my side

    Hard links instead of copies: app0/Engine.*.prx.guest.prx and app0/sce_module/*.prx.guest.prx are hard links to the files under app0/prx/. Windows then reuses the already-bound instance. With that the null call is gone.

    What happens now (current main, 0029e62, Windows, RTX 3060)

    • Game boots, Vulkan swapchain, the window title shows EA SPORTS UFC 5
    • Shader cache fills (about 200 shaders per 10 s, 0 hits on the first run)
    • Frames: 1.2 FPS at first, then about 51 FPS with 397 frames presented
    • Then the process aborts with the project's own message:
    AGC driver: COPY_DATA at DWORD 71: COPY_DATA GDS or performance-counter destination is not implemented
    [gpu] at DWORD 87  header 0xc0044000 COPY_DATA  6 dwords: 00106203 00000020 00000000 682507f8 00000011
    [gpu] at DWORD 113 header 0xc0044000 COPY_DATA  6 dwords: 06106305 00000000 00000000 00000020 00000000
    

    A WAIT_REG_MEM_64 on queue 0x29 also logs a timeout shortly before the abort. I do not know what the game was showing on screen at that point.

    So this is now a feature gap in the AGC driver (COPY_DATA to a GDS or performance-counter destination), not a crash from missing imports.

    Two notes for the project

    1. The Acm id 0x10B: now registered upstream and no longer logged as unknown, but it made no difference here. It was never the cause for this title.
    2. Titles that keep their own modules beside eboot.bin cannot be laid out by the relinker. The RelinkedModulePath lookup only checks for <requested path>.guest.prx in the requested directory and does not match modules that are already loaded. A short line in docs/user/USAGE.md about the hard-link workaround, or a relinker option to write those modules to the app0/ root, would save the next person the same detour.
  4. The-IOMIOU commented on Oct 15, 2026

    @The-IOMIOU
    Author

    Second update. UFC 5 now reaches its loading screen on main (0029e62, Windows, RTX 3060) with two local patches to libSceAgcDriver. I saw the loading screen with the loading badge animating. It then crashed with heap corruption. Details below, in case any of it helps.

    What each stage looked like (after the module layout fix from my previous comment)

    1. First abort, about frame 267 (renders black frames at 23 to 51 FPS until then):

      AGC driver: COPY_DATA at DWORD 71: COPY_DATA GDS or performance-counter destination is not implemented
      

      The six rejected packets are COPY_DATA with a GDS source or destination. Decoded:

      • 00106203 00000020 00000000 <lo> <hi>: src_sel 3 (GDS, offset 0x20) to memory
      • 06106305 00000000 00000000 00000020 00000000: src_sel 5 (immediate 0) to dst_sel 3 (GDS, offset 0x20)

      DMA_DATA already treats GDS as GdsAddress() + offset, so I did the same for COPY_DATA (validation with a gdsRange check, the source read in copyDataSource, the destination in Execute, and ResolveStore and DecodeStores skip a GDS destination as DMA_DATA does).

    2. Second abort, about frame 360:

      MSAA_ENABLE on a single-sample target for primitives other than triangles is unsupported (register 0x242 = 0x00000007)
      

      singleSampleMsaaRejection in Graphics/src/State.cpp accepts VGT_PRIMITIVE_TYPE 4, 5, 6. Type 7 is TRI_WITH_WFLAGS, still a triangle primitive, so I added it.

    3. After both patches the game reaches about 790 frames at about 45 FPS, then the queue stalls: a queue (0x29) logs repeated

      WAIT_REG_MEM_64 at 0x11155baad0 timed out after 1000ms (function 3 ref 0x312 mask 0xffffffffffffffff value 0x0)
      preceding packet COPY_DATA 02014209 00000000 00000000 156bc850 00000011
      preceding packet PUSH_MARKER 65726f43 6e654632 00006563
      preceding packet SET_UCONFIG_REG 00000342 c8020011 155baad0
      

      The label stays 0 while the CPU side waits. FPS drops to about 0.03 to 1. The title's own hang detector starts writing eadp.errorsdata.*_hang.st.dat. The title was also loading its UI scripts (ui/lua/...) at that point. I did not find what should write that label.

    4. The process then dies without a project crash handler message. Windows logged an APPCRASH with exception 0xc0000374 (heap corruption) in ntdll, at about 13 GB working set. I do not know if my patches contribute to it.

    Caveats

    • The patches are experiments and I have only checked that the game gets further, not that anything renders correctly. The GDS handling mirrors DMA_DATA, so it may be wrong for GDS contents that shaders also write (no synchronisation with GPU work, as DMA_DATA has none either).
    • The full change is about 14 lines in Execution/src/Pm4.cpp and 1 line in Graphics/src/State.cpp, described above. I can paste the diff if that helps.
    • The existing test in tests/Pm4.cpp that expects a GDS destination COPY_DATA to fail validation would need updating if this were upstreamed.

    Happy to test other builds or patches on this title.

  5. notaxeltv commented on Oct 15, 2026

    @notaxeltv
    Contributor

    Confirmed on current main (fa9365c) + _Cnd_init_with_default_name_override (#6088), Windows 10, RTX 3080, PPSA03541 v01.031:

    1. Plain main cannot load: unresolved jBOZAv6CwkM from libc.prx (_Cnd_init_with_default_name_override).
    2. With feat(libc): _Cnd_init_with_default_name_override (PPSA34015) #6088, Engine.Render loads, Vulkan device + swapchain come up, then Job0 still dies with rip 0 / execute of 0 inside Engine.Render.Core2.PlatformPs5 (same signature as this issue). No sceSysmoduleLoadModule unknown id: 267 line on this run before the fault.
  6. notaxeltv commented on Oct 15, 2026

    @notaxeltv
    Contributor

    Follow-up: rebuilt main @ 44df216 plus the open PPSA34015/libc stack (incl. #6088 _Cnd_init_with_default_name_override). PPSA03541 still dies on Job0 rip 0 immediately after threads start / _replay0.bin opens — no new NotImplemented / absent import on the path. Crash stacks also show libSceAgc / libSceVideoOut frames beside Engine.Render.Core2.PlatformPs5. Same signature on FC27 (PPSA34015) with the same stack.

  7. notaxeltv commented on Oct 15, 2026

    @notaxeltv
    Contributor

    Layout confirmation (PPSA03541): the Job0 rip 0 was a second LoadLibrary of /app0/Engine.Render.Core2.PlatformPs5.prx while app0/prx/Engine.Render.Core2.PlatformPs5.prx.guest.prx was already loaded. Replacing the root copies with hardlinks to the .guest.prx files removes the null call.

    With that layout (plus _Cnd_init_with_default_name_override), the title creates the swapchain, presents HDR10, compiles thousands of shaders, then stops on:

    AGC driver: COPY_DATA ... GDS or performance-counter destination is not implemented

    So #3123's null is a duplicate-module layout issue; the next blocker is COPY_DATA→GDS in AgcDriver.

  8. The-IOMIOU commented on Oct 16, 2026

    @The-IOMIOU
    Author

    @notaxeltv thanks for confirming, same layout fix and same wall here. The next two blockers after COPY_DATA to GDS, in case you want to reproduce:

    Build: main @ 0029e62 (2026-10-15 morning), Windows, RTX 3060. It loaded without #6088 for me, so that unresolved _Cnd_init_with_default_name_override may be new on later main.

    1. COPY_DATA with GDS source or destination (libSceAgcDriver/Execution/src/Pm4.cpp). The rejected packets decode as GDS offset 0x20 to memory (src_sel 3) and an immediate to GDS offset 0x20 (dst_sel 3). I mirrored what DMA_DATA already does: GDS is GdsAddress() + offset.

    2. Next abort, about frame 360: MSAA_ENABLE on a single-sample target for primitives other than triangles is unsupported (register 0x242 = 0x00000007). Type 7 is TRI_WITH_WFLAGS, still a triangle primitive, so singleSampleMsaaRejection in Graphics/src/State.cpp can accept it.

    After both: about 790 frames at around 45 FPS, a loading screen with the loading badge animating, the title starts loading its UI scripts, then a queue stalls on WAIT_REG_MEM_64 ... ref 0x312 ... value 0x0 (label never written), FPS drops to about 0.03 and the process finally dies with Windows 0xc0000374 heap corruption. Details are in my comment above.

    These are experiments. They get the game further but I have not checked that anything renders correctly, and I do not know if they contribute to the heap corruption.

    Patch (15 lines changed, against 0029e62)
    diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
    index bdee46ee1..33f4921ce 100644
    --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
    +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
    @@ -166,6 +166,7 @@ std::vector<std::byte> copyDataSource(std::span<const std::uint32_t> packet, con
             }
             return data;
         }
    +    if (source == 6) return copySource(GdsAddress() + packet[2], bytes, false);
         if (source != CopyDataGpuClockSource) return copySource(address(packet[2], packet[3]), bytes, CopyDataImmediate(source));
         const auto value = gpuClockCount();
         std::vector<std::byte> data(bytes);
    @@ -495,8 +496,13 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
                     require(packet[5] == 0 && packet[4] >= ContextRegisterBase && packet[4] - ContextRegisterBase <= 0x400u - copyDataCount(packet), "COPY_DATA to a register outside the context registers is not implemented");
                     break;
                 }
    -            require(destination == 2 || destination == 4 || destination == 10, "COPY_DATA GDS or performance-counter destination is not implemented");
    -            require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11 || source == CopyDataGpuClockSource || source == CopyDataRegisterSource, "COPY_DATA GDS or reference-clock source is not implemented");
    +            // EXPERIMENT: COPY_DATA to and from GDS (select 3), modelled like DMA_DATA's GDS access
    +            const bool gdsDestination = destination == 6;
    +            const bool gdsSource = source == 6;
    +            require(destination == 2 || destination == 4 || destination == 10 || gdsDestination, "COPY_DATA performance-counter destination is not implemented");
    +            require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11 || source == CopyDataGpuClockSource || source == CopyDataRegisterSource || gdsSource, "COPY_DATA reference-clock source is not implemented");
    +            require(!gdsDestination || (packet[5] == 0 && gdsRange(packet[4], CopyDataBytes(packet))), "COPY_DATA GDS destination range exceeds the GDS");
    +            require(!gdsSource || (packet[3] == 0 && gdsRange(packet[2], CopyDataBytes(packet))), "COPY_DATA GDS source range exceeds the GDS");
                 require(source != CopyDataRegisterSource || (packet[3] == 0 && registerBankBase(packet[2]) != 0 && registerBankBase(packet[2]) == registerBankBase(packet[2] + copyDataCount(packet) - 1)), "COPY_DATA from a register outside the shader, context and user config registers is not implemented");
                 require(source < 10 || source == CopyDataGpuClockSource || ((packet[1] & 0x10000u) == 0 && packet[3] == 0), "64-bit immediate COPY_DATA is not implemented");
                 break;
    @@ -625,6 +631,7 @@ std::optional<StoreWrite> ResolveStore(std::span<const std::uint32_t> packet, co
             case 0x40: {
                 if (packet.size() < 6 || CopyDataToRegister(packet)) return std::nullopt;
                 if (CopyDataSource(packet) == CopyDataRegisterSource) return std::nullopt;
    +            if ((((packet[1] >> 8u) & 0xfu) << 1u) == 6) return std::nullopt;
                 const std::size_t bytes = CopyDataBytes(packet);
                 const auto destination = address(packet[4], packet[5]);
                 if (!fits(destination, bytes)) return std::nullopt;
    @@ -703,6 +710,7 @@ std::vector<PacketStore> DecodeStores(std::span<const std::uint32_t> packet) {
             case 0x40: {
                 require(packet.size() == 6, "invalid COPY_DATA size");
                 if (CopyDataToRegister(packet)) return {};
    +            if ((((packet[1] >> 8u) & 0xfu) << 1u) == 6) return {};
                 const auto destination = address(packet[4], packet[5]);
                 if (CopyDataImmediate(CopyDataSource(packet))) return {pattern(destination, CopyDataBytes(packet), packet[2])};
                 return {runTime(destination, CopyDataBytes(packet))};
    @@ -1103,7 +1111,7 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
                     for (std::uint32_t i = 0; i < count; ++i) writeRegister(queue, 0x69, packet[4] - ContextRegisterBase + i, values[i]);
                     return;
                 }
    -            const auto destination = address(packet[4], packet[5]);
    +            const auto destination = (((packet[1] >> 8u) & 0xfu) << 1u) == 6 ? GdsAddress() + packet[4] : address(packet[4], packet[5]);
                 const auto data = copyDataSource(packet, queue, CopyDataBytes(packet));
                 GuestMemory::CheckRange(reinterpret_cast<void*>(destination), data.size(), 1, true);
                 GuestMemory::Write(destination, data);
    diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp
    index c16eb7847..cc8741394 100644
    --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp
    +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp
    @@ -408,7 +408,7 @@ std::string singleSampleMsaaRejection(const QueueState& queue) {
                 const auto topology = (*parameters >> 5u) & 7u;
                 if ((domain != 1u && domain != 2u) || (topology != 2u && topology != 3u)) return zeroMessage(0x2db, *parameters, primitives);
             }
    -    } else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u) {
    +    } else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u && *primitive != 7u /* EXPERIMENT: TRI_WITH_WFLAGS is a triangle primitive */) {
             return zeroMessage(0x242, *primitive, primitives);
         }
         return {};
  9. The-IOMIOU commented on Oct 17, 2026

    @The-IOMIOU
    Author

    Third update, on main @ b4f05ab (2026-10-18), Windows, RTX 3060, PPSA03541 v01.031, with the hard-link layout from above. The title now goes quite a bit further, then dies with heap corruption. Plus a separate bug in the new pipeline journal.

    How far it gets (screenshots taken from the window)

    1. Black frames while ~13k shaders compile on a cold cache (about 7 minutes, shader cache grows to ~10 GB)
    2. The EA SPORTS UFC 5 loading screen (title art, spinner bottom right), about frame 420 at 11 FPS
    3. The EA SPORTS "it's in the game!" intro video, about frame 576 at 15 FPS
    4. Main menu UI scripts start loading (ui/features/main/vvms/mainmenu/mainmenuhandler.lreq, movieutils.lreq, notifications.lreq), then /download0/postlaunchassets/postlaunchassets.catalog is opened for write, and about 7 s after the intro video the process dies with Windows 0xc0000374 (heap corruption). Same stage as the earlier run on 2026-10-15.

    Three local changes I needed to get that far

    These are experiments. They get past aborts, I have not verified rendering correctness beyond the screens above.

    1. COPY_DATA with a GDS source or destination (Execution/src/Pm4.cpp), as in my earlier comment: GDS offset 0x20 to memory (src_sel 3) and immediate to GDS (dst_sel 3), done like DMA_DATA does it with GdsAddress() + offset.
    2. MSAA_ENABLE on a single-sample target for primitives other than triangles is unsupported (register 0x242 = 0x00000007). On current main, singleSampleMsaaRejection calls nonTriangleRejection(queue, "...") without the rectangles argument, so VGT_PRIMITIVE_TYPE 7 is still rejected there, while singleSampleDepthScanRejection passes true. Passing true at that call lets the title through. (I called type 7 TRI_WITH_WFLAGS earlier; upstream treats 7 and 17 as rectangle primitives.)
    3. unsupported PA_CL_CLIP_CNTL flags is unsupported (register 0x204 = 0x0c090000). ClipControlMask in Graphics/src/State.cpp allows bits 19, 24, 26, 27 but not bit 16 (CLIP_DISABLE). The title sets 16 together with 19, 26 and 27. I added 0x10000u to the mask, which in practice means "depth clamp from 26/27, ignore the rest". Not a faithful implementation of CLIP_DISABLE.

    New bug: pipeline journal crashes the title on a warm start

    With the cache from a previous run present, the process dies within seconds, every time, before a window appears:

    [pipeline-cache] loaded 94191.7 KiB from ...\vk-pipelines-10de-2503-...bin
    [shader-disk-cache] ...\shader_cache\bb5f99f69ba99b5d (source version bb5f99f69ba99b5d, format 36)
    [pipeline-journal] loaded 126 pipelines and 136 modules (15.5 MiB) from ...\pipelines.journal in 18 ms
    

    Windows logs exception 0x80000001 (guard page violation) in ufc5.exe, fault offset 0x3c48300, identical in both runs, no project crash message. Renaming pipelines.journal out of the way makes the same build run normally (and fast, since the rest of the cache is warm). The first run, with no journal yet, also worked. So this looks like a bug in the journal's replay or in what it triggers, not in the title.

    Heap corruption (0xc0000374) in the driver worker

    I read the minidump (no debugger here, so by hand). The faulting thread is a driver worker (Driver::enqueue lambda → AgcDriver::DriverDetail::Driver::run(unsigned int) at +0x3113). The stack shows std::_Sp_counted_ptr_inplace<std::map<unsigned long long, std::vector<std::shared_ptr<AgcDriver::DriverDetail::ShaderSnapshot const>>>> (that is ShaderRegistry) in _M_release_last_use_cold, then a long recursion of std::_Rb_tree<...ShaderSnapshot...>::_M_erase, then ntdll's heap check. So Windows detects the damage while the last reference to a copy of the shader registry is released at the end of a submission. The write that corrupted the heap most likely happened earlier, somewhere else. I could not find it without a heap checker (page heap or ASan).

    I do not know whether my three changes above contribute to it. If someone can run this title under page heap or a sanitizer build, that would pin it down.

    Happy to test patches on this title.

  10. The-IOMIOU commented on Oct 18, 2026

    @The-IOMIOU
    Author

    Fourth update, PPSA03541 v01.031, Windows, RTX 3060. Short version: the heap corruption from my last comment was my toolchain, not AnyPS5, and with the supported compiler the title now goes through the menus (consent, difficulty) and loads into the ring.

    Root cause of the 0xc0000374: unsupported GCC 16.1

    I had WinLibs GCC 16.1.0 r2 instead of the supported 15.2.0 r7 from docs/dev/BUILD.md. In that build's libstdc++-6.dll, __gnu_cxx::__exchange_and_add is the generic mutex fallback (lock, load, add, store, unlock) instead of lock xadd. The shared_ptr paths that are inlined from the headers (weak_ptr::lock's CAS, the _M_release "both counts are 1" fast path) use real atomics and do not take that mutex, so concurrent updates get lost.

    How it showed up: the AGC driver's ShaderRegistry copies were freed while a submission or Driver::shaders still referenced them (registry nodes overwritten, double erase, heap check in Driver::run releasing a submission). I caught it with a local diagnostic that puts each published registry copy in its own read-only pages: the current registry's deleter ran from Driver::run → _M_release_last_use_cold while its control block still read use count 83. With 15.2.0 r7 (__exchange_and_add is a single lock xadd) the same diagnostic reports nothing, and the crash is gone.

    Maybe worth a configure-time check or a note, since winget currently installs 16.1 as "WinLibs POSIX UCRT".

    Real fix: warm-start crash was a stack-walk into the main thread's guard page

    The 0x80000001 I blamed on the pipeline journal is not the journal. The title runs a thread (entry in the eboot) that walks the main thread's stack from its top down to the bounds libkernel reports. Guest threads have fully committed stacks, but the adopted host main thread's bounds come from GetCurrentThreadStackLimits, which includes uncommitted pages and the guard page. Touching another thread's guard page raises STATUS_GUARD_PAGE_VIOLATION. With the journal the title just starts faster and gets there before the main stack has grown. I commit the whole adopted host stack in SetStackFromHost (libkernel/Pthread/src/Thread.cpp), as StartNativeThread already requires for guest threads. The warm start with the journal works after that.

    Local workarounds still needed (not proposals as they are)

    1. COPY_DATA with a GDS source or destination (Pm4.cpp), like DMA_DATA's GDS handling.
    2. singleSampleMsaaRejection passes rectangles = true (VGT_PRIMITIVE_TYPE 7).
    3. PA_CL_CLIP_CNTL bit 16 added to ClipControlMask (approximated by depth clamp).
    4. New, in the ring: shader info collection failed: interpolation parameter reference is out of range (attribute 0 component 0 mode 0, 0 inputs). The title draws with SPI_PS_IN_CONTROL.NUM_INTERP = 0 and a pixel shader that interpolates attribute 0. I replace out-of-range interpolation parameters with 0 before info collection to see what comes next. On hardware that read returns whatever is in parameter memory, so I would not upstream this as is.

    Happy to test patches or provide traces.

  11. The-IOMIOU commented on Oct 18, 2026

    @The-IOMIOU
    Author

    Performance numbers from the fight (in the Octagon, warm shader cache, same build as my last comment, Windows, RTX 3060). It renders correctly but runs at about 1 FPS. From APS5_TRACE_SUMMARY_EVERY=15 and APS5_PROFILE_GPU=1:

    frame_ms                 p50 ~900, p99 ~1200
    gpu_busy_ms_per_present  ~140
    worker_busy_pct          queue worker 0x0: 98 %, 0x28..0x2f: ~10 % each
    top phases (ms per frame)
      Driver.Packet.memory_wait          722
      Driver.Packet.draw / Driver.Draw   690
        Driver.Draw.draw_enqueued        473
        shader_compile_and_link          183
      Driver.Packet.dispatch             265
    [gputime] ~2800 batches per 10 s (~260 per present)
              program 0x11426ce000: 1 dispatch per present, ~30 ms GPU each (over a third of all GPU time)
    

    So the GPU is mostly idle. Queue 0 spends most of its time in memory_wait, and the batch count suggests each wait on an end-of-pipe label turns into a submit plus a CPU wait. The rest is single-threaded draw translation on queue worker 0. Separately, compute program 0x11426ce000 is unusually expensive for one dispatch.

    I can capture a full APS5_TRACE_FRAMES window of the fight, or run with any other switches, if that helps.

  12. Logan1h commented on Oct 21, 2026

    @Logan1h
    Contributor

    Current state on a local tree, with the fixes the title needs applied, for whoever is working on the frame cost: the title no longer stops and reaches a fight, and here is where its frame goes.

    Tree: main 2f161d4f3 plus #6549 (COPY_DATA GDS, required), #6604 (missing pixel interpolators, required), #7048 (write-watch refusal, required – it removes the abort this issue used to hit), and the perf PRs #8484, #8735, #8672, #8727, #8888, #8911, #8912, #8913, #8928, #8117. Draft #6946/#6947 are not included.
    Host: Windows 10.0.26300, Ryzen 5 7500F (12 threads), 32 GiB, RTX 4070 SUPER / NVIDIA 617.14, WinLibs GCC 15.2.0, Release, 1920x1080.
    Switches: APS5_TRACE_SUMMARY_EVERY=10, APS5_PROFILE_GPU=1.

    frame p50 p90 p99 GPU busy / present presents per 10 s
    front end / dojo 77 ms 81 ms 83 ms 19 ms (25%) 130
    in-game fight 359 ms 400 ms 460 ms 84 ms (23%) 28

    So the front end runs at ~13 FPS, a fight at ~2.8 FPS, and the GPU is idle for three quarters of the fight frame.

    Where a fight frame goes (top_phases_ms_per_frame, summed over traced threads):

    • Driver.Packet 626 ms
    • Driver.Packet.memory_wait 238 ms
    • Graphics.Draw 230 ms, of which Graphics.Draw.draw_and_resource_release 217 ms
    • Driver.Packet.draw 209 ms, Driver.Draw.draw_enqueued 125 ms
    • Driver.Packet.dispatch 120 ms
    • Driver.Draw.shader_compile_and_link 223 ms in the intervals that hit a new program (absent from the steady ones)

    Serialization is the main cost, not GPU work. With 8 queue workers, queue worker 0x0 sits at 97 % for the whole fight while the other seven are at 10–13 %. Per 10 s the main queue accepts ~10 300 DISPATCH_DIRECT and ~9 500 DRAW_INDEX_AUTO packets, so a fight frame is ~1 000 guest commands recorded on one thread, and memory_wait on that thread is time it cannot spend recording. That is exactly the shape #6946 (move the second half of a draw off the worker) and #6947 (stop rebuilding descriptors per draw on the CPU) are meant to cut, so those two look like the right next step for this title.

    The front end, for comparison, is CPU-bound but flat: Driver.Packet 102 ms, dispatch 55 ms, Graphics.Draw 48 ms, and memory_wait has dropped out of the top phases entirely.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions