Repository navigation
perf(agc): read draw vertex and index buffers in place from host imports - #2253
Conversation
|
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
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>
0e9769b to
c903d1a
Compare
|
Checked on current main (merges cleanly): libSceAgcDriver build and ctest 277/277, the new Not covered by tests: the offset/handle arithmetic in |
…s-in-place perf(agc): read draw vertex and index buffers in place from host imports
…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.
…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.
…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>
…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
…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
…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
…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
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.FlushGpuWritesthe 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.PlanVertexCopiesnow records that alignment per range (VertexCopyPlan::alignments).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).VERTEX_BUFFERandINDEX_BUFFERusage.#1968 and #1769 changed the same loops in
prepareDrawInputsand have merged. The in-place index scan uses #1968'sHighestDrawIndex, 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(newdrawInputInPlaceTests, over a write-watched block registered and imported like the other draw input tests):agc_driver_recorder_testsruns in ctest only as--pipeline-cache-only, so I ran the whole binary by hand.drawInputInPlaceTestspasses. The binary then stops atstorageRefreshTests("the refresh's store of the results did not reach guest memory"), and current main without this change stops at the same check.build.yml, run locally on this head (current main bc40a6b plus this commit): 597/598. The one failure isagc_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
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt.mdfiles or images added to the repository (attach them to this PR)