// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include #include #include #include #include #include #include #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 concept StreamUniformReusable = requires(R& runtime, u32 size) { typename R::StreamUniformRef; requires std::is_trivially_copyable_v; { runtime.LastStreamUniformRef() } -> std::same_as; runtime.RebindStreamUniform(runtime.LastStreamUniformRef(), size); { runtime.CurrentTick() } -> std::convertible_to; }; /// 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 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 bytes{}; }; void Reset() noexcept { for (auto& stage : slots) { for (Slot& slot : stage) { slot.size = 0; slot.tick = 0; } } } std::array, NumStages> slots{}; std::array scratch{}; }; template [[nodiscard]] inline UniformStreamReuse& UniformStreamReuseState() noexcept { static UniformStreamReuse 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 inline void ResetUniformStreamReuse() noexcept { if constexpr (StreamUniformReusable) { UniformStreamReuseState().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 [[nodiscard]] bool BindSmallUniformReusing(Runtime& runtime, DeviceMemory& device_memory, size_t stage, u32 index, u32 binding_index, u64 device_addr, u32 size) { using Reuse = UniformStreamReuse; if (size == 0 || size > Reuse::MaxBytes || stage >= Reuse::NumStages || index >= Reuse::NumSlots) { return false; } Reuse& reuse = UniformStreamReuseState(); 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 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