diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-27 17:39:54 -0400 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-27 17:39:54 -0400 |
| commit | 4af363d798adf3b906fcfa5cba37ec9304f27dab (patch) | |
| tree | 9cced5569a7189aac1a06d51299c3e7902159f48 | |
| parent | 2226865a47ed3c97c4f4631ff50660d11a152da7 (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.md | 17 | ||||
| -rw-r--r-- | src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp | 29 | ||||
| -rw-r--r-- | src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h | 5 | ||||
| -rw-r--r-- | src/core/hle/service/nvdrv/devices/nx2_stub_device.h | 30 | ||||
| -rw-r--r-- | src/core/hle/service/nx2_gsv.h | 45 | ||||
| -rw-r--r-- | tools/nx2/gdbrsp.py | 27 |
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])) |
