diff options
Diffstat (limited to 'src/video_core/buffer_cache')
| -rw-r--r-- | src/video_core/buffer_cache/buffer_cache.h | 73 | ||||
| -rw-r--r-- | src/video_core/buffer_cache/buffer_cache_base.h | 11 | ||||
| -rw-r--r-- | src/video_core/buffer_cache/uniform_stream_reuse.h | 121 |
3 files changed, 199 insertions, 6 deletions
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index cd176dec27..40369b16ef 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -10,11 +10,13 @@ #include <memory> #include <numeric> #include <stdexcept> +#include <utility> #include <vector> #include <fmt/ranges.h> #include "common/range_sets.inc" #include "video_core/buffer_cache/buffer_cache_base.h" +#include "video_core/buffer_cache/uniform_stream_reuse.h" #include "video_core/guest_memory.h" #include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/nx2.h" @@ -28,6 +30,9 @@ using Core::DEVICE_PAGESIZE; template <class P> BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_) : runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory} { + // A new runtime has a new scheduler (its ticks restart) and a new stream buffer: whatever the + // stream uniform reuse remembers refers to the old ones. Before the early return below. + VideoCommon::ResetUniformStreamReuse<Runtime>(); // Ensure the first slot is used for the null buffer void(slot_buffers.insert(runtime, NullBufferParams{})); gpu_modified_ranges.Clear(); @@ -180,6 +185,7 @@ void BufferCache<P>::WriteMemory(DAddr device_addr, u64 size) { gpu_modified_ranges.Subtract(device_addr, size); } memory_tracker.MarkRegionAsCpuModified(device_addr, size); + MarkVertexBuffersStale(device_addr, size); } template <class P> @@ -230,8 +236,17 @@ std::optional<VideoCore::RasterizerDownloadArea> BufferCache<P>::GetFlushArea(DA }; area->preemtive = !IsRegionGpuModified(device_addr_start_aligned, device_addr_end_aligned - device_addr_start_aligned); - memory_tracker.MarkRegionAsPreflushable(device_addr_start_aligned, - device_addr_end_aligned - device_addr_start_aligned); + // Mark only data that the GPU wrote. A mark stays for the session and sends later GPU writes + // in the range to a download at each fence. + if (!area->preemtive) { + memory_tracker.MarkRegionAsPreflushable(device_addr_start_aligned, + device_addr_end_aligned - device_addr_start_aligned); + } else { + // The core keeps this area and has no mark. The next GPU write here must arm the range + // again, which ends the area and sets the trap. + read_trapped_ranges.Subtract(device_addr_start_aligned, + device_addr_end_aligned - device_addr_start_aligned); + } return area; } @@ -265,6 +280,7 @@ bool BufferCache<P>::DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 am } ClearDownload(*cpu_dest_address, amount); + MarkVertexBuffersStale(*cpu_dest_address, amount); BufferId buffer_a; BufferId buffer_b; @@ -408,7 +424,8 @@ void BufferCache<P>::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr template <class P> void BufferCache<P>::DisableGraphicsUniformBuffer(size_t stage, u32 index) { auto& current = channel_state->uniform_buffers[stage][index]; - if (current.device_addr == 0 && current.size == 0 && !current.buffer_id) [[likely]] { + if (current.device_addr == 0 && current.size == 0 && current.buffer_id == NULL_BUFFER_ID) + [[likely]] { return; } current = NULL_BINDING; @@ -893,7 +910,39 @@ void BufferCache<P>::UpdateVertexBufferSlot(u32 index, const Binding& binding) { } template <class P> +void BufferCache<P>::MarkVertexBuffersStale(DAddr device_addr, u64 size) { + if constexpr (requires { runtime.IsVertexRepacked(u32{}); }) { + u32 mask = enabled_vertex_buffers_mask; + while (mask != 0) { + const u32 index = std::countr_zero(mask); + mask &= mask - 1; + const Binding& binding = v_buffer[index]; + if (device_addr < binding.device_addr + binding.size && + binding.device_addr < device_addr + size) { + stale_vertex_buffers_mask |= 1U << index; + } + } + } +} + +template <class P> +void BufferCache<P>::RefreshRepackedVertexBuffers() { + if constexpr (requires { runtime.IsVertexRepacked(u32{}); }) { + u32 mask = std::exchange(stale_vertex_buffers_mask, 0) & enabled_vertex_buffers_mask; + auto& flags = maxwell3d->dirty.flags; + while (mask != 0) { + const u32 index = std::countr_zero(mask); + mask &= mask - 1; + if (runtime.IsVertexRepacked(index)) { + flags[Dirty::VertexBuffer0 + index] = true; + } + } + } +} + +template <class P> void BufferCache<P>::BindHostVertexBuffers() { + RefreshRepackedVertexBuffers(); #ifdef __ANDROID__ const bool use_optimized_vertex_buffers = Settings::values.use_optimized_vertex_buffers.GetValue(); @@ -1078,8 +1127,20 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32 channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index; channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size; // Stream buffer path to avoid stalling on non-Nvidia drivers or Vulkan - const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size); - device_memory.ReadBlockUnsafe(device_addr, span.data(), size); + bool streamed = false; + if constexpr (!IS_OPENGL && VideoCommon::StreamUniformReusable<Runtime>) { + // The same bytes as this slot's previous bind, in the same command buffer: bind that + // stream allocation again instead of allocating and copying (uniform_stream_reuse.h). + // EDEN_NX2_UBOREUSE=0 turns it off. + if (VideoCommon::UniformStreamReuseEnabled()) { + streamed = VideoCommon::BindSmallUniformReusing( + runtime, device_memory, stage, index, binding_index, device_addr, size); + } + } + if (!streamed) { + const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size); + device_memory.ReadBlockUnsafe(device_addr, span.data(), size); + } return; } // Classic cached path @@ -1655,6 +1716,7 @@ void BufferCache<P>::MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u3 } } memory_tracker.MarkRegionAsGpuModified(device_addr, size); + MarkVertexBuffersStale(device_addr, size); gpu_modified_ranges.Add(device_addr, size); if (DownloadsAtFence(device_addr, size)) { uncommitted_gpu_modified_ranges.Add(device_addr, size); @@ -2205,6 +2267,7 @@ void BufferCache<P>::InlineMemoryImplementation(DAddr dest_address, size_t copy_ std::span<const u8> inlined_buffer) { ClearDownload(dest_address, copy_size); gpu_modified_ranges.Subtract(dest_address, copy_size); + MarkVertexBuffersStale(dest_address, copy_size); BufferId buffer_id = FindBuffer(dest_address, static_cast<u32>(copy_size), false); auto& buffer = slot_buffers[buffer_id]; diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 1f0c6cf10f..86c05c8f22 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -59,7 +59,8 @@ constexpr u32 NUM_VERTEX_BUFFERS = 32; constexpr u32 NUM_TRANSFORM_FEEDBACK_BUFFERS = 4; constexpr u32 NUM_GRAPHICS_UNIFORM_BUFFERS = 18; constexpr u32 NUM_COMPUTE_UNIFORM_BUFFERS = 8; -constexpr u32 NUM_STORAGE_BUFFERS = 16; +// Native compute shaders bind up to 18 global memory ranges; the enable masks are 32 bits wide. +constexpr u32 NUM_STORAGE_BUFFERS = 32; constexpr u32 NUM_TEXTURE_BUFFERS = 32; constexpr u32 NUM_STAGES = 5; @@ -525,6 +526,13 @@ private: void UpdateVertexBufferSlot(u32 index, const Binding& binding); + /// Flags the vertex buffers overlapping a written range so host copies derived from them are + /// rebuilt. + void MarkVertexBuffersStale(DAddr device_addr, u64 size); + + /// Re-dirties repacked vertex buffers whose source data was written since they were bound. + void RefreshRepackedVertexBuffers(); + void ClearDownload(DAddr base_addr, u64 size); void InlineMemoryImplementation(DAddr dest_address, size_t copy_size, @@ -545,6 +553,7 @@ private: u32 last_index_count = 0; u32 enabled_vertex_buffers_mask = 0; + u32 stale_vertex_buffers_mask = 0; u64 vertex_buffers_serial = 0; std::array<Binding, 32> v_buffer{}; diff --git a/src/video_core/buffer_cache/uniform_stream_reuse.h b/src/video_core/buffer_cache/uniform_stream_reuse.h new file mode 100644 index 0000000000..7ba31c4717 --- /dev/null +++ b/src/video_core/buffer_cache/uniform_stream_reuse.h @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> +#include <concepts> +#include <cstddef> +#include <cstdlib> +#include <cstring> +#include <span> +#include <type_traits> + +#include "common/common_types.h" + +namespace VideoCommon { + +/// What a buffer cache runtime offers for stream uniform reuse. +/// - StreamUniformRef: a small trivially copyable description of one stream allocation (the +/// host buffer, its offset and whatever else the runtime needs to add it to a descriptor +/// payload again, e.g. the device address of the descriptor buffer path). +/// - LastStreamUniformRef(): the allocation made by the latest BindMappedUniformBuffer call. +/// - RebindStreamUniform(ref, size): adds `ref` to the descriptor payload exactly as +/// BindMappedUniformBuffer added the allocation the first time. +template <class R> +concept StreamUniformReusable = requires(R& runtime, u32 size) { + typename R::StreamUniformRef; + requires std::is_trivially_copyable_v<typename R::StreamUniformRef>; + { runtime.LastStreamUniformRef() } -> std::same_as<typename R::StreamUniformRef>; + runtime.RebindStreamUniform(runtime.LastStreamUniformRef(), size); + { runtime.CurrentTick() } -> std::convertible_to<u64>; +}; + +/// EDEN_NX2_UBOREUSE=0 turns the reuse off (every small uniform buffer is copied, as before). +[[nodiscard]] inline bool UniformStreamReuseEnabled() noexcept { + static const bool enabled = [] { + const char* const value = std::getenv("EDEN_NX2_UBOREUSE"); + return value == nullptr || value[0] != '0'; + }(); + return enabled; +} + +template <class Ref> +class UniformStreamReuse { +public: + static constexpr u32 MaxBytes = 4096; // BufferCache's DEFAULT_SKIP_CACHE_SIZE + static constexpr size_t NumStages = 5; + static constexpr size_t NumSlots = 18; // Maxwell constant buffers per stage + + struct Slot { + Ref ref{}; + u64 tick{}; + u32 size{}; // 0: nothing streamed yet + std::array<u8, MaxBytes> bytes{}; + }; + + void Reset() noexcept { + for (auto& stage : slots) { + for (Slot& slot : stage) { + slot.size = 0; + slot.tick = 0; + } + } + } + + std::array<std::array<Slot, NumSlots>, NumStages> slots{}; + std::array<u8, MaxBytes> scratch{}; +}; + +template <class Ref> +[[nodiscard]] inline UniformStreamReuse<Ref>& UniformStreamReuseState() noexcept { + static UniformStreamReuse<Ref> state; + return state; +} + +/// Forgets every slot. Call it when a buffer cache (and so its runtime, scheduler and stream +/// buffer) is created: ticks restart with a new scheduler and the old references are dead. +template <class Runtime> +inline void ResetUniformStreamReuse() noexcept { + if constexpr (StreamUniformReusable<Runtime>) { + UniformStreamReuseState<typename Runtime::StreamUniformRef>().Reset(); + } +} + +/// Binds the small uniform buffer at `device_addr` for (stage, cbuf `index`) through the stream +/// buffer, reusing the slot's previous allocation when the bytes and the command buffer are the +/// same. Returns false (nothing bound) when the buffer does not qualify, so the caller binds it +/// the classic way. +template <class Runtime, class DeviceMemory> +[[nodiscard]] bool BindSmallUniformReusing(Runtime& runtime, DeviceMemory& device_memory, + size_t stage, u32 index, u32 binding_index, + u64 device_addr, u32 size) { + using Reuse = UniformStreamReuse<typename Runtime::StreamUniformRef>; + if (size == 0 || size > Reuse::MaxBytes || stage >= Reuse::NumStages || + index >= Reuse::NumSlots) { + return false; + } + Reuse& reuse = UniformStreamReuseState<typename Runtime::StreamUniformRef>(); + typename Reuse::Slot& slot = reuse.slots[stage][index]; + u8* const scratch = reuse.scratch.data(); + device_memory.ReadBlockUnsafe(device_addr, scratch, size); + + const bool same_bytes = + slot.size == size && std::memcmp(slot.bytes.data(), scratch, size) == 0; + if (same_bytes && slot.tick == runtime.CurrentTick()) { + runtime.RebindStreamUniform(slot.ref, size); + return true; + } + const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size); + std::memcpy(span.data(), scratch, size); + slot.ref = runtime.LastStreamUniformRef(); + // After the allocation: the tick the stream region is tagged with is the one current now. + slot.tick = runtime.CurrentTick(); + if (!same_bytes) { + slot.size = size; + std::memcpy(slot.bytes.data(), scratch, size); + } + return true; +} + +} // namespace VideoCommon |
