diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt index 4b3648d4e3..2f1212ee21 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt @@ -31,6 +31,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { ENABLE_BUFFER_HISTORY("enable_buffer_history"), USE_OPTIMIZED_VERTEX_BUFFERS("use_optimized_vertex_buffers"), ENABLE_GPU_BUFFER_READBACK("enable_gpu_buffer_readback"), + USE_UNIFIED_MEMORY("use_unified_memory"), SYNC_MEMORY_OPERATIONS("sync_memory_operations"), BUFFER_REORDER_DISABLE("disable_buffer_reorder"), RENDERER_DEBUG("debug"), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index d04ce0567f..7ce309d764 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -874,6 +874,13 @@ abstract class SettingsItem( descriptionId = R.string.enable_gpu_buffer_readback_description ) ) + put( + SwitchSetting( + BooleanSetting.USE_UNIFIED_MEMORY, + titleId = R.string.use_unified_memory, + descriptionId = R.string.use_unified_memory_description + ) + ) put( SwitchSetting( BooleanSetting.USE_OPTIMIZED_VERTEX_BUFFERS, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 060d0dae3b..74c7fc41cf 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -551,6 +551,7 @@ class SettingsFragmentPresenter( add(BooleanSetting.RENDERER_BARRIER_FEEDBACK_LOOPS.key) add(BooleanSetting.ENABLE_BUFFER_HISTORY.key) add(BooleanSetting.ENABLE_GPU_BUFFER_READBACK.key) + add(BooleanSetting.USE_UNIFIED_MEMORY.key) add(BooleanSetting.USE_OPTIMIZED_VERTEX_BUFFERS.key) add(HeaderSetting(R.string.hacks)) diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 3453e22190..b24cbceb18 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -587,6 +587,8 @@ Enable buffer history Enables access to previous buffer states. This option may improve rendering quality and performance consistency in some games. Enable GPU Buffer Readback + Unified Memory + Backs part of the emulated memory with GPU-shared buffers so textures are unswizzled directly from it, skipping a CPU copy. Needs 12 GB of RAM or more and a GPU with I/O coherency; it turns itself off otherwise. Takes effect on the next game launch. Preserves GPU-modified buffer data by reading it back before uploads. Some games require this to render certain effects properly. May cause issues if the hardware cannot handle the additional workload. Optimized Vertex Buffers Enables optimized vertex buffer binding for improved performance. Requires Mesa 26.0+ Turnip drivers/ QCOM drivers. Will crash on older Turnip drivers (25.3 and below). diff --git a/src/common/heap_tracker.cpp b/src/common/heap_tracker.cpp index b72abb16d8..1fa1514da7 100644 --- a/src/common/heap_tracker.cpp +++ b/src/common/heap_tracker.cpp @@ -4,34 +4,17 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include #include "common/heap_tracker.h" #include "common/logging.h" +#include "common/memory_detect.h" #include "common/assert.h" namespace Common { -namespace { - -s64 GetMaxPermissibleResidentMapCount() { - // Default value. - s64 value = 65530; - - // Try to read how many mappings we can make. - std::ifstream s("/proc/sys/vm/max_map_count"); - s >> value; - - // Print, for debug. - LOG_INFO(HW_Memory, "Current maximum map count: {}", value); - - // Allow 20000 maps for other code and to account for split inaccuracy. - return std::max(value - 20000, 0); -} - -} // namespace - HeapTracker::HeapTracker(Common::HostMemory& buffer) - : m_buffer(buffer), m_max_resident_map_count(GetMaxPermissibleResidentMapCount()) {} + : m_buffer(buffer), + m_has_hardware_buffer_backing(!buffer.BackingHardwareBuffers().empty()), + m_max_resident_map_count(static_cast(GetPermissibleMapCount())) {} HeapTracker::~HeapTracker() = default; void HeapTracker::Map(size_t virtual_offset, size_t host_offset, size_t length, @@ -85,7 +68,8 @@ void HeapTracker::Unmap(size_t virtual_offset, size_t size, bool is_separate_hea // If resident, erase from resident map. if (item->is_resident) { - ASSERT(--m_resident_map_count >= 0); + m_resident_map_count -= this->HostMapCount(item->paddr, item->size); + ASSERT(m_resident_map_count >= 0); m_resident_mappings.erase(m_resident_mappings.iterator_to(*item)); } @@ -191,7 +175,7 @@ bool HeapTracker::DeferredMapSeparateHeap(size_t virtual_offset) { // This map is now resident. it->is_resident = true; - m_resident_map_count++; + m_resident_map_count += this->HostMapCount(it->paddr, it->size); m_resident_mappings.insert(*it); } @@ -213,17 +197,17 @@ void HeapTracker::RebuildSeparateHeapAddressSpace() { // Despite being worse in theory, this has proven to be better in practice than more // regularly dumping a smaller amount, because it significantly reduces average case // lock contention. - std::size_t const desired_count = (std::min)(m_resident_map_count, m_max_resident_map_count) / 2; - std::size_t const evict_count = m_resident_map_count - desired_count; + s64 const desired_count = (std::min)(m_resident_map_count, m_max_resident_map_count) / 2; auto it = m_resident_mappings.begin(); - for (size_t i = 0; i < evict_count && it != m_resident_mappings.end(); i++) { + while (m_resident_map_count > desired_count && it != m_resident_mappings.end()) { // Unmark and unmap. it->is_resident = false; m_buffer.Unmap(it->vaddr, it->size, false); // Advance. - ASSERT(--m_resident_map_count >= 0); + m_resident_map_count -= this->HostMapCount(it->paddr, it->size); + ASSERT(m_resident_map_count >= 0); it = m_resident_mappings.erase(it); } } @@ -245,6 +229,7 @@ void HeapTracker::SplitHeapMapLocked(VAddr offset) { // Cache the original values. auto* const left = std::addressof(*it); const size_t orig_size = left->size; + const s64 orig_host_map_count = this->HostMapCount(left->paddr, orig_size); // Adjust the left map. const size_t left_size = offset - left->vaddr; @@ -266,11 +251,20 @@ void HeapTracker::SplitHeapMapLocked(VAddr offset) { // If resident, also insert into resident map. if (right->is_resident) { - m_resident_map_count++; + m_resident_map_count += this->HostMapCount(left->paddr, left->size) + + this->HostMapCount(right->paddr, right->size) - + orig_host_map_count; m_resident_mappings.insert(*right); } } +s64 HeapTracker::HostMapCount(PAddr paddr, size_t size) const { + if (!m_has_hardware_buffer_backing) { + return static_cast(size != 0); + } + return static_cast(m_buffer.BackingMapCount(paddr, size)); +} + HeapTracker::AddrTree::iterator HeapTracker::GetNearestHeapMapLocked(VAddr offset) { const SeparateHeapMap key{ .vaddr = offset, diff --git a/src/common/heap_tracker.h b/src/common/heap_tracker.h index ee5b0bf43a..8be66a1d4d 100644 --- a/src/common/heap_tracker.h +++ b/src/common/heap_tracker.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -82,10 +85,13 @@ private: AddrTree::iterator GetNearestHeapMapLocked(VAddr offset); + s64 HostMapCount(PAddr paddr, size_t size) const; + void RebuildSeparateHeapAddressSpace(); private: Common::HostMemory& m_buffer; + const bool m_has_hardware_buffer_backing; const s64 m_max_resident_map_count; std::shared_mutex m_rebuild_lock{}; diff --git a/src/common/host_memory.cpp b/src/common/host_memory.cpp index dd0780a283..65e73eb5a2 100644 --- a/src/common/host_memory.cpp +++ b/src/common/host_memory.cpp @@ -53,12 +53,42 @@ #include #include +#include #include "common/alignment.h" #include "common/assert.h" #include "common/free_region_manager.h" #include "common/host_memory.h" #include "common/logging.h" +#include "common/memory_detect.h" +#include "common/settings.h" + +#ifdef __ANDROID__ +#include +#include + +namespace { + +struct NativeHandle { + int version; + int numFds; + int numInts; + int data[1]; +}; + +using PFN_AHardwareBuffer_getNativeHandle = const NativeHandle* (*)(const AHardwareBuffer*); + +PFN_AHardwareBuffer_getNativeHandle ResolveGetNativeHandle() { + void* const lib = dlopen("libnativewindow.so", RTLD_NOW); + if (lib == nullptr) { + return nullptr; + } + return reinterpret_cast( + dlsym(lib, "AHardwareBuffer_getNativeHandle")); +} + +} +#endif #if defined(__ANDROID__) && __ANDROID_API__ < 30 #include @@ -122,7 +152,7 @@ static void GetFuncAddress(Common::DynamicLibrary& dll, const char* name, T& pfn class HostMemory::Impl { public: - explicit Impl(size_t backing_size_, size_t virtual_size_) + explicit Impl(size_t backing_size_, size_t virtual_size_, size_t) : backing_size{backing_size_} , virtual_size{virtual_size_} , process{GetCurrentProcess()} @@ -500,9 +530,10 @@ static int shm_open_anon(int flags, mode_t mode) { class HostMemory::Impl { public: - explicit Impl(size_t backing_size_, size_t virtual_size_) + explicit Impl(size_t backing_size_, size_t virtual_size_, size_t preferred_offset_) : backing_size{backing_size_} , virtual_size{virtual_size_} + , preferred_offset{preferred_offset_} {} bool Init() { @@ -542,10 +573,15 @@ public: LOG_WARNING(Common_Memory, "Using private mappings instead of shared ones"); backing_base = static_cast(mmap(nullptr, backing_size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)); if (fd > 0) { - fd = -1; close(fd); } + fd = -1; } else { +#ifdef __ANDROID__ + if (InitAhbBacking()) { + return InitVirtual(); + } +#endif backing_base = static_cast(mmap(nullptr, backing_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)); } if (backing_base == MAP_FAILED) { @@ -553,7 +589,10 @@ public: return false; } - // Virtual memory initialization + return InitVirtual(); + } + + bool InitVirtual() { virtual_base = virtual_map_base = static_cast(ChooseVirtualBase(virtual_size)); if (virtual_base == MAP_FAILED) { LOG_CRITICAL(HW_Memory, "mmap failed: {}", strerror(errno)); @@ -566,6 +605,240 @@ public: return true; } +#ifdef __ANDROID__ + static AHardwareBuffer_Desc MakeBlobDesc(size_t len) { + return AHardwareBuffer_Desc{ + .width = static_cast(len), + .height = 1, + .layers = 1, + .format = AHARDWAREBUFFER_FORMAT_BLOB, + .usage = AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | + AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | + AHARDWAREBUFFER_USAGE_GPU_DATA_BUFFER, + .stride = 0, + .rfu0 = 0, + .rfu1 = 0, + }; + } + + static bool ProbeAhbBacking(PFN_AHardwareBuffer_getNativeHandle get_native_handle) { + const AHardwareBuffer_Desc desc = MakeBlobDesc(PageAlignment * 2); + AHardwareBuffer* buffer{}; + if (AHardwareBuffer_allocate(&desc, &buffer) != 0 || buffer == nullptr) { + return false; + } + const NativeHandle* const handle = get_native_handle(buffer); + if (handle == nullptr || handle->numFds < 1) { + AHardwareBuffer_release(buffer); + return false; + } + const int probe_fd = handle->data[0]; + bool ok = true; + const auto try_map = [&](int prot, off_t offset) { + if (!ok) { + return; + } + void* const ptr = mmap(nullptr, PageAlignment, prot, MAP_SHARED, probe_fd, offset); + if (ptr == MAP_FAILED) { + ok = false; + return; + } + munmap(ptr, PageAlignment); + }; + try_map(PROT_READ | PROT_WRITE, 0); + try_map(PROT_READ | PROT_WRITE, static_cast(PageAlignment)); +#ifdef ARCHITECTURE_arm64 + try_map(PROT_READ | PROT_EXEC, 0); +#endif + AHardwareBuffer_release(buffer); + return ok; + } + + size_t ComputeAhbBudget(size_t window_size) const { + const u64 total_physical = Common::GetMemInfo().TotalPhysicalMemory; + constexpr u64 BaselineFootprint = 6ULL << 30; + if (total_physical <= BaselineFootprint) { + return 0; + } + const u64 permissible_maps = Common::GetPermissibleMapCount(); + if (permissible_maps == 0) { + return 0; + } + u64 budget = (total_physical - BaselineFootprint) / 2; + constexpr u64 MapSlotsPerWindow = 64; + const u64 affordable_windows = permissible_maps / MapSlotsPerWindow; + budget = (std::min)(budget, affordable_windows * window_size); + const u64 available = Common::GetAvailablePhysicalMemory(); + if (available != 0) { + budget = (std::min)(budget, available / 2); + } + budget = (std::min)(budget, static_cast(backing_size)); + budget = Common::AlignDown(budget, window_size); + constexpr u64 MinimumBudget = 256ULL << 20; + if (budget < MinimumBudget) { + return 0; + } + return static_cast(budget); + } + + bool InitAhbBacking() { + if (!Settings::values.use_unified_memory.GetValue()) { + return false; + } + static const PFN_AHardwareBuffer_getNativeHandle get_native_handle = + ResolveGetNativeHandle(); + if (get_native_handle == nullptr) { + return false; + } + constexpr size_t window_size = 256ULL << 20; + const size_t budget = ComputeAhbBudget(window_size); + if (budget == 0) { + return false; + } + if (!ProbeAhbBacking(get_native_handle)) { + return false; + } + const size_t aligned_backing = Common::AlignDown(backing_size, window_size); + const size_t max_windows = (std::min)(budget, aligned_backing) / window_size; + + std::vector buffers; + std::vector buffer_fds; + const auto cleanup = [&] { + for (AHardwareBuffer* buffer : buffers) { + AHardwareBuffer_release(buffer); + } + buffers.clear(); + buffer_fds.clear(); + }; + for (size_t i = 0; i < max_windows; ++i) { + const AHardwareBuffer_Desc desc = MakeBlobDesc(window_size); + AHardwareBuffer* buffer{}; + if (AHardwareBuffer_allocate(&desc, &buffer) != 0 || buffer == nullptr) { + break; + } + const NativeHandle* const handle = get_native_handle(buffer); + if (handle == nullptr || handle->numFds < 1) { + AHardwareBuffer_release(buffer); + break; + } + const int buffer_fd = handle->data[0]; + const off_t buffer_len = lseek(buffer_fd, 0, SEEK_END); + if (buffer_len < static_cast(window_size)) { + AHardwareBuffer_release(buffer); + break; + } + buffers.push_back(buffer); + buffer_fds.push_back(buffer_fd); + } + const size_t num_windows = buffers.size(); + if (num_windows == 0) { + return false; + } + const size_t region_size = num_windows * window_size; + const size_t region_base = Common::AlignDown( + (std::min)(preferred_offset, aligned_backing - region_size), window_size); + u8* const base = static_cast(mmap(nullptr, backing_size, PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0)); + if (base == MAP_FAILED) { + cleanup(); + return false; + } + const auto map_over_reservation = [&](size_t offset, size_t len, int map_fd, + off_t map_offset) { + if (len == 0) { + return true; + } + if (mmap(base + offset, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, map_fd, + map_offset) == MAP_FAILED) { + munmap(base, backing_size); + cleanup(); + return false; + } + return true; + }; + if (!map_over_reservation(0, region_base, fd, 0)) { + return false; + } + for (size_t i = 0; i < num_windows; ++i) { + if (!map_over_reservation(region_base + i * window_size, window_size, buffer_fds[i], + 0)) { + return false; + } + } + const size_t tail_offset = region_base + region_size; + if (!map_over_reservation(tail_offset, backing_size - tail_offset, fd, + static_cast(tail_offset))) { + return false; + } + backing_base = base; + ahb_windows = std::move(buffers); + ahb_fds = std::move(buffer_fds); + ahb_window_size = window_size; + ahb_base = region_base; + ahb_bytes = region_size; + return true; + } + + void MapBackingRange(size_t virtual_offset, size_t host_offset, size_t length, int prot_flags) { + while (length > 0) { + int map_fd = fd; + off_t map_offset = static_cast(host_offset); + size_t chunk = length; + if (host_offset < ahb_base) { + chunk = (std::min)(chunk, ahb_base - host_offset); + } else if (host_offset < ahb_base + ahb_bytes) { + const size_t relative = host_offset - ahb_base; + const size_t window = relative / ahb_window_size; + const size_t local = relative % ahb_window_size; + map_fd = ahb_fds[window]; + map_offset = static_cast(local); + chunk = (std::min)(chunk, ahb_window_size - local); + } + void* const ret = mmap(virtual_base + virtual_offset, chunk, prot_flags, + MAP_SHARED | MAP_FIXED, map_fd, map_offset); + ASSERT_MSG(ret != MAP_FAILED, "mmap: {}", strerror(errno)); + virtual_offset += chunk; + host_offset += chunk; + length -= chunk; + } + } + + size_t BackingMapCount(size_t host_offset, size_t length) const noexcept { + if (length == 0) { + return 0; + } + if (ahb_bytes == 0) { + return 1; + } + size_t count = 0; + while (length > 0) { + size_t chunk = length; + if (host_offset < ahb_base) { + chunk = (std::min)(chunk, ahb_base - host_offset); + } else if (host_offset < ahb_base + ahb_bytes) { + const size_t local = (host_offset - ahb_base) % ahb_window_size; + chunk = (std::min)(chunk, ahb_window_size - local); + } + host_offset += chunk; + length -= chunk; + ++count; + } + return count; + } + + std::span AhbWindows() const noexcept { + return ahb_windows; + } + + size_t AhbWindowSize() const noexcept { + return ahb_window_size; + } + + size_t AhbBase() const noexcept { + return ahb_base; + } +#endif + ~Impl() { Release(); } @@ -586,6 +859,12 @@ public: #ifdef ARCHITECTURE_arm64 if (True(perms & MemoryPermission::Execute)) prot_flags |= PROT_EXEC; +#endif +#ifdef __ANDROID__ + if (ahb_bytes != 0) { + MapBackingRange(virtual_offset, host_offset, length, prot_flags); + return; + } #endif int flags = (fd >= 0 ? MAP_SHARED : MAP_PRIVATE) | MAP_FIXED; void* ret = mmap(virtual_base + virtual_offset, length, prot_flags, flags, fd, host_offset); @@ -633,6 +912,7 @@ public: const size_t backing_size; ///< Size of the backing memory in bytes const size_t virtual_size; ///< Size of the virtual address placeholder in bytes + const size_t preferred_offset; u8* backing_base{reinterpret_cast(MAP_FAILED)}; u8* virtual_base{reinterpret_cast(MAP_FAILED)}; @@ -655,6 +935,15 @@ private: int ret = close(fd); ASSERT_MSG(ret == 0, "close failed: {}", strerror(errno)); } + +#ifdef __ANDROID__ + for (AHardwareBuffer* buffer : ahb_windows) { + AHardwareBuffer_release(buffer); + } + ahb_windows.clear(); + ahb_fds.clear(); + ahb_bytes = 0; +#endif } void AdjustMap(size_t* virtual_offset, size_t* length) { @@ -680,11 +969,19 @@ private: int fd{-1}; // memfd file descriptor, -1 is the error value of memfd_create FreeRegionManager free_manager{}; + +#ifdef __ANDROID__ + std::vector ahb_windows; + std::vector ahb_fds; + size_t ahb_window_size{}; + size_t ahb_base{}; + size_t ahb_bytes{}; +#endif }; #endif // ^^^ POSIX ^^^ -HostMemory::HostMemory(size_t backing_size_, size_t virtual_size_) +HostMemory::HostMemory(size_t backing_size_, size_t virtual_size_, size_t preferred_offset_) : backing_size(backing_size_) , virtual_size(virtual_size_) { @@ -696,7 +993,9 @@ HostMemory::HostMemory(size_t backing_size_, size_t virtual_size_) #else // Try to allocate a fastmem arena. // The implementation will fail with std::bad_alloc on errors. - impl = std::make_unique(AlignUp(backing_size, PageAlignment), AlignUp(virtual_size, PageAlignment) + HugePageSize); + impl = std::make_unique(AlignUp(backing_size, PageAlignment), + AlignUp(virtual_size, PageAlignment) + HugePageSize, + preferred_offset_); if (impl->Init()) { backing_base = impl->backing_base; virtual_base = impl->virtual_base; @@ -766,6 +1065,42 @@ void HostMemory::ClearBackingRegion(size_t physical_offset, size_t length, u32 f std::memset(backing_base + physical_offset, fill_value, length); } +std::span HostMemory::BackingHardwareBuffers() const noexcept { +#ifdef __ANDROID__ + if (impl) { + return impl->AhbWindows(); + } +#endif + return {}; +} + +size_t HostMemory::BackingMapCount(size_t host_offset, size_t length) const noexcept { +#ifdef __ANDROID__ + if (impl) { + return impl->BackingMapCount(host_offset, length); + } +#endif + return static_cast(length != 0); +} + +size_t HostMemory::BackingHardwareBufferWindowSize() const noexcept { +#ifdef __ANDROID__ + if (impl) { + return impl->AhbWindowSize(); + } +#endif + return 0; +} + +size_t HostMemory::BackingHardwareBufferBase() const noexcept { +#ifdef __ANDROID__ + if (impl) { + return impl->AhbBase(); + } +#endif + return 0; +} + void HostMemory::EnableDirectMappedAddress() { #if !(defined(__OPENORBIS__) || defined(__managarm__)) if (impl) { diff --git a/src/common/host_memory.h b/src/common/host_memory.h index 24f4670732..3299680580 100644 --- a/src/common/host_memory.h +++ b/src/common/host_memory.h @@ -8,10 +8,13 @@ #include #include +#include #include "common/common_funcs.h" #include "common/common_types.h" #include "common/virtual_buffer.h" +struct AHardwareBuffer; + namespace Common { enum class MemoryPermission : u32 { @@ -28,7 +31,7 @@ DECLARE_ENUM_FLAG_OPERATORS(MemoryPermission) */ class HostMemory { public: - explicit HostMemory(size_t backing_size_, size_t virtual_size_); + explicit HostMemory(size_t backing_size_, size_t virtual_size_, size_t preferred_offset_ = 0); ~HostMemory(); /** @@ -62,6 +65,18 @@ public: return backing_base; } + [[nodiscard]] size_t BackingSize() const noexcept { + return backing_size; + } + + [[nodiscard]] size_t BackingMapCount(size_t host_offset, size_t length) const noexcept; + + [[nodiscard]] std::span BackingHardwareBuffers() const noexcept; + + [[nodiscard]] size_t BackingHardwareBufferWindowSize() const noexcept; + + [[nodiscard]] size_t BackingHardwareBufferBase() const noexcept; + [[nodiscard]] u8* VirtualBasePointer() noexcept { return virtual_base; } diff --git a/src/common/memory_detect.cpp b/src/common/memory_detect.cpp index 86a3abcc6d..ae303a4f1a 100644 --- a/src/common/memory_detect.cpp +++ b/src/common/memory_detect.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -17,6 +20,10 @@ #endif #endif +#include +#include +#include + #include "common/memory_detect.h" namespace Common { @@ -69,4 +76,61 @@ const MemoryInfo& GetMemInfo() { return mem_info; } +u64 GetPermissibleMapCount() { + constexpr u64 DefaultMapCount = 65530; + constexpr u64 ReservedMaps = 20000; + u64 count = DefaultMapCount; +#ifdef __linux__ + if (std::FILE* const file = std::fopen("/proc/sys/vm/max_map_count", "re")) { + char line[32]; + if (std::fgets(line, sizeof(line), file) != nullptr) { + const u64 parsed = std::strtoull(line, nullptr, 10); + if (parsed != 0) { + count = parsed; + } + } + std::fclose(file); + } +#endif + if (count <= ReservedMaps) { + return 0; + } + return count - ReservedMaps; +} + +u64 GetAvailablePhysicalMemory() { +#ifdef _WIN32 + MEMORYSTATUSEX memorystatus; + memorystatus.dwLength = sizeof(memorystatus); + if (GlobalMemoryStatusEx(&memorystatus) == 0) { + return 0; + } + return memorystatus.ullAvailPhys; +#elif defined(__linux__) + static constexpr char AvailableKey[] = "MemAvailable:"; + if (std::FILE* const file = std::fopen("/proc/meminfo", "re")) { + char line[256]; + u64 available = 0; + while (std::fgets(line, sizeof(line), file) != nullptr) { + if (std::strncmp(line, AvailableKey, sizeof(AvailableKey) - 1) != 0) { + continue; + } + available = std::strtoull(line + sizeof(AvailableKey) - 1, nullptr, 10) * 1024; + break; + } + std::fclose(file); + if (available != 0) { + return available; + } + } + struct sysinfo meminfo; + if (sysinfo(&meminfo) != 0) { + return 0; + } + return static_cast(meminfo.freeram) * static_cast(meminfo.mem_unit); +#else + return 0; +#endif +} + } // namespace Common diff --git a/src/common/memory_detect.h b/src/common/memory_detect.h index c8f239aed3..367e3f754d 100644 --- a/src/common/memory_detect.h +++ b/src/common/memory_detect.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -18,4 +21,8 @@ struct MemoryInfo { */ [[nodiscard]] const MemoryInfo& GetMemInfo(); +[[nodiscard]] u64 GetPermissibleMapCount(); + +[[nodiscard]] u64 GetAvailablePhysicalMemory(); + } // namespace Common diff --git a/src/common/settings.h b/src/common/settings.h index e564a11cb5..a924007205 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -654,6 +654,9 @@ struct Values { SwitchableSetting use_asynchronous_shaders{linkage, false, "use_asynchronous_shaders", Category::RendererHacks}; + SwitchableSetting use_unified_memory{linkage, false, "use_unified_memory", + Category::RendererHacks}; + SwitchableSetting dyna_state{linkage, #if defined(__ANDROID__) ExtendedDynamicState::Disabled, diff --git a/src/core/core.cpp b/src/core/core.cpp index 046ee53346..10d1c336c4 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -120,6 +120,7 @@ struct System::Impl { is_multicore = Settings::values.use_multi_core.GetValue(); extended_memory_layout = Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_4Gb; + unified_memory = Settings::values.use_unified_memory.GetValue(); core_timing.SetMulticore(is_multicore); core_timing.Initialize([&system]() { system.RegisterHostThread(); }); @@ -147,7 +148,8 @@ struct System::Impl { !device_memory.has_value() || is_multicore != Settings::values.use_multi_core.GetValue() || extended_memory_layout != (Settings::values.memory_layout_mode.GetValue() != - Settings::MemoryLayout::Memory_4Gb); + Settings::MemoryLayout::Memory_4Gb) || + unified_memory != Settings::values.use_unified_memory.GetValue(); if (!must_reinitialize) { return; @@ -535,6 +537,7 @@ struct System::Impl { std::atomic_bool is_powered_on{}; bool is_multicore : 1 = false; bool extended_memory_layout : 1 = false; + bool unified_memory : 1 = false; bool exit_locked : 1 = false; bool exit_requested : 1 = false; diff --git a/src/core/device_memory.cpp b/src/core/device_memory.cpp index 1aea56a991..8d56f86be9 100644 --- a/src/core/device_memory.cpp +++ b/src/core/device_memory.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -12,9 +15,21 @@ constexpr size_t VirtualReserveSize = 1ULL << 38; constexpr size_t VirtualReserveSize = 1ULL << 39; #endif +namespace { +size_t ApplicationPoolOffset() { + using Init = Kernel::Board::Nintendo::Nx::KSystemControl::Init; + const size_t dram_size = Init::GetIntendedMemorySize(); + const size_t application_pool_size = Init::GetApplicationPoolSize(); + if (dram_size <= application_pool_size) { + return 0; + } + return dram_size - application_pool_size; +} +} + DeviceMemory::DeviceMemory() : buffer{Kernel::Board::Nintendo::Nx::KSystemControl::Init::GetIntendedMemorySize(), - VirtualReserveSize} {} + VirtualReserveSize, ApplicationPoolOffset()} {} DeviceMemory::~DeviceMemory() = default; diff --git a/src/core/device_memory_manager.h b/src/core/device_memory_manager.h index 3d97fdcc5c..72c8b7787f 100644 --- a/src/core/device_memory_manager.h +++ b/src/core/device_memory_manager.h @@ -20,6 +20,10 @@ #include "common/scratch_buffer.h" #include "common/virtual_buffer.h" +namespace Common { +class HostMemory; +} + namespace Core { constexpr size_t DEVICE_PAGEBITS = 12ULL; @@ -126,6 +130,10 @@ public: // New batch API to update multiple ranges with a single lock acquisition. void UpdatePagesCachedBatch(std::span> ranges, s32 delta); + const Common::HostMemory& GetHostMemory() const noexcept { + return host_memory; + } + private: struct TranslationEntry { DAddr guest_page{}; @@ -171,6 +179,7 @@ private: std::unique_ptr> impl; const uintptr_t physical_base; + const Common::HostMemory& host_memory; DeviceInterface* device_inter; struct TrackedEntry { diff --git a/src/core/device_memory_manager.inc b/src/core/device_memory_manager.inc index 9866f3a4f7..8f3ed53285 100644 --- a/src/core/device_memory_manager.inc +++ b/src/core/device_memory_manager.inc @@ -171,6 +171,7 @@ struct DeviceMemoryManagerAllocator { template DeviceMemoryManager::DeviceMemoryManager(const DeviceMemory& device_memory_) : physical_base{uintptr_t(device_memory_.buffer.BackingBasePointer())} + , host_memory{device_memory_.buffer} , device_inter{nullptr} , compressed_device_addr(1ULL << ((Settings::values.memory_layout_mode.GetValue() == Settings::MemoryLayout::Memory_4Gb ? physical_min_bits : physical_max_bits) - Memory::YUZU_PAGEBITS)) , tracked_entries(device_as_size >> Memory::YUZU_PAGEBITS) diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 82f0b68863..96facedf79 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -168,6 +168,8 @@ add_library(video_core STATIC renderer_vulkan/vk_fence_manager.h renderer_vulkan/vk_graphics_pipeline.cpp renderer_vulkan/vk_graphics_pipeline.h + renderer_vulkan/vk_guest_memory.cpp + renderer_vulkan/vk_guest_memory.h renderer_vulkan/vk_multi_range_buffer.cpp renderer_vulkan/vk_multi_range_buffer.h renderer_vulkan/vk_master_semaphore.cpp diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp index 2fcba26ce4..6bee2407b7 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp @@ -591,8 +591,21 @@ void QueriesPrefixScanPass::Run(VkBuffer accumulation_buffer, VkBuffer dst_buffe namespace { -void RecordUnswizzleBeginBarrier(Scheduler& scheduler, VkPipeline vk_pipeline, VkImage vk_image, - VkImageAspectFlags aspect_mask, bool is_initialized) { +template +void RecordUnswizzle(Scheduler& scheduler, bool reorder, Func&& func) { + if (reorder) { + scheduler.RecordWithUploadBuffer( + [func = std::forward(func)](vk::CommandBuffer, vk::CommandBuffer upload_cmdbuf) { + func(upload_cmdbuf); + }); + return; + } + scheduler.Record(std::forward(func)); +} + +void RecordUnswizzleBeginBarrier(Scheduler& scheduler, bool reorder, VkPipeline vk_pipeline, + VkImage vk_image, VkImageAspectFlags aspect_mask, + bool is_initialized) { VkAccessFlags2 src_access = VK_ACCESS_2_NONE; VkImageLayout old_layout = VK_IMAGE_LAYOUT_UNDEFINED; VkPipelineStageFlags2 src_stage = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT; @@ -601,8 +614,8 @@ void RecordUnswizzleBeginBarrier(Scheduler& scheduler, VkPipeline vk_pipeline, V old_layout = VK_IMAGE_LAYOUT_GENERAL; src_stage = vk::PIPELINE_STAGE_IMAGE_USERS; } - scheduler.Record([vk_pipeline, vk_image, aspect_mask, src_access, old_layout, - src_stage](vk::CommandBuffer cmdbuf) { + RecordUnswizzle(scheduler, reorder, [vk_pipeline, vk_image, aspect_mask, src_access, + old_layout, src_stage](vk::CommandBuffer cmdbuf) { const VkImageMemoryBarrier2 image_barrier{ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .pNext = nullptr, @@ -628,9 +641,9 @@ void RecordUnswizzleBeginBarrier(Scheduler& scheduler, VkPipeline vk_pipeline, V }); } -void RecordUnswizzleEndBarrier(Scheduler& scheduler, VkImage vk_image, +void RecordUnswizzleEndBarrier(Scheduler& scheduler, bool reorder, VkImage vk_image, VkImageAspectFlags aspect_mask) { - scheduler.Record([vk_image, aspect_mask](vk::CommandBuffer cmdbuf) { + RecordUnswizzle(scheduler, reorder, [vk_image, aspect_mask](vk::CommandBuffer cmdbuf) { const VkImageMemoryBarrier2 image_barrier{ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .pNext = nullptr, @@ -672,16 +685,19 @@ ASTCDecoderPass::ASTCDecoderPass(const Device& device_, Scheduler& scheduler_, ASTCDecoderPass::~ASTCDecoderPass() = default; void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, - std::span swizzles) { + std::span swizzles, + bool reorder) { using namespace VideoCommon::Accelerated; const std::array block_dims{ VideoCore::Surface::DefaultBlockWidth(image.info.format), VideoCore::Surface::DefaultBlockHeight(image.info.format), }; - scheduler.RequestOutsideRenderPassOperationContext(); + if (!reorder) { + scheduler.RequestOutsideRenderPassOperationContext(); + } const VkImageAspectFlags aspect_mask = image.AspectMask(); const VkImage vk_image = image.Handle(); - RecordUnswizzleBeginBarrier(scheduler, *pipeline, vk_image, aspect_mask, + RecordUnswizzleBeginBarrier(scheduler, reorder, *pipeline, vk_image, aspect_mask, image.ExchangeInitialization()); for (const VideoCommon::SwizzleParameters& swizzle : swizzles) { const size_t input_offset = swizzle.buffer_offset + map.offset; @@ -700,8 +716,9 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, ASSERT(params.origin == (std::array{0, 0, 0})); ASSERT(params.destination == (std::array{0, 0, 0})); ASSERT(params.bytes_per_block_log2 == 4); - scheduler.Record([this, num_dispatches_x, num_dispatches_y, num_dispatches_z, block_dims, - params, descriptor_data](vk::CommandBuffer cmdbuf) { + RecordUnswizzle(scheduler, reorder, + [this, num_dispatches_x, num_dispatches_y, num_dispatches_z, block_dims, + params, descriptor_data](vk::CommandBuffer cmdbuf) { const AstcPushConstants uniforms{ .blocks_dims = block_dims, .layer_stride = params.layer_stride, @@ -717,7 +734,7 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, cmdbuf.Dispatch(num_dispatches_x, num_dispatches_y, num_dispatches_z); }); } - RecordUnswizzleEndBarrier(scheduler, vk_image, aspect_mask); + RecordUnswizzleEndBarrier(scheduler, reorder, vk_image, aspect_mask); } BlockLinearUnswizzleImage2DPass::BlockLinearUnswizzleImage2DPass( @@ -736,12 +753,14 @@ BlockLinearUnswizzleImage2DPass::~BlockLinearUnswizzleImage2DPass() = default; void BlockLinearUnswizzleImage2DPass::Unswizzle( Image& image, const StagingBufferRef& map, - std::span swizzles) { + std::span swizzles, bool reorder) { using namespace VideoCommon::Accelerated; - scheduler.RequestOutsideRenderPassOperationContext(); + if (!reorder) { + scheduler.RequestOutsideRenderPassOperationContext(); + } const VkImageAspectFlags aspect_mask = image.AspectMask(); const VkImage vk_image = image.Handle(); - RecordUnswizzleBeginBarrier(scheduler, *pipeline, vk_image, aspect_mask, + RecordUnswizzleBeginBarrier(scheduler, reorder, *pipeline, vk_image, aspect_mask, image.ExchangeInitialization()); for (const VideoCommon::SwizzleParameters& swizzle : swizzles) { const size_t input_offset = swizzle.buffer_offset + map.offset; @@ -756,8 +775,9 @@ void BlockLinearUnswizzleImage2DPass::Unswizzle( const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; const auto params = MakeBlockLinearSwizzle2DParams(swizzle, image.info); - scheduler.Record([this, num_dispatches_x, num_dispatches_y, num_dispatches_z, params, - descriptor_data](vk::CommandBuffer cmdbuf) { + RecordUnswizzle(scheduler, reorder, + [this, num_dispatches_x, num_dispatches_y, num_dispatches_z, params, + descriptor_data](vk::CommandBuffer cmdbuf) { const VkDescriptorSet set = descriptor_allocator.Commit(); device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); @@ -765,7 +785,7 @@ void BlockLinearUnswizzleImage2DPass::Unswizzle( cmdbuf.Dispatch(num_dispatches_x, num_dispatches_y, num_dispatches_z); }); } - RecordUnswizzleEndBarrier(scheduler, vk_image, aspect_mask); + RecordUnswizzleEndBarrier(scheduler, reorder, vk_image, aspect_mask); } BlockLinearUnswizzleImage3DPass::BlockLinearUnswizzleImage3DPass( @@ -783,12 +803,14 @@ BlockLinearUnswizzleImage3DPass::~BlockLinearUnswizzleImage3DPass() = default; void BlockLinearUnswizzleImage3DPass::Unswizzle( Image& image, const StagingBufferRef& map, - std::span swizzles) { + std::span swizzles, bool reorder) { using namespace VideoCommon::Accelerated; - scheduler.RequestOutsideRenderPassOperationContext(); + if (!reorder) { + scheduler.RequestOutsideRenderPassOperationContext(); + } const VkImageAspectFlags aspect_mask = image.AspectMask(); const VkImage vk_image = image.Handle(); - RecordUnswizzleBeginBarrier(scheduler, *pipeline, vk_image, aspect_mask, + RecordUnswizzleBeginBarrier(scheduler, reorder, *pipeline, vk_image, aspect_mask, image.ExchangeInitialization()); for (const VideoCommon::SwizzleParameters& swizzle : swizzles) { const size_t input_offset = swizzle.buffer_offset + map.offset; @@ -815,8 +837,9 @@ void BlockLinearUnswizzleImage3DPass::Unswizzle( .block_depth = p.block_depth, .block_depth_mask = p.block_depth_mask, }; - scheduler.Record([this, num_dispatches_x, num_dispatches_y, num_dispatches_z, params, - descriptor_data](vk::CommandBuffer cmdbuf) { + RecordUnswizzle(scheduler, reorder, + [this, num_dispatches_x, num_dispatches_y, num_dispatches_z, params, + descriptor_data](vk::CommandBuffer cmdbuf) { const VkDescriptorSet set = descriptor_allocator.Commit(); device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); @@ -824,7 +847,7 @@ void BlockLinearUnswizzleImage3DPass::Unswizzle( cmdbuf.Dispatch(num_dispatches_x, num_dispatches_y, num_dispatches_z); }); } - RecordUnswizzleEndBarrier(scheduler, vk_image, aspect_mask); + RecordUnswizzleEndBarrier(scheduler, reorder, vk_image, aspect_mask); } PitchUnswizzlePass::PitchUnswizzlePass(const Device& device_, Scheduler& scheduler_, @@ -841,13 +864,16 @@ PitchUnswizzlePass::PitchUnswizzlePass(const Device& device_, Scheduler& schedul PitchUnswizzlePass::~PitchUnswizzlePass() = default; void PitchUnswizzlePass::Unswizzle(Image& image, const StagingBufferRef& map, - std::span swizzles) { + std::span swizzles, + bool reorder) { const u32 bytes_per_block = VideoCore::Surface::BytesPerBlock(image.info.format); ASSERT(std::has_single_bit(bytes_per_block)); - scheduler.RequestOutsideRenderPassOperationContext(); + if (!reorder) { + scheduler.RequestOutsideRenderPassOperationContext(); + } const VkImageAspectFlags aspect_mask = image.AspectMask(); const VkImage vk_image = image.Handle(); - RecordUnswizzleBeginBarrier(scheduler, *pipeline, vk_image, aspect_mask, + RecordUnswizzleBeginBarrier(scheduler, reorder, *pipeline, vk_image, aspect_mask, image.ExchangeInitialization()); for (const VideoCommon::SwizzleParameters& swizzle : swizzles) { const size_t input_offset = swizzle.buffer_offset + map.offset; @@ -866,8 +892,9 @@ void PitchUnswizzlePass::Unswizzle(Image& image, const StagingBufferRef& map, .bytes_per_block_log2 = static_cast(std::countr_zero(bytes_per_block)), .pitch = image.info.pitch, }; - scheduler.Record([this, num_dispatches_x, num_dispatches_y, params, - descriptor_data](vk::CommandBuffer cmdbuf) { + RecordUnswizzle(scheduler, reorder, + [this, num_dispatches_x, num_dispatches_y, params, + descriptor_data](vk::CommandBuffer cmdbuf) { const VkDescriptorSet set = descriptor_allocator.Commit(); device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); @@ -875,7 +902,7 @@ void PitchUnswizzlePass::Unswizzle(Image& image, const StagingBufferRef& map, cmdbuf.Dispatch(num_dispatches_x, num_dispatches_y, 1); }); } - RecordUnswizzleEndBarrier(scheduler, vk_image, aspect_mask); + RecordUnswizzleEndBarrier(scheduler, reorder, vk_image, aspect_mask); } } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.h b/src/video_core/renderer_vulkan/vk_compute_pass.h index f871648647..7517b65aee 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.h +++ b/src/video_core/renderer_vulkan/vk_compute_pass.h @@ -144,7 +144,7 @@ public: ~ASTCDecoderPass(); void Assemble(Image& image, const StagingBufferRef& map, - std::span swizzles); + std::span swizzles, bool reorder); private: Scheduler& scheduler; @@ -162,7 +162,7 @@ public: ~PitchUnswizzlePass(); void Unswizzle(Image& image, const StagingBufferRef& map, - std::span swizzles); + std::span swizzles, bool reorder); private: Scheduler& scheduler; @@ -179,7 +179,7 @@ public: ~BlockLinearUnswizzleImage3DPass(); void Unswizzle(Image& image, const StagingBufferRef& map, - std::span swizzles); + std::span swizzles, bool reorder); private: Scheduler& scheduler; @@ -196,7 +196,7 @@ public: ~BlockLinearUnswizzleImage2DPass(); void Unswizzle(Image& image, const StagingBufferRef& map, - std::span swizzles); + std::span swizzles, bool reorder); private: Scheduler& scheduler; diff --git a/src/video_core/renderer_vulkan/vk_guest_memory.cpp b/src/video_core/renderer_vulkan/vk_guest_memory.cpp new file mode 100644 index 0000000000..7d0d28852d --- /dev/null +++ b/src/video_core/renderer_vulkan/vk_guest_memory.cpp @@ -0,0 +1,176 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include + +#include "common/host_memory.h" +#include "video_core/renderer_vulkan/vk_guest_memory.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/vulkan_common/vulkan_device.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" + +namespace Vulkan { +namespace { +constexpr size_t PROBE_WORDS = 1024; +constexpr size_t PROBE_SIZE = PROBE_WORDS * sizeof(u32); +constexpr u32 PROBE_CPU_STRIDE = 0x9E3779B9U; +constexpr u32 PROBE_GPU_VALUE = 0x5AC3E10FU; +} + +GuestMemory::GuestMemory(const Device& device, MemoryAllocator& memory_allocator, + Scheduler& scheduler, const Common::HostMemory& host_memory) { + Import(device, host_memory); + if (!windows.empty() && !IsCoherent(memory_allocator, scheduler)) { + windows.clear(); + } +} + +std::optional GuestMemory::Find(const u8* pointer, size_t size) const { + if (windows.empty() || pointer < base) { + return std::nullopt; + } + const size_t offset = static_cast(pointer - base); + const size_t index = offset / window_size; + const size_t local_offset = offset % window_size; + if (index >= windows.size() || window_size - local_offset < size) { + return std::nullopt; + } + return Range{*windows[index].buffer, local_offset}; +} + +void GuestMemory::Import([[maybe_unused]] const Device& device, + [[maybe_unused]] const Common::HostMemory& host_memory) { +#ifdef __ANDROID__ + const std::span hardware_buffers = + host_memory.BackingHardwareBuffers(); + window_size = host_memory.BackingHardwareBufferWindowSize(); + if (hardware_buffers.empty() || window_size == 0 || !device.IsAhbImportSupported()) { + return; + } + base = const_cast(host_memory.BackingBasePointer()) + + host_memory.BackingHardwareBufferBase(); + const vk::Device& logical = device.GetLogical(); + try { + for (AHardwareBuffer* const hardware_buffer : hardware_buffers) { + const VkAndroidHardwareBufferPropertiesANDROID properties = + logical.GetAndroidHardwareBufferProperties(hardware_buffer); + const VkExternalMemoryBufferCreateInfo external_info{ + .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, + .pNext = nullptr, + .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID, + }; + Window& window = windows.emplace_back(); + window.buffer = logical.CreateExternalBuffer(VkBufferCreateInfo{ + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = &external_info, + .flags = 0, + .size = window_size, + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | + VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + }); + const u32 type_bits = + logical.GetBufferMemoryRequirements(*window.buffer).memoryTypeBits & + properties.memoryTypeBits; + if (type_bits == 0) { + windows.clear(); + return; + } + const VkImportAndroidHardwareBufferInfoANDROID import_info{ + .sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID, + .pNext = nullptr, + .buffer = hardware_buffer, + }; + const VkMemoryDedicatedAllocateInfo dedicated_info{ + .sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO, + .pNext = &import_info, + .image = VK_NULL_HANDLE, + .buffer = *window.buffer, + }; + window.memory = logical.AllocateMemory(VkMemoryAllocateInfo{ + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .pNext = &dedicated_info, + .allocationSize = properties.allocationSize, + .memoryTypeIndex = static_cast(std::countr_zero(type_bits)), + }); + logical.BindBufferMemory(*window.buffer, *window.memory, 0); + } + } catch (const vk::Exception&) { + windows.clear(); + } +#endif +} + +bool GuestMemory::IsCoherent(MemoryAllocator& memory_allocator, Scheduler& scheduler) const { + const VkDeviceSize probe_offset = window_size - PROBE_SIZE; + u8* const probe = base + (windows.size() - 1) * window_size + probe_offset; + std::array saved; + std::memcpy(saved.data(), probe, PROBE_SIZE); + std::array pattern; + for (size_t index = 0; index < PROBE_WORDS; ++index) { + pattern[index] = static_cast(index + 1) * PROBE_CPU_STRIDE; + } + std::memcpy(probe, pattern.data(), PROBE_SIZE); + + vk::Buffer readback = memory_allocator.CreateBuffer( + VkBufferCreateInfo{ + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = PROBE_SIZE, + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + }, + MemoryUsage::Download); + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([src = *windows.back().buffer, dst = *readback, + probe_offset](vk::CommandBuffer cmdbuf) { + static constexpr VkMemoryBarrier2 READ_BEFORE_WRITE{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + }; + static constexpr VkMemoryBarrier2 WRITE_TO_HOST{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT, + .dstAccessMask = VK_ACCESS_2_HOST_READ_BIT, + }; + const VkBufferCopy2 copy{ + .sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2, + .pNext = nullptr, + .srcOffset = probe_offset, + .dstOffset = 0, + .size = PROBE_SIZE, + }; + cmdbuf.CopyBuffer(src, dst, copy); + cmdbuf.PipelineBarrier(READ_BEFORE_WRITE); + cmdbuf.FillBuffer(src, probe_offset, PROBE_SIZE, PROBE_GPU_VALUE); + cmdbuf.PipelineBarrier(WRITE_TO_HOST); + }); + scheduler.Finish(); + + const bool gpu_sees_cpu = + std::memcmp(readback.Mapped().data(), pattern.data(), PROBE_SIZE) == 0; + std::array written; + std::memcpy(written.data(), probe, PROBE_SIZE); + const bool cpu_sees_gpu = + std::ranges::all_of(written, [](u32 word) { return word == PROBE_GPU_VALUE; }); + std::memcpy(probe, saved.data(), PROBE_SIZE); + return gpu_sees_cpu && cpu_sees_gpu; +} + +} diff --git a/src/video_core/renderer_vulkan/vk_guest_memory.h b/src/video_core/renderer_vulkan/vk_guest_memory.h new file mode 100644 index 0000000000..10128392e4 --- /dev/null +++ b/src/video_core/renderer_vulkan/vk_guest_memory.h @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Common { +class HostMemory; +} + +namespace Vulkan { + +class Device; +class MemoryAllocator; +class Scheduler; + +class GuestMemory { +public: + struct Range { + VkBuffer buffer; + VkDeviceSize offset; + }; + + explicit GuestMemory(const Device& device, MemoryAllocator& memory_allocator, + Scheduler& scheduler, const Common::HostMemory& host_memory); + + [[nodiscard]] std::optional Find(const u8* pointer, size_t size) const; + + [[nodiscard]] bool Empty() const noexcept { + return windows.empty(); + } + +private: + struct Window { + vk::DeviceMemory memory; + vk::ExternalBuffer buffer; + }; + + void Import(const Device& device, const Common::HostMemory& host_memory); + + [[nodiscard]] bool IsCoherent(MemoryAllocator& memory_allocator, Scheduler& scheduler) const; + + std::vector windows; + u8* base{}; + size_t window_size{}; +}; + +} diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 30ae972dfc..6bd608c9ea 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -227,6 +227,9 @@ RasterizerVulkan::RasterizerVulkan(Core::Frontend::EmuWindow& emu_window_, Tegra accelerate_dma(buffer_cache, texture_cache, scheduler), fence_manager(*this, gpu, texture_cache, buffer_cache, query_cache, device, scheduler) { scheduler.SetQueryCache(query_cache); + if (Settings::values.use_unified_memory.GetValue()) { + texture_cache_runtime.ImportGuestMemory(device_memory.GetHostMemory()); + } } RasterizerVulkan::~RasterizerVulkan() { diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 332dec09c0..55837ba8c0 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -3206,20 +3206,40 @@ void TextureCacheRuntime::AccelerateImageUpload( if (is_rescaled) { image.ScaleDown(true); } + const bool reorder = !is_rescaled && True(image.flags & ImageFlagBits::ReorderableUpload); if (IsPixelFormatASTC(image.info.format)) { - astc_decoder_pass->Assemble(image, map, swizzles); + astc_decoder_pass->Assemble(image, map, swizzles, reorder); } else if (image.info.type == ImageType::e3D) { - bl_unswizzle_3d_pass->Unswizzle(image, map, swizzles); + bl_unswizzle_3d_pass->Unswizzle(image, map, swizzles, reorder); } else if (image.info.type == ImageType::Linear) { - pitch_unswizzle_pass->Unswizzle(image, map, swizzles); + pitch_unswizzle_pass->Unswizzle(image, map, swizzles, reorder); } else { - bl_unswizzle_2d_pass->Unswizzle(image, map, swizzles); + bl_unswizzle_2d_pass->Unswizzle(image, map, swizzles, reorder); } if (is_rescaled) { image.ScaleUp(); } } +void TextureCacheRuntime::ImportGuestMemory(const Common::HostMemory& host_memory) { + guest_memory.emplace(device, memory_allocator, scheduler, host_memory); + if (guest_memory->Empty()) { + guest_memory.reset(); + } +} + +std::optional TextureCacheRuntime::GuestMemorySource(const u8* pointer, + size_t size) const { + if (pointer == nullptr) { + return std::nullopt; + } + const std::optional range = guest_memory->Find(pointer, size); + if (!range || range->offset % device.GetStorageBufferAlignment() != 0) { + return std::nullopt; + } + return StagingBufferRef{.buffer = range->buffer, .offset = range->offset}; +} + void TextureCacheRuntime::TransitionImageLayout(Image& image) { if (image.ExchangeInitialization()) { return; diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index ee0295d66a..37b476a489 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -13,6 +13,7 @@ #include "shader_recompiler/shader_info.h" #include "video_core/renderer_vulkan/vk_compute_pass.h" +#include "video_core/renderer_vulkan/vk_guest_memory.h" #include "video_core/renderer_vulkan/vk_render_pass_cache.h" #include "video_core/renderer_vulkan/vk_staging_buffer_pool.h" #include "video_core/texture_cache/image_view_base.h" @@ -93,6 +94,15 @@ public: void AccelerateImageUpload(Image&, const StagingBufferRef&, std::span); + void ImportGuestMemory(const Common::HostMemory& host_memory); + + [[nodiscard]] bool HasGuestMemory() const noexcept { + return guest_memory.has_value(); + } + + [[nodiscard]] std::optional GuestMemorySource(const u8* pointer, + size_t size) const; + void InsertUploadMemoryBarrier() {} void TransitionImageLayout(Image& image); @@ -153,6 +163,7 @@ public: std::optional bl_unswizzle_2d_pass; std::optional bl_unswizzle_3d_pass; std::optional pitch_unswizzle_pass; + std::optional guest_memory; const Settings::ResolutionScalingInfo& resolution; std::array, VideoCore::Surface::MaxPixelFormat> view_formats; std::bitset host_copy_formats; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index c199b5953c..5975ae13a7 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -1033,6 +1033,24 @@ void TextureCache

::RefreshContents(Image& image, ImageId image_id) { QueueAsyncDecode(image, image_id); return; } + if constexpr (requires { runtime.GuestMemorySource(nullptr, size_t{}); }) { + if (True(image.flags & ImageFlagBits::AcceleratedUpload) && runtime.HasGuestMemory() && + gpu_memory->IsContinuousRange(image.gpu_addr, image.guest_size_bytes)) { + const std::optional device_addr = gpu_memory->GpuToCpuAddress(image.gpu_addr); + if (device_addr) { + const auto source = runtime.GuestMemorySource( + device_memory.GetSpan(*device_addr, image.guest_size_bytes), + image.guest_size_bytes); + if (source) { + gpu_memory->FlushRegion(image.gpu_addr, image.guest_size_bytes, + VideoCommon::CacheType::NoTextureCache); + runtime.AccelerateImageUpload( + image, *source, FixSmallVectorADL(FullUploadSwizzles(image.info))); + return; + } + } + } + } if constexpr (requires { image.UploadHostMemory(unswizzle_data_buffer, {}); }) { if (True(image.flags & ImageFlagBits::ReorderableUpload) && image.CanUploadHostMemory()) { Tegra::Memory::GpuGuestMemory diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index cd5e5a8412..12d7cfbba8 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -934,6 +934,15 @@ bool Device::GetSuitability(bool requires_swapchain) { FOR_EACH_VK_FEATURE_EXT(FEATURE_EXTENSION); FOR_EACH_VK_EXTENSION(EXTENSION); +#ifdef __ANDROID__ + if (supported_extensions.contains( + VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME) && + supported_extensions.contains(VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME)) { + loaded_extensions.insert(VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME); + loaded_extensions.insert(VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME); + has_ahb_import = true; + } +#endif extensions.depth_stencil_resolve = extensions.depth_stencil_resolve && diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 63a2e5b7fa..ab96421c3a 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -993,6 +993,10 @@ FN_MAX_LIMIT_LIST return extensions.host_image_copy; } + bool IsAhbImportSupported() const { + return has_ahb_import; + } + bool HasNullDescriptor() const { return features.robustness2.nullDescriptor; } @@ -1208,6 +1212,7 @@ private: bool is_warp_potentially_bigger{}; ///< Host warp size can be bigger than guest. bool is_integrated{}; ///< Is GPU an iGPU. bool is_uma{}; + bool has_ahb_import{}; bool has_broken_compute{}; ///< Compute shaders can cause crashes bool has_broken_cube_compatibility{}; ///< Has broken cube compatibility bit bool has_broken_float16_math{}; diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp index 01a65f95f5..1d728e9113 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.cpp +++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp @@ -85,6 +85,12 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept { X(vkAcquireNextImageKHR); X(vkAllocateCommandBuffers); X(vkAllocateDescriptorSets); + X(vkAllocateMemory); + X(vkBindBufferMemory); + X(vkFreeMemory); +#ifdef __ANDROID__ + X(vkGetAndroidHardwareBufferPropertiesANDROID); +#endif X(vkBeginCommandBuffer); X(vkCmdBeginConditionalRenderingEXT); X(vkCmdBeginQuery); @@ -337,6 +343,10 @@ void Destroy(VkDevice device, VkBuffer handle, const DeviceDispatch& dld) noexce dld.vkDestroyBuffer(device, handle, nullptr); } +void Destroy(VkDevice device, VkDeviceMemory handle, const DeviceDispatch& dld) noexcept { + dld.vkFreeMemory(device, handle, nullptr); +} + void Destroy(VkDevice device, VkBufferView handle, const DeviceDispatch& dld) noexcept { dld.vkDestroyBufferView(device, handle, nullptr); } @@ -616,6 +626,51 @@ BufferView Device::CreateBufferView(const VkBufferViewCreateInfo& ci) const { return BufferView(object, handle, *dld); } +ExternalBuffer Device::CreateExternalBuffer(const VkBufferCreateInfo& ci) const { + VkBuffer object; + Check(dld->vkCreateBuffer(handle, &ci, nullptr, &object)); + return ExternalBuffer(object, handle, *dld); +} + +DeviceMemory Device::AllocateMemory(const VkMemoryAllocateInfo& ai) const { + VkDeviceMemory object; + Check(dld->vkAllocateMemory(handle, &ai, nullptr, &object)); + return DeviceMemory(object, handle, *dld); +} + +void Device::BindBufferMemory(VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) const { + Check(dld->vkBindBufferMemory(handle, buffer, memory, offset)); +} + +VkMemoryRequirements Device::GetBufferMemoryRequirements(VkBuffer buffer) const noexcept { + const VkBufferMemoryRequirementsInfo2 info{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_REQUIREMENTS_INFO_2, + .pNext = nullptr, + .buffer = buffer, + }; + VkMemoryRequirements2 requirements{ + .sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2, + .pNext = nullptr, + .memoryRequirements = {}, + }; + dld->vkGetBufferMemoryRequirements2(handle, &info, &requirements); + return requirements.memoryRequirements; +} + +#ifdef __ANDROID__ +VkAndroidHardwareBufferPropertiesANDROID Device::GetAndroidHardwareBufferProperties( + const AHardwareBuffer* buffer) const { + VkAndroidHardwareBufferPropertiesANDROID properties{ + .sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID, + .pNext = nullptr, + .allocationSize = 0, + .memoryTypeBits = 0, + }; + Check(dld->vkGetAndroidHardwareBufferPropertiesANDROID(handle, buffer, &properties)); + return properties; +} +#endif + ImageView Device::CreateImageView(const VkImageViewCreateInfo& ci) const { VkImageView object; Check(dld->vkCreateImageView(handle, &ci, nullptr, &object)); diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h index c896a22bad..e0bb7585ac 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.h +++ b/src/video_core/vulkan_common/vulkan_wrapper.h @@ -234,6 +234,12 @@ struct DeviceDispatch : InstanceDispatch { PFN_vkAcquireNextImageKHR vkAcquireNextImageKHR{}; PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers{}; PFN_vkAllocateDescriptorSets vkAllocateDescriptorSets{}; + PFN_vkAllocateMemory vkAllocateMemory{}; + PFN_vkBindBufferMemory vkBindBufferMemory{}; + PFN_vkFreeMemory vkFreeMemory{}; +#ifdef __ANDROID__ + PFN_vkGetAndroidHardwareBufferPropertiesANDROID vkGetAndroidHardwareBufferPropertiesANDROID{}; +#endif PFN_vkBeginCommandBuffer vkBeginCommandBuffer{}; PFN_vkCmdBeginConditionalRenderingEXT vkCmdBeginConditionalRenderingEXT{}; PFN_vkCmdBeginQuery vkCmdBeginQuery{}; @@ -387,6 +393,7 @@ void Destroy(VkDevice, const InstanceDispatch&) noexcept; void Destroy(VkDevice, VkBuffer, const DeviceDispatch&) noexcept; void Destroy(VkDevice, VkBufferView, const DeviceDispatch&) noexcept; +void Destroy(VkDevice, VkDeviceMemory, const DeviceDispatch&) noexcept; void Destroy(VkDevice, VkCommandPool, const DeviceDispatch&) noexcept; void Destroy(VkDevice, VkDescriptorPool, const DeviceDispatch&) noexcept; void Destroy(VkDevice, VkDescriptorSetLayout, const DeviceDispatch&) noexcept; @@ -646,6 +653,8 @@ using Pipeline = Handle; using PipelineLayout = Handle; using QueryPool = Handle; using Sampler = Handle; +using DeviceMemory = Handle; +using ExternalBuffer = Handle; using SurfaceKHR = Handle; using DescriptorSets = PoolAllocations; @@ -1000,6 +1009,19 @@ public: [[nodiscard]] BufferView CreateBufferView(const VkBufferViewCreateInfo& ci) const; + [[nodiscard]] ExternalBuffer CreateExternalBuffer(const VkBufferCreateInfo& ci) const; + + [[nodiscard]] DeviceMemory AllocateMemory(const VkMemoryAllocateInfo& ai) const; + + void BindBufferMemory(VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) const; + + [[nodiscard]] VkMemoryRequirements GetBufferMemoryRequirements(VkBuffer buffer) const noexcept; + +#ifdef __ANDROID__ + [[nodiscard]] VkAndroidHardwareBufferPropertiesANDROID GetAndroidHardwareBufferProperties( + const AHardwareBuffer* buffer) const; +#endif + [[nodiscard]] ImageView CreateImageView(const VkImageViewCreateInfo& ci) const; [[nodiscard]] Semaphore CreateSemaphore() const;