aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/dma_pusher.cpp
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-02 11:24:19 -0500
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-02 11:24:19 -0500
commit8f1435f8db104351fa3388551eee9d12d2043b40 (patch)
tree1cbaa0434f77ae07ba04ed41504ba48cbdd246a2 /src/video_core/dma_pusher.cpp
parent75bdb925dfb77db2370cb027eda81352c9622bbb (diff)
nx2: read GPU-written command lists from copies instead of draining
The trees fix (#52) drains the split and flushes for every command list on a GPU-written page: in a Mario Kart World race ~13,500 lists and ~400 GPU round trips a second, during which the back has nothing to do. The replay ran at ~40 fps instead of ~45. The front now marks the pages those lists are on. The buffer cache notes writes to them; after a compute dispatch or DMA copy that writes one it copies the page into host memory and submits. The front waits for the back only up to the last compute or copy work it sent (not everything queued), then reads the copy once the GPU is done with it. Pages the GPU wrote once, long before, and the CPU now writes lists to have no copy: their first list drains and flushes the whole page, which is then read from guest memory until GPU work writes it again. CPU writes and inline uploads reach guest memory, so they keep such a page synced but make a copy stale. A page a draw writes always drains. Mario Kart World time-trial replay, Radeon 780M: ~40 -> ~45 fps, trees drawn, no list drains left.
Diffstat (limited to 'src/video_core/dma_pusher.cpp')
-rw-r--r--src/video_core/dma_pusher.cpp98
1 files changed, 95 insertions, 3 deletions
diff --git a/src/video_core/dma_pusher.cpp b/src/video_core/dma_pusher.cpp
index 1144488c91..6b93bb7f60 100644
--- a/src/video_core/dma_pusher.cpp
+++ b/src/video_core/dma_pusher.cpp
@@ -9,6 +9,7 @@
#include <cstdlib>
#include <cstdio>
+#include "common/scope_exit.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/memory.h"
@@ -178,6 +179,11 @@ bool DmaPusher::Step() {
RecordParsedEntry(header, commands);
command_resume_offset +=
ProcessCommands(commands.subspan(command_resume_offset), command_resume_offset);
+ } else if (ReadGpuWrittenCommands(dma_state.dma_get, static_cast<u32>(header.size))) {
+ const std::span<const CommandHeader> commands{command_headers.data(), header.size};
+ RecordParsedEntry(header, commands);
+ command_resume_offset +=
+ ProcessCommands(commands.subspan(command_resume_offset), command_resume_offset);
} else {
FlushGpuWrittenCommands(dma_state.dma_get, size_t{header.size} * sizeof(u32));
Tegra::Memory::GpuGuestMemory<Tegra::CommandHeader, Tegra::Memory::GuestMemoryFlags::UnsafeRead>headers(memory_manager, dma_state.dma_get, header.size, &command_headers);
@@ -382,6 +388,15 @@ void DmaPusher::CallMethod(u32 argument) {
ReportUnboundSubchannel();
return;
}
+ // Compute dispatches and DMA copies write the command lists ReadGpuWrittenCommands reads.
+ const auto engine_type = subchannel_type[dma_state.subchannel];
+ const bool writer = engine_type == Engines::EngineTypes::KeplerCompute ||
+ engine_type == Engines::EngineTypes::MaxwellDMA;
+ SCOPE_EXIT {
+ if (writer) {
+ split.NoteWriter();
+ }
+ };
if (split.Active()) {
const GPUVAddr segment = dma_state.dma_get + dma_state.dma_word_offset;
if (split.FrontMacros()) {
@@ -432,6 +447,15 @@ void DmaPusher::CallMultiMethod(const u32* base_start, u32 num_methods) {
ReportUnboundSubchannel();
return;
}
+ // Compute dispatches and DMA copies write the command lists ReadGpuWrittenCommands reads.
+ const auto engine_type = subchannel_type[dma_state.subchannel];
+ const bool writer = engine_type == Engines::EngineTypes::KeplerCompute ||
+ engine_type == Engines::EngineTypes::MaxwellDMA;
+ SCOPE_EXIT {
+ if (writer) {
+ split.NoteWriter();
+ }
+ };
if (split.Active()) {
const GPUVAddr segment = dma_state.dma_get + dma_state.dma_word_offset;
if (split.FrontMacros()) {
@@ -476,18 +500,86 @@ bool DmaPusher::IsGpuWrittenCommands(GPUVAddr address, size_t size) {
return false;
}
+bool DmaPusher::ReadGpuWrittenCommands(GPUVAddr address, u32 count) {
+ // The pages are marked as command list pages: from the next write on, the buffer cache copies
+ // them to host memory right after the dispatch or copy that writes them and submits. The
+ // front then waits only for the back to run that work and for the GPU to finish the copy, not
+ // for everything queued and a cache flush (FlushGpuWrittenCommands: ~400 GPU round trips a
+ // second in a Mario Kart World race, during which the back had nothing to do). Pages
+ // FlushGpuWrittenCommands flushed whole and no GPU work wrote since are read from guest
+ // memory.
+ if (!Tegra::g_nx2_ampere.load(std::memory_order_relaxed) || count == 0 ||
+ rasterizer == nullptr) {
+ return false;
+ }
+ const GPUVAddr end = address + u64{count} * sizeof(u32);
+ bool written = false;
+ for (GPUVAddr page = address & ~GPUVAddr{0xfff}; page < end; page += 0x1000) {
+ if (const auto device = memory_manager.GpuToCpuAddress(page);
+ device && Tegra::Nx2IsGpuWritten(*device)) {
+ Tegra::Nx2MarkCommandListPage(*device);
+ written = true;
+ }
+ }
+ if (!written) {
+ return false;
+ }
+ if (auto& split = VideoCommon::GpuSplit::Get(); split.Active()) {
+ split.WaitForWriters();
+ }
+ using GpuWrittenList = VideoCore::RasterizerInterface::GpuWrittenList;
+ command_headers.resize_destructive(count);
+ u8* const out = reinterpret_cast<u8*>(command_headers.data());
+ for (GPUVAddr chunk = address; chunk < end;) {
+ const GPUVAddr chunk_end = (std::min)(end, (chunk & ~GPUVAddr{0xfff}) + 0x1000);
+ const std::span<u8> part{out + (chunk - address), static_cast<size_t>(chunk_end - chunk)};
+ const auto device = memory_manager.GpuToCpuAddress(chunk);
+ if (!device) {
+ return false;
+ }
+ GpuWrittenList where = GpuWrittenList::Synced;
+ if (Tegra::Nx2IsGpuWritten(*device)) {
+ where = rasterizer->ReadGpuWrittenCommandList(*device, part);
+ }
+ if (where == GpuWrittenList::Unknown) {
+ return false;
+ }
+ if (where == GpuWrittenList::Synced) {
+ memory_manager.ReadBlockUnsafe(chunk, part.data(), part.size());
+ }
+ chunk = chunk_end;
+ }
+ return true;
+}
+
void DmaPusher::FlushGpuWrittenCommands(GPUVAddr address, size_t size) {
// Title 1 runs small command lists its culling dispatches fill in each frame. Accurate GPU
// mode reads every command list through the caches (safe reads); here only the ones on pages
// the GPU has written are, or their draws run with an older frame's words (trees vanish).
+ // These are the lists ReadGpuWrittenCommands can't take from a copy or guest memory (a page's
+ // first list, or pages draws write too).
if (!IsGpuWrittenCommands(address, size)) {
return;
}
// The caches belong to the split's back: it first runs what the front queued (the dispatch
- // that writes these words among it).
+ // that writes these words among it). The whole pages are flushed: their next lists read
+ // guest memory until GPU work writes them again.
VideoCommon::GpuSplit::Get().Drain();
- if (memory_manager.IsMemoryDirty(address, size, VideoCommon::CacheType::BufferCache)) {
- memory_manager.FlushRegion(address, size, VideoCommon::CacheType::BufferCache);
+ const GPUVAddr first_page = address & ~GPUVAddr{0xfff};
+ const GPUVAddr end_page = (address + size + 0xfff) & ~GPUVAddr{0xfff};
+ if (memory_manager.IsMemoryDirty(first_page, end_page - first_page,
+ VideoCommon::CacheType::BufferCache)) {
+ memory_manager.FlushRegion(first_page, end_page - first_page,
+ VideoCommon::CacheType::BufferCache);
+ }
+ if (rasterizer == nullptr) {
+ return;
+ }
+ for (GPUVAddr page = first_page; page < end_page; page += 0x1000) {
+ if (const auto device = memory_manager.GpuToCpuAddress(page);
+ device && Tegra::Nx2IsCommandListPage(*device)) {
+ rasterizer->NoteGpuWrittenCommandListSynced(*device);
+ }
}
}