aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/buffer_cache
diff options
context:
space:
mode:
Diffstat (limited to 'src/video_core/buffer_cache')
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h259
-rw-r--r--src/video_core/buffer_cache/buffer_cache_base.h53
2 files changed, 306 insertions, 6 deletions
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index e90af3214d..cd176dec27 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -17,6 +17,7 @@
#include "video_core/buffer_cache/buffer_cache_base.h"
#include "video_core/guest_memory.h"
#include "video_core/host1x/gpu_device_memory_manager.h"
+#include "video_core/nx2.h"
#include "video_core/nx2_profile.h"
#include "video_core/texture_cache/util.h"
@@ -100,6 +101,12 @@ void BufferCache<P>::TickFrame() {
return;
}
runtime.TickFrame(slot_buffers);
+ if (frame_tick % 600 == 0 && !uploaded_buffer_ranges.empty()) {
+ for (const auto& [address, size] : uploaded_buffer_ranges) {
+ device_memory.HintCold(address, size);
+ }
+ uploaded_buffer_ranges.clear();
+ }
if (static u64 frames{}; ++frames % 600 == 0) {
u64 total{};
size_t count{};
@@ -141,9 +148,10 @@ void BufferCache<P>::TickFrame() {
// title 1's buffers were ~200 MiB of an 8 GB GPU's ~6.8 GB in use, yet this collector
// ran aggressively every frame on the device-wide figure, evicting up to 64 buffers (and
// downloading the GPU-written ones, ~5% of the GPU thread) that were recreated later.
- // Collect when the buffers themselves are large.
- if (buffer_bytes >= 1_GiB) {
- RunGarbageCollector(buffer_bytes >= 2_GiB);
+ // Collect when the buffers themselves are large. At 1 GiB it held geometry of scenes
+ // long gone: title 1's title screen grew to ~0.9 GB of buffers in ten minutes.
+ if (buffer_bytes >= 512_MiB) {
+ RunGarbageCollector(buffer_bytes >= 1_GiB);
}
} else if (total_used_memory >= minimum_memory) {
RunGarbageCollector(total_used_memory >= critical_memory);
@@ -166,6 +174,7 @@ void BufferCache<P>::UnmapGPUMemory(size_t as_id, GPUVAddr gpu_addr, size_t size
template <class P>
void BufferCache<P>::WriteMemory(DAddr device_addr, u64 size) {
+ Nx2NoteListHostWrite(device_addr, size);
if (IsRegionGpuModified(device_addr, size)) {
ClearDownload(device_addr, size);
gpu_modified_ranges.Subtract(device_addr, size);
@@ -186,6 +195,7 @@ void BufferCache<P>::CachedWriteMemory(DAddr device_addr, u64 size) {
return;
}
+ Nx2NoteListHostWrite(device_addr, size);
tmp_buffer.resize_destructive(size);
device_memory.ReadBlockUnsafe(device_addr, tmp_buffer.data(), size);
@@ -291,6 +301,10 @@ bool BufferCache<P>::DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 am
const bool has_new_downloads = tmp_intervals.size() != 0;
for (const auto& pair : tmp_intervals) {
gpu_modified_ranges.Add(pair.first, pair.second);
+ if (Tegra::g_nx2_ampere.load(std::memory_order_relaxed)) {
+ Tegra::Nx2MarkGpuWritten(pair.first, pair.second);
+ Nx2NoteListWrite(buffer_b, pair.first, pair.second);
+ }
}
const auto& copy = copies[0];
src_buffer.MarkUsage(copy.src_offset, copy.size);
@@ -379,6 +393,10 @@ template <class P>
void BufferCache<P>::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr,
u32 size) {
const std::optional<DAddr> device_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
+ if (!device_addr) {
+ channel_state->uniform_buffers[stage][index] = NULL_BINDING;
+ return;
+ }
const Binding binding{
.device_addr = *device_addr,
.size = size,
@@ -389,7 +407,11 @@ void BufferCache<P>::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr
template <class P>
void BufferCache<P>::DisableGraphicsUniformBuffer(size_t stage, u32 index) {
- channel_state->uniform_buffers[stage][index] = NULL_BINDING;
+ auto& current = channel_state->uniform_buffers[stage][index];
+ if (current.device_addr == 0 && current.size == 0 && !current.buffer_id) [[likely]] {
+ return;
+ }
+ current = NULL_BINDING;
}
template <class P>
@@ -994,6 +1016,26 @@ void BufferCache<P>::BindHostGraphicsUniformBuffers(size_t stage) {
template <class P>
void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32 binding_index, bool needs_bind) {
++channel_state->uniform_cache_shots[0];
+ // Bank 24 (root table 0, NVN2's push constants) is read from bank 17, which such games do not
+ // bind: stream the table's current words as that buffer.
+ if (index == 17 && maxwell3d->root_tables.loaded && channel_state->uniform_buffers[stage][index].size == 0) {
+ const auto& words = maxwell3d->root_tables.tables[0];
+ const u32 size = (*channel_state->uniform_buffer_sizes)[stage][index];
+ const u32 copied = (std::min)(size, static_cast<u32>(sizeof(words)));
+ if constexpr (IS_OPENGL) {
+ std::array<u8, sizeof(words)> data{};
+ std::memcpy(data.data(), words.data(), copied);
+ runtime.BindFastUniformBuffer(stage, binding_index, copied);
+ runtime.PushFastUniformBuffer(stage, binding_index, std::span<const u8>(data.data(), copied));
+ } else {
+ const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
+ std::memset(span.data(), 0, span.size());
+ std::memcpy(span.data(), words.data(), copied);
+ }
+ channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index;
+ channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size;
+ return;
+ }
const Binding& binding = channel_state->uniform_buffers[stage][index];
const DAddr device_addr = binding.device_addr;
const u32 size = (std::min)(binding.size, (*channel_state->uniform_buffer_sizes)[stage][index]);
@@ -1494,6 +1536,9 @@ void BufferCache<P>::UpdateDrawIndirect() {
template <class P>
void BufferCache<P>::UpdateUniformBuffers(size_t stage) {
+ if (channel_state->enabled_uniform_buffer_masks[stage] == 0) [[likely]] {
+ return;
+ }
ForEachEnabledBit(channel_state->enabled_uniform_buffer_masks[stage], [&](u32 index) {
Binding& binding = channel_state->uniform_buffers[stage][index];
if (binding.buffer_id) {
@@ -1511,6 +1556,9 @@ void BufferCache<P>::UpdateUniformBuffers(size_t stage) {
template <class P>
void BufferCache<P>::UpdateStorageBuffers(size_t stage) {
+ if (channel_state->enabled_storage_buffers[stage] == 0) [[likely]] {
+ return;
+ }
ForEachEnabledBit(channel_state->enabled_storage_buffers[stage], [&](u32 index) {
// Resolve buffer
Binding& binding = channel_state->storage_buffers[stage][index];
@@ -1523,6 +1571,9 @@ void BufferCache<P>::UpdateStorageBuffers(size_t stage) {
template <class P>
void BufferCache<P>::UpdateTextureBuffers(size_t stage) {
+ if (channel_state->enabled_texture_buffers[stage] == 0) [[likely]] {
+ return;
+ }
ForEachEnabledBit(channel_state->enabled_texture_buffers[stage], [&](u32 index) {
Binding& binding = channel_state->texture_buffers[stage][index];
binding.buffer_id = FindBuffer(binding.device_addr, binding.size, false);
@@ -1610,14 +1661,205 @@ void BufferCache<P>::MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u3
} else {
ArmReadTraps(device_addr, size);
}
+ if (Tegra::g_nx2_ampere.load(std::memory_order_relaxed)) {
+ Tegra::Nx2MarkGpuWritten(device_addr, size);
+ Nx2NoteListWrite(buffer_id, device_addr, size);
+ }
+}
+
+template <class P>
+void BufferCache<P>::Nx2NoteListWrite(BufferId buffer_id, DAddr device_addr, u64 size) {
+ if constexpr (!IS_OPENGL) {
+ if (size == 0 || !Tegra::g_nx2_has_command_list_pages.load(std::memory_order_relaxed)) {
+ return;
+ }
+ constexpr u64 page_bits = Tegra::Nx2WrittenPageBits;
+ const u64 last = (std::min<u64>)((device_addr + size - 1) >> page_bits,
+ Tegra::g_nx2_command_list_pages.size() * 64 - 1);
+ for (u64 page = device_addr >> page_bits; page <= last;) {
+ // Storage buffers can be large: skip 64 pages at a time where none is a list page.
+ const u64 word =
+ Tegra::g_nx2_command_list_pages[page / 64].load(std::memory_order_relaxed) >>
+ (page % 64);
+ if (word == 0) {
+ page = (page / 64 + 1) * 64;
+ continue;
+ }
+ if (word & 1) {
+ const DAddr page_addr = page << page_bits;
+ {
+ std::scoped_lock lock{nx2_list_mutex};
+ Nx2ListPage& entry = nx2_list_pages[page_addr];
+ ++entry.writes;
+ if (!nx2_collect_list_copies && !entry.draw_written) {
+ entry.draw_written = true;
+ LOG_INFO(HW_GPU, "Command list page {:#x} is written by draws too: its "
+ "lists drain the split",
+ page_addr);
+ }
+ }
+ if (nx2_collect_list_copies) {
+ nx2_list_copy_requests.emplace_back(buffer_id, page_addr);
+ }
+ }
+ ++page;
+ }
+ }
+}
+
+template <class P>
+bool BufferCache<P>::Nx2CopyListPages(u64 tick) {
+ if constexpr (IS_OPENGL) {
+ return false;
+ } else {
+ if (nx2_list_copy_requests.empty()) {
+ return false;
+ }
+ std::ranges::sort(nx2_list_copy_requests, {}, &std::pair<BufferId, DAddr>::second);
+ constexpr DAddr page_size = DAddr{1} << Tegra::Nx2WrittenPageBits;
+ bool recorded = false;
+ for (size_t i = 0; i < nx2_list_copy_requests.size(); ++i) {
+ const auto [buffer_id, page] = nx2_list_copy_requests[i];
+ if (i + 1 < nx2_list_copy_requests.size() &&
+ nx2_list_copy_requests[i + 1].second == page) {
+ continue;
+ }
+ Buffer& buffer = slot_buffers[buffer_id];
+ const DAddr begin = (std::max<DAddr>)(page, buffer.CpuAddr());
+ const DAddr end =
+ (std::min<DAddr>)(page + page_size, buffer.CpuAddr() + buffer.SizeBytes());
+ if (begin >= end) {
+ continue;
+ }
+ Nx2Staging staging = runtime.DownloadStagingBuffer(page_size, true);
+ const BufferCopy copy{
+ .src_offset = buffer.Offset(begin),
+ .dst_offset = staging.offset + (begin - page),
+ .size = end - begin,
+ };
+ buffer.MarkUsage(copy.src_offset, copy.size);
+ runtime.CopyBuffer(staging.buffer, buffer, std::span{&copy, 1}, true);
+ recorded = true;
+ std::scoped_lock lock{nx2_list_mutex};
+ Nx2ListPage& entry = nx2_list_pages[page];
+ if (entry.copy) {
+ nx2_retired_list_copies.emplace_back(entry.copy_tick, std::move(*entry.copy));
+ }
+ entry.copy = staging;
+ entry.copy_writes = entry.writes;
+ entry.copy_tick = tick;
+ entry.copy_begin = static_cast<u32>(begin - page);
+ entry.copy_end = static_cast<u32>(end - page);
+ }
+ nx2_list_copy_requests.clear();
+ std::erase_if(nx2_retired_list_copies, [this](auto& retired) {
+ if (!runtime.IsFree(retired.first)) {
+ return false;
+ }
+ runtime.FreeDeferredStagingBuffer(retired.second);
+ return true;
+ });
+ return recorded;
+ }
+}
+
+template <class P>
+auto BufferCache<P>::Nx2ReadListCopy(DAddr device_addr, std::span<u8> out) -> Nx2ListRead {
+ if constexpr (IS_OPENGL) {
+ return Nx2ListRead::Unknown;
+ } else {
+ constexpr DAddr page_size = DAddr{1} << Tegra::Nx2WrittenPageBits;
+ const DAddr page = device_addr & ~(page_size - 1);
+ const u64 offset = device_addr - page;
+ if (offset + out.size() > page_size) {
+ return Nx2ListRead::Unknown;
+ }
+ const auto current = [&](const Nx2ListPage& entry) {
+ return entry.copy && !entry.draw_written && entry.copy_writes == entry.writes &&
+ offset >= entry.copy_begin && offset + out.size() <= entry.copy_end;
+ };
+ u64 tick{};
+ u64 writes{};
+ {
+ std::scoped_lock lock{nx2_list_mutex};
+ const auto it = nx2_list_pages.find(page);
+ if (it == nx2_list_pages.end()) {
+ return Nx2ListRead::Unknown;
+ }
+ const Nx2ListPage& entry = it->second;
+ // Pages the CPU writes the lists of (GPU-written once, long before) stay synced.
+ if (!entry.draw_written && entry.synced_writes == entry.writes) {
+ return Nx2ListRead::Synced;
+ }
+ if (!current(entry)) {
+ return Nx2ListRead::Unknown;
+ }
+ // The lists after the first of a batch find the copy done: read it at once.
+ if (runtime.IsFree(entry.copy_tick)) {
+ std::memcpy(out.data(), entry.copy->mapped_span.data() + offset, out.size());
+ return Nx2ListRead::Copied;
+ }
+ tick = entry.copy_tick;
+ writes = entry.copy_writes;
+ }
+ // Submitted with the work that wrote the page (Nx2CopyListPages's caller flushes).
+ runtime.WaitSubmittedTick(tick);
+ std::scoped_lock lock{nx2_list_mutex};
+ const auto it = nx2_list_pages.find(page);
+ if (it == nx2_list_pages.end() || !current(it->second) || it->second.copy_tick != tick ||
+ it->second.copy_writes != writes) {
+ return Nx2ListRead::Unknown;
+ }
+ std::memcpy(out.data(), it->second.copy->mapped_span.data() + offset, out.size());
+ return Nx2ListRead::Copied;
+ }
+}
+
+template <class P>
+void BufferCache<P>::Nx2NoteListSynced(DAddr page) {
+ if constexpr (!IS_OPENGL) {
+ std::scoped_lock lock{nx2_list_mutex};
+ Nx2ListPage& entry = nx2_list_pages[page];
+ entry.synced_writes = entry.writes;
+ }
+}
+
+template <class P>
+void BufferCache<P>::Nx2NoteListHostWrite(DAddr device_addr, u64 size) {
+ if constexpr (!IS_OPENGL) {
+ if (size == 0 || !Tegra::g_nx2_has_command_list_pages.load(std::memory_order_relaxed)) {
+ return;
+ }
+ constexpr u64 page_bits = Tegra::Nx2WrittenPageBits;
+ const u64 last = (std::min<u64>)((device_addr + size - 1) >> page_bits,
+ Tegra::g_nx2_command_list_pages.size() * 64 - 1);
+ for (u64 page = device_addr >> page_bits; page <= last; ++page) {
+ if (!((Tegra::g_nx2_command_list_pages[page / 64].load(std::memory_order_relaxed) >>
+ (page % 64)) &
+ 1)) {
+ continue;
+ }
+ std::scoped_lock lock{nx2_list_mutex};
+ const auto it = nx2_list_pages.find(page << page_bits);
+ if (it == nx2_list_pages.end()) {
+ continue;
+ }
+ Nx2ListPage& entry = it->second;
+ const bool synced = entry.synced_writes == entry.writes;
+ ++entry.writes;
+ if (synced) {
+ entry.synced_writes = entry.writes;
+ }
+ }
+ }
}
template <class P>
BufferId BufferCache<P>::FindBuffer(DAddr device_addr, u32 size, bool sparse_compatible) {
- if (device_addr == 0) {
+ const u64 page = device_addr >> CACHING_PAGEBITS;
+ if (device_addr == 0 || page >= page_table.size()) {
return NULL_BUFFER_ID;
}
- const u64 page = device_addr >> CACHING_PAGEBITS;
const BufferId buffer_id = page_table[page];
if (buffer_id) {
Buffer& buffer = slot_buffers[buffer_id];
@@ -1927,6 +2169,11 @@ void BufferCache<P>::MappedUploadMemory([[maybe_unused]] Buffer& buffer,
u8* const src_pointer = staging_pointer.data() + copy.src_offset;
const DAddr device_addr = buffer.CpuAddr() + copy.dst_offset;
device_memory.ReadBlockUnsafe(device_addr, src_pointer, copy.size);
+ // Large uploads are mostly geometry the host GPU now has its own copy of: TickFrame
+ // marks their guest memory cold (see TextureCache::RefreshContents).
+ if (copy.size >= 256_KiB) {
+ uploaded_buffer_ranges.emplace_back(device_addr, copy.size);
+ }
// Apply the staging offset
copy.src_offset += upload_staging.offset;
}
diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h
index d8a61b7913..1f0c6cf10f 100644
--- a/src/video_core/buffer_cache/buffer_cache_base.h
+++ b/src/video_core/buffer_cache/buffer_cache_base.h
@@ -14,7 +14,9 @@
#include <memory>
#include <mutex>
#include <numeric>
+#include <optional>
#include <span>
+#include <unordered_map>
#include <vector>
#include "common/container/unordered_map.h"
@@ -321,6 +323,28 @@ public:
bool DMAClear(GPUVAddr src_address, u64 amount, u32 value);
+ /// Switch 2 GPU-written command lists (nx2.h, g_nx2_command_list_pages). While collecting (a
+ /// compute dispatch or a DMA copy being recorded), writes to command list pages are noted;
+ /// Nx2CopyListPages then records copies of those pages into host memory after the work.
+ void Nx2CollectListCopies(bool collect) {
+ nx2_collect_list_copies = collect;
+ if (!collect) {
+ nx2_list_copy_requests.clear();
+ }
+ }
+ /// True when it recorded copies, which the GPU completes at `tick` (the caller submits).
+ bool Nx2CopyListPages(u64 tick);
+ enum class Nx2ListRead { Unknown, Copied, Synced };
+ /// Front thread: Synced when guest memory holds the page `device_addr` lies in (flushed and
+ /// written by no GPU work since), else Copied with `out` filled from the copy of its last
+ /// write once the GPU is done with it. Unknown otherwise: the caller drains and flushes.
+ Nx2ListRead Nx2ReadListCopy(DAddr device_addr, std::span<u8> out);
+ /// Front thread, the split drained: the command list page was flushed to guest memory.
+ void Nx2NoteListSynced(DAddr page);
+ /// CPU and inline writes: they reach guest memory, so a synced page stays synced, but its copy
+ /// no longer holds the page.
+ void Nx2NoteListHostWrite(DAddr device_addr, u64 size);
+
/// Return true when a CPU region is modified from the GPU
[[nodiscard]] bool IsRegionGpuModified(DAddr addr, size_t size);
@@ -440,6 +464,10 @@ private:
void MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u32 size, bool needs_sync = false);
+ /// Counts a GPU write to the command list pages in the range (invalidating their copies) and,
+ /// while collecting, asks for new copies.
+ void Nx2NoteListWrite(BufferId buffer_id, DAddr device_addr, u64 size);
+
[[nodiscard]] BufferId FindBuffer(DAddr device_addr, u32 size, bool sparse_compatible);
void SynchronizeBufferWrites(Buffer& buffer);
@@ -580,6 +608,8 @@ private:
size_t immediate_buffer_capacity = 0;
Common::ScratchBuffer<u8> immediate_buffer_alloc;
+ /// Guest memory large uploads came from since the last cold hint (device address, size).
+ std::vector<std::pair<DAddr, u64>> uploaded_buffer_ranges;
struct LRUItemParams {
using ObjectType = BufferId;
@@ -602,6 +632,29 @@ private:
// Covers the device address space (MaxwellDeviceTraits::device_virtual_bits).
std::array<BufferId, ((1ULL << 36) >> CACHING_PAGEBITS)> page_table;
Common::ScratchBuffer<u8> tmp_buffer;
+
+ /// A command list page (Nx2CopyListPages): the GPU writes noted to it and the last copy.
+ using Nx2Staging = decltype(std::declval<Runtime&>().DownloadStagingBuffer(size_t{}, true));
+ struct Nx2ListPage {
+ u64 writes{}; ///< GPU writes noted to the page
+ u64 copy_writes{}; ///< `writes` when the copy was made: the copy is current while equal
+ u64 copy_tick{}; ///< GPU tick that completes the copy
+ /// `writes` when the front last flushed the page: guest memory holds it while equal
+ u64 synced_writes{~u64{0}};
+ u32 copy_begin{}; ///< Bytes of the page the copy holds (a buffer can end inside it)
+ u32 copy_end{};
+ /// Written outside a compute dispatch or a DMA copy (a draw's storage writes) at least
+ /// once: the front can't tell from what it sent whether such a write is still queued, so
+ /// the page's lists always drain the split instead of reading a copy.
+ bool draw_written{};
+ std::optional<Nx2Staging> copy;
+ };
+ std::mutex nx2_list_mutex; ///< nx2_list_pages: the split's front reads the copies
+ std::unordered_map<DAddr, Nx2ListPage> nx2_list_pages;
+ /// Copies replaced while the GPU may still write them: freed once it is past their tick.
+ std::vector<std::pair<u64, Nx2Staging>> nx2_retired_list_copies;
+ boost::container::small_vector<std::pair<BufferId, DAddr>, 8> nx2_list_copy_requests;
+ bool nx2_collect_list_copies{};
};
} // namespace VideoCommon