Skip to content

perf(agc): draw an indirect rect list from its records on the GPU - #5979

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
alfalfa-star:perf/agc-rect-list-indirect-gpu
Oct 17, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
alfalfa-star:perf/agc-rect-list-indirect-gpu

Conversation

@alfalfa-star

@alfalfa-star alfalfa-star commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

ClassifyIndirectDraw sends every indirect rect-list draw to the CPU path (non-vertex path), which reads the records on the CPU. When the GPU writes those records earlier in the frame, the read waits for every batch up to the producer. In Kena: Bridge of Spirits (PPSA01802) that is one DRAW_INDIRECT rect list about once per frame, each waiting 3 to 15 ms for the producing batch ([syncwait] ... indirect-args 127/995ms in one 10 s window, 116/1744ms in another).

A rect list on the vertex path is drawn by its generated tessellation stages from three-vertex patches. Those stages use no draw-dependent constants, so the records can be consumed on the GPU like any vertex-path draw, including the existing argument rewrite for constant offsets.

  • The classifier keeps a rect list without mesh or tessellation stages on the GPU path.
  • Graphics::Draw no longer requires the driver to resolve indirect rect lists.
  • APS5_NO_RECT_INDIRECT=1 restores the CPU path.

Tested

Arch Linux (kernel 7.2.9), AMD Radeon RX 9070 XT, RADV Mesa 26.2.4, Release build of this branch on main d92a8da.

  • New agc_driver_rect_list_indirect. It requires every indirect draw on the GPU path (read from the indirect-draw counters, the records in imported guest memory), so it fails with APS5_NO_RECT_INDIRECT=1 or without the driver change. Each indirect draw must also write the same bytes as its direct counterpart:
    • one rectangle from a DRAW_INDIRECT record {3, 1, 0, 0}, and a record without vertices that must draw nothing;
    • 6 vertices and 2 instances ({6, 2, 0, 0} against NUM_INSTANCES 2 + DRAW_INDEX_AUTO 6). The vertex shader puts patch vertex id / 3 + 2 × instance id in its own slot, and all four slots must be filled;
    • an indexed rect list (DRAW_INDEX_INDIRECT {6, 1, 0, 0, 0} against DRAW_INDEX_2, 16-bit indices {6, 7, 8, 3, 4, 5}). Only slots 1 and 2 may be filled. The classifier lets indexed indirect rect lists through as well.
  • agc_driver_draw_offset_indices: an auto indirect rect-list record whose packet names no offset registers must be classified to the GPU. It fails with APS5_NO_RECT_INDIRECT=1.
  • ctest -R "agc_|shader|recompiler": 575 of 576 pass (3 skipped). agc_driver_tessellation segfaults on this machine without this change as well.

Kena on plain main does not reach the title scene (it needs unmerged GPU PRs and stand-ins), so this is not a main-vs-PR capture. Same change on my local Kena stack, same save, APS5_TRACE_SECONDS=50:10, switch off (CPU path) and on, alternating:

run path DRAW_INDIRECT time / 10 s indirect-args waits / 10 s presents in window frame p50 / p90 ms
1 CPU 570 ms (167) 168 / 536 ms 161 60.5 / 68.7
1 GPU not listed none 140 69.9 / 83.4
2 CPU 994 ms (132) 127 / 995 ms 142 65.5 / 90.3
2 GPU not listed none 182 54.6 / 58.0

The wait goes away. The frame-time effect is within this scene's run-to-run variance in these four runs; queue worker 0 is about 97 % busy on both paths. The four summary lines, the traces and a per-phase breakdown are in this comment. Run 1 on the GPU path was its first run and compiled the new pipeline variants cold (shader_compile_and_link 258 ms against 16 to 19 ms in the other runs).

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: AMD Radeon RX 9070 XT, RADV Mesa 26.2.4, Arch Linux 7.2.9
  • AI-assisted: yes

🤖 Generated with Claude Code

@github-actions github-actions Bot added the agc libSceAgc* label Oct 14, 2026

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

Removing a CPU read that waits 3–15 ms on the producing batch once per frame is worthwhile, and the classifier change is small.

On the evidence: the two pairs disagree. Run 1 is slower on the GPU path (p50 60.5 → 69.9 ms) and run 2 is faster (65.5 → 54.6 ms). They come from a local stack, not main, and as the description says, that is within the scene's variance. The traces were offered on request. Attaching the four aps5-summary.jsonl lines and traces would let a maintainer check whether the indirect-args time moved somewhere else on queue 0 (for example into memory_wait or flush_between_packets), since the worker stays at about 97 % either way.

On coverage: the new test draws one rect from a {3, 1, 0, 0} record. Two cases would pin the GPU path where rect lists differ from triangle lists:

  • a record with 6 vertices and 2 instances, so that several patches per instance and firstInstance go through the generated tessellation stages; and
  • an indexed indirect rect list (DRAW_INDEX_INDIRECT), if the classifier lets it through. If it doesn't, a line in the description saying so would do.

ClassifyIndirectDraw sent every indirect rect-list draw to the CPU path
(non-vertex path), which reads the records on the CPU. When the GPU writes
those records earlier in the frame, the read waits for every batch up to
the producer: in Kena (PPSA01802) each such DRAW_INDIRECT waited about
15 ms, about once per frame.

A rect list on the vertex path is drawn by its generated tessellation
stages from three-vertex patches, with no draw-dependent constants, so the
records can be consumed on the GPU like any vertex-path draw. The
classifier now keeps a rect list without mesh or tessellation stages on
the GPU path, and Graphics::Draw accepts it. APS5_NO_RECT_INDIRECT=1
restores the CPU path.

agc_driver_rect_list_indirect submits a direct rect-list draw and the
same rectangle from a DRAW_INDIRECT record and requires the same bytes,
and a record without vertices to draw nothing; it passes on either path.
agc_driver_draw_offset_indices requires an auto indirect rect-list record
whose packet names no offset registers to be classified to the GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alfalfa-star
alfalfa-star force-pushed the perf/agc-rect-list-indirect-gpu branch from e53bb43 to 17ca453 Compare October 15, 2026 06:20
@alfalfa-star

Copy link
Copy Markdown
Contributor Author

@CnRJay thanks. Both points are addressed; rebased onto main d92a8da (17ca45319).

Coverage. agc_driver_rect_list_indirect now has a second vertex shader that places each rectangle in its own 16-pixel slot. The slot is vertex id / 3 + 2 × instance id, so a dropped patch or instance leaves a slot empty, and a misplaced one fills the wrong slot. Two new cases:

  • 6 vertices, 2 instances: a direct NUM_INSTANCES 2 + DRAW_INDEX_AUTO 6 must fill all four slots (32 pixels each). A DRAW_INDIRECT record {6, 2, 0, 0} must write the same bytes.
  • Indexed: the classifier does let DRAW_INDEX_INDIRECT rect lists through. A direct DRAW_INDEX_2 with 16-bit indices {6, 7, 8, 3, 4, 5} must fill only slots 2 and 1 (0 32 32 0), which a non-indexed reading would not produce. A DRAW_INDEX_INDIRECT record {6, 1, 0, 0, 0} must write the same bytes.

With APS5_TRACE_INDIRECT_DRAWS=1, all four indirect draws in the test classify as gpu vertex=const 0 instance=const 0. With APS5_NO_RECT_INDIRECT=1 they classify as cpu (non-vertex path), and the test passes both ways. The records keep firstInstance 0. A non-zero start instance with no start-instance register would test the existing constant-instance rule shared with triangle lists, not something specific to rect lists, so I left it out.

Evidence. The four summary lines are below, and the traces are attached here. Per-present time on queue worker 0x0, from the traces:

phase (ms/present) run 1 CPU run 1 GPU run 2 CPU run 2 GPU
Driver.Packet 42.1 50.5 49.1 35.2
Driver.Packet.draw 12.3 15.6 18.2 7.0
↳ draws ≥ 2 ms (count, total) 156, 619 ms 98, 415 ms 140, 1045 ms 1, 5 ms
Driver.Packet.prepare_memory 3.95 9.74 4.39 5.01
↳ Recorder.WaitSerial inside it 3.65 9.37 4.08 4.70
Driver.Packet.flush_between_packets 0.42 0.44 0.79 0.39
Driver.Packet.device_lock 10.4 9.6 10.2 9.0
presents 161 140 142 182
  • memory_wait in the summary lines is on queue worker 0x20 (38.8 / 45.1 / 44.5 / 34.3 ms/present), not queue 0.
  • On the CPU path, the long draws are Driver.Draw.draw_and_resource_release, about one per present. That is the record read waiting for its producer. It is gone in run 2 on the GPU path.
  • flush_between_packets does not grow on the GPU path.
  • Recorder.WaitSerial runs exactly once per present inside prepare_memory on both paths, so the removed wait did not move there. Its higher average in GPU run 1 comes from 6 waits over 15 ms.
  • GPU run 1 was the first run with the GPU path. shader_compile_and_link took 258 ms in it against 16 to 19 ms in the other three runs, and its 98 long draws are draw_prepared_on_pool, which is consistent with the new pipeline variants compiling cold. Run 2 had a warm cache.

So run 1's slower GPU numbers look like a cold pipeline cache rather than the wait moving elsewhere. Two pairs are still too few to claim a frame-time gain, and the description still says only that the wait goes away.

aps5-summary.jsonl lines (CPU 1, GPU 1, CPU 2, GPU 2)
{"commit":"9c64e198a6a6","dirty":true,"env":{"APS5_DRAW_PREP_THREADS":"4","APS5_NO_RECT_INDIRECT":"1","APS5_PAD_SCRIPT":"pad-idle.txt","APS5_PIPELINED_DRAWS":"1","APS5_PIPELINE_DMA":"1","APS5_PROFILE_DRAW":"1","APS5_SPAN_MIRROR":"1","APS5_STRICT_VSHARP":"1","APS5_SYNC_FLIP":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"50:10"},"gpu":"AMD Radeon RX 9070 XT (RADV GFX1201)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","resolution":"3840x2160","window":{"spec":"seconds 50:10","first_present":816,"ms":10052.416},"presents":161,"frame_ms":{"p50":60.548,"p90":68.746,"p99":83.392,"max":86.448},"gpu_busy_ms_per_present":50.893,"worker_busy_pct":{"queue worker 0x0":97.373,"queue worker 0x20":63.527},"top_phases_ms_per_frame":{"Driver.Packet":100.004,"Driver.Packet.memory_wait":38.835,"Driver.Packet.draw":23.927,"Driver.Draw":23.640,"Driver.Draw.draw_prepared_on_pool":13.778,"Driver.Packet.device_lock":12.127,"Driver.Packet.dispatch":7.307,"Driver.Dispatch":4.945},"trace_dropped":false,"trace":"aps5-trace-9c64e198a6a6-1299615.json.gz"}
{"commit":"9c64e198a6a6","dirty":true,"env":{"APS5_AB_ON":"1","APS5_DRAW_PREP_THREADS":"4","APS5_PAD_SCRIPT":"pad-idle.txt","APS5_PIPELINED_DRAWS":"1","APS5_PIPELINE_DMA":"1","APS5_PROFILE_DRAW":"1","APS5_SPAN_MIRROR":"1","APS5_STRICT_VSHARP":"1","APS5_SYNC_FLIP":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"50:10"},"gpu":"AMD Radeon RX 9070 XT (RADV GFX1201)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","resolution":"3840x2160","window":{"spec":"seconds 50:10","first_present":759,"ms":10026.136},"presents":140,"frame_ms":{"p50":69.901,"p90":83.411,"p99":98.652,"max":99.447},"gpu_busy_ms_per_present":58.719,"worker_busy_pct":{"queue worker 0x0":97.577,"queue worker 0x20":64.040},"top_phases_ms_per_frame":{"Driver.Packet":115.296,"Driver.Packet.memory_wait":45.100,"Driver.Packet.draw":27.636,"Driver.Draw":27.338,"Driver.Draw.draw_prepared_on_pool":18.821,"Driver.Packet.device_lock":11.495,"Driver.Packet.prepare_memory":10.573,"Recorder.WaitSerial":9.370},"trace_dropped":false,"trace":"aps5-trace-9c64e198a6a6-1305189.json.gz"}
{"commit":"9c64e198a6a6","dirty":true,"env":{"APS5_DRAW_PREP_THREADS":"4","APS5_NO_RECT_INDIRECT":"1","APS5_PAD_SCRIPT":"pad-idle.txt","APS5_PIPELINED_DRAWS":"1","APS5_PIPELINE_DMA":"1","APS5_PROFILE_DRAW":"1","APS5_SPAN_MIRROR":"1","APS5_STRICT_VSHARP":"1","APS5_SYNC_FLIP":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"50:10"},"gpu":"AMD Radeon RX 9070 XT (RADV GFX1201)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","resolution":"3840x2160","window":{"spec":"seconds 50:10","first_present":732,"ms":10026.645},"presents":142,"frame_ms":{"p50":65.548,"p90":90.311,"p99":114.980,"max":115.647},"gpu_busy_ms_per_present":57.627,"worker_busy_pct":{"queue worker 0x0":97.462,"queue worker 0x20":64.129},"top_phases_ms_per_frame":{"Driver.Packet":113.651,"Driver.Packet.memory_wait":44.506,"Driver.Packet.draw":30.535,"Driver.Draw":30.226,"Driver.Draw.draw_prepared_on_pool":16.275,"Driver.Packet.device_lock":12.061,"Driver.Packet.dispatch":7.692,"Driver.Draw.draw_and_resource_release":6.109},"trace_dropped":false,"trace":"aps5-trace-9c64e198a6a6-1309652.json.gz"}
{"commit":"9c64e198a6a6","dirty":true,"env":{"APS5_AB_ON":"1","APS5_DRAW_PREP_THREADS":"4","APS5_PAD_SCRIPT":"pad-idle.txt","APS5_PIPELINED_DRAWS":"1","APS5_PIPELINE_DMA":"1","APS5_PROFILE_DRAW":"1","APS5_SPAN_MIRROR":"1","APS5_STRICT_VSHARP":"1","APS5_SYNC_FLIP":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"50:10"},"gpu":"AMD Radeon RX 9070 XT (RADV GFX1201)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","resolution":"3840x2160","window":{"spec":"seconds 50:10","first_present":771,"ms":10010.128},"presents":182,"frame_ms":{"p50":54.631,"p90":57.973,"p99":61.263,"max":61.840},"gpu_busy_ms_per_present":45.119,"worker_busy_pct":{"queue worker 0x0":97.253,"queue worker 0x20":63.690},"top_phases_ms_per_frame":{"Driver.Packet":88.042,"Driver.Packet.memory_wait":34.335,"Driver.Packet.draw":18.409,"Driver.Draw":18.139,"Driver.Draw.draw_prepared_on_pool":12.326,"Driver.Packet.device_lock":10.673,"Driver.Packet.dispatch":6.874,"Driver.Packet.prepare_memory":5.764},"trace_dropped":false,"trace":"aps5-trace-9c64e198a6a6-1315361.json.gz"}

AI-assisted: yes (Claude Code)

@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 6529 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp L1037

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 1368 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 1395 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 1896 core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 4072 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 5390 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 5431 core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 5441 core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 5499 core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 5609 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 5925 core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6215 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6222 core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6279 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6539 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6612 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6718 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6763 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6946 core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6947 core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Checked build + ctest on the PR merged onto current main: agc_driver_draw_offset_indices and agc_driver_rect_list_indirect pass. One thing to fix before merging:

  • core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp: the test passes with the non-test changes reverted (IndirectDraw.cpp, Draw.cpp), because the CPU path writes the same bytes as the direct draw. It checks the output is equal, not that the indirect draw took the GPU path, so the 6-vertex/2-instance, indexed and empty-record cases would pass on main too. Only the single non-offset case in DrawOffsetIndices.cpp fails without the change. Please pin the route for each of those draws (classify with ClassifyIndirectDraw as DrawOffsetIndices.cpp does and require the GPU, or assert on the indirect-draw trace/counter), and check that the test fails with APS5_NO_RECT_INDIRECT=1.

Not blocking, for the record: the frame-time numbers are within variance (run 1 slower, run 2 faster), so the claim here is the removed indirect-args wait only.

agc_driver_rect_list_indirect compared the indirect draws' output with
the direct draws only, so it also passed when the records were read on
the CPU. Its records lived in a static array that is not guest memory,
so every indirect draw fell back to the CPU path as not imported.

The records now sit in a registered 64 KiB guest allocation, the test
turns on APS5_PROFILE_DRAW and reads the indirect-draw counters through
the new Graphics::IndirectDrawCounts, and it requires all four indirect
draws on the GPU path. It fails with APS5_NO_RECT_INDIRECT=1 and with
the IndirectDraw.cpp and Draw.cpp changes reverted (4 non-vertex path).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alfalfa-star

Copy link
Copy Markdown
Contributor Author

@adriadam10 thanks, you were right, and it was worse than the test missing a check: the end-to-end draws never took the GPU path at all. The records were in a static array that isn't guest memory, so all four indirect draws got past ClassifyIndirectDraw (which is what my earlier APS5_TRACE_INDIRECT_DRAWS lines showed) and then fell back to the CPU in Draw as not imported.

Fixed in ac64ea452 (pushed on top, no rebase):

  • The records now sit in a registered 64 KiB guest allocation, the same way DrawOffsetIndices.cpp does it.
  • The test turns on APS5_PROFILE_DRAW and reads the indirect-draw counters through a new Graphics::IndirectDrawCounts(), a read of the counters the [draws] line already prints. It requires all four indirect draws (plain, empty record, 6 vertices × 2 instances, indexed) on gpu-side, with no CPU path.

Linux (Arch), RX 9070 XT (RADV, Mesa 26.2.4), Release:

build agc_driver_rect_list_indirect
this PR passes 5 of 5 (and through ctest)
this PR, APS5_NO_RECT_INDIRECT=1 fails: indirect rect-list draws read their records on the CPU: 4 non-vertex path
IndirectDraw.cpp and Draw.cpp changes reverted fails: ... 4 non-vertex path
previous test (records not imported), for reference fails with the new check: ... 4 not imported

agc_driver_draw_offset_indices still passes, and tools/check_conventions.py passes. Agreed on the frame-time numbers: the claim is the removed indirect-args wait only.

AI-assisted: yes (Claude Code)

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-checked ac64ea452: the records now live in a registered guest allocation and the test reads the indirect-draw counters, requiring 4 GPU draws and 0 CPU-path draws. agc_driver_rect_list_indirect, agc_driver_draw_offset_indices and agc_driver_graphics pass on RADV and on CI's lavapipe; with APS5_NO_RECT_INDIRECT=1 both new tests fail ("read their records on the CPU: 4 non-vertex path" / "expected the GPU"). Merging once the full Build is green.

@adriadam10
adriadam10 merged commit 49f5dc1 into boykopovar:main Oct 17, 2026
9 checks passed
@alfalfa-star
alfalfa-star deleted the perf/agc-rect-list-indirect-gpu branch October 17, 2026 06:41
@ee6aba54

Copy link
Copy Markdown
Contributor

Since this PR, PPSA15304 renders bright red, blue and magenta blotches between the tree canopies at the start position of the first area, and red light patches on the tree trunks. APS5_NO_RECT_INDIRECT=1 removes both, so the GPU path for indirect rect lists draws something differently from the CPU path here.

  • Bisect: our working tree on main 40255d774, with upstream merged up to fix(shader): translate v_ldexp_f32 in f32 denormal modes 1 and 2 #6008 (c9cabcaf8), is clean. Merged up to this PR (49f5dc1a5) it shows the blotches, and the same build with APS5_NO_RECT_INDIRECT=1 is clean again.
  • Plain main bf2053816: blotches present; with APS5_NO_RECT_INDIRECT=1 they are gone (the scene there is also too dark, which is fix(libSceAgcDriver): retire a depth surface once another writer covers its memory #454 and unrelated to this PR). Frame dumps (APS5_DUMP_FRAMES) of the same save at the same camera position, so it is the title's output, not the window.
  • Counters (APS5_PROFILE_DRAW, one 10 s window in that scene): with the GPU path, indirect: gpu-side 7356 (7356 rewritten), cpu-side 0; with the switch, gpu-side 4877 (4877 rewritten), cpu-side 1820 (non-vertex path 1820), CPU argument reads 64.6 ms. So about 1800 indirect rect-list draws per 10 s, all of which take the argument rewrite on the GPU path.

Ubuntu 24.04, kernel 7.0.0, NVIDIA GeForce RTX 5080, driver 595.99.02, Release build.

I have not found which part of those draws differs yet (records, the rewritten constant, or the generated rect-list stages with indirect counts). I can trace one of the draws if you tell me which values you want; the frame dumps of both runs are available.

AI-assisted: yes

@alfalfa-star

Copy link
Copy Markdown
Contributor Author

Thanks for the bisect, the frame dumps and the counters. APS5_NO_RECT_INDIRECT=1 is the workaround until this is fixed.

On the GPU path these draws take the same record rules as indirect triangle lists (ClassifyIndirectDraw, recordIndirectArguments). The only difference is the generated rect-list control and evaluation stages. So the first thing to settle is whether the GPU draw reads the same records as the CPU path. Could you run the same 10 s window twice with APS5_TRACE_INDIRECT_DRAWS=1:

  1. GPU path, also with APS5_CHECK_INDIRECT_ARGS=1: the [draw] indirect 0x… args 0x… stride … count … -> gpu vertex=… instance=… lines, and the [draw] indirect record 0x…: count … instances … first … vertexOffset … startInstance … read-backs that APS5_CHECK_INDIRECT_ARGS prints for the same args addresses (it prints the first 16), plus its indirect records checked …, with a value in a Constant dimension … line.
  2. CPU path with APS5_NO_RECT_INDIRECT=1: the -> cpu (non-vertex path) lines and the [draw] record N: count … instances … first … vertexOffset … startInstance … lines after them.

A few draws with the same args address in both runs are enough. If the counts, instances and constants match, the difference is in how the draw runs (patch-list draw against the per-record direct draws), and I'll look there next.

AI-assisted: yes (Claude Code)

@ee6aba54

Copy link
Copy Markdown
Contributor

@alfalfa-star The records match.

Same save, same camera position, plain main bf2053816 with #7050 merged, one 10 s window after the scene is reached, APS5_TRACE_INDIRECT_DRAWS=1 in all runs:

  • GPU path (APS5_CHECK_INDIRECT_ARGS=1): blotches present.
  • CPU path (APS5_NO_RECT_INDIRECT=1): clean; 905 -> cpu (non-vertex path) draws in the window.

The rect-list draws are DRAW_INDEX_INDIRECT, stride 20, count 1, base=none inst=none idx=none, -> gpu vertex=const 0 instance=const 0 on the GPU path. Their args sit in one buffer of 179 records at a 32-byte spacing (offsets 0x0 to 0x1640); the buffer's base address differs between runs (0x335aaa0000 CPU, 0x33a13c0000 GPU), the offsets are identical, so I matched the draws by offset.

APS5_CHECK_INDIRECT_ARGS printed its 16 read-backs during loading (stride 32, count 384, not these draws), so for the GPU side I made a third run with a local build that prints every read-back (the < 16 limit removed, nothing else changed). In that window, for the 179 rect-list offsets:

CPU path records GPU path read-backs
records 890 716
count / first / vertexOffset / startInstance always 3 / 0 / 0 / 0 always 3 / 0 / 0 / 0
instances 0 545 (61%) 439 (61%)
mean instances 15.1 15.1
largest instances 311 309

Example, same offset:

cpu: [draw] indirect 0x25 args 0x335aaa0680 stride 20 count 1 ... -> cpu (non-vertex path)
     [draw]   record 0: count 3 instances 0 first 0 vertexOffset 0 startInstance 0
gpu: [draw] indirect 0x25 args 0x33a13c0680 stride 20 count 1 ... -> gpu vertex=const 0 instance=const 0
     [draw] indirect record 0x339fd70680: count 3 instances 0 first 0 vertexOffset 0 startInstance 0 (vertex const: record value ignored, instance const: record value ignored)

indirect records checked 139520, with a value in a Constant dimension 0. The instance counts per record change from frame to frame (the title writes them on the GPU), so they are compared as distributions, not draw by draw.

So the GPU draw reads the same kind of records with the same constants; about 60% of them have zero instances, the rest up to about 300.

Ubuntu 24.04, kernel 7.0.0, NVIDIA GeForce RTX 5080, driver 595.99.02, Release build.

AI-assisted: yes

@alfalfa-star

Copy link
Copy Markdown
Contributor Author

@ee6aba54 Thanks, that settles the records: same count, first index, offsets and instance distribution, and no value in a Constant dimension.

That leaves the draw itself. With vertex=const 0 instance=const 0 and no base/instance/draw-index locations, both routes use the same pipeline (the generated rect-list control and evaluation stages), the same index buffer and the same values. The CPU route issues one vkCmdDrawIndexed per record after Pm4::ReadDrawArguments. The GPU route issues one indexed indirect draw from the rewritten copy. So I see two remaining differences:

  1. When the data is final. The CPU route reads the records through the flush hook, which waits for the batch that produced them. Everything the draw reads after that is also final: the per-instance data the vertex shader fetches and any copied buffer's write-back. The GPU route checks only the record range (decide in Draw.cpp: pending images, labels and copied writers over the arguments). The title writes the instance counts on the GPU, so the per-instance data probably comes from the same pass. If any of it is still in flight when the draw is recorded, the draw reads last frame's or unwritten instances. That fits colored blotches that change with the instance count. The read-backs you compared were taken at batch completion, so they can't rule this out.
  2. The draw call. One patch-list vkCmdDrawIndexedIndirect against per-record vkCmdDrawIndexed. That is what RectListIndirect covers, but only with instanced or indexed draws, never both together as here.

One run separates them: the GPU route (no APS5_NO_RECT_INDIRECT) with APS5_SYNC_DRAWS=1, same save and camera. Each draw then waits for the work recorded before it. If the blotches go away, it is (1), and I'll extend the GPU route's check to the vertex-buffer ranges the draw fetches. If they stay, it is (2), and I'll add an indexed instanced case to RectListIndirect to reproduce it. APS5_SYNC_DRAWS is slow, but a single frame dump at that camera position is enough.

AI-assisted: yes (Claude Code)

@ee6aba54

Copy link
Copy Markdown
Contributor

@alfalfa-star With APS5_SYNC_DRAWS=1 the blotches are gone.

Same save and camera position, GPU route (no APS5_NO_RECT_INDIRECT), main 07377573f with #7050 (a714d1b37) merged and #6270 reverted (with #6270 this title aborts at startup, reported there). One frame dump (APS5_DUMP_FRAMES, 960x540) per run; blotch pixels are the strongly saturated red, blue or magenta ones, mean gray is 0-255:

Run Blotch pixels Mean gray
GPU route 6495 (1.25%) 90.1
GPU route, APS5_SYNC_DRAWS=1 0 90.8

For comparison, the earlier runs on bf2053816 + #7050: GPU route 6291 (1.21%) and 3734 (0.72%) in two runs, CPU route (APS5_NO_RECT_INDIRECT=1) 0.

Ubuntu 24.04, kernel 7.0.0, NVIDIA GeForce RTX 5080, driver 595.99.02, Release build.

AI-assisted: yes

@alfalfa-star

Copy link
Copy Markdown
Contributor Author

@ee6aba54 Thanks, that settles it: the draw call is fine, and the GPU route reads something before it is final.

I need to correct my last comment, though. Extending decide to the vertex ranges wouldn't change anything. Those ranges already get the same checks when the draw binds them in place (InPlaceDrawInput in Draw.cpp: pending images, labels, copied writers and the recorder's pending writes, plus a barrier). The shader's buffer reads get the same checks in ShaderResources. So I don't know yet which read is early, and I don't want to guess at the fix. Two things differ between the routes:

  • On the CPU route, the record read waits for the batch that wrote the records. Everything recorded before the draw has finished when the draw is recorded.
  • On the GPU route, the draw goes into the open batch while that work is still running. With main's default APS5_PIPELINED_DRAWS (depth 8), it is also prepared ahead of the submit thread.

Could you do two more runs on the GPU route (no APS5_NO_RECT_INDIRECT), same save, camera and frame dump as before, and count the blotch pixels:

  1. APS5_NO_DRAW_INPUTS_IN_PLACE=1: vertex and index ranges are copied instead of read from the host import. If this is clean, the in-place vertex read is the early one.
  2. APS5_PIPELINED_DRAWS=0: draws are recorded inline instead of prepared ahead. If this is clean, the draw is decided before the work that writes its data has been recorded.

If both still show blotches, it is a shader buffer read, and I'll send a build that logs which one overlaps work still in flight.

AI-assisted: yes (Claude Code)

@ee6aba54

Copy link
Copy Markdown
Contributor

@alfalfa-star On current main 84870f8aa the blotches are gone without any switch.

Same save and camera position, GPU route (no APS5_NO_RECT_INDIRECT), plain main 84870f8aa. One frame dump (APS5_DUMP_FRAMES, 960x540) per run, blotch pixels counted as before; indirect line from APS5_PROFILE_DRAW, one 10 s window in the scene:

Run Blotch pixels Mean gray indirect
no switch 0 93.0 gpu-side 8016 (8016 rewritten), cpu-side 0
APS5_NO_DRAW_INPUTS_IN_PLACE=1 0 88.4 gpu-side 3685 (3685 rewritten), cpu-side 0
APS5_PIPELINED_DRAWS=0 0 92.9 gpu-side 3488 (3488 rewritten), cpu-side 0

The last build with blotches was main 07377573f + #7050, #6270 reverted (6495 blotch pixels, see above). Between 07377573f and 84870f8aa there are 723 merges; I have not bisected them.

Ubuntu 24.04, kernel 7.0.0, NVIDIA GeForce RTX 5080, driver 595.99.02, Release build.

AI-assisted: yes

@alfalfa-star

Copy link
Copy Markdown
Contributor Author

@ee6aba54 Thanks for checking. If plain main 84870f8aa is clean on the GPU route, and stays clean with both switches, then there's no early read left on this route. Something in those 723 merges fixed it. I'm closing the blotch follow-up for #5979 with no change. If the blotches come back on a later main, a bisect over that range would be the next step, and I can help with it.

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*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants