Skip to content

perf(agc): read DCC keys behind a pattern fill kernel's fast clear without a wait - #5497

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
gjonn:perf/agc-pattern-fill-dcc-keys
Oct 19, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
gjonn:perf/agc-pattern-fill-dcc-keys

Conversation

@gjonn

@gjonn gjonn commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

PPSA01317 fast-clears its render-target DCC keys every frame with a 64-thread compute kernel (23 code words) that stores pattern[i % 4], the pattern in the user SGPRs s4..s7, to dword i of the stride-4 V# in s0..s3 for every i < min(groups * 64, s8, num_records), with the group id in s9:

bfa00001 d7460004 04010c09 7e000204 7e020205 7e040206 7e060207 7da80808
bf88000d 360a0883 be84047e be86047e 7e100505 7da40a08 befc0308 7e0c8700
8a867e06 befe0406 bf85fff9 befe0404 e0102000 80000604 bf810000

On current main it runs as a plain dispatch, so its store is a pending write of unknown value. The next key read of those targets (currentDccKeys / WaitForKeyWriters in Graphics/src/DccMetadata.cpp) finds PendingWriteOverlaps true and PendingStoreKeys empty, and calls Recorder::SyncThrough, waiting for the GPU while holding the GPU mutex (#3505).

This PR leaves the dispatch as it is. MatchSgprPatternFill recognises the kernel (exact code, 9 user SGPRs with the group id enabled, 64x1x1 threads, one group row, no partial or indirect dispatch, a 32-bit format, stride 4, not swizzled, no add-tid, index-bounded OOB mode). When the pattern is one repeated DCC key code (KeysFillMemo from #4603, taken as for its memory-value fill kernel), Driver::dispatch calls Graphics::NoteKeysFillOnGpu under the GPU mutex right after the dispatch is recorded. It does this only if the dispatch's own write note is the newest note and covers exactly the filled range. PendingStoreKeys answers from that note only while it is still the newest over the range, so any later writer makes the read wait as before. The kernel needs no 16-byte multiple or alignment and is not taken as a buffer fill.

Related:

This PR covers the 23-word kernel that takes its pattern from user SGPRs.

Tested

Windows 11 Pro, MinGW GCC, Release, NVIDIA GeForce RTX 5090 Laptop GPU (driver 616.92).

New test agc_driver_sgpr_pattern_fill_keys (tests/execution/SgprPatternFillKeys.cpp):

  • Matcher cases.
  • Through Submit:
    • a pending fill of each key code reads as its keys without a wait;
    • a non-key byte or a mixed pattern still waits;
    • a later writer over half of a pending 0001 fill still waits and reads mixed;
    • a fill of half the keys answers only that half.
  • The stored bytes are checked after each case.

The test fails on main (a pending key code fill by the kernel: the keys read 0000 and the read waited for the GPU) and passes with this change, 15 of 15 repeated runs.

These also pass:

  • dword_pattern_fill, zero_dword_fill, pattern_buffer_fill
  • dcc_keys_first_write, fill_cover_keys
  • cmask_fast_clear, depth_fast_clear, depth_fast_clear_submit
  • depth_storage_dispatch, gated_dispatch_capture, oversized_dispatch
  • store_over_untracked, store_idle_results, compute_user_data_calls, resource_cache_storage_clear
  • the recorder storage and unit-shadow cases

Running agc_driver_recorder_tests without arguments fails with (9) a scan under a pending write was kept on unmodified main as well. tools/check_conventions.py passes.

PPSA01317: the same note in a local tree raised race heavy-phase FPS from 1.395 to 1.833 (+31%, 3 of 3 A/B blocks) and halved the GPU waits made under the GPU mutex. The image was unchanged. That measurement was taken on a local branch, not on this exact main commit.

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 (performance; PPSA01317 Gran Turismo 7 (PPSA01317) #4697)
  • GPUs tested: NVIDIA GeForce RTX 5090 Laptop GPU, driver 616.92, Windows 11 Pro
  • AI-assisted: yes

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Two things before this can go in:

  • perf(agc): take the memory-value dword fill kernel as a buffer fill #4603 is about to merge and adds IsByteFill and KeysFillMemo over the same key switch in BufferFill.cpp. Please rebase on it once it's in and use those instead of FilledKeys (BufferFill.cpp:101).
  • The PPSA01317 numbers come from a local branch. Please post the summary lines of main and of this change from the same scene, with the traces, as the Performance report form describes.

@gjonn

gjonn commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main 63fb0cd, which has #4603 (force-push, one commit, e095ee4).

  • FilledKeys is gone. Driver::dispatch takes the key code from KeysFillMemo(pattern, false, true), the same way perf(agc): take the memory-value dword fill kernel as a buffer fill #4603's memory-value fill kernel does: a recognised fill kernel's keys are noted whether or not the range is a storage image's keys. That is what this PR did before.
  • The FilledKeys unit cases are dropped, because perf(agc): take the memory-value dword fill kernel as a buffer fill #4603's MemoryValueFill.cpp already covers the key codes. The end-to-end cases through Submit stay unchanged.
  • Windows, MinGW GCC, Release, RTX 5090 Laptop: agc_driver_sgpr_pattern_fill_keys passes 3 of 3 runs. memory_value_fill, dword_pattern_fill, dcc_keys_first_write and fill_cover_keys also pass, and tools/check_conventions.py passes.

The PPSA01317 before/after from the same scene is still to come. On stock main the title does not reach the race, so the comparison will be one local tree with and without this commit, run in alternating blocks with traces. I'll post the summary lines here and say exactly what the tree contains.

AI-assisted: yes

@github-actions

github-actions Bot commented Oct 15, 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 6804 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp L12
PR 7600 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp L12

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 5925 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 6292 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp
PR 6615 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp
PR 7201 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp
PR 7332 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 7446 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 7642 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Still open from 10-13: the PPSA01317 summary lines of main and of this change from the same scene, with the traces, in alternating blocks as you described. The branch also conflicts with main again, so please rebase. If you can't run the title now, say so and I'll leave it waiting.

…thout a wait

A 64-thread compute kernel (23 dwords) stores pattern[i % 4], the pattern
in s4..s7, to dword i of a 4-byte-stride buffer for every i below
min(launched threads, s8, num_records), with the group id in s9.
PPSA01317 fast-clears its render-target DCC keys with it every frame. As a
plain dispatch its writes had no known value, so every key read of those
targets waited for the GPU under the GPU mutex.

The dispatch still runs as before. When it stores one repeated key code,
its keys are noted in the key-store memo right after the dispatch is
recorded, as a buffer fill of keys already is; the memo answers only while
the dispatch's own write note is the newest over the range, so any later
writer still makes the read wait. The key codes come from KeysFillMemo, as
for the memory-value fill kernel.

AI-assisted: yes
@gjonn
gjonn force-pushed the perf/agc-pattern-fill-dcc-keys branch from e095ee4 to 448ccee Compare October 17, 2026 18:41
@gjonn

gjonn commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main f219248 (force-push, still one commit, 448ccee). The only conflict was an include list in Dispatch.cpp; the change itself is unchanged. The PR test and the other fill and key tests pass, and tools/check_conventions.py passes.

On the measurement: I can't give you main against this change from the same scene right now. On main the title still stops before the title screen, so it never gets to the race where this kernel runs every frame. The blockers are tracked in discussion #4697 and in #7137, #7207, #5958 and #7175, with more to come.

The only numbers I have are from our local tree, which isn't main, so I won't count them as the comparison you asked for. Once main reaches the race, I'll post the summary lines of main and of this change from the same scene, run in alternating blocks, with the traces.

Do you want to leave this waiting until then?

AI-assisted: yes

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
Repository owner deleted a comment from cozidozi Oct 17, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

@gjonn thanks for the rebase, and for being straight about the numbers.

Yes, let's leave this waiting. On main, PPSA01317 still stops before the title screen, so it never reaches the race where this fill kernel runs every frame, and a main-vs-PR comparison from the same scene isn't possible yet. We're parking this PR until main can get there (the blockers in #4697, #7137, #7207, #5958 and #7175).

Once main reaches the race, please post the summary lines of main and of this change from the same scene, in alternating blocks with the traces, as you described, and we'll pick it back up from there. A rebase at that point is fine; no need to keep it current in the meantime.

@github-actions github-actions Bot added perf Performance change perf numbers missing perf pull request without a before and after summary line unblocks title Names a title ID on the Unblocks line labels Oct 18, 2026
@oneandonlydean
oneandonlydean merged commit 030ba40 into boykopovar:main Oct 19, 2026
8 checks passed
@Amoenus

Amoenus commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Independent fixture check on literal main bf2c63f853, Windows11 Enterprise26200, GCC15.2 Release. Runtime libraries/relinker and agc_driver_sgpr_pattern_fill_keys_tests were rebuilt together.

agc_driver_sgpr_pattern_fill_keys intermittently stops at:

AGC graphics: the submitted dispatch never became a pending write over the keys

Three fresh processes per GPU with the normal implicit host layers plus explicit Khronos validation: RTX5080 2 pass /1 fail, AMD Radeon(TM) Graphics 2 pass /1 fail. Repeating three fresh processes per GPU with VK_LOADER_LAYERS_DISABLE=~implicit~ gives 3/3 pass on each. This small sample does not establish implicit-layer causality or a fix. Each fixture's two device selections and loaded Khronos layer were verified; zero VUID/errors/warnings or CTest timeouts in all12 processes. Shader cache and pipeline journal were disabled throughout; driver caches were not isolated.

Reproduce serially with ANYPS5_REQUIRE_VULKAN=1, ANYPS5_NO_SHADER_CACHE=1, APS5_NO_PIPELINE_JOURNAL=1, VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, the installed SDK layer path, and each requested ANYPS5_GPU:

ctest --test-dir build -V -j1 --timeout 30 --no-tests=error -R '^agc_driver_sgpr_pattern_fill_keys$'

Invoke it in three separate processes per device. RTX5080 10de:2c02, driver610.62 /32.0.16.1062; AMD 1002:13c0, driver32.0.21036.18. This is a current-main fixture observation, not a claim that this PR introduced it, a title regression or a performance comparison. The failing observation needs investigation before treating the fixture as a reliable integration gate. AI-assisted: yes (Codex).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Hi @gjonn, agc_driver_sgpr_pattern_fill_keys from #5497 has failed 4 times on Linux CI in the last day on unrelated PRs, each time at ~10.4 s with "the submitted dispatch never became a pending write over the keys" (CI 39341103236, 39351840152, 39352635933, 39353294932; reruns pass). It looks like a race in the test: ReadAfterWrite() waits for Recorder::NewestWriteNote over the keys, but that only scans the open and in-flight batches. If the dispatch's batch is submitted, completes and is reaped before the polling thread gets a turn (llvmpipe finishes the tiny fill almost immediately and ctest runs 4 tests in parallel), the note is gone and the loop waits out its 10 s deadline. Could you make the test hold the batch in flight until it has read the keys, or resubmit when the batch retired unseen? Naming the failing case in that message would also help.

@simonbbby

Copy link
Copy Markdown
Contributor

Native Linux reproduction of the retired-before-observation case in Dean's request, on pinned main 11ee778434dacf31f2ce0ecff8229e0839e36b34.

The unmodified registered test passed 10/10 serial lavapipe runs. Adding only a 100 ms delay at entry to ReadAfterWrite reproduced the original timeout 3/3. Immediately after that delay, the probe printed:

last=0 newest=0 idle=1 completed=1

The batch was already retired; there was no pending write note left for the loop to find. This is a controlled scheduling reproduction, not a measured natural CI failure rate.

I tested the resubmission option below. When no new note is observed for 10 ms, it first drains the previously submitted work, then resubmits the same case's idempotent fill. Draining before resubmission prevents queued duplicates from leaking into the next case. The existing 10-second deadline, key classification, pending/waited expectations and stored-dword assertions remain; timeout messages now identify the case.

  • Normal candidate: 20/20 fresh processes per driver on RTX 3090, RADV gfx1036 and lavapipe, 60/60 total.
  • With the same 100 ms reader delay: 3/3 per driver, 9/9 total. A temporary print confirmed all 12 reads retried in each process (108 retries), with the existing assertions passing.
  • Disable only the retry branch, retaining that delay and the new case labels: 3/3 lavapipe failures, now naming uniform key 0 in the same timeout.
  • After removing the probes and restoring the exact candidate, 3/3 per driver pass again. Conventions and git diff --check pass.

Ubuntu 24.04, kernel 7.0.0-38, Ryzen 9950X, GCC 13.3, Release; NVIDIA 595.99.02, Mesa 25.2.8. ICD restricted separately and actual devices verified; required Vulkan, shader cache and pipeline journal disabled. No Khronos validation layer is installed. Windows/macOS, loaded validation and parallel-CI failure rates are not verified. No production files are changed. Raw logs are retained.

Test-only candidate against that main pin
diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp
index e35849eb7..1d495d061 100644
--- a/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp
+++ b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp
@@ -95,7 +95,12 @@ void MatchTests() {
     Require(!MatchSgprPatternFill(packet, Code, data, noGroup), "a dispatch without the group id matched");
 }
 
+std::vector<std::uint32_t> submittedData;
+std::uint32_t submittedGroups = 0;
+
 void Submit(const std::vector<std::uint32_t>& data, std::uint32_t groups) {
+    submittedData = data;
+    submittedGroups = groups;
     std::vector<std::uint32_t> words;
     const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) {
         words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u));
@@ -122,7 +127,10 @@ struct KeyRead {
 
 std::uint64_t lastNote = 0;
 
-KeyRead ReadAfterWrite(std::uint64_t surfaceBytes = SurfaceBytes) {
+KeyRead ReadAfterWrite(const std::string& what, std::uint64_t surfaceBytes = SurfaceBytes) {
+    const auto data = submittedData;
+    const auto groups = submittedGroups;
+    auto retryAt = std::chrono::steady_clock::now() + std::chrono::milliseconds(10);
     const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
     for (;;) {
         {
@@ -134,7 +142,13 @@ KeyRead ReadAfterWrite(std::uint64_t surfaceBytes = SurfaceBytes) {
                 return {keys, !recorder->PendingWriteOverlaps(KeysAddress(), KeyBytes)};
             }
         }
-        Require(std::chrono::steady_clock::now() < deadline, "the submitted dispatch never became a pending write over the keys");
+        const auto now = std::chrono::steady_clock::now();
+        Require(now < deadline, what + ": the submitted dispatch never became a pending write over the keys");
+        if (now >= retryAt) {
+            AgcDriverWaitIdle_nid_postfix();
+            Submit(data, groups);
+            retryAt = std::chrono::steady_clock::now() + std::chrono::milliseconds(10);
+        }
         std::this_thread::yield();
     }
 }
@@ -160,37 +174,37 @@ void ExecuteTests() {
     for (const auto& [key, keys] : codes) {
         Settle();
         Submit(UserData(Dwords, Dwords, Uniform(key)), Dwords / 64);
-        const auto read = ReadAfterWrite();
+        const auto read = ReadAfterWrite("uniform key " + std::to_string(key));
         Require(read.keys == keys && !read.waited, Describe("a pending key code fill by the kernel", read));
         RequireStored(Dwords, Uniform(key), 0, "the kernel's key code fill");
     }
 
     Submit(UserData(Dwords, Dwords, Uniform(0x5a)), Dwords / 64);
-    const auto other = ReadAfterWrite();
+    const auto other = ReadAfterWrite("non-key byte fill");
     Require(other.keys == DccKeys::Mixed && other.waited, Describe("a pending fill of a byte that is not a key code", other));
     RequireStored(Dwords, Uniform(0x5a), 0, "the kernel's fill of another byte");
 
     const Pattern several{0x40404040u, 0x40404040u, 0x40404040u, 0x80808080u};
     Submit(UserData(Dwords, Dwords, several), Dwords / 64);
-    const auto mixed = ReadAfterWrite();
+    const auto mixed = ReadAfterWrite("mixed key fill");
     Require(mixed.keys == DccKeys::Mixed && mixed.waited, Describe("a pending fill of several key codes", mixed));
     RequireStored(Dwords, several, 0, "the kernel's fill of several key codes");
 
     Submit(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64);
-    const auto first = ReadAfterWrite();
+    const auto first = ReadAfterWrite("0001 fill before the later writer");
     Require(first.keys == DccKeys::Clear0001 && !first.waited, Describe("a pending 0001 fill by the kernel", first));
     Submit(UserData(Dwords, Dwords / 2, Uniform(0x5a)), Dwords / 64);
-    const auto later = ReadAfterWrite();
+    const auto later = ReadAfterWrite("later half-range writer");
     Require(later.keys == DccKeys::Mixed && later.waited, Describe("a later writer over half of a pending 0001 fill", later));
     Settle();
     for (std::uint32_t i = 0; i < Dwords; ++i) Require(Keys[i] == (i < Dwords / 2 ? 0x5a5a5a5au : 0x40404040u), "the later writer over the 0001 fill: dword " + std::to_string(i) + " holds " + std::to_string(Keys[i]));
 
     Submit(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64);
-    const auto half = ReadAfterWrite();
+    const auto half = ReadAfterWrite("half-range 1111 fill");
     Require(half.keys == DccKeys::Mixed, Describe("a pending 1111 fill of half the keys over 0001 keys", half));
     Settle();
     Submit(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64);
-    const auto inside = ReadAfterWrite(SurfaceBytes / 2);
+    const auto inside = ReadAfterWrite("inside the half-range 1111 fill", SurfaceBytes / 2);
     Require(inside.keys == DccKeys::Clear1111, Describe("the half of the keys a pending 1111 fill covers", inside));
     Settle();
     for (std::uint32_t i = 0; i < Dwords; ++i) Require(Keys[i] == (i < Dwords / 2 ? 0xc0c0c0c0u : 0x40404040u), "the 1111 fill of half the keys: dword " + std::to_string(i) + " holds " + std::to_string(Keys[i]));

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

Labels

agc libSceAgc* perf numbers missing perf pull request without a before and after summary line perf Performance change unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants