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