aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-28 04:56:16 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-28 04:56:16 -0400
commit2c6d8f03aadb3c9d900909bd4af64bbb18614d6d (patch)
treef7ae707fd278434de8ced86a266d43a6752dc827 /src
parent64c91974f4455a63a025149a4ba8e2c46894bdc7 (diff)
nx2: unmap GPU memory the game frees
nx2:gpu 213 (NormalUnmapGpuMemory4K) and 102 (destroy GPU-accessible memory) were no-ops, so Eden kept freed memory mapped for the GPU. Title 4 frees its customization-room memory right after saving and reuses it as ordinary heap; pending GPU->CPU write-backs for the old buffers then landed on the new objects, and two threads crashed on garbage pointers ~5 s later (the post-customization freeze). Unmap the range from every address space (the GMMU unmap drops cached buffers/images and their pending downloads), keep the registry's mapping list in step, re-map 211 requests even at the memory's own address, and drop freed regions from the semaphore mirror table, which every 8 KiB allocation used to fill for good after 32 entries.
Diffstat (limited to 'src')
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp16
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h1
-rw-r--r--src/core/hle/service/nvdrv/devices/nx2_stub_device.h45
-rw-r--r--src/core/hle/service/nx2_gsv.h49
-rw-r--r--src/video_core/nx2.h17
5 files changed, 115 insertions, 13 deletions
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
index 6de442fcdf..4dc667fa18 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
@@ -58,9 +58,11 @@ NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> i
const auto result = WrapFixed(this, &nvhost_as_gpu::AllocAsEx, input, output);
if (result == NvResult::Success && g_nx2_driver) {
Nx2GpuMemoryRegistry::Get().AddListener(
- this, [this](u64 gpu_address, u64 cpu_address, u64 size) {
+ this,
+ [this](u64 gpu_address, u64 cpu_address, u64 size) {
Nx2Map(gpu_address, cpu_address, size);
- });
+ },
+ [this](u64 gpu_address, u64 size) { Nx2Unmap(gpu_address, size); });
}
return result;
}
@@ -143,6 +145,16 @@ void nvhost_as_gpu::Nx2Map(u64 gpu_address, u64 cpu_address, u64 size) {
LOG_DEBUG(Service_NVDRV, "Switch 2 GPU mapping {:#x}+{:#x} -> CPU {:#x}", gpu_address, size,
cpu_address);
}
+
+void nvhost_as_gpu::Nx2Unmap(u64 gpu_address, u64 size) {
+ std::scoped_lock lock(mutex);
+ if (!gmmu) {
+ return;
+ }
+ gmmu->Unmap(gpu_address, size);
+ LOG_DEBUG(Service_NVDRV, "Switch 2 GPU unmapping {:#x}+{:#x}", gpu_address, size);
+}
+
void nvhost_as_gpu::OnClose(DeviceFD fd) {}
// Switch 2 NvRm (with characteristics flag 26) asks for the read-only syncpoint shim: a GPU page
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
index ca94dc2632..b094ab9936 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
@@ -233,6 +233,7 @@ private:
// Switch 2: maps GPU-accessible process memory (CPU range) at a GPU address.
void Nx2Map(u64 gpu_address, u64 cpu_address, u64 size);
+ void Nx2Unmap(u64 gpu_address, u64 size);
NvCore::SessionId nx2_session{};
std::map<std::pair<u64, u64>, DAddr> nx2_device_ranges;
};
diff --git a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
index b8556af894..e5220cd26f 100644
--- a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
+++ b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
@@ -38,6 +38,7 @@ inline NvResult Nx2UnknownIoctl(const char* device, Ioctl command, std::span<con
class Nx2GpuMemoryRegistry {
public:
using Listener = std::function<void(u64 gpu_address, u64 cpu_address, u64 size)>;
+ using UnmapListener = std::function<void(u64 gpu_address, u64 size)>;
static Nx2GpuMemoryRegistry& Get() {
static Nx2GpuMemoryRegistry registry;
@@ -50,18 +51,28 @@ public:
void MapAt(u64 gpu_address, u64 cpu_address, u64 size) {
std::scoped_lock lk{mutex};
+ TrimMappings(gpu_address, size);
mappings.push_back({gpu_address, cpu_address, size});
for (auto& [owner, listener] : listeners) {
- listener(gpu_address, cpu_address, size);
+ listener.map(gpu_address, cpu_address, size);
}
}
- void AddListener(const void* owner, Listener listener) {
+ // NormalUnmapGpuMemory4K, or destroying the memory: the GPU range stops reaching it.
+ void Unmap(u64 gpu_address, u64 size) {
+ std::scoped_lock lk{mutex};
+ TrimMappings(gpu_address, size);
+ for (auto& [owner, listener] : listeners) {
+ listener.unmap(gpu_address, size);
+ }
+ }
+
+ void AddListener(const void* owner, Listener listener, UnmapListener unmap_listener) {
std::scoped_lock lk{mutex};
for (const auto& m : mappings) {
listener(m.gpu_address, m.cpu_address, m.size);
}
- listeners.insert_or_assign(owner, std::move(listener));
+ listeners.insert_or_assign(owner, Listeners{std::move(listener), std::move(unmap_listener)});
}
void RemoveListener(const void* owner) {
@@ -100,9 +111,35 @@ private:
u64 size;
};
+ struct Listeners {
+ Listener map;
+ UnmapListener unmap;
+ };
+
+ // Drops [gpu_address, +size) from the recorded mappings, keeping the parts outside it.
+ void TrimMappings(u64 gpu_address, u64 size) {
+ const u64 end = gpu_address + size;
+ std::vector<Mapping> kept;
+ kept.reserve(mappings.size() + 1);
+ for (const auto& m : mappings) {
+ const u64 m_end = m.gpu_address + m.size;
+ if (m_end <= gpu_address || m.gpu_address >= end) {
+ kept.push_back(m);
+ continue;
+ }
+ if (m.gpu_address < gpu_address) {
+ kept.push_back({m.gpu_address, m.cpu_address, gpu_address - m.gpu_address});
+ }
+ if (m_end > end) {
+ kept.push_back({end, m.cpu_address + (end - m.gpu_address), m_end - end});
+ }
+ }
+ mappings = std::move(kept);
+ }
+
std::mutex mutex;
std::vector<Mapping> mappings;
- std::map<const void*, Listener> listeners;
+ std::map<const void*, Listeners> listeners;
};
// Device node that Switch 2 nvgpu drivers open but Switch 1 never had (e.g. /dev/nvhost-tsg-gpu).
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index 302c5a41a8..b8156b1921 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -254,9 +254,9 @@ public:
{203, &INx2GpuSession::GetVaRange, "GetVaRange (guessed)"},
// Sub-session commands (gpu 22 hands out a second session to this same object).
{101, &INx2GpuSession::SubRegisterMemory, "CreateGpuAccessibleMemory"},
- {102, &INx2GpuSession::SubSlotJob<3>, "Sub102 (job, slot in word 3)"},
+ {102, &INx2GpuSession::SubDestroyMemory, "DestroyGpuAccessibleMemory (guessed)"},
{211, &INx2GpuSession::SubSubmitJob, "NormalMapGpuMemory4K"},
- {213, &INx2GpuSession::SubSlotJob<8>, "NormalUnmapGpuMemory4K"},
+ {213, &INx2GpuSession::SubUnmapJob, "NormalUnmapGpuMemory4K"},
{214, &INx2GpuSession::SubCreateGpuOnlyRegion, "CreateGpuOnlyRegion (guessed)"},
});
}
@@ -391,10 +391,10 @@ private:
LOG_INFO(Service, "nx2:gpu 211 map [{:#x}, {:#x}] handle={} offset={:#x} slot={}", a, b,
handle, c, slot);
if (const auto it = m_memory.find(handle); it != m_memory.end() && b > a) {
+ // Also at the memory's own address: an earlier 213 may have unmapped the mapping 101
+ // made there.
const u64 cpu = it->second.address + c;
- if (cpu != a) {
- Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapAt(a, cpu, b - a + 1);
- }
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapAt(a, cpu, b - a + 1);
} else {
LOG_WARNING(Service, "nx2:gpu 211 map of unknown handle {}", handle);
}
@@ -403,6 +403,45 @@ private:
rb.Push(ResultSuccess);
}
+ // Sub 213: words 3-4 first GPU address, 5-6 last, job slot in word 8.
+ void SubUnmapJob(HLERequestContext& ctx) {
+ const auto* w = ctx.CommandBuffer();
+ const u64 first = u64{w[3]} | (u64{w[4]} << 32);
+ const u64 last = u64{w[5]} | (u64{w[6]} << 32);
+ LOG_DEBUG(Service, "nx2:gpu 213 unmap [{:#x}, {:#x}] slot={}", first, last, w[8]);
+ if (last > first) {
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Unmap(first, last - first + 1);
+ }
+ EventFor(w[8])->Signal(system.Kernel());
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ // Sub 102: word 2 is the memory handle 101 returned, word 3 the job slot. The game frees the
+ // memory (svc 0x80 clears GPU access) and reuses it as ordinary heap right after, so nothing
+ // on the GPU side may keep it: drop the mapping 101 made at its own address.
+ void SubDestroyMemory(HLERequestContext& ctx) {
+ const auto* w = ctx.CommandBuffer();
+ const u32 handle = w[2];
+ if (const auto it = m_memory.find(handle); it != m_memory.end()) {
+ const auto [address, size] = it->second;
+ LOG_DEBUG(Service, "nx2:gpu 102 destroy handle={} [{:#x}, +{:#x}] slot={}", handle,
+ address, size, w[3]);
+ if (size != 0) {
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Unmap(address, size);
+ }
+ if (size == 0x2000) {
+ Tegra::Nx2RemoveSemaphoreRegion(address);
+ }
+ m_memory.erase(it);
+ } else {
+ LOG_WARNING(Service, "nx2:gpu 102 destroy of unknown handle {}", handle);
+ }
+ EventFor(w[3])->Signal(system.Kernel());
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
// Sub 214: GPU-only memory [start, last] (words 3-6, job slot in word 11). The graphics
// server backs it itself: the client never maps anything there but renders into it (title 4
// keeps its render targets in six 300 MB regions at 0x81_0000_0000). Back each region with
diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h
index ef6619df3f..7bbbf14785 100644
--- a/src/video_core/nx2.h
+++ b/src/video_core/nx2.h
@@ -23,19 +23,32 @@ inline std::array<std::atomic<u64>, Nx2MaxSemaphoreRegions> g_nx2_semaphore_regi
inline void Nx2AddSemaphoreRegion(u64 start) {
for (auto& region : g_nx2_semaphore_regions) {
+ if (region.load() == start) {
+ return;
+ }
+ }
+ for (auto& region : g_nx2_semaphore_regions) {
u64 expected = 0;
- if (region.load() == start || region.compare_exchange_strong(expected, start)) {
+ if (region.compare_exchange_strong(expected, start)) {
return;
}
}
}
+/// The region's memory was destroyed: stop mirroring into it.
+inline void Nx2RemoveSemaphoreRegion(u64 start) {
+ for (auto& region : g_nx2_semaphore_regions) {
+ u64 expected = start;
+ region.compare_exchange_strong(expected, 0);
+ }
+}
+
/// GPU VA in the first page that mirrors a release at `address`, or 0.
inline u64 Nx2SemaphoreMirror(u64 address) {
for (const auto& region : g_nx2_semaphore_regions) {
const u64 start = region.load(std::memory_order_relaxed);
if (start == 0) {
- break;
+ continue;
}
if (address >= start + 0x1000 && address < start + 0x2000) {
return address - 0x1000;