From 4a0bc1497cad1e8a903534118fdd291e374bf308 Mon Sep 17 00:00:00 2001 From: Bruno <56844494+fab918@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:17:36 +0200 Subject: [PATCH] Application data handling (#392) Return checked application metadata and icons, handle early controller disconnects, and register limited StreamPlay interfaces for firmware 23 or newer. NS and StreamPlay compatibility support adapted from Eden. Preserve its copyright and license notices. --- .../core/file_sys/control_metadata.cpp | 4 +- .../core/file_sys/control_metadata.h | 2 + src/nxemu-loader/system_loader.cpp | 20 +++ src/nxemu-loader/system_loader.h | 2 + .../application_control_data.h | 44 +++++++ src/nxemu-module-spec/base.h | 6 +- src/nxemu-module-spec/system_loader.h | 2 + src/nxemu-os/core/hle/service/ngc/ngc.cpp | 2 + .../core/hle/service/ngc/streamplay.cpp | 80 ++++++++++++ .../core/hle/service/ngc/streamplay.h | 70 +++++++++++ .../service/ns/application_control_data.cpp | 114 ++++++++++++++++++ .../hle/service/ns/application_control_data.h | 25 ++++ .../ns/application_manager_interface.cpp | 6 +- .../ns/application_manager_interface.h | 1 + ...nly_application_control_data_interface.cpp | 114 +++++++++++++++++- ..._only_application_control_data_interface.h | 12 ++ src/nxemu-os/nxemu-os.vcxproj | 4 + src/nxemu-os/nxemu-os.vcxproj.filters | 12 ++ src/yuzu_hid_core/resources/npad/npad.cpp | 16 ++- 19 files changed, 527 insertions(+), 9 deletions(-) create mode 100644 src/nxemu-module-spec/application_control_data.h create mode 100644 src/nxemu-os/core/hle/service/ngc/streamplay.cpp create mode 100644 src/nxemu-os/core/hle/service/ngc/streamplay.h create mode 100644 src/nxemu-os/core/hle/service/ns/application_control_data.cpp create mode 100644 src/nxemu-os/core/hle/service/ns/application_control_data.h diff --git a/src/nxemu-loader/core/file_sys/control_metadata.cpp b/src/nxemu-loader/core/file_sys/control_metadata.cpp index eac03e7d..4330f2f1 100644 --- a/src/nxemu-loader/core/file_sys/control_metadata.cpp +++ b/src/nxemu-loader/core/file_sys/control_metadata.cpp @@ -69,7 +69,8 @@ NACP::NACP() = default; NACP::NACP(VirtualFile file) { - file->ReadObject(&raw); + valid = file != nullptr && file->GetSize() == sizeof(raw) && + file->ReadObject(&raw) == sizeof(raw); version = Common::StringFromFixedZeroTerminatedBuffer(raw.version_string.data(), raw.version_string.size()); } @@ -80,6 +81,7 @@ NACP & NACP::operator=(const NACP & other) if (this != &other) { raw = other.raw; + valid = other.valid; version = other.version; developer = other.developer; } diff --git a/src/nxemu-loader/core/file_sys/control_metadata.h b/src/nxemu-loader/core/file_sys/control_metadata.h index eaa5abf5..5b1e8e70 100644 --- a/src/nxemu-loader/core/file_sys/control_metadata.h +++ b/src/nxemu-loader/core/file_sys/control_metadata.h @@ -113,6 +113,7 @@ public: uint64_t GetDefaultNormalSaveSize() const; uint64_t GetDefaultJournalSaveSize() const; std::vector GetRawBytes() const; + bool IsValid() const { return valid; } uint64_t GetDeviceSaveDataSize() const; //IFileSysNACP @@ -130,6 +131,7 @@ private: std::string version; mutable std::string developer; RawNACP raw{}; + bool valid{}; }; } // namespace FileSys diff --git a/src/nxemu-loader/system_loader.cpp b/src/nxemu-loader/system_loader.cpp index de3c6cf9..af9958d5 100644 --- a/src/nxemu-loader/system_loader.cpp +++ b/src/nxemu-loader/system_loader.cpp @@ -1,4 +1,5 @@ #include "system_loader.h" +#include #include "core/core.h" #include "core/file_sys/card_image.h" #include "core/file_sys/common_funcs.h" @@ -818,6 +819,25 @@ IFileSysNACP * Systemloader::GetPMControlMetadata(uint64_t programID) return metadata.first.release(); } +LoaderResultStatus Systemloader::GetPMControlData(uint64_t program_id, uint8_t * buffer, + uint32_t buffer_size, uint32_t & actual_size) +{ + actual_size = 0; + const FileSys::PatchManager pm(program_id, GetFileSystemController(), GetContentProvider()); + const auto metadata = pm.GetControlMetadata(); + if (metadata.first == nullptr || !metadata.first->IsValid()) + { + return LoaderResultStatus::ErrorNoControl; + } + static_assert(sizeof(FileSys::RawNACP) == ApplicationControlData::NacpSize); + const auto nacp = metadata.first->GetRawBytes(); + const auto & icon = metadata.second; + return ApplicationControlData::Read(nacp, icon != nullptr ? icon->GetSize() : 0, + [&icon](uint8_t * destination, uint64_t size) { + return icon->Read(destination, static_cast(size), 0); + }, buffer, buffer_size, actual_size); +} + IManualContentProvider & Systemloader::ManualContentProvider() { return *(impl->m_manualContentProvider.get()); diff --git a/src/nxemu-loader/system_loader.h b/src/nxemu-loader/system_loader.h index bd495a26..278fcde0 100644 --- a/src/nxemu-loader/system_loader.h +++ b/src/nxemu-loader/system_loader.h @@ -49,6 +49,8 @@ public: bool InstallFirmwarePackage(const char * utf8_path) override; void SetDisabledAddons(uint64_t program_id, const char * const * names, uint32_t count) override; bool IsAddonDisabled(uint64_t program_id, const char * name) const override; + LoaderResultStatus GetPMControlData(uint64_t program_id, uint8_t * buffer, + uint32_t buffer_size, uint32_t & actual_size) override; private: Systemloader() = delete; diff --git a/src/nxemu-module-spec/application_control_data.h b/src/nxemu-module-spec/application_control_data.h new file mode 100644 index 00000000..b5ffb0c4 --- /dev/null +++ b/src/nxemu-module-spec/application_control_data.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include "system_loader.h" + +namespace ApplicationControlData { + +inline constexpr uint32_t NacpSize = 0x4000; +inline constexpr uint32_t MaxIconSize = 0x20000; +inline constexpr uint32_t MaxSize = NacpSize + MaxIconSize; + +// A null, zero-sized buffer queries the size. Other calls copy the complete payload. +// The reader must report the number of bytes actually read, never its requested size. +template +LoaderResultStatus Read(std::span nacp, uint64_t icon_size, + IconReader&& read_icon, uint8_t* buffer, uint32_t buffer_size, + uint32_t& actual_size) { + actual_size = 0; + if (nacp.size() != NacpSize) { + return LoaderResultStatus::ErrorNoControl; + } + if (icon_size > MaxIconSize) { + return LoaderResultStatus::ErrorNoIcon; + } + const auto size = NacpSize + static_cast(icon_size); + if (buffer == nullptr && buffer_size == 0) { + actual_size = size; + return LoaderResultStatus::Success; + } + if (buffer == nullptr || buffer_size < size) { + return LoaderResultStatus::ErrorBufferTooSmall; + } + if (icon_size != 0 && read_icon(buffer + NacpSize, icon_size) != icon_size) { + return LoaderResultStatus::ErrorNoIcon; + } + std::memcpy(buffer, nacp.data(), NacpSize); + actual_size = size; + return LoaderResultStatus::Success; +} + +} // namespace ApplicationControlData diff --git a/src/nxemu-module-spec/base.h b/src/nxemu-module-spec/base.h index 4080c3aa..86d01e1d 100644 --- a/src/nxemu-module-spec/base.h +++ b/src/nxemu-module-spec/base.h @@ -25,10 +25,10 @@ enum { - MODULE_LOADER_SPECS_VERSION = 0x0130, + MODULE_LOADER_SPECS_VERSION = 0x0131, MODULE_VIDEO_SPECS_VERSION = 0x0123, MODULE_CPU_SPECS_VERSION = 0x011d, - MODULE_OPERATING_SYSTEM_SPECS_VERSION = 0x0124, + MODULE_OPERATING_SYSTEM_SPECS_VERSION = 0x0126, }; enum MODULE_TYPE : uint16_t @@ -356,4 +356,4 @@ Purpose: Called when emulation is saving settings Input: None Output: None */ -EXPORT void CALL FlushSettings(); \ No newline at end of file +EXPORT void CALL FlushSettings(); diff --git a/src/nxemu-module-spec/system_loader.h b/src/nxemu-module-spec/system_loader.h index 8cf3aa98..ed87040a 100644 --- a/src/nxemu-module-spec/system_loader.h +++ b/src/nxemu-module-spec/system_loader.h @@ -369,6 +369,8 @@ nxinterface ISystemloader virtual bool InstallFirmwarePackage(const char * utf8_path) = 0; virtual void SetDisabledAddons(uint64_t program_id, const char * const * names, uint32_t count) = 0; virtual bool IsAddonDisabled(uint64_t program_id, const char * name) const = 0; + virtual LoaderResultStatus GetPMControlData(uint64_t program_id, uint8_t * buffer, + uint32_t buffer_size, uint32_t & actual_size) = 0; }; EXPORT ISystemloader * CALL CreateSystemLoader(ISystemModules & modules); diff --git a/src/nxemu-os/core/hle/service/ngc/ngc.cpp b/src/nxemu-os/core/hle/service/ngc/ngc.cpp index 8c722388..5fea739c 100644 --- a/src/nxemu-os/core/hle/service/ngc/ngc.cpp +++ b/src/nxemu-os/core/hle/service/ngc/ngc.cpp @@ -5,6 +5,7 @@ #include "core/core.h" #include "core/hle/service/ipc_helpers.h" #include "core/hle/service/ngc/ngc.h" +#include "core/hle/service/ngc/streamplay.h" #include "core/hle/service/server_manager.h" #include "core/hle/service/service.h" @@ -144,6 +145,7 @@ void LoopProcess(Core::System& system) { server_manager->RegisterNamedService("ngct:u", std::make_shared(system)); server_manager->RegisterNamedService("ngc:u", std::make_shared(system)); + RegisterStreamPlayServices(*server_manager, system); ServerManager::RunServer(std::move(server_manager)); } diff --git a/src/nxemu-os/core/hle/service/ngc/streamplay.cpp b/src/nxemu-os/core/hle/service/ngc/streamplay.cpp new file mode 100644 index 00000000..1a8e2ac1 --- /dev/null +++ b/src/nxemu-os/core/hle/service/ngc/streamplay.cpp @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// Service names, scoped interfaces and command tables are adapted from Eden. +// Unsupported operations use error-only replies rather than reporting success. + +#include "core/hle/service/ngc/streamplay.h" +#include +#include +#include "core/core.h" +#include "core/hle/service/ipc_helpers.h" +#include "core/hle/service/server_manager.h" +#include "core/hle/service/service.h" + +namespace Service::NGC { +namespace { + +class StreamPlayShim final : public ServiceFramework { +public: + StreamPlayShim(Core::System& system_, bool system_scope) + : ServiceFramework{system_, system_scope ? "ISystemShimScopedObject" : "IUserShimScopedObject"} { + const std::span commands = system_scope + ? std::span{SystemShimCommands} + : std::span{UserShimCommands}; + for (const auto& command : commands) { + const FunctionInfo function{command.id, &StreamPlayShim::Unsupported, command.name}; + RegisterHandlers(&function, 1); + } + } + +private: + void Unsupported(HLERequestContext& ctx) { + LOG_WARNING(Service_NGC, "StreamPlay operation unavailable: {} command={}", + GetServiceName(), ctx.GetCommand()); + // No save handles, writes, events or asynchronous objects exist yet. An + // error-only response must not imply a successful read or durable write. + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(Result{StreamPlayNotSupported}); + } +}; + +class StreamPlayService final : public ServiceFramework { +public: + StreamPlayService(Core::System& system_, bool system_scope_) + : ServiceFramework{system_, system_scope_ ? "stpl:sys" : "stpl:u"}, + system_scope{system_scope_} { + static const FunctionInfo functions[]{ + {0, &StreamPlayService::CreateShim, "CreateShimScopedObject"}, + }; + RegisterHandlers(functions); + } + +private: + void CreateShim(HLERequestContext& ctx) { + if (!system_scope) { + IPC::RequestParser parser{ctx}; + const auto mode = parser.Pop(); + LOG_DEBUG(Service_NGC, "StreamPlay user shim requested, mode={:#x}", mode); + } + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface(system, system_scope); + } + + bool system_scope; +}; + +} // namespace + +void RegisterStreamPlayServices(ServerManager& manager, Core::System& system) { + char version[32]{}; + if (system.GetSystemloader().GetInstalledFirmwareDisplayVersion(version, sizeof(version)) == 0 || + !IsStreamPlayFirmwareSupported(version)) { + return; + } + manager.RegisterNamedService("stpl:u", std::make_shared(system, false), 4); + manager.RegisterNamedService("stpl:sys", std::make_shared(system, true), 4); +} + +} // namespace Service::NGC diff --git a/src/nxemu-os/core/hle/service/ngc/streamplay.h b/src/nxemu-os/core/hle/service/ngc/streamplay.h new file mode 100644 index 00000000..6abb7084 --- /dev/null +++ b/src/nxemu-os/core/hle/service/ngc/streamplay.h @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +namespace Core { class System; } +namespace Service { class ServerManager; } + +namespace Service::NGC { + +// SF/CMIF NotSupported (module 10, description 1), not HIPC (module 11). +inline constexpr uint32_t StreamPlayNotSupported = (1u << 9) | 10u; + +struct StreamPlayCommand { + uint32_t id; + const char* name; +}; + +inline constexpr std::array UserShimCommands{ + StreamPlayCommand{450, "InitializeForSaveData"}, + StreamPlayCommand{451, "FinalizeForSaveData"}, + StreamPlayCommand{452, "OpenSaveData"}, + StreamPlayCommand{453, "CloseSaveData"}, + StreamPlayCommand{454, "ReadSaveSlot"}, + StreamPlayCommand{455, "WriteSaveSlot"}, + StreamPlayCommand{456, "FlushSaveSlot"}, + StreamPlayCommand{457, "CommitSaveData"}, +}; + +inline constexpr std::array SystemShimCommands{ + StreamPlayCommand{106, "Cmd106"}, StreamPlayCommand{107, "Cmd107"}, + StreamPlayCommand{108, "Cmd108"}, StreamPlayCommand{207, "Cmd207"}, + StreamPlayCommand{208, "Cmd208"}, StreamPlayCommand{209, "Cmd209"}, + StreamPlayCommand{210, "Cmd210"}, StreamPlayCommand{211, "Cmd211"}, + StreamPlayCommand{212, "Cmd212"}, +}; + +inline bool IsStreamPlayFirmwareSupported(std::string_view version) { + if (version.empty()) { + return false; + } + std::array components{}; + const char* cursor = version.data(); + const char* end = cursor + version.size(); + for (size_t i = 0; i < components.size(); ++i) { + if (cursor == end || *cursor < '0' || *cursor > '9') { + return false; + } + const auto parsed = std::from_chars(cursor, end, components[i]); + if (parsed.ec != std::errc{}) { + return false; + } + cursor = parsed.ptr; + if (i + 1 != components.size()) { + if (cursor == end || *cursor++ != '.') { + return false; + } + } + } + return cursor == end && components[0] >= 23; +} + +void RegisterStreamPlayServices(ServerManager& manager, Core::System& system); + +} // namespace Service::NGC diff --git a/src/nxemu-os/core/hle/service/ns/application_control_data.cpp b/src/nxemu-os/core/hle/service/ns/application_control_data.cpp new file mode 100644 index 00000000..5c49d447 --- /dev/null +++ b/src/nxemu-os/core/hle/service/ns/application_control_data.cpp @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// Application-control reply flags and icon conversion are adapted from Eden. + +#include "core/hle/service/ns/application_control_data.h" + +#include +#include +#include +#include "yuzu_common/stb.h" + +namespace Service::NS { +namespace { + +constexpr int IconDimension = 174; +constexpr int MaxDecodedDimension = 1024; + +struct JpegOutput { + std::vector bytes; + bool overflow{}; +}; + +void WriteJpeg(void* context, void* bytes, int count) { + auto& output = *static_cast(context); + if (count < 0 || static_cast(count) > ApplicationControlData::MaxIconSize - output.bytes.size()) { + output.overflow = true; + return; + } + if (!output.overflow && count != 0) { + const auto* begin = static_cast(bytes); + output.bytes.insert(output.bytes.end(), begin, begin + count); + } +} + +bool ResizeIcon(std::span input, std::vector& output) { + // Restrict the decode before allocating pixels. Icons are small JPEG images. + constexpr uint8_t end_marker[]{0xff, 0xd9}; + if (input.size() < 4 || input[0] != 0xff || input[1] != 0xd8 || + std::search(input.begin() + 2, input.end(), std::begin(end_marker), std::end(end_marker)) == input.end()) { + return false; + } + int width{}, height{}, channels{}; + const auto size = static_cast(input.size()); + if (!stbi_info_from_memory(input.data(), size, &width, &height, &channels) || + width <= 0 || height <= 0 || width > MaxDecodedDimension || height > MaxDecodedDimension) { + return false; + } + std::unique_ptr pixels{ + stbi_load_from_memory(input.data(), size, &width, &height, &channels, STBI_rgb), + stbi_image_free}; + if (!pixels) { + return false; + } + if (width == IconDimension && height == IconDimension) { + output.assign(input.begin(), input.end()); + return true; + } + std::vector resized(IconDimension * IconDimension * STBI_rgb); + // RGB has no alpha channel. The final argument is a flags bitmask, not a filter enum. + if (!stbir_resize_uint8_srgb(pixels.get(), width, height, 0, resized.data(), + IconDimension, IconDimension, 0, STBI_rgb, + STBIR_ALPHA_CHANNEL_NONE, 0)) { + return false; + } + JpegOutput encoded; + if (!stbi_write_jpg_to_func(WriteJpeg, &encoded, IconDimension, IconDimension, + STBI_rgb, resized.data(), 90) || encoded.overflow || encoded.bytes.empty()) { + return false; + } + output = std::move(encoded.bytes); + return true; +} + +} // namespace + +LoaderResultStatus WriteApplicationControlData(std::span data, + std::span output, bool modern, bool resize_icon, ControlDataResponse& response) { + response = {}; + if (data.size() < ApplicationControlData::NacpSize || data.size() > ApplicationControlData::MaxSize) { + return LoaderResultStatus::ErrorNoControl; + } + auto icon = data.subspan(ApplicationControlData::NacpSize); + // The modern success flags follow Eden's existing compatibility contract. Do not + // return them when the icon is absent or its requested conversion failed. + if (modern && icon.empty()) { + return LoaderResultStatus::ErrorNoIcon; + } + std::vector resized; + if (resize_icon) { + if (!ResizeIcon(icon, resized)) { + return LoaderResultStatus::ErrorNoIcon; + } + icon = resized; + } + const auto size = ApplicationControlData::NacpSize + icon.size(); + if (output.size() < size) { + return LoaderResultStatus::ErrorBufferTooSmall; + } + // Validate everything before touching the guest's output. Never truncate JPEG data. + std::memcpy(output.data(), data.data(), ApplicationControlData::NacpSize); + if (!icon.empty()) { + std::memcpy(output.data() + ApplicationControlData::NacpSize, icon.data(), icon.size()); + } + std::fill(output.begin() + size, output.end(), 0); + response.actual_size = static_cast(size); + if (modern) { + response.flags_a = 0x10001; + response.flags_b = resize_icon ? 0x10001 : 0; + } + return LoaderResultStatus::Success; +} + +} // namespace Service::NS diff --git a/src/nxemu-os/core/hle/service/ns/application_control_data.h b/src/nxemu-os/core/hle/service/ns/application_control_data.h new file mode 100644 index 00000000..2eccc5bb --- /dev/null +++ b/src/nxemu-os/core/hle/service/ns/application_control_data.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace Service::NS { + +struct ControlDataResponse { + uint32_t flags_a{}; + uint32_t flags_b{}; + uint32_t actual_size{}; +}; + +// Only the installed application's primary control-data entry is available. +constexpr bool IsControlDataRequestSupported(uint8_t source, uint8_t resize, uint8_t index) { + return source <= 2 && resize <= 1 && index == 0; +} + +LoaderResultStatus WriteApplicationControlData(std::span data, + std::span output, bool modern, bool resize_icon, ControlDataResponse& response); + +} // namespace Service::NS diff --git a/src/nxemu-os/core/hle/service/ns/application_manager_interface.cpp b/src/nxemu-os/core/hle/service/ns/application_manager_interface.cpp index 695b9dbd..7d75e6aa 100644 --- a/src/nxemu-os/core/hle/service/ns/application_manager_interface.cpp +++ b/src/nxemu-os/core/hle/service/ns/application_manager_interface.cpp @@ -109,7 +109,7 @@ IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_ {305, nullptr, "TerminateSystemApplet"}, {306, nullptr, "LaunchOverlayApplet"}, {307, nullptr, "TerminateOverlayApplet"}, - {400, D<&IApplicationManagerInterface::GetApplicationControlData>, "GetApplicationControlData"}, + {400, &IApplicationManagerInterface::GetApplicationControlDataRequest, "GetApplicationControlData"}, {401, nullptr, "InvalidateAllApplicationControlCache"}, {402, nullptr, "RequestDownloadApplicationControlData"}, {403, nullptr, "GetMaxApplicationControlCacheCount"}, @@ -305,6 +305,10 @@ IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_ IApplicationManagerInterface::~IApplicationManagerInterface() = default; +void IApplicationManagerInterface::GetApplicationControlDataRequest(HLERequestContext& ctx) { + IReadOnlyApplicationControlDataInterface(system).GetApplicationControlDataRequest(ctx); +} + Result IApplicationManagerInterface::GetApplicationControlData( OutBuffer out_buffer, Out out_actual_size, ApplicationControlSource application_control_source, u64 application_id) { diff --git a/src/nxemu-os/core/hle/service/ns/application_manager_interface.h b/src/nxemu-os/core/hle/service/ns/application_manager_interface.h index b608e441..828d8f83 100644 --- a/src/nxemu-os/core/hle/service/ns/application_manager_interface.h +++ b/src/nxemu-os/core/hle/service/ns/application_manager_interface.h @@ -15,6 +15,7 @@ class IApplicationManagerInterface final : public ServiceFramework out_buffer, Out out_actual_size, diff --git a/src/nxemu-os/core/hle/service/ns/read_only_application_control_data_interface.cpp b/src/nxemu-os/core/hle/service/ns/read_only_application_control_data_interface.cpp index b6c0a440..bb727af9 100644 --- a/src/nxemu-os/core/hle/service/ns/read_only_application_control_data_interface.cpp +++ b/src/nxemu-os/core/hle/service/ns/read_only_application_control_data_interface.cpp @@ -1,8 +1,17 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "core/hle/service/ns/read_only_application_control_data_interface.h" +#include +#include +#include +#include "core/core.h" +#include "core/hle/service/ns/application_control_data.h" #include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ipc_helpers.h" #include "core/hle/service/ns/language.h" #include "core/hle/service/ns/ns_results.h" #include "core/hle/service/set/settings_server.h" @@ -11,16 +20,35 @@ namespace Service::NS { +namespace { +Result ReadControlData(Core::System& system, u64 application_id, std::vector& data) { + data.resize(ApplicationControlData::MaxSize); + u32 actual_size{}; + const auto status = system.GetSystemloader().GetPMControlData(application_id, data.data(), + static_cast(data.size()), actual_size); + if (status != LoaderResultStatus::Success || actual_size < ApplicationControlData::NacpSize || + actual_size > data.size()) { + LOG_ERROR(Service_NS, "Control-data read failed: application_id={:016X}, status={}, size={:#x}", + application_id, static_cast(status), actual_size); + R_THROW(ResultUnknown); + } + data.resize(actual_size); + R_SUCCEED(); +} +} // namespace + IReadOnlyApplicationControlDataInterface::IReadOnlyApplicationControlDataInterface(Core::System & system_) : ServiceFramework{system_, "IReadOnlyApplicationControlDataInterface"} { // clang-format off static const FunctionInfo functions[] = { - {0, D<&IReadOnlyApplicationControlDataInterface::GetApplicationControlData>, "GetApplicationControlData"}, + {0, &IReadOnlyApplicationControlDataInterface::GetApplicationControlDataRequest, "GetApplicationControlData"}, {1, D<&IReadOnlyApplicationControlDataInterface::GetApplicationDesiredLanguage>, "GetApplicationDesiredLanguage"}, {2, D<&IReadOnlyApplicationControlDataInterface::ConvertApplicationLanguageToLanguageCode>, "ConvertApplicationLanguageToLanguageCode"}, {3, nullptr, "ConvertLanguageCodeToApplicationLanguage"}, {4, nullptr, "SelectApplicationDesiredLanguage"}, + {19, &IReadOnlyApplicationControlDataInterface::GetApplicationControlData3Request, "GetApplicationControlData3"}, + {23, &IReadOnlyApplicationControlDataInterface::GetApplicationControlData3Request, "GetApplicationControlData3"}, }; // clang-format on @@ -29,11 +57,93 @@ IReadOnlyApplicationControlDataInterface::IReadOnlyApplicationControlDataInterfa IReadOnlyApplicationControlDataInterface::~IReadOnlyApplicationControlDataInterface() = default; +void IReadOnlyApplicationControlDataInterface::GetApplicationControlDataRequest(HLERequestContext& ctx) { + ReplyApplicationControlData(ctx, false); +} + +void IReadOnlyApplicationControlDataInterface::GetApplicationControlData3Request(HLERequestContext& ctx) { + ReplyApplicationControlData(ctx, true); +} + +void IReadOnlyApplicationControlDataInterface::ReplyApplicationControlData(HLERequestContext& ctx, bool modern) { + struct Input { + ApplicationControlSource source; + u8 resize_icon; + u8 index; + u8 padding[5]; + u64 application_id; + }; + static_assert(sizeof(Input) == 16 && offsetof(Input, application_id) == 8); + // The two flag bytes are padding in the legacy command and must be ignored there. + IPC::RequestParser parser{ctx}; + const auto input = parser.PopRaw(); + std::vector buffer(std::min(ctx.GetWriteBufferSize(), size_t{ApplicationControlData::MaxSize})); + ControlDataResponse response; + const auto output = OutBuffer{std::span{buffer}}; + const Result result = modern + ? GetApplicationControlData3(output, &response.flags_a, &response.flags_b, + &response.actual_size, input.source, input.resize_icon, input.index, input.application_id) + : GetApplicationControlData(output, &response.actual_size, input.source, input.application_id); + if (result.IsFailure()) { + // The generic CMIF wrapper writes its temporary output buffers even on failure. + // Send an error-only reply here, without copying any bytes to guest memory. + IPC::ResponseBuilder reply{ctx, 2}; + reply.Push(result); + return; + } + ctx.WriteBuffer(buffer); + IPC::ResponseBuilder reply{ctx, modern ? 5u : 3u}; + reply.Push(result); + if (modern) { + reply.Push(response.flags_a); + reply.Push(response.flags_b); + } + reply.Push(response.actual_size); +} + Result IReadOnlyApplicationControlDataInterface::GetApplicationControlData(OutBuffer out_buffer, Out out_actual_size, ApplicationControlSource application_control_source, u64 application_id) { LOG_INFO(Service_NS, "called with control_source={}, application_id={:016X}", application_control_source, application_id); - UNIMPLEMENTED(); + *out_actual_size = 0; + if (!IsControlDataRequestSupported(static_cast(application_control_source), 0, 0)) { + R_THROW(ResultUnknown); + } + std::vector data; + R_TRY(ReadControlData(system, application_id, data)); + ControlDataResponse response; + if (WriteApplicationControlData(data, out_buffer, false, false, response) != LoaderResultStatus::Success) { + R_THROW(ResultUnknown); + } + *out_actual_size = response.actual_size; + R_SUCCEED(); +} + +Result IReadOnlyApplicationControlDataInterface::GetApplicationControlData3( + OutBuffer out_buffer, Out out_flags_a, Out out_flags_b, + Out out_actual_size, ApplicationControlSource application_control_source, + u8 resize_icon, u8 control_data_index, u64 application_id) +{ + *out_flags_a = 0; + *out_flags_b = 0; + *out_actual_size = 0; + LOG_INFO(Service_NS, "called with control_source={}, resize={}, index={}, application_id={:016X}", + application_control_source, resize_icon, control_data_index, application_id); + if (!IsControlDataRequestSupported(static_cast(application_control_source), resize_icon, control_data_index)) { + R_THROW(ResultUnknown); + } + std::vector data; + R_TRY(ReadControlData(system, application_id, data)); + ControlDataResponse response; + const auto status = WriteApplicationControlData(data, out_buffer, true, resize_icon == 1, response); + if (status != LoaderResultStatus::Success) { + LOG_ERROR(Service_NS, "Control-data response failed: application_id={:016X}, status={}, buffer={:#x}", + application_id, static_cast(status), out_buffer.size()); + R_THROW(ResultUnknown); + } + *out_flags_a = response.flags_a; + *out_flags_b = response.flags_b; + *out_actual_size = response.actual_size; R_SUCCEED(); } diff --git a/src/nxemu-os/core/hle/service/ns/read_only_application_control_data_interface.h b/src/nxemu-os/core/hle/service/ns/read_only_application_control_data_interface.h index ac099435..7a6c67de 100644 --- a/src/nxemu-os/core/hle/service/ns/read_only_application_control_data_interface.h +++ b/src/nxemu-os/core/hle/service/ns/read_only_application_control_data_interface.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -15,6 +18,8 @@ class IReadOnlyApplicationControlDataInterface final public: explicit IReadOnlyApplicationControlDataInterface(Core::System& system_); ~IReadOnlyApplicationControlDataInterface() override; + void GetApplicationControlDataRequest(HLERequestContext& ctx); + void GetApplicationControlData3Request(HLERequestContext& ctx); public: Result GetApplicationControlData(OutBuffer out_buffer, @@ -25,6 +30,13 @@ public: u32 supported_languages); Result ConvertApplicationLanguageToLanguageCode(Out out_language_code, ApplicationLanguage application_language); + Result GetApplicationControlData3(OutBuffer out_buffer, + Out out_flags_a, Out out_flags_b, + Out out_actual_size, + ApplicationControlSource application_control_source, + u8 resize_icon, u8 control_data_index, u64 application_id); +private: + void ReplyApplicationControlData(HLERequestContext& ctx, bool modern); }; } // namespace Service::NS diff --git a/src/nxemu-os/nxemu-os.vcxproj b/src/nxemu-os/nxemu-os.vcxproj index c8708dfe..af78579b 100644 --- a/src/nxemu-os/nxemu-os.vcxproj +++ b/src/nxemu-os/nxemu-os.vcxproj @@ -426,6 +426,7 @@ + @@ -443,6 +444,7 @@ + @@ -932,6 +934,7 @@ + @@ -951,6 +954,7 @@ + diff --git a/src/nxemu-os/nxemu-os.vcxproj.filters b/src/nxemu-os/nxemu-os.vcxproj.filters index 4ab9da06..a20a9d21 100644 --- a/src/nxemu-os/nxemu-os.vcxproj.filters +++ b/src/nxemu-os/nxemu-os.vcxproj.filters @@ -1191,6 +1191,9 @@ Header Files\core\hle\service\ngc + + Header Files\core\hle\service\ngc + Header Files\core\hle\service\nim @@ -1248,6 +1251,9 @@ Header Files\core\hle\service\ns + + Header Files\core\hle\service\ns + Header Files\core\hle\service\ns @@ -2770,6 +2776,9 @@ Source Files\core\hle\service\ngc + + Source Files\core\hle\service\ngc + Source Files\core\hle\service\nim @@ -2830,6 +2839,9 @@ Source Files\core\hle\service\ns + + Source Files\core\hle\service\ns + Source Files\core\hle\service\ns diff --git a/src/yuzu_hid_core/resources/npad/npad.cpp b/src/yuzu_hid_core/resources/npad/npad.cpp index dd7eb4c7..9c76208b 100644 --- a/src/yuzu_hid_core/resources/npad/npad.cpp +++ b/src/yuzu_hid_core/resources/npad/npad.cpp @@ -886,12 +886,24 @@ Result NPad::DisconnectNpad(u64 aruid, NpadIdType npad_id) { return ResultInvalidNpadId; } + // Applications can disconnect unused pads before creating their HID resource. + // Do not fall back to another application's controller data in this case. + const auto aruid_index = applet_resource_holder.applet_resource->GetIndexFromAruid(aruid); + if (aruid_index >= AruidIndexMax) { + return ResultSuccess; + } + const auto* data = applet_resource_holder.applet_resource->GetAruidDataByIndex(aruid_index); + if (data == nullptr || !data->flag.is_assigned) { + return ResultSuccess; + } + LOG_DEBUG(Service_HID, "Npad disconnected {}", npad_id); - auto& controller = GetControllerFromNpadIdType(aruid, npad_id); + auto& controller = controller_data[aruid_index][NpadIdTypeToIndex(npad_id)]; auto* shared_memory = controller.shared_memory; if (!shared_memory) { - LOG_WARNING(Service_HID, "shared_memory is null for npad_id={}", npad_id); + LOG_DEBUG(Service_HID, "Npad resource is not active for aruid={:#x}, npad_id={}", aruid, + npad_id); return ResultSuccess; } // Don't reset shared_memory->assignment_mode this value is persistent