aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 16:19:35 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 16:19:35 -0400
commit2835fec7fc66e1a1659710de8eddf2dfe67e20f9 (patch)
tree071b0aa0c55324c44241985379a63dc275cb0754 /src
parent81cff29955cd1130ae83ec658adf5e6cd0264399 (diff)
Switch 2 GPU: syncpt server, identity GPU mappings, Ampere classes, submit fences
- gsv 204: real syncpoint shared page (values mirrored from host1x, armed waits acknowledged by a 500us poller); first handle is the writable request event. - syncpt 22/201/202: per-slot events signalled through host1x guest actions. - nx2:gpu sub 101 memory is identity-mapped (GPU VA == CPU VA) into every nvhost-as-gpu GMMU via Nx2GpuMemoryRegistry; SMMU physical range widened to 34 bits for the 10/12 GiB layouts. - Puller runs Ampere 3D/compute/copy classes on the Maxwell engines; class 0 unbinds. - Turing+ macros are skipped (Eden only has the Fermi macro ISA). - Submits with increment_value get Eden-side syncpoint increments, since NVN2 signals completion with semaphore releases only.
Diffstat (limited to 'src')
-rw-r--r--src/core/device_memory_manager.h2
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp47
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h4
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp2
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp39
-rw-r--r--src/core/hle/service/nvdrv/devices/nx2_stub_device.h45
-rw-r--r--src/core/hle/service/nx2_gsv.h235
-rw-r--r--src/video_core/CMakeLists.txt1
-rw-r--r--src/video_core/engines/maxwell_3d.cpp7
-rw-r--r--src/video_core/engines/puller.cpp29
-rw-r--r--src/video_core/nx2.h14
11 files changed, 397 insertions, 28 deletions
diff --git a/src/core/device_memory_manager.h b/src/core/device_memory_manager.h
index b4b3b46088..b80c02ef36 100644
--- a/src/core/device_memory_manager.h
+++ b/src/core/device_memory_manager.h
@@ -139,7 +139,7 @@ private:
static constexpr size_t device_virtual_bits = Traits::device_virtual_bits;
static constexpr size_t device_as_size = 1ULL << device_virtual_bits;
static constexpr size_t physical_min_bits = 32;
- static constexpr size_t physical_max_bits = 33;
+ static constexpr size_t physical_max_bits = 34; // covers the 10 and 12 GiB layouts
static constexpr size_t page_bits = 12;
static constexpr size_t page_size = 1ULL << page_bits;
static constexpr size_t page_mask = page_size - 1ULL;
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 0733b5afaf..f309949c5f 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
@@ -14,13 +14,16 @@
#include "common/logging.h"
#include "core/core.h"
#include "core/hle/service/nvdrv/core/container.h"
+#include "core/hle/service/nvdrv/core/heap_mapper.h"
#include "core/hle/service/nvdrv/core/nvmap.h"
#include "core/hle/service/nvdrv/devices/ioctl_serialization.h"
#include "core/hle/service/nvdrv/devices/nvhost_as_gpu.h"
#include "core/hle/service/nvdrv/devices/nvhost_gpu.h"
+#include "core/hle/service/nvdrv/devices/nx2_stub_device.h"
#include "core/hle/service/nvdrv/nvdrv.h"
#include "video_core/control/channel_state.h"
#include "video_core/gpu.h"
+#include "video_core/host1x/host1x.h"
#include "video_core/memory_manager.h"
#include "video_core/rasterizer_interface.h"
@@ -30,7 +33,9 @@ nvhost_as_gpu::nvhost_as_gpu(Core::System& system_, Module& module_, NvCore::Con
: nvdevice{system_}, module{module_}, container{core}, nvmap{core.GetNvMapFile()}, vm{},
gmmu{} {}
-nvhost_as_gpu::~nvhost_as_gpu() = default;
+nvhost_as_gpu::~nvhost_as_gpu() {
+ Nx2GpuMemoryRegistry::Get().RemoveListener(this);
+}
NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> input,
std::span<u8> output) {
@@ -49,8 +54,14 @@ NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> i
return WrapFixed(this, &nvhost_as_gpu::MapBufferEx, input, output);
case 0x8:
return WrapFixed(this, &nvhost_as_gpu::GetVARegions1, input, output);
- case 0x9:
- return WrapFixed(this, &nvhost_as_gpu::AllocAsEx, input, output);
+ case 0x9: {
+ 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); });
+ }
+ return result;
+ }
case 0x14:
return WrapVariable(this, &nvhost_as_gpu::Remap, input, output);
default:
@@ -90,7 +101,35 @@ NvResult nvhost_as_gpu::Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8> i
return NvResult::NotImplemented;
}
-void nvhost_as_gpu::OnOpen(NvCore::SessionId session_id, DeviceFD fd) {}
+void nvhost_as_gpu::OnOpen(NvCore::SessionId session_id, DeviceFD fd) {
+ nx2_session = session_id;
+}
+
+void nvhost_as_gpu::Nx2MapIdentity(u64 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);
+ return;
+ }
+ DAddr device_address{};
+ if (session->has_preallocated_area && session->mapper->IsInBounds(address, size)) {
+ device_address = session->mapper->Map(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);
+ return;
+ }
+ smmu.Map(device_address, address, size, session->asid, true);
+ }
+ gmmu->Map(address, device_address, size, Tegra::PTEKind::PITCH, false);
+ LOG_INFO(Service_NVDRV, "Switch 2 GPU memory {:#x}+{:#x} mapped at the same GPU address", address,
+ size);
+}
void nvhost_as_gpu::OnClose(DeviceFD fd) {}
NvResult nvhost_as_gpu::AllocAsEx(IoctlAllocAsEx& params) {
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 9102252c96..419cf4cce7 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
@@ -221,6 +221,10 @@ private:
bool absolute_big_page_limit{}; //!< Switch 2 layout: big-page limit is va_range_end
} 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);
+ NvCore::SessionId nx2_session{};
};
} // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
index b15828aedc..53c7f85517 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
@@ -14,6 +14,7 @@
#include "core/hle/service/nvdrv/devices/ioctl_serialization.h"
#include "core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.h"
#include "core/hle/service/nvdrv/devices/nx2_stub_device.h"
+#include "video_core/nx2.h"
#include "core/hle/service/nvdrv/nvdrv.h"
namespace Service::Nvidia::Devices {
@@ -290,6 +291,7 @@ NvResult nvhost_ctrl_gpu::GetCharacteristics3(
if (nx2) {
nx2_caller = true;
g_nx2_driver = true;
+ Tegra::g_nx2_ampere = true;
ApplyNx2Characteristics(params.gc);
params.gpu_characteristics_buf_size = Nx2CharacteristicsSize;
LOG_WARNING(Service_NVDRV, "Switch 2 caller, reporting Ampere chip 0x17E");
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
index 15458cd82d..b650d3cac8 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
@@ -367,6 +367,27 @@ static boost::container::small_vector<Tegra::CommandHeader, 512> BuildIncrementC
return result;
}
+// Switch 2 pushbuffers signal completion with semaphore releases, never host1x syncpoint methods,
+// but the driver still has the kernel raise the syncpoint max by the submission's increment value
+// and then waits for that value. Append the increments after its commands.
+static boost::container::small_vector<Tegra::CommandHeader, 512> BuildIncrementCountCommandList(
+ NvFence fence, u32 count) {
+ boost::container::small_vector<Tegra::CommandHeader, 512> result{
+ Tegra::BuildCommandHeader(Tegra::BufferMethods::WaitForIdle, 1,
+ Tegra::SubmissionMode::Increasing),
+ {},
+ Tegra::BuildCommandHeader(Tegra::BufferMethods::SyncpointPayload, 1,
+ Tegra::SubmissionMode::Increasing),
+ {}};
+ for (u32 i = 0; i < count; ++i) {
+ result.push_back(Tegra::BuildCommandHeader(Tegra::BufferMethods::SyncpointOperation, 1,
+ Tegra::SubmissionMode::Increasing));
+ result.push_back(
+ BuildFenceAction(Tegra::Engines::Puller::FenceOperation::Increment, fence.id));
+ }
+ return result;
+}
+
static boost::container::small_vector<Tegra::CommandHeader, 512> BuildIncrementWithWfiCommandList(
NvFence fence) {
boost::container::small_vector<Tegra::CommandHeader, 512> result{
@@ -393,6 +414,13 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
const auto bind_id = channel_state->bind_id;
+ if (g_nx2_driver) {
+ for (const auto& entry : entries.command_lists) {
+ LOG_DEBUG(Service_NVDRV, "nx2 GPFIFO entry raw={:#018x} addr={:#x} size={:#x}", entry.raw,
+ entry.addr.Value(), entry.size.Value());
+ }
+ }
+
auto& flags = params.flags;
if (flags.fence_wait.Value()) {
@@ -412,6 +440,11 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
params.fence.value = syncpoint_manager.IncrementSyncpointMaxExt(channel_syncpoint, increment);
gpu.PushGPUEntries(bind_id, std::move(entries));
+ if (g_nx2_driver && !flags.fence_increment.Value() && increment > 0) {
+ gpu.PushGPUEntries(bind_id,
+ Tegra::CommandList{BuildIncrementCountCommandList(params.fence, increment)});
+ }
+
if (flags.fence_increment.Value()) {
if (flags.suppress_wfi.Value()) {
gpu.PushGPUEntries(bind_id,
@@ -422,6 +455,12 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
}
}
+ if (g_nx2_driver) {
+ LOG_DEBUG(Service_NVDRV,
+ "nx2 SubmitGPFIFO entries={} flags={:#x} fence_out=({}, {}) guest_value={}",
+ params.num_entries, flags.raw, params.fence.id, params.fence.value,
+ system.Host1x().GetSyncpointManager().GetGuestSyncpointValue(params.fence.id));
+ }
flags.raw = 0;
return NvResult::Success;
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 4e8862a10a..1e766a6d06 100644
--- a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
+++ b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
@@ -5,7 +5,12 @@
#include <algorithm>
#include <atomic>
+#include <functional>
+#include <map>
+#include <mutex>
#include <string>
+#include <utility>
+#include <vector>
#include "common/logging.h"
#include "core/hle/service/nvdrv/devices/nvdevice.h"
@@ -25,6 +30,46 @@ 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.
+class Nx2GpuMemoryRegistry {
+public:
+ using Listener = std::function<void(u64 address, u64 size)>;
+
+ static Nx2GpuMemoryRegistry& Get() {
+ static Nx2GpuMemoryRegistry registry;
+ return registry;
+ }
+
+ void Register(u64 address, u64 size) {
+ std::scoped_lock lk{mutex};
+ ranges.emplace_back(address, size);
+ for (auto& [owner, listener] : listeners) {
+ listener(address, size);
+ }
+ }
+
+ void AddListener(const void* owner, Listener listener) {
+ std::scoped_lock lk{mutex};
+ for (const auto& [address, size] : ranges) {
+ listener(address, size);
+ }
+ listeners.insert_or_assign(owner, std::move(listener));
+ }
+
+ void RemoveListener(const void* owner) {
+ std::scoped_lock lk{mutex};
+ listeners.erase(owner);
+ }
+
+private:
+ std::mutex mutex;
+ std::vector<std::pair<u64, u64>> ranges;
+ std::map<const void*, Listener> listeners;
+};
+
// Device node that Switch 2 nvgpu drivers open but Switch 1 never had (e.g. /dev/nvhost-tsg-gpu).
// Every ioctl succeeds with a zeroed reply and is logged, so the driver's use of it shows up.
class nx2_stub_device final : public nvdevice {
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index e8f31d0556..ef222befca 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -5,9 +5,12 @@
#include <array>
#include <initializer_list>
+#include <atomic>
+#include <map>
#include <vector>
#include "core/core.h"
+#include "core/core_timing.h"
#include "core/hle/kernel/k_client_session.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/k_resource_limit.h"
@@ -17,9 +20,11 @@
#include "core/hle/kernel/k_session.h"
#include "core/hle/kernel/k_transfer_memory.h"
#include "core/memory.h"
+#include "video_core/host1x/host1x.h"
#include "core/hle/service/hle_ipc.h"
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
+#include "core/hle/service/nvdrv/devices/nx2_stub_device.h"
#include "core/hle/service/nvdrv/nvdrv.h"
#include "core/hle/service/nvdrv/nvdrv_interface.h"
#include "core/hle/service/nx2_stub_interface.h"
@@ -79,19 +84,113 @@ public:
// Placeholder for the Switch 2 syncpoint server (nn::syncpt), handed out by gsv 202. TIPC 1
// returns a u32 the SDK requires to be < 0x400 and then passes min(value, 63) to TIPC 21.
// Everything else is auto-stubbed through the nx2: prefix.
+// - TIPC 22(u32 slot): an event handle for that slot (copy handle at word 3, result at word 4).
+// NVN's NvOsEventPool asks for slots and multi-waits on them.
+// - TIPC 201(slot, counter, u64 value, mode): signal the slot's event once the counter reaches
+// value. The counter is a SyncCounterId, the bitwise NOT of the host1x syncpoint id; mode 0
+// compares 32-bit wrapping, mode 1 64-bit. Eden's guest syncpoints back it.
+// - TIPC 202(slot): cancel the slot's request.
class INx2SyncptSession final : public ServiceFramework<INx2SyncptSession> {
public:
- explicit INx2SyncptSession(Core::System& system_) : ServiceFramework{system_, "nx2:syncpt"} {
+ explicit INx2SyncptSession(Core::System& system_)
+ : ServiceFramework{system_, "nx2:syncpt"}, m_context{system_, "nx2:syncpt"} {
RegisterHandlersTipc({
{1, &INx2SyncptSession::GetCount, "GetCount (guessed)"},
+ {22, &INx2SyncptSession::GetSlotEvent, "GetSlotEvent (guessed)"},
+ {201, &INx2SyncptSession::RequestSlotEvent, "RequestSlotEvent (guessed)"},
+ {202, &INx2SyncptSession::CancelSlotEvent, "CancelSlotEvent (guessed)"},
});
}
+ ~INx2SyncptSession() override {
+ for (auto& [slot, request] : m_requests) {
+ Cancel(request);
+ }
+ for (auto& [slot, event] : m_events) {
+ m_context.CloseEvent(event);
+ }
+ }
+
private:
void GetCount(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) nx2:syncpt TIPC 1, returning 32");
TipcReplyWords(ctx, {32});
}
+
+ void GetSlotEvent(HLERequestContext& ctx) {
+ const u32 slot = ctx.CommandBuffer()[2];
+ LOG_WARNING(Service, "(STUBBED) nx2:syncpt TIPC 22 slot={:#x}, returning an event", slot);
+ auto*& event = m_events[slot];
+ if (event == nullptr) {
+ event = m_context.CreateEvent(fmt::format("nx2:syncpt:Slot{:x}", slot));
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 1, 0};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, event->GetReadableEvent());
+ }
+
+ // Map nodes are stable, so a registered action can hold a reference to its request.
+ struct SlotRequest {
+ u32 syncpoint{};
+ std::atomic<bool> pending{};
+ Tegra::Host1x::SyncpointManager::ActionHandle handle{};
+ };
+
+ void RequestSlotEvent(HLERequestContext& ctx) {
+ const auto* cmd_buf = ctx.CommandBuffer();
+ const u32 slot = cmd_buf[2];
+ const u32 syncpoint = ~cmd_buf[3];
+ const u64 value = u64{cmd_buf[4]} | (u64{cmd_buf[5]} << 32);
+ const u32 mode = cmd_buf[6];
+ LOG_INFO(Service, "nx2:syncpt TIPC 201 slot={:#x} syncpoint={} value={:#x} mode={}", slot,
+ syncpoint, value, mode);
+ auto*& event = m_events[slot];
+ if (event == nullptr) {
+ event = m_context.CreateEvent(fmt::format("nx2:syncpt:Slot{:x}", slot));
+ }
+ if (syncpoint >= HostSyncpoints) {
+ LOG_WARNING(Service, "nx2:syncpt TIPC 201 syncpoint {} out of range, signalling",
+ syncpoint);
+ event->Signal(system.Kernel());
+ TipcReplyWords(ctx, {});
+ return;
+ }
+ auto& request = m_requests[slot];
+ Cancel(request);
+ request.syncpoint = syncpoint;
+ request.pending = true;
+ auto& manager = system.Host1x().GetSyncpointManager();
+ // The action runs on the GPU thread under the manager's lock, or inline if the value is
+ // already reached.
+ request.handle = manager.RegisterGuestAction(
+ syncpoint, static_cast<u32>(value), [this, &request, event] {
+ request.pending = false;
+ event->Signal(system.Kernel());
+ });
+ TipcReplyWords(ctx, {});
+ }
+
+ void CancelSlotEvent(HLERequestContext& ctx) {
+ const u32 slot = ctx.CommandBuffer()[2];
+ LOG_INFO(Service, "nx2:syncpt TIPC 202 slot={:#x}", slot);
+ if (auto it = m_requests.find(slot); it != m_requests.end()) {
+ Cancel(it->second);
+ }
+ TipcReplyWords(ctx, {});
+ }
+
+ void Cancel(SlotRequest& request) {
+ if (request.pending.exchange(false)) {
+ system.Host1x().GetSyncpointManager().DeregisterGuestAction(request.syncpoint,
+ request.handle);
+ }
+ }
+
+ static constexpr u32 HostSyncpoints = 192; // Tegra::Host1x::SyncpointManager
+
+ KernelHelpers::ServiceContext m_context;
+ std::map<u32, Kernel::KEvent*> m_events;
+ std::map<u32, SlotRequest> m_requests;
};
// Placeholder for the Switch 2 GPU session (nn::gpu::sdk), handed out by gsv 497.
@@ -186,8 +285,13 @@ private:
// Sub 101: (start u64, last u64, flags...) after CreateGpuAccessibleMemory's svc 0x80.
void SubRegisterMemory(HLERequestContext& ctx) {
const auto* b = ctx.CommandBuffer();
- LOG_WARNING(Service, "(STUBBED) nx2:gpu sub 101 start={:#x} last={:#x} [6]={:#x} [7]={:#x}",
- u64{b[2]} | (u64{b[3]} << 32), u64{b[4]} | (u64{b[5]} << 32), b[6], b[7]);
+ const u64 start = u64{b[2]} | (u64{b[3]} << 32);
+ const u64 last = u64{b[4]} | (u64{b[5]} << 32);
+ LOG_WARNING(Service, "nx2:gpu sub 101 start={:#x} last={:#x} [6]={:#x} [7]={:#x}", start,
+ last, b[6], b[7]);
+ if (last > start) {
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Register(start, last - start + 1);
+ }
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
@@ -275,6 +379,17 @@ public:
});
}
+ ~INx2GraphicsSystemSession() override {
+ if (m_syncpt_poll) {
+ system.CoreTiming().UnscheduleEvent(m_syncpt_poll);
+ }
+ for (auto* event : m_syncpt_events) {
+ if (event != nullptr) {
+ m_context.CloseEvent(event);
+ }
+ }
+ }
+
private:
// Index of the first raw data word of a TIPC request: header (2 words), then an optional
// special header with pid and copy/move handle words.
@@ -302,42 +417,109 @@ private:
// nn::syncpt::sdk::InitializeEx sends two transfer memories (0x1000 bytes RW, then 0x2000
// bytes the client maps read-only) and expects the server to have written a header into the
- // second one: u32 version (must be 1) at +0, then u32s at +4 and +8 (a count; the client
- // requires ((count + 0x40) >> 6) - 1 < 0x40) and five offsets at +0xc..+0x1c (into the RO,
- // RW, RO, RW and RO regions). Reply: two handles (words 3 and 4), then the result.
+ // second one: u32 version (must be 1) at +0, u32 base id at +4, u32 count at +8 (the client
+ // requires ((count + 0x40) >> 6) - 1 < 0x40), then offsets of: the u64 syncpoint values (RO,
+ // +0xc), the RW "armed" summary word (+0x10), the RO summary word (+0x14), the RW armed bits
+ // (+0x18) and the RO acknowledge bits (+0x1c).
+ // To wait, the client makes its RW bit differ from the RO bit, signals the first returned
+ // event (it attaches that handle as writable), and waits on the second until the server makes
+ // the bits equal again; it then rereads the value and rearms if the threshold isn't reached.
+ // Reply: the two handles (words 3 and 4), then the result.
+ static constexpr u32 SyncptValuesOffset = 0x40; // RO, u64 per syncpoint
+ static constexpr u32 SyncptRoSummaryOffset = 0x1040;
+ static constexpr u32 SyncptRoBitsOffset = 0x1100; // RO, up to 0x40 u64 words
+ static constexpr u32 SyncptRwSummaryOffset = 0x0;
+ static constexpr u32 SyncptRwBitsOffset = 0x100; // RW, up to 0x40 u64 words
+ static constexpr u32 HostSyncpoints = 192; // Tegra::Host1x::SyncpointManager
+
void AttachSyncptMemory(HLERequestContext& ctx) {
// RequestParser doesn't skip TIPC handle words, so read the raw data words.
const auto* cmd_buf = ctx.CommandBuffer();
const size_t data = TipcDataOffset(cmd_buf);
const u32 version_requested = cmd_buf[data];
- const u32 max_syncpoints = cmd_buf[data + 1];
+ const u32 max_syncpoints = std::min<u32>(cmd_buf[data + 1], (0x1000 - 0x40) / 8);
LOG_WARNING(Service, "(STUBBED) nx2:gsv TIPC 204, version={} max_syncpoints={:#x}",
version_requested, max_syncpoints);
+ auto rw = ctx.GetObjectFromHandle<Kernel::KTransferMemory>(ctx.GetCopyHandle(0));
auto ro = ctx.GetObjectFromHandle<Kernel::KTransferMemory>(ctx.GetCopyHandle(1));
- if (ro.IsNotNull() && ro->GetSize() >= 0x2000) {
- auto& memory = ro->GetOwner()->GetMemory();
- const u64 base = GetInteger(ro->GetSourceAddress());
- memory.Write32(base + 0x00, 1); // version
- memory.Write32(base + 0x04, 0); // base id (guess)
- memory.Write32(base + 0x08, max_syncpoints); // count
- memory.Write32(base + 0x0c, 0x40); // -> RO region
- memory.Write32(base + 0x10, 0x000); // -> RW region
- memory.Write32(base + 0x14, 0x800); // -> RO region
- memory.Write32(base + 0x18, 0x800); // -> RW region
- memory.Write32(base + 0x1c, 0x1000); // -> RO region
+ if (rw.IsNotNull() && ro.IsNotNull() && ro->GetSize() >= 0x2000) {
+ m_syncpt_memory = &ro->GetOwner()->GetMemory();
+ m_syncpt_rw = GetInteger(rw->GetSourceAddress());
+ m_syncpt_ro = GetInteger(ro->GetSourceAddress());
+ m_syncpt_count = max_syncpoints;
+ auto& memory = *m_syncpt_memory;
+ memory.Write32(m_syncpt_ro + 0x00, 1); // version
+ memory.Write32(m_syncpt_ro + 0x04, 0); // base id (guess)
+ memory.Write32(m_syncpt_ro + 0x08, max_syncpoints);
+ memory.Write32(m_syncpt_ro + 0x0c, SyncptValuesOffset);
+ memory.Write32(m_syncpt_ro + 0x10, SyncptRwSummaryOffset);
+ memory.Write32(m_syncpt_ro + 0x14, SyncptRoSummaryOffset);
+ memory.Write32(m_syncpt_ro + 0x18, SyncptRwBitsOffset);
+ memory.Write32(m_syncpt_ro + 0x1c, SyncptRoBitsOffset);
+ UpdateSyncptValues();
+ if (!m_syncpt_poll) {
+ m_syncpt_poll = Core::Timing::CreateEvent(
+ "nx2:gsv:SyncptPoll",
+ [this](s64, std::chrono::nanoseconds) -> std::optional<std::chrono::nanoseconds> {
+ PollSyncpts();
+ return std::nullopt;
+ });
+ system.CoreTiming().ScheduleLoopingEvent(SyncptPollInterval, SyncptPollInterval,
+ m_syncpt_poll);
+ }
} else {
- LOG_ERROR(Service, "nx2:gsv TIPC 204: no read-only transfer memory");
+ LOG_ERROR(Service, "nx2:gsv TIPC 204: missing syncpt transfer memory");
}
if (m_syncpt_events[0] == nullptr) {
- m_syncpt_events[0] = m_context.CreateEvent("nx2:gsv:SyncptEvent0");
- m_syncpt_events[1] = m_context.CreateEvent("nx2:gsv:SyncptEvent1");
+ m_syncpt_events[0] = m_context.CreateEvent("nx2:gsv:SyncptRequest");
+ m_syncpt_events[1] = m_context.CreateEvent("nx2:gsv:SyncptNotify");
}
IPC::ResponseBuilder rb{ctx, 2, 2, 0};
rb.Push(ResultSuccess);
- rb.PushCopyObjects(ctx, m_syncpt_events[0]->GetReadableEvent(),
- m_syncpt_events[1]->GetReadableEvent());
+ rb.PushCopyObjects(ctx, *m_syncpt_events[0]); // signalled by the client
+ rb.PushCopyObjects(ctx, m_syncpt_events[1]->GetReadableEvent());
+ }
+
+ // Mirrors Eden's guest syncpoint values into the shared RO page. Returns true if any changed.
+ bool UpdateSyncptValues() {
+ auto& manager = system.Host1x().GetSyncpointManager();
+ bool changed = false;
+ for (u32 id = 0; id < std::min(m_syncpt_count, HostSyncpoints); ++id) {
+ const u64 value = manager.GetGuestSyncpointValue(id);
+ const u64 address = m_syncpt_ro + SyncptValuesOffset + id * 8;
+ if (m_syncpt_memory->Read64(address) != value) {
+ m_syncpt_memory->Write64(address, value);
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ // Answers every armed wait: the value page is current, so the client rechecks its threshold
+ // and rearms if it still has to wait. Acknowledging without a value change costs the client a
+ // wakeup per poll, which keeps waits whose syncpoint we never see from hanging.
+ void PollSyncpts() {
+ if (m_syncpt_memory == nullptr) {
+ return;
+ }
+ UpdateSyncptValues();
+ bool acked = false;
+ const u32 words = (m_syncpt_count + 63) / 64;
+ for (u32 w = 0; w < words; ++w) {
+ const u64 rw_bits = m_syncpt_memory->Read64(m_syncpt_rw + SyncptRwBitsOffset + w * 8);
+ const u64 ro_address = m_syncpt_ro + SyncptRoBitsOffset + w * 8;
+ if (m_syncpt_memory->Read64(ro_address) != rw_bits) {
+ m_syncpt_memory->Write64(ro_address, rw_bits);
+ acked = true;
+ }
+ }
+ m_syncpt_memory->Write64(m_syncpt_ro + SyncptRoSummaryOffset,
+ m_syncpt_memory->Read64(m_syncpt_rw + SyncptRwSummaryOffset));
+ if (acked) {
+ m_syncpt_events[1]->Signal(system.Kernel());
+ }
}
void OpenViSession(HLERequestContext& ctx) {
@@ -372,8 +554,15 @@ private:
ReturnRawSession(system, ctx, std::move(nvsrv));
}
+ static constexpr std::chrono::nanoseconds SyncptPollInterval{std::chrono::microseconds{500}};
+
KernelHelpers::ServiceContext m_context;
std::array<Kernel::KEvent*, 2> m_syncpt_events{};
+ Core::Memory::Memory* m_syncpt_memory{};
+ u64 m_syncpt_rw{};
+ u64 m_syncpt_ro{};
+ u32 m_syncpt_count{};
+ std::shared_ptr<Core::Timing::EventType> m_syncpt_poll;
};
} // namespace Service
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 64d14ab2a6..312a6a09db 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -99,6 +99,7 @@ add_library(video_core STATIC
invalidation_accumulator.h
memory_manager.cpp
memory_manager.h
+ nx2.h
present.h
pte_kind.h
query_cache/bank_base.h
diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp
index e7da9d9138..1a43133296 100644
--- a/src/video_core/engines/maxwell_3d.cpp
+++ b/src/video_core/engines/maxwell_3d.cpp
@@ -12,6 +12,7 @@
#include "common/settings.h"
#include "core/core.h"
#include "core/core_timing.h"
+#include "video_core/nx2.h"
#include "video_core/dirty_flags.h"
#include "video_core/engines/maxwell_3d.h"
#include "video_core/engines/maxwell_3d.h"
@@ -380,6 +381,12 @@ void Maxwell3D::CallMacroMethod(Core::System& system, u32 method, const std::vec
const u32 entry =
((method - MacroRegistersStart) >> 1) % static_cast<u32>(macro_positions.size());
+ // Switch 2 uploads Turing+ (MME64) macros, which Eden's Fermi-style macro engine can't run.
+ if (g_nx2_ampere) {
+ LOG_DEBUG(HW_GPU, "Switch 2 macro {} with {} parameters skipped", entry, parameters.size());
+ return;
+ }
+
// Execute the current macro.
macro_engine.Execute(system, *this, macro_positions[entry], parameters);
draw_manager.DrawDeferred(*this);
diff --git a/src/video_core/engines/puller.cpp b/src/video_core/engines/puller.cpp
index 0b05d31718..c9c1153bbf 100644
--- a/src/video_core/engines/puller.cpp
+++ b/src/video_core/engines/puller.cpp
@@ -4,6 +4,8 @@
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
+#include <cstdlib>
+
#include "common/assert.h"
#include "common/logging.h"
#include "common/settings.h"
@@ -18,6 +20,7 @@
#include "video_core/engines/puller.h"
#include "video_core/gpu.h"
#include "video_core/memory_manager.h"
+#include "video_core/nx2.h"
#include "video_core/rasterizer_interface.h"
namespace Tegra::Engines {
@@ -35,6 +38,27 @@ void Puller::ProcessBindMethod(DmaPusher& dma_pusher, const MethodCall& method_c
if ((engine & ~Gf100BindClassMask) != 0 && (engine & ~Gf100BindValidMask) == 0) {
engine &= Gf100BindClassMask;
}
+ // Switch 2 binds Ampere classes; run them on the Maxwell engines (same method layout for
+ // most state). Class 0 unbinds the subchannel.
+ switch (engine & 0xFFFF) {
+ case 0xC797: // AMPERE_B
+ engine = static_cast<u32>(EngineID::MAXWELL_B);
+ break;
+ case 0xC7C0: // AMPERE_COMPUTE_B
+ engine = static_cast<u32>(EngineID::KEPLER_COMPUTE_B);
+ break;
+ case 0xC7B5: // AMPERE_DMA_COPY_B
+ engine = static_cast<u32>(EngineID::MAXWELL_DMA_COPY_A);
+ break;
+ case 0x0:
+ if (g_nx2_ampere) {
+ LOG_DEBUG(HW_GPU, "Switch 2 unbinds subchannel {}", method_call.subchannel);
+ return;
+ }
+ break;
+ default:
+ break;
+ }
const auto engine_id = static_cast<EngineID>(engine);
bound_engines[method_call.subchannel] = engine_id;
@@ -139,6 +163,11 @@ void Puller::ProcessSemaphoreAcquire(DmaPusher& dma_pusher) {
/// Calls a GPU puller method.
void Puller::CallPullerMethod(DmaPusher& dma_pusher, const MethodCall& method_call) {
+ static const bool nx2_trace = std::getenv("EDEN_NX2_PULLER_TRACE") != nullptr;
+ if (nx2_trace) {
+ LOG_INFO(HW_GPU, "nx2 puller sub={} method={:#x} arg={:#x}", method_call.subchannel,
+ method_call.method, method_call.argument);
+ }
regs.reg_array[method_call.method] = method_call.argument;
const auto method = static_cast<BufferMethods>(method_call.method);
diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h
new file mode 100644
index 0000000000..b8258ca243
--- /dev/null
+++ b/src/video_core/nx2.h
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <atomic>
+
+namespace Tegra {
+
+// Set once a Switch 2 (Ampere, NVN2) driver is talking to the GPU. Its command streams use
+// Ampere classes and the Turing+ macro ISA, which Eden's Maxwell engines don't implement.
+inline std::atomic<bool> g_nx2_ampere{false};
+
+} // namespace Tegra