aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 17:39:54 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 17:39:54 -0400
commit4af363d798adf3b906fcfa5cba37ec9304f27dab (patch)
tree9cced5569a7189aac1a06d51299c3e7902159f48
parent2226865a47ed3c97c4f4631ff50660d11a152da7 (diff)
Switch 2 GPU memory handles and mappings at requested addresses
nx2:gpu 101 (CreateGpuAccessibleMemory) returns real GpuMemoryHandles; 211 (NormalMapGpuMemory4K) maps handle+offset at the requested GPU address, including sparse regions, through Nx2GpuMemoryRegistry::MapAt (SMMU ranges are reused). 102/213/214 complete through their job-slot events. gdbrsp.py waits for a real thread before driving the stub and gains a write-watch op. Notes record the current stop: an NVN command buffer with no command memory.
-rw-r--r--NX2_NOTES.md17
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp29
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h5
-rw-r--r--src/core/hle/service/nvdrv/devices/nx2_stub_device.h30
-rw-r--r--src/core/hle/service/nx2_gsv.h45
-rw-r--r--tools/nx2/gdbrsp.py27
6 files changed, 120 insertions, 33 deletions
diff --git a/NX2_NOTES.md b/NX2_NOTES.md
index f67067600a..034e185d24 100644
--- a/NX2_NOTES.md
+++ b/NX2_NOTES.md
@@ -97,6 +97,7 @@ With the patched decoder, games 1/3/4 only leave ~60-110 rare SIMD encodings in
| 39 | every audio `RequestUpdate` fails | updates tagged `REv?` (lowercase v), renderer opened with `REV?` | compare revision numbers (2e6c39c) |
| 40 | main thread parked in `ExecuteVirtualMachineMapBufferEx` | gpu sub 211's job slot is request word 17, not word 2 | signal word 17's event (2e6c39c) |
| 41 | "too many files open" reading RomFS | eden-cli never raised the CRT stdio limit | `_setmaxstdio(8192)` (2e6c39c) |
+| 42 | GPU memory model | gpu sub 101 = `CreateGpuAccessibleMemory` (reply word 3 = GpuMemoryHandle); 211 = `NormalMapGpuMemory4K` (words 3-4 first VA, 5-6 last, 7 handle, 8-9 offset, 17 slot), also into sparse regions at 0x81_xxxx_xxxx; 102/213 (`NormalUnmapGpuMemory4K`)/214 (`CreateSparseRegion`) complete through slot events (words 3/8/11) | real handles, `Nx2GpuMemoryRegistry::MapAt` with SMMU range reuse, slot signals |
Run settings used (in `build/bin/user/config/sdl2-config.ini`): `memory_layout_mode=4`
(12 GB). Debug knobs: `EDEN_NX2_AUTO_STUB`, `EDEN_NX2_IPC_TRACE`, `EDEN_NX2_SVC_TRACE`,
@@ -114,9 +115,13 @@ fonts (its own `.bfotf` plus system fonts via pl:u 2010) -> heaps (incl. 300 MB
audio renderer -> Gyml sequence files -> main loop.
- GPU submissions are executed by Eden's puller/engines (Ampere classes on Maxwell engines),
syncpoints and queue semaphores advance, but Turing+ macros are skipped and nothing is presented.
-- At ~12.4 s one NVN render path writes commands to address 0 (command memory with no CPU
- mapping) and calls a null out-of-memory callback (`sdk+0x638814`, from game `main+0x3201560`).
- The loop thread keeps running.
+- Only six GPU submissions happen (all during init). At ~11.5-12.4 s the main thread hands NVN
+ a command buffer (from a per-context table at game `main+0x320149c`, used at `main+0x32014e8`)
+ that was initialised but never given command memory or a memory callback: the whole state at
+ cmdbuf+0x2be8.. is zero, so NVN writes its first methods to address 0 and then calls a null
+ function. Some earlier game-side setup of that command buffer (its command-memory pool or the
+ frame-begin that adds memory) silently failed or never ran; that is the next thing to find.
+ Another thread keeps polling input afterwards.
**Title 3**: last checked before the graphics work; should now track title 4.
@@ -140,8 +145,10 @@ get the larger memory budget (it sets a fixed 0x1BD600000 budget at `main+0xCF25
## Prioritized next steps
-0. **Render path null command memory** at ~12.4 s (title 4): find which NVN memory pool's CPU
- pointer is null (nvnMemoryPoolMap through nn::gpu mapping jobs) and give it the mapping.
+0. **Empty command buffer** at ~12 s (title 4): find where the game normally adds command memory
+ to the per-context command buffers (`main+0x320149c` table) and which earlier call failed.
+ Break on NVN's command-buffer init and on the game's frame-begin, compare with the stores to
+ cmdbuf+0x2be8 (53 sites in sdk).
1. **Present**: decode gsv 315/423/429/435/111 and vi2 present/acquire, and blit the presented
image through Eden's renderer. First visible milestone: the game's clear colour.
2. **MME64 interpreter** behind `g_nx2_ampere` (replace the skip in `Maxwell3D::CallMacroMethod`).
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 9543d09d26..6d9fd4bd36 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
@@ -58,7 +58,9 @@ 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 address, u64 size) { Nx2MapIdentity(address, size); });
+ this, [this](u64 gpu_address, u64 cpu_address, u64 size) {
+ Nx2Map(gpu_address, cpu_address, size);
+ });
}
return result;
}
@@ -105,30 +107,35 @@ void nvhost_as_gpu::OnOpen(NvCore::SessionId session_id, DeviceFD fd) {
nx2_session = session_id;
}
-void nvhost_as_gpu::Nx2MapIdentity(u64 address, u64 size) {
+void nvhost_as_gpu::Nx2Map(u64 gpu_address, u64 cpu_address, u64 size) {
std::scoped_lock lock(mutex);
auto* session = container.GetSession(nx2_session);
if (!gmmu || session == nullptr) {
- LOG_ERROR(Service_NVDRV, "Switch 2 GPU memory {:#x}+{:#x}: no address space or session",
- address, size);
+ LOG_ERROR(Service_NVDRV, "Switch 2 GPU mapping {:#x}+{:#x}: no address space or session",
+ gpu_address, size);
return;
}
+ // The same CPU range is often mapped twice (at its own address and in a sparse region), so
+ // reuse its SMMU mapping.
DAddr device_address{};
- if (session->has_preallocated_area && session->mapper->IsInBounds(address, size)) {
- device_address = session->mapper->Map(address, size);
+ if (const auto it = nx2_device_ranges.find({cpu_address, size}); it != nx2_device_ranges.end()) {
+ device_address = it->second;
+ } else if (session->has_preallocated_area && session->mapper->IsInBounds(cpu_address, size)) {
+ device_address = session->mapper->Map(cpu_address, size);
} else {
auto& smmu = system.Host1x().MemoryManager();
device_address = smmu.Allocate(Common::AlignUp(size, 0x20000));
if (device_address == 0) {
LOG_CRITICAL(Service_NVDRV, "Switch 2 GPU memory {:#x}+{:#x}: out of SMMU space",
- address, size);
+ cpu_address, size);
return;
}
- smmu.Map(device_address, address, size, session->asid, true);
+ smmu.Map(device_address, cpu_address, size, session->asid, true);
}
- gmmu->Map(address, device_address, size, Tegra::PTEKind::PITCH, false);
- LOG_DEBUG(Service_NVDRV, "Switch 2 GPU memory {:#x}+{:#x} mapped at the same GPU address", address,
- size);
+ nx2_device_ranges[{cpu_address, size}] = device_address;
+ gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
+ LOG_DEBUG(Service_NVDRV, "Switch 2 GPU mapping {:#x}+{:#x} -> CPU {:#x}", gpu_address, size,
+ cpu_address);
}
void nvhost_as_gpu::OnClose(DeviceFD fd) {}
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 419cf4cce7..e9bd9daf73 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
@@ -222,9 +222,10 @@ private:
} vm;
std::unique_ptr<Tegra::MemoryManager> gmmu;
- // Switch 2: maps GPU-accessible process memory at GPU address == CPU address.
- void Nx2MapIdentity(u64 address, u64 size);
+ // Switch 2: maps GPU-accessible process memory (CPU range) at a GPU address.
+ void Nx2Map(u64 gpu_address, u64 cpu_address, u64 size);
NvCore::SessionId nx2_session{};
+ std::map<std::pair<u64, u64>, DAddr> nx2_device_ranges;
};
} // namespace Service::Nvidia::Devices
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 1e766a6d06..cdf32683c9 100644
--- a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
+++ b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
@@ -30,13 +30,13 @@ inline NvResult Nx2UnknownIoctl(const char* device, Ioctl command, std::span<con
return NvResult::Success;
}
-// GPU-accessible memory a Switch 2 process registered through nx2:gpu (sub-session 101). The GPU
-// shares the process address space there: GPFIFO entries and pushbuffers use the CPU address as
-// the GPU address. Every nvhost-as-gpu address space maps each range at that address, including
-// ranges registered before the address space existed.
+// GPU mappings a Switch 2 process made through nx2:gpu: CreateGpuAccessibleMemory (sub 101) maps
+// the memory at GPU address == CPU address, and NormalMapGpuMemory4K (211) maps a memory handle at
+// a chosen GPU address (also in sparse regions). Every nvhost-as-gpu address space applies each
+// mapping, including ones made before the address space existed.
class Nx2GpuMemoryRegistry {
public:
- using Listener = std::function<void(u64 address, u64 size)>;
+ using Listener = std::function<void(u64 gpu_address, u64 cpu_address, u64 size)>;
static Nx2GpuMemoryRegistry& Get() {
static Nx2GpuMemoryRegistry registry;
@@ -44,17 +44,21 @@ public:
}
void Register(u64 address, u64 size) {
+ MapAt(address, address, size);
+ }
+
+ void MapAt(u64 gpu_address, u64 cpu_address, u64 size) {
std::scoped_lock lk{mutex};
- ranges.emplace_back(address, size);
+ mappings.push_back({gpu_address, cpu_address, size});
for (auto& [owner, listener] : listeners) {
- listener(address, size);
+ listener(gpu_address, cpu_address, size);
}
}
void AddListener(const void* owner, Listener listener) {
std::scoped_lock lk{mutex};
- for (const auto& [address, size] : ranges) {
- listener(address, size);
+ for (const auto& m : mappings) {
+ listener(m.gpu_address, m.cpu_address, m.size);
}
listeners.insert_or_assign(owner, std::move(listener));
}
@@ -65,8 +69,14 @@ public:
}
private:
+ struct Mapping {
+ u64 gpu_address;
+ u64 cpu_address;
+ u64 size;
+ };
+
std::mutex mutex;
- std::vector<std::pair<u64, u64>> ranges;
+ std::vector<Mapping> mappings;
std::map<const void*, Listener> listeners;
};
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index a86f85ea0e..0a85ef5926 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -214,8 +214,11 @@ public:
{33, &INx2GpuSession::GetCount33, "Unknown33 (u32 count)"},
{203, &INx2GpuSession::GetVaRange, "GetVaRange (guessed)"},
// Sub-session commands (gpu 22 hands out a second session to this same object).
- {101, &INx2GpuSession::SubRegisterMemory, "Sub101 RegisterMemory (guessed)"},
- {211, &INx2GpuSession::SubSubmitJob, "Sub211 SubmitJob (guessed)"},
+ {101, &INx2GpuSession::SubRegisterMemory, "CreateGpuAccessibleMemory"},
+ {102, &INx2GpuSession::SubSlotJob<3>, "Sub102 (job, slot in word 3)"},
+ {211, &INx2GpuSession::SubSubmitJob, "NormalMapGpuMemory4K"},
+ {213, &INx2GpuSession::SubSlotJob<8>, "NormalUnmapGpuMemory4K"},
+ {214, &INx2GpuSession::SubSlotJob<11>, "CreateSparseRegion"},
});
}
@@ -302,6 +305,20 @@ private:
start + 0x1000, start);
}
}
+ // nn::gpu::sdk::CreateGpuAccessibleMemory stores word 3 of the reply as the
+ // GpuMemoryHandle that later map jobs (211) name.
+ const u32 handle = m_next_memory_handle++;
+ m_memory[handle] = {start, last - start + 1};
+ TipcReplyWords(ctx, {handle});
+ }
+
+ // Jobs that complete through a job-slot event (word Slot of the request), like 211.
+ template <size_t Slot>
+ void SubSlotJob(HLERequestContext& ctx) {
+ const auto* w = ctx.CommandBuffer();
+ LOG_DEBUG(Service, "(STUBBED) nx2:gpu {} [2]={:#x} [3]={:#x} [4]={:#x} [5]={:#x} [6]={:#x} slot={}",
+ ctx.GetCommand(), w[2], w[3], w[4], w[5], w[6], w[Slot]);
+ EventFor(w[Slot])->Signal(system.Kernel());
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
@@ -312,15 +329,35 @@ private:
void SubSubmitJob(HLERequestContext& ctx) {
const auto* cmd_buf = ctx.CommandBuffer();
const u32 slot = cmd_buf[17];
- LOG_DEBUG(Service, "(STUBBED) nx2:gpu sub 211 [2]={} slot={}, signalling completion",
- cmd_buf[2], slot);
+ const u64 a = u64{cmd_buf[3]} | (u64{cmd_buf[4]} << 32);
+ const u64 b = u64{cmd_buf[5]} | (u64{cmd_buf[6]} << 32);
+ const u64 c = u64{cmd_buf[8]} | (u64{cmd_buf[9]} << 32);
+ // Words 3-4: first GPU address, 5-6: last, 7: memory handle, 8-9: offset into it.
+ const u32 handle = cmd_buf[7];
+ LOG_DEBUG(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) {
+ const u64 cpu = it->second.address + c;
+ if (cpu != a) {
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapAt(a, cpu, b - a + 1);
+ }
+ } else {
+ LOG_WARNING(Service, "nx2:gpu 211 map of unknown handle {}", handle);
+ }
EventFor(slot)->Signal(system.Kernel());
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
+ struct GpuMemory {
+ u64 address;
+ u64 size;
+ };
+
KernelHelpers::ServiceContext m_context;
std::array<Kernel::KEvent*, NumIds> m_events{};
+ std::map<u32, GpuMemory> m_memory;
+ u32 m_next_memory_handle{1};
};
// Placeholder for the Switch 2 display session (nn::vi2::sdk), handed out by gsv 305. Reply
diff --git a/tools/nx2/gdbrsp.py b/tools/nx2/gdbrsp.py
index f0acd2aebd..d7c8a16460 100644
--- a/tools/nx2/gdbrsp.py
+++ b/tools/nx2/gdbrsp.py
@@ -66,8 +66,18 @@ class Rsp:
def main():
+ import re
+ import time
rsp = Rsp(int(sys.argv[1]))
- print("halt reason:", rsp.cmd("?"))
+ # Until the guest process exists the stub answers with a bogus thread id; commands sent then
+ # are lost, so wait for a real one.
+ for _ in range(120):
+ reason = rsp.cmd("?")
+ m = re.search(r"thread:([0-9a-f]+);", reason)
+ if m and int(m.group(1), 16) < 0x10000:
+ break
+ time.sleep(0.5)
+ print("halt reason:", reason)
regs = {}
for line in open(sys.argv[2]):
parts = line.split()
@@ -236,6 +246,21 @@ def main():
break
rsp.cmd(f"z0,{addr:x},4")
rsp.cmd("s")
+ elif op == "watch":
+ # watch <addr> <n> [stopval]: write watchpoint on a u64; print pc, lr and the new value
+ # for up to n hits, stopping early once it equals stopval.
+ addr, n = int(parts[1], 16), int(parts[2])
+ stopval = int(parts[3], 16) if len(parts) > 3 else None
+ print("watch ->", rsp.cmd(f"Z2,{addr:x},8"))
+ for i in range(n):
+ reply = rsp.cmd("c")
+ r = rsp.regs()
+ regs = r
+ d = rsp.mem(addr, 8)
+ val = struct.unpack("<Q", d)[0] if len(d) == 8 else None
+ print(f" w{i} {reply[:40]} pc={r.get('pc', 0):#x} lr={r.get('x30', 0) & 0xFFFFFFFFFFFF:#x} value={val if val is None else hex(val)}")
+ if stopval is not None and val == stopval:
+ break
elif op == "sleep":
import time
time.sleep(float(parts[1]))