aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAlex <78863999+Sssnowyyy@users.noreply.github.com>2026-10-31 10:54:52 -0400
committerGitHub <noreply@github.com>2026-10-31 15:54:52 +0100
commitee2d55ee497c9467c8df82515165a21eac13fff5 (patch)
treecf30cc700b791838380f6f3019d70ceae4250dd0 /src
parent01423a2470ee8a604218c1a6d5a0be6769624078 (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.txt1
-rw-r--r--src/core/hle/kernel/k_address_space_info.cpp6
-rw-r--r--src/core/hle/kernel/svc/svc_physical_memory.cpp7
-rw-r--r--src/core/hle/service/audio/nx2_audio_user_client.h16
-rw-r--r--src/core/hle/service/nx2_streamplay.h65
-rw-r--r--src/core/hle/service/sm/sm.cpp9
-rw-r--r--src/qt_common/game_list/model.cpp23
-rw-r--r--src/qt_common/game_list/model.h1
-rw-r--r--src/qt_common/game_list/worker.cpp12
-rw-r--r--src/qt_common/game_list/worker.h3
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.