From 079a4af651f8a8e33c788c6e9b4a3545ebedb4d3 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 17 Jun 2026 16:24:09 +0000 Subject: [PATCH 1/6] debugging: Add GDB pretty printers for common types (#1957) By default, GDB will not autoload this for security reasons. See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Auto_002dloading-safe-path.html to see how to enable it. --- .gdbinit | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .gdbinit diff --git a/.gdbinit b/.gdbinit new file mode 100644 index 00000000..d43d8198 --- /dev/null +++ b/.gdbinit @@ -0,0 +1,47 @@ +python + +import gdb +import re + + +class BetypePrinter: + PATTERN = re.compile("^betype<.*>$") + + def __init__(self, obj): + self._obj = obj + + def to_string(self): + underlying = self._obj["m_value"] + reversed_bytes = bytes(reversed(underlying.bytes)) + return gdb.Value(reversed_bytes, underlying.type) + +class MemptrPrinter: + PATTERN = re.compile("^MEMPTR<.*>$") + + def __init__(self, obj): + self._ptr_type = obj.type.strip_typedefs().unqualified().template_argument(0).pointer() + self._guest_address = obj["m_value"].cast(gdb.lookup_type("uint32")) + + def ptr(self): + if self._guest_address == 0: + return gdb.Value(0, self._ptr_type) + base_addr = gdb.parse_and_eval('memory_base', global_context=True) + return (self._guest_address + base_addr).reinterpret_cast(self._ptr_type) + + def children(self): + return [("raw", self.ptr())] + + def to_string(self): + return self._guest_address.format_string(format="x") + +def lookup(val): + tag = val.type.strip_typedefs().unqualified().tag + if tag is None: + return None + printers = [BetypePrinter, MemptrPrinter] + for printer in printers: + if printer.PATTERN.match(tag): + return printer(val) + return None + +gdb.pretty_printers.append(lookup) From 4ee027d3a846f5bc1ba0fbcdbbc45e2cc42e32f0 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 21 Jun 2026 17:54:49 +0200 Subject: [PATCH 2/6] UI: Show "Could not decrypt title" for drag and drop too (#1962) --- src/gui/wxgui/MainWindow.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/gui/wxgui/MainWindow.cpp b/src/gui/wxgui/MainWindow.cpp index c1cfc242..542554f2 100644 --- a/src/gui/wxgui/MainWindow.cpp +++ b/src/gui/wxgui/MainWindow.cpp @@ -548,8 +548,7 @@ bool MainWindow::FileLoad(const fs::path launchPath, wxLaunchGameEvent::INITIATE wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); return false; } - else if (initiatedBy == wxLaunchGameEvent::INITIATED_BY::MENU || - initiatedBy == wxLaunchGameEvent::INITIATED_BY::COMMAND_LINE) + else { wxString t = _("Unable to launch game\nPath:\n"); t.append(_pathToUtf8(launchPath)); @@ -566,13 +565,6 @@ bool MainWindow::FileLoad(const fs::path launchPath, wxLaunchGameEvent::INITIATE wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); return false; } - else - { - wxString t = _("Unable to launch game\nPath:\n"); - t.append(_pathToUtf8(launchPath)); - wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return false; - } } if(launchTitle.IsValid()) From f3d5788f0cbc5b4ecf607f040367563f36f7cef5 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 23 Jun 2026 04:50:35 +0200 Subject: [PATCH 3/6] FST: Look for .key file next to wud/wux file (#1961) --- src/Cafe/Filesystem/FST/FST.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Cafe/Filesystem/FST/FST.cpp b/src/Cafe/Filesystem/FST/FST.cpp index ec112b9a..1652b1fe 100644 --- a/src/Cafe/Filesystem/FST/FST.cpp +++ b/src/Cafe/Filesystem/FST/FST.cpp @@ -200,7 +200,7 @@ bool FSTVolume::FindDiscKey(const fs::path& path, NCrypto::AesKey& discTitleKey) if (dataSource->readData(0, 0, 0x18000 + 0x100, header, sizeof(header)) != sizeof(header)) return false; - // try all the keys + // try all the keys in the key cache uint8 headerDecrypted[sizeof(header)-16]; for (sint32 i = 0; i < 0x7FFFFFFF; i++) { @@ -215,7 +215,12 @@ bool FSTVolume::FindDiscKey(const fs::path& path, NCrypto::AesKey& discTitleKey) return true; } } - return false; + + // check for a key file next to the WUD file + fs::path keyPath = path; + keyPath.replace_extension("key"); + std::unique_ptr keyFile(FileStream::openFile2(keyPath)); + return keyFile && keyFile->readData(discTitleKey.b, sizeof(discTitleKey.b)) == sizeof(discTitleKey.b); } // open WUD image using key cache From a3d6395ed779a40cd236a35bc6697e277a5606c5 Mon Sep 17 00:00:00 2001 From: Cynthia Coan Date: Mon, 22 Jun 2026 19:59:53 -0700 Subject: [PATCH 4/6] Add command line parameters to make Cemu more useful for running tests (#1959) --- src/Cafe/CafeSystem.cpp | 55 +++++++++++++++------ src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp | 10 ++++ src/config/LaunchSettings.cpp | 30 ++++++++++- src/config/LaunchSettings.h | 18 +++++-- 4 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/Cafe/CafeSystem.cpp b/src/Cafe/CafeSystem.cpp index 79184393..20a76812 100644 --- a/src/Cafe/CafeSystem.cpp +++ b/src/Cafe/CafeSystem.cpp @@ -685,6 +685,22 @@ namespace CafeSystem fsc_unmount("/cemuBossStorage/", FSC_PRIORITY_BASE); } + void MountExtras() + { + for (const auto& [host_path, emulatedPath] : LaunchSettings::CosMounts()) + { + FSCDeviceHostFS_Mount(boost::nowide::narrow(emulatedPath), _pathToUtf8(host_path), FSC_PRIORITY_BASE); + } + } + + void UnmountExtras() + { + for (const auto& [_, emulatedPath] : LaunchSettings::CosMounts()) + { + fsc_unmount(boost::nowide::narrow(emulatedPath), FSC_PRIORITY_BASE); + } + } + PREPARE_STATUS_CODE LoadAndMountForegroundTitle(TitleId titleId) { cemuLog_log(LogType::Force, "Mounting title {:016x}", (uint64)titleId); @@ -838,6 +854,7 @@ namespace CafeSystem if (r != PREPARE_STATUS_CODE::SUCCESS) return r; InitVirtualMlcStorage(); + MountExtras(); return PREPARE_STATUS_CODE::SUCCESS; } @@ -881,6 +898,7 @@ namespace CafeSystem // load executable PrepareExecutable(); InitVirtualMlcStorage(); + MountExtras(); return PREPARE_STATUS_CODE::SUCCESS; } @@ -960,6 +978,9 @@ namespace CafeSystem std::string GetForegroundTitleArgStr() { + auto optional_arguments = LaunchSettings::CosArgstr(); + if (optional_arguments.has_value()) + return *optional_arguments; if (sLaunchModeIsStandalone) return ""; auto& update = sGameInfo_ForegroundTitle.GetUpdate(); @@ -1044,24 +1065,26 @@ namespace CafeSystem { if(!sSystemRunning) return; - coreinit::OSSchedulerEnd(); - Latte_Stop(); - // reset Cafe OS userspace modules - snd_core::reset(); - coreinit::OSAlarm_Shutdown(); - GX2::_GX2DriverReset(); - nn::save::ResetToDefaultState(); - coreinit::__OSDeleteAllActivePPCThreads(); - RPLLoader_UnloadAll(); + coreinit::OSSchedulerEnd(); + Latte_Stop(); + // reset Cafe OS userspace modules + snd_core::reset(); + coreinit::OSAlarm_Shutdown(); + GX2::_GX2DriverReset(); + nn::save::ResetToDefaultState(); + coreinit::__OSDeleteAllActivePPCThreads(); + RPLLoader_UnloadAll(); for(auto it = s_iosuModules.rbegin(); it != s_iosuModules.rend(); ++it) (*it)->TitleStop(); - // reset Cemu subsystems - PPCRecompiler_Shutdown(); - GraphicPack2::Reset(); - UnmountCurrentTitle(); - MlcStorageUnmountAllTitles(); - UnmountBaseDirectories(); - DestroyMemorySpace(); + // reset Cemu subsystems + PPCRecompiler_Shutdown(); + GraphicPack2::Reset(); + UnmountCurrentTitle(); + UnmountExtras(); + MlcStorageUnmountAllTitles(); + UnmountBaseDirectories(); + DestroyMemorySpace(); + LaunchSettings::ClearCosArgstr(); sSystemRunning = false; } diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp index 13ef6613..820f336c 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp @@ -4,6 +4,7 @@ #include "Cafe/OS/libs/coreinit/coreinit_OSScreen.h" #include "Cafe/CafeSystem.h" #include "Cafe/Filesystem/fsc.h" +#include "config/LaunchSettings.h" #include namespace coreinit @@ -544,6 +545,7 @@ namespace coreinit } std::mutex sCafeConsoleMutex; + bool s_forwardConsoleLogs; void WriteCafeConsole(CafeLogType cafeLogType, const char* msg, sint32 len) { @@ -556,6 +558,13 @@ namespace coreinit cafeLogBuffer.lineLength = 0; }; + if (s_forwardConsoleLogs) { + if (cafeLogType == CafeLogType::OSCONSOLE) { + fwrite(msg, 1, len, stdout); + } else { + fwrite(msg, 1, len, stderr); + } + } while (len) { char c = *msg; @@ -846,6 +855,7 @@ namespace coreinit { s_currentTitleId = CafeSystem::GetForegroundTitleId(); s_sdkVersion = CafeSystem::GetForegroundTitleSDKVersion(); + s_forwardConsoleLogs = LaunchSettings::ForwardConsoleLogging(); s_transitionToBackground = false; s_transitionToForeground = false; diff --git a/src/config/LaunchSettings.cpp b/src/config/LaunchSettings.cpp index 4ac12c53..2d4a5316 100644 --- a/src/config/LaunchSettings.cpp +++ b/src/config/LaunchSettings.cpp @@ -63,7 +63,7 @@ bool LaunchSettings::HandleCommandline(const std::vector& args) #endif ("game,g", po::wvalue(), "Path of game to launch") - ("title-id,t", po::value(), "Title ID of the title to be launched (overridden by --game)") + ("title-id,t", po::value(), "Title ID of the title to be launched (overridden by --game)") ("mlc,m", po::wvalue(), "Custom mlc folder location") ("fullscreen,f", po::value()->implicit_value(true), "Launch games in fullscreen mode") @@ -71,6 +71,10 @@ bool LaunchSettings::HandleCommandline(const std::vector& args) ("account,a", po::value(), "Persistent id of account") + ("cos-mounts", po::wvalue>()->composing(), "A series of mounts in the form of: (path on host:path within emulated system, e.g. `/tmp:/vol/temporary/`)") + ("forward-console-logging", "Forward OSReport, OSConsoleWrite, etc. to stdout/stderr.") + ("cos-argstr", po::value(), "A custom argstr used to override to the arguments to the first RPX that is launched, will be unset after the first launch.") + ("force-interpreter", po::value()->implicit_value(true), "Force interpreter CPU emulation, disables recompiler. Useful for debugging purposes where you want to get accurate memory accesses and stack traces.") ("force-multicore-interpreter", po::value()->implicit_value(true), "Force multi-core interpreter CPU emulation, disables recompiler. Only useful for getting stack traces, but slightly faster than the single-core interpreter mode.") ("enable-gdbstub", po::value()->implicit_value(true), "Enable GDB stub to debug executables inside Cemu using an external debugger"); @@ -189,6 +193,30 @@ bool LaunchSettings::HandleCommandline(const std::vector& args) if (vm.count("enable-gdbstub")) s_enable_gdbstub = vm["enable-gdbstub"].as(); + if (vm.count("forward-console-logging")) + { + requireConsole(); + s_forward_console_logging = true; + } + + if (vm.count("cos-argstr")) + s_cos_argstr = vm["cos-argstr"].as(); + + if (vm.count("cos-mounts")) + { + for (const auto& argument : vm["cos-mounts"].as>()) + { + size_t colon_location = argument.find(L':'); + if (colon_location == std::wstring::npos) + { + std::cerr << "Argument for a mount expects to be in the format: `path on host:path in emulated system`, was not: `" << boost::nowide::narrow(argument) << "`\n"; + continue; + } + + s_cos_mounts[argument.substr(0, colon_location)] = argument.substr(colon_location + 1); + } + } + std::wstring extract_path, log_path; std::string output_path; if (vm.count("extract")) diff --git a/src/config/LaunchSettings.h b/src/config/LaunchSettings.h index 13665cb7..df52a84b 100644 --- a/src/config/LaunchSettings.h +++ b/src/config/LaunchSettings.h @@ -2,6 +2,7 @@ #include #include +#include class LaunchSettings { @@ -16,7 +17,7 @@ public: static bool HandleCommandline(const std::vector& args); static std::optional GetLoadFile() { return s_load_game_file; } - static std::optional GetLoadTitleID() {return s_load_title_id;} + static std::optional GetLoadTitleID() {return s_load_title_id;} static std::optional GetMLCPath() { return s_mlc_path; } static std::optional RenderUpsideDownEnabled() { return s_render_upside_down; } @@ -24,6 +25,11 @@ public: static bool Verbose() { return s_verbose; } + static bool ForwardConsoleLogging() { return s_forward_console_logging; } + static std::optional CosArgstr() { return s_cos_argstr; } + static void ClearCosArgstr() { s_cos_argstr.reset(); } + static std::unordered_map& CosMounts() { return s_cos_mounts; } + static bool GDBStubEnabled() { return s_enable_gdbstub; } static bool NSightModeEnabled() { return s_nsight_mode; } @@ -37,14 +43,18 @@ public: private: inline static std::optional s_load_game_file{}; - inline static std::optional s_load_title_id{}; + inline static std::optional s_load_title_id{}; inline static std::optional s_mlc_path{}; inline static std::optional s_render_upside_down{}; inline static std::optional s_fullscreen{}; inline static bool s_verbose = false; - + + inline static bool s_forward_console_logging = false; + inline static std::optional s_cos_argstr{}; + inline static std::unordered_map s_cos_mounts{}; + inline static bool s_enable_gdbstub = false; inline static bool s_nsight_mode = false; @@ -59,5 +69,3 @@ private: static bool ExtractorTool(std::wstring_view wud_path, std::string_view output_path, std::wstring_view log_path); }; - - From 53a74e44df53865e9471f8139e5dd93518f7da14 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 25 Jun 2026 06:42:08 +0200 Subject: [PATCH 5/6] coreinit+UI: Implement exit() When coreinit exit() is hit, then Cemu will only close when the game was launched directly via command line parameters (-g or -t). Otherwise it returns to the game list. In the case where Cemu closes it will forward the coreinit exit status code as it's own exit status code. This is useful for cases where Cemu is used as a CLI tool for testing homebrew --- src/Cafe/CafeSystem.cpp | 13 +++++++++ src/Cafe/CafeSystem.h | 4 +++ src/Cafe/OS/libs/coreinit/coreinit.cpp | 10 ------- src/Cafe/OS/libs/coreinit/coreinit.h | 14 +++++----- src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp | 30 ++++++++++++++++++++- src/gui/wxgui/CemuApp.cpp | 9 ++++--- src/gui/wxgui/MainWindow.cpp | 23 ++++++++++++++++ src/gui/wxgui/MainWindow.h | 2 ++ 8 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/Cafe/CafeSystem.cpp b/src/Cafe/CafeSystem.cpp index 20a76812..e52b8a7c 100644 --- a/src/Cafe/CafeSystem.cpp +++ b/src/Cafe/CafeSystem.cpp @@ -443,6 +443,7 @@ namespace CafeSystem static SystemImplementation* s_implementation{nullptr}; bool sLaunchModeIsStandalone = false; std::optional> s_overrideArgs; + std::optional s_foregroundReturnStatus; bool sSystemRunning = false; TitleId sForegroundTitleId = 0; @@ -836,6 +837,7 @@ namespace CafeSystem { CafeTitleList::WaitForMandatoryScan(); sLaunchModeIsStandalone = false; + s_foregroundReturnStatus = std::nullopt; _pathToExecutable.clear(); TitleIdParser tip(titleId); if (tip.GetType() == TitleIdParser::TITLE_TYPE::AOC || tip.GetType() == TitleIdParser::TITLE_TYPE::BASE_TITLE_UPDATE) @@ -1186,4 +1188,15 @@ namespace CafeSystem s_implementation->CafeRecreateCanvas(); } + void NotifyPPCProcessExit(sint32 status) + { + s_foregroundReturnStatus = status; + s_implementation->CafePPCProcessExit(); + } + + std::optional GetForegroundTitleReturnStatus() + { + return s_foregroundReturnStatus; + } + } diff --git a/src/Cafe/CafeSystem.h b/src/Cafe/CafeSystem.h index 4003b393..ddf7621c 100644 --- a/src/Cafe/CafeSystem.h +++ b/src/Cafe/CafeSystem.h @@ -12,6 +12,7 @@ namespace CafeSystem { public: virtual void CafeRecreateCanvas() = 0; + virtual void CafePPCProcessExit() = 0; // emulated process exited }; enum class PREPARE_STATUS_CODE @@ -43,6 +44,7 @@ namespace CafeSystem std::string GetForegroundTitleArgStr(); uint32 GetForegroundTitleOlvAccesskey(); CosCapabilityBits GetForegroundTitleCosCapabilities(CosCapabilityGroup group); + std::optional GetForegroundTitleReturnStatus(); // valid once the foreground title exited gracefully via coreinit exit void ShutdownTitle(); @@ -55,6 +57,8 @@ namespace CafeSystem uint32 GetRPXHashUpdated(); void RequestRecreateCanvas(); + void NotifyPPCProcessExit(sint32 status); + }; extern RPLModule* applicationRPX; diff --git a/src/Cafe/OS/libs/coreinit/coreinit.cpp b/src/Cafe/OS/libs/coreinit/coreinit.cpp index fd3ed23b..4d454e0e 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit.cpp @@ -238,15 +238,6 @@ namespace coreinit osLib_returnFromFunction(hCPU, 1); } - void coreinit_exit(uint32 r) - { - cemuLog_log(LogType::Force, "The title terminated the process by calling coreinit.exit({})", (sint32)r); - DebugLogStackTrace(coreinit::OSGetCurrentThread(), coreinit::OSGetStackPointer()); - cemu_assert_debug(false); - // never return - while (true) std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - bool OSIsOffBoot() { return true; @@ -289,7 +280,6 @@ namespace coreinit osLib_addFunction("coreinit", "ENVGetEnvironmentVariable", coreinitExport_ENVGetEnvironmentVariable); - cafeExportRegisterFunc(coreinit_exit, "coreinit", "exit", LogType::CoreinitThread); cafeExportRegister("coreinit", OSIsOffBoot, LogType::CoreinitThread); cafeExportRegister("coreinit", OSGetBootPMFlags, LogType::CoreinitThread); cafeExportRegister("coreinit", OSGetSystemMode, LogType::CoreinitThread); diff --git a/src/Cafe/OS/libs/coreinit/coreinit.h b/src/Cafe/OS/libs/coreinit/coreinit.h index 2aa3f1df..e7b88821 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit.h +++ b/src/Cafe/OS/libs/coreinit/coreinit.h @@ -15,13 +15,13 @@ void coreinitAsyncCallback_addWithLock(MPTR functionMPTR, uint32 numParameters, // coreinit shared memory struct CoreinitSharedData { - MEMPTR MEMAllocFromDefaultHeap; - MEMPTR MEMAllocFromDefaultHeapEx; - MEMPTR MEMFreeToDefaultHeap; - MPTR __atexit_cleanup; - MPTR __cpp_exception_init_ptr; - MPTR __cpp_exception_cleanup_ptr; - MPTR __stdio_cleanup; + MEMPTR MEMAllocFromDefaultHeap{nullptr}; + MEMPTR MEMAllocFromDefaultHeapEx{nullptr}; + MEMPTR MEMFreeToDefaultHeap{nullptr}; + MEMPTR __atexit_cleanup{nullptr}; + MEMPTR __stdio_cleanup{nullptr}; + MPTR __cpp_exception_init_ptr{0}; + MPTR __cpp_exception_cleanup_ptr{0}; }; extern CoreinitSharedData* gCoreinitData; diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp index 820f336c..6a6cafe0 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp @@ -693,7 +693,6 @@ namespace coreinit return s_sdkVersion; } - // move this to CafeSystem.cpp? void OSLauncherThread(uint64 titleId) { CafeSystem::ShutdownTitle(); @@ -702,6 +701,12 @@ namespace coreinit CafeSystem::LaunchForegroundTitle(); } + void OSShutdownThread(sint32 status) + { + CafeSystem::ShutdownTitle(); + CafeSystem::NotifyPPCProcessExit(status); + } + uint32 __LaunchByTitleId(uint64 titleId, uint32 argc, MEMPTR* argv) { // prepare argument buffer @@ -851,6 +856,26 @@ namespace coreinit return 0; } + void __PPCExit(sint32 status) + { + // spawn shutdown thread (the current thread has to be destroyed as part of the shutdown process) + std::thread shutdownThread(OSShutdownThread, status); + shutdownThread.detach(); + // suspend this thread + coreinit::OSSuspendThread(coreinit::OSGetCurrentThread()); + } + + void coreinit_exit(sint32 status) + { + cemuLog_log(LogType::Force, "The title terminated the process by calling coreinit.exit({})", (sint32)status); + DebugLogStackTrace(coreinit::OSGetCurrentThread(), coreinit::OSGetStackPointer()); + if (gCoreinitData->__atexit_cleanup) + PPCCoreCallback(gCoreinitData->__atexit_cleanup, status); + if (gCoreinitData->__stdio_cleanup) + PPCCoreCallback(gCoreinitData->__stdio_cleanup); + __PPCExit(status); + } + void miscInit() { s_currentTitleId = CafeSystem::GetForegroundTitleId(); @@ -886,6 +911,9 @@ namespace coreinit cafeExportRegister("coreinit", OSDriver_Register, LogType::Placeholder); cafeExportRegister("coreinit", OSDriver_Deregister, LogType::Placeholder); + + cafeExportRegisterFunc(coreinit_exit, "coreinit", "exit", LogType::CoreinitThread); + cafeExportRegister("coreinit", __PPCExit, LogType::CoreinitThread); } }; diff --git a/src/gui/wxgui/CemuApp.cpp b/src/gui/wxgui/CemuApp.cpp index 9a636989..bb7b6c7c 100644 --- a/src/gui/wxgui/CemuApp.cpp +++ b/src/gui/wxgui/CemuApp.cpp @@ -34,6 +34,7 @@ #include "Cafe/TitleList/TitleList.h" #include "Cafe/TitleList/SaveList.h" +#include "Cafe/CafeSystem.h" wxIMPLEMENT_APP_NO_MAIN(CemuApp); @@ -414,17 +415,19 @@ int CemuApp::OnExit() m_sdlEventPumpTimer = nullptr; } #endif - wxApp::OnExit(); wxTheClipboard->Flush(); InputManager::instance().Shutdown(); + int retValue = 0; + if (auto r = CafeSystem::GetForegroundTitleReturnStatus(); (LaunchSettings::GetLoadFile() || LaunchSettings::GetLoadTitleID()) && r) + retValue = *r; #if BOOST_OS_MACOS SDLControllerProvider::ShutdownSDL(); #endif #if BOOST_OS_WINDOWS - ExitProcess(0); + ExitProcess(retValue); #else - _Exit(0); + _Exit(retValue); #endif } diff --git a/src/gui/wxgui/MainWindow.cpp b/src/gui/wxgui/MainWindow.cpp index 542554f2..e1e88ff8 100644 --- a/src/gui/wxgui/MainWindow.cpp +++ b/src/gui/wxgui/MainWindow.cpp @@ -162,6 +162,7 @@ wxDEFINE_EVENT(wxEVT_SET_WINDOW_TITLE, wxCommandEvent); wxDEFINE_EVENT(wxEVT_REQUEST_GAMELIST_REFRESH, wxCommandEvent); wxDEFINE_EVENT(wxEVT_LAUNCH_GAME, wxLaunchGameEvent); wxDEFINE_EVENT(wxEVT_REQUEST_RECREATE_CANVAS, wxCommandEvent); +wxDEFINE_EVENT(wxEVT_REQUEST_GAME_EXIT, wxCommandEvent); wxBEGIN_EVENT_TABLE(MainWindow, wxFrame) EVT_TIMER(MAINFRAME_ID_TIMER1, MainWindow::OnTimer) @@ -243,6 +244,7 @@ EVT_COMMAND(wxID_ANY, wxEVT_ACCOUNTLIST_REFRESH, MainWindow::OnAccountListRefres EVT_COMMAND(wxID_ANY, wxEVT_SET_WINDOW_TITLE, MainWindow::OnSetWindowTitle) EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_RECREATE_CANVAS, MainWindow::OnRequestRecreateCanvas) +EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_GAME_EXIT, MainWindow::OnRequestGameExit) wxEND_EVENT_TABLE() @@ -2469,6 +2471,27 @@ void MainWindow::CafeRecreateCanvas() sem.decrementWithWait(); } +void MainWindow::CafePPCProcessExit() +{ + // this is called from the emulated PPC thread, so queue an event instead of handling it directly + wxQueueEvent(g_mainFrame, new wxCommandEvent(wxEVT_REQUEST_GAME_EXIT)); +} + +void MainWindow::OnRequestGameExit(wxCommandEvent& event) +{ + // if the title was launched via the command line we close Cemu and use the foreground title's exit status as Cemu's process return code (via CemuApp:OnExit) + // this is useful for homebrew testing setups that use Cemu via CLI + // otherwise the title was launched from the game list, so we just stop it and return to the game list instead of closing Cemu + if (LaunchSettings::GetLoadFile() || LaunchSettings::GetLoadTitleID()) + { + Close(); + } + else + { + EndEmulation(); + } +} + bool MainWindow::FullscreenEnabled() const { return LaunchSettings::FullscreenEnabled().value_or(GetWxGUIConfig().fullscreen); diff --git a/src/gui/wxgui/MainWindow.h b/src/gui/wxgui/MainWindow.h index eec043b8..e1cfc8b5 100644 --- a/src/gui/wxgui/MainWindow.h +++ b/src/gui/wxgui/MainWindow.h @@ -157,8 +157,10 @@ private: // CafeSystem implementation void CafeRecreateCanvas() override; + void CafePPCProcessExit() override; void OnRequestRecreateCanvas(wxCommandEvent& event); + void OnRequestGameExit(wxCommandEvent& event); wxRect GetDesktopRect(); From 1c7fbafb00ae62ae1e52ecdaf18c6a5e84210491 Mon Sep 17 00:00:00 2001 From: Cynthia Coan Date: Thu, 25 Jun 2026 08:26:41 -0700 Subject: [PATCH 6/6] coreinit: Flush logs when forwarding (#1965) --- src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp index 6a6cafe0..bae0dca8 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp @@ -561,6 +561,7 @@ namespace coreinit if (s_forwardConsoleLogs) { if (cafeLogType == CafeLogType::OSCONSOLE) { fwrite(msg, 1, len, stdout); + fflush(stdout); } else { fwrite(msg, 1, len, stderr); }