Skip to content

fix(agc): store an image-copied buffer copy's destination before later labels - #6324

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
ps54all:fix/agc-alias-copy-store-destination
Oct 18, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
ps54all:fix/agc-alias-copy-store-destination

Conversation

@ps54all

@ps54all ps54all commented Oct 15, 2026

Copy link
Copy Markdown

What

VulkanDevice::CopyBuffer turns a copy whose source range is exactly a live storage image into a device copy between storage images (AliasCopy, StorageTexture::CopyFrom). The copied bytes then stay in the destination image as pending results, like a shader's: they reach guest memory only when something in the driver reads that range (FlushPending through the flush hook), at the idle store, or at a flip/label when APS5_STORE_AT_LABEL/APS5_STORE_AT_FLIP is set. A guest that reads the destination on the CPU after waiting for a label that follows the copy, which is how a GPU readback works, reads the bytes that were there before the copy: the CPU read does not go through the driver.

HOT WHEELS Infinite Rush (PPSA22408, Unreal Engine) copies its 256 KiB GPU message buffer into a readback buffer every frame with a DMA copy, waits for the frame's label and walks the records ([count][id, payload dwords, payload...]). The message buffer's range is also exactly the surface of a live storage image the driver still caches (transient memory the engine reuses), so the copy takes the image path, the readback buffer keeps old vertex data, the walker reads a float as a record length and faults (RenderThread 0, app.exe+0x93f044, read of 0x125...4758) at 115-140 s into every run. With a temporary trace of copies into the readback range:

[copyrange] 0x115ec00000 -> 0x134ab80000 +0x40000 queue 0x0 path 3 ... src 00000000 0f0f0f0f 0f0f0f0f 0f0f0f0f dst 46071d87 4548b393 c43fd1d2 46082aaa

The source holds an empty message buffer (count 0); the destination still holds floats when the CPU reads it.

After AliasCopy succeeds, CopyBuffer now stores the destination image's results at once (FlushPending(destination, bytes, nullptr, "buffer copy destination", PublishScope::Whole)). The store is recorded in the same batch behind the image copy, so a label recorded after the copy is ordered behind it, as it is for the transfer path. The destination image stays cached, so GPU readers of the destination keep using it.

Tested

New test agc_driver_alias_copy_label (tests/execution/AliasCopyLabel.cpp): a compute store leaves a pending 256x256 R32_UINT image over a 256 KiB source, CopyBuffer copies it to a second 256 KiB range (asserts the image-copy path), a label is recorded on the GPU after it and submitted, the test spins on the label with plain CPU reads and then compares the destination's raw bytes with the source. On main: 262144 of 262144 bytes still unwritten (5 of 5 runs). With the change: 0 (5 of 5 runs).

  • Windows 11, i9-10850K, NVIDIA RTX 3080 (driver 610.74), WinLibs GCC 15.2.0 r7, Release: ctest -R agc_ 589/589 passed (3 skipped for device features); tools/check_conventions.py --base origin/main and tools/check_test_registration.py pass.
  • HOT WHEELS Infinite Rush (PPSA22408), same machine: on main 0afa6a6 the crash above in every run (3 of 3). With the change: alive in 3 runs of 420/420/300 s, title screen at 4.9-5.3 FPS (4.5-4.9 with APS5_NO_COPY_ALIAS=1, 2.0-2.9 with APS5_STORE_AT_LABEL=1, both of which also avoid the crash); "PRESS ANY BUTTON", then the first-run settings screen. Gameplay not reached yet (the title runs at ~5 FPS).

Related: #6249 (alias copies of surfaces with padding lose bytes outside the image) changes AliasCopy's eligibility; this PR does not touch it and the two apply independently. #5480 moves alias selection into PlanAlias; the store here belongs after whichever alias path succeeds.

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; no silent stubs added
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source (none added)
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: HOT WHEELS Infinite Rush (PPSA22408) past its title screen
  • GPUs tested: NVIDIA GeForce RTX 3080, driver 610.74, Windows 11
  • AI-assisted: yes (Claude Code). I am responsible for the change.

🤖 Generated with Claude Code

…r labels

A buffer copy whose source is exactly a live storage image becomes a device
copy between images (AliasCopy). Its results stayed pending in the destination
image and reached guest memory only at a later flush, so a guest that waits
for a label recorded after the copy and reads the destination on the CPU (a
readback) read the bytes from before the copy. Store the destination's results
in the same batch, behind the image copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

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 1395 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 2434 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 4040 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 4072 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 4472 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 4650 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 4891 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 5021 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 5431 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 5441 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 5499 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 5925 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6177 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6360 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6414 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6454 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6615 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6683 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6763 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6786 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6788 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6947 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 7002 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 7201 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 7332 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 7390 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 7642 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp

@coltken95

Copy link
Copy Markdown

Tested on Linux x86-64 (Release build, lavapipe / llvmpipe LLVM 20.1.2), with 21cdb8f merged into main ce991f1:

  • With the PR: agc_driver_alias_copy_label passes.
  • With main's sources: it fails with AGC graphics: a CPU read after the label that followed the image copy found 262144 of 262144 destination bytes still unwritten.

AI-assisted: yes (Claude Code)

@mrfelek

mrfelek commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Hot Wheels (PPSA22408) at head 21cdb8f05: segfault about three minutes in, not fixed by this PR

Setup

  • Head: 21cdb8f05, clean worktree, Release, with its libs targets built
  • Title: Hot Wheels Infinite Rush (PPSA22408), relinked with this head's relinker
  • Host: Linux, Ryzen 9 5950X, RTX 3080 Ti, NVIDIA 615.71.09

Result

  • The title reaches the boot screen, loads, and presents frames for about three minutes.
  • Then it segfaults at the same instruction in every run (three runs, one of them under the debugger): app.elf+0x5641523, a null read right after a virtual call.
  • We did not run main without this PR, so we cannot say yet whether the crash is new.

What the debugger showed

  • The call returns zero. The method it calls looks a key up in a table on the object, and the table is empty at that point (count 0, capacity 0, pointer null).
  • The object's constructor creates the table empty, and nothing fills it before the first lookup on this run. We did not find the code that is supposed to fill it.
  • We could not identify the game system involved: the binary has no symbols, and the unwound stacks are unreliable beyond the first frame.

Not established

  • Whether the empty table is the cause or only a symptom.
  • Whether this PR changes it (we have no main run to compare).

@coltken95 you reported the Hot Wheels crash earlier. If you have symbols or a main run for this title, that would show whether the crash is new. We will keep the debugger captures if they are useful.

AI-assisted; the build and runs are from my machine, and the analysis is from disassembly, not a symbolized build.

@ps54all

ps54all commented Oct 16, 2026

Copy link
Copy Markdown
Author

Thanks for the runs. On Windows (RTX 3080, NVIDIA 610.74) the crash this PR addresses looks different: without the change the title dies at 115-295 s on the RenderThread with a wild read in a loop that walks UE's GPUMessage readback stream (a record length read back as float bits, e.g. 0xc43fd1d5, sends the index negative). With the change, three runs stayed alive for 420, 420 and 300 s with no fault and reached the first-run settings screen at about 5 FPS.

Your fault (a null read after a virtual call that returns zero from an empty lookup table) does not match that signature, so it is probably a separate stop further on; it would help to know whether main without this PR dies earlier (at the readback crash) or at the same +0x5641523. If main dies earlier, that also confirms the PR on Linux. I have not run the title on Linux.

AI-assisted (Claude Code); numbers from runs on my machine.

@mrfelek

mrfelek commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Hot Wheels (PPSA22408): correction, the crash we reported here was caused by our setup

The null read at app.elf+0x5641523 (Linux) and app.exe+0x5651523 (Windows) came from our runtime, not from AnyPS5 or this PR. We had replaced the title's EOS SDK (EOSSDK-PS5-Shipping) with a stub that returns 0 from every call. The title then never registers its MILESTONEEOS online subsystem, and later looks it up and dereferences null.

Relinking the title's own prx/ modules (eossdk-ps5-shipping.prx and the kitt modules) removes that crash. The title then reaches the shader abort from #6117, which #6160 fixes (results on #6160).

For this PR: no regression on Linux, and the crash above happens with or without it.

AI-assisted: this comment was drafted with the help of Claude Code and checked against the logs by us.

@savvaniss

Copy link
Copy Markdown
Contributor

Intel and AMD Windows data: the new test passes on both GPUs with this change and fails on both without it, as on the RTX 3080.

Windows 11 Pro 26300, WinLibs GCC 15.2.0 r7, Release, every execution test built, ANYPS5_NO_SHADER_CACHE=1, serial CTest. Intel UHD Graphics 770 (8086:a780), driver 32.0.101.7088. AMD Radeon RX 9070 XT (1002:7550), driver 32.0.31041.1004, with VkCache emptied first. "Without" means this PR's test files on main c9e6c45de, with none of its other changes.

UHD 770 RX 9070 XT
This head 21cdb8f05 merged into main 673f7406c: agc_driver_alias_copy_label, ×3 pass pass
Without, ×3 fails: a CPU read after the label that followed the image copy found 262144 of 262144 destination bytes still unwritten fails, same

Full CTest (--parallel 4) on main c9e6c45de with seven open GPU PRs merged (#6258, #6657, #6324, #6246, #6174, #6360, #6598): UHD 770 1381 of 1389, RX 9070 XT 1383 of 1389. main alone passes 1378 of 1385 and 1381 of 1385. The merge adds only two failures: agc_buffer_pool (#6174, both GPUs) and agc_driver_indirect_dispatch_limit (#6360, RX 9070 XT).

AI-assisted: yes (Claude Code)

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

Labels

agc libSceAgc* unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants