Skip to content

fix(libSceAgcDriver): mask WAIT_REG_MEM addresses and elide non-overlapping pipeline drains - #6660

Merged
adriadam10 merged 3 commits into
boykopovar:mainfrom
al3w0f205:fix/agcdriver-wait-reg-mem-masking-and-pipeline-drain
Oct 19, 2026
Merged

adriadam10 merged 3 commits into
boykopovar:mainfrom
al3w0f205:fix/agcdriver-wait-reg-mem-masking-and-pipeline-drain

Conversation

@al3w0f205

Copy link
Copy Markdown
Contributor

What

This pull request resolves a critical abort and removes heavy pipeline drains during WAIT_REG_MEM command processing in libSceAgcDriver:

  1. CP WAIT_REG_MEM address masking: Command Processor hardware ignores unaligned address bits (masking with ~3ull for 32-bit and ~7ull for 64-bit comparisons). The previous strict alignment assertion caused SIGABRT exceptions in titles such as Overcooked! All You Can Eat during save slot initialization.
  2. Non-overlapping drain elision: When a WAIT_REG_MEM condition is satisfied and the polled memory range does not overlap with any pending draw writes in queue 0, avoid calling DrawPipeline::Drain(), preventing redundant command buffer splits and pipeline stalls.

Tested

  • Windows 11 Home 24H2, Overcooked! All You Can Eat (PPSA01528): verified save slot screen transitions cleanly without aborting.
  • libSceAgcDriver tests suite passed.

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
  • Unblocks: none
  • GPUs tested: NVIDIA GeForce RTX 4070 Laptop GPU, driver 572.16, Windows 11
  • AI-assisted: yes

@MarcZX10

Copy link
Copy Markdown
Contributor

Could you add focused regression cases for the two behaviors changed here? I don't see test files in this PR's diff. In particular, a PM4 case for misaligned 32-bit and 64-bit WAIT_REG_MEM addresses would verify the correct low bits are masked consistently through validation, decoding, and execution. A queue test could also verify that an already-satisfied wait on a non-overlapping range avoids the pipeline drain, while an overlapping pending write still takes the drain/wait path. The Overcooked run is useful evidence for the reported 32-bit abort, but does not cover the 64-bit form or the drain-elision branch.

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

The new masking uses two different alignments for one packet shape: WAIT_REG_MEM_64 (0x93) with a zero high mask dword (packet[7] == 0).

  • WaitMemory.cpp and the PacketExecution.cpp gate mask with awaitedBytes == 8 ? ~7 : ~3. WaitAwaitedBytes returns 4 for that packet, so they keep the address 4-aligned.
  • Pm4.cpp WaitSatisfied, WaitSatisfiedUnchecked and DecodeWait mask with wide ? ~7 : ~3, so they use 8 bytes for the same packet.

When the address is 4 mod 8, waitMemory checks the range, looks up labels and reports timeouts at A. Its satisfaction checks (WaitSatisfiedUnchecked, lines 137 and 334 on the head) read the qword at A - 4. With the high mask dword 0, that compares the dword at A - 4, not the dword at A. Before this PR, Validate rejected that address, so this combination could not happen.

I compiled a standalone copy of the head's expressions (WaitAwaitedBytes, waitCompares, WaitSatisfiedUnchecked and the WaitMemory.cpp mask) with Apple clang, -std=c++20, on arm64. Packet: 0x93, compare ==, reference 1, mask 0x00000000ffffffff, address at offset 4 of an 8-aligned buffer.

addr%8=4 awaitedBytes=4 WaitMemory.awaited=+0 Pm4.source=+-4
label written at packet address -> WaitSatisfiedUnchecked=0
after writing the dword 4 bytes below -> 1

So a title that writes the label at the packet's address keeps waiting, and a write to the neighbouring dword releases the wait. One mask in both places would remove the mismatch: either wide in WaitMemory.cpp and PacketExecution.cpp, or WaitAwaitedBytes(packet) == 8 in Pm4.cpp. A PM4 case for this exact shape would also cover the "masked consistently" test requested above. Separately, LabelHistory.cpp:53 still records the unmasked address for WAIT_REG_MEM. That only affects diagnostics.

Static review against main 5d9dcc4, head 97e5318. No full build and no GPU run.

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

@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 6252 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp L376-377

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 4448 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
PR 5021 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp
PR 5115 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
PR 5925 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
PR 6185 core/libs/prx/libSceAgcDriver/tests/Pm4.cpp
PR 6186 core/libs/prx/libSceAgcDriver/tests/Pm4.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
PR 6292 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp
PR 6501 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
PR 6549 core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
core/libs/prx/libSceAgcDriver/tests/Pm4.cpp
PR 6615 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp
PR 6946 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
PR 6947 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
PR 6963 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
PR 7008 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
core/libs/prx/libSceAgcDriver/tests/Pm4.cpp
PR 7340 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
PR 7642 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
PR 7659 core/libs/prx/libSceAgcDriver/tests/Pm4.cpp
PR 7683 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
core/libs/prx/libSceAgcDriver/tests/Pm4.cpp

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The follow-up review found a concrete mismatch in the 64-bit WAIT_REG_MEM path: with a zero high mask dword, the execution/validation path treats the wait as 4-byte aligned while the PM4 satisfaction/decode path aligns it as 8-byte. For an address that is 4 mod 8, those paths inspect different dwords, so a write at the packet address may not release the wait. Could you reconcile the alignment rule across these three files and add a focused PM4 regression for opcode 0x93 with packet[7] == 0 and an address 4 mod 8? The separate queue drain-elision coverage I asked about earlier remains useful too.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I found one concrete mismatch in the 64-bit WAIT_REG_MEM path: with a zero high mask dword, execution/validation treats the wait as 4-byte aligned while PM4 satisfaction/decode aligns it as 8-byte. For an address that is 4 mod 8, those paths inspect different dwords, so a write at the packet address may not release the wait. Please reconcile the alignment rule across these files and add a focused regression for opcode 0x93 with packet[7] == 0 and an address 4 mod 8. The queue drain-elision coverage from my earlier comment remains useful too.

@al3w0f205
al3w0f205 force-pushed the fix/agcdriver-wait-reg-mem-masking-and-pipeline-drain branch from 97e5318 to 6ab4e09 Compare October 16, 2026 04:18
@al3w0f205

Copy link
Copy Markdown
Contributor Author

Added focused regression test coverage in core/libs/prx/libSceAgcDriver/tests/Pm4.cpp:

  1. Misaligned 32-bit (WAIT_REG_MEM, masking to & ~3ull) and 64-bit (WAIT_REG_MEM_64, masking to & ~7ull) addresses across all low-bit offset variations, verifying consistency across Validate, DecodeWait, WaitSatisfied, and WaitSatisfiedUnchecked.
  2. Unification of the mask to wide ? ~7ull : ~3ull across WaitMemory.cpp, PacketExecution.cpp, and LabelHistory.cpp, resolving the alignment mismatch on 64-bit waits with zero high mask dword and covering that shape in the test.
  3. Queue test verifying that an already-satisfied wait on a non-overlapping range avoids draining the pipeline while the pipeline committer is busy, whereas an overlapping pending write takes the drain path before completion.

All checks passed in agc_driver_pm4_tests.

@MarcZX10

Copy link
Copy Markdown
Contributor

Thanks for adding the focused cases. I checked the updated diff: the 64-bit opcode now uses 8-byte address masking consistently in validation/execution, decoding and satisfaction checks, including the zero-high-mask case; the queue test also exercises both non-overlap elision and overlap draining. This addresses the gaps I raised. I have not run the full PR build.

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

Re-checked at head 6ab4e09fd against my earlier note on 97e531811. The mask is now wide ? ~7 : ~3 in all three places, so the mismatch I reported is gone. I also ran the tests, and the new zero-high-mask case does not catch that mismatch.

  • Machine: Ubuntu 26.04, x86_64 (i7-8700), GCC 15.2.0, Release. Base main 5d9dcc42d.
  • agc_driver_pm4_tests does not need a GPU. I ran it with no Vulkan driver available (VK_DRIVER_FILES pointing to a missing file). Under strace it opens no libvulkan, /dev/nvidia* or /dev/dri file, and it passes.

Results:

  • Head 6ab4e09fd: agc_driver_pm4, agc_driver_pm4_failure and agc_driver_pm4_failure_inline pass.
  • main with only the new tests/Pm4.cpp: fails with misaligned WAIT_REG_MEM address, as expected.
  • Old head 97e531811 with the new tests/Pm4.cpp: passes. testWaitRegMemMasking, zero-high-mask case included, only calls Pm4::Validate, DecodeWait, WaitSatisfied and WaitSatisfiedUnchecked. Those helpers already masked with wide ? ~7 : ~3 at 97e531811. The mismatch was in WaitMemory.cpp and the PacketExecution.cpp elision gate, and no new case submits a 0x93 packet with packet[7] == 0 at an address that is 4 mod 8. So the test would not catch a regression back to awaitedBytes == 8 ? ~7 : ~3 there.

This submission-level case does catch it. It uses the same pattern as testWaitRegMemPipelineDrain: a pending pipeline item covers the dword the wait compares, and the wait must not skip the drain.

void testWaitRegMem64ZeroHighMaskDrain() {
    alignas(8) static std::uint32_t qword[2] = {42, 0};
    const auto base = reinterpret_cast<std::uintptr_t>(&qword[0]);
    const auto packetAddr = base + 4;
    std::atomic<bool> hold{true};
    std::atomic<bool> started{false};
    AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([&] {
        started.store(true);
        while (hold.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }, { {base, base + 4} });
    while (!started.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
    auto words = makePacket(0x93, {0x13, static_cast<std::uint32_t>(packetAddr), static_cast<std::uint32_t>(packetAddr >> 32u), 42, 0, 0xffffffffu, 0, 0});
    submitWords(words, 0);
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    const auto outstanding = AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding();
    hold.store(false);
    AgcDriverWaitIdle_nid_postfix();
    check(outstanding == 1, "WAIT_REG_MEM_64 with zero high mask at 4 mod 8 skipped the drain of a pending write to the dword it compares");
}

I called it after testWaitRegMemPipelineDrain() and ran it 3 times on each head:

Head Result
6ab4e09fd 3/3 pass
97e531811 3/3 fail: the gate checks [A, A+4) for overlap, finds none, and then opens on the qword at A-4, whose low dword is the one with the pending write

Feel free to take it as is if it's useful.

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

@github-actions github-actions Bot added agc libSceAgc* needs rebase Conflicts with main, rebase needed labels Oct 16, 2026
@al3w0f205
al3w0f205 force-pushed the fix/agcdriver-wait-reg-mem-masking-and-pipeline-drain branch from 6ab4e09 to ffeae68 Compare October 16, 2026 13:10
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 16, 2026
@JAB-dev

JAB-dev commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Besiege (PPSA20313), Windows, RTX 4070 SUPER, Ancient Grounds sandbox (about 1000 draws per frame), head ffeae68 on top of current main plus open perf PRs: 22.3 -> 24.4 FPS with this PR, pipeline drains per 10 s 11,000 -> 1,700. A 10 minute run in the level had no errors and the frame looks the same.

@JAB-dev

JAB-dev commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Correction to my earlier numbers here: stacked with #6082 this change shows stale frames in Besiege (PPSA20313, Windows, RTX 4070 SUPER). In the cube-selection menu with the window unfocused the full-screen background is occasionally drawn with a diagonal split (one triangle stale). The cause is the new branch in Driver::execute that skips a wait satisfied at entry (!Queue0().Overlaps(...) && Pm4::WaitSatisfiedUnchecked(packet)): waitMemory bumps the collect epoch at entry even when the wait is already satisfied, and #6082's UnwrittenDrawSnapshot reuse relies on that bump to notice a CPU write to a small buffer. Adding bumpEpoch(&EpochBumps::waits); before the continue in that branch removes it: 90 s of the menu unfocused, 5,000 captures, no changed frames (before: about 1,600 spike frames); frame rate unchanged. The same capture is also clean with APS5_PACKET_EPOCH=1 and with APS5_DRAW_UPLOAD_MAX=0.

@wachi35676

Copy link
Copy Markdown
Contributor

Two titles hit the 64-bit form of this, PS5NES (PPSA99204, v1.0.0.9) and PS5N64 (PPSA99206, v.1.0.0.7). Both are converted with --trap-sdk-syscall-stubs. Setup: Windows 11, RTX 5070 Ti (NVIDIA 617.42), WinLibs GCC 15.2.0, Release. Titles are the authors' GitHub releases, unwrapped with tools/unwrap_self.py and converted with --windows. "main" is 922dc8999. For "this PR", the PR was cherry-picked onto 11acfdd84 and only the PR's own .prx was swapped into the 922dc8999 library set.

On main. PS5NES aborts in 3 of 3 runs (17–21 s) and PS5N64 in 2 of 3 with AGC driver: WAIT_REG_MEM_64 at DWORD 0: misaligned WAIT_REG_MEM address. The rejected packet (APS5_DUMP_REJECTED) is:

c0079300 06000113 00dfebfc 00000002 000003d1 00000000 ffffffff 00000000 00000040

That is a 64-bit equal wait on 0x2_00dfebfc, which is only 4-byte aligned, with mask 0x00000000_ffffffff. It comes from the titles' PS5_Vulkan driver: ps5vk_marker_wait_words waits on the queue's marker, the last word of its buffer. The packet is only emitted when the previous submission hasn't finished, so it is timing dependent.

With this PR (ffeae6826). Masking to ~7 polls 0x200dfebf8, so the value the title wrote lands in the high dword, which the mask drops:

[gpu] queue 0x0 WAIT_REG_MEM_64 at 0x200dfebf8 timed out after 1000ms (function 3 ref 0x80 mask 0xffffffff value 0x8000000000)

PS5NES then ends with VideoOut: cannot unregister a buffer with a pending flip in 1 of 3 runs, and PS5N64 likewise in 1 of 3.

Experiment, not proposed. Accepting a 4-byte-aligned 64-bit address and reading at the exact address let 3 of 3 runs of each title reach the 70 s limit, with no timeouts and no rejected packets. AMD PAL's PM4_MEC_WAIT_REG_MEM64 marks address bits [2:0] reserved, so what the console's CP does with this packet is the open question. For these two titles, masking to ~7 turns the abort into a wait on the wrong qword.

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

@al3w0f205
al3w0f205 force-pushed the fix/agcdriver-wait-reg-mem-masking-and-pipeline-drain branch from ffeae68 to cdedb1e Compare October 18, 2026 01:01
@al3w0f205

Copy link
Copy Markdown
Contributor Author

Updated with fixes for both reported cases and rebased onto current main:

  1. When a wait is satisfied at entry on non-overlapping ranges, added �umpEpoch(&EpochBumps::waits); before continuing so downstream buffer reuse checks observe CPU writes.
  2. For 64-bit waits (WAIT_REG_MEM_64) where only the lower 32 bits are awaited (e.g. zero high mask dword packet[7] == 0), alignment masking is relaxed from 8 bytes to 4 bytes (~3u), and memory read / compare reads the 32-bit value directly at the target address.
  3. Expanded unit tests in core/libs/prx/libSceAgcDriver/tests/Pm4.cpp to cover 4-byte-aligned addresses across all byte offsets for zero-high-mask 64-bit waits.

Verified with ctest -R agc_driver_pm4 passing 100% and ools/check_conventions.py passing cleanly.

@wachi35676

Copy link
Copy Markdown
Contributor

Re-tested head cdedb1e87 with PS5NES (PPSA99204, v1.0.0.9) and PS5N64 (PPSA99206, v.1.0.0.7), converted with --windows --trap-sdk-syscall-stubs. Before is main 0bb238b4a; after is the same build with this PR cherry-picked (clean), so both use one base. 70 s per run, 3 runs each, no input. Windows 11, RTX 5070 Ti (617.42), WinLibs GCC 15.2.0.

Build Title Aborts (misaligned WAIT_REG_MEM_64) _64 timed out VideoOut pending-flip Rejected submissions
main PS5NES 2 of 3 (at 52 s and 10 s) 0 0 2
main PS5N64 1 of 3 (at 9 s) 0 0 1
this PR PS5NES 0 of 3; all ran the full 70 s 0 0 0
this PR PS5N64 0 of 3; all ran the full 70 s 0 0 0
  • The title only sends this wait while its previous submission is still running, which is why some runs survive on main.
  • The previous head's timed out ... at 0x200dfebf8 no longer appears.
  • ctest -R agc_driver on the PR build: 516 of 516 passed, 4 skipped.

Not checked on Linux or on a console. These runs show the new handling matches what the titles expect. They don't measure what the CP does with a 4-byte-aligned 64-bit wait whose high mask is 0; PAL marks address bits [2:0] reserved.

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

@MarcZX10

Copy link
Copy Markdown
Contributor

Thanks for the exact-base title comparison. I verified the PR is still at the tested head cdedb1e87 and is clean/mergeable. The 3/3 PS5NES and PS5N64 runs on the same main base confirm the misaligned WAIT_REG_MEM_64 abort is gone in those runs; the hosted build/test jobs remain skipped by path filters, so I’m treating the hardware report as the runtime evidence.

@al3w0f205
al3w0f205 force-pushed the fix/agcdriver-wait-reg-mem-masking-and-pipeline-drain branch from cdedb1e to 13fb5a2 Compare October 18, 2026 01:58
@al3w0f205

Copy link
Copy Markdown
Contributor Author

Added the submission-level regression test testWaitRegMem64ZeroHighMaskDrain() in core/libs/prx/libSceAgcDriver/tests/Pm4.cpp covering opcode 0x93 with packet[7] == 0 at 4 mod 8 against a pending pipeline write. Verified locally with ctest -R agc_driver_pm4 passing cleanly and conventions passing 100%.

@MarcZX10

Copy link
Copy Markdown
Contributor

That regression assertion covers the specific opcode/mask/alignment combination from the title report, and the focused PM4 test plus conventions pass on the updated head. Thanks for adding it. The earlier 3/3 PS5NES and PS5N64 comparison remains useful title evidence; both reports are now recorded on this PR.

@adriadam10
adriadam10 force-pushed the fix/agcdriver-wait-reg-mem-masking-and-pipeline-drain branch from 13fb5a2 to d0bf343 Compare October 19, 2026 09:52
@adriadam10
adriadam10 merged commit 12e42b2 into boykopovar:main Oct 19, 2026
10 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

The full Build failed on agc_driver_pm4: testWaitRegMemPipelineDrain asserted Queue0().Outstanding() == 0 right after AgcDriverWaitIdle, but with the drain elided WaitIdle no longer waits for queue 0, so the check raced with the draw pipeline finishing. I pushed two test-only commits that poll the draw pipeline to idle (bounded at 5 s) before both assertions, rebased on main, and reran the AgcDriver/Agc tests plus a repeat run for flakes; all green. Logic unchanged; merged.

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

Labels

agc libSceAgc*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants