From 5815abdabe650ccca153ccab7e457ccb15f5557b Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:06:49 +0900 Subject: [PATCH] [Emulator] Add in-process title relaunch removing emulator restarts Tear down and reinitialize all subsystems on module reload only keeping the emulator window and input system persistent, everything else gets fully re-initialized similar to a full restart but much less jarring for the user. --- src/xenia/app/xenia_main.cc | 95 +++---------- src/xenia/emulator.cc | 231 +++++++++++++++++++++++++++---- src/xenia/emulator.h | 26 ++++ src/xenia/kernel/kernel_state.cc | 8 +- src/xenia/kernel/xam/xam_info.cc | 37 +++++ 5 files changed, 295 insertions(+), 102 deletions(-) diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index d6e6f996c..90589d246 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -32,7 +32,6 @@ #include "xenia/ui/window_listener.h" #include "xenia/ui/windowed_app.h" #include "xenia/ui/windowed_app_context.h" -#include "xenia/vfs/devices/host_path_device.h" // Available audio systems: #include "xenia/apu/nop/nop_audio_system.h" @@ -704,79 +703,7 @@ void EmulatorApp::EmulatorThread(bool is_game_process) { app_context().CallInUIThread( [this]() { emulator_window_->SetupGraphicsSystemPresenterPainting(); }); - const auto fs = emulator_->file_system(); - - if (cvars::mount_scratch) { - auto scratch_device = std::make_unique( - "\\SCRATCH", emulator_->storage_root() / "scratch", false); - if (!scratch_device->Initialize()) { - XELOGE("Unable to scan scratch path"); - } else { - if (!fs->RegisterDevice(std::move(scratch_device))) { - XELOGE("Unable to register scratch path"); - } else { - fs->RegisterSymbolicLink("scratch:", "\\SCRATCH"); - } - } - } - - if (cvars::mount_cache) { - auto cache0_device = std::make_unique( - "\\CACHE0", emulator_->storage_root() / "cache0", false); - if (!cache0_device->Initialize()) { - XELOGE("Unable to scan cache0 path"); - } else { - if (!fs->RegisterDevice(std::move(cache0_device))) { - XELOGE("Unable to register cache0 path"); - } else { - fs->RegisterSymbolicLink("cache0:", "\\CACHE0"); - } - } - - auto cache1_device = std::make_unique( - "\\CACHE1", emulator_->storage_root() / "cache1", false); - if (!cache1_device->Initialize()) { - XELOGE("Unable to scan cache1 path"); - } else { - if (!fs->RegisterDevice(std::move(cache1_device))) { - XELOGE("Unable to register cache1 path"); - } else { - fs->RegisterSymbolicLink("cache1:", "\\CACHE1"); - } - } - - // Some (older?) games try accessing cache:\ too - // NOTE: this must be registered _after_ the cache0/cache1 devices, due to - // substring/start_with logic inside VirtualFileSystem::ResolvePath, else - // accesses to those devices will go here instead - auto cache_device = std::make_unique( - "\\CACHE", emulator_->storage_root() / "cache", false); - if (!cache_device->Initialize()) { - XELOGE("Unable to scan cache path"); - } else { - if (!fs->RegisterDevice(std::move(cache_device))) { - XELOGE("Unable to register cache path"); - } else { - fs->RegisterSymbolicLink("cache:", "\\CACHE"); - } - } - } - - if (cvars::force_mount_devkit) { - auto devkit_device = - std::make_unique("\\DEVKIT", "devkit", false); - - if (!devkit_device->Initialize()) { - XELOGE("Unable to scan devkit path"); - } - - if (!fs->RegisterDevice(std::move(devkit_device))) { - XELOGE("Unable to register devkit path"); - } - - fs->RegisterSymbolicLink("DEVKIT:", "\\DEVKIT"); - fs->RegisterSymbolicLink("e:", "\\DEVKIT"); - } + emulator_->MountStandardDrives(); // Set a debug handler. // This will respond to debugging requests so we can open the debug UI. @@ -802,7 +729,17 @@ void EmulatorApp::EmulatorThread(bool is_game_process) { discord::DiscordPresence::PlayingTitle( game_title.empty() ? "Unknown Title" : std::string(game_title)); } - app_context().CallInUIThread([this]() { emulator_window_->UpdateTitle(); }); + // Re-setup presenter painting — needed after in-process relaunch + // creates a new graphics system. + if (app_context().IsInUIThread()) { + emulator_window_->SetupGraphicsSystemPresenterPainting(); + emulator_window_->UpdateTitle(); + } else { + app_context().CallInUIThread([this]() { + emulator_window_->SetupGraphicsSystemPresenterPainting(); + emulator_window_->UpdateTitle(); + }); + } emulator_thread_event_->Set(); }); @@ -823,6 +760,14 @@ void EmulatorApp::EmulatorThread(bool is_game_process) { } }); + emulator_->on_before_shutdown.AddListener([this]() { + // Tear down presenter painting while the graphics system is still alive, + // so the D3D12 immediate drawer can release its resources cleanly. + app_context().CallInUIThreadSynchronous([this]() { + emulator_window_->ShutdownGraphicsSystemPresenterPainting(); + }); + }); + // Enable emulator input now that the emulator is properly loaded. app_context().CallInUIThread( [this]() { emulator_window_->OnEmulatorInitialized(); }); diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index d0666f281..7eabca439 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -101,6 +101,10 @@ DECLARE_string(user_language); DECLARE_bool(allow_plugins); +DECLARE_bool(mount_scratch); +DECLARE_bool(mount_cache); +DECLARE_bool(force_mount_devkit); + DEFINE_int32(priority_class, 0, "Forces Xenia to use different process priority than default one. " "It might affect performance and cause unexpected bugs. Possible " @@ -169,7 +173,18 @@ Emulator::Emulator(const std::filesystem::path& command_line, #endif } -Emulator::~Emulator() { +Emulator::~Emulator() { Shutdown(); } + +void Emulator::Shutdown() { + XELOGI("Emulator::Shutdown: starting teardown"); + + // During relaunch, notify listeners before teardown so they can disconnect + // UI resources while subsystems are still alive. Skip during normal + // destructor — the UI loop may not be running. + if (relaunching_) { + on_before_shutdown(); + } + // Note that we delete things in the reverse order they were initialized. // Give the systems time to shutdown before we delete them. @@ -180,19 +195,35 @@ Emulator::~Emulator() { audio_system_->Shutdown(); } - input_system_.reset(); + main_thread_ = nullptr; + + // Keep input_system_ alive across relaunch — it's bound to the persistent + // window and SDL requires init/quit on the same thread. + if (!relaunching_) { + input_system_.reset(); + } graphics_system_.reset(); audio_system_.reset(); audio_media_player_.reset(); kernel_state_.reset(); file_system_.reset(); + patcher_.reset(); + plugin_loader_.reset(); processor_.reset(); - export_resolver_.reset(); + memory_.reset(); ExceptionHandler::Uninstall(Emulator::ExceptionCallbackThunk, this); + + title_id_ = std::nullopt; + title_name_.clear(); + title_version_.clear(); + game_info_database_.reset(); + paused_ = false; + + XELOGI("Emulator::Shutdown: teardown complete"); } X_STATUS Emulator::Setup( @@ -206,8 +237,15 @@ X_STATUS Emulator::Setup( input_driver_factory) { X_STATUS result = X_STATUS_UNSUCCESSFUL; - display_window_ = display_window; - imgui_drawer_ = imgui_drawer; + // Store parameters for reuse across Shutdown/Setup cycles. + // Only overwrite if non-null so re-calls after Shutdown keep prior values. + if (display_window) display_window_ = display_window; + if (imgui_drawer) imgui_drawer_ = imgui_drawer; + require_cpu_backend_ = require_cpu_backend; + if (audio_system_factory) audio_system_factory_ = audio_system_factory; + if (graphics_system_factory) + graphics_system_factory_ = graphics_system_factory; + if (input_driver_factory) input_driver_factory_ = input_driver_factory; // Initialize clock. // 360 uses a 50MHz clock. @@ -246,7 +284,7 @@ X_STATUS Emulator::Setup( #endif // XE_ARCH } } - if (!backend && !require_cpu_backend) { + if (!backend && !require_cpu_backend_) { backend.reset(new xe::cpu::backend::NullBackend()); } @@ -260,9 +298,9 @@ X_STATUS Emulator::Setup( } // Initialize the APU (optional for UI process). - if (audio_system_factory) { + if (audio_system_factory_) { XELOGI("{}: Initializing Audio...", __func__); - audio_system_ = audio_system_factory(processor_.get()); + audio_system_ = audio_system_factory_(processor_.get()); if (!audio_system_) { XELOGE("{}: Cannot initalize audio_system!", __func__); return X_STATUS_NOT_IMPLEMENTED; @@ -270,32 +308,35 @@ X_STATUS Emulator::Setup( } // Initialize the GPU (optional for UI process). - if (graphics_system_factory) { + if (graphics_system_factory_) { XELOGI("{}: Initializing Graphics...", __func__); - graphics_system_ = graphics_system_factory(); + graphics_system_ = graphics_system_factory_(); if (!graphics_system_) { XELOGE("{}: Cannot initalize graphics_system!", __func__); return X_STATUS_NOT_IMPLEMENTED; } } - XELOGI("{}: Initializing HID...", __func__); - // Initialize the HID. - input_system_ = std::make_unique(display_window_); + // Input system persists across relaunch — SDL requires init/quit on the + // same thread. if (!input_system_) { - XELOGE("{}: Cannot initalize input_system!", __func__); - return X_STATUS_NOT_IMPLEMENTED; - } - if (input_driver_factory) { - auto input_drivers = input_driver_factory(display_window_); - for (size_t i = 0; i < input_drivers.size(); ++i) { - input_system_->AddDriver(std::move(input_drivers[i])); + XELOGI("{}: Initializing HID...", __func__); + input_system_ = std::make_unique(display_window_); + if (!input_system_) { + XELOGE("{}: Cannot initalize input_system!", __func__); + return X_STATUS_NOT_IMPLEMENTED; + } + if (input_driver_factory_) { + auto input_drivers = input_driver_factory_(display_window_); + for (size_t i = 0; i < input_drivers.size(); ++i) { + input_system_->AddDriver(std::move(input_drivers[i])); + } } - } - result = input_system_->Setup(); - if (result) { - return result; + result = input_system_->Setup(); + if (result) { + return result; + } } // Add inputSystem to UI (if imgui is enabled) @@ -539,6 +580,11 @@ Emulator::FileSignatureType Emulator::GetFileSignature( } X_STATUS Emulator::LaunchPath(const std::filesystem::path& path) { + // Remember for relaunch fallback + if (!path.empty()) { + last_launch_path_ = path; + } + X_STATUS mount_result = X_STATUS_SUCCESS; switch (GetFileSignature(path)) { @@ -1384,6 +1430,135 @@ bool Emulator::RestoreFromFile(const std::filesystem::path& path) { return true; } +void Emulator::RelaunchTitle(const std::string& host_path, + const std::string& launch_module, + uint32_t launch_flags, + std::vector launch_data) { + XELOGI( + "RelaunchTitle: starting full in-process relaunch, target={}, module={}", + host_path, launch_module); + + // Tell WaitUntilExit not to fire on_exit when main thread dies. + relaunching_ = true; + + // Force-terminate all threads. Cooperative shutdown isn't possible since + // workers may be stuck in processor_->Execute(). + { + auto threads = + kernel_state()->object_table()->GetObjectsByType( + kernel::XObject::Type::Thread); + XELOGI("RelaunchTitle: terminating {} threads", threads.size()); + for (auto thread : threads) { + thread->Terminate(0); + } + } + + Shutdown(); + Setup(nullptr, nullptr, require_cpu_backend_, nullptr, nullptr, nullptr); + MountStandardDrives(); + + // Populate launch data on the fresh xam module. + auto xam_new = + kernel_state_->GetKernelModule("xam.xex"); + if (xam_new) { + auto& ld = xam_new->loader_data(); + ld.host_path = + host_path.empty() ? xe::path_to_utf8(command_line_) : host_path; + ld.launch_flags = launch_flags; + ld.launch_data = std::move(launch_data); + ld.launch_data_present = !ld.launch_data.empty(); + } + + // CompleteLaunch reads this cvar to determine the executable module. + cvars::launch_module = launch_module; + + // Fall back to the initial launch path if host_path is empty (command-line + // launch rather than loader_data-driven). + auto launch_target = + host_path.empty() ? last_launch_path_ : xe::to_path(host_path); + XELOGI("RelaunchTitle: launching '{}'", xe::path_to_utf8(launch_target)); + LaunchPath(launch_target); + + relaunching_ = false; + XELOGI("RelaunchTitle: relaunch complete"); +} + +void Emulator::MountStandardDrives() { + auto fs = file_system_.get(); + + if (cvars::mount_scratch) { + auto scratch_device = std::make_unique( + "\\SCRATCH", storage_root_ / "scratch", false); + if (!scratch_device->Initialize()) { + XELOGE("Unable to scan scratch path"); + } else { + if (!fs->RegisterDevice(std::move(scratch_device))) { + XELOGE("Unable to register scratch path"); + } else { + fs->RegisterSymbolicLink("scratch:", "\\SCRATCH"); + } + } + } + + if (cvars::mount_cache) { + auto cache0_device = std::make_unique( + "\\CACHE0", storage_root_ / "cache0", false); + if (!cache0_device->Initialize()) { + XELOGE("Unable to scan cache0 path"); + } else { + if (!fs->RegisterDevice(std::move(cache0_device))) { + XELOGE("Unable to register cache0 path"); + } else { + fs->RegisterSymbolicLink("cache0:", "\\CACHE0"); + } + } + + auto cache1_device = std::make_unique( + "\\CACHE1", storage_root_ / "cache1", false); + if (!cache1_device->Initialize()) { + XELOGE("Unable to scan cache1 path"); + } else { + if (!fs->RegisterDevice(std::move(cache1_device))) { + XELOGE("Unable to register cache1 path"); + } else { + fs->RegisterSymbolicLink("cache1:", "\\CACHE1"); + } + } + + // Some (older?) games try accessing cache:\ too + // NOTE: this must be registered _after_ the cache0/cache1 devices, due to + // substring/start_with logic inside VirtualFileSystem::ResolvePath, else + // accesses to those devices will go here instead + auto cache_device = std::make_unique( + "\\CACHE", storage_root_ / "cache", false); + if (!cache_device->Initialize()) { + XELOGE("Unable to scan cache path"); + } else { + if (!fs->RegisterDevice(std::move(cache_device))) { + XELOGE("Unable to register cache path"); + } else { + fs->RegisterSymbolicLink("cache:", "\\CACHE"); + } + } + } + + if (cvars::force_mount_devkit) { + auto devkit_device = + std::make_unique("\\DEVKIT", "devkit", false); + + if (!devkit_device->Initialize()) { + XELOGE("Unable to scan devkit path"); + } + + if (!fs->RegisterDevice(std::move(devkit_device))) { + XELOGE("Unable to register devkit path"); + } + + fs->RegisterSymbolicLink("DEVKIT:", "\\DEVKIT"); + fs->RegisterSymbolicLink("e:", "\\DEVKIT"); + } +} + const std::filesystem::path Emulator::GetNewDiscPath( std::string window_message) { std::filesystem::path path = ""; @@ -1628,8 +1803,14 @@ void Emulator::WaitUntilExit() { if (restoring_) { restore_fence_.Wait(); + } else if (relaunching_) { + // RelaunchTitle is running on another thread - wait for it to finish + // and set the new main_thread_, then loop back to wait on it. + while (relaunching_) { + xe::threading::Sleep(std::chrono::milliseconds(10)); + } } else { - // Not restoring and the thread exited. We're finished. + // Not restoring/relaunching and the thread exited. We're finished. break; } } diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index c24d294a2..ab1135f54 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -168,6 +168,12 @@ class Emulator { std::function>(ui::Window*)> input_driver_factory); + // Tears down all subsystems. Called by the destructor and by RelaunchTitle. + void Shutdown(); + + // Mounts scratch, cache, and devkit drives based on cvars. + void MountStandardDrives(); + // Terminates the currently running title. X_STATUS TerminateTitle(); @@ -370,6 +376,12 @@ class Emulator { bool SaveToFile(const std::filesystem::path& path); bool RestoreFromFile(const std::filesystem::path& path); + // Full in-process relaunch: terminates threads, Shutdown(), Setup(), + // then launches with new params. Must be called from a non-guest thread. + void RelaunchTitle(const std::string& host_path, + const std::string& launch_module, uint32_t launch_flags, + std::vector launch_data); + // The game can request another title to be loaded. const std::filesystem::path GetNewDiscPath(std::string window_message = ""); @@ -382,6 +394,9 @@ class Emulator { xe::Delegate<> on_terminate; xe::Delegate<> on_exit; + // Fired before Shutdown() during relaunch, while subsystems are still alive. + xe::Delegate<> on_before_shutdown; + // Called when XamLoaderLaunchTitle requests launching a new title. // The callback should spawn a new process with the given parameters. // Parameters: host_path, launch_module, launch_flags, launch_data (hex) @@ -416,6 +431,7 @@ class Emulator { const std::string_view module_path); std::filesystem::path command_line_; + std::filesystem::path last_launch_path_; // persists across relaunch std::filesystem::path storage_root_; std::filesystem::path content_root_; std::filesystem::path cache_root_; @@ -448,8 +464,18 @@ class Emulator { bool paused_; bool restoring_; + bool relaunching_ = false; threading::Fence restore_fence_; // Fired on restore finish. + // Persisted across Shutdown/Setup for relaunch. + bool require_cpu_backend_ = false; + std::function(cpu::Processor*)> + audio_system_factory_; + std::function()> + graphics_system_factory_; + std::function>(ui::Window*)> + input_driver_factory_; + LaunchNewTitleCallback on_launch_new_title_; DiscSwapCallback on_disc_swap_; }; diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index 967791745..9f6501a01 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -82,8 +82,12 @@ KernelState::~KernelState() { if (dispatch_thread_running_) { dispatch_thread_running_ = false; - dispatch_cond_.notify_all(); - dispatch_thread_->Wait(0, 0, 0, nullptr); + if (dispatch_thread_ && dispatch_thread_->is_running()) { + dispatch_cond_.notify_all(); + dispatch_thread_->Wait(0, 0, 0, nullptr); + } + // Skip notify/Wait if already force-terminated — mutex may be abandoned. + dispatch_thread_.reset(); } executable_module_.reset(); diff --git a/src/xenia/kernel/xam/xam_info.cc b/src/xenia/kernel/xam/xam_info.cc index 6f9b841ae..1d860aeb2 100644 --- a/src/xenia/kernel/xam/xam_info.cc +++ b/src/xenia/kernel/xam/xam_info.cc @@ -7,9 +7,13 @@ ****************************************************************************** */ +#include + #include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/base/string_util.h" +#include "xenia/base/utf8.h" +#include "xenia/emulator.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/title_id_utils.h" #include "xenia/kernel/user_module.h" @@ -49,6 +53,11 @@ DEFINE_bool(staging_mode, 0, "Enables preview mode in dashboards to render debug information.", "Kernel"); +DEFINE_bool(in_process_title_relaunch, true, + "Handle title-to-title launches in-process via full " + "Shutdown/Setup cycle instead of spawning a new emulator process.", + "Kernel"); + namespace xe { namespace kernel { namespace xam { @@ -417,6 +426,34 @@ void XamLoaderLaunchTitle_entry(lpstring_t raw_name_ptr, dword_t flags) { XELOGI("XamLoaderLaunchTitle: normalized host_path={}, launch_path={}", xe::path_to_utf8(host_path), launch_path); + // Handle title launch in-process via full Shutdown/Setup cycle + if (cvars::in_process_title_relaunch) { + auto emulator = kernel_state()->emulator(); + + XELOGI("XamLoaderLaunchTitle: in-process relaunch to '{}'", + xe::path_to_utf8(host_path)); + + auto new_host_path = xe::path_to_utf8(host_path); + auto new_launch_module = launch_path; + auto new_flags = loader_data.launch_flags; + auto new_data = loader_data.launch_data; + auto current_thread = XThread::GetCurrentThread(); + + // Must dispatch from a non-guest thread — RelaunchTitle terminates + // all guest threads including the caller. + std::thread([emulator, new_host_path = std::move(new_host_path), + new_launch_module = std::move(new_launch_module), + new_flags, new_data = std::move(new_data)]() mutable { + emulator->RelaunchTitle(new_host_path, new_launch_module, new_flags, + std::move(new_data)); + }).detach(); + + current_thread->Suspend(nullptr); + + // Unreachable — thread is terminated during relaunch. + assert_always(); + } + // Convert launch_data to hex string std::string launch_data_hex; for (uint8_t byte : loader_data.launch_data) {