Skip to content

fix(agc): land GPU writes over guest memory before a host write into it - #6926

Open
EagleBearerYS wants to merge 11 commits into
boykopovar:mainfrom
EagleBearerYS:fix/agc-host-write-pending-storage
Open

EagleBearerYS wants to merge 11 commits into
boykopovar:mainfrom
EagleBearerYS:fix/agc-host-write-pending-storage

Conversation

@EagleBearerYS

@EagleBearerYS EagleBearerYS commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

A host write into guest memory doesn't wait for the GPU writes recorded or in flight over the same memory, and doesn't store the storage-image results still pending there.

  • Which writes are host writes: every write made through a GuestArena::HostWrite scope. That covers the APR read (libkernel/Apr/src/Apr.cpp:159-170), AIO, read/pread/readv, recv/recvfrom in libkernel and libSceNet, the fcntl/ioctl outputs, libc's file reads and WebKit's mkstemp.
  • What the scope does today: it never reaches the driver. On Linux it does nothing at all, since GuestWriteWatchBeginHostWrite is Apple-only.

Why that loses data: a storage image's write-back into host-imported guest memory is recorded, not made, while a recorder is active.

  • StorageTexture::writeBackLayers (Graphics/src/Texture.cpp) decides which 64 KiB blocks to keep for the CPU, records the retile and the copy into the open batch, and notes a pending write. The bytes land when that batch runs.
  • So a store recorded before a host write can land after it, over the data the host just wrote.
  • A host write that covers only part of a block over pending results loses the other way. The store keeps the whole block for the CPU, so the image's results in the rest of it never land.

Related: #6636, where the crash below was traced; #6234, the read direction (pending GPU results flushed before file writes), whose hook this one pairs with.

The title. Epic Mickey: Rebrushed (PPSA15657, UE 4.27) crashes now and then while it loads its first level. johnmaia ran it on Linux, RX 7600 (RADV, Mesa 25.2.8), on main 5d9dcc4 + #6236 with no other changes (#6636, comment):

Runs Failed before reaching the level
APS5_STORE_AT_FLIP=1 16 0
Unset, alternated 8 3
  • The three failures:
    • ObjectSerializationError on SM_MachineSet_01b (UE's "Serial size mismatch");
    • LowLevelFatalError [Line: 1609] right after the title opens /savedata0/ue4savegame.dpx.sav;
    • a SIGSEGV in the title's own code.
  • The trace behind them:
    • On main, about 2 runs in 5 crash this way. In those, a 64 KiB block of file data the title had just read is zeroed about 18 ms after the read, with no CPU write path of the driver involved (#6636, comment).
    • In a traced run, the APR read of a crashing pair landed on a block that a storage-image write-back had covered about 50 ms before it (#6636, comment).
  • How this PR fits it: APS5_STORE_AT_FLIP=1 stores pending results at every flip, which closes that window. This PR closes it at the host write itself.
  • Confirmed in the title: on that base, none of johnmaia's 12 runs with this fix failed, against 6 of 24 without it (#6636, comment; Tested below). No open issue tracks this crash.

The fix:

  • libc: GuestArenaBeginHostWrite_nid_postfix now calls a hook first, on every platform, for every non-empty range. The setter is GuestArenaSetHostWriteHook_nid_no_patch, so every HostWrite user above gets it with no change at the call site.
  • Driver: it registers GuestMemory::PrepareHostWrite in Driver::Driver and clears it in ~Driver. PrepareHostWrite calls FlushGpuWrites under ReadSiteScope(ReadSite::Store), as the driver's own guest stores (GuestMemory::Write, WriteChanged) already do. That call:
    • waits for every open or in-flight batch whose writes overlap the range;
    • stores the results still pending there;
    • waits for that store too.
  • Teardown (2ff4ed6, from CnRJay's review): the hook is an atomic pointer (GuestArena.cpp). Driver::stop() now clears it first, before the worker joins, then sets a stopped flag and waits up to 1 s for calls already inside the hook to leave. PrepareHostWrite counts itself in flight (seq_cst) before it checks the flag and returns at once if it is set, so either a call sees the flag or stop() sees the call. The wait is bounded because on Windows ExitProcess can end a thread inside the hook before ~Driver runs; if the bound expires, stop() logs it and goes on. ~Driver still clears the hook as before.
  • The macOS import window: it opens its own HostWrite scope under the tracker lock, so it skips the hook through a thread-local guard and can't re-enter the tracker.
  • Locks: no caller holds a mutex when the hook runs.
    • libSceNet releases its g_mutex before its HostWrite.
    • DescriptorControl's lock is the guest record-lock struct, not a mutex.
    • The other sites hold nothing.
    • The GPU waits run with the GPU mutex released when the hook's acquisition is the outermost, as for any CPU access.
    • Checked (c272007, from CnRJay's review): a host write made while the thread holds the GPU mutex is counted (GuestMemory::LockedHostWrites) and logged once with its thread, address and size, then goes on. With APS5_ASSERT_GPU_LOCK=1, the switch AssertGpuLockHeld already reads, it aborts with a [lock] ASSERT: line instead. It doesn't throw in a normal run: with the mutex held, the hook takes FlushForAccess's nested locked wait, which hangs only if completing needs another thread to take the GPU mutex, so a throw would turn a possible hang into a certain crash. The debt file states the rule.
  • The debt entry (docs/dev/debt/libSceAgcDriver.md) is updated. The title's own CPU stores still aren't ordered after a recorded store: they survive only in the 64 KiB blocks the write watch saw written. And its CPU reads after a label still read stale memory.

Cost: the hook adds about 75-90 ns to a 64 KiB host write with nothing to wait for (82.5-97.7 ns against 6.9-7.1 ns, the test's --cost mode). That's the flush hook's fast path, well under the read() it precedes.

  • The slow path (276f67d, from CnRJay's review): with APS5_PROFILE_DRAW=1, a [hostwrite] line every 10 s counts the host writes, those that waited for GPU work over their range, and the total and longest wait, e.g. [hostwrite] 2 host writes, 1 waited for GPU work over their range: 1.34 ms in total, 1.34 ms at most. Without the switch nothing is counted. The title's numbers aren't measured yet.

How:

  1. fix(agc): land GPU writes over guest memory before a host write into it: the hook, the driver's registration, the debt entry and the new test agc_driver_host_write_pending_storage (tests/execution/HostWritePendingStorage.cpp).
    • The setup: a compute dispatch writes a 128×120 RGBA16F, 64 KiB-tiled storage image over a 128 KiB write-watched guest surface.
    • The host write: a file is read into the surface the way libc's _readFile does it, std::ifstream::read inside a GuestArena::HostWrite scope. A later FlushGpuWrites plays the guest's read.
    • The check: the file's bytes must survive, and the image's texels must be everywhere else.
    • Its four cases:
      • a store recorded before a 64 KiB host write;
      • the same with a 4 KiB host write;
      • a 64 KiB host write over pending results;
      • a 4 KiB host write over pending results.
  2. refactor(agc): name the host write hook as a pair to the host read hook: names the setter and the driver function after fix(libs): flush pending GPU results before file writes #6234's host read hook (GuestArenaSetHostReadHook_nid_no_patch, PrepareHostRead). The _nid_no_patch suffix follows CONTRIBUTING.md for helper symbols that must not become NIDs.

Tested

  • Epic Mickey: Rebrushed (PPSA15657), johnmaia's runs: Linux, RX 7600 (RADV, Mesa 25.2.8), short runs to the first level, APS5_STORE_AT_FLIP unset (#6636, comment). Current main rarely shows the crash, so they ran the fix on the base where it fails:

    Tree Runs Failed before reaching the level
    main 5d9dcc4 + fix(libScePlayGo): let users answer unlisted chunks #6236 8 3
    the same + fix(agc): store pending image results after the range's writers finish #6364 (unrelated to this case) 16 3
    the same + this PR's first two commits (99af947, 082c326) 12 0
    main e751dd2 8 0
    • All 12 runs with the fix reached the level and played to the end of the run. None showed the ObjectSerializationError, the LowLevelFatalError [Line: 1609] or the SIGSEGV.
    • The title's log line: in a traced run with the fix (APS5_TRACE_APR=1 APS5_TRACE_FLUSH=1), 23 APR reads were each followed at once by the hook storing a pending image over their destination, before the read's bytes were written:
      [apr] read …/recolored-ps5.ucas offset=0x10a89ca0 size=0x41338 -> 0x204c540000
      [flush] image 0x204c540000+0x60000 for memory access 0x204c540000+0x41338
      
    • They ran the first two commits. The three after them (the teardown, the lock check and the counters) keep the same flush: PrepareHostWrite still calls FlushGpuWrites over the written range.
  • Alone in the Dark (PPSA08240, v1.00), LeoFeitosa's runs (comment): Windows 11 (26200), RTX 5070 Ti (driver 617.42), WinLibs GCC 15.2.0, Release.

    • Both sides were built from the same tree, with the same shader cache and save state. Every run sets APS5_PLAYGO_UNLISTED_CHUNKS=installed, and has no input until the main menu.
    • Six alternating pairs, each from launch to the main menu or the crash:
    Tree Runs Reached the main menu
    main cd700fe 6 1
    this PR (2c5b772) 6 5
    main + APS5_STORE_AT_FLIP=1 3 3
    • The crashes on main, all while loading:
      • 2 × LowLevelFatalError [Line: 3230], "Serial size mismatch" on UI textures;
      • 1 × 0xc000001d on FAsyncLoadingThread;
      • 2 × 0xc0000005 at Alone in the Dark.exe+0x20ec0bc.
    • The one crash with this PR is the last kind, 0xc0000005 at +0x20ec0bc. Whether it shares the cause isn't traced.
    • Eight more runs with this PR under APS5_TRACE_APR=1 APS5_TRACE_FLUSH=1 all reached the main menu, so this PR (2c5b772; the head since only merges main) has reached it in 13 of 14 runs (comment). The tracing changes the timing, so those eight runs don't rule the remaining crash out.
  • Linux (lavapipe, Mesa 25.2.8, gcc 13.3, CI's Linux configure), on main 66e290f. Each check ran in all three write-watch modes: userfaultfd user-mode only (unprivileged), full userfaultfd (root), and unwatched (APS5_NO_WRITE_WATCH=1).

    • Red (only the driver's registration reverted, so libc calls nothing, as on main): 0/20 in each mode. Every run fails the same three cases:
      host write over pending storage, a store recorded before a 64 KiB host write: FAILED, byte 0x0 of the file read was overwritten by the image's store after it (0x0, the file has 0x40)
      host write over pending storage, a store recorded before a 4 KiB host write: FAILED, byte 0x3000 of the file read was overwritten by the image's store after it (0x0, the file has 0x40)
      host write over pending storage, a 4 KiB host write over pending results: FAILED, byte 0x5 outside the file read holds 0x0, not the image's 0x47, so its results there never landed
      
      The recorded store zeroes the file's bytes, which is the zeroed block of the title's trace. The case of a whole-block host write over pending results already passes on main.
    • Green: 20/20 in each mode, at both commits.
    • ctest -R '^agc' --parallel 4 --timeout 120: 625/625 at the head, 43 skipped as expected.
    • ctest -R 'guest|libc|kernel': the same as main on that machine. The 5 failures are IPv6 tests the container always fails.
  • GitHub runners (run 38952984815), with only the registration reverted for the red:

    • Linux: red, the test failed in each of 5 runs; green 20/20. The libc and libkernel suites pass both ways.
    • Windows: it builds with MinGW, but the test skips on that runner, which has no Vulkan device. guest_kqueue fails there with and without the change.
  • Merged with fix(libs): flush pending GPU results before file writes #6234 (0f5651f) and with fix(agc): cache inaccessible guest pages #6184 (4a96406), each on its own, on lavapipe. The new test passes 10/10 on both merges.

  • Windows code: MinGW -fsyntax-only of GuestMemory.cpp, Lifecycle.cpp, GuestArena.cpp and the test: 0 errors.

  • The review follow-ups (Linux, lavapipe, CI's configuration), each with its case in the new agc_driver_host_write_hook, in the same commit as its change:

    • Teardown: after Driver::Shutdown, a host write over a range with a recorded store must not reach the driver's flush hook. Red at the previous head: "a host write over a range with a recorded store reached the driver's flush hook 1 time(s) after Driver::Shutdown".
    • The lock check: a host write made while holding the GPU mutex returns and moves the count by one. Red on a base with only the counter's accessor: "the count moved by 0, not 1". With APS5_ASSERT_GPU_LOCK=1 the test aborts at the [lock] ASSERT: line, as intended.
    • The counters: a host write over a recorded store counts one call that waited; one over an idle surface counts a call that didn't.
    • agc_driver_host_write_hook and agc_driver_host_write_pending_storage pass 20/20 in each of the three write-watch modes, and ^agc passes 626/626.
    • GitHub runners (run, with test(agc): align the indirect arguments block at run time #6452's MinGW fix for an unrelated test applied for this check only): on Linux both tests pass 10/10; on Windows they skip without a Vulkan device and the build is clean. The ^agc sweeps' only failures are agc_driver_preparation_scope_settle on Linux, a timing check under parallel load, and agc_driver_pm4_replay_tool (0xc0000139) on Windows, which fails on main there too.
  • AMD Radeon 780M (RDNA 3, AMD driver 25.10.20.01), Windows 11 (WinLibs GCC 15.2.0, Release), run by keravyn-ops at 276f67d (comment): agc_driver_host_write_hook and agc_driver_host_write_pending_storage pass.

  • NVIDIA GeForce RTX 3080 (NVIDIA 617.42), Windows 10 Pro 22H2 (WinLibs GCC 15.2.0, Release), run by TheSovietWaffle at 276f67d (comment): agc_driver_host_write_hook and agc_driver_host_write_pending_storage pass 5 of 5 runs each, and the 12 guest_socket_host_write_* tests, which go through the changed GuestArena host-write path, pass.

  • NVIDIA GeForce RTX 3070 (driver 595.99.02) and lavapipe (Mesa 26.0.3, LLVM 21.1.8), Ubuntu 26.04 (GCC 15.2, Release), run by jotitadev at 276f67d merged onto main 12a71f3 (review): both new tests pass, 3/3 on the 3070. ^agc has no new failures against main: 636 of 638 on the 3070, where agc_driver_f16_fma_denormal_modes and agc_driver_fma_mix_f16_denormal_modes fail on main too, and 638 of 638 on lavapipe. With only the hook's registration in Driver::Driver removed, the same three cases fail on both devices as in the red above, and the whole-block case passes.

  • Merged with main cd700fe and 86b8424 (2c5b772, then 9f5745d, the head). Each conflicted only in the debt file, where both sides' entries are kept. On GitHub's runners at the head (run), agc_driver_host_write_pending_storage, agc_driver_host_write_hook, agc_driver_storage_file_write and agc_driver_graphics pass 3 runs in a row on Linux. On Windows three of the four skip without Vulkan and agc_driver_graphics passes. The libc and libkernel suites pass on both.

  • Merged with main 0d078f7 (46d8f39, no conflicts), on GitHub's runners (run): the ^agc tests pass on Linux (711, 0 failed), and the libc and libkernel suites pass. On Windows the only failures are agc_driver_pm4_replay_tool and agc_shader_replay_tool, which fail the same way on that runner for an unrelated branch (run).

  • LEGO Horizon Adventures (PPSA14632), run by winstrex (comment): Windows 11, NVIDIA GeForce RTX 5070 Ti (NVIDIA 591.86), warm shader cache, the same input script each run.

    • With this PR, the [hostwrite] line shows host writes landing on pending GPU work over their range in every run (4 to 60 per run).
    • The FAsyncLoadingThread crash with ObjectSerializationError ... Serial size mismatch after the graphics quality screen came in 2 of 2 runs on main 7ce3b1d, 1 of 6 on this PR's base 86b8424 and 0 of 7 with this PR (9f5745d). winstrex calls that not conclusive at these counts.
    • One of the 7 runs with this PR crashed later with a different corruption (UObjectHash.cpp line 42).
  • tools/check_conventions.py: passed.

Overlap. On main 1b1f848, the overlap check finds three open PRs that conflict with this one. 37 others that touch the same files conflict with main themselves, so they couldn't be checked.

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
  • 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
  • Related: perf(agc): reuse address-based draw builds whose inputs are unchanged #6636, fix(libs): flush pending GPU results before file writes #6234
  • Unblocks: Epic Mickey: Rebrushed (PPSA15657), its first-level load crash from perf(agc): reuse address-based draw builds whose inputs are unchanged #6636's thread (johnmaia: 0 of 12 runs failed with the fix, 6 of 24 without); Alone in the Dark (PPSA08240), its crashes while loading to the main menu (LeoFeitosa: the main menu in 13 of 14 runs with this PR, 1 of 6 on main)
  • GPUs tested: lavapipe (Mesa 25.2.8) on Linux; johnmaia's RX 7600 (RADV, Mesa 25.2.8) in the title; NVIDIA GeForce RTX 5070 Ti (617.42), Windows 11, in Alone in the Dark (LeoFeitosa); AMD Radeon 780M (RDNA 3, AMD driver 25.10.20.01), Windows 11: both new tests pass (keravyn-ops); NVIDIA GeForce RTX 3080 (NVIDIA 617.42), Windows 10: both new tests pass 5 of 5 (TheSovietWaffle); NVIDIA GeForce RTX 3070 (driver 595.99.02) and lavapipe (Mesa 26.0.3), Ubuntu 26.04: both new tests pass (jotitadev); NVIDIA GeForce RTX 5070 Ti (NVIDIA 591.86), Windows 11, in LEGO Horizon Adventures (winstrex); the Windows runner has no Vulkan device
  • AI-assisted: yes

A host write into guest memory (a file, AIO or APR read, a socket receive) went straight to the pages. Storage image results pending over the range were stored later, when something read the memory, and a store already recorded or in flight landed whenever its batch ran. Either could overwrite the host's bytes: a write-back recorded before an APR read landed over the file data after it, and a host write into part of a 64 KiB block left the rest of the block without the image's results.

libc now calls a host write observer at the start of every GuestArena::HostWrite scope, on every platform. The driver registers one that does what its own guest stores do: it stores the results pending over the range and waits for every recorded or in-flight GPU write there, so the host's bytes land last, as on the console. The driver's own import window on macOS skips it.

The test reads a file into guest memory under a HostWrite scope over a storage image's results, with the image's store recorded before the read and with the results still pending, over a whole 64 KiB block and over 4 KiB inside one.
GuestArenaSetHostWriteObserver_nid_postfix becomes GuestArenaSetHostWriteHook_nid_no_patch, taking the address as std::uintptr_t, and the driver's GuestMemory::FlushBeforeHostWrite becomes PrepareHostWrite, so they read as one pair with the host read hook boykopovar#6234 adds (GuestArenaSetHostReadHook_nid_no_patch and PrepareHostRead). The setter is a helper that must not become a NID, so it takes the _nid_no_patch suffix. Behaviour is unchanged.
@github-actions

github-actions Bot commented Oct 16, 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 8707 docs/dev/debt/libSceAgcDriver.md L111-112
PR 8932 docs/dev/debt/libSceAgcDriver.md L112
PR 9685 docs/dev/debt/libSceAgcDriver.md L112
PR 9879 docs/dev/debt/libSceAgcDriver.md L112

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 1835 core/libs/prx/libc/src/GuestArena.cpp
PR 2434 docs/dev/debt/libSceAgcDriver.md
PR 4448 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 4449 core/libs/prx/libc/include/GuestArena.hpp
core/libs/prx/libc/src/GuestArena.cpp
docs/dev/debt/libSceAgcDriver.md
PR 5021 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libc/src/GuestArena.cpp
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5925 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6169 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6186 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6187 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6192 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6194 docs/dev/debt/libSceAgcDriver.md
PR 6209 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6223 docs/dev/debt/libSceAgcDriver.md
PR 6273 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6280 docs/dev/debt/libSceAgcDriver.md
PR 6292 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6300 docs/dev/debt/libSceAgcDriver.md
PR 6412 docs/dev/debt/libSceAgcDriver.md
PR 6469 core/libs/prx/libc/src/GuestArena.cpp
PR 6481 docs/dev/debt/libSceAgcDriver.md
PR 6548 docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6615 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6741 docs/dev/debt/libSceAgcDriver.md
PR 6804 docs/dev/debt/libSceAgcDriver.md
PR 6808 docs/dev/debt/libSceAgcDriver.md
PR 6867 docs/dev/debt/libSceAgcDriver.md
PR 6868 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6959 docs/dev/debt/libSceAgcDriver.md
PR 6960 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6977 docs/dev/debt/libSceAgcDriver.md
PR 7201 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libc/src/GuestArena.cpp
PR 7256 docs/dev/debt/libSceAgcDriver.md
PR 7604 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7618 docs/dev/debt/libSceAgcDriver.md
PR 7622 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
PR 7803 docs/dev/debt/libSceAgcDriver.md
PR 7978 docs/dev/debt/libSceAgcDriver.md
PR 8014 docs/dev/debt/libSceAgcDriver.md
PR 8180 docs/dev/debt/libSceAgcDriver.md
PR 8365 docs/dev/debt/libSceAgcDriver.md
PR 8395 docs/dev/debt/libSceAgcDriver.md
PR 8527 docs/dev/debt/libSceAgcDriver.md
PR 8542 docs/dev/debt/libSceAgcDriver.md
PR 8592 docs/dev/debt/libSceAgcDriver.md
PR 8672 docs/dev/debt/libSceAgcDriver.md
PR 8999 docs/dev/debt/libSceAgcDriver.md
PR 9020 docs/dev/debt/libSceAgcDriver.md
PR 9335 docs/dev/debt/libSceAgcDriver.md
PR 9387 docs/dev/debt/libSceAgcDriver.md
PR 9412 docs/dev/debt/libSceAgcDriver.md
PR 9431 docs/dev/debt/libSceAgcDriver.md
PR 9432 docs/dev/debt/libSceAgcDriver.md
PR 9508 docs/dev/debt/libSceAgcDriver.md
PR 9592 docs/dev/debt/libSceAgcDriver.md
PR 9672 docs/dev/debt/libSceAgcDriver.md
PR 9673 docs/dev/debt/libSceAgcDriver.md
PR 9674 docs/dev/debt/libSceAgcDriver.md
PR 9675 docs/dev/debt/libSceAgcDriver.md
PR 9676 docs/dev/debt/libSceAgcDriver.md
PR 9748 docs/dev/debt/libSceAgcDriver.md
PR 9856 docs/dev/debt/libSceAgcDriver.md
PR 9891 docs/dev/debt/libSceAgcDriver.md
PR 9892 docs/dev/debt/libSceAgcDriver.md
PR 9893 docs/dev/debt/libSceAgcDriver.md
PR 9895 docs/dev/debt/libSceAgcDriver.md
PR 9898 docs/dev/debt/libSceAgcDriver.md
PR 10026 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
docs/dev/debt/libSceAgcDriver.md

@CnRJay CnRJay 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 right place to fix this (the host write itself, rather than a store at every flip), and the fast path is cheap (82-98 ns against 7 ns for a 64 KiB write with nothing to wait for). Three questions on the slow path and on teardown.

  • When a host write does overlap pending results, PrepareHostWrite waits for the in-flight batches and the store. The call comes from whatever thread did the read()/recv()/APR read: file I/O threads, the network thread, and the title's own loader threads. A loader that streams into a buffer the GPU is still writing now stalls on a GPU round trip per read, which is the stall the description describes as the cost of the flip-time store, only moved. The PR measures the empty case only. Please add counters (hook calls, calls that waited, time waited) to the [rescache]-style profile line and post them for Epic Mickey's first-level load, so the cost of the slow path is seen on a real title, not only in the test.
  • GuestArenaSetHostWriteHook_nid_no_patch(&PrepareHostWrite) is registered in Driver::Driver and cleared in ~Driver. A thread inside a HostWrite scope when the driver is destroyed can call the hook after the clear started, or while Driver members it uses are gone. State that the hook is an atomic pointer and what makes PrepareHostWrite safe once the driver is stopping (it should do nothing after stop()).
  • The lock audit is in the description, but nothing enforces it. A future HostWrite call under a mutex the GPU wait needs (the GPU mutex, a libSceNet lock) deadlocks. Add a check in the hook that throws when the thread holds the GPU mutex outside the supported case, the way DrawPipeline::Drain does, so a violation is an error and not a hang.

Also: the description says the title has not run the fix yet ("Related", not "Fixes"). A log line from Epic Mickey (PPSA15657) on this branch reaching the first level is what the maintainers will ask for; it is worth getting from the reporter before this is looked at in detail.

Only the driver's destructor cleared the host write hook, so a host
write made after Driver::Shutdown still ran PrepareHostWrite and the
flush hook behind it: the queued-label branch takes the GPU mutex and
records labels against the reset device, and on Linux exit() runs the
destructor while title threads still run.

stop() now clears the hook before the workers are joined, sets a
stopped flag PrepareHostWrite checks after counting itself in flight,
and waits for the calls already inside to leave. The wait is bounded
to 1 s, as a thread ExitProcess killed inside the hook never leaves.

The test records a storage image's store over a range, shuts the
driver down, then makes a host write over the range and checks that
it no longer reaches the flush hook.
A host write waits for the GPU work over its range before its bytes
land. Every HostWrite site is a guest I/O path that holds no driver
lock, but one made with GuestMemory::GpuMutex held would take the flush
hook's nested locked wait, which hangs if that work needs another
thread's hold of the mutex to complete.

PrepareHostWrite now counts such a call (GuestMemory::LockedHostWrites)
and logs the first one with its thread tag, address and size, then
continues. Under APS5_ASSERT_GPU_LOCK=1 it aborts with a [lock]
ASSERT: line instead. The debt notes the rule.

The test makes a host write while holding the GPU mutex, checks that
it returns and that the count moved by one, and that a host write
without the mutex is not counted.
Under APS5_PROFILE_DRAW, PrepareHostWrite reads the calling thread's
GPU waits (Recorder::ThreadWaitedMs, given to GuestMemory by the driver
as it gives ShaderMemory) before and after its flush, and counts the
calls, the calls that waited, and the total and longest wait. A
[hostwrite] line reports them every 10 s with the other profile lines.

The test makes a host write over a range with a recorded store, which
counts one call that waited, and one over an idle range, which counts
a call that did not.
@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@CnRJay Thanks. All three are in, each with its test in the same commit:

  • The slow path (276f67d): with APS5_PROFILE_DRAW=1, a [hostwrite] line every 10 s gives the host writes, those that waited for GPU work over their range, and the total and longest wait. For example, from the test: [hostwrite] 2 host writes, 1 waited for GPU work over their range: 1.34 ms in total, 1.34 ms at most. @johnmaia, when you next run Epic Mickey: Rebrushed (PPSA15657) on this branch, could you add APS5_PROFILE_DRAW=1 and post the [hostwrite] lines from the first-level load? That run would also give the log line for "Fixes".
  • Teardown (2ff4ed6): the hook is an atomic pointer. Driver::stop() now clears it first, then sets a stopped flag and waits up to 1 s for calls already inside the hook. PrepareHostWrite counts itself in flight before it checks the flag and returns immediately once it's set, so it does nothing after stop(). The wait is bounded because on Windows ExitProcess can end a thread inside the hook before ~Driver runs. The new test shuts the driver down and checks that a host write over a recorded store no longer reaches it; it reached it once at the previous head.
  • The lock check (c272007): a host write made while the thread holds the GPU mutex is counted and logged once, with its thread, address and size. Under APS5_ASSERT_GPU_LOCK=1 (the switch AssertGpuLockHeld reads) it aborts instead. I didn't make it throw in a normal run. With the mutex held, the hook takes FlushForAccess's nested locked wait, which only hangs if completing needs another thread to take the GPU mutex, so a throw would turn a possible hang into a certain crash. The debt file now states the rule.

@keravyn-ops

Copy link
Copy Markdown
Contributor

AMD / Windows run of this PR at 276f67d: Radeon 780M (RDNA 3, 0x15bf), AMD driver 25.10.20.01 (32.0.21020.1007), Windows 11 Home 10.0.26100, WinLibs GCC 15.2.0 r7, Release.

Every ctest entry built from the test targets this PR changes passes (2):

  • agc_driver_host_write_hook (0.37 s): 463: host write hook tests passed
  • agc_driver_host_write_pending_storage (0.40 s): 464: host write pending storage tests passed

AI-assisted: yes (runs made on the device above).

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

Thanks @keravyn-ops for the AMD runs here, on #6911 and on #6944; they're in each PR's Tested and GPUs-tested lines now.

@TheSovietWaffle

Copy link
Copy Markdown
Contributor

NVIDIA data point for this PR: head 276f67d36, RTX 3080 (NVIDIA 617.42), Ryzen 7 7800X3D, Windows 10 Pro 22H2 (19045), WinLibs GCC 15.2.0 r7, Release, libraries rebuilt from the head.

  • agc_driver_host_write_hook and agc_driver_host_write_pending_storage pass 5 of 5 runs each on the RTX 3080 (device selected in the log, not skipped).
  • The 12 guest_socket_host_write_* tests, which go through the changed GuestArena host-write path, pass.

Not checked: whether the new tests fail on main without the change on this GPU, and titles (I don't have Epic Mickey: Rebrushed).

AI-assisted: yes (Claude Code).

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

Thanks @TheSovietWaffle for the RTX 3080 runs; they're in the Tested and GPUs-tested lines now.

main 4827f36 adds the shader disk cache include to the driver
lifecycle (boykopovar#6641) where this branch adds the guest arena's. Keep both.

@jotitadev jotitadev 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.

A Linux run on NVIDIA and lavapipe, with the hook taken out to see what the new tests catch.

Machine: Ubuntu 26.04, i7-8700, GCC 15.2, Release, BUILD_TESTING=ON. NVIDIA GeForce RTX 3070 (driver 595.99.02) and lavapipe (Mesa 26.0.3, LLVM 21.1.8) on the same host. PR merged onto main 12a71f3f5 (clean). ANYPS5_NO_SHADER_CACHE=1, -j1 --timeout 30.

Run RTX 3070 lavapipe
agc_driver_host_write_hook, agc_driver_host_write_pending_storage pass 3/3 pass
^agc (638 tests), PR vs main no new failures (636/638; agc_driver_f16_fma_denormal_modes and agc_driver_fma_mix_f16_denormal_modes fail on main on this GPU too) 638/638 on both

The tests don't build on main (they use HostWritesCounted), so as the negative control I removed only the GuestArenaSetHostWriteHook_nid_no_patch(&GuestMemory::PrepareHostWrite) line from Driver::Driver. Both tests fail on both devices, 3/3 on the 3070:

host write over pending storage, a store recorded before a 64 KiB host write: FAILED, byte 0x0 of the file read was overwritten by the image's store after it (0x0, the file has 0x40)
host write over pending storage, a store recorded before a 4 KiB host write: FAILED, byte 0x3000 of the file read was overwritten by the image's store after it (0x0, the file has 0x40)
host write over pending storage, a 64 KiB host write over pending results: the file's bytes survived the image's store and its texels landed around them
host write over pending storage, a 4 KiB host write over pending results: FAILED, byte 0x5 outside the file read holds 0x0, not the image's 0x47, so its results there never landed

So on Linux the file-read path reaches the driver only through this hook, and three of the four cases catch it missing. The third case, a 64 KiB host write over pending results, passes without the hook on both devices, so it does not cover the fix on this platform.

Run on 276f67d36; the current head be2be36b5 only merges main, the PR's five commits are unchanged.

Not run: Windows, AMD, Epic Mickey or any title.

GPUs tested: NVIDIA GeForce RTX 3070 (595.99.02) and lavapipe (Mesa 26.0.3), Ubuntu 26.04

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

Thanks for the Linux runs and the negative control. Your three failures are the same ones the red in Tested shows, and the whole-block case passes on main there too, as Tested says. That case isn't a red/green case: it checks that after a whole-block host write over pending results, the file's bytes survive and the image's texels land around them, which main already gets right. I've added your runs to Tested and the GPU list.

@github-actions github-actions Bot added unblocks title Names a title ID on the Unblocks line needs rebase Conflicts with main, rebase needed labels Oct 18, 2026
Keeps the host-write hook setter next to boykopovar#6184's access observer setter in GuestArena.
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 18, 2026
Keeps the host-write hook next to boykopovar#6234's host-read hook in GuestArena, and adds boykopovar#6234's file-write sentence to this branch's pending storage-image debt entry.
…nding-storage

The debt entry keeps this branch's pending storage-image and host-write
bullets next to main's updated bullet for results over memory outside the
write watch.
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 21, 2026
@LeoFeitosa

LeoFeitosa commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Windows / NVIDIA data point, another UE4 title: Alone in the Dark (PPSA08240, v1.00).

Windows 11 (build 26200), RTX 5070 Ti, driver 617.42, WinLibs GCC 15.2.0 r7, Release. Both sides built from the same tree: main cd700fe (the main this PR last merged) and this PR's head 2c5b772. Same converted title, shader cache directory and save state for every run. The dump has no PlayGo files, so every run sets APS5_PLAYGO_UNLISTED_CHUNKS=installed. No input until the main menu is up.

Six alternating pairs, each run from launch to the main menu or the crash (anyps5.log of every run kept):

Reached the main menu Crashed while loading
main cd700fe 1 of 6 5 of 6
this PR 5 of 6 1 of 6
main + APS5_STORE_AT_FLIP=1 3 of 3 0 of 3

The main crashes, all during the boot to the menu:

  • 2 × LowLevelFatalError [Line: 3230] with "Serial size mismatch" on UI textures (T_img_loading_french_quarter, T_Gfx_Derceto_Map_Background);
  • 1 × 0xc000001d on FAsyncLoadingThread at Alone in the Dark.exe+0x21925a0;
  • 2 × 0xc0000005 at Alone in the Dark.exe+0x20ec0bc (Windows event 1000; nothing in anyps5.log).

The one crash with this PR is the last kind: 0xc0000005 at Alone in the Dark.exe+0x20ec0bc. I have not traced whether that one shares the cause.

Earlier, not alternated, on main 559f35d: 2 of 4 runs crashed with these signatures (one of them with APS5_TRACE_LABEL set), and the one run with APS5_STORE_AT_FLIP=1 reached the menu.

The debt file keeps this branch's two entries on pending storage-image
results and host writes, and main's new APS5_SKIP_REFUSED_DRAWS entry
after them.
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 21, 2026
@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@LeoFeitosa, thank you, that's a clear pair: the main menu in 1 of 6 runs on main and in 5 of 6 with this PR, and in 3 of 3 with APS5_STORE_AT_FLIP=1. That main's crashes also go away when pending results are stored at every flip points at the window this PR closes at the host write: a file or APR read landing in memory that a pending GPU store then overwrites.

The remaining 0xc0000005 at +0x20ec0bc may have another cause. This PR orders only host writes (file, AIO and APR reads, socket receives) after pending stores. The title's own CPU stores and reads aren't ordered (the debt entry says so), while APS5_STORE_AT_FLIP=1 stores everything pending at each flip, whoever touches the memory next. If you have time for more runs, the anyps5.log of a crash with this PR under APS5_TRACE_APR=1 APS5_TRACE_FLUSH=1 would show which reads met pending images before it: each one prints a [flush] image … for memory access … line right after its [apr] read.

I've added your runs to the description.

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

Copy link
Copy Markdown
Contributor

@EagleBearerYS I ran it with APS5_TRACE_APR=1 APS5_TRACE_FLUSH=1 on the same head (2c5b772), same setup as before: 8 runs, all 8 reached the main menu, so there is no crash log to show yet. Each log has about 26,000 [apr] lines and about 110,000 [flush] lines, so the tracing was active.

With the untraced runs, this head has now reached the menu in 13 of 14 runs. The tracing changes the timing, so it may be hiding the remaining +0x20ec0bc crash; if it shows up in later runs I'll post its log here.

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@LeoFeitosa, thanks for the eight traced runs. 13 of 14 with this PR against 1 of 6 on main is now in the description. If the +0x20ec0bc crash comes back in a later run, its log is welcome, but there's no need to run more for it.

@JJenkx

JJenkx commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Second title with the same failure signature, for the record (not a test of this PR).

Setup: Code Violet (PPSA26528) 01.000.028 on Linux, RX 6650 XT, RADV (Mesa 26.2.4). main b5d7637 with #4449 merged locally, APS5_UNIT_SHADOW_MIB=4096, and APS5_STORE_AT_FLIP unset.

Failure: 1 of 8 cold starts ended 8 s after launch with:

LowLevelFatalError [File:.\Runtime/CoreUObject/Private/Serialization/AsyncLoading2.cpp] [Line: 8281]
ObjectSerializationError: /Game/Sci_Fi_Cargo_Hub/Meshes/SM_terminal_1 (0xC47F387B039AAFC) ... StaticMesh ... Serial size mismatch

It was followed by a segmentation fault. The other 7 starts loaded past it to the menus.

I have not run this PR or APS5_STORE_AT_FLIP=1 on this title, so this only adds a second Unreal title where an asset read during async loading sees a wrong size early in startup.

AI-assisted: yes (Claude Code); results above were run on this machine.

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@JJenkx, thank you for the Code Violet (PPSA26528) data point. It's the same signature this PR fixed in Epic Mickey: Rebrushed (PPSA15657), where a storage image's write-back landed after an APR read into the same memory.

If you have time, could you merge this PR (9f5745d; it merges cleanly with main) onto the same tree, main b5d7637 with #4449, and run Code Violet the same way: cold starts on RADV with APS5_STORE_AT_FLIP unset, alternated with that tree without it? At 1 failure in 8, a clean run with the PR only means something over enough starts: 16 per side is a start, and 24 or more would make it clear. A run with APS5_STORE_AT_FLIP=1 and no PR would also show whether it's the same late store.

@winstrex

Copy link
Copy Markdown
Contributor

Another data point, LEGO Horizon Adventures (PPSA14632), Windows 11, RTX 5070 Ti (NVIDIA 591.86), warm shader cache, same input script each run:

  • With this PR, the [hostwrite] line shows host writes landing on pending GPU work over their range in every run (4 to 60 per run).
  • The FAsyncLoadingThread crash with ObjectSerializationError ... Serial size mismatch after the graphics quality screen: 2/2 runs on main 7ce3b1d, 1/6 on this PR's base 86b8424, 0/7 with this PR (9f5745d). Not conclusive at these counts.
  • One of the 7 runs with this PR crashed later with a different corruption: UObjectHash.cpp [Line: 42] ... hash itself may be corrupted.

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@winstrex thanks for the LEGO Horizon Adventures (PPSA14632) runs, and for keeping the input script the same across them. The [hostwrite] counts show this PR's path taken in every run, and the crash counts are in Tested now, with your note that they aren't conclusive yet.

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

agc libSceAgc* 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.

9 participants