aboutsummaryrefslogtreecommitdiff
path: root/src/core
diff options
context:
space:
mode:
authorDjihads <76681596+Djihads80@users.noreply.github.com>2026-10-29 02:25:36 +0100
committerGitHub <noreply@github.com>2026-10-29 02:25:36 +0100
commit0bb5055b6541c74f47603a8dff0569a509cbaf18 (patch)
treeda827ff9c4f2f9a5309ee2804ee689f608c0ac2f /src/core
parent1af7fe72927af9fc64aa4af117bc9b30043c4dc7 (diff)
parent535b6769709eb22333af26269f1b16f37adce4db (diff)
Merge pull request #1 from Djihads80/yumekiru/ns2-progress-v2
Experimental NS2 support and accumulated GPU fixes
Diffstat (limited to 'src/core')
-rw-r--r--src/core/CMakeLists.txt14
-rw-r--r--src/core/arm/dynarmic/arm_dynarmic_64.cpp3
-rw-r--r--src/core/device_memory_manager.inc3
-rw-r--r--src/core/file_sys/program_metadata.cpp55
-rw-r--r--src/core/file_sys/program_metadata.h8
-rw-r--r--src/core/hle/kernel/k_memory_block.h4
-rw-r--r--src/core/hle/kernel/k_page_table_base.cpp3
-rw-r--r--src/core/hle/kernel/k_process.cpp1
-rw-r--r--src/core/hle/kernel/k_process.h5
-rw-r--r--src/core/hle/kernel/kernel.cpp12
-rw-r--r--src/core/hle/kernel/svc.cpp4
-rw-r--r--src/core/hle/kernel/svc.h2
-rw-r--r--src/core/hle/kernel/svc/svc_info.cpp27
-rw-r--r--src/core/hle/kernel/svc/svc_ipc.cpp19
-rw-r--r--src/core/hle/kernel/svc/svc_memory.cpp25
-rw-r--r--src/core/hle/kernel/svc/svc_synchronization.cpp21
-rw-r--r--src/core/hle/kernel/svc/svc_thread.cpp11
-rw-r--r--src/core/hle/kernel/svc_types.h1
-rw-r--r--src/core/hle/service/am/applet.h1
-rw-r--r--src/core/hle/service/am/service/application_functions.cpp14
-rw-r--r--src/core/hle/service/am/service/application_functions.h2
-rw-r--r--src/core/hle/service/am/service/application_proxy.cpp19
-rw-r--r--src/core/hle/service/am/service/application_proxy.h6
-rw-r--r--src/core/hle/service/am/service/application_proxy_service.cpp11
-rw-r--r--src/core/hle/service/am/service/application_proxy_service.h4
-rw-r--r--src/core/hle/service/am/service/gpu_memory_registration.h22
-rw-r--r--src/core/hle/service/am/service/gpu_session.cpp633
-rw-r--r--src/core/hle/service/am/service/gpu_session.h45
-rw-r--r--src/core/hle/service/am/service/graphics_scaling_parameters_functions.cpp45
-rw-r--r--src/core/hle/service/am/service/graphics_scaling_parameters_functions.h34
-rw-r--r--src/core/hle/service/am/service/graphics_session.cpp33
-rw-r--r--src/core/hle/service/am/service/graphics_session.h19
-rw-r--r--src/core/hle/service/am/service/graphics_supervisor.cpp1612
-rw-r--r--src/core/hle/service/am/service/graphics_supervisor.h93
-rw-r--r--src/core/hle/service/am/service/graphics_supervisor_protocol.h37
-rw-r--r--src/core/hle/service/am/service/graphics_syncpoint_cache.h49
-rw-r--r--src/core/hle/service/am/service/native_image_layout.h40
-rw-r--r--src/core/hle/service/am/service/performance_functions.cpp19
-rw-r--r--src/core/hle/service/am/service/performance_functions.h16
-rw-r--r--src/core/hle/service/am/service/self_controller.cpp45
-rw-r--r--src/core/hle/service/am/service/self_controller.h3
-rw-r--r--src/core/hle/service/audio/audio.cpp64
-rw-r--r--src/core/hle/service/hid/hid_server.cpp80
-rw-r--r--src/core/hle/service/hid/hid_server.h24
-rw-r--r--src/core/hle/service/ngc/ngc.cpp50
-rw-r--r--src/core/hle/service/nvdrv/core/syncpoint_manager.cpp8
-rw-r--r--src/core/hle/service/nvdrv/core/syncpoint_manager.h3
-rw-r--r--src/core/hle/service/nvdrv/devices/gpu_address_space_layout.h34
-rw-r--r--src/core/hle/service/nvdrv/devices/native_gpu_characteristics.h67
-rw-r--r--src/core/hle/service/nvdrv/devices/native_gpu_context.h23
-rw-r--r--src/core/hle/service/nvdrv/devices/native_gpu_kind.h51
-rw-r--r--src/core/hle/service/nvdrv/devices/nvdevice.h6
-rw-r--r--src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp61
-rw-r--r--src/core/hle/service/nvdrv/devices/nvdisp_disp0.h4
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp290
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h34
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp21
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp99
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_gpu.h11
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_tsg_gpu.cpp85
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_tsg_gpu.h38
-rw-r--r--src/core/hle/service/nvdrv/nvdrv.cpp19
-rw-r--r--src/core/hle/service/nvdrv/nvdrv_interface.cpp25
-rw-r--r--src/core/hle/service/nvdrv/nvdrv_interface.h2
-rw-r--r--src/core/hle/service/nvnflinger/buffer_queue_producer.cpp9
-rw-r--r--src/core/hle/service/nvnflinger/buffer_queue_producer.h2
-rw-r--r--src/core/hle/service/service.cpp28
-rw-r--r--src/core/launch_params.cpp2
-rw-r--r--src/core/loader/deconstructed_rom_directory.cpp39
-rw-r--r--src/core/loader/deconstructed_rom_directory.h1
-rw-r--r--src/core/loader/loader.cpp7
71 files changed, 4139 insertions, 68 deletions
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 1b578f67ba..168037e5dc 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -460,6 +460,18 @@ add_library(core STATIC
hle/service/am/service/application_proxy.h
hle/service/am/service/application_proxy_service.cpp
hle/service/am/service/application_proxy_service.h
+ hle/service/am/service/performance_functions.cpp
+ hle/service/am/service/performance_functions.h
+ hle/service/am/service/gpu_session.cpp
+ hle/service/am/service/gpu_session.h
+ hle/service/am/service/graphics_session.cpp
+ hle/service/am/service/graphics_session.h
+ hle/service/am/service/graphics_supervisor.cpp
+ hle/service/am/service/graphics_supervisor.h
+ hle/service/am/service/graphics_supervisor_protocol.h
+ hle/service/am/service/graphics_syncpoint_cache.h
+ hle/service/am/service/graphics_scaling_parameters_functions.cpp
+ hle/service/am/service/graphics_scaling_parameters_functions.h
hle/service/am/service/audio_controller.cpp
hle/service/am/service/audio_controller.h
hle/service/am/service/common_state_getter.cpp
@@ -855,6 +867,8 @@ add_library(core STATIC
hle/service/nvdrv/devices/nvhost_ctrl_gpu.h
hle/service/nvdrv/devices/nvhost_gpu.cpp
hle/service/nvdrv/devices/nvhost_gpu.h
+ hle/service/nvdrv/devices/nvhost_tsg_gpu.cpp
+ hle/service/nvdrv/devices/nvhost_tsg_gpu.h
hle/service/nvdrv/devices/nvhost_nvdec.cpp
hle/service/nvdrv/devices/nvhost_nvdec.h
hle/service/nvdrv/devices/nvhost_nvdec_common.cpp
diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.cpp b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
index 894857a86d..b5190e40cf 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_64.cpp
+++ b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
@@ -194,6 +194,7 @@ void ArmDynarmic64::MakeJit(Common::PageTable* page_table, std::size_t address_s
// Callbacks
config.callbacks = std::addressof(*m_cb);
+ config.enable_pauth_returns = m_cb->m_process && m_cb->m_process->UsesPAuthReturns();
// Memory
if (page_table) {
@@ -444,6 +445,8 @@ void ArmDynarmic64::ClearInstructionCache() {
}
void ArmDynarmic64::InvalidateCacheRange(u64 addr, std::size_t size) {
+ // Refetch the source page too, including instructions restored by the debugger.
+ m_cb->last_code_addr = u64(-1);
m_jit->InvalidateCacheRange(addr, size);
}
diff --git a/src/core/device_memory_manager.inc b/src/core/device_memory_manager.inc
index 97c2706abb..0087548978 100644
--- a/src/core/device_memory_manager.inc
+++ b/src/core/device_memory_manager.inc
@@ -21,6 +21,7 @@
#include "common/scope_exit.h"
#include "common/settings.h"
#include "core/device_memory.h"
+#include "core/hle/kernel/board/nintendo/nx/k_system_control.h"
#include "core/device_memory_manager.h"
#include "core/memory.h"
@@ -172,7 +173,7 @@ template <typename Traits>
DeviceMemoryManager<Traits>::DeviceMemoryManager(const DeviceMemory& device_memory_)
: physical_base{uintptr_t(device_memory_.buffer.BackingBasePointer())}
, 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))
+ , compressed_device_addr(Kernel::Board::Nintendo::Nx::KSystemControl::Init::GetIntendedMemorySize() >> Memory::YUZU_PAGEBITS)
, tracked_entries(device_as_size >> Memory::YUZU_PAGEBITS)
{
impl = std::make_unique<DeviceMemoryManagerAllocator<Traits>>();
diff --git a/src/core/file_sys/program_metadata.cpp b/src/core/file_sys/program_metadata.cpp
index 63ca9d3fe3..b528523e67 100644
--- a/src/core/file_sys/program_metadata.cpp
+++ b/src/core/file_sys/program_metadata.cpp
@@ -20,6 +20,10 @@ ProgramMetadata::ProgramMetadata() = default;
ProgramMetadata::~ProgramMetadata() = default;
Loader::ResultStatus ProgramMetadata::Load(VirtualFile file) {
+ uses_extended_acid = false;
+ if (!file) {
+ return Loader::ResultStatus::ErrorBadNPDMHeader;
+ }
const std::size_t total_size = file->GetSize();
if (total_size < sizeof(Header)) {
return Loader::ResultStatus::ErrorBadNPDMHeader;
@@ -29,17 +33,55 @@ Loader::ResultStatus ProgramMetadata::Load(VirtualFile file) {
return Loader::ResultStatus::ErrorBadNPDMHeader;
}
- if (sizeof(AcidHeader) != file->ReadObject(&acid_header, npdm_header.acid_offset)) {
+ if (npdm_header.magic != std::array{'M', 'E', 'T', 'A'}) {
+ return Loader::ResultStatus::ErrorBadNPDMHeader;
+ }
+
+ const auto contains = [](u64 offset, u64 size, u64 limit) {
+ return offset <= limit && size <= limit - offset;
+ };
+ const u64 acid_offset = npdm_header.acid_offset;
+ const u64 acid_size = npdm_header.acid_size;
+ const u64 aci_offset = npdm_header.aci_offset;
+ const u64 aci_size = npdm_header.aci_size;
+ if (acid_offset < sizeof(Header) || !contains(acid_offset, acid_size, total_size)) {
return Loader::ResultStatus::ErrorBadACIDHeader;
}
- if (sizeof(AciHeader) != file->ReadObject(&aci_header, npdm_header.aci_offset)) {
+ const auto read_acid = [&](u64 body_offset) {
+ return contains(body_offset, sizeof(AcidHeader), acid_size) &&
+ file->ReadObject(&acid_header, acid_offset + body_offset) == sizeof(AcidHeader) &&
+ acid_header.magic == std::array{'A', 'C', 'I', 'D'};
+ };
+ if (!read_acid(0x200)) {
+ if (!read_acid(0x300)) {
+ return Loader::ResultStatus::ErrorBadACIDHeader;
+ }
+ uses_extended_acid = true;
+ }
+
+ if (aci_offset < sizeof(Header) || !contains(aci_offset, aci_size, total_size) ||
+ aci_size < sizeof(AciHeader) ||
+ sizeof(AciHeader) != file->ReadObject(&aci_header, aci_offset) ||
+ aci_header.magic != std::array{'A', 'C', 'I', '0'}) {
return Loader::ResultStatus::ErrorBadACIHeader;
}
+ constexpr u64 fac_read_size = 4 + sizeof(FileAccessControl::permissions) +
+ sizeof(FileAccessControl::unknown);
+ if (acid_header.fac_size < fac_read_size ||
+ !contains(acid_header.fac_offset, acid_header.fac_size, acid_size)) {
+ return Loader::ResultStatus::ErrorBadFileAccessControl;
+ }
+ constexpr u64 fah_read_size = 4 + sizeof(FileAccessHeader::permissions) + 4 * sizeof(u32);
+ if (aci_header.fah_size < fah_read_size ||
+ !contains(aci_header.fah_offset, aci_header.fah_size, aci_size)) {
+ return Loader::ResultStatus::ErrorBadFileAccessHeader;
+ }
+
// Load acid_file_access per-component instead of the entire struct, since this struct does not
// reflect the layout of the real data.
- std::size_t current_offset = acid_header.fac_offset;
+ std::size_t current_offset = acid_offset + acid_header.fac_offset;
if (sizeof(FileAccessControl::version) != file->ReadBytes(&acid_file_access.version,
sizeof(FileAccessControl::version),
current_offset)) {
@@ -57,7 +99,7 @@ Loader::ResultStatus ProgramMetadata::Load(VirtualFile file) {
}
// Load aci_file_access per-component instead of the entire struct, same as acid_file_access
- current_offset = aci_header.fah_offset;
+ current_offset = aci_offset + aci_header.fah_offset;
if (sizeof(FileAccessHeader::version) != file->ReadBytes(&aci_file_access.version,
sizeof(FileAccessHeader::version),
current_offset)) {
@@ -89,6 +131,10 @@ Loader::ResultStatus ProgramMetadata::Load(VirtualFile file) {
return Loader::ResultStatus::ErrorBadFileAccessHeader;
}
+ if (aci_header.kac_size % sizeof(u32) != 0 ||
+ !contains(aci_header.kac_offset, aci_header.kac_size, aci_size)) {
+ return Loader::ResultStatus::ErrorBadKernelCapabilityDescriptors;
+ }
aci_kernel_capabilities.resize(aci_header.kac_size / sizeof(u32));
const u64 read_size = aci_header.kac_size;
const u64 read_offset = npdm_header.aci_offset + aci_header.kac_offset;
@@ -128,6 +174,7 @@ void ProgramMetadata::LoadManual(bool is_64_bit, ProgramAddressSpaceType address
u32 main_thread_stack_size, u64 title_id,
u64 filesystem_permissions, u32 system_resource_size,
KernelCapabilityDescriptors capabilities) {
+ uses_extended_acid = false;
npdm_header.has_64_bit_instructions.Assign(is_64_bit);
npdm_header.address_space_type.Assign(address_space);
npdm_header.main_thread_priority = static_cast<u8>(main_thread_prio);
diff --git a/src/core/file_sys/program_metadata.h b/src/core/file_sys/program_metadata.h
index 115e6d6cd1..eb4cf11894 100644
--- a/src/core/file_sys/program_metadata.h
+++ b/src/core/file_sys/program_metadata.h
@@ -77,6 +77,9 @@ public:
u8 GetMainThreadCore() const;
u32 GetMainThreadStackSize() const;
u64 GetTitleID() const;
+ bool UsesExtendedAcid() const {
+ return uses_extended_acid;
+ }
u64 GetFilesystemPermissions() const;
u32 GetSystemResourceSize() const;
PoolPartition GetPoolPartition() const;
@@ -116,8 +119,6 @@ private:
static_assert(sizeof(Header) == 0x80, "NPDM header structure size is wrong");
struct AcidHeader {
- std::array<u8, 0x100> signature;
- std::array<u8, 0x100> nca_modulus;
std::array<char, 4> magic;
u32_le nca_size;
std::array<u8, 0x4> reserved;
@@ -139,7 +140,7 @@ private:
INSERT_PADDING_BYTES(0x8);
};
- static_assert(sizeof(AcidHeader) == 0x240, "ACID header structure size is wrong");
+ static_assert(sizeof(AcidHeader) == 0x40, "ACID header body size is wrong");
struct AciHeader {
std::array<char, 4> magic;
@@ -177,6 +178,7 @@ private:
};
Header npdm_header{};
+ bool uses_extended_acid{};
AciHeader aci_header{};
AcidHeader acid_header{};
diff --git a/src/core/hle/kernel/k_memory_block.h b/src/core/hle/kernel/k_memory_block.h
index f6543bdb3a..2abca31d87 100644
--- a/src/core/hle/kernel/k_memory_block.h
+++ b/src/core/hle/kernel/k_memory_block.h
@@ -195,7 +195,9 @@ enum class KMemoryAttribute : u8 {
Uncached = static_cast<u8>(Svc::MemoryAttribute::Uncached),
PermissionLocked = static_cast<u8>(Svc::MemoryAttribute::PermissionLocked),
- SetMask = Uncached | PermissionLocked,
+ GpuAccessible = static_cast<u8>(Svc::MemoryAttribute::GpuAccessible),
+
+ SetMask = Uncached | PermissionLocked | GpuAccessible,
};
DECLARE_ENUM_FLAG_OPERATORS(KMemoryAttribute);
diff --git a/src/core/hle/kernel/k_page_table_base.cpp b/src/core/hle/kernel/k_page_table_base.cpp
index 2a0a7d49d5..fd5d2c2dce 100644
--- a/src/core/hle/kernel/k_page_table_base.cpp
+++ b/src/core/hle/kernel/k_page_table_base.cpp
@@ -2102,7 +2102,8 @@ Result KPageTableBase::SetMemoryAttribute(KProcessAddress addr, size_t size, KMe
constexpr KMemoryAttribute AttributeTestMask =
~(KMemoryAttribute::SetMask | KMemoryAttribute::DeviceShared);
const KMemoryState state_test_mask =
- (True(mask & KMemoryAttribute::Uncached) ? KMemoryState::FlagCanChangeAttribute
+ (True(mask & (KMemoryAttribute::Uncached | KMemoryAttribute::GpuAccessible))
+ ? KMemoryState::FlagCanChangeAttribute
: KMemoryState::None) |
(True(mask & KMemoryAttribute::PermissionLocked) ? KMemoryState::FlagCanPermissionLock
: KMemoryState::None);
diff --git a/src/core/hle/kernel/k_process.cpp b/src/core/hle/kernel/k_process.cpp
index 27d1b1bfc4..813d8e9402 100644
--- a/src/core/hle/kernel/k_process.cpp
+++ b/src/core/hle/kernel/k_process.cpp
@@ -1260,6 +1260,7 @@ Result KProcess::LoadFromMetadata(KernelCore& kernel, const FileSys::ProgramMeta
// Assign remaining properties.
m_ideal_core_id = metadata.GetMainThreadCore();
+ m_uses_pauth_returns = metadata.UsesExtendedAcid();
// Set up emulation context.
this->InitializeInterfaces(kernel);
diff --git a/src/core/hle/kernel/k_process.h b/src/core/hle/kernel/k_process.h
index b5693751c3..ca96591ed0 100644
--- a/src/core/hle/kernel/k_process.h
+++ b/src/core/hle/kernel/k_process.h
@@ -135,6 +135,7 @@ private:
bool m_is_signaled : 1 = false;
bool m_is_initialized : 1 = false;
bool m_is_application : 1 = false;
+ bool m_uses_pauth_returns : 1 = false;
bool m_is_default_application_system_resource : 1 = false;
bool m_is_suspended : 1 = false;
bool m_is_immortal : 1 = false;
@@ -248,6 +249,10 @@ public:
return m_is_application;
}
+ bool UsesPAuthReturns() const {
+ return m_uses_pauth_returns;
+ }
+
bool IsDefaultApplicationSystemResource() const {
return m_is_default_application_system_resource;
}
diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp
index 4f1d849209..c7beb9c3ce 100644
--- a/src/core/hle/kernel/kernel.cpp
+++ b/src/core/hle/kernel/kernel.cpp
@@ -120,13 +120,13 @@ struct KernelCore::Impl {
}
void TerminateAllProcesses() {
- std::scoped_lock lk{process_list_lock};
- for (auto& process : process_list) {
- process->Terminate(system.Kernel());
- process->Close(system.Kernel());
- process = nullptr;
+ auto& kernel = system.Kernel();
+ // Applet cleanup can remove processes while termination waits for its threads.
+ auto processes = kernel.GetProcessList();
+ for (auto& process : processes) {
+ process->Terminate(kernel);
+ kernel.RemoveProcess(process.GetPointerUnsafe());
}
- process_list.clear();
}
void Shutdown(KernelCore& kernel) {
diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp
index 3542d5a3b6..c91ae43dd8 100644
--- a/src/core/hle/kernel/svc.cpp
+++ b/src/core/hle/kernel/svc.cpp
@@ -2370,6 +2370,10 @@ static void Call32(Core::System& system, u32 imm, std::span<uint64_t, 8> args) {
static void Call64(Core::System& system, u32 imm, std::span<uint64_t, 8> args) {
switch (SvcId(imm)) {
+ case SvcId::SetGpuMemoryAttribute:
+ args[0] = SetGpuMemoryAttribute(system, args[0], args[1],
+ static_cast<u32>(args[2]), static_cast<u32>(args[3])).raw;
+ return;
case SvcId::SetHeapSize: return SvcWrap_SetHeapSize64(system, args);
case SvcId::SetMemoryPermission: return SvcWrap_SetMemoryPermission64(system, args);
case SvcId::SetMemoryAttribute: return SvcWrap_SetMemoryAttribute64(system, args);
diff --git a/src/core/hle/kernel/svc.h b/src/core/hle/kernel/svc.h
index 356c1c109a..422b8b8719 100644
--- a/src/core/hle/kernel/svc.h
+++ b/src/core/hle/kernel/svc.h
@@ -22,6 +22,7 @@ namespace Kernel::Svc {
Result SetHeapSize(Core::System& system, uint64_t* out_address, uint64_t size);
Result SetMemoryPermission(Core::System& system, uint64_t address, uint64_t size, MemoryPermission perm);
Result SetMemoryAttribute(Core::System& system, uint64_t address, uint64_t size, uint32_t mask, uint32_t attr);
+Result SetGpuMemoryAttribute(Core::System& system, uint64_t address, uint64_t size, uint32_t mask, uint32_t attr);
Result MapMemory(Core::System& system, uint64_t dst_address, uint64_t src_address, uint64_t size);
Result UnmapMemory(Core::System& system, uint64_t dst_address, uint64_t src_address, uint64_t size);
Result QueryMemory(Core::System& system, uint64_t out_memory_info, PageInfo* out_page_info, uint64_t address);
@@ -504,6 +505,7 @@ enum class SvcId : u32 {
CreateResourceLimit = 0x7d,
SetResourceLimitLimitValue = 0x7e,
CallSecureMonitor = 0x7f,
+ SetGpuMemoryAttribute = 0x80,
MapInsecureMemory = 0x90,
UnmapInsecureMemory = 0x91,
};
diff --git a/src/core/hle/kernel/svc/svc_info.cpp b/src/core/hle/kernel/svc/svc_info.cpp
index 863bb58fa8..aea7407542 100644
--- a/src/core/hle/kernel/svc/svc_info.cpp
+++ b/src/core/hle/kernel/svc/svc_info.cpp
@@ -14,11 +14,32 @@
#include "core/hle/kernel/svc_version.h"
namespace Kernel::Svc {
+namespace {
+// Identified from the extended SDK's RemoteRegionManager initialization.
+constexpr InfoType RemoteRegionAddress = static_cast<InfoType>(29);
+constexpr InfoType RemoteRegionSize = static_cast<InfoType>(30);
+constexpr InfoType RemoteMemoryUsed = static_cast<InfoType>(31);
+constexpr InfoType RemoteMemoryTotal = static_cast<InfoType>(32);
+constexpr InfoType MemoryMappingAlignment = static_cast<InfoType>(33);
+} // namespace
/// Gets system/memory information for the current process
Result GetInfo(Core::System& system, u64* result, InfoType info_id_type, Handle handle, u64 info_sub_id) {
LOG_TRACE(Kernel_SVC, "called info_id={:#x}, info_sub_id={:#x}, handle={:#08x}", info_id_type, info_sub_id, handle);
u32 info_id = u32(info_id_type);
+ if (info_id_type == RemoteRegionAddress || info_id_type == RemoteRegionSize ||
+ info_id_type == RemoteMemoryUsed || info_id_type == RemoteMemoryTotal ||
+ info_id_type == MemoryMappingAlignment) {
+ R_UNLESS(info_sub_id == 0, ResultInvalidEnumValue);
+ const auto& handle_table = GetCurrentProcess(system.Kernel()).GetHandleTable();
+ KScopedAutoObject process = handle_table.GetObject<KProcess>(system.Kernel(), handle);
+ R_UNLESS(process.IsNotNull(), ResultInvalidHandle);
+ R_UNLESS(process->UsesPAuthReturns(), ResultInvalidEnumValue);
+ // Remote mappings are absent; ordinary mappings use the kernel page size.
+ *result = info_id_type == MemoryMappingAlignment ? PageSize : 0;
+ LOG_DEBUG(Kernel_SVC, "Extended memory info_id={:#x}, value={:#x}", info_id, *result);
+ R_SUCCEED();
+ }
switch (info_id_type) {
case InfoType::CoreMask:
case InfoType::PriorityMask:
@@ -124,7 +145,8 @@ Result GetInfo(Core::System& system, u64* result, InfoType info_id_type, Handle
default:
break;
}
- LOG_ERROR(Kernel_SVC, "Unimplemented svcGetInfo id={:#016x}", info_id);
+ LOG_ERROR(Kernel_SVC, "Unimplemented svcGetInfo id={:#x}, sub_id={:#x}, handle={:#x}",
+ info_id, info_sub_id, handle);
R_THROW(ResultInvalidEnumValue);
}
case InfoType::DebuggerAttached:
@@ -258,7 +280,8 @@ Result GetInfo(Core::System& system, u64* result, InfoType info_id_type, Handle
R_SUCCEED();
}
default:
- LOG_ERROR(Kernel_SVC, "Unimplemented svcGetInfo id={:#016x}", info_id);
+ LOG_ERROR(Kernel_SVC, "Unimplemented svcGetInfo id={:#x}, sub_id={:#x}, handle={:#x}",
+ info_id, info_sub_id, handle);
R_THROW(ResultInvalidEnumValue);
}
}
diff --git a/src/core/hle/kernel/svc/svc_ipc.cpp b/src/core/hle/kernel/svc/svc_ipc.cpp
index 875590d6f7..fadde6c093 100644
--- a/src/core/hle/kernel/svc/svc_ipc.cpp
+++ b/src/core/hle/kernel/svc/svc_ipc.cpp
@@ -13,6 +13,7 @@
#include "core/hle/kernel/k_scoped_resource_reservation.h"
#include "core/hle/kernel/k_server_session.h"
#include "core/hle/kernel/k_session.h"
+#include "core/hle/kernel/k_thread.h"
#include "core/hle/kernel/svc.h"
#include "core/hle/kernel/svc_results.h"
@@ -142,7 +143,23 @@ Result ReplyAndReceiveImpl(KernelCore& kernel, int32_t* out_index, uintptr_t mes
/// Makes a blocking IPC call to a service.
Result SendSyncRequest(Core::System& system, Handle session_handle) {
- R_RETURN(SendSyncRequestImpl(system.Kernel(), 0, 0, session_handle));
+ auto& process = GetCurrentProcess(system.Kernel());
+ if (!process.UsesPAuthReturns()) {
+ R_RETURN(SendSyncRequestImpl(system.Kernel(), 0, 0, session_handle));
+ }
+ const auto tls = GetCurrentThread(system.Kernel()).GetTlsAddress();
+ const u64 header = process.GetMemory().Read64(tls);
+ const bool trace_tipc = (header & 0xffff) >= 16;
+ if (trace_tipc) {
+ LOG_DEBUG(Kernel_SVC, "TIPC send handle={:#x}, header={:#x}, special={:#x}",
+ session_handle, header, process.GetMemory().Read64(tls + 8));
+ }
+ const auto result = SendSyncRequestImpl(system.Kernel(), 0, 0, session_handle);
+ if (trace_tipc) {
+ LOG_DEBUG(Kernel_SVC, "TIPC receive result={:#x}, header={:#x}, data={:#x}",
+ result.raw, process.GetMemory().Read64(tls), process.GetMemory().Read64(tls + 8));
+ }
+ R_RETURN(result);
}
Result SendSyncRequestWithUserBuffer(Core::System& system, uint64_t message, uint64_t buffer_size,
diff --git a/src/core/hle/kernel/svc/svc_memory.cpp b/src/core/hle/kernel/svc/svc_memory.cpp
index 970912fe50..53c0d43456 100644
--- a/src/core/hle/kernel/svc/svc_memory.cpp
+++ b/src/core/hle/kernel/svc/svc_memory.cpp
@@ -126,6 +126,31 @@ Result SetMemoryAttribute(Core::System& system, u64 address, u64 size, u32 mask,
R_UNLESS(page_table.Contains(address, size), ResultInvalidCurrentMemory);
// Set the memory attribute.
+ const auto result = page_table.SetMemoryAttribute(address, size,
+ static_cast<KMemoryAttribute>(mask),
+ static_cast<KMemoryAttribute>(attr));
+ if (GetCurrentProcess(system.Kernel()).UsesPAuthReturns()) {
+ LOG_DEBUG(Kernel_SVC, "native memory attribute result={:#x}", result.raw);
+ }
+ R_RETURN(result);
+}
+
+// Captured native SDK callers set bit 0x20 before registering GPU-accessible
+// memory and clear it during destruction. This tracks eligibility on real mapped
+// memory; GPU mappings and submission remain separate operations.
+Result SetGpuMemoryAttribute(Core::System& system, u64 address, u64 size, u32 mask, u32 attr) {
+ auto& process = GetCurrentProcess(system.Kernel());
+ R_UNLESS(process.UsesPAuthReturns(), ResultNotImplemented);
+ LOG_DEBUG(Kernel_SVC, "GPU memory address={:#x}, size={:#x}, mask={:#x}, attr={:#x}",
+ address, size, mask, attr);
+ R_UNLESS(Common::IsAligned(address, PageSize), ResultInvalidAddress);
+ R_UNLESS(size > 0 && Common::IsAligned(size, PageSize), ResultInvalidSize);
+ R_UNLESS(address < address + size, ResultInvalidCurrentMemory);
+ constexpr u32 SupportedMask = static_cast<u32>(MemoryAttribute::GpuAccessible);
+ R_UNLESS((attr & ~mask) == 0 && ((mask | attr) & ~SupportedMask) == 0,
+ ResultInvalidCombination);
+ auto& page_table = process.GetPageTable();
+ R_UNLESS(page_table.Contains(address, size), ResultInvalidCurrentMemory);
R_RETURN(page_table.SetMemoryAttribute(address, size, static_cast<KMemoryAttribute>(mask),
static_cast<KMemoryAttribute>(attr)));
}
diff --git a/src/core/hle/kernel/svc/svc_synchronization.cpp b/src/core/hle/kernel/svc/svc_synchronization.cpp
index 2b019c52d1..2f72f60215 100644
--- a/src/core/hle/kernel/svc/svc_synchronization.cpp
+++ b/src/core/hle/kernel/svc/svc_synchronization.cpp
@@ -15,13 +15,24 @@
namespace Kernel::Svc {
+namespace {
+void LogInvalidNativeSyncHandle(Core::System& system, const char* operation, Handle handle) {
+ if (!GetCurrentProcess(system.Kernel()).UsesPAuthReturns()) { return; }
+ const auto& context = GetCurrentThread(system.Kernel()).GetContext();
+ LOG_DEBUG(Kernel_SVC, "native {} invalid handle={:#x}, pc={:#x}, lr={:#x}, fp={:#x}",
+ operation, handle, context.pc, context.lr, context.fp);
+}
+}
+
/// Close a handle
Result CloseHandle(Core::System& system, Handle handle) {
LOG_TRACE(Kernel_SVC, "Closing handle {:#08x}", handle);
// Remove the handle.
- R_UNLESS(GetCurrentProcess(system.Kernel()).GetHandleTable().Remove(system.Kernel(), handle),
- ResultInvalidHandle);
+ if (!GetCurrentProcess(system.Kernel()).GetHandleTable().Remove(system.Kernel(), handle)) {
+ LogInvalidNativeSyncHandle(system, "CloseHandle", handle);
+ R_THROW(ResultInvalidHandle);
+ }
R_SUCCEED();
}
@@ -49,6 +60,7 @@ Result ResetSignal(Core::System& system, Handle handle) {
}
}
+ LogInvalidNativeSyncHandle(system, "ResetSignal", handle);
R_THROW(ResultInvalidHandle);
}
@@ -72,7 +84,10 @@ Result WaitSynchronization(Core::System& system, int32_t* out_index, u64 user_ha
R_UNLESS(GetCurrentMemory(system.Kernel()).ReadBlock(user_handles, handles.data(), sizeof(Handle) * num_handles), ResultInvalidPointer);
// Convert the handles to objects.
- R_UNLESS(handle_table.GetMultipleObjects<KSynchronizationObject>(system.Kernel(), objs.data(), handles.data(), num_handles), ResultInvalidHandle);
+ if (!handle_table.GetMultipleObjects<KSynchronizationObject>(system.Kernel(), objs.data(), handles.data(), num_handles)) {
+ for (s32 i = 0; i < num_handles; ++i) { LogInvalidNativeSyncHandle(system, "WaitSynchronization set", handles[i]); }
+ R_THROW(ResultInvalidHandle);
+ }
}
// Ensure handles are closed when we're done.
diff --git a/src/core/hle/kernel/svc/svc_thread.cpp b/src/core/hle/kernel/svc/svc_thread.cpp
index dbca13dd26..b4eb0b5719 100644
--- a/src/core/hle/kernel/svc/svc_thread.cpp
+++ b/src/core/hle/kernel/svc/svc_thread.cpp
@@ -16,8 +16,11 @@
namespace Kernel::Svc {
namespace {
-constexpr bool IsValidVirtualCoreId(int32_t core_id) {
- return (0 <= core_id && core_id < static_cast<int32_t>(Core::Hardware::NUM_CPU_CORES));
+bool IsValidVirtualCoreId(const KProcess& process, int32_t core_id) {
+ // Extended processes use the kernel's existing virtual-core mapping.
+ const auto core_count = process.UsesPAuthReturns() ? Core::Hardware::NumVirtualCores
+ : Core::Hardware::NUM_CPU_CORES;
+ return 0 <= core_id && core_id < static_cast<int32_t>(core_count);
}
} // Anonymous namespace
@@ -38,7 +41,7 @@ Result CreateThread(Core::System& system, Handle* out_handle, u64 entry_point, u
}
// Validate arguments.
- R_UNLESS(IsValidVirtualCoreId(core_id), ResultInvalidCoreId);
+ R_UNLESS(IsValidVirtualCoreId(process, core_id), ResultInvalidCoreId);
R_UNLESS(((1ull << core_id) & process.GetCoreMask()) != 0, ResultInvalidCoreId);
R_UNLESS(HighestThreadPriority <= priority && priority <= LowestThreadPriority,
@@ -262,7 +265,7 @@ Result SetThreadCoreMask(Core::System& system, Handle thread_handle, s32 core_id
R_UNLESS(affinity_mask != 0, ResultInvalidCombination);
// Validate the core id.
- if (IsValidVirtualCoreId(core_id)) {
+ if (IsValidVirtualCoreId(GetCurrentProcess(system.Kernel()), core_id)) {
R_UNLESS(((1ULL << core_id) & affinity_mask) != 0, ResultInvalidCombination);
} else {
R_UNLESS(core_id == IdealCoreNoUpdate || core_id == IdealCoreDontCare,
diff --git a/src/core/hle/kernel/svc_types.h b/src/core/hle/kernel/svc_types.h
index b0f4796da4..c7b68cc0df 100644
--- a/src/core/hle/kernel/svc_types.h
+++ b/src/core/hle/kernel/svc_types.h
@@ -50,6 +50,7 @@ enum class MemoryAttribute : u32 {
DeviceShared = (1 << 2),
Uncached = (1 << 3),
PermissionLocked = (1 << 4),
+ GpuAccessible = (1 << 5),
};
DECLARE_ENUM_FLAG_OPERATORS(MemoryAttribute);
diff --git a/src/core/hle/service/am/applet.h b/src/core/hle/service/am/applet.h
index 571fa65219..69d2876363 100644
--- a/src/core/hle/service/am/applet.h
+++ b/src/core/hle/service/am/applet.h
@@ -84,6 +84,7 @@ struct Applet {
// Application functions
bool game_play_recording_supported{};
bool media_playback_state{};
+ bool graphics_scale_changed_notification_enabled{};
GamePlayRecordingState game_play_recording_state{GamePlayRecordingState::Disabled};
bool jit_service_launched{};
bool application_crash_report_enabled{};
diff --git a/src/core/hle/service/am/service/application_functions.cpp b/src/core/hle/service/am/service/application_functions.cpp
index 90d292c4a2..f86806dd3c 100644
--- a/src/core/hle/service/am/service/application_functions.cpp
+++ b/src/core/hle/service/am/service/application_functions.cpp
@@ -118,7 +118,8 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_
{220, nullptr, "Unknown220"}, // [20.0.0+]
{300, nullptr, "CreateMovieWriter"}, // [19.0.0+]
{310, nullptr, "Unknown310"}, // [20.0.0+]
- {320, nullptr, "Unknown320"}, // [20.0.0+]
+ {320, D<&IApplicationFunctions::GetScreenShotAndContinuousRecordingInitiallyEnabled>,
+ "GetScreenShotAndContinuousRecordingInitiallyEnabled"},
{330, D<&IApplicationFunctions::Unknown330>, "Unknown330"}, // [20.0.0+]
{500, nullptr, "StartContinuousRecordingFlushForDebug"},
{1000, nullptr, "CreateMovieMaker"},
@@ -544,6 +545,17 @@ Result IApplicationFunctions::GetUnknownEvent210(
R_SUCCEED();
}
+Result IApplicationFunctions::GetScreenShotAndContinuousRecordingInitiallyEnabled(
+ Out<bool> out_screenshot_enabled, Out<bool> out_recording_enabled) {
+ std::scoped_lock lk{m_applet->lock};
+ *out_screenshot_enabled = m_applet->screenshot_permission != ScreenshotPermission::Disable;
+ *out_recording_enabled = m_applet->game_play_recording_supported &&
+ m_applet->game_play_recording_state == GamePlayRecordingState::Enabled;
+ LOG_DEBUG(Service_AM, "called, screenshot={}, recording={}", *out_screenshot_enabled,
+ *out_recording_enabled);
+ R_SUCCEED();
+}
+
Result IApplicationFunctions::Unknown330(Out<u8> out) {
LOG_DEBUG(Service_AM, "called");
*out = 0;
diff --git a/src/core/hle/service/am/service/application_functions.h b/src/core/hle/service/am/service/application_functions.h
index b97ed7e25e..172b712f14 100644
--- a/src/core/hle/service/am/service/application_functions.h
+++ b/src/core/hle/service/am/service/application_functions.h
@@ -82,6 +82,8 @@ private:
Result GetHealthWarningDisappearedSystemEvent(OutCopyHandle<Kernel::KReadableEvent> out_event);
Result CreateApplicationAndRequestToStart(u64 application_id);
Result GetUnknownEvent210(OutCopyHandle<Kernel::KReadableEvent> out_event);
+ Result GetScreenShotAndContinuousRecordingInitiallyEnabled(
+ Out<bool> out_screenshot_enabled, Out<bool> out_recording_enabled);
Result Unknown330(Out<u8> out);
Result PrepareForJit();
diff --git a/src/core/hle/service/am/service/application_proxy.cpp b/src/core/hle/service/am/service/application_proxy.cpp
index df438e1e17..02502ae764 100644
--- a/src/core/hle/service/am/service/application_proxy.cpp
+++ b/src/core/hle/service/am/service/application_proxy.cpp
@@ -11,7 +11,9 @@
#include "core/hle/service/am/service/common_state_getter.h"
#include "core/hle/service/am/service/debug_functions.h"
#include "core/hle/service/am/service/display_controller.h"
+#include "core/hle/service/am/service/graphics_scaling_parameters_functions.h"
#include "core/hle/service/am/service/library_applet_creator.h"
+#include "core/hle/service/am/service/performance_functions.h"
#include "core/hle/service/am/service/process_winding_controller.h"
#include "core/hle/service/am/service/self_controller.h"
#include "core/hle/service/am/service/window_controller.h"
@@ -33,6 +35,9 @@ IApplicationProxy::IApplicationProxy(Core::System& system_, std::shared_ptr<Appl
{10, D<&IApplicationProxy::GetProcessWindingController>, "GetProcessWindingController"},
{11, D<&IApplicationProxy::GetLibraryAppletCreator>, "GetLibraryAppletCreator"},
{20, D<&IApplicationProxy::GetApplicationFunctions>, "GetApplicationFunctions"},
+ {40, D<&IApplicationProxy::GetPerformanceFunctions>, "GetPerformanceFunctions"},
+ {60, D<&IApplicationProxy::GetGraphicsScalingParametersFunctions>,
+ "GetGraphicsScalingParametersFunctions"},
{1000, D<&IApplicationProxy::GetDebugFunctions>, "GetDebugFunctions"},
};
// clang-format on
@@ -106,4 +111,18 @@ Result IApplicationProxy::GetApplicationFunctions(
R_SUCCEED();
}
+Result IApplicationProxy::GetPerformanceFunctions(
+ Out<SharedPointer<IPerformanceFunctions>> out_functions) {
+ LOG_DEBUG(Service_AM, "called");
+ *out_functions = std::make_shared<IPerformanceFunctions>(system);
+ R_SUCCEED();
+}
+
+Result IApplicationProxy::GetGraphicsScalingParametersFunctions(
+ Out<SharedPointer<IGraphicsScalingParametersFunctions>> out_functions) {
+ LOG_DEBUG(Service_AM, "called");
+ *out_functions = std::make_shared<IGraphicsScalingParametersFunctions>(system, m_applet);
+ R_SUCCEED();
+}
+
} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/application_proxy.h b/src/core/hle/service/am/service/application_proxy.h
index b56f597605..aa1ab1938a 100644
--- a/src/core/hle/service/am/service/application_proxy.h
+++ b/src/core/hle/service/am/service/application_proxy.h
@@ -17,7 +17,9 @@ class IApplicationFunctions;
class ICommonStateGetter;
class IDebugFunctions;
class IDisplayController;
+class IGraphicsScalingParametersFunctions;
class ILibraryAppletCreator;
+class IPerformanceFunctions;
class IProcessWindingController;
class ISelfController;
class IWindowController;
@@ -43,6 +45,10 @@ private:
Result GetApplicationFunctions(
Out<SharedPointer<IApplicationFunctions>> out_application_functions);
+ Result GetPerformanceFunctions(Out<SharedPointer<IPerformanceFunctions>> out_functions);
+ Result GetGraphicsScalingParametersFunctions(
+ Out<SharedPointer<IGraphicsScalingParametersFunctions>> out_functions);
+
private:
WindowSystem& m_window_system;
Kernel::KProcess* const m_process;
diff --git a/src/core/hle/service/am/service/application_proxy_service.cpp b/src/core/hle/service/am/service/application_proxy_service.cpp
index 7727951d8c..da11cce94d 100644
--- a/src/core/hle/service/am/service/application_proxy_service.cpp
+++ b/src/core/hle/service/am/service/application_proxy_service.cpp
@@ -19,6 +19,8 @@ IApplicationProxyService::IApplicationProxyService(Core::System& system_,
: ServiceFramework{system_, "appletOE"}, m_window_system{window_system} {
static const FunctionInfo functions[] = {
{0, D<&IApplicationProxyService::OpenApplicationProxy>, "OpenApplicationProxy"},
+ {10, D<&IApplicationProxyService::OpenApplicationProxyWithFlags>,
+ "OpenApplicationProxyWithFlags"},
};
RegisterHandlers(functions);
}
@@ -40,6 +42,15 @@ Result IApplicationProxyService::OpenApplicationProxy(
}
}
+Result IApplicationProxyService::OpenApplicationProxyWithFlags(
+ Out<SharedPointer<IApplicationProxy>> out_application_proxy, u32 flags, ClientProcessId pid,
+ InCopyHandle<Kernel::KProcess> process_handle) {
+ LOG_DEBUG(Service_AM, "called, flags={:#x}", flags);
+ // The extended SDK supplies an additional u32 before the process ID.
+ R_UNLESS(flags == 0 || flags == 2, ResultUnknown);
+ R_RETURN(OpenApplicationProxy(out_application_proxy, pid, process_handle));
+}
+
std::shared_ptr<Applet> IApplicationProxyService::GetAppletFromProcessId(ProcessId process_id) {
return m_window_system.GetByAppletResourceUserId(process_id.pid);
}
diff --git a/src/core/hle/service/am/service/application_proxy_service.h b/src/core/hle/service/am/service/application_proxy_service.h
index fce860a7c1..5812f6133a 100644
--- a/src/core/hle/service/am/service/application_proxy_service.h
+++ b/src/core/hle/service/am/service/application_proxy_service.h
@@ -26,6 +26,10 @@ private:
Result OpenApplicationProxy(Out<SharedPointer<IApplicationProxy>> out_application_proxy,
ClientProcessId pid, InCopyHandle<Kernel::KProcess> process_handle);
+ Result OpenApplicationProxyWithFlags(
+ Out<SharedPointer<IApplicationProxy>> out_application_proxy, u32 flags, ClientProcessId pid,
+ InCopyHandle<Kernel::KProcess> process_handle);
+
private:
std::shared_ptr<Applet> GetAppletFromProcessId(ProcessId pid);
diff --git a/src/core/hle/service/am/service/gpu_memory_registration.h b/src/core/hle/service/am/service/gpu_memory_registration.h
new file mode 100644
index 0000000000..37ac7fd377
--- /dev/null
+++ b/src/core/hle/service/am/service/gpu_memory_registration.h
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+#include <optional>
+#include <span>
+#include "common/common_types.h"
+
+namespace Service::AM {
+struct GpuMemoryRegistration {
+ u64 begin;
+ u64 end;
+ u8 flags;
+ u8 device;
+};
+
+constexpr std::optional<GpuMemoryRegistration> DecodeGpuMemoryRegistration(std::span<const u32> packet) {
+ if (packet.size() < 8 || packet[0] != 0x75 || packet[1] != 6) return std::nullopt;
+ // The SDK writes STRB at offsets 0x18 and 0x1c, leaving three padding bytes.
+ return GpuMemoryRegistration{u64{packet[2]} | (u64{packet[3]} << 32),
+ u64{packet[4]} | (u64{packet[5]} << 32), static_cast<u8>(packet[6]), static_cast<u8>(packet[7])};
+}
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/gpu_session.cpp b/src/core/hle/service/am/service/gpu_session.cpp
new file mode 100644
index 0000000000..a83381d7bc
--- /dev/null
+++ b/src/core/hle/service/am/service/gpu_session.cpp
@@ -0,0 +1,633 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <cstring>
+#include <mutex>
+#include <vector>
+#include "common/swap.h"
+#include "common/alignment.h"
+#include "core/hle/service/nvdrv/core/nvmap.h"
+#include "core/core.h"
+#include "core/hle/kernel/k_client_session.h"
+#include "core/hle/kernel/k_event.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/kernel/svc_results.h"
+#include "core/hle/service/am/service/graphics_session.h"
+#include "core/hle/service/am/service/gpu_session.h"
+#include "core/hle/service/am/service/gpu_memory_registration.h"
+#include "core/hle/service/ipc_helpers.h"
+#include "core/hle/service/nvdrv/nvdrv.h"
+#include "core/hle/service/nvdrv/devices/nvhost_as_gpu.h"
+#include "core/hle/service/nvdrv/devices/native_gpu_kind.h"
+
+namespace Service::AM {
+struct GpuDeviceRegistry {
+ GpuDeviceRegistry(Core::System& system, std::shared_ptr<Nvidia::Module> module_, Kernel::KProcess* process_)
+ : context{system, "GpuDevices"}, module{std::move(module_)}, process{process_} {
+ process->Open(context.kernel);
+ }
+ ~GpuDeviceRegistry() {
+ for (const auto& fd : address_spaces) { if (fd) { module->Close(*fd); } }
+ if (nv_session) { module->GetContainer().CloseSession(Nvidia::NvCore::SessionId{*nv_session}); }
+ process->Close(context.kernel);
+ for (auto* event : events) { context.CloseEvent(event); }
+ }
+ KernelHelpers::ServiceContext context;
+ std::array<Kernel::KEvent*, 64> events{};
+ struct Device { u64 name; u64 debug_label; };
+ std::mutex mutex;
+ std::shared_ptr<Nvidia::Module> module;
+ Kernel::KProcess* process;
+ std::array<std::optional<s32>, 8> address_spaces{};
+ std::array<bool, 8> address_space_registered{};
+ std::optional<size_t> nv_session;
+ std::array<std::optional<Device>, 256> devices{};
+};
+namespace {
+class IGpuWorker final : public ServiceFramework<IGpuWorker> {
+public:
+ IGpuWorker(Core::System& system, std::shared_ptr<Nvidia::Module> module,
+ Kernel::KProcess* process, std::shared_ptr<GpuDeviceRegistry> devices)
+ : ServiceFramework{system, "IGpuWorker"}, m_devices{std::move(devices)},
+ m_module{std::move(module)}, m_process{process} {
+ m_process->Open(kernel);
+ static const FunctionInfo functions[] = {
+ {101, &IGpuWorker::CreateGpuAccessibleMemory, "CreateGpuAccessibleMemory"},
+ {102, &IGpuWorker::DestroyGpuAccessibleMemory, "DestroyGpuAccessibleMemory"},
+ {211, &IGpuWorker::NormalMapGpuMemory4K, "NormalMapGpuMemory4K"},
+ {212, &IGpuWorker::NormalModifyGpuMemory4K, "NormalModifyGpuMemory4K"},
+ {213, &IGpuWorker::NormalUnmapGpuMemory4K, "NormalUnmapGpuMemory4K"},
+ {214, &IGpuWorker::CreateSparseRegion, "CreateSparseRegion"},
+ {215, &IGpuWorker::DestroySparseRegion, "DestroySparseRegion"},
+ };
+ RegisterHandlersTipc(functions);
+ }
+ ~IGpuWorker() override {
+ for (const auto& mapping : m_mappings) {
+ if (auto as = m_module->GetDevice<Nvidia::Devices::nvhost_as_gpu>(mapping.as_id)) {
+ as->UnmapNative4K(mapping.address, mapping.size);
+ }
+ }
+ for (const auto& region : m_sparse_regions) {
+ if (auto as = m_module->GetDevice<Nvidia::Devices::nvhost_as_gpu>(region.as_id)) {
+ as->DestroyNativeSparseRegion(region.address, region.size);
+ }
+ }
+ auto& nvmap = m_module->GetContainer().GetNvMapFile();
+ for (const auto id : m_memory) {
+ if (const auto info = nvmap.FreeHandle(id, false); info && info->can_unlock) {
+ const auto result = m_process->GetPageTable().UnlockForDeviceAddressSpace(
+ info->address, info->size);
+ if (result.IsError()) {
+ LOG_ERROR(Service_AM, "GPU memory cleanup failed: {:#x}", result.raw);
+ }
+ }
+ }
+ if (m_nv_session) {
+ m_module->GetContainer().CloseSession(Nvidia::NvCore::SessionId{*m_nv_session});
+ }
+ m_process->Close(kernel);
+ }
+ Result HandleSyncRequest(Kernel::KServerSession& session, HLERequestContext& ctx) override {
+ if (!ctx.IsTipc() || handlers_tipc.contains(ctx.GetCommand())) {
+ return ServiceFrameworkBase::HandleSyncRequest(session, ctx);
+ }
+ const auto guard = LockService();
+ InvokeRequestTipc(ctx);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(Kernel::ResultNotImplemented.raw);
+ return system.IsPoweredOn() ? ctx.WriteToOutgoingCommandBuffer() : ResultSuccess;
+ }
+private:
+ struct Mapping { u32 as_id; u64 address; u64 size; u32 memory_id; };
+ std::vector<Mapping> m_mappings;
+ std::vector<Mapping> m_sparse_regions;
+ static u64 ReadPair(const u32* packet, size_t index) {
+ return u64{packet[index]} | (u64{packet[index + 1]} << 32);
+ }
+ bool OwnsAddressSpace(u32 id) const {
+ for (size_t i = 0; i < m_devices->address_spaces.size(); ++i) {
+ if (m_devices->address_spaces[i] &&
+ static_cast<u32>(*m_devices->address_spaces[i]) == id &&
+ m_devices->address_space_registered[i]) { return true; }
+ }
+ return false;
+ }
+ void CreateSparseRegion(HLERequestContext& ctx) {
+ const std::scoped_lock device_lock{m_devices->mutex};
+ const auto* p = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ const u64 begin = ReadPair(p, 3), end = ReadPair(p, 5);
+ const u32 notification = p[11];
+ if (p[0] == 0xe6 && p[1] == 10 && notification < m_devices->events.size() && m_devices->events[notification]) {
+ result = Kernel::ResultNotFound;
+ const u32 shift = p[7] & 0xff, device = p[9] & 0xff;
+ if (OwnsAddressSpace(p[2]) && device < m_devices->devices.size() && m_devices->devices[device]) {
+ result = Kernel::ResultInvalidArgument;
+ if ((shift == 12 || shift == 16) && (p[8] & 0xff) <= 1 && (p[10] & 0xff) <= 1 &&
+ end >= begin && end != ~u64{0} && Common::IsAligned(begin, 1ULL << shift) &&
+ Common::IsAligned(end - begin + 1, 1ULL << shift)) {
+ const auto as = m_module->GetDevice<Nvidia::Devices::nvhost_as_gpu>(p[2]);
+ result = as && as->CreateNativeSparseRegion(begin, end - begin + 1) == Nvidia::NvResult::Success
+ ? ResultSuccess : Kernel::ResultInvalidCurrentMemory;
+ if (result.IsSuccess()) { m_sparse_regions.push_back({p[2], begin, end - begin + 1, 0}); }
+ }
+ }
+ m_devices->events[notification]->Signal(kernel);
+ }
+ LOG_DEBUG(Service_AM, "GPU sparse region as={}, VA={:#x}..{:#x}, result={:#x}", p[2], begin, end, result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void DestroySparseRegion(HLERequestContext& ctx) {
+ const std::scoped_lock device_lock{m_devices->mutex};
+ const auto* p = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ const u64 begin = ReadPair(p, 3), end = ReadPair(p, 5);
+ const u32 notification = p[8];
+ if (p[0] == 0xe7 && p[1] == 7 && (p[7] & 0xff) <= 1 &&
+ notification < m_devices->events.size() && m_devices->events[notification]) {
+ const auto it = std::find_if(m_sparse_regions.begin(), m_sparse_regions.end(), [&](const auto& region) {
+ return region.as_id == p[2] && region.address == begin && end >= begin && region.size == end - begin + 1;
+ });
+ result = Kernel::ResultNotFound;
+ if (it != m_sparse_regions.end()) {
+ const auto as = m_module->GetDevice<Nvidia::Devices::nvhost_as_gpu>(p[2]);
+ if (as && as->DestroyNativeSparseRegion(begin, it->size) == Nvidia::NvResult::Success) {
+ std::erase_if(m_mappings, [&](const auto& mapping) {
+ return mapping.as_id == p[2] && mapping.address >= begin && mapping.address <= end;
+ });
+ m_sparse_regions.erase(it);
+ result = ResultSuccess;
+ }
+ }
+ m_devices->events[notification]->Signal(kernel);
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ Result MapMemory(const u32* packet, u8& selected_kind) {
+ R_UNLESS(OwnsAddressSpace(packet[2]), Kernel::ResultNotFound);
+ R_UNLESS(std::find(m_memory.begin(), m_memory.end(), packet[7]) != m_memory.end(),
+ Kernel::ResultNotFound);
+ R_UNLESS(packet[15] < m_devices->devices.size() && m_devices->devices[packet[15]],
+ Kernel::ResultNotFound);
+ R_UNLESS(packet[10] == 0 && packet[11] <= 1 && packet[14] == 0 && packet[16] <= 1,
+ Kernel::ResultNotImplemented);
+ const auto kind = Nvidia::Devices::SelectNativeMemoryKind(
+ static_cast<u8>(packet[12]), static_cast<u8>(packet[13]));
+ R_UNLESS(kind, Kernel::ResultNotImplemented);
+ const u64 begin = ReadPair(packet, 3), end = ReadPair(packet, 5);
+ R_UNLESS(end >= begin && end != ~u64{0}, Kernel::ResultInvalidSize);
+ R_UNLESS(m_mappings.size() < 1024, Kernel::ResultOutOfResource);
+ const auto as = m_module->GetDevice<Nvidia::Devices::nvhost_as_gpu>(packet[2]);
+ R_UNLESS(as, Kernel::ResultNotFound);
+ const auto result = as->MapNative4K(packet[7], begin, ReadPair(packet, 8),
+ end - begin + 1, kind->backend);
+ R_UNLESS(result == Nvidia::NvResult::Success, Kernel::ResultInvalidCurrentMemory);
+ m_mappings.push_back({packet[2], begin, end - begin + 1, packet[7]});
+ selected_kind = kind->selected;
+ R_SUCCEED();
+ }
+ void NormalMapGpuMemory4K(HLERequestContext& ctx) {
+ std::array<u32, 18> words;
+ std::copy_n(ctx.CommandBuffer(), words.size(), words.begin());
+ // These arguments are single-byte TIPC fields; SDK padding is not zeroed.
+ for (size_t i = 10; i <= 16; ++i) { words[i] &= 0xff; }
+ const auto* packet = words.data();
+ const std::scoped_lock device_lock{m_devices->mutex};
+ Result result = Kernel::ResultInvalidCombination;
+ u8 selected_kind{};
+ if (packet[0] == 0xe3 && packet[1] == 16 && packet[17] < m_devices->events.size() &&
+ m_devices->events[packet[17]]) {
+ result = MapMemory(packet, selected_kind);
+ m_devices->events[packet[17]]->Signal(kernel);
+ LOG_DEBUG(Service_AM, "GPU map4K as={}, memory={}, VA={:#x}..{:#x}, permission={}, cache={}, kinds={:#x}/{:#x}, mode={}, sync={}, notification={}, selected={:#x}, result={:#x}",
+ packet[2], packet[7], ReadPair(packet, 3), ReadPair(packet, 5), packet[10],
+ packet[11], packet[12], packet[13], packet[14], packet[16], packet[17], selected_kind, result.raw);
+ }
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result.raw);
+ rb.Push<u32>(selected_kind);
+ }
+ void NormalModifyGpuMemory4K(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const std::scoped_lock device_lock{m_devices->mutex};
+ Result result = Kernel::ResultInvalidCombination;
+ u8 selected{};
+ const u64 begin = ReadPair(packet, 3), end = ReadPair(packet, 5);
+ const auto requested = static_cast<u8>(packet[7]);
+ const auto fallback = static_cast<u8>(packet[8]);
+ if (packet[0] == 0xe4 && packet[1] == 9 && (packet[9] & 0xff) <= 1 &&
+ packet[10] < m_devices->events.size() && m_devices->events[packet[10]]) {
+ result = Kernel::ResultNotFound;
+ const auto kind = Nvidia::Devices::SelectNativeMemoryKind(requested, fallback);
+ const auto owner = std::find_if(m_mappings.begin(), m_mappings.end(), [&](const auto& mapping) {
+ return mapping.as_id == packet[2] && begin >= mapping.address &&
+ begin - mapping.address < mapping.size && end >= begin &&
+ end - mapping.address < mapping.size;
+ });
+ if (kind && OwnsAddressSpace(packet[2]) && owner != m_mappings.end()) {
+ const auto as = m_module->GetDevice<Nvidia::Devices::nvhost_as_gpu>(packet[2]);
+ if (as && as->ModifyNativeKind(begin, end - begin + 1, requested, fallback) ==
+ Nvidia::NvResult::Success) {
+ selected = kind->selected;
+ result = ResultSuccess;
+ }
+ }
+ m_devices->events[packet[10]]->Signal(kernel);
+ }
+ LOG_DEBUG(Service_AM, "GPU modify4K as={}, VA={:#x}..{:#x}, kinds={:#x}/{:#x}, sync={}, notification={}, selected={}, result={:#x}",
+ packet[2], begin, end, requested, fallback, packet[9], packet[10], selected, result.raw);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result.raw);
+ rb.Push<u32>(selected);
+ }
+ void NormalUnmapGpuMemory4K(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const std::scoped_lock device_lock{m_devices->mutex};
+ Result result = Kernel::ResultInvalidCombination;
+ u64 unmapped{};
+ if (packet[0] == 0xe5 && packet[1] == 7 && (packet[7] & 0xff) <= 1 &&
+ packet[8] < m_devices->events.size() && m_devices->events[packet[8]]) {
+ const u64 begin = ReadPair(packet, 3), end = ReadPair(packet, 5);
+ const auto it = std::find_if(m_mappings.begin(), m_mappings.end(), [&](const auto& mapping) {
+ return mapping.as_id == packet[2] && mapping.address == begin &&
+ end >= begin && end - begin + 1 == mapping.size;
+ });
+ result = Kernel::ResultNotFound;
+ if (it != m_mappings.end()) {
+ const auto as = m_module->GetDevice<Nvidia::Devices::nvhost_as_gpu>(it->as_id);
+ result = Kernel::ResultInvalidState;
+ if (as && as->UnmapNative4K(it->address, it->size) == Nvidia::NvResult::Success) {
+ unmapped = it->size;
+ m_mappings.erase(it);
+ result = ResultSuccess;
+ }
+ }
+ m_devices->events[packet[8]]->Signal(kernel);
+ LOG_DEBUG(Service_AM, "GPU unmap4K as={}, VA={:#x}..{:#x}, bytes={:#x}, result={:#x}",
+ packet[2], begin, end, unmapped, result.raw);
+ }
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(result.raw);
+ rb.Push(unmapped);
+ }
+ Result RegisterMemory(u64 begin, u64 end, u32 flags, u32 device, u32& id) {
+ R_UNLESS(flags == 0, Kernel::ResultNotImplemented);
+ R_UNLESS(device < m_devices->devices.size(), Kernel::ResultNotFound);
+ const std::scoped_lock device_lock{m_devices->mutex};
+ R_UNLESS(m_devices->devices[device], Kernel::ResultNotFound);
+ R_UNLESS(end >= begin && end != ~u64{0}, Kernel::ResultInvalidSize);
+ const u64 size = end - begin + 1;
+ R_UNLESS(Common::IsAligned(begin, Kernel::PageSize), Kernel::ResultInvalidAddress);
+ R_UNLESS(Common::IsAligned(size, Kernel::PageSize), Kernel::ResultInvalidSize);
+ R_UNLESS(m_memory.size() < 1024, Kernel::ResultOutOfResource);
+ auto& page_table = m_process->GetPageTable();
+ R_UNLESS(page_table.Contains(begin, size), Kernel::ResultInvalidCurrentMemory);
+ for (u64 address = begin; address <= end;) {
+ Kernel::KMemoryInfo info{};
+ Kernel::Svc::PageInfo page{};
+ R_TRY(page_table.QueryInfo(&info, &page, address));
+ R_UNLESS(True(info.m_attribute & Kernel::KMemoryAttribute::GpuAccessible),
+ Kernel::ResultInvalidCurrentMemory);
+ const u64 next = info.m_address + info.m_size;
+ R_UNLESS(next > address, Kernel::ResultInvalidCurrentMemory);
+ address = next;
+ }
+ if (!m_nv_session) {
+ m_nv_session = m_module->GetContainer().OpenSession(m_process).id;
+ }
+ auto& nvmap = m_module->GetContainer().GetNvMapFile();
+ std::shared_ptr<Nvidia::NvCore::NvMap::Handle> handle;
+ R_UNLESS(nvmap.CreateHandle(size, handle) == Nvidia::NvResult::Success,
+ Kernel::ResultOutOfResource);
+ bool is_io{};
+ const auto lock_result = page_table.LockForMapDeviceAddressSpace(
+ &is_io, begin, size, Kernel::KMemoryPermission::None, true, true);
+ if (lock_result.IsError()) {
+ nvmap.FreeHandle(handle->id, false);
+ R_RETURN(lock_result);
+ }
+ const auto alloc_result = handle->Alloc({}, Kernel::PageSize, 0, begin,
+ Nvidia::NvCore::SessionId{*m_nv_session});
+ if (alloc_result != Nvidia::NvResult::Success) {
+ page_table.UnlockForDeviceAddressSpace(begin, size);
+ nvmap.FreeHandle(handle->id, false);
+ R_RETURN(Kernel::ResultOutOfResource);
+ }
+ id = handle->id;
+ m_memory.push_back(id);
+ R_SUCCEED();
+ }
+ Result ReleaseMemory(u32 id, u32 notification) {
+ const std::scoped_lock device_lock{m_devices->mutex};
+ R_UNLESS(notification < m_devices->events.size() && m_devices->events[notification],
+ Kernel::ResultInvalidCombination);
+ const auto it = std::find(m_memory.begin(), m_memory.end(), id);
+ R_UNLESS(it != m_memory.end(), Kernel::ResultNotFound);
+ auto& nvmap = m_module->GetContainer().GetNvMapFile();
+ auto handle = nvmap.GetHandle(id);
+ R_UNLESS(handle, Kernel::ResultNotFound);
+ {
+ const std::scoped_lock handle_lock{handle->mutex};
+ // No asynchronous GPU work is implemented yet. Never report release
+ // completion for memory still retained or pinned by another user.
+ R_UNLESS(handle->pins == 0 && handle->internal_dupes == 0 && handle->dupes == 1,
+ Kernel::ResultBusy);
+ }
+ // FreeHandle uses the final shared reference to decide when CPU memory
+ // may be unshared. Drop this temporary inspection reference first.
+ handle.reset();
+ const auto info = nvmap.FreeHandle(id, false);
+ R_UNLESS(info && info->can_unlock, Kernel::ResultInvalidState);
+ m_memory.erase(it);
+ R_TRY(m_process->GetPageTable().UnlockForDeviceAddressSpace(info->address, info->size));
+ m_devices->events[notification]->Signal(kernel);
+ R_SUCCEED();
+ }
+ void DestroyGpuAccessibleMemory(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const auto result = packet[0] == 0x76 && packet[1] == 2
+ ? ReleaseMemory(packet[2], packet[3]) : Kernel::ResultInvalidCombination;
+ LOG_DEBUG(Service_AM, "GPU memory release id={}, notification={}, result={:#x}",
+ packet[2], packet[3], result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void CreateGpuAccessibleMemory(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ u32 id{};
+ Result result = Kernel::ResultInvalidCombination;
+ if (const auto request = DecodeGpuMemoryRegistration(std::span<const u32>{packet, 8})) {
+ result = RegisterMemory(request->begin, request->end, request->flags, request->device, id);
+ LOG_DEBUG(Service_AM, "GPU memory {:#x}..{:#x}, device={}, raw_device={:#x}, flags={}, id={}, result={:#x}",
+ request->begin, request->end, request->device, packet[7], request->flags, id, result.raw);
+ }
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result.raw);
+ rb.Push(id);
+ }
+ std::shared_ptr<GpuDeviceRegistry> m_devices;
+ std::optional<size_t> m_nv_session;
+ std::vector<u32> m_memory;
+ std::shared_ptr<Nvidia::Module> m_module;
+ Kernel::KProcess* const m_process;
+};
+} // namespace
+
+IGpuSession::IGpuSession(Core::System& system, std::shared_ptr<Nvidia::Module> module,
+ Kernel::KProcess* process, bool legacy_protocol)
+ : ServiceFramework{system, "IGpuSession"},
+ m_process{process}, m_module{std::move(module)}, m_legacy_protocol{legacy_protocol} {
+ m_process->Open(kernel);
+ m_devices = std::make_shared<GpuDeviceRegistry>(system, m_module, m_process);
+ static const FunctionInfo functions[] = {
+ {11, &IGpuSession::GetNotificationRange, "GetNotificationRange"},
+ {12, &IGpuSession::GetNotificationEvent, "GetNotificationEvent"},
+ {21, &IGpuSession::RequestWorker, "RequestWorker"},
+ {22, &IGpuSession::AcquireWorker, "AcquireWorker"},
+ {33, &IGpuSession::CreateAddressSpace, "CreateAddressSpace"},
+ {41, &IGpuSession::CreateGraphicsDevice, "CreateGraphicsDevice"},
+ {42, &IGpuSession::DestroyGraphicsDevice, "DestroyGraphicsDevice"},
+ {201, &IGpuSession::GetAddressSpacePartition, "GetLegacyGsvPartition"},
+ {202, &IGpuSession::GetAddressSpacePartition, "GetGsvPartition"},
+ {203, &IGpuSession::GetAddressSpacePartition, "GetUserPartition"},
+ };
+ RegisterHandlersTipc(functions);
+}
+
+IGpuSession::~IGpuSession() {
+ if (m_worker) { m_worker->Close(kernel); }
+ m_process->Close(kernel);
+}
+
+Result IGpuSession::HandleSyncRequest(Kernel::KServerSession& session, HLERequestContext& ctx) {
+ if (!ctx.IsTipc() || handlers_tipc.contains(ctx.GetCommand())) {
+ return ServiceFrameworkBase::HandleSyncRequest(session, ctx);
+ }
+ const auto guard = LockService();
+ InvokeRequestTipc(ctx);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(Kernel::ResultNotImplemented.raw);
+ return system.IsPoweredOn() ? ctx.WriteToOutgoingCommandBuffer() : ResultSuccess;
+}
+
+Result IGpuSession::RegisterAddressSpace(u32 id) {
+ const auto guard = LockService();
+ const std::scoped_lock device_lock{m_devices->mutex};
+ for (size_t i = 0; i < m_devices->address_spaces.size(); ++i) {
+ if (m_devices->address_spaces[i] && static_cast<u32>(*m_devices->address_spaces[i]) == id) {
+ m_devices->address_space_registered[i] = true;
+ R_SUCCEED();
+ }
+ }
+ R_THROW(Kernel::ResultNotFound);
+}
+
+void IGpuSession::GetAddressSpacePartition(HLERequestContext& ctx) {
+ const std::scoped_lock device_lock{m_devices->mutex};
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ std::array<u64, 9> values{};
+ const bool legacy_user = m_legacy_protocol && (ctx.GetCommand() == 201 || ctx.GetCommand() == 202);
+ const bool user = legacy_user || ctx.GetCommand() == 203;
+ if (!user) {
+ values[0] = ~u64{0}; // No supervisor-owned partition is reserved by this adapter.
+ }
+ if (packet[0] == 16 + ctx.GetCommand() && packet[1] == 1) {
+ const u32 id = packet[2];
+ const bool exists = std::any_of(m_devices->address_spaces.begin(), m_devices->address_spaces.end(),
+ [id](const auto& fd) { return fd && static_cast<u32>(*fd) == id; });
+ result = exists ? ResultSuccess : Kernel::ResultNotFound;
+ if (exists && user) {
+ std::array<u8, 64> input{};
+ std::array<u8, 64> output{};
+ const u32_le buffer_size{48};
+ std::memcpy(input.data() + 8, &buffer_size, sizeof(buffer_size));
+ Nvidia::Ioctl command{};
+ command.raw = 0xc0404108;
+ if (m_module->Ioctl1(static_cast<s32>(id), command, input, output) ==
+ Nvidia::NvResult::Success) {
+ std::array<Nvidia::Devices::nvhost_as_gpu::VaRegion, 2> regions;
+ std::memcpy(regions.data(), output.data() + 16, sizeof(regions));
+ const u64 end = regions[1].offset + regions[1].pages * regions[1].page_size - 1;
+ if (legacy_user) {
+ const auto& region = regions[ctx.GetCommand() == 201 ? 0 : 1];
+ values[0] = region.offset;
+ values[1] = region.offset + region.pages * region.page_size - 1;
+ } else {
+ values[0] = 1;
+ values[1] = regions[0].offset;
+ values[2] = end;
+ }
+ } else {
+ result = Kernel::ResultInvalidState;
+ }
+ }
+ LOG_DEBUG(Service_AM, "GPU partition command={}, id={}, begin={:#x}, end={:#x}, result={:#x}",
+ ctx.GetCommand(), id, user ? values[1] : values[0],
+ user ? values[2] : values[1], result.raw);
+ }
+ IPC::ResponseBuilder rb{ctx, user && !legacy_user ? 20U : 6U};
+ rb.Push(result.raw);
+ for (size_t i = 0; i < (user && !legacy_user ? values.size() : size_t{2}); ++i) {
+ rb.Push(values[i]);
+ }
+}
+
+void IGpuSession::CreateAddressSpace(HLERequestContext& ctx) {
+ const std::scoped_lock device_lock{m_devices->mutex};
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ u32 id{};
+ if (packet[0] == 0x31 && packet[1] == 1 && packet[2] == 0) {
+ result = Kernel::ResultOutOfResource;
+ for (auto& slot : m_devices->address_spaces) {
+ if (slot) {
+ continue;
+ }
+ if (!m_devices->nv_session) {
+ m_devices->nv_session = m_module->GetContainer().OpenSession(m_process).id;
+ }
+ const auto fd = m_module->Open("/dev/nvhost-as-gpu",
+ Nvidia::NvCore::SessionId{*m_devices->nv_session});
+ if (fd == Nvidia::INVALID_NVDRV_FD) {
+ break;
+ }
+ std::array<u8, 40> input{};
+ std::array<u8, 40> output{};
+ const u32_le big_page_size{0x20000};
+ std::memcpy(input.data() + 8, &big_page_size, sizeof(big_page_size));
+ Nvidia::Ioctl command{};
+ command.raw = 0x40284109;
+ const auto nv_result = m_module->Ioctl1(fd, command, input, output);
+ if (nv_result == Nvidia::NvResult::Success) {
+ slot = fd;
+ id = fd;
+ result = ResultSuccess;
+ } else {
+ m_module->Close(fd);
+ result = Kernel::ResultInvalidState;
+ }
+ break;
+ }
+ }
+ LOG_DEBUG(Service_AM, "GPU address space id={}, result={:#x}", id, result.raw);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result.raw);
+ rb.Push(id);
+}
+
+void IGpuSession::CreateGraphicsDevice(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ u32 id{};
+ if (packet[0] == 0x39 && packet[1] == 6) {
+ const u64 name = u64{packet[2]} | (u64{packet[3]} << 32);
+ const u64 flags = u64{packet[4]} | (u64{packet[5]} << 32);
+ const u64 debug_label = u64{packet[6]} | (u64{packet[7]} << 32);
+ result = flags == 0 ? Kernel::ResultOutOfResource : Kernel::ResultNotImplemented;
+ if (flags == 0) {
+ const std::scoped_lock device_lock{m_devices->mutex};
+ for (u32 candidate = 1; candidate < m_devices->devices.size(); ++candidate) {
+ if (!m_devices->devices[candidate]) {
+ id = candidate;
+ m_devices->devices[id] = GpuDeviceRegistry::Device{name, debug_label};
+ result = ResultSuccess;
+ break;
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "graphics device name={:#x}, flags={:#x}, id={}, result={:#x}",
+ name, flags, id, result.raw);
+ }
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result.raw);
+ rb.Push(id);
+}
+
+void IGpuSession::DestroyGraphicsDevice(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x3a && packet[1] == 1) {
+ const u8 id = static_cast<u8>(packet[2]);
+ const std::scoped_lock device_lock{m_devices->mutex};
+ result = m_devices->devices[id] ? ResultSuccess : Kernel::ResultNotFound;
+ m_devices->devices[id].reset();
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGpuSession::GetNotificationRange(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const Result result = packet[0] == 0x1b && packet[1] == 0
+ ? ResultSuccess : Kernel::ResultInvalidCombination;
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(result.raw);
+ rb.Push<u32>(0);
+ rb.Push<u32>(static_cast<u32>(m_devices->events.size() - 1));
+ LOG_DEBUG(Service_AM, "GPU notification range 0..{}", m_devices->events.size() - 1);
+}
+
+void IGpuSession::GetNotificationEvent(HLERequestContext& ctx) {
+ const std::scoped_lock device_lock{m_devices->mutex};
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ Kernel::KReadableEvent* readable{};
+ if (packet[0] == 0x1c && packet[1] == 1 && packet[2] < m_devices->events.size()) {
+ const u32 index = packet[2];
+ if (!m_devices->events[index]) { m_devices->events[index] = m_devices->context.CreateEvent("GpuNotification"); }
+ if (m_devices->events[index]) {
+ readable = &m_devices->events[index]->GetReadableEvent();
+ readable->Open(kernel);
+ result = ResultSuccess;
+ } else { result = Kernel::ResultOutOfResource; }
+ LOG_DEBUG(Service_AM, "GPU notification event index={}, result={:#x}", index, result.raw);
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(result.raw);
+ ctx.AddMoveObject(readable);
+}
+
+void IGpuSession::RequestWorker(HLERequestContext& ctx) {
+ const std::scoped_lock device_lock{m_devices->mutex};
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x25 && packet[1] == 1 && packet[2] < m_devices->events.size()) {
+ const u32 index = packet[2];
+ if (!m_devices->events[index]) {
+ result = Kernel::ResultInvalidState;
+ } else {
+ result = ResultSuccess;
+ if (!m_worker) {
+ result = CreateGraphicsSession(system, ctx,
+ std::make_shared<IGpuWorker>(system, m_module, m_process, m_devices), &m_worker);
+ }
+ if (result.IsSuccess()) { m_devices->events[index]->Signal(kernel); }
+ }
+ LOG_DEBUG(Service_AM, "GPU worker request index={}, result={:#x}", index, result.raw);
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGpuSession::AcquireWorker(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ Kernel::KClientSession* client{};
+ if (packet[0] == 0x26 && packet[1] == 0) {
+ if (m_worker) {
+ client = m_worker;
+ client->Open(kernel);
+ result = ResultSuccess;
+ } else { result = Kernel::ResultInvalidState; }
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(result.raw);
+ ctx.AddMoveObject(client);
+ LOG_DEBUG(Service_AM, "GPU worker acquisition result={:#x}", result.raw);
+}
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/gpu_session.h b/src/core/hle/service/am/service/gpu_session.h
new file mode 100644
index 0000000000..e8f580bef1
--- /dev/null
+++ b/src/core/hle/service/am/service/gpu_session.h
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <array>
+#include <optional>
+#include "core/hle/service/service.h"
+#include "core/hle/service/kernel_helpers.h"
+
+namespace Kernel {
+class KClientSession;
+class KProcess;
+class KEvent;
+}
+namespace Service::Nvidia { class Module; }
+namespace Service::AM {
+
+struct GpuDeviceRegistry;
+
+class IGpuSession final : public ServiceFramework<IGpuSession> {
+public:
+ IGpuSession(Core::System& system, std::shared_ptr<Nvidia::Module> module,
+ Kernel::KProcess* process, bool legacy_protocol = false);
+ ~IGpuSession() override;
+ Result RegisterAddressSpace(u32 id);
+ Result HandleSyncRequest(Kernel::KServerSession& session, HLERequestContext& ctx) override;
+
+private:
+ void GetAddressSpacePartition(HLERequestContext& ctx);
+ void CreateAddressSpace(HLERequestContext& ctx);
+ void CreateGraphicsDevice(HLERequestContext& ctx);
+ void DestroyGraphicsDevice(HLERequestContext& ctx);
+ void GetNotificationRange(HLERequestContext& ctx);
+ void GetNotificationEvent(HLERequestContext& ctx);
+ void RequestWorker(HLERequestContext& ctx);
+ void AcquireWorker(HLERequestContext& ctx);
+
+ std::shared_ptr<GpuDeviceRegistry> m_devices;
+ Kernel::KClientSession* m_worker{};
+ Kernel::KProcess* const m_process;
+ std::shared_ptr<Nvidia::Module> m_module;
+ const bool m_legacy_protocol;
+};
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/graphics_scaling_parameters_functions.cpp b/src/core/hle/service/am/service/graphics_scaling_parameters_functions.cpp
new file mode 100644
index 0000000000..931c81850d
--- /dev/null
+++ b/src/core/hle/service/am/service/graphics_scaling_parameters_functions.cpp
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "core/hle/service/am/applet.h"
+#include "core/hle/service/am/service/graphics_scaling_parameters_functions.h"
+#include "core/hle/service/cmif_serialization.h"
+
+namespace Service::AM {
+
+IGraphicsScalingParametersFunctions::IGraphicsScalingParametersFunctions(
+ Core::System& system, std::shared_ptr<Applet> applet)
+ : ServiceFramework{system, "IGraphicsScalingParametersFunctions"}, m_applet{std::move(applet)} {
+ static const FunctionInfo functions[] = {
+ {10, D<&IGraphicsScalingParametersFunctions::GetGraphicsScalingParameters>,
+ "GetGraphicsScalingParameters"},
+ {1000, nullptr, "SetSupportingGraphicsScalingParametersForDebug"},
+ {1010, nullptr, "SetGpuQuotaForDebug"},
+ {1020, nullptr, "ClearGpuQuotaForDebug"},
+ {1030, nullptr, "SetScreenRatioForDebug"},
+ {1040, nullptr, "ClearScreenRatioForDebug"},
+ {1050, D<&IGraphicsScalingParametersFunctions::SetGraphicsScaleChangedNotificationEnabled>,
+ "SetGraphicsScaleChangedNotificationEnabled"},
+ };
+ RegisterHandlers(functions);
+}
+
+IGraphicsScalingParametersFunctions::~IGraphicsScalingParametersFunctions() = default;
+
+Result IGraphicsScalingParametersFunctions::GetGraphicsScalingParameters(
+ Out<GraphicsScalingParameters> out_parameters) {
+ // Prototype policy: lowest SDK scale (10 maps to public scale 0), unit screen ratio.
+ // The ratio field interpretation is inferred from the adjacent debug API.
+ *out_parameters = {10, 1, 1};
+ LOG_DEBUG(Service_AM, "called, prototype scale=10, screen ratio=1/1");
+ R_SUCCEED();
+}
+
+Result IGraphicsScalingParametersFunctions::SetGraphicsScaleChangedNotificationEnabled(bool enabled) {
+ LOG_DEBUG(Service_AM, "called, enabled={}", enabled);
+ std::scoped_lock lk{m_applet->lock};
+ m_applet->graphics_scale_changed_notification_enabled = enabled;
+ R_SUCCEED();
+}
+
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/graphics_scaling_parameters_functions.h b/src/core/hle/service/am/service/graphics_scaling_parameters_functions.h
new file mode 100644
index 0000000000..7c92cb53f4
--- /dev/null
+++ b/src/core/hle/service/am/service/graphics_scaling_parameters_functions.h
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "core/hle/service/cmif_types.h"
+#include "core/hle/service/service.h"
+
+namespace Service::AM {
+
+struct Applet;
+
+struct GraphicsScalingParameters {
+ u32 graphics_scale;
+ u32 screen_ratio_numerator;
+ u32 screen_ratio_denominator;
+};
+static_assert(sizeof(GraphicsScalingParameters) == 12);
+
+class IGraphicsScalingParametersFunctions final
+ : public ServiceFramework<IGraphicsScalingParametersFunctions> {
+public:
+ explicit IGraphicsScalingParametersFunctions(Core::System& system,
+ std::shared_ptr<Applet> applet);
+ ~IGraphicsScalingParametersFunctions() override;
+
+private:
+ Result GetGraphicsScalingParameters(Out<GraphicsScalingParameters> out_parameters);
+ Result SetGraphicsScaleChangedNotificationEnabled(bool enabled);
+
+ const std::shared_ptr<Applet> m_applet;
+};
+
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/graphics_session.cpp b/src/core/hle/service/am/service/graphics_session.cpp
new file mode 100644
index 0000000000..902e7f9706
--- /dev/null
+++ b/src/core/hle/service/am/service/graphics_session.cpp
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "core/core.h"
+#include "core/hle/kernel/k_client_session.h"
+#include "core/hle/kernel/k_scoped_resource_reservation.h"
+#include "core/hle/kernel/k_session.h"
+#include "core/hle/kernel/svc_results.h"
+#include "core/hle/service/am/service/graphics_session.h"
+#include "core/hle/service/server_manager.h"
+
+namespace Service::AM {
+Result CreateGraphicsSession(Core::System& system, HLERequestContext& ctx,
+ std::shared_ptr<SessionRequestHandler> handler,
+ Kernel::KClientSession** out_client) {
+ auto& kernel = system.Kernel();
+ Kernel::KScopedResourceReservation reservation(
+ kernel, Kernel::GetCurrentProcessPointer(kernel), Kernel::LimitableResource::SessionCountMax);
+ R_UNLESS(reservation.Succeeded(), Kernel::ResultLimitReached);
+ auto* session = Kernel::KSession::Create(kernel);
+ R_UNLESS(session != nullptr, Kernel::ResultOutOfResource);
+ session->Initialize(kernel, nullptr, 0);
+ reservation.Commit();
+ Kernel::KSession::Register(kernel, session);
+ auto& server = ctx.GetManager()->GetServerManager();
+ auto manager = std::make_shared<SessionRequestManager>(kernel, server);
+ manager->SetSessionHandler(std::move(handler));
+ R_TRY(server.RegisterSession(&session->GetServerSession(), std::move(manager)));
+ *out_client = &session->GetClientSession();
+ R_SUCCEED();
+}
+
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/graphics_session.h b/src/core/hle/service/am/service/graphics_session.h
new file mode 100644
index 0000000000..698b5ee8ee
--- /dev/null
+++ b/src/core/hle/service/am/service/graphics_session.h
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <memory>
+#include "core/hle/result.h"
+
+namespace Core { class System; }
+namespace Kernel { class KClientSession; }
+namespace Service {
+class HLERequestContext;
+class SessionRequestHandler;
+}
+namespace Service::AM {
+Result CreateGraphicsSession(Core::System& system, HLERequestContext& ctx,
+ std::shared_ptr<SessionRequestHandler> handler,
+ Kernel::KClientSession** out_client);
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/graphics_supervisor.cpp b/src/core/hle/service/am/service/graphics_supervisor.cpp
new file mode 100644
index 0000000000..7759f575e7
--- /dev/null
+++ b/src/core/hle/service/am/service/graphics_supervisor.cpp
@@ -0,0 +1,1612 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <atomic>
+#include <bit>
+#include <array>
+#include "common/alignment.h"
+#include "core/hle/service/nvdrv/core/nvmap.h"
+#include "core/core.h"
+#include "core/core_timing.h"
+#include "core/hle/service/nvnflinger/buffer_queue_producer.h"
+#include "core/hle/service/nvnflinger/ui/graphic_buffer.h"
+#include "core/hle/kernel/k_client_session.h"
+#include "core/hle/kernel/k_scoped_resource_reservation.h"
+#include "core/hle/kernel/k_session.h"
+#include "core/hle/kernel/k_readable_event.h"
+#include "core/hle/kernel/k_transfer_memory.h"
+#include "core/hle/kernel/k_event.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/kernel/svc_results.h"
+#include "core/hle/service/am/service/graphics_supervisor.h"
+#include "core/hle/service/am/service/graphics_session.h"
+#include "core/hle/service/am/service/gpu_session.h"
+#include "core/hle/service/am/service/graphics_supervisor_protocol.h"
+#include "core/hle/service/am/service/graphics_syncpoint_cache.h"
+#include "core/hle/service/am/service/native_image_layout.h"
+#include "core/hle/service/ipc_helpers.h"
+#include "core/hle/service/nvdrv/nvdrv_interface.h"
+#include "core/hle/service/nvdrv/core/container.h"
+#include "core/hle/service/nvdrv/core/syncpoint_manager.h"
+#include "core/hle/service/server_manager.h"
+#include "core/hle/service/sm/sm.h"
+#include "core/hle/service/vi/application_display_service.h"
+#include "core/hle/service/vi/container.h"
+#include "core/hle/service/vi/manager_root_service.h"
+#include "core/hle/service/vi/vi_types.h"
+
+namespace Service::AM {
+struct NativeImageRegistry {
+ struct Image {
+ u32 id;
+ u32 memory_id;
+ std::array<u32, 16> preference;
+ u64 size;
+ bool window_pool{};
+ };
+ NativeImageRegistry(Kernel::KernelCore& kernel_, Kernel::KProcess* process_)
+ : kernel{kernel_}, process{process_} {
+ process->Open(kernel);
+ }
+ ~NativeImageRegistry() {
+ if (module) {
+ for (const auto& image : images) {
+ ReleaseMemory(image.memory_id);
+ }
+ }
+ if (memory_session) {
+ module->GetContainer().CloseSession(*memory_session);
+ }
+ process->Close(kernel);
+ }
+ void ReleaseMemory(u32 id) {
+ const auto info = module->GetContainer().GetNvMapFile().FreeHandle(id, true);
+ if (info && info->can_unlock && info->address != 0) {
+ process->GetPageTable().UnlockForDeviceAddressSpace(info->address, info->size);
+ }
+ }
+ Kernel::KernelCore& kernel;
+ Kernel::KProcess* process;
+ std::mutex mutex;
+ std::shared_ptr<Nvidia::Module> module;
+ std::optional<Nvidia::NvCore::SessionId> memory_session;
+ std::vector<Image> images;
+};
+using NativeImage = NativeImageRegistry::Image;
+
+
+namespace {
+
+// TIPC payloads follow a 32-bit result, so these handlers push result.raw.
+
+class ISyncpointSupervisor final : public ServiceFramework<ISyncpointSupervisor> {
+public:
+ ISyncpointSupervisor(Core::System& system, std::shared_ptr<Nvidia::Module> module)
+ : ServiceFramework{system, "ISyncpointSupervisor"}, m_module{std::move(module)},
+ m_context{system, "NativeSyncpoint"} {
+ static const FunctionInfo functions[] = {
+ {0, &ISyncpointSupervisor::ExchangeClientToken, "ExchangeClientToken"},
+ {1, &ISyncpointSupervisor::GetMaximumEventCount, "GetMaximumEventCount"},
+ {21, &ISyncpointSupervisor::SetEventCount, "SetEventCount"},
+ {22, &ISyncpointSupervisor::GetEvent, "GetEvent"},
+ {201, &ISyncpointSupervisor::ArmEvent, "ArmEvent"},
+ {202, &ISyncpointSupervisor::CancelEvent, "CancelEvent"},
+ {103, &ISyncpointSupervisor::QueryCounter, "QueryCounter"},
+ };
+ RegisterHandlersTipc(functions);
+ }
+
+ ~ISyncpointSupervisor() override {
+ if (m_poll) system.CoreTiming().UnscheduleEvent(m_poll);
+ for (auto& event : m_events) m_context.CloseEvent(event.event);
+ }
+
+ Result HandleSyncRequest(Kernel::KServerSession& session, HLERequestContext& ctx) override {
+ if (!ctx.IsTipc() || handlers_tipc.contains(ctx.GetCommand())) {
+ return ServiceFrameworkBase::HandleSyncRequest(session, ctx);
+ }
+ const auto guard = LockService();
+ LOG_DEBUG(Service_AM, "syncpoint unsupported command={}, words={:08x}", ctx.GetCommand(),
+ fmt::join(std::span<const u32>(ctx.CommandBuffer() + 2, std::min<u32>(ctx.CommandBuffer()[1], 32)), ","));
+ InvokeRequestTipc(ctx);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(Kernel::ResultNotImplemented.raw);
+ return system.IsPoweredOn() ? ctx.WriteToOutgoingCommandBuffer() : ResultSuccess;
+ }
+
+private:
+ static constexpr u32 MaximumEventCount = 63;
+
+ void ExchangeClientToken(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const Result result = packet[0] == 0x10 && packet[1] == 2
+ ? ResultSuccess : Kernel::ResultInvalidCombination;
+ const u64 token = u64{packet[2]} | (u64{packet[3]} << 32);
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(result.raw);
+ rb.Push(token);
+ LOG_DEBUG(Service_AM, "syncpoint token={:#x}, result={:#x}", token, result.raw);
+ }
+ void GetMaximumEventCount(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const Result result = packet[0] == 0x11 && packet[1] == 0
+ ? ResultSuccess : Kernel::ResultInvalidCombination;
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result.raw);
+ rb.Push(MaximumEventCount);
+ LOG_DEBUG(Service_AM, "syncpoint maximum events={}", MaximumEventCount);
+ }
+
+ void SetEventCount(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x25 && packet[1] == 1 && packet[2] <= MaximumEventCount) {
+ m_event_count = packet[2];
+ result = ResultSuccess;
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ LOG_DEBUG(Service_AM, "syncpoint event count={}, result={:#x}", m_event_count, result.raw);
+ }
+
+ void GetEvent(HLERequestContext& ctx) {
+ const auto* p = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ Kernel::KReadableEvent* readable{};
+ if (p[0] == 0x26 && p[1] == 1 && p[2] < m_event_count) {
+ auto& event = m_events[p[2]];
+ if (!event.event) event.event = m_context.CreateEvent("SyncpointCompletion");
+ result = event.event ? ResultSuccess : Kernel::ResultOutOfResource;
+ if (event.event) {
+ readable = &event.event->GetReadableEvent();
+ readable->Open(kernel);
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(result.raw);
+ ctx.AddMoveObject(readable);
+ }
+ void RefreshEvents() {
+ auto& manager = m_module->GetContainer().GetSyncpointManager();
+ for (auto& event : m_events) {
+ if (!event.pending) continue;
+ const auto current = manager.ReadAllocatedSyncpoint(event.id);
+ if (current && event.last_observed != *current) {
+ event.last_observed = *current;
+ LOG_INFO(Service_AM, "Syncpoint wait progress: id={}, current={}, target={}", event.id, *current, event.threshold);
+ }
+ if (current && static_cast<s32>(*current - event.threshold) >= 0) {
+ event.pending = false;
+ event.event->Signal(kernel);
+ }
+ }
+ }
+ void ArmEvent(HLERequestContext& ctx) {
+ const auto* p = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ const u32 id = ~p[3];
+ if (p[0] == 0xd9 && p[1] == 5 && p[2] < m_event_count &&
+ p[5] == 0 && p[6] == 0 &&
+ m_module->GetContainer().GetSyncpointManager().ReadAllocatedSyncpoint(id)) {
+ auto& event = m_events[p[2]];
+ if (!event.event) event.event = m_context.CreateEvent("SyncpointCompletion");
+ result = event.event ? ResultSuccess : Kernel::ResultOutOfResource;
+ if (event.event) {
+ event.event->Clear(kernel);
+ event.id = id;
+ event.threshold = p[4];
+ event.last_observed = ~u32{0};
+ LOG_INFO(Service_AM, "Arm syncpoint wait: slot={}, id={}, target={}", p[2], id, event.threshold);
+ event.pending = true;
+ RefreshEvents();
+ if (!m_poll) {
+ m_poll = Core::Timing::CreateEvent("NativeSyncpointCompletion",
+ [this](s64, std::chrono::nanoseconds) -> std::optional<std::chrono::nanoseconds> {
+ const auto guard = LockService();
+ RefreshEvents();
+ return std::nullopt;
+ });
+ system.CoreTiming().ScheduleLoopingEvent(std::chrono::milliseconds{1},
+ std::chrono::milliseconds{1}, m_poll);
+ }
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ LOG_DEBUG(Service_AM, "syncpoint arm event={}, id={}, threshold={}, result={:#x}",
+ p[2], id, p[4], result.raw);
+ }
+ void CancelEvent(HLERequestContext& ctx) {
+ const auto* p = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (p[0] == 0xda && p[1] == 1 && p[2] < m_event_count) {
+ auto& event = m_events[p[2]];
+ event.pending = false;
+ // The native SDK waits for this event after cancelling a timed wait
+ // before it releases the event slot. Cancellation acknowledges that
+ // the wait is retired; it does not advance the syncpoint counter.
+ if (event.event) event.event->Signal(kernel);
+ result = ResultSuccess;
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+
+ void QueryCounter(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ u64 value{};
+ if (packet[0] == 0x77 && packet[1] == 1) {
+ const u32 id = ~packet[2];
+ auto current = m_module->GetContainer().GetSyncpointManager().ReadAllocatedSyncpoint(id);
+ result = current ? ResultSuccess : Kernel::ResultNotFound;
+ value = current.value_or(0);
+ LOG_DEBUG(Service_AM, "query syncpoint id={}, value={}, result={:#x}", id, value, result.raw);
+ }
+ IPC::ResponseBuilder rb{ctx, 5};
+ rb.Push(result.raw);
+ rb.Push(value);
+ rb.Push<u32>(0); // Existing NVDRV counters use 32-bit modular comparisons.
+ }
+
+ std::shared_ptr<Nvidia::Module> m_module;
+ u32 m_event_count{};
+ struct CompletionEvent {
+ Kernel::KEvent* event{};
+ u32 id{};
+ u32 threshold{};
+ u32 last_observed = ~u32{0};
+ bool pending{};
+ };
+ KernelHelpers::ServiceContext m_context;
+ std::array<CompletionEvent, MaximumEventCount> m_events{};
+ std::shared_ptr<Core::Timing::EventType> m_poll;
+};
+
+class IVi2Session final : public ServiceFramework<IVi2Session> {
+ struct ClientState {
+ u64 token{};
+ u32 slots{};
+ std::optional<u64> display;
+ std::optional<u64> layer;
+ u64 available{0x7fff};
+ u32 retire_notification{};
+ u64 output_encoding{0x14};
+ u32 available_notification{};
+ std::shared_ptr<android::BufferQueueProducer> producer;
+ std::array<std::optional<s32>, 15> queued{};
+ std::optional<size_t> player_display;
+ };
+
+public:
+ explicit IVi2Session(Core::System& system, u64 aruid, std::shared_ptr<NativeImageRegistry> images) : ServiceFramework{system, "IVi2Session"}, m_images{std::move(images)}, m_aruid{aruid}, m_context{system, "Vi2Session"} {
+ static const FunctionInfo functions[] = {
+ {1, &IVi2Session::GetSessionLimit, "GetNotificationLimit"},
+ {11, &IVi2Session::GetNotificationEvent, "GetNotificationEvent"},
+ {13, &IVi2Session::ReleaseNotificationEvent, "ReleaseNotificationEvent"},
+ {21, &IVi2Session::GetNotifications, "GetNotifications"},
+ {31, &IVi2Session::DuplicateVsyncEvent, "GetDisplayEvent"},
+ {32, &IVi2Session::DuplicateDisplayEvent, "DuplicateDisplayEvent"},
+ {3, &IVi2Session::GetSessionLimit, "GetSessionLimit"},
+ {5, &IVi2Session::GetSessionLimit, "GetRequestGroupLimit"},
+ {51, &IVi2Session::ConfigureRequestGroups, "ConfigureRequestGroups"},
+ {101, &IVi2Session::OpenDisplay, "OpenDisplay"},
+ {102, &IVi2Session::CloseDisplay, "CloseDisplay"},
+ {111, &IVi2Session::OpenEndpoint, "OpenEndpoint"},
+ {112, &IVi2Session::CloseEndpoint, "CloseEndpoint"},
+ {201, &IVi2Session::RegisterClientState, "RegisterClientState"},
+ {202, &IVi2Session::UnregisterClientState, "UnregisterClientState"},
+ {212, &IVi2Session::GetClientAvailability, "GetClientAvailability"},
+ {229, &IVi2Session::PresentClientBuffer, "PresentClientBuffer"},
+ {228, &IVi2Session::CancelClientBuffer, "CancelClientBuffer"},
+ {351, &IVi2Session::SetClientOutputEncoding, "SetClientOutputEncoding"},
+ {261, &IVi2Session::SetDisplayCapability, "SetDisplayCapability261"},
+ {265, &IVi2Session::SetDisplayCapability, "SetDisplayCapability265"},
+ {267, &IVi2Session::SetDisplayCapability, "SetDisplayCapability267"},
+ {271, &IVi2Session::SetDisplayCapability, "SetDisplayCapability271"},
+ {251, &IVi2Session::BindClientDisplay, "BindClientDisplay"},
+ {252, &IVi2Session::UnbindClientDisplay, "UnbindClientDisplay"},
+ };
+ RegisterHandlersTipc(functions);
+ }
+
+ ~IVi2Session() override {
+ if (m_release_poll) system.CoreTiming().UnscheduleEvent(m_release_poll);
+ if (m_display_service) {
+ for (auto& state : m_client_states) {
+ if (state.layer) m_display_service->GetContainer()->DestroyManagedLayer(*state.layer);
+ }
+ }
+ if (m_default_display) { m_display_service->CloseDisplay(*m_default_display); }
+ for (auto* event : m_display_change_events) m_context.CloseEvent(event);
+ m_context.CloseEvent(m_event);
+ }
+
+ Result HandleSyncRequest(Kernel::KServerSession& session, HLERequestContext& ctx) override {
+ if (!ctx.IsTipc() || handlers_tipc.contains(ctx.GetCommand())) {
+ return ServiceFrameworkBase::HandleSyncRequest(session, ctx);
+ }
+ const auto guard = LockService();
+ LOG_DEBUG(Service_AM, "VI2 unsupported command={}, words={:08x}", ctx.GetCommand(),
+ fmt::join(std::span<const u32>(ctx.CommandBuffer() + 2, std::min<u32>(ctx.CommandBuffer()[1], 32)), ","));
+ InvokeRequestTipc(ctx);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(Kernel::ResultNotImplemented.raw);
+ return system.IsPoweredOn() ? ctx.WriteToOutgoingCommandBuffer() : ResultSuccess;
+ }
+private:
+ void SetDisplayCapability(HLERequestContext& ctx) {
+ const auto* p = ctx.CommandBuffer();
+ const u64 name = u64{p[2]} | (u64{p[3]} << 32);
+ Result result = Kernel::ResultInvalidCombination;
+ if (p[0] == ctx.GetCommand() + 16 && p[1] == 3 && p[4] <= 1) {
+ result = name == 0x00746c7561666564ULL && m_default_display
+ ? ResultSuccess : Kernel::ResultNotFound;
+ if (result.IsSuccess()) {
+ // Per-client display mode capabilities do not change the host video mode.
+ m_display_capabilities[(ctx.GetCommand() - 261) / 2] = p[4] != 0;
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void DuplicateVsyncEvent(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ Kernel::KReadableEvent* readable{};
+ if (packet[0] == 0x2f && packet[1] == 1 && packet[2] < 5) {
+ result = m_display_service && m_default_display
+ ? ResultSuccess : Kernel::ResultInvalidState;
+ if (result.IsSuccess() && !m_vsync_event) {
+ result = m_display_service->GetDisplayVsyncEvent(&m_vsync_event, *m_default_display);
+ }
+ if (result.IsSuccess()) {
+ if (packet[2] == 0) {
+ readable = m_vsync_event;
+ } else {
+ // Fixed host display properties have no change to report until changed.
+ auto*& event = m_display_change_events[packet[2] - 1];
+ if (!event) event = m_context.CreateEvent("DisplayPropertyChange");
+ if (event) readable = &event->GetReadableEvent();
+ else result = Kernel::ResultOutOfResource;
+ }
+ if (readable) readable->Open(kernel);
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(result.raw);
+ ctx.AddMoveObject(readable);
+ LOG_DEBUG(Service_AM, "VI2 vsync event result={:#x}", result.raw);
+ }
+ void DuplicateDisplayEvent(HLERequestContext& ctx) {
+ const auto* p = ctx.CommandBuffer();
+ auto source = ctx.GetObjectFromHandle<Kernel::KReadableEvent>(ctx.GetCopyHandle(0));
+ const Result result = p[0] == 0x30 && p[1] == 0x80000001 && p[4] <= 1 && source.IsNotNull()
+ ? ResultSuccess : Kernel::ResultInvalidCombination;
+ Kernel::KReadableEvent* readable{};
+ if (result.IsSuccess()) {
+ readable = source.GetPointerUnsafe();
+ readable->Open(kernel);
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(result.raw);
+ ctx.AddMoveObject(readable);
+ }
+ static std::optional<size_t> GetPlayerDisplayIndex(u64 name) {
+ if ((name & 0xffffffffffffULL) != 0x726579616c70ULL || (name >> 56) != 0) {
+ return std::nullopt;
+ }
+ const u32 player = (name >> 48) & 0xff;
+ if (player < '1' || player > '3') return std::nullopt;
+ return player - '1';
+ }
+ Result OpenDefaultDisplay(u64 name, u32 flags) {
+ if (const auto player = GetPlayerDisplayIndex(name)) {
+ R_UNLESS(flags < m_notifications.size() * 32, Kernel::ResultInvalidCombination);
+ R_UNLESS(m_player_display_references[*player] < 1024, Kernel::ResultOutOfResource);
+ // GameShare's detached player displays retain their own lifetime;
+ // they do not target the host's default display compositor.
+ m_player_display_notifications[*player] = flags;
+ ++m_player_display_references[*player];
+ R_SUCCEED();
+ }
+ // The native display token is an eight-byte name; command 101 returns
+ // only a result. Retain the real VI display ID for later operations.
+ R_UNLESS(name == 0x00746c7561666564ULL, Kernel::ResultNotFound);
+ R_UNLESS(flags == 1, Kernel::ResultNotImplemented);
+ R_UNLESS(m_display_references < 1024, Kernel::ResultOutOfResource);
+ if (!m_display_service) {
+ const auto root = system.ServiceManager().GetService<VI::IManagerRootService>("vi:m", true);
+ R_UNLESS(root, Kernel::ResultInvalidState);
+ R_TRY(root->GetDisplayService(&m_display_service, VI::Policy::Compositor));
+ }
+ if (!m_default_display) {
+ u64 id{};
+ R_TRY(m_display_service->OpenDefaultDisplay(&id));
+ m_default_display = id;
+ }
+ m_display_notification = flags;
+ ++m_display_references;
+ R_SUCCEED();
+ }
+ void OpenDisplay(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 name = u64{packet[2]} | (u64{packet[3]} << 32);
+ const auto result = packet[0] == 0x75 && packet[1] == 3
+ ? OpenDefaultDisplay(name, packet[4]) : Kernel::ResultInvalidCombination;
+ LOG_DEBUG(Service_AM, "VI2 open display name={:#x}, flags={:#x}, references={}, result={:#x}",
+ name, packet[4], m_display_references, result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void CloseDisplay(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ const u64 name = u64{packet[2]} | (u64{packet[3]} << 32);
+ if (packet[0] == 0x76 && packet[1] == 2 && GetPlayerDisplayIndex(name)) {
+ const auto player = *GetPlayerDisplayIndex(name);
+ result = Kernel::ResultNotFound;
+ if (m_player_display_references[player]) {
+ if (--m_player_display_references[player] == 0) {
+ const auto notification = m_player_display_notifications[player];
+ m_notifications[notification / 32] |= 1U << (notification % 32);
+ if (m_event) m_event->Signal(kernel);
+ }
+ result = ResultSuccess;
+ }
+ } else if (packet[0] == 0x76 && packet[1] == 2 && name == 0x00746c7561666564ULL) {
+ result = Kernel::ResultNotFound;
+ if (m_default_display && m_display_references) {
+ result = ResultSuccess;
+ if (m_display_references == 1) {
+ result = m_display_service->CloseDisplay(*m_default_display);
+ if (result.IsSuccess()) { m_default_display.reset(); }
+ }
+ if (result.IsSuccess()) {
+ --m_display_references;
+ if (!m_display_references) {
+ m_notifications[m_display_notification / 32] |= 1U << (m_display_notification % 32);
+ if (m_event) m_event->Signal(kernel);
+ LOG_DEBUG(Service_AM, "VI2 display retired notification={}", m_display_notification);
+ }
+ }
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void RegisterClientState(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 token = u64{packet[2]} | (u64{packet[3]} << 32);
+ const u32 first = packet[4];
+ const u32 second = packet[5];
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0xd9 && packet[1] == 4 && token != 0 &&
+ first < 32 && second < 32 && first != second) {
+ const u32 slots = (1U << first) | (1U << second);
+ const auto existing = std::ranges::find(m_client_states, token, &ClientState::token);
+ const auto free = std::ranges::find(m_client_states, u64{0}, &ClientState::token);
+ result = Kernel::ResultInvalidState;
+ if (existing == m_client_states.end() && !(m_client_slots & slots)) {
+ result = Kernel::ResultOutOfResource;
+ if (free != m_client_states.end()) {
+ // The SDK sends its object address as an opaque identity, not a
+ // service-accessible buffer. Keep the associated slots session-local.
+ *free = {token, slots};
+ free->retire_notification = first;
+ free->available_notification = second;
+ m_client_slots |= slots;
+ result = ResultSuccess;
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "VI2 register client state token={:#x}, slots={}/{}, result={:#x}",
+ token, first, second, result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void UnregisterClientState(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 token = u64{packet[2]} | (u64{packet[3]} << 32);
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0xda && packet[1] == 2 && token != 0) {
+ const auto state = std::ranges::find(m_client_states, token, &ClientState::token);
+ result = Kernel::ResultNotFound;
+ if (state != m_client_states.end()) {
+ if (m_default_endpoint == token) { m_default_endpoint.reset(); }
+ if (state->layer) m_display_service->GetContainer()->DestroyManagedLayer(*state->layer);
+ m_notifications[state->retire_notification / 32] |= 1U << (state->retire_notification % 32);
+ if (m_event) m_event->Signal(kernel);
+ LOG_DEBUG(Service_AM, "VI2 client retired token={:#x}, notification={}", token, state->retire_notification);
+ m_client_slots &= ~state->slots;
+ *state = {};
+ result = ResultSuccess;
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void GetClientAvailability(HLERequestContext& ctx) {
+ RefreshReleasedBuffers();
+ const auto* packet = ctx.CommandBuffer();
+ const u64 token = u64{packet[2]} | (u64{packet[3]} << 32);
+ Result result = Kernel::ResultInvalidCombination;
+ u64 id{};
+ if (packet[0] == 0xe4 && packet[1] == 2 && token) {
+ auto state = std::ranges::find(m_client_states, token, &ClientState::token);
+ result = Kernel::ResultNotFound;
+ if (state != m_client_states.end() && m_default_display) {
+ result = ResultSuccess;
+ if (state->player_display) {
+ id = 0;
+ } else if (!state->layer) {
+ result = m_display_service->GetContainer()->CreateManagedLayer(&id, *m_default_display, m_aruid);
+ if (result.IsSuccess()) state->layer = id;
+ } else {
+ id = *state->layer;
+ }
+ }
+ }
+ const auto state = std::ranges::find(m_client_states, token, &ClientState::token);
+ const u64 available = result.IsSuccess() ? state->available : 0;
+ LOG_DEBUG(Service_AM, "VI2 client availability token={:#x}, layer={}, mask={:#x}, result={:#x}", token, id, available, result.raw);
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(result.raw);
+ rb.Push(available);
+ }
+ void CancelClientBuffer(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 token = u64{packet[2]} | (u64{packet[3]} << 32);
+ const u32 slot = GraphicsSupervisorProtocol::DecodeBufferSlot(packet[4]);
+ Result result = Kernel::ResultInvalidCombination;
+ u64 available{};
+ if (packet[0] == 0xf4 && packet[1] == 19 && slot < 15) {
+ auto state = std::ranges::find(m_client_states, token, &ClientState::token);
+ result = Kernel::ResultNotFound;
+ if (token && state != m_client_states.end()) {
+ // Unlike present (229), cancel has no image or input GPU fence.
+ state->available |= u64{1} << slot;
+ available = state->available;
+ result = ResultSuccess;
+ }
+ }
+ LOG_DEBUG(Service_AM, "VI2 cancel buffer token={:#x}, slot={}, mask={:#x}, result={:#x}", token, slot, available, result.raw);
+ IPC::ResponseBuilder rb{ctx, 8};
+ rb.Push(result.raw);
+ rb.Push(available);
+ for (u32 i = 0; i < 4; ++i) rb.Push<u32>(0);
+ }
+ void RefreshReleasedBuffers() {
+ for (auto& state : m_client_states) {
+ if (!state.producer || !m_images->module) continue;
+ bool changed{};
+ for (size_t slot = 0; slot < state.queued.size(); ++slot) {
+ if (!state.queued[slot]) continue;
+ const auto fence = state.producer->GetReleasedBufferFence(*state.queued[slot]);
+ if (!fence) continue;
+ bool complete = true;
+ for (u32 i = 0; i < fence->num_fences; ++i) {
+ const auto& point = fence->fences[i];
+ if (point.id != ~u32{0}) {
+ const auto counter = m_images->module->GetContainer().GetSyncpointManager().ReadAllocatedSyncpoint(point.id);
+ if (!counter || static_cast<s32>(*counter - point.value) < 0) {
+ complete = false;
+ }
+ }
+ }
+ if (!complete) continue;
+ state.queued[slot].reset();
+ state.available |= u64{1} << slot;
+ LOG_DEBUG(Service_AM, "Native display buffer retired: slot={}, mask={:#x}", slot, state.available);
+ changed = true;
+ }
+ if (changed) {
+ m_notifications[state.available_notification / 32] |= 1U << (state.available_notification % 32);
+ if (m_event) m_event->Signal(kernel);
+ }
+ }
+ }
+ Result QueueNativeImage(ClientState& state, const u32* packet) {
+ const u32 slot = GraphicsSupervisorProtocol::DecodeBufferSlot(packet[4]);
+ R_UNLESS(!state.player_display, Kernel::ResultNotImplemented);
+ R_UNLESS(state.layer && state.display && slot < 15 && !state.queued[slot], Kernel::ResultInvalidState);
+ const std::scoped_lock images_lock{m_images->mutex};
+ const auto image = std::ranges::find(m_images->images, packet[5], &NativeImage::id);
+ R_UNLESS(image != m_images->images.end() && image->window_pool && m_images->module, Kernel::ResultNotFound);
+ // The observed presentation is a four-byte color image with the
+ // backend's 16-GOB block height and no byte offset within its pool.
+ R_UNLESS(packet[6] == 0 && packet[7] == image->preference[2] && packet[8] == 3 &&
+ image->preference[4] == 7 &&
+ (state.output_encoding == 0x14 || state.output_encoding == 0x5),
+ Kernel::ResultNotImplemented);
+ const u32 fence_id = ~packet[22];
+ R_UNLESS(packet[23] == 0 && packet[25] == 0 &&
+ (fence_id == ~u32{0} || m_images->module->GetContainer().GetSyncpointManager().IsSyncpointAllocated(fence_id)),
+ Kernel::ResultInvalidCombination);
+ if (!state.producer) {
+ R_TRY(m_display_service->GetContainer()->GetLayerProducerHandle(&state.producer, *state.layer));
+ android::QueueBufferOutput output{};
+ R_UNLESS(state.producer->Connect({}, android::NativeWindowApi::Egl, false, &output) == android::Status::NoError,
+ Kernel::ResultInvalidState);
+ R_UNLESS(state.producer->SetBufferCount(16) == android::Status::NoError, Kernel::ResultInvalidState);
+ s32 binder{};
+ R_TRY(m_display_service->GetContainer()->OpenLayer(&binder, *state.layer, m_aruid));
+ R_TRY(m_display_service->GetContainer()->SetLayerVisibility(*state.layer, true));
+ }
+ auto native = std::make_shared<android::NvGraphicBuffer>();
+ native->width = static_cast<s32>((image->preference[2] & 0xffffU) + 1);
+ native->height = static_cast<s32>((image->preference[2] >> 16) + 1);
+ native->stride = static_cast<s32>(image->preference[5] * 16);
+ native->format = android::PixelFormat::Rgba8888;
+ native->external_format = native->format;
+ native->buffer_id = image->memory_id;
+ auto buffer = std::make_shared<android::GraphicBuffer>(m_images->module->GetContainer().GetNvMapFile(), native);
+ s32 queue_slot{};
+ R_UNLESS(state.producer->AttachBuffer(&queue_slot, buffer) == android::Status::NoError, Kernel::ResultOutOfResource);
+ android::QueueBufferInput input{};
+ android::QueueBufferOutput output{};
+ input.crop = {0, 0, native->width, native->height};
+ input.swap_interval = 1;
+ input.fence = android::Fence::NoFence();
+ if (fence_id != ~u32{0}) {
+ input.fence.num_fences = 1;
+ input.fence.fences[0] = {static_cast<s32>(fence_id), packet[24]};
+ }
+ if (state.producer->QueueBuffer(queue_slot, input, &output) != android::Status::NoError) {
+ state.producer->CancelBuffer(queue_slot, input.fence);
+ return Kernel::ResultInvalidState;
+ }
+ state.queued[slot] = queue_slot;
+ state.available &= ~(u64{1} << slot);
+ if (!m_release_poll) {
+ m_release_poll = Core::Timing::CreateEvent("NativeViBufferRelease", [this](s64, std::chrono::nanoseconds) -> std::optional<std::chrono::nanoseconds> {
+ const auto guard = LockService();
+ RefreshReleasedBuffers();
+ return std::nullopt;
+ });
+ system.CoreTiming().ScheduleLoopingEvent(std::chrono::milliseconds{1}, std::chrono::milliseconds{1}, m_release_poll);
+ }
+ LOG_INFO(Service_AM, "Native image queued for display: image={}, slot={}, fence={}/{}, dimensions={}x{}",
+ image->id, slot, fence_id, packet[24], native->width, native->height);
+ R_SUCCEED();
+ }
+ void PresentClientBuffer(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 token = u64{packet[2]} | (u64{packet[3]} << 32);
+ RefreshReleasedBuffers();
+ Result result = Kernel::ResultInvalidCombination;
+ u64 available{};
+ if (packet[0] == 0xf5 && packet[1] == 24) {
+ const auto state = std::ranges::find(m_client_states, token, &ClientState::token);
+ result = Kernel::ResultNotFound;
+ if (token && state != m_client_states.end()) {
+ result = QueueNativeImage(*state, packet);
+ available = state->available;
+ }
+ }
+ LOG_DEBUG(Service_AM, "VI2 present token={:#x}, image={}, slot={}, mask={:#x}, result={:#x}", token, packet[5], packet[4], available, result.raw);
+ IPC::ResponseBuilder rb{ctx, 8};
+ rb.Push(result.raw);
+ rb.Push(available);
+ for (u32 i = 0; i < 4; ++i) rb.Push<u32>(0);
+ }
+ void SetClientOutputEncoding(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 token = u64{packet[2]} | (u64{packet[3]} << 32);
+ const u64 encoding = u64{packet[4]} | (u64{packet[5]} << 32);
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x16f && packet[1] == 4) {
+ const auto state = std::ranges::find(m_client_states, token, &ClientState::token);
+ result = Kernel::ResultNotFound;
+ if (token && state != m_client_states.end()) {
+ result = Kernel::ResultNotImplemented;
+ // Only the SDK's observed default output encoding is supported.
+ if (encoding == 0x14 || encoding == 0x5) {
+ state->output_encoding = encoding;
+ result = ResultSuccess;
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "VI2 output encoding token={:#x}, encoding={:#x}, result={:#x}", token, encoding, result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void BindClientDisplay(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 token = u64{packet[2]} | (u64{packet[3]} << 32);
+ const u64 name = u64{packet[4]} | (u64{packet[5]} << 32);
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x10b && packet[1] == 4) {
+ const auto state = std::ranges::find(m_client_states, token, &ClientState::token);
+ result = Kernel::ResultNotFound;
+ if (token && state != m_client_states.end() && GetPlayerDisplayIndex(name)) {
+ const auto player = *GetPlayerDisplayIndex(name);
+ result = Kernel::ResultInvalidState;
+ if (m_player_display_references[player] && !state->display) {
+ result = ResultSuccess;
+ if (state->layer) {
+ result = m_display_service->GetContainer()->DestroyManagedLayer(*state->layer);
+ }
+ if (result.IsSuccess()) {
+ state->layer.reset();
+ state->display = name;
+ state->player_display = player;
+ }
+ }
+ } else if (token && state != m_client_states.end() && name == 0x00746c7561666564ULL &&
+ m_default_display && m_display_references) {
+ result = Kernel::ResultInvalidState;
+ if (!state->display) {
+ state->display = m_default_display;
+ result = ResultSuccess;
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "VI2 bind client token={:#x}, display={:#x}, result={:#x}",
+ token, name, result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void UnbindClientDisplay(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 token = u64{packet[2]} | (u64{packet[3]} << 32);
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x10c && packet[1] == 2) {
+ const auto state = std::ranges::find(m_client_states, token, &ClientState::token);
+ result = Kernel::ResultNotFound;
+ if (token && state != m_client_states.end() && state->display) {
+ state->display.reset();
+ state->player_display.reset();
+ result = ResultSuccess;
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void OpenEndpoint(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 name = u64{packet[2]} | (u64{packet[3]} << 32);
+ const u64 display_name = u64{packet[4]} | (u64{packet[5]} << 32);
+ const u64 token = u64{packet[6]} | (u64{packet[7]} << 32);
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x7f && packet[1] == 6) {
+ const auto state = std::ranges::find(m_client_states, token, &ClientState::token);
+ result = Kernel::ResultNotFound;
+ if (token && state != m_client_states.end() && state->display &&
+ m_default_display == state->display && name == 0x00746c7561666564ULL &&
+ display_name == name) {
+ result = Kernel::ResultInvalidState;
+ if (!m_default_endpoint) {
+ m_default_endpoint = token;
+ result = ResultSuccess;
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "VI2 open endpoint name={:#x}, display={:#x}, client={:#x}, result={:#x}",
+ name, display_name, token, result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void CloseEndpoint(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 name = u64{packet[2]} | (u64{packet[3]} << 32);
+ const u64 display_name = u64{packet[4]} | (u64{packet[5]} << 32);
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x80 && packet[1] == 4 && name == 0x00746c7561666564ULL &&
+ display_name == name) {
+ result = Kernel::ResultNotFound;
+ if (m_default_endpoint) {
+ m_default_endpoint.reset();
+ result = ResultSuccess;
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void ConfigureRequestGroups(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x43 && packet[1] == 1 && packet[2] < 8) {
+ m_request_group_limit = packet[2];
+ m_request_groups_initialized = true;
+ result = ResultSuccess;
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void GetSessionLimit(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const Result result = packet[0] == 16 + ctx.GetCommand() && packet[1] == 0
+ ? ResultSuccess : Kernel::ResultInvalidCombination;
+ // The SDK converts this inclusive upper group index into 32 client slots.
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result.raw);
+ rb.Push<u32>(ctx.GetCommand() == 5 ? 7 : 0);
+ }
+
+ void GetNotifications(HLERequestContext& ctx) {
+ Result result = Kernel::ResultInvalidSize;
+ if (ctx.GetWriteBufferSize() >= sizeof(m_notifications)) {
+ ctx.WriteBuffer(m_notifications);
+ LOG_DEBUG(Service_AM, "VI2 notifications={:08x}", fmt::join(m_notifications, ","));
+ m_notifications.fill(0);
+ if (m_event) m_event->Clear(kernel);
+ result = ResultSuccess;
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void ReleaseNotificationEvent(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const auto result = packet[0] == 0x1d && packet[1] == 1 && packet[2] == 0
+ ? ResultSuccess : Kernel::ResultInvalidCombination;
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ }
+ void GetNotificationEvent(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ Kernel::KReadableEvent* readable{};
+ if (packet[0] == 0x1b && packet[1] == 1 && packet[2] == 0) {
+ if (!m_event) {
+ m_event = m_context.CreateEvent("Vi2Notification");
+ }
+ result = m_event ? ResultSuccess : Kernel::ResultOutOfResource;
+ if (m_event) {
+ if (std::ranges::any_of(m_notifications, [](u32 value) { return value != 0; })) m_event->Signal(kernel);
+ readable = &m_event->GetReadableEvent();
+ readable->Open(kernel);
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(result.raw);
+ ctx.AddMoveObject(readable);
+ }
+
+ std::shared_ptr<NativeImageRegistry> m_images;
+ std::shared_ptr<Core::Timing::EventType> m_release_poll;
+ std::array<ClientState, 16> m_client_states{};
+ u32 m_client_slots{};
+ std::shared_ptr<VI::IApplicationDisplayService> m_display_service;
+ Kernel::KReadableEvent* m_vsync_event{};
+ std::array<Kernel::KEvent*, 4> m_display_change_events{};
+ std::optional<u64> m_default_display;
+ std::array<bool, 6> m_display_capabilities{};
+ u32 m_display_references{};
+ u32 m_display_notification{};
+ std::array<u32, 3> m_player_display_references{};
+ std::array<u32, 3> m_player_display_notifications{};
+ bool m_request_groups_initialized{};
+ u32 m_request_group_limit{};
+ std::optional<u64> m_default_endpoint;
+ std::array<u32, 32> m_notifications{};
+ const u64 m_aruid;
+ KernelHelpers::ServiceContext m_context;
+ Kernel::KEvent* m_event{};
+};
+
+} // namespace
+
+
+IGraphicsSupervisor::IGraphicsSupervisor(Core::System& system, Kernel::KProcess* process, u64 aruid)
+ : ServiceFramework{system, "IGraphicsSupervisor"}, m_context{system, "GraphicsSupervisor"},
+ m_syncpoint_cache_event{m_context}, m_process{process}, m_aruid{aruid} {
+ m_process->Open(kernel);
+ m_image_registry = std::make_shared<NativeImageRegistry>(kernel, m_process);
+ static const FunctionInfo functions[] = {
+ {4, &IGraphicsSupervisor::Initialize, "Initialize"},
+ {11, &IGraphicsSupervisor::BindProcess, "BindProcess"},
+ {101, &IGraphicsSupervisor::CompleteBootstrap, "CompleteBootstrap"},
+ {115, &IGraphicsSupervisor::ConfigureNativeMemory, "ConfigureNativeMemory"},
+ {111, &IGraphicsSupervisor::CreateNativeMemory, "CreateNativeMemory"},
+ {112, &IGraphicsSupervisor::DestroyNativeMemory, "DestroyNativeMemory"},
+ {114, &IGraphicsSupervisor::BindNativeMemory, "BindNativeMemory"},
+ {201, &IGraphicsSupervisor::InitializeSyncpointContext, "InitializeSyncpointContext"},
+ {202, &IGraphicsSupervisor::GetSyncpointSession, "GetSyncpointSession"},
+ {204, &IGraphicsSupervisor::BindSyncpointMemory, "BindSyncpointMemory"},
+ {301, &IGraphicsSupervisor::InitializeDisplayContext, "InitializeDisplayContext"},
+ {305, &IGraphicsSupervisor::OpenDisplaySession, "OpenDisplaySession"},
+ {314, &IGraphicsSupervisor::ConfigureNativeWindowImagePool, "DestroyNativeWindowAliasImagePool"},
+ {315, &IGraphicsSupervisor::ConfigureNativeWindowImagePool, "CreateNativeWindowAliasImagePool"},
+ {401, &IGraphicsSupervisor::InitializeGpuContext, "InitializeGpuContext"},
+ {402, &IGraphicsSupervisor::InitializeGpuContext, "ActivateGpuContext"},
+ {423, &IGraphicsSupervisor::BindNativeGpuMemory, "BindNativeGpuMemory"},
+ {429, &IGraphicsSupervisor::CreateNativeImage, "CreateNativeImage"},
+ {430, &IGraphicsSupervisor::DestroyNativeImage, "DestroyNativeImage"},
+ {434, &IGraphicsSupervisor::RegisterGpuAddressSpace, "RegisterGpuAddressSpace"},
+ {497, &IGraphicsSupervisor::OpenGpuSession, "OpenGpuSession"},
+ {1101, &IGraphicsSupervisor::InitializeFrameContext, "InitializeFrameContext"},
+ {1102, &IGraphicsSupervisor::InitializeFrameContext, "ActivateFrameContext"},
+ {1402, &IGraphicsSupervisor::GetNvidiaDriver, "GetNvidiaDriver"},
+ {1501, &IGraphicsSupervisor::InitializeImageContext, "InitializeImageContext1501"},
+ {435, &IGraphicsSupervisor::CalculateImageRequirements, "CalculateNativeImageSize"},
+ {1510, &IGraphicsSupervisor::CalculateImageRequirements, "CalculateImageRequirements"},
+ {1502, &IGraphicsSupervisor::InitializeImageContext, "InitializeImageContext1502"},
+ };
+ RegisterHandlersTipc(functions);
+}
+
+IGraphicsSupervisor::~IGraphicsSupervisor() {
+ if (m_cache_poll) {
+ system.CoreTiming().UnscheduleEvent(m_cache_poll);
+ }
+ if (m_syncpoint_rw) {
+ m_syncpoint_rw->Close(kernel);
+ m_syncpoint_ro->Close(kernel);
+ }
+ if (m_nv_module) {
+ auto& nvmap = m_nv_module->GetContainer().GetNvMapFile();
+ for (const auto id : m_native_memories) {
+ if (const auto info = nvmap.FreeHandle(id, false); info && info->can_unlock && info->address != 0) {
+ m_process->GetPageTable().UnlockForDeviceAddressSpace(info->address, info->size);
+ }
+ }
+ if (m_native_memory_session) {
+ m_nv_module->GetContainer().CloseSession(Nvidia::NvCore::SessionId{*m_native_memory_session});
+ }
+ }
+ m_context.CloseEvent(m_syncpoint_request_event);
+ m_process->Close(kernel);
+}
+
+Result IGraphicsSupervisor::HandleSyncRequest(Kernel::KServerSession& session,
+ HLERequestContext& ctx) {
+ if (!ctx.IsTipc() || handlers_tipc.contains(ctx.GetCommand())) {
+ return ServiceFrameworkBase::HandleSyncRequest(session, ctx);
+ }
+ const auto guard = LockService();
+ InvokeRequestTipc(ctx);
+ // Unsupported TIPC must return an error instead of leaving request data as a reply.
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(Kernel::ResultNotImplemented.raw);
+ return system.IsPoweredOn() ? ctx.WriteToOutgoingCommandBuffer() : ResultSuccess;
+}
+
+void IGraphicsSupervisor::Initialize(HLERequestContext& ctx) {
+ const auto request = GraphicsSupervisorProtocol::DecodeBootstrap(
+ std::span<const u32>{ctx.CommandBuffer(), IPC::COMMAND_BUFFER_LENGTH});
+ Result result = Kernel::ResultInvalidCombination;
+ if (request) {
+ auto process = ctx.GetObjectFromHandle<Kernel::KProcess>(request->process_handle);
+ const bool empty_remote_memory = std::ranges::all_of(
+ request->parameters, [](u64 value) { return value == 0; });
+ if (process.IsNull() || process.GetPointerUnsafe() != m_process) {
+ result = Kernel::ResultInvalidHandle;
+ } else if (m_phase != Phase::Created) {
+ result = Kernel::ResultInvalidState;
+ } else if (request->applet_resource_user_id != m_aruid) {
+ result = Kernel::ResultInvalidArgument;
+ } else if ((request->flags != 0x8000000000000001ULL && request->flags != 0x8000000000000005ULL && request->flags != 0x8000000000000006ULL && request->flags != 0x8000000000000007ULL) || !empty_remote_memory) {
+ result = Kernel::ResultNotImplemented;
+ } else {
+ m_phase = Phase::Initialized;
+ result = ResultSuccess;
+ }
+ LOG_DEBUG(Service_AM, "graphics bootstrap aruid={:#x}, flags={:#x}, result={:#x}",
+ request->applet_resource_user_id, request->flags, result.raw);
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::BindProcess(HLERequestContext& ctx) {
+ Result result = Kernel::ResultInvalidCombination;
+ const auto* packet = ctx.CommandBuffer();
+ if (packet[0] == 0x1b && packet[1] == 0x80000000 && packet[2] == 2) {
+ auto process = ctx.GetObjectFromHandle<Kernel::KProcess>(ctx.GetCopyHandle(0));
+ if (process.IsNull() || process.GetPointerUnsafe() != m_process) {
+ result = Kernel::ResultInvalidHandle;
+ } else if (m_phase != Phase::Initialized) {
+ result = Kernel::ResultInvalidState;
+ } else {
+ m_phase = Phase::ProcessBound;
+ result = ResultSuccess;
+ }
+ }
+ LOG_DEBUG(Service_AM, "graphics process binding result={:#x}", result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::CompleteBootstrap(HLERequestContext& ctx) {
+ Result result = Kernel::ResultInvalidCombination;
+ const auto* packet = ctx.CommandBuffer();
+ if (packet[0] == 0x75 && packet[1] == 0) {
+ if (m_phase != Phase::ProcessBound) {
+ result = Kernel::ResultInvalidState;
+ } else {
+ m_phase = Phase::Ready;
+ result = ResultSuccess;
+ }
+ }
+ LOG_DEBUG(Service_AM, "graphics bootstrap completion result={:#x}", result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::InitializeImageContext(HLERequestContext& ctx) {
+ Result result = Kernel::ResultInvalidCombination;
+ const auto* packet = ctx.CommandBuffer();
+ if (packet[0] == 16 + ctx.GetCommand() && packet[1] == 0) {
+ if (m_phase != Phase::Ready) {
+ result = Kernel::ResultInvalidState;
+ } else if (ctx.GetCommand() == 1501) {
+ m_image_context_initialized = true;
+ result = ResultSuccess;
+ } else if (m_image_context_initialized) {
+ m_image_formats_initialized = true;
+ result = ResultSuccess;
+ } else {
+ result = Kernel::ResultInvalidState;
+ }
+ }
+ LOG_DEBUG(Service_AM, "empty image context command={}, result={:#x}",
+ ctx.GetCommand(), result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::OpenGpuSession(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ Kernel::KClientSession* client{};
+ const u64 flags = u64{packet[2]} | (u64{packet[3]} << 32);
+ if (packet[0] == 0x201 && packet[1] == 2 && (flags == 3 || flags == 4)) {
+ if (m_phase != Phase::Ready || !m_nv_module) {
+ result = Kernel::ResultInvalidState;
+ } else {
+ auto handler = std::make_shared<IGpuSession>(system, m_nv_module, m_process, flags == 3);
+ result = CreateGraphicsSession(system, ctx, handler, &client);
+ if (result.IsSuccess()) {
+ if (flags == 3) { m_image_formats_initialized = true; }
+ std::erase_if(m_gpu_sessions, [](const auto& item) { return item.expired(); });
+ m_gpu_sessions.emplace_back(handler);
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "GPU control session flags={:#x}, result={:#x}", flags, result.raw);
+ // This SDK reads the move-handle slot before the result, including on failure.
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(result.raw);
+ ctx.AddMoveObject(client);
+}
+
+void IGraphicsSupervisor::RegisterGpuAddressSpace(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x1c2 && packet[1] == 1) {
+ result = m_gpu_context_ready ? Kernel::ResultNotFound : Kernel::ResultInvalidState;
+ if (m_gpu_context_ready) {
+ for (const auto& item : m_gpu_sessions) {
+ if (auto handler = item.lock()) {
+ result = handler->RegisterAddressSpace(packet[2]);
+ if (result.IsSuccess()) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "register GPU address space id={}, result={:#x}", packet[2], result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::InitializeDisplayContext(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x13d && packet[1] == 0) {
+ result = m_phase == Phase::Ready ? ResultSuccess : Kernel::ResultInvalidState;
+ if (result.IsSuccess()) {
+ m_display_context_initialized = true;
+ }
+ }
+ LOG_DEBUG(Service_AM, "display context initialization result={:#x}", result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::OpenDisplaySession(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Kernel::KClientSession* client{};
+ const u64 flags = u64{packet[2]} | (u64{packet[3]} << 32);
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x141 && packet[1] == 2 && (flags == 0x300010000ULL || flags == 0x300020000ULL)) {
+ result = m_display_context_initialized ? ResultSuccess : Kernel::ResultInvalidState;
+ if (result.IsSuccess()) {
+ result = CreateGraphicsSession(system, ctx, std::make_shared<IVi2Session>(system, m_aruid, m_image_registry),
+ &client);
+ }
+ }
+ LOG_DEBUG(Service_AM, "VI2 session flags={:#x}, result={:#x}", flags, result.raw);
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(result.raw);
+ ctx.AddMoveObject(client);
+}
+
+void IGraphicsSupervisor::InitializeGpuContext(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 16 + ctx.GetCommand() && packet[1] == 0) {
+ if (m_phase != Phase::Ready) {
+ result = Kernel::ResultInvalidState;
+ } else if (ctx.GetCommand() == 401) {
+ m_gpu_context_initialized = true;
+ result = ResultSuccess;
+ } else if (m_gpu_context_initialized) {
+ m_gpu_context_ready = true;
+ result = ResultSuccess;
+ } else {
+ result = Kernel::ResultInvalidState;
+ }
+ }
+ LOG_DEBUG(Service_AM, "GPU context command={}, result={:#x}", ctx.GetCommand(), result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::InitializeFrameContext(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 16 + ctx.GetCommand() && packet[1] == 0) {
+ if (m_phase != Phase::Ready) {
+ result = Kernel::ResultInvalidState;
+ } else if (ctx.GetCommand() == 1101) {
+ m_frame_context_initialized = true;
+ result = ResultSuccess;
+ } else if (m_frame_context_initialized) {
+ m_frame_context_ready = true;
+ result = ResultSuccess;
+ } else {
+ result = Kernel::ResultInvalidState;
+ }
+ }
+ LOG_DEBUG(Service_AM, "empty frame context command={}, result={:#x}", ctx.GetCommand(), result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::ConfigureNativeMemory(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x83 && packet[1] == 2 && packet[3] == 0 &&
+ (packet[2] == 0 || packet[2] == 2)) {
+ result = m_phase == Phase::Ready ? ResultSuccess : Kernel::ResultInvalidState;
+ }
+ // Clear the optional Gpuk-memory mode: Eden currently provides the NVDRV path.
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(result.raw);
+ rb.Push<u64>(0);
+ LOG_DEBUG(Service_AM, "native memory uses NVDRV-compatible mode, result={:#x}", result.raw);
+}
+
+void IGraphicsSupervisor::InitializeSyncpointContext(HLERequestContext& ctx) {
+ Result result = Kernel::ResultInvalidCombination;
+ const auto* packet = ctx.CommandBuffer();
+ if (packet[0] == 0xd9 && packet[1] == 0) {
+ if (m_phase != Phase::Ready) {
+ result = Kernel::ResultInvalidState;
+ } else {
+ m_syncpoint_context_initialized = true;
+ result = ResultSuccess;
+ }
+ }
+ LOG_DEBUG(Service_AM, "syncpoint context initialization result={:#x}", result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::GetSyncpointSession(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidState;
+ Kernel::KClientSession* client{};
+ if (packet[0] == 0xda && packet[1] == 0 && m_syncpoint_context_initialized && m_nv_module) {
+ result = CreateGraphicsSession(system, ctx,
+ std::make_shared<ISyncpointSupervisor>(system, m_nv_module), &client);
+ }
+ if (result.IsError()) {
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ return;
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess.raw);
+ rb.PushMoveObjects(ctx, client);
+ LOG_DEBUG(Service_AM, "opened syncpoint-supervisor session");
+}
+
+void IGraphicsSupervisor::GetNvidiaDriver(HLERequestContext& ctx) {
+ const auto Fail = [&](Result result) {
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+ };
+ const auto* packet = ctx.CommandBuffer();
+ if (packet[0] != 0x58a || packet[1] != 0) {
+ Fail(Kernel::ResultInvalidCombination);
+ return;
+ }
+ if (m_phase != Phase::Ready) {
+ Fail(Kernel::ResultInvalidState);
+ return;
+ }
+ auto source = system.ServiceManager().GetService<Nvidia::NVDRV>("nvdrv:s", true);
+ if (!source) {
+ Fail(Kernel::ResultNotFound);
+ return;
+ }
+ m_nv_module = source->GetModule();
+ Kernel::KClientSession* client{};
+ const auto result = CreateGraphicsSession(system, ctx,
+ std::make_shared<Nvidia::NVDRV>(system, m_nv_module, "nvdrv:gsv", m_process, m_aruid),
+ &client);
+ if (result.IsError()) {
+ Fail(result);
+ return;
+ }
+ LOG_DEBUG(Service_AM, "opened process-bound NVDRV session for aruid={:#x}", m_aruid);
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess.raw);
+ rb.PushMoveObjects(ctx, client);
+}
+
+void IGraphicsSupervisor::BindSyncpointMemory(HLERequestContext& ctx) {
+ using namespace GraphicsSyncpointCache;
+ const auto Fail = [&](Result result) {
+ IPC::ResponseBuilder rb{ctx, 2, 0, 2};
+ rb.Push(result.raw);
+ ctx.AddMoveObject(nullptr);
+ ctx.AddMoveObject(nullptr);
+ };
+ const auto* packet = ctx.CommandBuffer();
+ if (packet[0] != 0xdc || packet[1] != 0x80000006 || packet[2] != 4 ||
+ packet[5] != 1 || packet[6] != 0x1ff || packet[7] != 0x1000 || packet[8] != 0 ||
+ packet[9] != 0x2000 || packet[10] != 0) {
+ Fail(Kernel::ResultInvalidCombination);
+ return;
+ }
+ if (!m_syncpoint_context_initialized || !m_nv_module || m_syncpoint_rw) {
+ Fail(Kernel::ResultInvalidState);
+ return;
+ }
+ auto rw = ctx.GetObjectFromHandle<Kernel::KTransferMemory>(ctx.GetCopyHandle(0));
+ auto ro = ctx.GetObjectFromHandle<Kernel::KTransferMemory>(ctx.GetCopyHandle(1));
+ if (rw.IsNull() || ro.IsNull() || rw->GetOwner() != m_process || ro->GetOwner() != m_process ||
+ rw.GetPointerUnsafe() == ro.GetPointerUnsafe()) {
+ Fail(Kernel::ResultInvalidHandle);
+ return;
+ }
+ if (rw->GetSize() != 0x1000 || ro->GetSize() != 0x2000) {
+ Fail(Kernel::ResultInvalidSize);
+ return;
+ }
+ auto* request_event = m_context.CreateEvent("SyncpointCacheRequest");
+ if (request_event == nullptr) {
+ Fail(Kernel::ResultOutOfResource);
+ return;
+ }
+ m_syncpoint_request_event = request_event;
+ m_syncpoint_rw = rw.GetPointerUnsafe();
+ m_syncpoint_ro = ro.GetPointerUnsafe();
+ m_syncpoint_rw->Open(kernel);
+ m_syncpoint_ro->Open(kernel);
+ auto& memory = m_process->GetMemory();
+ const auto rw_base = rw->GetSourceAddress();
+ const auto ro_base = ro->GetSourceAddress();
+ const std::array<u8, 0x2000> zeros{};
+ memory.WriteBlock(rw_base, zeros.data(), 0x1000);
+ memory.WriteBlock(ro_base, zeros.data(), 0x2000);
+ memory.WriteBlock(ro_base, Header.data(), sizeof(Header));
+ for (u32 id = 0; id < CounterCount; ++id) {
+ memory.Write64(ro_base + ValuesOffset + id * sizeof(u64), Unallocated);
+ }
+ m_cache_poll = Core::Timing::CreateEvent("GraphicsSyncpointCache",
+ [this](s64, std::chrono::nanoseconds) -> std::optional<std::chrono::nanoseconds> {
+ const auto guard = LockService();
+ this->RefreshSyncpointCache();
+ return std::nullopt;
+ });
+ system.CoreTiming().ScheduleLoopingEvent(std::chrono::milliseconds{1},
+ std::chrono::milliseconds{1}, m_cache_poll);
+ request_event->Open(kernel);
+ auto* completed = m_syncpoint_cache_event.GetHandle();
+ completed->Open(kernel);
+ IPC::ResponseBuilder rb{ctx, 2, 0, 2};
+ rb.Push(ResultSuccess.raw);
+ ctx.AddMoveObject(request_event);
+ ctx.AddMoveObject(completed);
+ LOG_DEBUG(Service_AM, "bound syncpoint cache: {} real counters, rw={:#x}, ro={:#x}",
+ CounterCount, GetInteger(rw_base), GetInteger(ro_base));
+}
+
+void IGraphicsSupervisor::RefreshSyncpointCache() {
+ using namespace GraphicsSyncpointCache;
+ if (!m_syncpoint_rw || !system.IsPoweredOn()) {
+ return;
+ }
+ auto& memory = m_process->GetMemory();
+ const auto rw = m_syncpoint_rw->GetSourceAddress();
+ const auto ro = m_syncpoint_ro->GetSourceAddress();
+ const u64 requests = memory.Read64(rw + GroupRequestOffset);
+ const u64 acknowledgements = memory.Read64(ro + GroupAckOffset);
+ u64 groups = (requests ^ acknowledgements) & ((1ULL << GroupCount) - 1);
+ if (groups == 0) {
+ return;
+ }
+ auto& syncpoints = m_nv_module->GetContainer().GetSyncpointManager();
+ while (groups) {
+ const u32 group = static_cast<u32>(std::countr_zero(groups));
+ groups &= groups - 1;
+ const u64 requested_slots = memory.Read64(rw + SlotRequestOffset + group * sizeof(u64));
+ u64 slots = requested_slots ^ memory.Read64(ro + SlotAckOffset + group * sizeof(u64));
+ while (slots) {
+ const u32 bit = static_cast<u32>(std::countr_zero(slots));
+ slots &= slots - 1;
+ const u32 id = group * 64 + bit;
+ const auto address = ro + ValuesOffset + id * sizeof(u64);
+ const auto value = syncpoints.ReadAllocatedSyncpoint(id);
+ const u64 updated = m_counter_values[id].Observe(value);
+ memory.Write64(address, updated);
+ }
+ std::atomic_thread_fence(std::memory_order_release);
+ memory.Write64(ro + SlotAckOffset + group * sizeof(u64), requested_slots);
+ }
+ std::atomic_thread_fence(std::memory_order_release);
+ memory.Write64(ro + GroupAckOffset, requests);
+ m_syncpoint_request_event->Clear(kernel);
+ m_syncpoint_cache_event.Signal(kernel);
+}
+
+void IGraphicsSupervisor::CalculateImageRequirements(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ std::optional<NativeImageRequirements> requirements;
+ const bool gpu_image = ctx.GetCommand() == 435;
+ if (packet[0] == (gpu_image ? 0x1c3U : 0x5f6U) && packet[1] == 17 &&
+ packet[3] == (gpu_image ? 1U : 2U)) {
+ result = Kernel::ResultInvalidState;
+ if (m_phase == Phase::Ready && m_image_formats_initialized) {
+ requirements = CalculateNativeImageRequirements(std::span{packet + 2, 16}, packet[18]);
+ result = requirements ? ResultSuccess : Kernel::ResultNotImplemented;
+ }
+ }
+ LOG_DEBUG(Service_AM, "native image requirements dimensions={:#x}, block={}, pitch={}, size={:#x}, alignment={:#x}, result={:#x}",
+ packet[4], packet[6], packet[7], requirements ? requirements->size : 0,
+ requirements ? requirements->alignment : 0, result.raw);
+ IPC::ResponseBuilder rb{ctx, gpu_image ? 4U : 6U};
+ rb.Push(result.raw);
+ rb.Push<u64>(requirements ? requirements->size : 0);
+ if (!gpu_image) { rb.Push<u64>(requirements ? requirements->alignment : 0); }
+}
+void IGraphicsSupervisor::CreateNativeMemory(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 size = u64{packet[3]} | (u64{packet[4]} << 32);
+ Result result = Kernel::ResultInvalidCombination;
+ u32 id{};
+ if (packet[0] == 0x7f && packet[1] == 3 && (packet[2] & 0xff) == 3) {
+ result = Kernel::ResultInvalidState;
+ if (m_phase == Phase::Ready && m_image_formats_initialized && m_nv_module) {
+ result = Kernel::ResultInvalidSize;
+ if (size && size <= (1ULL << 32) && Common::IsAligned(size, Kernel::PageSize)) {
+ result = Kernel::ResultOutOfResource;
+ if (m_native_memories.size() < 1024) {
+ auto& nvmap = m_nv_module->GetContainer().GetNvMapFile();
+ std::shared_ptr<Nvidia::NvCore::NvMap::Handle> handle;
+ if (nvmap.CreateHandle(size, handle) == Nvidia::NvResult::Success) {
+ id = handle->id;
+ m_native_memories.push_back(id);
+ result = ResultSuccess;
+ }
+ }
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "native memory create size={:#x}, id={}, result={:#x}", size, id, result.raw);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result.raw);
+ rb.Push(id);
+}
+
+Result IGraphicsSupervisor::BindNativeMemoryBacking(u32 id, u64 address, u64 size, u64 flags, u64 offset) {
+ R_UNLESS(m_nv_module && std::ranges::find(m_native_memories, id) != m_native_memories.end(),
+ Kernel::ResultNotFound);
+ R_UNLESS((flags == 0 || flags == 2) && offset == 0, Kernel::ResultNotImplemented);
+ auto handle = m_nv_module->GetContainer().GetNvMapFile().GetHandle(id);
+ R_UNLESS(handle && !handle->allocated, Kernel::ResultInvalidState);
+ R_UNLESS(size && size == handle->size && address != 0 && address + size > address,
+ Kernel::ResultInvalidSize);
+ R_UNLESS(Common::IsAligned(address, Kernel::PageSize), Kernel::ResultInvalidAddress);
+ auto& page_table = m_process->GetPageTable();
+ R_UNLESS(page_table.Contains(address, size), Kernel::ResultInvalidCurrentMemory);
+ if (flags == 2) {
+ for (u64 current = address; current < address + size;) {
+ Kernel::KMemoryInfo info{};
+ Kernel::Svc::PageInfo page{};
+ R_TRY(page_table.QueryInfo(&info, &page, current));
+ R_UNLESS(True(info.m_attribute & Kernel::KMemoryAttribute::GpuAccessible),
+ Kernel::ResultInvalidCurrentMemory);
+ const u64 next = info.m_address + info.m_size;
+ R_UNLESS(next > current, Kernel::ResultInvalidCurrentMemory);
+ current = next;
+ }
+ }
+ bool is_io{};
+ R_TRY(page_table.LockForMapDeviceAddressSpace(&is_io, address, size,
+ Kernel::KMemoryPermission::None, true, true));
+ if (is_io) {
+ page_table.UnlockForDeviceAddressSpace(address, size);
+ R_RETURN(Kernel::ResultNotImplemented);
+ }
+ if (!m_native_memory_session) {
+ m_native_memory_session = m_nv_module->GetContainer().OpenSession(m_process).id;
+ }
+ const auto result = handle->Alloc({}, Kernel::PageSize, 0, address,
+ Nvidia::NvCore::SessionId{*m_native_memory_session});
+ if (result != Nvidia::NvResult::Success) {
+ page_table.UnlockForDeviceAddressSpace(address, size);
+ R_RETURN(Kernel::ResultOutOfResource);
+ }
+ R_SUCCEED();
+}
+
+void IGraphicsSupervisor::BindNativeMemory(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const auto pair = [&](size_t i) { return u64{packet[i]} | (u64{packet[i + 1]} << 32); };
+ const auto result = packet[0] == 0x82 && packet[1] == 9
+ ? BindNativeMemoryBacking(packet[2], pair(3), pair(5), pair(7), pair(9))
+ : Kernel::ResultInvalidCombination;
+ LOG_DEBUG(Service_AM, "native memory bind id={}, address={:#x}, size={:#x}, flags={:#x}, result={:#x}",
+ packet[2], pair(3), pair(5), pair(7), result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+Result IGraphicsSupervisor::ReleaseNativeMemory(u32 id) {
+ const auto it = std::ranges::find(m_native_memories, id);
+ R_UNLESS(m_nv_module && it != m_native_memories.end(), Kernel::ResultNotFound);
+ auto& nvmap = m_nv_module->GetContainer().GetNvMapFile();
+ auto handle = nvmap.GetHandle(id);
+ R_UNLESS(handle, Kernel::ResultNotFound);
+ {
+ const std::scoped_lock lock{handle->mutex};
+ R_UNLESS(handle->pins == 0 && handle->internal_dupes == 0 && handle->dupes == 1,
+ Kernel::ResultBusy);
+ }
+ const bool allocated = handle->allocated;
+ handle.reset();
+ const auto info = nvmap.FreeHandle(id, false);
+ m_native_memories.erase(it);
+ if (allocated) {
+ R_UNLESS(info && info->can_unlock, Kernel::ResultInvalidState);
+ R_TRY(m_process->GetPageTable().UnlockForDeviceAddressSpace(info->address, info->size));
+ }
+ R_SUCCEED();
+}
+
+void IGraphicsSupervisor::DestroyNativeMemory(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const auto result = packet[0] == 0x80 && packet[1] == 1
+ ? ReleaseNativeMemory(packet[2]) : Kernel::ResultInvalidCombination;
+ LOG_DEBUG(Service_AM, "native memory destroy id={}, result={:#x}", packet[2], result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::BindNativeGpuMemory(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const auto pair = [&](size_t i) { return u64{packet[i]} | (u64{packet[i + 1]} << 32); };
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x1b7 && packet[1] == 12 && pair(10) == 0 && pair(12) == 0) {
+ result = Kernel::ResultNotFound;
+ if (m_nv_module) {
+ auto& container = m_nv_module->GetContainer();
+ const auto parent = container.GetNvMapFile().GetHandle(packet[7]);
+ if (parent && parent->allocated) {
+ const auto* session = container.GetSession(parent->session_id);
+ const u64 offset = pair(8);
+ const u64 address = pair(3);
+ const u64 size = pair(5);
+ if (session && session->process == m_process && offset <= parent->size &&
+ size <= parent->size - offset && parent->address + offset >= parent->address &&
+ address == parent->address + offset) {
+ result = BindNativeMemoryBacking(packet[2], address, size, 2, 0);
+ }
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "native GPU memory bind id={}, parent={}, address={:#x}, size={:#x}, offset={:#x}, result={:#x}",
+ packet[2], packet[7], pair(3), pair(5), pair(8), result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::CreateNativeImage(HLERequestContext& ctx) {
+ const std::scoped_lock images_lock{m_image_registry->mutex};
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ u32 id{};
+ if (packet[0] == 0x1bd && packet[1] == 18 && packet[3] == 1) {
+ result = Kernel::ResultInvalidState;
+ if (m_phase == Phase::Ready && m_gpu_context_ready && m_image_formats_initialized && m_nv_module) {
+ const auto requirements = CalculateNativeImageRequirements(std::span{packet + 2, 16}, packet[18]);
+ result = Kernel::ResultNotImplemented;
+ if (requirements) {
+ result = Kernel::ResultNotFound;
+ auto& nvmap = m_nv_module->GetContainer().GetNvMapFile();
+ const auto memory = nvmap.GetHandle(packet[19]);
+ if (std::ranges::find(m_native_memories, packet[19]) != m_native_memories.end() &&
+ memory && memory->allocated) {
+ result = Kernel::ResultInvalidSize;
+ if (memory->size >= requirements->size) {
+ result = Kernel::ResultOutOfResource;
+ if (m_image_registry->images.size() < 1024 && m_next_native_image_id != 0 &&
+ memory->Duplicate(true) == Nvidia::NvResult::Success) {
+ NativeImage image{m_next_native_image_id++, packet[19], {}, requirements->size};
+ std::copy_n(packet + 2, image.preference.size(), image.preference.begin());
+ id = image.id;
+ if (!m_image_registry->module) {
+ m_image_registry->module = m_nv_module;
+ m_image_registry->memory_session = m_nv_module->GetContainer().OpenSession(m_process);
+ }
+ m_image_registry->images.push_back(image);
+ result = ResultSuccess;
+ }
+ }
+ }
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "native image create memory={}, count={}, id={}, result={:#x}",
+ packet[19], packet[18], id, result.raw);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result.raw);
+ rb.Push(id);
+}
+
+void IGraphicsSupervisor::DestroyNativeImage(HLERequestContext& ctx) {
+ const std::scoped_lock images_lock{m_image_registry->mutex};
+ const auto* packet = ctx.CommandBuffer();
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == 0x1be && packet[1] == 1) {
+ const auto image = std::ranges::find(m_image_registry->images, packet[2], &NativeImage::id);
+ result = Kernel::ResultNotFound;
+ if (m_nv_module && image != m_image_registry->images.end()) {
+ result = Kernel::ResultBusy;
+ if (!image->window_pool) {
+ m_image_registry->ReleaseMemory(image->memory_id);
+ m_image_registry->images.erase(image);
+ result = ResultSuccess;
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "native image destroy id={}, result={:#x}", packet[2], result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+void IGraphicsSupervisor::ConfigureNativeWindowImagePool(HLERequestContext& ctx) {
+ const std::scoped_lock images_lock{m_image_registry->mutex};
+ const auto* packet = ctx.CommandBuffer();
+ const bool create = ctx.GetCommand() == 315;
+ Result result = Kernel::ResultInvalidCombination;
+ if (packet[0] == (create ? 0x14bU : 0x14aU) && packet[1] == (create ? 2U : 1U)) {
+ const auto image = std::ranges::find(m_image_registry->images, packet[2], &NativeImage::id);
+ result = Kernel::ResultNotFound;
+ if (image != m_image_registry->images.end()) {
+ result = Kernel::ResultNotImplemented;
+ if (!create || packet[3] == 1) {
+ result = Kernel::ResultInvalidState;
+ if (image->window_pool != create) {
+ image->window_pool = create;
+ result = ResultSuccess;
+ }
+ }
+ }
+ }
+ LOG_DEBUG(Service_AM, "native window image pool id={}, create={}, result={:#x}",
+ packet[2], create, result.raw);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result.raw);
+}
+
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/graphics_supervisor.h b/src/core/hle/service/am/service/graphics_supervisor.h
new file mode 100644
index 0000000000..4163c2c150
--- /dev/null
+++ b/src/core/hle/service/am/service/graphics_supervisor.h
@@ -0,0 +1,93 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <vector>
+
+#include "core/hle/service/service.h"
+#include "core/hle/service/am/service/graphics_syncpoint_cache.h"
+#include "core/hle/service/kernel_helpers.h"
+#include "core/hle/service/os/event.h"
+
+namespace Kernel {
+class KProcess;
+class KTransferMemory;
+class KEvent;
+}
+
+namespace Core::Timing {
+struct EventType;
+}
+namespace Service::Nvidia {
+class Module;
+}
+
+namespace Service::AM {
+struct NativeImageRegistry;
+
+class IGpuSession;
+
+class IGraphicsSupervisor final : public ServiceFramework<IGraphicsSupervisor> {
+public:
+ explicit IGraphicsSupervisor(Core::System& system, Kernel::KProcess* process, u64 aruid);
+ ~IGraphicsSupervisor() override;
+ Result HandleSyncRequest(Kernel::KServerSession& session, HLERequestContext& ctx) override;
+
+private:
+ enum class Phase { Created, Initialized, ProcessBound, Ready };
+ void Initialize(HLERequestContext& ctx);
+ void BindProcess(HLERequestContext& ctx);
+ void CompleteBootstrap(HLERequestContext& ctx);
+ void InitializeImageContext(HLERequestContext& ctx);
+ void GetNvidiaDriver(HLERequestContext& ctx);
+ void CalculateImageRequirements(HLERequestContext& ctx);
+ void CreateNativeMemory(HLERequestContext& ctx);
+ void CreateNativeImage(HLERequestContext& ctx);
+ void DestroyNativeImage(HLERequestContext& ctx);
+ void ConfigureNativeWindowImagePool(HLERequestContext& ctx);
+ void BindNativeMemory(HLERequestContext& ctx);
+ void BindNativeGpuMemory(HLERequestContext& ctx);
+ void DestroyNativeMemory(HLERequestContext& ctx);
+ Result BindNativeMemoryBacking(u32 id, u64 address, u64 size, u64 flags, u64 offset);
+ Result ReleaseNativeMemory(u32 id);
+ void ConfigureNativeMemory(HLERequestContext& ctx);
+ void InitializeFrameContext(HLERequestContext& ctx);
+ void RegisterGpuAddressSpace(HLERequestContext& ctx);
+ void OpenGpuSession(HLERequestContext& ctx);
+ void OpenDisplaySession(HLERequestContext& ctx);
+ void InitializeDisplayContext(HLERequestContext& ctx);
+ void InitializeGpuContext(HLERequestContext& ctx);
+ void InitializeSyncpointContext(HLERequestContext& ctx);
+ void GetSyncpointSession(HLERequestContext& ctx);
+ void BindSyncpointMemory(HLERequestContext& ctx);
+ void RefreshSyncpointCache();
+
+ std::vector<std::weak_ptr<IGpuSession>> m_gpu_sessions;
+ std::shared_ptr<Nvidia::Module> m_nv_module;
+ std::optional<size_t> m_native_memory_session;
+ std::vector<u32> m_native_memories;
+ std::shared_ptr<NativeImageRegistry> m_image_registry;
+ u32 m_next_native_image_id{1};
+ std::shared_ptr<Core::Timing::EventType> m_cache_poll;
+ std::array<GraphicsSyncpointCache::CounterValue, GraphicsSyncpointCache::CounterCount>
+ m_counter_values{};
+ Kernel::KTransferMemory* m_syncpoint_rw{};
+ Kernel::KTransferMemory* m_syncpoint_ro{};
+ Kernel::KEvent* m_syncpoint_request_event{};
+ KernelHelpers::ServiceContext m_context;
+ Event m_syncpoint_cache_event;
+ bool m_syncpoint_context_initialized{};
+ Kernel::KProcess* const m_process;
+ const u64 m_aruid;
+ Phase m_phase{Phase::Created};
+ bool m_display_context_initialized{};
+ bool m_gpu_context_initialized{};
+ bool m_gpu_context_ready{};
+ bool m_frame_context_initialized{};
+ bool m_frame_context_ready{};
+ bool m_image_context_initialized{};
+ bool m_image_formats_initialized{};
+};
+
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/graphics_supervisor_protocol.h b/src/core/hle/service/am/service/graphics_supervisor_protocol.h
new file mode 100644
index 0000000000..7762ced531
--- /dev/null
+++ b/src/core/hle/service/am/service/graphics_supervisor_protocol.h
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <array>
+#include <optional>
+#include <span>
+#include "common/common_types.h"
+
+namespace Service::AM::GraphicsSupervisorProtocol {
+
+constexpr u8 DecodeBufferSlot(u32 packed) {
+ return static_cast<u8>(packed);
+}
+
+struct BootstrapRequest {
+ u32 process_handle;
+ u64 applet_resource_user_id;
+ u64 flags;
+ std::array<u64, 4> parameters;
+};
+
+inline std::optional<BootstrapRequest> DecodeBootstrap(std::span<const u32> packet) {
+ // Captured remote-memory bootstrap: TIPC 4, one copied handle, 48 data bytes.
+ if (packet.size() < 16 || packet[0] != 0x14 || packet[1] != 0x8000000c ||
+ packet[2] != 2) {
+ return std::nullopt;
+ }
+ const auto Read64 = [&](size_t index) {
+ return u64{packet[index]} | (u64{packet[index + 1]} << 32);
+ };
+ return BootstrapRequest{packet[3], Read64(4), Read64(6),
+ {Read64(8), Read64(10), Read64(12), Read64(14)}};
+}
+
+} // namespace Service::AM::GraphicsSupervisorProtocol
diff --git a/src/core/hle/service/am/service/graphics_syncpoint_cache.h b/src/core/hle/service/am/service/graphics_syncpoint_cache.h
new file mode 100644
index 0000000000..23361b006c
--- /dev/null
+++ b/src/core/hle/service/am/service/graphics_syncpoint_cache.h
@@ -0,0 +1,49 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <array>
+#include <limits>
+#include <optional>
+#include "common/common_types.h"
+
+namespace Service::AM::GraphicsSyncpointCache {
+constexpr u32 ValuesOffset = 0x40;
+constexpr u32 GroupAckOffset = 0x640;
+constexpr u32 SlotAckOffset = 0x648;
+constexpr u32 GroupRequestOffset = 0;
+constexpr u32 SlotRequestOffset = 8;
+constexpr u32 CounterCount = 192;
+constexpr u32 GroupCount = (CounterCount + 63) / 64;
+constexpr u64 Unallocated = (std::numeric_limits<u64>::max)();
+
+constexpr std::array<u32, 8> Header{
+ 1, 0, CounterCount - 1, ValuesOffset, GroupRequestOffset,
+ GroupAckOffset, SlotRequestOffset, SlotAckOffset};
+static_assert(ValuesOffset + CounterCount * sizeof(u64) <= GroupAckOffset);
+static_assert(SlotAckOffset + GroupCount * sizeof(u64) <= 0x2000);
+static_assert(SlotRequestOffset + GroupCount * sizeof(u64) <= 0x1000);
+
+constexpr u64 ExtendCounter(u64 previous, u32 current) {
+ if (previous == Unallocated) {
+ return current;
+ }
+ u64 high = previous & 0xffffffff00000000ULL;
+ if (current < static_cast<u32>(previous)) {
+ high += 1ULL << 32;
+ }
+ return high | current;
+}
+struct CounterValue {
+ u64 previous{Unallocated};
+
+ u64 Observe(std::optional<u32> current) {
+ if (!current) {
+ return Unallocated;
+ }
+ previous = ExtendCounter(previous, *current);
+ return previous;
+ }
+};
+} // namespace Service::AM::GraphicsSyncpointCache
diff --git a/src/core/hle/service/am/service/native_image_layout.h b/src/core/hle/service/am/service/native_image_layout.h
new file mode 100644
index 0000000000..eb45fc470d
--- /dev/null
+++ b/src/core/hle/service/am/service/native_image_layout.h
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <optional>
+#include <span>
+#include "common/common_types.h"
+
+namespace Service::AM {
+
+struct NativeImageRequirements {
+ u64 size;
+ u64 alignment;
+};
+
+// Experimental inter-module block image layout, restricted to the observed
+// four-byte color format. Row pitch is in 64-byte GOBs; block height is in pixels.
+inline std::optional<NativeImageRequirements> CalculateNativeImageRequirements(
+ std::span<const u32> preference, u32 count) {
+ if (preference.size() != 16 || count != 1 || preference[0] != 0 ||
+ (preference[1] != 1 && preference[1] != 2) || preference[3] != 0 || preference[6] != 1 ||
+ preference[7] != 1 || preference[4] < 3 || preference[4] > 8) {
+ return std::nullopt;
+ }
+ for (size_t i = 8; i < preference.size(); ++i) {
+ if (preference[i] != 0) { return std::nullopt; }
+ }
+ const u64 width = (preference[2] & 0xffffU) + 1;
+ const u64 height = (preference[2] >> 16) + 1;
+ const u64 pitch_gobs = preference[5];
+ if (pitch_gobs < (width * 4 + 63) / 64 || pitch_gobs > 4096) {
+ return std::nullopt;
+ }
+ const u64 block_height = 1ULL << preference[4];
+ const u64 aligned_height = (height + block_height - 1) & ~(block_height - 1);
+ return NativeImageRequirements{pitch_gobs * 64 * aligned_height, 64 * block_height};
+}
+
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/performance_functions.cpp b/src/core/hle/service/am/service/performance_functions.cpp
new file mode 100644
index 0000000000..71a46cc5b5
--- /dev/null
+++ b/src/core/hle/service/am/service/performance_functions.cpp
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "core/hle/service/am/service/performance_functions.h"
+
+namespace Service::AM {
+
+IPerformanceFunctions::IPerformanceFunctions(Core::System& system)
+ : ServiceFramework{system, "IPerformanceFunctions"} {
+ static const FunctionInfo functions[] = {
+ {0, nullptr, "IsCpuOverclockEnabled"},
+ {1, nullptr, "SetCpuOverclockEnabled"},
+ };
+ RegisterHandlers(functions);
+}
+
+IPerformanceFunctions::~IPerformanceFunctions() = default;
+
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/performance_functions.h b/src/core/hle/service/am/service/performance_functions.h
new file mode 100644
index 0000000000..12195d1217
--- /dev/null
+++ b/src/core/hle/service/am/service/performance_functions.h
@@ -0,0 +1,16 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "core/hle/service/service.h"
+
+namespace Service::AM {
+
+class IPerformanceFunctions final : public ServiceFramework<IPerformanceFunctions> {
+public:
+ explicit IPerformanceFunctions(Core::System& system);
+ ~IPerformanceFunctions() override;
+};
+
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/self_controller.cpp b/src/core/hle/service/am/service/self_controller.cpp
index 3a654b9daa..45b9120093 100644
--- a/src/core/hle/service/am/service/self_controller.cpp
+++ b/src/core/hle/service/am/service/self_controller.cpp
@@ -5,10 +5,17 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging.h"
+#include "core/hle/kernel/k_scoped_resource_reservation.h"
+#include "core/hle/kernel/k_session.h"
+#include "core/hle/kernel/k_client_session.h"
+#include "core/hle/kernel/k_thread.h"
#include "core/hle/result.h"
#include "core/hle/service/am/am_results.h"
#include "core/hle/service/am/frontend/applets.h"
#include "core/hle/service/am/service/self_controller.h"
+#include "core/hle/service/am/service/graphics_supervisor.h"
+#include "core/hle/service/ipc_helpers.h"
+#include "core/hle/service/server_manager.h"
#include "core/hle/service/caps/caps_su.h"
#include "core/hle/service/cmif_serialization.h"
#include "core/hle/service/nvnflinger/nvnflinger.h"
@@ -73,6 +80,7 @@ ISelfController::ISelfController(Core::System& system_, std::shared_ptr<Applet>
{230, D<&ISelfController::Unknown230>, "Unknown230"},
{240, D<&ISelfController::Unknown240>, "Unknown240"},
{1000, nullptr, "GetDebugStorageChannel"},
+ {2000, &ISelfController::GetGraphicsSupervisor, "GetGraphicsSupervisor"},
};
// clang-format on
@@ -84,10 +92,47 @@ ISelfController::ISelfController(Core::System& system_, std::shared_ptr<Applet>
}
ISelfController::~ISelfController() {
+ if (m_graphics_supervisor_session != nullptr) {
+ m_graphics_supervisor_session->Close(kernel);
+ }
std::scoped_lock lk{m_applet->lock};
m_applet->display_layer_manager.Finalize();
}
+void ISelfController::GetGraphicsSupervisor(HLERequestContext& ctx) {
+ if (m_graphics_supervisor_session != nullptr) {
+ IPC::ResponseBuilder rb{ctx, 2, 1};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, m_graphics_supervisor_session);
+ return;
+ }
+ Kernel::KScopedResourceReservation reservation(
+ kernel, Kernel::GetCurrentProcessPointer(kernel), Kernel::LimitableResource::SessionCountMax);
+ if (!reservation.Succeeded()) {
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(Kernel::ResultLimitReached);
+ return;
+ }
+ auto* session = Kernel::KSession::Create(kernel);
+ if (session == nullptr) {
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(Kernel::ResultOutOfResource);
+ return;
+ }
+ session->Initialize(kernel, nullptr, 0);
+ reservation.Commit();
+ Kernel::KSession::Register(kernel, session);
+ auto& server = ctx.GetManager()->GetServerManager();
+ auto manager = std::make_shared<SessionRequestManager>(kernel, server);
+ manager->SetSessionHandler(std::make_shared<IGraphicsSupervisor>(system, m_process, m_applet->aruid.pid));
+ server.RegisterSession(&session->GetServerSession(), std::move(manager));
+ LOG_DEBUG(Service_AM, "created graphics-supervisor session");
+ m_graphics_supervisor_session = &session->GetClientSession();
+ IPC::ResponseBuilder rb{ctx, 2, 1};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, m_graphics_supervisor_session);
+}
+
Result ISelfController::Exit() {
LOG_DEBUG(Service_AM, "called");
diff --git a/src/core/hle/service/am/service/self_controller.h b/src/core/hle/service/am/service/self_controller.h
index c9f480f575..52970ed4a8 100644
--- a/src/core/hle/service/am/service/self_controller.h
+++ b/src/core/hle/service/am/service/self_controller.h
@@ -12,6 +12,7 @@
namespace Kernel {
class KReadableEvent;
+class KClientSession;
}
namespace Service::Capture {
@@ -30,6 +31,7 @@ public:
~ISelfController() override;
private:
+ void GetGraphicsSupervisor(HLERequestContext& ctx);
Result Exit();
Result LockExit();
Result UnlockExit();
@@ -70,6 +72,7 @@ private:
Result Unknown230(u32 in_val, Out<u16> out_val);
Result Unknown240(u32 in_val);
+ Kernel::KClientSession* m_graphics_supervisor_session{};
Kernel::KProcess* const m_process;
const std::shared_ptr<Applet> m_applet;
};
diff --git a/src/core/hle/service/audio/audio.cpp b/src/core/hle/service/audio/audio.cpp
index fde766a275..63d578462f 100644
--- a/src/core/hle/service/audio/audio.cpp
+++ b/src/core/hle/service/audio/audio.cpp
@@ -5,6 +5,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/core.h"
+#include "core/hle/kernel/svc_results.h"
+#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/audio/audio.h"
#include "core/hle/service/audio/audio_controller.h"
#include "core/hle/service/audio/audio_in_manager.h"
@@ -18,6 +20,57 @@
namespace Service::Audio {
+class INativeAudioManager final : public ServiceFramework<INativeAudioManager> {
+public:
+ INativeAudioManager(Core::System& system_, std::shared_ptr<IAudioInManager> input,
+ std::shared_ptr<IAudioOutManager> output,
+ std::shared_ptr<IAudioRendererManager> renderer)
+ : ServiceFramework{system_, "aud:u"}, m_input{std::move(input)},
+ m_output{std::move(output)}, m_renderer{std::move(renderer)} {
+ static const FunctionInfo functions[] = {
+ {0, &INativeAudioManager::GetAudioInManager, "GetAudioInManager"},
+ {1, &INativeAudioManager::GetAudioOutManager, "GetAudioOutManager"},
+ {2, &INativeAudioManager::GetAudioRendererManager, "GetAudioRendererManager"},
+ };
+ RegisterHandlers(functions);
+ }
+
+ Result HandleSyncRequest(Kernel::KServerSession& session, HLERequestContext& ctx) override {
+ if (!ctx.IsTipc()) {
+ return ServiceFrameworkBase::HandleSyncRequest(session, ctx);
+ }
+ const auto guard = LockService();
+ LOG_WARNING(Service_Audio, "Native audio unsupported command={}, words={:08x}", ctx.GetCommand(),
+ fmt::join(std::span<const u32>(ctx.CommandBuffer() + 2,
+ std::min<u32>(ctx.CommandBuffer()[1], 32)), ","));
+ InvokeRequestTipc(ctx);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(Kernel::ResultNotImplemented.raw);
+ return system.IsPoweredOn() ? ctx.WriteToOutgoingCommandBuffer() : ResultSuccess;
+ }
+
+private:
+ void GetAudioInManager(HLERequestContext& ctx) {
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface(ctx, m_input);
+ }
+ void GetAudioOutManager(HLERequestContext& ctx) {
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface(ctx, m_output);
+ }
+ void GetAudioRendererManager(HLERequestContext& ctx) {
+ LOG_DEBUG(Service_Audio, "Native user client opens existing audio renderer manager");
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface(ctx, m_renderer);
+ }
+ std::shared_ptr<IAudioInManager> m_input;
+ std::shared_ptr<IAudioOutManager> m_output;
+ std::shared_ptr<IAudioRendererManager> m_renderer;
+};
+
class IAudioOutManagerForApplet final : public ServiceFramework<IAudioOutManagerForApplet> {
public:
explicit IAudioOutManagerForApplet(Core::System& system_)
@@ -193,18 +246,23 @@ void LoopProcess(Core::System& system) {
server_manager->RegisterNamedService("audrec:d", std::make_shared<IFinalOutputRecorderManagerForDebugger>(system), 30);
server_manager->RegisterNamedService("audren:d", std::make_shared<IAudioRendererManagerForDebugger>(system), 30);
- server_manager->RegisterNamedService("audin:u", std::make_shared<IAudioInManager>(system), 30);
+ auto audio_input = std::make_shared<IAudioInManager>(system);
+ server_manager->RegisterNamedService("audin:u", audio_input, 30);
server_manager->RegisterNamedService("audin:a", std::make_shared<IAudioInManagerForApplet>(system), 30);
- server_manager->RegisterNamedService("audout:u", std::make_shared<IAudioOutManager>(system), 30);
+ auto audio_output = std::make_shared<IAudioOutManager>(system);
+ server_manager->RegisterNamedService("audout:u", audio_output, 30);
server_manager->RegisterNamedService("audout:a", std::make_shared<IAudioOutManagerForApplet>(system), 30);
server_manager->RegisterNamedService("auddev", std::make_shared<IAudioSnoopManager>(system), 30);
// Depends on audout:u and audin:u on ctor!
server_manager->RegisterNamedService("audctl", std::make_shared<IAudioController>(system), 30);
server_manager->RegisterNamedService("audrec:a", std::make_shared<IFinalOutputRecorderManagerForApplet>(system), 30);
server_manager->RegisterNamedService("audrec:u", std::make_shared<IFinalOutputRecorderManager>(system), 30);
- server_manager->RegisterNamedService("audren:u", std::make_shared<IAudioRendererManager>(system), 30);
+ auto audio_renderer = std::make_shared<IAudioRendererManager>(system);
+ server_manager->RegisterNamedService("audren:u", audio_renderer, 30);
server_manager->RegisterNamedService("audren:a", std::make_shared<IAudioRendererManagerForApplet>(system), 30);
server_manager->RegisterNamedService("hwopus", std::make_shared<IHardwareOpusDecoderManager>(system), 25);
+ server_manager->RegisterNamedService("aud:u",
+ std::make_shared<INativeAudioManager>(system, audio_input, audio_output, audio_renderer), 30);
ServerManager::RunServer(std::move(server_manager));
}
diff --git a/src/core/hle/service/hid/hid_server.cpp b/src/core/hle/service/hid/hid_server.cpp
index 9689bce8dd..7ddb944abe 100644
--- a/src/core/hle/service/hid/hid_server.cpp
+++ b/src/core/hle/service/hid/hid_server.cpp
@@ -132,6 +132,7 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{210, C<&IHidServer::EndPermitVibrationSession>, "EndPermitVibrationSession"},
{211, C<&IHidServer::IsVibrationDeviceMounted>, "IsVibrationDeviceMounted"},
{212, C<&IHidServer::SendVibrationValueInBool>, "SendVibrationValueInBool"},
+ {214, C<&IHidServer::SendNativeVibrationValues>, "SendNativeVibrationValues"},
{300, C<&IHidServer::ActivateConsoleSixAxisSensor>, "ActivateConsoleSixAxisSensor"},
{301, C<&IHidServer::StartConsoleSixAxisSensor>, "StartConsoleSixAxisSensor"},
{302, C<&IHidServer::StopConsoleSixAxisSensor>, "StopConsoleSixAxisSensor"},
@@ -237,6 +238,11 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{3013, nullptr, "SetDebugPadGenericPadMap"}, //21.0.0+
{3014, nullptr, "GetDebugPadKeyboardMap"}, //21.0.0+
{3015, nullptr, "SetDebugPadKeyboardMap"}, //21.0.0+
+ {3110, C<&IHidServer::RegisterJcmsMemory>, "RegisterJcmsMemory"},
+ {3136, C<&IHidServer::SetNmouseButtonConfigJoyLeftJcms>, "SetNmouseButtonConfigJoyLeftJcms"},
+ {3137, C<&IHidServer::SetNmouseButtonConfigJoyRightJcms>, "SetNmouseButtonConfigJoyRightJcms"},
+ {3138, C<&IHidServer::SetMergedNmouseSourceTypes>, "SetMergedNmouseSourceTypes"},
+ {3139, C<&IHidServer::RegisterNativeInputMemory>, "RegisterNativeInputMemory"},
{3150, C<&IHidServer::SetMouseLibraryVersion>, "SetMouseLibraryVersion"}, //21.0.0+
// What? -- {12010, nullptr, "SetButtonConfigLeft"},
};
@@ -245,7 +251,11 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
RegisterHandlers(functions);
}
-IHidServer::~IHidServer() = default;
+IHidServer::~IHidServer() {
+ for (const auto& registration : native_input_memories) {
+ registration.memory->Close(kernel);
+ }
+}
Result IHidServer::CreateAppletResource(OutInterface<IAppletResource> out_applet_resource,
ClientAppletResourceUserId aruid) {
@@ -984,6 +994,17 @@ Result IHidServer::SendVibrationValues(
R_SUCCEED();
}
+Result IHidServer::SendNativeVibrationValues(
+ u16 flags, AppletResourceUserId aruid,
+ InArray<Core::HID::VibrationDeviceHandle, BufferAttr_HipcPointer> vibration_handles,
+ InArray<Core::HID::VibrationValue, BufferAttr_HipcPointer> vibration_values) {
+ static u32 diagnostic_batches{};
+ if (diagnostic_batches++ < 8) LOG_INFO(Service_HID, "Native vibration batch flags={:#x}, aruid={}, handles={}, values={}",
+ flags, aruid.pid, vibration_handles.size(), vibration_values.size());
+ R_UNLESS(flags == 0 || flags == 2, IPC::ResultNotSupported);
+ R_RETURN(SendVibrationValues(ClientAppletResourceUserId{aruid.pid}, vibration_handles, vibration_values));
+}
+
Result IHidServer::SendVibrationGcErmCommand(
Core::HID::VibrationDeviceHandle vibration_device_handle, ClientAppletResourceUserId aruid,
Core::HID::VibrationGcErmCommand gc_erm_command) {
@@ -1471,6 +1492,63 @@ Result IHidServer::SetTouchScreenResolution(u32 width, u32 height,
R_SUCCEED();
}
+Result IHidServer::RegisterNativeInputMemory(
+ u8 mode, u32 parameter0, u32 parameter1, u32 parameter2,
+ ClientAppletResourceUserId aruid, u64 size,
+ InCopyHandle<Kernel::KTransferMemory> memory) {
+ LOG_INFO(Service_HID, "Native input memory mode={}, parameters={:#x}/{:#x}/{:#x}, aruid={}, size={:#x}",
+ mode, parameter0, parameter1, parameter2, aruid.pid, size);
+ R_UNLESS(memory.Get() && memory->GetOwner()->UsesPAuthReturns() &&
+ memory->GetOwner()->GetProcessId() == aruid.pid, Kernel::ResultInvalidHandle);
+ R_UNLESS(mode <= 1 && size && size == memory->GetSize() && size <= 0x1000000,
+ Kernel::ResultInvalidSize);
+ R_UNLESS(native_input_memories.size() < 64, Kernel::ResultOutOfResource);
+ const auto existing = std::ranges::find(native_input_memories, memory.Get(), &NativeInputMemory::memory);
+ R_UNLESS(existing == native_input_memories.end(), Kernel::ResultInvalidState);
+ memory->Open(kernel);
+ native_input_memories.push_back({memory.Get(), aruid.pid, size, {mode, parameter0, parameter1, parameter2}});
+ // Retain the real guest-owned input buffer. No Joy-Con mouse samples are
+ // produced while those devices are disconnected.
+ R_SUCCEED();
+}
+
+Result IHidServer::RegisterJcmsMemory(ClientAppletResourceUserId aruid, u64 size,
+ InCopyHandle<Kernel::KTransferMemory> memory) {
+ LOG_INFO(Service_HID, "Native Joy-Con mouse memory: aruid={}, size={:#x}", aruid.pid, size);
+ return RegisterNativeInputMemory(1, 0, 0, 0, aruid, size, memory);
+}
+
+Result IHidServer::SetMergedNmouseSourceTypes(u32 source_types, ClientAppletResourceUserId aruid) {
+ LOG_INFO(Service_HID, "Native merged mouse sources: types={:#x}, aruid={}", source_types, aruid.pid);
+ R_UNLESS((source_types & ~6U) == 0, Kernel::ResultInvalidCombination);
+ const auto memory = std::ranges::find_if(native_input_memories, [&](const auto& registration) {
+ return registration.aruid == aruid.pid && registration.parameters[0] == 0;
+ });
+ R_UNLESS(memory != native_input_memories.end(), Kernel::ResultNotFound);
+ memory->merged_source_types = source_types;
+ R_SUCCEED();
+}
+
+Result IHidServer::SetNmouseButtonConfiguration(ClientAppletResourceUserId aruid, u64 configuration,
+ size_t side) {
+ LOG_INFO(Service_HID, "Native Joy-Con mouse buttons: side={}, config={:#x}, aruid={}",
+ side, configuration, aruid.pid);
+ const auto memory = std::ranges::find_if(native_input_memories, [&](const auto& registration) {
+ return registration.aruid == aruid.pid && registration.parameters[0] == 0;
+ });
+ R_UNLESS(memory != native_input_memories.end(), Kernel::ResultNotFound);
+ memory->jcms_button_configurations[side] = configuration;
+ R_SUCCEED();
+}
+
+Result IHidServer::SetNmouseButtonConfigJoyLeftJcms(ClientAppletResourceUserId aruid, u64 configuration) {
+ return SetNmouseButtonConfiguration(aruid, configuration, 0);
+}
+
+Result IHidServer::SetNmouseButtonConfigJoyRightJcms(ClientAppletResourceUserId aruid, u64 configuration) {
+ return SetNmouseButtonConfiguration(aruid, configuration, 1);
+}
+
Result IHidServer::SetMouseLibraryVersion(ClientAppletResourceUserId aruid) {
LOG_INFO(Service_HID, "(STUBBED) called, applet_resource_user_id={}", aruid.pid);
diff --git a/src/core/hle/service/hid/hid_server.h b/src/core/hle/service/hid/hid_server.h
index 57a1a04b9c..45097f361d 100644
--- a/src/core/hle/service/hid/hid_server.h
+++ b/src/core/hle/service/hid/hid_server.h
@@ -6,6 +6,8 @@
#pragma once
+#include <array>
+#include <vector>
#include "core/hle/service/cmif_types.h"
#include "core/hle/service/service.h"
#include "hid_core/hid_types.h"
@@ -175,6 +177,10 @@ private:
ClientAppletResourceUserId aruid,
InArray<Core::HID::VibrationDeviceHandle, BufferAttr_HipcPointer> vibration_handles,
InArray<Core::HID::VibrationValue, BufferAttr_HipcPointer> vibration_values);
+ Result SendNativeVibrationValues(
+ u16 flags, AppletResourceUserId aruid,
+ InArray<Core::HID::VibrationDeviceHandle, BufferAttr_HipcPointer> vibration_handles,
+ InArray<Core::HID::VibrationValue, BufferAttr_HipcPointer> vibration_values);
Result SendVibrationGcErmCommand(Core::HID::VibrationDeviceHandle vibration_device_handle,
ClientAppletResourceUserId aruid,
Core::HID::VibrationGcErmCommand gc_erm_command);
@@ -265,6 +271,24 @@ private:
Result SetTouchScreenResolution(u32 width, u32 height, ClientAppletResourceUserId aruid);
Result SetMouseLibraryVersion(ClientAppletResourceUserId aruid);
+ Result RegisterNativeInputMemory(u8 mode, u32 parameter0, u32 parameter1, u32 parameter2,
+ ClientAppletResourceUserId aruid, u64 size,
+ InCopyHandle<Kernel::KTransferMemory> memory);
+ Result RegisterJcmsMemory(ClientAppletResourceUserId aruid, u64 size,
+ InCopyHandle<Kernel::KTransferMemory> memory);
+ Result SetMergedNmouseSourceTypes(u32 source_types, ClientAppletResourceUserId aruid);
+ Result SetNmouseButtonConfigJoyLeftJcms(ClientAppletResourceUserId aruid, u64 configuration);
+ Result SetNmouseButtonConfigJoyRightJcms(ClientAppletResourceUserId aruid, u64 configuration);
+ Result SetNmouseButtonConfiguration(ClientAppletResourceUserId aruid, u64 configuration, size_t side);
+ struct NativeInputMemory {
+ Kernel::KTransferMemory* memory;
+ u64 aruid;
+ u64 size;
+ std::array<u32, 4> parameters;
+ u32 merged_source_types{};
+ std::array<u64, 2> jcms_button_configurations{};
+ };
+ std::vector<NativeInputMemory> native_input_memories;
std::shared_ptr<ResourceManager> resource_manager;
std::shared_ptr<HidFirmwareSettings> firmware_settings;
};
diff --git a/src/core/hle/service/ngc/ngc.cpp b/src/core/hle/service/ngc/ngc.cpp
index e79108a1eb..2c79336d93 100644
--- a/src/core/hle/service/ngc/ngc.cpp
+++ b/src/core/hle/service/ngc/ngc.cpp
@@ -7,6 +7,7 @@
#include "common/string_util.h"
#include "core/core.h"
#include "core/hle/kernel/k_client_session.h"
+#include "core/hle/kernel/k_shared_memory.h"
#include "core/hle/result.h"
#include "core/hle/service/acc/profile_manager.h"
#include "core/hle/service/cmif_types.h"
@@ -182,6 +183,8 @@ public:
explicit IUserShimScopedObject(Core::System& system_) : ServiceFramework(system_, "IUserShimScopedObject") {
// clang-format off
static const FunctionInfo functions[] = {
+ {1, D<&IUserShimScopedObject::GetRequiredMemorySize>, "GetRequiredMemorySize"},
+ {300, D<&IUserShimScopedObject::GetGuestHidSharedMemory>, "GetGuestHidSharedMemory"},
{450, nullptr, "InitializeForSaveData"},
{451, nullptr, "FinalizeForSaveData"},
{452, D<&IUserShimScopedObject::OpenSaveData>, "OpenSaveData"},
@@ -195,6 +198,50 @@ public:
RegisterHandlers(functions);
}
+ ~IUserShimScopedObject() override {
+ if (m_guest_hid_memory) {
+ m_guest_hid_memory->Close(kernel);
+ }
+ }
+
+ struct GuestHidMemoryConfig {
+ u64 size;
+ u64 offset;
+ };
+
+ Result GetRequiredMemorySize(Out<u32> out_size, u8 type, u8 mode, s32 guest_count) {
+ LOG_INFO(Service_NGC, "GameShare work memory query type={}, mode={}, guests={}", type, mode, guest_count);
+ R_UNLESS(type <= 1 && mode == 1 && guest_count >= 0 && guest_count <= 3,
+ IPC::ResultNotSupported);
+ // The disconnected implementation reserves one page for its session
+ // control state. Streaming/encoding work is not implemented.
+ *out_size = 0x1000;
+ R_SUCCEED();
+ }
+
+ Result GetGuestHidSharedMemory(Out<GuestHidMemoryConfig> out_config,
+ OutCopyHandle<Kernel::KSharedMemory> out_memory) {
+ // Three guest pad blocks, touch rings, and JCMS rings. Empty rings and
+ // zero style sets describe no connected GameShare guests.
+ constexpr u64 GuestHidMemorySize = 0x84000;
+ if (!m_guest_hid_memory) {
+ auto* memory = Kernel::KSharedMemory::Create(kernel);
+ const auto result = memory->Initialize(kernel, system.DeviceMemory(), nullptr,
+ Kernel::Svc::MemoryPermission::ReadWrite, Kernel::Svc::MemoryPermission::Read,
+ GuestHidMemorySize);
+ if (R_FAILED(result)) {
+ memory->Close(kernel);
+ R_RETURN(result);
+ }
+ Kernel::KSharedMemory::Register(kernel, memory);
+ m_guest_hid_memory = memory;
+ }
+ *out_config = GuestHidMemoryConfig{GuestHidMemorySize, 0};
+ *out_memory = m_guest_hid_memory;
+ LOG_DEBUG(Service_NGC, "GameShare guest HID memory size={:#x}, no guests connected", GuestHidMemorySize);
+ R_SUCCEED();
+ }
+
Result OpenSaveData(Account::Uid unk0, Out<SaveDataHandle> unk1) {
LOG_WARNING(Service_NGC, "stubbed");
R_THROW(IPC::ResultNotSupported);
@@ -210,6 +257,9 @@ public:
// to implement
R_SUCCEED();
}
+
+private:
+ Kernel::KSharedMemory* m_guest_hid_memory{};
};
class IUserService final : public ServiceFramework<IUserService> {
diff --git a/src/core/hle/service/nvdrv/core/syncpoint_manager.cpp b/src/core/hle/service/nvdrv/core/syncpoint_manager.cpp
index c4c4c25932..8b929a7ef3 100644
--- a/src/core/hle/service/nvdrv/core/syncpoint_manager.cpp
+++ b/src/core/hle/service/nvdrv/core/syncpoint_manager.cpp
@@ -63,6 +63,14 @@ void SyncpointManager::FreeSyncpoint(u32 id) {
syncpoint.reserved = false;
}
+std::optional<u32> SyncpointManager::ReadAllocatedSyncpoint(u32 id) {
+ std::lock_guard lock(reservation_lock);
+ if (id >= SyncpointCount || !syncpoints[id].reserved) {
+ return std::nullopt;
+ }
+ return UpdateMin(id);
+}
+
bool SyncpointManager::IsSyncpointAllocated(u32 id) const {
return (id < SyncpointCount) && syncpoints[id].reserved;
}
diff --git a/src/core/hle/service/nvdrv/core/syncpoint_manager.h b/src/core/hle/service/nvdrv/core/syncpoint_manager.h
index 7728ff5967..42e1d547b8 100644
--- a/src/core/hle/service/nvdrv/core/syncpoint_manager.h
+++ b/src/core/hle/service/nvdrv/core/syncpoint_manager.h
@@ -7,6 +7,7 @@
#include <array>
#include <atomic>
#include <mutex>
+#include <optional>
#include "common/common_types.h"
#include "core/hle/service/nvdrv/nvdata.h"
@@ -38,6 +39,8 @@ enum class ChannelType : u32 {
*/
class SyncpointManager final {
public:
+ std::optional<u32> ReadAllocatedSyncpoint(u32 id);
+
explicit SyncpointManager(Tegra::Host1x::Host1x& host1x);
~SyncpointManager();
diff --git a/src/core/hle/service/nvdrv/devices/gpu_address_space_layout.h b/src/core/hle/service/nvdrv/devices/gpu_address_space_layout.h
new file mode 100644
index 0000000000..a54ebb18d7
--- /dev/null
+++ b/src/core/hle/service/nvdrv/devices/gpu_address_space_layout.h
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "common/common_types.h"
+
+namespace Service::Nvidia::Devices {
+
+struct GpuAddressSpaceLayout {
+ u64 start;
+ u64 split;
+ u64 end;
+};
+
+constexpr GpuAddressSpaceLayout GetGpuAddressSpaceLayout(bool extended) {
+ // Leave room for other 4 KiB allocations beyond the native bootstrap
+ // reservation of 0x7ff8000000 bytes.
+ // Both layouts fit within the GPU's reported 40-bit virtual address width.
+ return {0x8000000, extended ? 3ULL << 38 : 1ULL << 34,
+ extended ? 1ULL << 40 : 1ULL << 37};
+}
+
+constexpr u32 GetGpuAddressSpaceBigPageSize(bool extended, u32 requested) {
+ // The native SDK passes its preferred allocation size here. A preference for
+ // ordinary 4 KiB mappings still needs a separate large-page pool in NVDRV.
+ return extended && requested == 0x1000 ? 0x10000 : requested;
+}
+
+constexpr u64 GpuPageAddress(u32 page, u32 page_bits) {
+ return static_cast<u64>(page) << page_bits;
+}
+
+} // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/native_gpu_characteristics.h b/src/core/hle/service/nvdrv/devices/native_gpu_characteristics.h
new file mode 100644
index 0000000000..44f45cab75
--- /dev/null
+++ b/src/core/hle/service/nvdrv/devices/native_gpu_characteristics.h
@@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <array>
+#include <cstring>
+#include <span>
+#include "common/common_types.h"
+#include "common/swap.h"
+
+namespace Service::Nvidia::Devices {
+
+inline constexpr size_t NativeGpuCharacteristicsSize = 0xd0;
+
+inline std::array<u8, NativeGpuCharacteristicsSize> MakeNativeGpuCharacteristics(
+ std::span<const u8, 0xa0> legacy) {
+ std::array<u8, NativeGpuCharacteristicsSize> result{};
+ // The GPU description and engine classes share the legacy prefix. The native
+ // SDK consumes signed ioctl interface levels where the legacy ABI has topology.
+ std::memcpy(result.data(), legacy.data(), 0x70);
+ // Select the SDK's native control ABI, whose normal mappings support
+ // 4 KiB CPU/GPU aliases. Engine execution remains a separate implementation.
+ const u32_le native_arch{0x170};
+ const u32_le native_impl{0xe};
+ std::memcpy(result.data(), &native_arch, sizeof(native_arch));
+ std::memcpy(result.data() + 4, &native_impl, sizeof(native_impl));
+ // Native NVN's built-in shader table accepts Ampere SM 8.x. Carrying the
+ // inherited Maxwell SM 5.3 value here rejects its initialization shaders.
+ const u32_le native_sm_version{0x807};
+ std::memcpy(result.data() + 0x3c, &native_sm_version, sizeof(native_sm_version));
+ std::memcpy(result.data() + 0x40, &native_sm_version, sizeof(native_sm_version));
+ // Native NVN maps CPU buffers into its shared VA reservation. Those buffers
+ // are 4 KiB aligned, so large pages must remain optional for allocation.
+ const u32_le preferred_page_size{0x1000};
+ std::memcpy(result.data() + 0x28, &preferred_page_size, sizeof(preferred_page_size));
+ u32_le available_page_sizes;
+ std::memcpy(&available_page_sizes, legacy.data() + 0x34, sizeof(available_page_sizes));
+ available_page_sizes = (static_cast<u32>(available_page_sizes) & 0x10000) | 0x1000;
+ std::memcpy(result.data() + 0x34, &available_page_sizes, sizeof(available_page_sizes));
+ u64_le flags;
+ std::memcpy(&flags, legacy.data() + 0x50, sizeof(flags));
+ // Native SDK class enumeration filters 2D/3D/compute/inline families
+ // with bits 40..43 before selecting context identities from its profile.
+ // The native SDK gates ModifyKind (AS ioctl 12) on capability bit 34.
+ flags = static_cast<u64>(flags) | (1ULL << 8) | (1ULL << 22) | (1ULL << 26) | (1ULL << 34) | (0xfULL << 40);
+ std::memcpy(result.data() + 0x50, &flags, sizeof(flags));
+ constexpr std::array<s16, 9> levels{
+ 0x1c, // GPU control: GetGpuTime
+ 11, // TSG: bind/unbind channel with native subcontext IDs
+ -1, // GPU debugger device is unavailable
+ 0x1d, // Channel: SetTimeslice
+ 0x14, // Address space: Remap
+ 0, 0, 0, 0, // Additional interfaces have not been implemented.
+ };
+ for (size_t i = 0; i < levels.size(); ++i) {
+ const u16_le value{static_cast<u16>(levels[i])};
+ std::memcpy(result.data() + 0x70 + i * sizeof(value), &value, sizeof(value));
+ }
+ // The native SDK allocates shared queue subcontexts locally, then binds
+ // each channel with TSG ioctl 11. Each emulated channel retains its own state.
+ const u32_le max_subcontexts{64};
+ std::memcpy(result.data() + 0xb4, &max_subcontexts, sizeof(max_subcontexts));
+ return result;
+}
+
+} // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/native_gpu_context.h b/src/core/hle/service/nvdrv/devices/native_gpu_context.h
new file mode 100644
index 0000000000..7c7131f211
--- /dev/null
+++ b/src/core/hle/service/nvdrv/devices/native_gpu_context.h
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <optional>
+#include "common/common_types.h"
+
+namespace Service::Nvidia::Devices {
+
+// Native profile context identities. This classifies allocation metadata;
+// engine method execution is handled separately by the GPU frontend.
+inline std::optional<u32> GetNativeGpuContextSlot(u32 identity) {
+ switch (identity) {
+ case 0xc797: return 1;
+ case 0xc7c0: return 2;
+ case 0xc7b5: return 4;
+ case 0xc56f: return 5;
+ default: return std::nullopt;
+ }
+}
+
+} // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/native_gpu_kind.h b/src/core/hle/service/nvdrv/devices/native_gpu_kind.h
new file mode 100644
index 0000000000..ecf3781162
--- /dev/null
+++ b/src/core/hle/service/nvdrv/devices/native_gpu_kind.h
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <optional>
+#include "common/common_types.h"
+#include "video_core/pte_kind.h"
+
+namespace Service::Nvidia::Devices {
+
+inline std::optional<u8> TranslateNativeUncompressedKind(u8 kind, u8 fallback) {
+ // NVIDIA's Turing MMU uses 6 for GENERIC_MEMORY and 7 for INVALID:
+ // open-gpu-doc/manuals/turing/tu104/dev_mmu.ref.txt. Eden's older kind
+ // numbering uses 0xfe for generic memory. Equal kinds need no compression.
+ if (kind != fallback) { return std::nullopt; }
+ switch (kind) {
+ case 0: return static_cast<u8>(Tegra::PTEKind::PITCH);
+ case 1: return static_cast<u8>(Tegra::PTEKind::Z16);
+ case 3: return static_cast<u8>(Tegra::PTEKind::S8Z24);
+ case 6: return static_cast<u8>(Tegra::PTEKind::GENERIC_16BX2);
+ default: return std::nullopt;
+ }
+}
+
+struct NativeMemoryKind {
+ u8 backend;
+ u8 selected;
+};
+
+inline std::optional<NativeMemoryKind> SelectNativeMemoryKind(u8 requested, u8 fallback) {
+ if (const auto kind = TranslateNativeUncompressedKind(requested, fallback)) {
+ return NativeMemoryKind{*kind, requested};
+ }
+ // The native caller supplies an uncompressed alternative explicitly. Return
+ // that selected native kind so the SDK invalidates its compressed preference.
+ if (requested == 8 && fallback == 6) {
+ return NativeMemoryKind{static_cast<u8>(Tegra::PTEKind::GENERIC_16BX2), 6};
+ }
+ // TU104 kind 0x0c is the compressible S8Z24 variant; kind 3 is
+ // the caller's uncompressed S8Z24 alternative (dev_mmu.ref.txt).
+ if (requested == 0xb && fallback == 1) {
+ return NativeMemoryKind{static_cast<u8>(Tegra::PTEKind::Z16), 1};
+ }
+ if (requested == 0xc && fallback == 3) {
+ return NativeMemoryKind{static_cast<u8>(Tegra::PTEKind::S8Z24), 3};
+ }
+ return std::nullopt;
+}
+
+} // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvdevice.h b/src/core/hle/service/nvdrv/devices/nvdevice.h
index d23ec63bae..cbcee2a938 100644
--- a/src/core/hle/service/nvdrv/devices/nvdevice.h
+++ b/src/core/hle/service/nvdrv/devices/nvdevice.h
@@ -27,7 +27,9 @@ namespace Service::Nvidia::Devices {
/// implement the ioctl interface.
class nvdevice {
public:
- explicit nvdevice(Core::System& system_) : system{system_} {}
+ enum class Type { Other, GpuChannel, GpuTsg, GpuAddressSpace };
+ explicit nvdevice(Core::System& system_, Type type_ = Type::Other) : system{system_}, type{type_} {}
+ Type GetType() const { return type; }
virtual ~nvdevice() = default;
/**
@@ -80,6 +82,8 @@ public:
protected:
Core::System& system;
+private:
+ const Type type;
};
} // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp
index b8a8979315..0db83215ca 100644
--- a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp
@@ -5,17 +5,24 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <boost/container/small_vector.hpp>
+#include <cstdlib>
+#include <fstream>
+#include <filesystem>
+#include "core/frontend/framebuffer_layout.h"
+#include "video_core/renderer_base.h"
#include "common/adpf.h"
#include "common/assert.h"
#include "common/logging.h"
#include "core/core.h"
+#include "core/hle/kernel/k_process.h"
#include "core/core_timing.h"
#include "core/hle/service/nvdrv/core/container.h"
#include "core/hle/service/nvdrv/core/nvmap.h"
#include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
#include "core/perf_stats.h"
#include "video_core/gpu.h"
+#include "video_core/host1x/host1x.h"
namespace Service::Nvidia::Devices {
@@ -67,6 +74,9 @@ void nvdisp_disp0::Composite(std::span<const Nvnflinger::HwcLayer> sorted_layers
output_fences.reserve(sorted_layers.size());
for (auto& layer : sorted_layers) {
+ const auto handle = nvmap.GetHandle(layer.buffer_handle);
+ const auto* session = handle ? container.GetSession(handle->session_id) : nullptr;
+ const bool native_tiling = session && session->process->UsesPAuthReturns();
output_layers.emplace_back(Tegra::FramebufferConfig{
.address = nvmap.GetHandleAddress(layer.buffer_handle),
.offset = layer.offset,
@@ -77,6 +87,7 @@ void nvdisp_disp0::Composite(std::span<const Nvnflinger::HwcLayer> sorted_layers
.transform_flags = layer.transform,
.crop_rect = layer.crop_rect,
.blending = ConvertBlending(layer.blending),
+ .native_tiling = native_tiling,
.layer_stack_mask = layer.layer_stack_mask,
});
@@ -87,6 +98,56 @@ void nvdisp_disp0::Composite(std::span<const Nvnflinger::HwcLayer> sorted_layers
}
}
+ const char* capture = std::getenv("EDEN_NATIVE_FRAME_CAPTURE");
+ const bool sequence = std::getenv("EDEN_NATIVE_FRAME_SEQUENCE") != nullptr;
+ const bool on_demand = std::getenv("EDEN_NATIVE_FRAME_ON_DEMAND") != nullptr;
+ const bool requested = on_demand && !output_layers.empty() &&
+ !diagnostic_frame_pending->load() && std::filesystem::exists("native-frame.request");
+ if (requested) std::filesystem::remove("native-frame.request");
+ const bool sample_frame = requested || (capture && std::string_view{capture} == "1" && !output_layers.empty() &&
+ diagnostic_frame_count < (sequence ? 36000U : 3840U) && ++diagnostic_frame_count != 0 &&
+ (sequence ? (diagnostic_frame_count == 1 || diagnostic_frame_count % 120 == 0)
+ : !diagnostic_visible_frame->load()));
+ if (sample_frame && !diagnostic_frame_pending->exchange(true)) {
+ for (const auto& fence : output_fences) {
+ LOG_INFO(Service_NVDRV, "Native composite waits for syncpoint={}/{}, current={}", fence.id, fence.value,
+ system.Host1x().GetSyncpointManager().GetGuestSyncpointValue(fence.id));
+ }
+ LOG_INFO(Service_NVDRV, "Native composite submitted: layers={}, address={:#x}", output_layers.size(), output_layers.front().address);
+ const auto layout = Layout::DefaultFrameLayout(1920, 1080);
+ auto pixels = std::make_shared<std::vector<u8>>(layout.width * layout.height * 4);
+ const bool first = diagnostic_frame_count == 1;
+ auto pending = diagnostic_frame_pending;
+ auto visible = diagnostic_visible_frame;
+ system.GPU().Renderer().RequestScreenshot(pixels->data(), [pixels, layout, first, pending, visible, sequence, on_demand](bool invert_y) {
+ std::array<u8, 18> header{};
+ header[2] = 2;
+ header[12] = static_cast<u8>(layout.width & 255);
+ header[13] = static_cast<u8>(layout.width >> 8);
+ header[14] = static_cast<u8>(layout.height & 255);
+ header[15] = static_cast<u8>(layout.height >> 8);
+ header[16] = 32;
+ header[17] = invert_y ? 8 : 0x28;
+ bool nonblack{};
+ for (size_t i = 0; i < pixels->size(); i += 4) {
+ if ((*pixels)[i] || (*pixels)[i + 1] || (*pixels)[i + 2]) {
+ nonblack = true;
+ break;
+ }
+ }
+ const auto save = [&](const char* path) {
+ std::ofstream out(path, std::ios::binary);
+ out.write(reinterpret_cast<const char*>(header.data()), header.size());
+ out.write(reinterpret_cast<const char*>(pixels->data()), pixels->size());
+ return out.good();
+ };
+ if (first) save("native-first-frame.tga");
+ const bool saved = (first || nonblack || sequence || on_demand) ? save("native-latest-frame.tga") : true;
+ if (nonblack && !visible->load() && save("native-visible-frame.tga")) visible->store(true);
+ LOG_INFO(Service_NVDRV, "Captured composited frame: {}x{}, saved={}, nonblack={}", layout.width, layout.height, saved, nonblack);
+ pending->store(false);
+ }, layout);
+ }
system.GPU().RequestComposite(std::move(output_layers), std::move(output_fences));
Common::ADPF::ReportFrameInterval();
system.SpeedLimiter().DoSpeedLimiting(system.CoreTiming().GetGlobalTimeUs());
diff --git a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h
index ff0bfb2ec9..f795144ebe 100644
--- a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h
+++ b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h
@@ -6,6 +6,7 @@
#pragma once
+#include <atomic>
#include <memory>
#include <vector>
#include "common/common_types.h"
@@ -45,6 +46,9 @@ public:
Kernel::KEvent* QueryEvent(u32 event_id) override;
private:
+ u32 diagnostic_frame_count{};
+ std::shared_ptr<std::atomic_bool> diagnostic_frame_pending{std::make_shared<std::atomic_bool>(false)};
+ std::shared_ptr<std::atomic_bool> diagnostic_visible_frame{std::make_shared<std::atomic_bool>(false)};
NvCore::Container& container;
NvCore::NvMap& nvmap;
};
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 3c9e42f602..947ea6088f 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
@@ -6,6 +6,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <cstring>
+#include <limits>
#include <utility>
#include "common/alignment.h"
@@ -13,10 +14,13 @@
#include "common/common_types.h"
#include "common/logging.h"
#include "core/core.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/service/nvdrv/devices/gpu_address_space_layout.h"
#include "core/hle/service/nvdrv/core/container.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/native_gpu_kind.h"
#include "core/hle/service/nvdrv/devices/nvhost_gpu.h"
#include "core/hle/service/nvdrv/nvdrv.h"
#include "video_core/control/channel_state.h"
@@ -27,10 +31,12 @@
namespace Service::Nvidia::Devices {
nvhost_as_gpu::nvhost_as_gpu(Core::System& system_, Module& module_, NvCore::Container& core)
- : nvdevice{system_}, module{module_}, container{core}, nvmap{core.GetNvMapFile()}, vm{},
+ : nvdevice{system_, Type::GpuAddressSpace}, module{module_}, container{core}, nvmap{core.GetNvMapFile()}, vm{},
gmmu{} {}
-nvhost_as_gpu::~nvhost_as_gpu() = default;
+nvhost_as_gpu::~nvhost_as_gpu() {
+ if (m_owner_process) { m_owner_process->Close(system.Kernel()); }
+}
NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> input,
std::span<u8> output) {
@@ -51,6 +57,10 @@ NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> i
return WrapFixed(this, &nvhost_as_gpu::GetVARegions1, input, output);
case 0x9:
return WrapFixed(this, &nvhost_as_gpu::AllocAsEx, input, output);
+ case 0xb:
+ return WrapFixed(this, &nvhost_as_gpu::GetSyncpointShimInfo, input, output);
+ case 0xc:
+ return WrapFixed(this, &nvhost_as_gpu::ModifyKind, input, output);
case 0x14:
return WrapVariable(this, &nvhost_as_gpu::Remap, input, output);
default:
@@ -90,11 +100,139 @@ 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) {
+ m_fd = fd;
+ if (const auto* session = container.GetSession(session_id)) {
+ m_owner_process = session->process;
+ m_owner_process->Open(system.Kernel());
+ m_extended_abi = m_owner_process->UsesPAuthReturns();
+ }
+}
void nvhost_as_gpu::OnClose(DeviceFD fd) {}
+NvResult nvhost_as_gpu::CreateNativeSparseRegion(u64 address, u64 size) {
+ if (!m_extended_abi || !size || !Common::IsAligned(address, VM::YUZU_PAGESIZE) ||
+ !Common::IsAligned(size, VM::YUZU_PAGESIZE) || (size >> VM::PAGE_SIZE_BITS) > 0xffffffffULL) {
+ return NvResult::BadParameter;
+ }
+ {
+ const std::scoped_lock lock{mutex};
+ if (!vm.initialised || address < vm.va_range_start || address >= vm.va_range_split ||
+ size > vm.va_range_split - address) { return NvResult::BadParameter; }
+ const auto next = allocation_map.lower_bound(address);
+ if ((next != allocation_map.end() && next->first < address + size) ||
+ (next != allocation_map.begin() && std::prev(next)->first + std::prev(next)->second.size > address)) {
+ return NvResult::BadParameter;
+ }
+ }
+ IoctlAllocSpace reservation{};
+ reservation.pages = static_cast<u32>(size >> VM::PAGE_SIZE_BITS);
+ reservation.page_size = VM::YUZU_PAGESIZE;
+ reservation.flags = MappingFlags::Fixed | MappingFlags::Sparse;
+ reservation.offset = address;
+ return AllocateSpace(reservation);
+}
+
+NvResult nvhost_as_gpu::DestroyNativeSparseRegion(u64 address, u64 size) {
+ {
+ const std::scoped_lock lock{mutex};
+ const auto it = allocation_map.find(address);
+ if (!m_extended_abi || it == allocation_map.end() || !it->second.sparse || it->second.size != size) {
+ return NvResult::BadParameter;
+ }
+ }
+ IoctlFreeSpace release{};
+ release.offset = address;
+ release.pages = static_cast<u32>(size >> VM::PAGE_SIZE_BITS);
+ release.page_size = VM::YUZU_PAGESIZE;
+ return FreeSpace(release);
+}
+
+NvResult nvhost_as_gpu::MapNative4K(u32 handle_id, u64 address, u64 buffer_offset, u64 size, u8 kind) {
+ if (!m_extended_abi || !size || !Common::IsAligned(address, VM::YUZU_PAGESIZE) ||
+ !Common::IsAligned(size, VM::YUZU_PAGESIZE) ||
+ !Common::IsAligned(buffer_offset, VM::YUZU_PAGESIZE)) {
+ return NvResult::BadParameter;
+ }
+ const auto handle = nvmap.GetHandle(handle_id);
+ if (!handle || buffer_offset > handle->orig_size || size > handle->orig_size - buffer_offset) {
+ return NvResult::BadParameter;
+ }
+ bool sparse_reservation = false;
+ {
+ const std::scoped_lock lock{mutex};
+ if (!vm.initialised || address < vm.va_range_start || address >= vm.va_range_split ||
+ size > vm.va_range_split - address) {
+ return NvResult::BadParameter;
+ }
+ const auto upper = allocation_map.upper_bound(address);
+ if (upper != allocation_map.begin()) {
+ const auto owner = std::prev(upper);
+ sparse_reservation = owner->second.sparse && address >= owner->first &&
+ address - owner->first < owner->second.size && size <= owner->second.size - (address - owner->first);
+ }
+ const auto next = allocation_map.lower_bound(address);
+ if (!sparse_reservation && ((next != allocation_map.end() && next->first < address + size) ||
+ (next != allocation_map.begin() &&
+ std::prev(next)->first + std::prev(next)->second.size > address))) {
+ return NvResult::BadParameter;
+ }
+ }
+ IoctlAllocSpace reservation{};
+ reservation.pages = static_cast<u32>(size >> VM::PAGE_SIZE_BITS);
+ reservation.page_size = VM::YUZU_PAGESIZE;
+ reservation.flags = MappingFlags::Fixed;
+ reservation.offset = address;
+ const auto reserved = sparse_reservation ? NvResult::Success : AllocateSpace(reservation);
+ if (reserved != NvResult::Success) { return reserved; }
+ IoctlMapBufferEx mapping{};
+ mapping.flags = MappingFlags::Fixed;
+ mapping.kind = kind;
+ mapping.handle = handle_id;
+ mapping.page_size = VM::YUZU_PAGESIZE;
+ mapping.buffer_offset = static_cast<s64>(buffer_offset);
+ mapping.mapping_size = size;
+ mapping.offset = static_cast<s64>(address);
+ const auto result = MapBufferEx(mapping);
+ if (result != NvResult::Success && !sparse_reservation) {
+ nvmap.UnpinHandle(handle_id);
+ IoctlFreeSpace release{};
+ release.offset = address;
+ release.pages = reservation.pages;
+ release.page_size = VM::YUZU_PAGESIZE;
+ FreeSpace(release);
+ }
+ return result;
+}
+
+NvResult nvhost_as_gpu::UnmapNative4K(u64 address, u64 size) {
+ if (!m_extended_abi || !size || !Common::IsAligned(address, VM::YUZU_PAGESIZE) ||
+ !Common::IsAligned(size, VM::YUZU_PAGESIZE)) {
+ return NvResult::BadParameter;
+ }
+ bool sparse_mapping = false;
+ {
+ const std::scoped_lock lock{mutex};
+ const auto it = mapping_map.find(address);
+ sparse_mapping = it != mapping_map.end() && it->second.sparse_alloc && it->second.size == size;
+ }
+ if (sparse_mapping) {
+ IoctlUnmapBuffer unmap{};
+ unmap.offset = address;
+ return UnmapBuffer(unmap);
+ }
+ IoctlFreeSpace release{};
+ release.offset = address;
+ release.pages = static_cast<u32>(size >> VM::PAGE_SIZE_BITS);
+ release.page_size = VM::YUZU_PAGESIZE;
+ return FreeSpace(release);
+}
+
+
NvResult nvhost_as_gpu::AllocAsEx(IoctlAllocAsEx& params) {
- LOG_DEBUG(Service_NVDRV, "called, big_page_size={:#x}", params.big_page_size);
+ LOG_DEBUG(Service_NVDRV, "called, flags={:#x}, as_fd={}, big_page_size={:#x}, reserved={:#x}, VA={:#x}/{:#x}/{:#x}",
+ params.flags, params.as_fd, params.big_page_size, params.reserved,
+ params.va_range_start, params.va_range_split, params.va_range_end);
std::scoped_lock lock(mutex);
@@ -103,21 +241,43 @@ NvResult nvhost_as_gpu::AllocAsEx(IoctlAllocAsEx& params) {
return NvResult::InvalidState;
}
- if (params.big_page_size) {
- if (!std::has_single_bit(params.big_page_size)) {
- LOG_ERROR(Service_NVDRV, "Non power-of-2 big page size: {:#x}!", params.big_page_size);
+ std::shared_ptr<nvhost_as_gpu> shared_parent;
+ if (m_extended_abi && params.as_fd != 0) {
+ if (params.as_fd == m_fd || params.flags != 5 || params.big_page_size != 0x1000 ||
+ params.reserved != 0 || params.va_range_split != 0) { return NvResult::BadParameter; }
+ const auto device = module.GetDevice<nvdevice>(params.as_fd);
+ if (!device || device->GetType() != Type::GpuAddressSpace) { return NvResult::BadParameter; }
+ shared_parent = std::static_pointer_cast<nvhost_as_gpu>(device);
+ if (!shared_parent->m_extended_abi || !shared_parent->vm.initialised ||
+ shared_parent->m_owner_process != m_owner_process ||
+ params.va_range_start < shared_parent->vm.va_range_start ||
+ params.va_range_start >= params.va_range_end ||
+ params.va_range_end > shared_parent->vm.va_range_split) { return NvResult::BadParameter; }
+ }
+
+ const u32 big_page_size = GetGpuAddressSpaceBigPageSize(m_extended_abi, params.big_page_size);
+ if (big_page_size) {
+ if (!std::has_single_bit(big_page_size)) {
+ LOG_ERROR(Service_NVDRV, "Non power-of-2 big page size: {:#x}!", big_page_size);
return NvResult::BadValue;
}
- if ((params.big_page_size & VM::SUPPORTED_BIG_PAGE_SIZES) == 0) {
- LOG_ERROR(Service_NVDRV, "Unsupported big page size: {:#x}!", params.big_page_size);
+ if ((big_page_size & VM::SUPPORTED_BIG_PAGE_SIZES) == 0) {
+ LOG_ERROR(Service_NVDRV, "Unsupported big page size: {:#x}!", big_page_size);
return NvResult::BadValue;
}
- vm.big_page_size = params.big_page_size;
- vm.big_page_size_bits = static_cast<u32>(std::countr_zero(params.big_page_size));
+ vm.big_page_size = big_page_size;
+ vm.big_page_size_bits = static_cast<u32>(std::countr_zero(big_page_size));
+
+ vm.va_range_start = big_page_size << VM::VA_START_SHIFT;
+ }
- vm.va_range_start = params.big_page_size << VM::VA_START_SHIFT;
+ if (m_extended_abi && !params.va_range_start) {
+ const auto layout = GetGpuAddressSpaceLayout(true);
+ vm.va_range_start = layout.start;
+ vm.va_range_split = layout.split;
+ vm.va_range_end = layout.end;
}
// If this is unspecified then default values should be used
@@ -127,6 +287,12 @@ NvResult nvhost_as_gpu::AllocAsEx(IoctlAllocAsEx& params) {
vm.va_range_end = params.va_range_end;
}
+ if (shared_parent) {
+ // Native flags 5 expose the parent's small-page region as a separate
+ // reservation view. GPU mappings must resolve through the same page table.
+ vm.va_range_split = params.va_range_end;
+ }
+
const u64 max_big_page_bits = Common::Log2Ceil(vm.va_range_end);
const auto start_pages{static_cast<u32>(vm.va_range_start >> VM::PAGE_SIZE_BITS)};
@@ -135,11 +301,17 @@ NvResult nvhost_as_gpu::AllocAsEx(IoctlAllocAsEx& params) {
const auto start_big_pages{static_cast<u32>(vm.va_range_split >> vm.big_page_size_bits)};
const auto end_big_pages{
- static_cast<u32>((vm.va_range_end - vm.va_range_split) >> vm.big_page_size_bits)};
+ static_cast<u32>(vm.va_range_end >> vm.big_page_size_bits)};
vm.big_page_allocator.emplace(start_big_pages, end_big_pages);
- gmmu = std::make_unique<Tegra::MemoryManager>(system, max_big_page_bits, vm.va_range_split, vm.big_page_size_bits, VM::PAGE_SIZE_BITS);
- system.GPU().InitAddressSpace(*gmmu);
+ if (shared_parent) {
+ m_shared_parent = std::move(shared_parent);
+ gmmu = m_shared_parent->gmmu;
+ LOG_DEBUG(Service_NVDRV, "native AS view fd={} shares page table with fd={}", m_fd, params.as_fd);
+ } else {
+ gmmu = std::make_shared<Tegra::MemoryManager>(system, max_big_page_bits, vm.va_range_split, vm.big_page_size_bits, VM::PAGE_SIZE_BITS);
+ system.GPU().InitAddressSpace(*gmmu);
+ }
vm.initialised = true;
return NvResult::Success;
@@ -159,7 +331,7 @@ NvResult nvhost_as_gpu::AllocateSpace(IoctlAllocSpace& params) {
return NvResult::BadValue;
}
- if (params.page_size != vm.big_page_size &&
+ if (!m_extended_abi && params.page_size != vm.big_page_size &&
((params.flags & MappingFlags::Sparse) != MappingFlags::None)) {
UNIMPLEMENTED_MSG("Sparse small pages are not implemented!");
return NvResult::NotImplemented;
@@ -184,7 +356,7 @@ NvResult nvhost_as_gpu::AllocateSpace(IoctlAllocSpace& params) {
u64 size{static_cast<u64>(params.pages) * params.page_size};
if ((params.flags & MappingFlags::Sparse) != MappingFlags::None) {
- gmmu->MapSparse(params.offset, size);
+ gmmu->MapSparse(params.offset, size, params.page_size != VM::YUZU_PAGESIZE);
}
allocation_map[params.offset] = {
@@ -217,6 +389,7 @@ bool nvhost_as_gpu::FreeMappingLocked(u64 offset) noexcept {
gmmu->Unmap(offset, mapping.size);
}
mapping_map.erase(offset);
+ map_buffer_offsets.erase(static_cast<s64>(offset));
return true;
}
return false;
@@ -251,6 +424,77 @@ NvResult nvhost_as_gpu::FreeSpace(IoctlFreeSpace& params) {
return NvResult::BadValue;
}
+NvResult nvhost_as_gpu::GetNativeSyncpointShim(u64& address, u32& stride, u32& count) {
+ if (!m_extended_abi || !vm.initialised) { return NvResult::NotSupported; }
+ address = vm.va_range_split - VM::YUZU_PAGESIZE;
+ if (!m_syncpoint_shim_reserved) {
+ IoctlAllocSpace allocation{};
+ allocation.pages = 1;
+ allocation.page_size = VM::YUZU_PAGESIZE;
+ allocation.flags = MappingFlags::Fixed;
+ allocation.offset = address;
+ const auto result = AllocateSpace(allocation);
+ if (result != NvResult::Success) { return result; }
+ m_syncpoint_shim_reserved = true;
+ gmmu->SetNativeSyncpointShim(address);
+ }
+ stride = sizeof(u32);
+ count = 192;
+ return NvResult::Success;
+}
+
+NvResult nvhost_as_gpu::GetSyncpointShimInfo(IoctlSyncpointShimInfo& params) {
+ u64 address{};
+ u32 stride{}, count{};
+ const auto result = GetNativeSyncpointShim(address, stride, count);
+ params.address = address;
+ params.stride = stride;
+ params.count = count;
+ LOG_DEBUG(Service_NVDRV, "native syncpoint shim address={:#x}, stride={}, count={}, result={}",
+ address, stride, count, static_cast<u32>(result));
+ return result;
+}
+
+NvResult nvhost_as_gpu::ModifyNativeKind(u64 address, u64 size, u8 requested, u8 fallback) {
+ IoctlModifyKind params{};
+ params.compressed_kind = requested;
+ params.uncompressed_kind = fallback;
+ params.mapping_base = address;
+ params.size = size;
+ return ModifyKind(params);
+}
+
+NvResult nvhost_as_gpu::ModifyKind(IoctlModifyKind& params) {
+ const u32 requested = params.compressed_kind;
+ const u32 fallback = params.uncompressed_kind;
+ const u64 base = params.mapping_base;
+ const u64 offset = params.buffer_offset;
+ const u64 size = params.size;
+ LOG_DEBUG(Service_NVDRV, "native modify kind base={:#x}, offset={:#x}, size={:#x}, kinds={:#x}/{:#x}",
+ base, offset, size, requested, fallback);
+ if (!m_extended_abi || params.reserved != 0 || fallback > 255 ||
+ (requested > 255 && requested != 0xffff) || !size ||
+ offset > std::numeric_limits<u64>::max() - base ||
+ !Common::IsAligned(base + offset, VM::YUZU_PAGESIZE) ||
+ !Common::IsAligned(size, VM::YUZU_PAGESIZE)) {
+ return NvResult::BadParameter;
+ }
+ const auto kind = SelectNativeMemoryKind(static_cast<u8>(requested == 0xffff ? fallback : requested),
+ static_cast<u8>(fallback));
+ if (!kind) { return NvResult::NotSupported; }
+ const std::scoped_lock lock{mutex};
+ const u64 address = base + offset;
+ auto it = mapping_map.upper_bound(address);
+ if (!vm.initialised || it == mapping_map.begin()) { return NvResult::BadParameter; }
+ --it;
+ const auto& mapping = it->second;
+ const u64 delta = address - it->first;
+ if (delta >= mapping.size || size > mapping.size - delta) { return NvResult::BadParameter; }
+ gmmu->Map(address, mapping.ptr + delta, size, static_cast<Tegra::PTEKind>(kind->backend),
+ mapping.big_page);
+ return NvResult::Success;
+}
+
NvResult nvhost_as_gpu::Remap(std::span<IoctlRemapEntry> entries) {
LOG_DEBUG(Service_NVDRV, "called, num_entries={:#x}", entries.size());
@@ -414,7 +658,13 @@ NvResult nvhost_as_gpu::UnmapBuffer(IoctlUnmapBuffer& params) {
NvResult nvhost_as_gpu::BindChannel(IoctlBindChannel& params) {
LOG_DEBUG(Service_NVDRV, "called, fd={:X}", params.fd);
- auto gpu_channel_device = module.GetDevice<nvhost_gpu>(params.fd);
+ const auto device = module.GetDevice<nvdevice>(params.fd);
+ if (!device || device->GetType() != nvdevice::Type::GpuChannel) { return NvResult::BadParameter; }
+ const auto gpu_channel_device = std::static_pointer_cast<nvhost_gpu>(device);
+ if (!vm.initialised || !gmmu || (gpu_channel_device->channel_state->initialized &&
+ gpu_channel_device->channel_state->memory_manager != gmmu.get())) { return NvResult::InvalidState; }
+ gpu_channel_device->channel_state->memory_manager_owner =
+ std::shared_ptr<Tegra::MemoryManager>(module.GetDevice<nvhost_as_gpu>(m_fd), gmmu.get());
gpu_channel_device->channel_state->memory_manager = gmmu.get();
return NvResult::Success;
}
@@ -424,13 +674,13 @@ void nvhost_as_gpu::GetVARegionsImpl(IoctlGetVaRegions& params) {
params.regions = std::array<VaRegion, 2>{
VaRegion{
- .offset = vm.small_page_allocator->GetVAStart() << VM::PAGE_SIZE_BITS,
+ .offset = GpuPageAddress(vm.small_page_allocator->GetVAStart(), VM::PAGE_SIZE_BITS),
.page_size = VM::YUZU_PAGESIZE,
._pad0_{},
.pages = vm.small_page_allocator->GetVALimit() - vm.small_page_allocator->GetVAStart(),
},
VaRegion{
- .offset = vm.big_page_allocator->GetVAStart() << vm.big_page_size_bits,
+ .offset = GpuPageAddress(vm.big_page_allocator->GetVAStart(), vm.big_page_size_bits),
.page_size = vm.big_page_size,
._pad0_{},
.pages = vm.big_page_allocator->GetVALimit() - vm.big_page_allocator->GetVAStart(),
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 b13b1c9efa..8c7306e42a 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
@@ -64,6 +64,13 @@ public:
Kernel::KEvent* QueryEvent(u32 event_id) override;
+ NvResult CreateNativeSparseRegion(u64 address, u64 size);
+ NvResult DestroyNativeSparseRegion(u64 address, u64 size);
+ NvResult MapNative4K(u32 handle, u64 address, u64 buffer_offset, u64 size, u8 kind);
+ NvResult UnmapNative4K(u64 address, u64 size);
+ NvResult GetNativeSyncpointShim(u64& address, u32& stride, u32& count);
+ NvResult ModifyNativeKind(u64 address, u64 size, u8 requested, u8 fallback);
+
struct VaRegion {
u64 offset;
u32 page_size;
@@ -145,6 +152,25 @@ private:
static_assert(sizeof(IoctlGetVaRegions) == 16 + sizeof(VaRegion) * 2,
"IoctlGetVaRegions is incorrect size");
+ struct IoctlSyncpointShimInfo {
+ u64_le address;
+ u32_le stride;
+ u32_le count;
+ };
+ static_assert(sizeof(IoctlSyncpointShimInfo) == 16);
+ NvResult GetSyncpointShimInfo(IoctlSyncpointShimInfo& params);
+
+ struct IoctlModifyKind {
+ u16_le compressed_kind;
+ u16_le uncompressed_kind;
+ u32_le reserved;
+ u64_le buffer_offset;
+ u64_le size;
+ u64_le mapping_base;
+ };
+ static_assert(sizeof(IoctlModifyKind) == 32);
+ NvResult ModifyKind(IoctlModifyKind& params);
+
NvResult AllocAsEx(IoctlAllocAsEx& params);
NvResult AllocateSpace(IoctlAllocSpace& params);
NvResult Remap(std::span<IoctlRemapEntry> params);
@@ -196,6 +222,12 @@ private:
std::map<u64, Allocation> allocation_map;
std::mutex mutex; //!< Locks all AS operations
+ bool m_extended_abi{};
+ bool m_syncpoint_shim_reserved{};
+ DeviceFD m_fd{-1};
+ Kernel::KProcess* m_owner_process{};
+ std::shared_ptr<nvhost_as_gpu> m_shared_parent;
+
struct VM {
static constexpr u32 YUZU_PAGESIZE{0x1000};
static constexpr u32 PAGE_SIZE_BITS{static_cast<u32>(std::countr_zero<u32>(YUZU_PAGESIZE))};
@@ -219,7 +251,7 @@ private:
bool initialised{};
} vm;
- std::unique_ptr<Tegra::MemoryManager> gmmu;
+ std::shared_ptr<Tegra::MemoryManager> gmmu;
};
} // 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 14cdd8fda4..6684352b00 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
@@ -4,6 +4,7 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <algorithm>
#include <cstring>
#include "common/assert.h"
#include "common/logging.h"
@@ -11,6 +12,7 @@
#include "core/core_timing.h"
#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/native_gpu_characteristics.h"
#include "core/hle/service/nvdrv/nvdrv.h"
namespace Service::Nvidia::Devices {
@@ -94,6 +96,25 @@ NvResult nvhost_ctrl_gpu::Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8>
case 'G':
switch (command.cmd) {
case 0x5:
+ if (command.raw == 0xc0e04705) {
+ constexpr size_t packet_size = 0x10 + NativeGpuCharacteristicsSize;
+ if (input.size() < packet_size || output.size() < packet_size ||
+ inline_output.size() < NativeGpuCharacteristicsSize) {
+ return NvResult::BadParameter;
+ }
+ IoctlCharacteristics legacy{};
+ GetCharacteristics3(legacy, {});
+ const auto native = MakeNativeGpuCharacteristics(
+ std::span<const u8, 0xa0>{reinterpret_cast<const u8*>(&legacy.gc), 0xa0});
+ std::fill(output.begin(), output.end(), u8{});
+ std::fill(inline_output.begin(), inline_output.end(), u8{});
+ std::memcpy(output.data(), input.data(), 0x10);
+ const u64_le size{NativeGpuCharacteristicsSize};
+ std::memcpy(output.data(), &size, sizeof(size));
+ std::memcpy(output.data() + 0x10, native.data(), native.size());
+ std::memcpy(inline_output.data(), native.data(), native.size());
+ return NvResult::Success;
+ }
return WrapFixedInlOut(this, &nvhost_ctrl_gpu::GetCharacteristics3, input, output,
inline_output);
case 0x6:
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
index 1e571be4c9..6b9678e440 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
@@ -4,6 +4,8 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <cstdlib>
+#include <fstream>
#include <cstring>
#include "common/assert.h"
#include "common/logging.h"
@@ -14,11 +16,13 @@
#include "core/hle/service/nvdrv/core/syncpoint_manager.h"
#include "core/hle/service/nvdrv/devices/ioctl_serialization.h"
#include "core/hle/service/nvdrv/devices/nvhost_gpu.h"
+#include "core/hle/service/nvdrv/devices/native_gpu_context.h"
#include "core/hle/service/nvdrv/nvdrv.h"
#include "core/memory.h"
#include "video_core/control/channel_state.h"
#include "video_core/engines/puller.h"
#include "video_core/gpu.h"
+#include "video_core/memory_manager.h"
#include "video_core/host1x/host1x.h"
namespace Service::Nvidia::Devices {
@@ -33,7 +37,7 @@ Tegra::CommandHeader BuildFenceAction(Tegra::Engines::Puller::FenceOperation op,
nvhost_gpu::nvhost_gpu(Core::System& system_, EventInterface& events_interface_,
NvCore::Container& core_)
- : nvdevice{system_}, events_interface{events_interface_}, core{core_},
+ : nvdevice{system_, Type::GpuChannel}, events_interface{events_interface_}, core{core_},
syncpoint_manager{core_.GetSyncpointManager()}, nvmap{core.GetNvMapFile()},
channel_state{system.GPU().AllocateChannel()} {
channel_syncpoint = syncpoint_manager.AllocateSyncpoint(false);
@@ -128,6 +132,7 @@ NvResult nvhost_gpu::Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8> inpu
void nvhost_gpu::OnOpen(NvCore::SessionId session_id, DeviceFD fd) {
sessions[fd] = session_id;
+ channel_state->native_abi = UsesNativeAbi();
}
void nvhost_gpu::OnClose(DeviceFD fd) {
@@ -280,6 +285,9 @@ NvResult nvhost_gpu::AllocateObjectContext(IoctlAllocObjCtx& params) {
s32_le ctx_class_number_index =
GetObjectContextClassNumberIndex(static_cast<CtxClasses>(params.class_num));
+ if (const auto native_slot = GetNativeGpuContextSlot(params.class_num)) {
+ if (UsesNativeAbi()) { ctx_class_number_index = *native_slot; }
+ }
if (ctx_class_number_index < 0) {
LOG_ERROR(Service_NVDRV, "Invalid class number for object context: {:#x}",
params.class_num);
@@ -311,13 +319,13 @@ static boost::container::small_vector<Tegra::CommandHeader, 512> BuildWaitComman
}
static boost::container::small_vector<Tegra::CommandHeader, 512> BuildIncrementCommandList(
- NvFence fence) {
+ NvFence fence, u32 increment_count = 2) {
boost::container::small_vector<Tegra::CommandHeader, 512> result{
Tegra::BuildCommandHeader(Tegra::BufferMethods::SyncpointPayload, 1,
Tegra::SubmissionMode::Increasing),
{}};
- for (u32 count = 0; count < 2; ++count) {
+ for (u32 count = 0; count < increment_count; ++count) {
result.push_back(Tegra::BuildCommandHeader(Tegra::BufferMethods::SyncpointOperation, 1,
Tegra::SubmissionMode::Increasing));
result.push_back(
@@ -328,12 +336,12 @@ static boost::container::small_vector<Tegra::CommandHeader, 512> BuildIncrementC
}
static boost::container::small_vector<Tegra::CommandHeader, 512> BuildIncrementWithWfiCommandList(
- NvFence fence) {
+ NvFence fence, u32 increment_count = 2) {
boost::container::small_vector<Tegra::CommandHeader, 512> result{
Tegra::BuildCommandHeader(Tegra::BufferMethods::WaitForIdle, 1,
Tegra::SubmissionMode::Increasing),
{}};
- auto increment_list{BuildIncrementCommandList(fence)};
+ auto increment_list{BuildIncrementCommandList(fence, increment_count)};
result.insert(result.end(), increment_list.begin(), increment_list.end());
return result;
}
@@ -351,6 +359,41 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
system.GPU().InitChannel(*channel_state, channel_state->program_id);
}
+ if (UsesNativeAbi() && native_submit_logs++ < 8) {
+ LOG_DEBUG(Service_NVDRV, "native submit channel={}, initialized={}, entries={}, flags={:#x}, fence={}:{}",
+ channel_state->bind_id, channel_state->initialized, entries.command_lists.size(),
+ params.flags.raw, params.fence.id, params.fence.value);
+ for (size_t i = 0; i < std::min<size_t>(entries.command_lists.size(), 4); ++i) {
+ const auto& entry = entries.command_lists[i];
+ std::array<u32, 16> words{};
+ const size_t bytes = std::min<size_t>(entry.size * sizeof(u32), sizeof(words));
+ const bool mapped = channel_state->memory_manager && bytes &&
+ channel_state->memory_manager->IsFullyMappedRange(entry.addr, bytes);
+ if (mapped) { channel_state->memory_manager->ReadBlockUnsafe(entry.addr, words.data(), bytes); }
+ if (mapped && entry.size > 1000 && std::getenv("EDEN_NATIVE_FRAME_CAPTURE")) {
+ std::vector<u32> contents(static_cast<size_t>(entry.size));
+ channel_state->memory_manager->ReadBlockUnsafe(entry.addr, contents.data(), contents.size() * sizeof(u32));
+ std::ofstream dump("native-init-channel-" + std::to_string(channel_state->bind_id) + "-" + std::to_string(i) + ".bin", std::ios::binary);
+ dump.write(reinterpret_cast<const char*>(contents.data()), contents.size() * sizeof(u32));
+ }
+ LOG_DEBUG(Service_NVDRV, "native pushbuffer entry={} raw={:#x}, address={:#x}, words={}, mapped={}, data={:08x}",
+ i, entry.raw, static_cast<u64>(entry.addr), static_cast<u64>(entry.size),
+ mapped, fmt::join(words, ","));
+ }
+ }
+
+ if (UsesNativeAbi() && std::getenv("EDEN_NATIVE_FRAME_CAPTURE") && native_submit_logs < 100) {
+ for (size_t i = 0; i < entries.command_lists.size(); ++i) {
+ const auto& entry = entries.command_lists[i];
+ const auto bytes = static_cast<size_t>(entry.size) * sizeof(u32);
+ if (!bytes || bytes > 0x100000 || !channel_state->memory_manager->IsFullyMappedRange(entry.addr, bytes)) continue;
+ std::vector<u32> contents(static_cast<size_t>(entry.size));
+ channel_state->memory_manager->ReadBlockUnsafe(entry.addr, contents.data(), bytes);
+ std::ofstream dump("native-submit-" + std::to_string(channel_state->bind_id) + "-" + std::to_string(native_submit_logs) + "-" + std::to_string(i) + ".bin", std::ios::binary);
+ dump.write(reinterpret_cast<const char*>(contents.data()), bytes);
+ }
+ }
+
const auto bind_id = channel_state->bind_id;
auto& flags = params.flags;
@@ -365,6 +408,8 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
}
}
+ const u32 native_completion = UsesNativeAbi() && flags.increment_value.Value() && !flags.fence_increment.Value() ? params.fence.value : 0;
+ if (native_completion > 64) return NvResult::BadParameter;
params.fence.id = channel_syncpoint;
u32 increment{(flags.fence_increment.Value() != 0 ? 2 : 0) +
@@ -382,6 +427,14 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
}
}
+ if (native_completion) {
+ // The native C56F stream reports its completion count without emitting
+ // host syncpoint methods. Order actual increments after that GPU work.
+ gpu.PushGPUEntries(bind_id, Tegra::CommandList{flags.suppress_wfi.Value()
+ ? BuildIncrementCommandList(params.fence, native_completion)
+ : BuildIncrementWithWfiCommandList(params.fence, native_completion)});
+ LOG_DEBUG(Service_NVDRV, "Native ordered completion: syncpoint={}, increments={}, target={}", channel_syncpoint, native_completion, params.fence.value);
+ }
flags.raw = 0;
return NvResult::Success;
@@ -463,10 +516,46 @@ Kernel::KEvent* nvhost_gpu::QueryEvent(u32 event_id) {
return sm_exception_breakpoint_pause_report_event;
case 3:
return error_notifier_event;
+ case 7:
+ // The native SDK requests this event immediately before configuring
+ // CHANNEL_SET_ERROR_NOTIFIER, then wraps it in its channel error wait.
+ return UsesNativeAbi() ? error_notifier_event : nullptr;
default:
LOG_CRITICAL(Service_NVDRV, "Unknown Ctrl GPU Event {}", event_id);
return nullptr;
}
}
+NvResult nvhost_gpu::BindToTsg(DeviceFD group, Kernel::KProcess* process, std::shared_ptr<Tegra::NativeMacroContext> macros) {
+ const std::scoped_lock lock{tsg_mutex};
+ if (tsg_group) { return NvResult::Busy; }
+ bool owned{};
+ for (const auto& [fd, session_id] : sessions) {
+ const auto* session = core.GetSession(session_id);
+ owned |= session && session->process == process;
+ }
+ if (!owned) { return NvResult::AccessDenied; }
+ if (channel_state->native_abi) {
+ channel_state->native_macro_context = std::move(macros);
+ if (channel_state->payload) {
+ channel_state->payload->maxwell_3d.SetNativeMacroContext(channel_state->native_macro_context);
+ }
+ }
+ tsg_group = group;
+ return NvResult::Success;
+}
+
+void nvhost_gpu::UnbindFromTsg(DeviceFD group) {
+ const std::scoped_lock lock{tsg_mutex};
+ if (tsg_group == group) { tsg_group.reset(); }
+}
+
+bool nvhost_gpu::UsesNativeAbi() const {
+ for (const auto& [fd, session_id] : sessions) {
+ const auto* session = core.GetSession(session_id);
+ if (session && session->process && session->process->UsesPAuthReturns()) { return true; }
+ }
+ return false;
+}
+
} // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_gpu.h
index 70a6b1bd68..8219e4afea 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.h
@@ -7,6 +7,8 @@
#pragma once
#include <memory>
+#include <mutex>
+#include <optional>
#include <vector>
#include "common/bit_field.h"
#include "common/common_funcs.h"
@@ -20,7 +22,10 @@ namespace Core::Memory {
class Memory;
}
+namespace Kernel { class KProcess; }
+
namespace Tegra {
+struct NativeMacroContext;
namespace Control {
struct ChannelState;
}
@@ -58,9 +63,12 @@ public:
void OnClose(DeviceFD fd) override;
Kernel::KEvent* QueryEvent(u32 event_id) override;
+ NvResult BindToTsg(DeviceFD group, Kernel::KProcess* process, std::shared_ptr<Tegra::NativeMacroContext> macros);
+ void UnbindFromTsg(DeviceFD group);
private:
friend class nvhost_as_gpu;
+ bool UsesNativeAbi() const;
enum class CtxClasses : u32_le {
Ctx2D = 0x902D,
Ctx3D = 0xB197,
@@ -218,6 +226,9 @@ private:
NvCore::NvMap& nvmap;
std::shared_ptr<Tegra::Control::ChannelState> channel_state;
::Common::unordered_map<DeviceFD, NvCore::SessionId> sessions;
+ std::mutex tsg_mutex;
+ std::optional<DeviceFD> tsg_group;
+ u32 native_submit_logs{};
u32 channel_syncpoint;
std::mutex channel_mutex;
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_tsg_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_tsg_gpu.cpp
new file mode 100644
index 0000000000..ae9d2a50f5
--- /dev/null
+++ b/src/core/hle/service/nvdrv/devices/nvhost_tsg_gpu.cpp
@@ -0,0 +1,85 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include "common/logging.h"
+#include "core/core.h"
+#include "video_core/macro.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/service/nvdrv/nvdrv.h"
+#include "core/hle/service/nvdrv/devices/nvhost_gpu.h"
+#include "core/hle/service/nvdrv/devices/nvhost_tsg_gpu.h"
+
+namespace Service::Nvidia::Devices {
+
+nvhost_tsg_gpu::nvhost_tsg_gpu(Core::System& system, Module& module_, NvCore::Container& core_)
+ : nvdevice{system, Type::GpuTsg}, native_macros{std::make_shared<Tegra::NativeMacroContext>()}, module{module_}, core{core_} { native_macros->engine.native_turing = true; native_macros->compute_engine.native_turing = true; }
+
+nvhost_tsg_gpu::~nvhost_tsg_gpu() { OnClose(0); }
+
+void nvhost_tsg_gpu::OnOpen(NvCore::SessionId id, DeviceFD fd) {
+ group_fd = fd;
+ if (const auto* session = core.GetSession(id)) {
+ process = session->process;
+ process->Open(system.Kernel());
+ }
+}
+
+void nvhost_tsg_gpu::OnClose(DeviceFD fd) {
+ for (const auto& [channel_fd, channel] : channels) { channel->UnbindFromTsg(group_fd); }
+ channels.clear();
+ if (process) {
+ process->Close(system.Kernel());
+ process = nullptr;
+ }
+}
+
+
+NvResult nvhost_tsg_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> input,
+ std::span<u8> output) {
+ std::array<u32, 8> words{};
+ if (!input.empty()) { std::memcpy(words.data(), input.data(), std::min(input.size(), sizeof(words))); }
+ LOG_DEBUG(Service_NVDRV, "native TSG ioctl={:#x}, input={}, output={}, words={:#x},{:#x},{:#x},{:#x},{:#x},{:#x},{:#x},{:#x}",
+ command.raw, input.size(), output.size(), words[0], words[1], words[2], words[3],
+ words[4], words[5], words[6], words[7]);
+ if (command.raw != 0xc0045401 && command.raw != 0xc0045402 && command.raw != 0xc018540b) { return NvResult::NotImplemented; }
+ const size_t packet_size = command.raw == 0xc018540b ? 24 : 4;
+ if (!process || input.size() != packet_size || output.size() != packet_size) {
+ return NvResult::BadParameter;
+ }
+ if (packet_size == 24 && (words[1] >= 64 || words[2] || words[3] || words[4] || words[5])) {
+ return NvResult::BadParameter;
+ }
+ const DeviceFD channel_fd = static_cast<s32>(words[0]);
+ if (command.cmd == 1 || command.cmd == 11) {
+ if (channels.contains(channel_fd)) { return NvResult::Busy; }
+ if (channels.size() >= 64) { return NvResult::InsufficientMemory; }
+ const auto device = module.GetDevice<nvdevice>(channel_fd);
+ if (!device || device->GetType() != Type::GpuChannel) { return NvResult::BadParameter; }
+ const auto channel = std::static_pointer_cast<nvhost_gpu>(device);
+ const auto result = channel->BindToTsg(fd, process, native_macros);
+ if (result != NvResult::Success) { return result; }
+ channels.emplace(channel_fd, channel);
+ } else {
+ const auto it = channels.find(channel_fd);
+ if (it == channels.end()) { return NvResult::BadParameter; }
+ it->second->UnbindFromTsg(fd);
+ channels.erase(it);
+ }
+ std::copy(input.begin(), input.end(), output.begin());
+ return NvResult::Success;
+}
+
+NvResult nvhost_tsg_gpu::Ioctl2(DeviceFD fd, Ioctl command, std::span<const u8> input,
+ std::span<const u8> inline_input, std::span<u8> output) {
+ return NvResult::NotImplemented;
+}
+
+NvResult nvhost_tsg_gpu::Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8> input,
+ std::span<u8> output, std::span<u8> inline_output) {
+ return NvResult::NotImplemented;
+}
+
+} // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_tsg_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_tsg_gpu.h
new file mode 100644
index 0000000000..d64c5df3e0
--- /dev/null
+++ b/src/core/hle/service/nvdrv/devices/nvhost_tsg_gpu.h
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include "core/hle/service/nvdrv/devices/nvdevice.h"
+
+namespace Tegra { struct NativeMacroContext; }
+namespace Kernel { class KProcess; }
+namespace Service::Nvidia { class Module; }
+
+namespace Service::Nvidia::Devices {
+class nvhost_gpu;
+
+class nvhost_tsg_gpu final : public nvdevice {
+public:
+ nvhost_tsg_gpu(Core::System& system, Module& module, NvCore::Container& core);
+ ~nvhost_tsg_gpu() override;
+ NvResult Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> input,
+ std::span<u8> output) override;
+ NvResult Ioctl2(DeviceFD fd, Ioctl command, std::span<const u8> input,
+ std::span<const u8> inline_input, std::span<u8> output) override;
+ NvResult Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8> input,
+ std::span<u8> output, std::span<u8> inline_output) override;
+ void OnOpen(NvCore::SessionId session, DeviceFD fd) override;
+ void OnClose(DeviceFD fd) override;
+private:
+ std::shared_ptr<Tegra::NativeMacroContext> native_macros;
+ Module& module;
+ NvCore::Container& core;
+ Kernel::KProcess* process{};
+ DeviceFD group_fd{-1};
+ std::map<DeviceFD, std::shared_ptr<nvhost_gpu>> channels;
+};
+
+} // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/nvdrv.cpp b/src/core/hle/service/nvdrv/nvdrv.cpp
index b252a2efa5..9873d07c24 100644
--- a/src/core/hle/service/nvdrv/nvdrv.cpp
+++ b/src/core/hle/service/nvdrv/nvdrv.cpp
@@ -18,6 +18,8 @@
#include "core/hle/service/nvdrv/devices/nvhost_ctrl.h"
#include "core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.h"
#include "core/hle/service/nvdrv/devices/nvhost_gpu.h"
+#include "core/hle/service/nvdrv/devices/nvhost_tsg_gpu.h"
+#include "core/hle/kernel/k_process.h"
#include "core/hle/service/nvdrv/devices/nvhost_nvdec.h"
#include "core/hle/service/nvdrv/devices/nvhost_nvdec_common.h"
#include "core/hle/service/nvdrv/devices/nvhost_nvjpg.h"
@@ -133,6 +135,10 @@ Module::Module(Core::System& system)
auto device = std::make_shared<Devices::nvhost_gpu>(system, events_interface, container);
return open_files.emplace(fd, std::move(device)).first;
};
+ builders["/dev/nvhost-tsg-gpu"] = [this, &system](DeviceFD fd) {
+ auto device = std::make_shared<Devices::nvhost_tsg_gpu>(system, *this, container);
+ return open_files.emplace(fd, std::move(device)).first;
+ };
builders["/dev/nvhost-ctrl-gpu"] = [this, &system](DeviceFD fd) {
auto device = std::make_shared<Devices::nvhost_ctrl_gpu>(system, events_interface);
return open_files.emplace(fd, std::move(device)).first;
@@ -163,7 +169,12 @@ Module::Module(Core::System& system)
};
}
-Module::~Module() {}
+Module::~Module() {
+ // Devices (including TSG-retained channels) release events and core state.
+ // Destroy them while those module members are still alive.
+ for (const auto& [fd, device] : open_files) { device->OnClose(fd); }
+ open_files.clear();
+}
NvResult Module::VerifyFD(DeviceFD fd) const {
if (fd < 0) {
@@ -180,6 +191,12 @@ NvResult Module::VerifyFD(DeviceFD fd) const {
}
DeviceFD Module::Open(const std::string& device_name, NvCore::SessionId session_id) {
+ if (device_name == "/dev/nvhost-tsg-gpu") {
+ const auto* session = container.GetSession(session_id);
+ if (!session || !session->process || !session->process->UsesPAuthReturns()) {
+ return INVALID_NVDRV_FD;
+ }
+ }
auto it = builders.find(device_name);
if (it == builders.end()) {
LOG_ERROR(Service_NVDRV, "Trying to open unknown device {}", device_name);
diff --git a/src/core/hle/service/nvdrv/nvdrv_interface.cpp b/src/core/hle/service/nvdrv/nvdrv_interface.cpp
index 5fae68adbb..3f81720a39 100644
--- a/src/core/hle/service/nvdrv/nvdrv_interface.cpp
+++ b/src/core/hle/service/nvdrv/nvdrv_interface.cpp
@@ -72,6 +72,10 @@ void NVDRV::Ioctl1(HLERequestContext& ctx) {
const auto input_buffer = ctx.ReadBuffer(0);
const auto nv_result = nvdrv->Ioctl1(fd, command, input_buffer, output_buffer);
+ if (nv_result != NvResult::Success && ctx.GetThread().GetOwnerProcess()->UsesPAuthReturns()) {
+ LOG_DEBUG(Service_NVDRV, "native ioctl failure fd={}, ioctl={:#x}, result={}, caller={:#x}",
+ fd, command.raw, static_cast<u32>(nv_result), ctx.GetThread().GetContext().lr);
+ }
if (command.is_out != 0) {
ctx.WriteBuffer(output_buffer);
}
@@ -99,6 +103,10 @@ void NVDRV::Ioctl2(HLERequestContext& ctx) {
const auto nv_result =
nvdrv->Ioctl2(fd, command, input_buffer, input_inlined_buffer, output_buffer);
+ if (nv_result != NvResult::Success && ctx.GetThread().GetOwnerProcess()->UsesPAuthReturns()) {
+ LOG_DEBUG(Service_NVDRV, "native ioctl failure fd={}, ioctl={:#x}, result={}, caller={:#x}",
+ fd, command.raw, static_cast<u32>(nv_result), ctx.GetThread().GetContext().lr);
+ }
if (command.is_out != 0) {
ctx.WriteBuffer(output_buffer);
}
@@ -126,6 +134,11 @@ void NVDRV::Ioctl3(HLERequestContext& ctx) {
const auto nv_result =
nvdrv->Ioctl3(fd, command, input_buffer, output_buffer, inline_output_buffer);
+ if (command.raw == 0xc0e04705) {
+ LOG_DEBUG(Service_NVDRV, "native characteristics input={}, output={}, inline={}, result={}",
+ input_buffer.size(), output_buffer.size(), inline_output_buffer.size(),
+ static_cast<u32>(nv_result));
+ }
if (command.is_out != 0) {
ctx.WriteBuffer(output_buffer, 0);
ctx.WriteBuffer(inline_output_buffer, 1);
@@ -185,6 +198,10 @@ void NVDRV::QueryEvent(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto fd = rp.Pop<DeviceFD>();
const auto event_id = rp.Pop<u32>();
+ if (ctx.GetThread().GetOwnerProcess()->UsesPAuthReturns()) {
+ LOG_DEBUG(Service_NVDRV, "native QueryEvent fd={}, id={}, caller={:#x}", fd, event_id,
+ ctx.GetThread().GetContext().lr);
+ }
if (!is_initialized) {
ServiceError(ctx, NvResult::NotInitialized);
@@ -265,6 +282,14 @@ NVDRV::NVDRV(Core::System& system_, std::shared_ptr<Module> nvdrv_, const char*
RegisterHandlers(functions);
}
+NVDRV::NVDRV(Core::System& system, std::shared_ptr<Module> module, const char* name,
+ Kernel::KProcess* process, u64 aruid)
+ : NVDRV{system, std::move(module), name} {
+ session_id = nvdrv->GetContainer().OpenSession(process);
+ pid = aruid;
+ is_initialized = true;
+}
+
NVDRV::~NVDRV() {
if (is_initialized) {
auto& container = nvdrv->GetContainer();
diff --git a/src/core/hle/service/nvdrv/nvdrv_interface.h b/src/core/hle/service/nvdrv/nvdrv_interface.h
index f628fca7df..d6fe4c18f5 100644
--- a/src/core/hle/service/nvdrv/nvdrv_interface.h
+++ b/src/core/hle/service/nvdrv/nvdrv_interface.h
@@ -17,6 +17,8 @@ namespace Service::Nvidia {
class NVDRV final : public ServiceFramework<NVDRV> {
public:
explicit NVDRV(Core::System& system_, std::shared_ptr<Module> nvdrv_, const char* name);
+ explicit NVDRV(Core::System& system, std::shared_ptr<Module> module, const char* name,
+ Kernel::KProcess* process, u64 aruid);
~NVDRV() override;
std::shared_ptr<Module> GetModule() const {
diff --git a/src/core/hle/service/nvnflinger/buffer_queue_producer.cpp b/src/core/hle/service/nvnflinger/buffer_queue_producer.cpp
index c3eb2796e3..9de8abaeb6 100644
--- a/src/core/hle/service/nvnflinger/buffer_queue_producer.cpp
+++ b/src/core/hle/service/nvnflinger/buffer_queue_producer.cpp
@@ -393,6 +393,15 @@ Status BufferQueueProducer::DetachNextBuffer(std::shared_ptr<GraphicBuffer>* out
return Status::NoError;
}
+std::optional<Fence> BufferQueueProducer::GetReleasedBufferFence(s32 slot) {
+ const std::scoped_lock lock{core->mutex};
+ if (slot < 0 || static_cast<size_t>(slot) >= slots.size() ||
+ slots[slot].buffer_state != BufferState::Free) {
+ return std::nullopt;
+ }
+ return slots[slot].fence;
+}
+
Status BufferQueueProducer::AttachBuffer(s32* out_slot,
const std::shared_ptr<GraphicBuffer>& buffer) {
if (out_slot == nullptr) {
diff --git a/src/core/hle/service/nvnflinger/buffer_queue_producer.h b/src/core/hle/service/nvnflinger/buffer_queue_producer.h
index 697de0ac9b..bf815eb8d8 100644
--- a/src/core/hle/service/nvnflinger/buffer_queue_producer.h
+++ b/src/core/hle/service/nvnflinger/buffer_queue_producer.h
@@ -12,6 +12,7 @@
#include <condition_variable>
#include <memory>
#include <mutex>
+#include <optional>
#include "common/common_funcs.h"
#include "common/cpu_features.h"
@@ -57,6 +58,7 @@ public:
Kernel::KReadableEvent* GetNativeHandle(u32 type_id) override;
public:
+ std::optional<Fence> GetReleasedBufferFence(s32 slot);
Status RequestBuffer(s32 slot, std::shared_ptr<GraphicBuffer>* buf);
Status SetBufferCount(s32 buffer_count);
Status DequeueBuffer(s32* out_slot, android::Fence* out_fence, bool async, u32 width,
diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp
index b4fe2b3ed5..a7e7e10199 100644
--- a/src/core/hle/service/service.cpp
+++ b/src/core/hle/service/service.cpp
@@ -4,12 +4,16 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <chrono>
+#include <limits>
#include <fmt/ranges.h>
#include "common/assert.h"
#include "common/logging.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/hle/ipc.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/kernel/k_thread.h"
#include "core/hle/kernel/kernel.h"
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/service.h"
@@ -67,6 +71,26 @@ void ServiceFrameworkBase::ReportUnimplementedFunction(HLERequestContext& ctx,
fmt::format_to(std::back_inserter(buf), ", [{}]={:#x}", i, cmd_buf[i]);
buf.push_back('}');
+ if (ctx.GetThread().GetOwnerProcess()->UsesPAuthReturns()) {
+ const auto& context = ctx.GetThread().GetContext();
+ LOG_DEBUG(Service, "Unsupported IPC caller pc={:#x}, lr={:#x}, sp={:#x}, fp={:#x}",
+ context.pc, context.lr, context.sp, context.fp);
+ u64 frame = context.fp;
+ for (u32 depth = 0; depth < 8 && frame != 0; ++depth) {
+ if (frame > (std::numeric_limits<u64>::max)() - 15 ||
+ !ctx.GetMemory().IsValidVirtualAddress(frame) ||
+ !ctx.GetMemory().IsValidVirtualAddress(frame + 15)) {
+ break;
+ }
+ const u64 next = ctx.GetMemory().Read64(frame);
+ LOG_DEBUG(Service, "Unsupported IPC frame {}: return={:#x}", depth,
+ ctx.GetMemory().Read64(frame + 8));
+ if (next <= frame) {
+ break;
+ }
+ frame = next;
+ }
+ }
system.GetReporter().SaveUnimplementedFunctionReport(ctx, ctx.GetCommand(), function_name, service_name);
UNIMPLEMENTED_MSG("Unknown / unimplemented {}", fmt::to_string(buf));
if (Settings::values.use_auto_stub) {
@@ -82,7 +106,7 @@ void ServiceFrameworkBase::InvokeRequest(HLERequestContext& ctx) {
if (info == nullptr || info->handler_callback == nullptr)
return ReportUnimplementedFunction(ctx, info);
- LOG_TRACE(Service, "{}", MakeFunctionString(info->name, GetServiceName(), ctx.CommandBuffer()));
+ LOG_DEBUG(Service, "{}", MakeFunctionString(info->name, GetServiceName(), ctx.CommandBuffer()));
handler_invoker(this, info->handler_callback, ctx);
}
@@ -92,7 +116,7 @@ void ServiceFrameworkBase::InvokeRequestTipc(HLERequestContext& ctx) {
if (info == nullptr || info->handler_callback == nullptr)
return ReportUnimplementedFunction(ctx, info);
- LOG_TRACE(Service, "{}", MakeFunctionString(info->name, GetServiceName(), ctx.CommandBuffer()));
+ LOG_DEBUG(Service, "{}", MakeFunctionString(info->name, GetServiceName(), ctx.CommandBuffer()));
handler_invoker(this, info->handler_callback, ctx);
}
diff --git a/src/core/launch_params.cpp b/src/core/launch_params.cpp
index a0483226ec..814a2cb553 100644
--- a/src/core/launch_params.cpp
+++ b/src/core/launch_params.cpp
@@ -94,7 +94,7 @@ LaunchParams ParseLaunchParams(Core::System& system, int argc, char *argv[], wch
break;
case 'u': {
// Launch game with a specific user
- bool argument_ok = isdigit(optarg[0]);
+ bool argument_ok = isdigit(static_cast<unsigned char>(optarg[0])) != 0;
p.selected_user = atoi(optarg);
if (!argument_ok) {
// try to look it up by username, only finds the first username that matches.
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp
index 5e2ec7e0b9..1dce245f7f 100644
--- a/src/core/loader/deconstructed_rom_directory.cpp
+++ b/src/core/loader/deconstructed_rom_directory.cpp
@@ -13,6 +13,7 @@
#include "core/file_sys/control_metadata.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/romfs_factory.h"
+#include "core/file_sys/romfs.h"
#include "core/hle/kernel/k_page_table.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/service/filesystem/filesystem.h"
@@ -129,10 +130,14 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(
: AppLoader(directory->GetFile("main"))
, dir(std::move(directory))
, override_update(override_update_)
+ , standalone(false)
{}
FileType AppLoader_DeconstructedRomDirectory::IdentifyType(const FileSys::VirtualFile& dir_file) {
- if (FileSys::IsDirectoryExeFS(dir_file->GetContainingDirectory())) {
+ const auto directory = dir_file->GetContainingDirectory();
+ if (dir_file->GetName() == "main" && directory &&
+ AppLoader_NSO::IdentifyType(dir_file) == FileType::NSO &&
+ FileSys::IsDirectoryExeFS(directory)) {
return FileType::DeconstructedRomDirectory;
}
@@ -182,11 +187,11 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
}
metadata.Print();
- // Enable NCE only for applications with 39-bit address space.
+ // Extended ACID programs currently require the Dynarmic compatibility path.
const bool is_39bit =
metadata.GetAddressSpaceType() == FileSys::ProgramAddressSpaceType::Is39Bit;
const bool is_application = metadata.GetPoolPartition() == FileSys::PoolPartition::Application;
- Settings::SetNceEnabled(is_39bit);
+ Settings::SetNceEnabled(is_39bit && !metadata.UsesExtendedAcid());
const std::array static_modules = {"rtld", "main", "subsdk0", "subsdk1", "subsdk2",
"subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7",
@@ -267,11 +272,39 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
}
is_loaded = true;
+ if (standalone) {
+ title_id = metadata.GetTitleID();
+ system.GetFileSystemController().RegisterProcess(
+ process.GetProcessId(), {},
+ std::make_shared<FileSys::RomFSFactory>(*this, system.GetContentProvider(),
+ system.GetFileSystemController()));
+ }
return {ResultStatus::Success,
LoadParameters{metadata.GetMainThreadPriority(), metadata.GetMainThreadStackSize()}};
}
ResultStatus AppLoader_DeconstructedRomDirectory::ReadRomFS(FileSys::VirtualFile& out_dir) {
+ if (romfs == nullptr && standalone) {
+ const auto directory = file->GetContainingDirectory();
+ for (const auto& entry : directory->GetFiles()) {
+ if (entry->GetExtension() == "romfs") {
+ romfs = entry;
+ break;
+ }
+ }
+ if (romfs == nullptr) {
+ auto loose = directory->GetSubdirectory("romfs");
+ if (!loose && directory->GetName() == "exefs") {
+ const auto parent = directory->GetParentDirectory();
+ if (parent) {
+ loose = parent->GetSubdirectory("romfs");
+ }
+ }
+ if (loose) {
+ romfs = FileSys::CreateRomFS(std::move(loose));
+ }
+ }
+ }
if (romfs == nullptr) {
return ResultStatus::ErrorNoRomFS;
}
diff --git a/src/core/loader/deconstructed_rom_directory.h b/src/core/loader/deconstructed_rom_directory.h
index 7283db0662..aba89935a3 100644
--- a/src/core/loader/deconstructed_rom_directory.h
+++ b/src/core/loader/deconstructed_rom_directory.h
@@ -62,6 +62,7 @@ private:
std::string name;
u64 title_id{};
bool override_update;
+ bool standalone{true};
Modules modules;
};
diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp
index 92e2e2f602..d802d15e9f 100644
--- a/src/core/loader/loader.cpp
+++ b/src/core/loader/loader.cpp
@@ -125,6 +125,10 @@ bool HasApplicationProgramContent(const std::shared_ptr<FileSys::NSP>& nsp) {
} // namespace
FileType IdentifyFile(FileSys::VirtualFile file) {
+ if (const auto romdir_type =
+ IdentifyFileLoader<AppLoader_DeconstructedRomDirectory>(file)) {
+ return *romdir_type;
+ }
if (const auto nsp_type = IdentifyFileLoader<AppLoader_NSP>(file)) {
return *nsp_type;
} else if (const auto xci_type = IdentifyFileLoader<AppLoader_XCI>(file)) {
@@ -139,9 +143,6 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
return *kip_type;
} else if (const auto nso_type = IdentifyFileLoader<AppLoader_NSO>(file)) {
return *nso_type;
- } else if (const auto romdir_type =
- IdentifyFileLoader<AppLoader_DeconstructedRomDirectory>(file)) {
- return *romdir_type;
} else {
return FileType::Unknown;
}