diff options
| author | Alex <78863999+Sssnowyyy@users.noreply.github.com> | 2026-10-31 10:54:52 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-10-31 15:54:52 +0100 |
| commit | ee2d55ee497c9467c8df82515165a21eac13fff5 (patch) | |
| tree | cf30cc700b791838380f6f3019d70ceae4250dd0 /src | |
| parent | 01423a2470ee8a604218c1a6d5a0be6769624078 (diff) | |
boot Pokopia past audio init (Switch 2 heap limits, aud:u 1, stpl:u) (#77)
* game_list: don't free the worker while it is processing events
OnPopulatingCompleted runs a nested event loop from inside
GameListWorker::ProcessEvents. If the directory watcher fires there, the
repopulate destroys the worker while ProcessEvents is still on the stack
and the next lock hits a freed mutex. Stop the worker synchronously but
free it with deleteLater, and stop ProcessEvents once it has been
replaced.
Assisted-by: Claude Opus 5.5 (code changes, debugging and commit message)
* Fix Switch 2 heap limits and game list reloads during emulation
- SetHeapSize rejected heaps of 8 GiB or more (Switch 1's MainMemorySizeMax),
so Switch 2 titles that size their heap from the 12 GiB layout (Pokopia
asks for 9.5 GiB) got ResultInvalidSize and aborted with 2001-0101. Allow
up to the memory the active layout provides.
- Grow the 39-bit heap region from 8 to 16 GiB so such a heap fits.
- Skip watcher-driven game list reloads while a game runs: Repopulate calls
CreateFactories, which replaces content providers the running game reads
(segfault in ContentProviderUnion::GetEntry). Stopping emulation already
repopulates the list.
Assisted-by: Claude Opus 5.5 (code changes, debugging and commit message)
* nx2: answer aud:u 1 with an IAudioOutManager
nn::audio::OpenAudioOut gets its manager from aud:u command 1 and calls
OpenAudioOut through slot 1 of the returned object. The auto stub
returned no object, so Pokopia jumped through a null vtable 5 s into boot
and never got past its first frame.
Assisted-by: Claude Opus 5.5 (code changes, debugging and commit message)
* nx2: add a stpl:u (stream play) stub with a user service
nn::streamplay::detail::GetUserService opens a session with stpl:u 0
and attaches a system event to the handle command 300 returns. The
generic missing-service stub returned no session, so Pokopia's first
nn::streamplay call jumped through null right after audio setup.
Assisted-by: Claude Opus 5.5 (code changes, debugging and commit message)
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/core/hle/kernel/k_address_space_info.cpp | 6 | ||||
| -rw-r--r-- | src/core/hle/kernel/svc/svc_physical_memory.cpp | 7 | ||||
| -rw-r--r-- | src/core/hle/service/audio/nx2_audio_user_client.h | 16 | ||||
| -rw-r--r-- | src/core/hle/service/nx2_streamplay.h | 65 | ||||
| -rw-r--r-- | src/core/hle/service/sm/sm.cpp | 9 | ||||
| -rw-r--r-- | src/qt_common/game_list/model.cpp | 23 | ||||
| -rw-r--r-- | src/qt_common/game_list/model.h | 1 | ||||
| -rw-r--r-- | src/qt_common/game_list/worker.cpp | 12 | ||||
| -rw-r--r-- | src/qt_common/game_list/worker.h | 3 |
10 files changed, 132 insertions, 11 deletions
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 56fa653738..0075838d49 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1036,6 +1036,7 @@ add_library(core STATIC hle/service/server_manager.h hle/service/service.cpp hle/service/service.h + hle/service/nx2_streamplay.h hle/service/nx2_stub_interface.h hle/service/nx2_gsv.h hle/service/services.cpp diff --git a/src/core/hle/kernel/k_address_space_info.cpp b/src/core/hle/kernel/k_address_space_info.cpp index 23258071e1..13a669cff5 100644 --- a/src/core/hle/kernel/k_address_space_info.cpp +++ b/src/core/hle/kernel/k_address_space_info.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -32,7 +35,8 @@ constexpr std::array<KAddressSpaceInfo, 13> AddressSpaceInfos{{ { .bit_width = 39, .address = 128_MiB , .size = 512_GiB - 128_MiB, .type = KAddressSpaceInfo::Type::Map39Bit, }, #endif { .bit_width = 39, .address = Size_Invalid, .size = 64_GiB , .type = KAddressSpaceInfo::Type::MapSmall }, - { .bit_width = 39, .address = Size_Invalid, .size = 8_GiB , .type = KAddressSpaceInfo::Type::Heap, }, + // Switch 2 titles size their heap from up to 12 GiB of memory, which doesn't fit Switch 1's 8 GiB. + { .bit_width = 39, .address = Size_Invalid, .size = 16_GiB , .type = KAddressSpaceInfo::Type::Heap, }, { .bit_width = 39, .address = Size_Invalid, .size = 64_GiB , .type = KAddressSpaceInfo::Type::Alias, }, { .bit_width = 39, .address = Size_Invalid, .size = 2_GiB , .type = KAddressSpaceInfo::Type::Stack, }, }}; diff --git a/src/core/hle/kernel/svc/svc_physical_memory.cpp b/src/core/hle/kernel/svc/svc_physical_memory.cpp index 4fde85fdaa..280731e488 100644 --- a/src/core/hle/kernel/svc/svc_physical_memory.cpp +++ b/src/core/hle/kernel/svc/svc_physical_memory.cpp @@ -4,10 +4,12 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <algorithm> #include <cstdlib> #include "core/core.h" #include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/k_system_control.h" #include "core/hle/kernel/svc.h" namespace Kernel::Svc { @@ -18,7 +20,10 @@ Result SetHeapSize(Core::System& system, u64* out_address, u64 size) { // Validate size. R_UNLESS(Common::IsAligned(size, HeapSizeAlignment), ResultInvalidSize); - R_UNLESS(size < MainMemorySizeMax, ResultInvalidSize); + // Switch 2 titles size their heap from the total memory, which exceeds Switch 1's 8 GiB. + const size_t max_heap_size = + (std::max)(MainMemorySizeMax, KSystemControl::Init::GetIntendedMemorySize()); + R_UNLESS(size < max_heap_size, ResultInvalidSize); // Set the heap size. KProcessAddress address{}; diff --git a/src/core/hle/service/audio/nx2_audio_user_client.h b/src/core/hle/service/audio/nx2_audio_user_client.h index 2d1a158c84..f053ad62af 100644 --- a/src/core/hle/service/audio/nx2_audio_user_client.h +++ b/src/core/hle/service/audio/nx2_audio_user_client.h @@ -14,14 +14,16 @@ namespace Service::Audio { // Switch 2 "aud:u" (nn::audio::CreateUserClientSessionByHipc). The SDK reaches the Switch 1 // audio interfaces through it: command 2 returns an object used exactly like // IAudioRendererManager (vtable slot 1 = GetWorkBufferSize with a Switch 1 parameter block), so -// it is backed by Eden's audren:u implementation. The nx2: name keeps unknown commands -// auto-stubbed and logged. +// it is backed by Eden's audren:u implementation. Command 1 is nn::audio::OpenAudioOut's +// manager, called like IAudioOutManager (slot 1 = OpenAudioOut), backed by audout:u. The nx2: name +// keeps unknown commands auto-stubbed and logged. class INx2AudioUserClient final : public ServiceFramework<INx2AudioUserClient> { public: explicit INx2AudioUserClient(Core::System& system_) : ServiceFramework{system_, "nx2:aud:u"} { // clang-format off static const FunctionInfo functions[] = { + {1, &INx2AudioUserClient::GetAudioOutManager, "GetAudioOutManager (guessed)"}, {2, &INx2AudioUserClient::GetAudioRendererManager, "GetAudioRendererManager (guessed)"}, }; // clang-format on @@ -29,6 +31,16 @@ public: } private: + void GetAudioOutManager(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const u32 arg = rp.Pop<u32>(); + LOG_WARNING(Service_Audio, "(STUBBED) Switch 2 aud:u 1 arg={}, returning IAudioOutManager", + arg); + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<IAudioOutManager>(ctx, system); + } + void GetAudioRendererManager(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const u32 arg = rp.Pop<u32>(); diff --git a/src/core/hle/service/nx2_streamplay.h b/src/core/hle/service/nx2_streamplay.h new file mode 100644 index 0000000000..4dc839d04b --- /dev/null +++ b/src/core/hle/service/nx2_streamplay.h @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "core/hle/kernel/k_event.h" +#include "core/hle/service/ipc_helpers.h" +#include "core/hle/service/kernel_helpers.h" +#include "core/hle/service/service.h" + +namespace Service { + +// User service returned by stpl:u 0 (nn::streamplay::detail::GetUserService). The SDK attaches a +// system event to the handle command 300 returns right after opening it, so that one needs a real +// event; everything else falls through to the nx2 auto stub (zero sizes, nothing streaming). +class INx2StreamPlayUserService final : public ServiceFramework<INx2StreamPlayUserService> { +public: + explicit INx2StreamPlayUserService(Core::System& system_) + : ServiceFramework{system_, "nx2:stpl:u::0"}, service_context{system_, "stpl:u"} { + static const FunctionInfo functions[] = { + {300, &INx2StreamPlayUserService::GetEvent, "GetEvent (guessed)"}, + }; + RegisterHandlers(functions); + event = service_context.CreateEvent("INx2StreamPlayUserService:Event"); + } + + ~INx2StreamPlayUserService() override { + service_context.CloseEvent(event); + } + +private: + void GetEvent(HLERequestContext& ctx) { + LOG_WARNING(Service, "(STUBBED) stpl:u::0 {}, returning an event", ctx.GetCommand()); + IPC::ResponseBuilder rb{ctx, 2, 1}; + rb.Push(ResultSuccess); + rb.PushCopyObjects(ctx, event->GetReadableEvent()); + } + + KernelHelpers::ServiceContext service_context; + Kernel::KEvent* event; +}; + +// Switch 2 stream play (GameShare), stpl:u. Command 0 (u32 0x12) opens the user service the SDK +// keeps for the process; without an object the first nn::streamplay call jumps through null. +class INx2StreamPlayService final : public ServiceFramework<INx2StreamPlayService> { +public: + explicit INx2StreamPlayService(Core::System& system_) + : ServiceFramework{system_, "nx2:stpl:u"} { + static const FunctionInfo functions[] = { + {0, &INx2StreamPlayService::GetUserService, "GetUserService (guessed)"}, + }; + RegisterHandlers(functions); + } + +private: + void GetUserService(HLERequestContext& ctx) { + LOG_WARNING(Service, "(STUBBED) stpl:u 0 [8]={:#x}, returning a user service", + ctx.CommandBuffer()[8]); + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<INx2StreamPlayUserService>(ctx, system); + } +}; + +} // namespace Service diff --git a/src/core/hle/service/sm/sm.cpp b/src/core/hle/service/sm/sm.cpp index 60f0dd89cb..406d6c9028 100644 --- a/src/core/hle/service/sm/sm.cpp +++ b/src/core/hle/service/sm/sm.cpp @@ -23,6 +23,7 @@ #include "core/hle/service/cmif_types.h" #include "core/hle/service/hle_ipc.h" #include "core/hle/service/ipc_helpers.h" +#include "core/hle/service/nx2_streamplay.h" #include "core/hle/service/nx2_stub_interface.h" #include "core/hle/service/server_manager.h" #include "core/hle/service/sm/sm.h" @@ -194,8 +195,12 @@ Kernel::KClientSession* CreateStubSession(Core::System& system, HLERequestContex auto manager = std::make_shared<SessionRequestManager>(kernel, ctx.GetManager()->GetServerManager()); - manager->SetSessionHandler(std::make_shared<INx2StubInterface>( - system, INx2StubInterface::InternName(fmt::format("{}{}", Nx2StubPrefix, name)))); + if (name == "stpl:u") { + manager->SetSessionHandler(std::make_shared<INx2StreamPlayService>(system)); + } else { + manager->SetSessionHandler(std::make_shared<INx2StubInterface>( + system, INx2StubInterface::InternName(fmt::format("{}{}", Nx2StubPrefix, name)))); + } ctx.GetManager()->GetServerManager().RegisterSession(&session->GetServerSession(), manager); return &session->GetClientSession(); } diff --git a/src/qt_common/game_list/model.cpp b/src/qt_common/game_list/model.cpp index 708ef576c6..4f3916b04d 100644 --- a/src/qt_common/game_list/model.cpp +++ b/src/qt_common/game_list/model.cpp @@ -49,7 +49,7 @@ GameListModel::~GameListModel() = default; void GameListModel::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { emit PopulatingStarted(); - current_worker.reset(); + StopWorker(); removeRows(0, rowCount()); current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, @@ -157,8 +157,27 @@ void GameListModel::RemoveFavorite(u64 program_id) { } } +void GameListModel::StopWorker() { + if (!current_worker) { + return; + } + + // The worker may still be inside ProcessEvents further up the stack (a nested event loop + // can trigger a repopulate), so stop it now but defer freeing it to the event loop. + current_worker->disconnect(); + current_worker->Stop(); + current_worker.release()->deleteLater(); +} + void GameListModel::Repopulate() { - current_worker.reset(); + // CreateFactories replaces the content providers a running game reads from. Stopping + // emulation repopulates the list anyway, so skip watcher-driven reloads until then. + if (QtCommon::system->IsPoweredOn()) { + LOG_INFO(Frontend, "Emulation is running, deferring game list reload."); + return; + } + + StopWorker(); QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs); PopulateAsync(UISettings::values.game_dirs); } diff --git a/src/qt_common/game_list/model.h b/src/qt_common/game_list/model.h index 98614ae33a..b850895d2f 100644 --- a/src/qt_common/game_list/model.h +++ b/src/qt_common/game_list/model.h @@ -78,6 +78,7 @@ private: void AddFavorite(u64 program_id); void RemoveFavorite(u64 program_id); void Repopulate(); + void StopWorker(); bool m_flat = false; diff --git a/src/qt_common/game_list/worker.cpp b/src/qt_common/game_list/worker.cpp index 14afe2f276..d18f84089a 100644 --- a/src/qt_common/game_list/worker.cpp +++ b/src/qt_common/game_list/worker.cpp @@ -313,12 +313,18 @@ GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_, GameListWorker::~GameListWorker() { this->disconnect(); - stop_requested.store(true); - processing_completed.Wait(); + Stop(); +} + +void GameListWorker::Stop() { + if (!stop_requested.exchange(true)) { + processing_completed.Wait(); + } } void GameListWorker::ProcessEvents(GameListModel* model) { - while (true) { + // An event may spin a nested event loop that replaces this worker, in which case stop. + while (model->current_worker.get() == this) { std::function<void(GameListModel*)> func; { // Lock queue to protect concurrent modification. diff --git a/src/qt_common/game_list/worker.h b/src/qt_common/game_list/worker.h index c007498d4b..cbc1fa7021 100644 --- a/src/qt_common/game_list/worker.h +++ b/src/qt_common/game_list/worker.h @@ -52,6 +52,9 @@ public: /// Starts the processing of directory tree information. void run() override; + /// Requests the worker to stop and blocks until processing has finished. + void Stop(); + public: /** * Synchronously processes any events queued by the worker. |
