Skip to content

perf(agc): read draw vertex and index buffers in place from host imports - #2253

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
Raghav314:perf/agc-draw-inputs-in-place
Oct 10, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
Raghav314:perf/agc-draw-inputs-in-place

Conversation

@Raghav314

@Raghav314 Raghav314 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What

Every draw copies its index range and each of its vertex ranges out of guest memory into a new host buffer (CopyDrawInput) before the GPU can read them. A range that lies in a host import is now bound from the import itself, at its offset in it. That's also how the console reads them: the input assembler fetches straight from guest memory, with no copy step.

  • A range is bound in place only when nothing could still write over it: no recorded or in-flight GPU write after the pending results are flushed (the same FlushGpuWrites the copy does, which waits for such a write), no completion label, no queued label store, no copied buffer a dispatch or draw writes back. Otherwise it's copied as before.
  • Its offset in the import has to suit the reader: a multiple of the index size for indices, and for vertices the strictest format alignment of the attributes that share the range. A copy starts its range at offset 0, so only an in-place range can end up misaligned; PlanVertexCopies now records that alignment per range (VertexCopyPlan::alignments).
  • The read is noted on the batch (Recorder::ReadKind::DrawInput), so a CPU store into the range waits for the draw like the other in-place readers, and the barrier validation sees it. The import's buffer stays alive until the batch completes the same way it does for those readers (retireImport).
  • Host imports gain VERTEX_BUFFER and INDEX_BUFFER usage.
  • An index range bound in place is scanned for its highest index at every draw; a copied one keeps that value in the draw input cache with its copy. I tried keeping it for in-place ranges too (same cache, same write and registry generations): standing still in Hades II's hub with 10 s windows alternated, it was slower, 37.5 against 38.4 fps (median of 6 windows each). Its index ranges are small, so the scan costs less than the cache's write collect and lookup.

#1968 and #1769 changed the same loops in prepareDrawInputs and have merged. The in-place index scan uses #1968's HighestDrawIndex, so a primitive restart index is left out of the vertex range whether the indices are copied or bound in place. Attributes whose fetch is out of range still get #1769's zero buffers; only ranges that are actually read can be bound in place. #1969 (null V#s) changes the same loop, so whichever of the two lands second needs a rebase.

PPSA36082

Hades II issues thousands of small draws a frame from vertex and index data in GPU-mapped memory. On main f145139 with the fixes it needs to reach gameplay, it ran at about 2 fps, and the frame time went to copying draw inputs. With this change it ran at 27-30 fps in the same scene (MangoHud). The previous head of this PR (0e9769b), on main d70b899 with the rest of what Hades needs (including #1810), ran gameplay at 40-45 fps with no skipped draws, so the alignment check doesn't cost it anything. The rebase onto current main changed only how the index and vertex paths fit #1968's and #1769's code, not what is bound in place.

Tested

Linux (CachyOS), x86-64, GCC, 7800X3D / RTX 4090.

  • agc_driver_graphics (new checks): a merged vertex range takes the strictest alignment of its attributes.
  • agc_driver_recorder_tests (new drawInputInPlaceTests, over a write-watched block registered and imported like the other draw input tests):
    • An aligned imported range is bound in place, and its read is noted on the open batch as a draw input read. It's gone once the batch completes.
    • A range misaligned for its format is copied; the same range is bound in place for a 2-byte reader.
    • A range under a pending GPU write is bound only after the write completed.
    • A range under a queued label store is copied; a disjoint range still binds in place.
    • Memory without an import, and a draw without a recorder, are copied.
  • agc_driver_recorder_tests runs in ctest only as --pipeline-cache-only, so I ran the whole binary by hand. drawInputInPlaceTests passes. The binary then stops at storageRefreshTests ("the refresh's store of the results did not reach guest memory"), and current main without this change stops at the same check.
  • Mutations, each failing a check: alignment ignored, queued label stores ignored, read not noted, a range keeping its first attribute's alignment, an import bound without a recorder. After the rebase I ran the first and third again, and both still fail.
  • The linux job of current build.yml, run locally on this head (current main bc40a6b plus this commit): 597/598. The one failure is agc_driver_dcc_keys_first_write, which fails the same way on main without this change on this machine (RTX 4090) and passes in GitHub's CI.

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 in TechnicalDebt
  • 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
  • AI-assisted: yes

@popcoder565

Copy link
Copy Markdown
Contributor

Native Windows run, since no Windows results were posted. Setup: Windows 10 (19045), RTX 3060 Laptop (NVIDIA 616.92), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release. Head 0e9769bd, merged onto main d70b899 in one build with 33 other AGC/shader PRs that had no Windows run.

No title was run.

🤖 Addressed by Claude Code

Every draw copied its index range and its vertex ranges out of guest
memory into new host buffers (CopyDrawInput) before the GPU could read
them. A range that lies in a host import is now bound from the import
itself, at its offset in it, the way the console's input assembler reads
guest memory directly.

A range is bound in place only when the batch has no write over it that
could still land (a recorded or in-flight GPU write after the pending
results are flushed, a completion label, a queued label store, a copied
buffer a dispatch or draw writes back) and its offset in the import is a
multiple of what the reader needs: the index size, or the strictest
format alignment of the vertex attributes that share the range
(VertexCopyPlan::alignments; a copy starts its range at offset 0, so
only an in-place range can be misaligned). Otherwise it is copied as
before. The read is noted on the batch (Recorder::ReadKind::DrawInput),
so a CPU store into the range waits for the draw like the other in-place
readers, and the barrier validation sees it. Imports gain vertex and
index buffer usage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Raghav314
Raghav314 force-pushed the perf/agc-draw-inputs-in-place branch from 0e9769b to c903d1a Compare October 10, 2026 05:14
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 10, 2026
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on current main (merges cleanly): libSceAgcDriver build and ctest 277/277, the new agc_driver_graphics alignment checks fail to build without the change, and the full agc_driver_recorder_tests binary run by hand passes drawInputInPlaceTests (it stops later at storageRefreshTests, as on main). Also covered by popcoder565's Windows run on the previous head.

Not covered by tests: the offset/handle arithmetic in prepareDrawInputs itself (only InPlaceDrawInput is pinned). Merging; whichever of this and #1969 lands second needs a rebase (same loop, tests/Graphics.cpp, TechnicalDebt).

@adriadam10
adriadam10 merged commit 394b6d0 into boykopovar:main Oct 10, 2026
12 of 13 checks passed
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
…s-in-place

perf(agc): read draw vertex and index buffers in place from host imports
oneandonlydean added a commit to oneandonlydean/AnyPS5 that referenced this pull request Oct 11, 2026
…x buffer

An indexed draw that binds its index buffer in place from a host import
(boykopovar#2253) scans the bound range for its highest index on every draw. An
indirect draw binds the whole declared index buffer, so each of its
draws scanned the whole buffer again, where the copied path reuses the
value kept with its snapshot.

The recorder now keeps the highest index of an in-place index range with
the write generation collected before the scan, and an indirect draw
takes it while the range is unchanged since (GuestMemory::UnchangedSince
and the allocation registry generation, as for a reused snapshot). A
direct draw scans its own indices as before.
oneandonlydean added a commit that referenced this pull request Oct 12, 2026
…ile it is unchanged

An indexed draw that binds its index buffer in place from a host import
(#2253) scans the bound range for its highest index on every draw, where
the copied path reuses the value kept with its snapshot. A draw of many
indices pays for the scan each time, and an indirect draw, which binds
the whole declared index buffer, scans the whole buffer per draw.

The recorder now keeps the highest index of an in-place index range of
4096 indices or more with the write generation collected before the
scan, and a draw takes it while the range is unchanged since
(GuestMemory::UnchangedSince and the allocation registry generation, as
for a reused snapshot). A smaller range is scanned at every draw as
before: the scan costs 0.4 ns per index and the write collect and lookup
0.2 to 1.4 us per draw, so the scan is the cheaper of the two below
about 3,000 indices, as #2253 measured on small ranges.
SP4C3B4R-8 added a commit to SP4C3B4R-8/AnyPS5 that referenced this pull request Oct 16, 2026
…s in place

On Windows the GPU reads a host import of guest memory far slower than a buffer the driver allocated.
Since draw vertex and index ranges are bound in place from host imports (boykopovar#2253, measured on Linux), Astro
Bot's Crash Site on Windows 11 with an RTX 5070 Ti spends 164 ms of every frame in draws on the GPU, and
45 ms with the ranges copied as before; with the same build, 4.71 fps in place against 7.52 copied.

On Windows draw inputs are copied again; APS5_DRAW_INPUTS_IN_PLACE=1 binds them in place there, and
APS5_NO_DRAW_INPUTS_IN_PLACE=1 copies them on other hosts. agc_draw_inputs_copied checks that an aligned
imported range is copied and its read not noted; agc_driver_recorder_tests sets APS5_DRAW_INPUTS_IN_PLACE=1
on Windows for its in-place cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SP4C3B4R-8 added a commit to SP4C3B4R-8/AnyPS5 that referenced this pull request Oct 17, 2026
…ding host imports in place

NVIDIA's Windows driver reads a host import of guest memory far slower than a buffer it allocated.
Since draw vertex and index ranges are bound in place from host imports (boykopovar#2253, measured on Linux), Astro
Bot's Crash Site on Windows 11 with an RTX 5070 Ti spends 164 ms of every frame in draws on the GPU, and
45 ms with the ranges copied as before; with the same build, 4.71 fps in place against 7.52 copied. An
RTX 3060 shows the same (draws 32.3 against 24.3 ms per frame), while AMD's Windows driver reads them in
place about as fast as a copy (RX 9070 XT: 69.8 against 73.1 ms), so copying there only costs CPU.

The device decides once (DrawInputsInPlaceUsable, Context::drawInputsInPlace): ranges are copied with
VK_DRIVER_ID_NVIDIA_PROPRIETARY on Windows and bound in place everywhere else. APS5_DRAW_INPUTS_IN_PLACE=1
binds them in place and APS5_NO_DRAW_INPUTS_IN_PLACE=1 copies them on any device, the second winning.
agc_draw_inputs_in_place_driver checks the decision per driver and host; agc_draw_inputs_copied checks
that an aligned imported range is copied and its read not noted when the context copies, whatever the
inherited environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GN8FJFTedayRXLHythXkZ4
SP4C3B4R-8 added a commit to SP4C3B4R-8/AnyPS5 that referenced this pull request Oct 18, 2026
…ding host imports in place

NVIDIA's Windows driver reads a host import of guest memory far slower than a buffer it allocated.
Since draw vertex and index ranges are bound in place from host imports (boykopovar#2253, measured on Linux), Astro
Bot's Crash Site on Windows 11 with an RTX 5070 Ti spends 164 ms of every frame in draws on the GPU, and
45 ms with the ranges copied as before; with the same build, 4.71 fps in place against 7.52 copied. An
RTX 3060 shows the same (draws 32.3 against 24.3 ms per frame), while AMD's Windows driver reads them in
place about as fast as a copy (RX 9070 XT: 69.8 against 73.1 ms), so copying there only costs CPU.

The device decides once (DrawInputsInPlaceUsable, Context::drawInputsInPlace): ranges are copied with
VK_DRIVER_ID_NVIDIA_PROPRIETARY on Windows and bound in place everywhere else. APS5_DRAW_INPUTS_IN_PLACE=1
binds them in place and APS5_NO_DRAW_INPUTS_IN_PLACE=1 copies them on any device, the second winning.
agc_draw_inputs_in_place_driver checks the decision per driver and host; agc_draw_inputs_copied checks
that an aligned imported range is copied and its read not noted when the context copies, whatever the
inherited environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GN8FJFTedayRXLHythXkZ4
SP4C3B4R-8 added a commit to SP4C3B4R-8/AnyPS5 that referenced this pull request Oct 19, 2026
…ding host imports in place

NVIDIA's Windows driver reads a host import of guest memory far slower than a buffer it allocated.
Since draw vertex and index ranges are bound in place from host imports (boykopovar#2253, measured on Linux), Astro
Bot's Crash Site on Windows 11 with an RTX 5070 Ti spends 164 ms of every frame in draws on the GPU, and
45 ms with the ranges copied as before; with the same build, 4.71 fps in place against 7.52 copied. An
RTX 3060 shows the same (draws 32.3 against 24.3 ms per frame), while AMD's Windows driver reads them in
place about as fast as a copy (RX 9070 XT: 69.8 against 73.1 ms), so copying there only costs CPU.

The device decides once (DrawInputsInPlaceUsable, Context::drawInputsInPlace): ranges are copied with
VK_DRIVER_ID_NVIDIA_PROPRIETARY on Windows and bound in place everywhere else. APS5_DRAW_INPUTS_IN_PLACE=1
binds them in place and APS5_NO_DRAW_INPUTS_IN_PLACE=1 copies them on any device, the second winning.
agc_draw_inputs_in_place_driver checks the decision per driver and host; agc_draw_inputs_copied checks
that an aligned imported range is copied and its read not noted when the context copies, whatever the
inherited environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GN8FJFTedayRXLHythXkZ4
adriadam10 pushed a commit to SP4C3B4R-8/AnyPS5 that referenced this pull request Oct 19, 2026
…ding host imports in place

NVIDIA's Windows driver reads a host import of guest memory far slower than a buffer it allocated.
Since draw vertex and index ranges are bound in place from host imports (boykopovar#2253, measured on Linux), Astro
Bot's Crash Site on Windows 11 with an RTX 5070 Ti spends 164 ms of every frame in draws on the GPU, and
45 ms with the ranges copied as before; with the same build, 4.71 fps in place against 7.52 copied. An
RTX 3060 shows the same (draws 32.3 against 24.3 ms per frame), while AMD's Windows driver reads them in
place about as fast as a copy (RX 9070 XT: 69.8 against 73.1 ms), so copying there only costs CPU.

The device decides once (DrawInputsInPlaceUsable, Context::drawInputsInPlace): ranges are copied with
VK_DRIVER_ID_NVIDIA_PROPRIETARY on Windows and bound in place everywhere else. APS5_DRAW_INPUTS_IN_PLACE=1
binds them in place and APS5_NO_DRAW_INPUTS_IN_PLACE=1 copies them on any device, the second winning.
agc_draw_inputs_in_place_driver checks the decision per driver and host; agc_draw_inputs_copied checks
that an aligned imported range is copied and its read not noted when the context copies, whatever the
inherited environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GN8FJFTedayRXLHythXkZ4
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.

3 participants