From b5496e9ea5d2f72488b759275fd4db5b17aaa0df Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:37:29 +0900 Subject: [PATCH] [App] Implement title-to-title launches without intermediate file Removes the need to store launch_data.txt file and require a new app start by directly spawning the child process with correct args and exiting instead. --- src/xenia/app/emulator_window.cc | 277 +++++++++++++--------------- src/xenia/app/emulator_window.h | 5 +- src/xenia/app/xenia_main.cc | 53 ++---- src/xenia/base/logging.cc | 10 +- src/xenia/emulator.h | 26 ++- src/xenia/kernel/xam/xam_content.cc | 56 ++---- src/xenia/kernel/xam/xam_info.cc | 57 ++++-- src/xenia/kernel/xam/xam_module.cc | 98 ---------- src/xenia/kernel/xam/xam_module.h | 6 - 9 files changed, 233 insertions(+), 355 deletions(-) diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 8d608a748..5ba84bd73 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -426,29 +426,119 @@ void EmulatorWindow::OnEmulatorInitialized() { Gamepad_HotKeys_Listener->set_name("Gamepad HotKeys Listener"); } - // Register callback for launch data restart requests from the kernel - auto* qt_window = dynamic_cast(window_.get()); - if (qt_window) { - emulator_->set_on_launch_data_restart([this, qt_window]() { - // Show notification in UI thread - window_->app_context().CallInUIThread([this, qt_window]() { - auto* notification = new NotificationWidgetQt( - qt_window->qwindow(), "Title Restart Required", - "Title is restarting with new launch data.\n" - "Game will be loaded automatically.", - 5000); // 5 second duration - notification->Show(); + // Register callback for title-to-title launches from the kernel + emulator_->set_on_launch_new_title([this](const std::string& host_path, + const std::string& launch_module, + uint32_t launch_flags, + const std::string& launch_data) { + XELOGI( + "Launching new title: host_path={}, launch_module={}, flags={}, " + "data_len={}", + host_path, launch_module, launch_flags, launch_data.length()); - // Schedule terminate and exit after notification duration - QTimer::singleShot(5000, [this]() { - if (emulator_->kernel_state()) { - emulator_->kernel_state()->TerminateTitle(); - } - std::quick_exit(0); - }); - }); - }); - } + std::filesystem::path executable_path = xe::filesystem::GetExecutablePath(); + +#if XE_PLATFORM_WIN32 + auto exe_path_u16 = xe::path_to_utf16(executable_path); + std::u16string cmd_line = u"\"" + exe_path_u16 + u"\""; + + if (!cvars::config.empty()) { + cmd_line += u" --config=\"" + xe::to_utf16(cvars::config) + u"\""; + } + // Append to log file instead of overwriting + cmd_line += u" --log_append=true"; + if (!launch_module.empty()) { + cmd_line += u" --launch_module=\"" + xe::to_utf16(launch_module) + u"\""; + } + if (launch_flags != 0) { + cmd_line += + u" --launch_flags=" + xe::to_utf16(fmt::format("{}", launch_flags)); + } + if (!launch_data.empty()) { + cmd_line += u" --launch_data=" + xe::to_utf16(launch_data); + } + if (!host_path.empty()) { + cmd_line += u" \"" + xe::to_utf16(host_path) + u"\""; + } + + STARTUPINFOW si = {}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi = {}; + + if (!CreateProcessW(nullptr, + const_cast( + reinterpret_cast(cmd_line.c_str())), + nullptr, nullptr, FALSE, CREATE_NEW_CONSOLE, nullptr, + nullptr, &si, &pi)) { + XELOGE("Failed to launch new process: {}", GetLastError()); + return; + } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); +#else + pid_t pid = fork(); + if (pid == 0) { + // Child process + if (cvars::use_mangohud) { + setenv("MANGOHUD", "1", 1); + } + + std::vector arg_storage; + std::vector argv; + + std::string gamemode_cmd; + if (cvars::use_gamemode) { + gamemode_cmd = "gamemoderun"; + argv.push_back(gamemode_cmd.c_str()); + } + arg_storage.push_back(executable_path.string()); + argv.push_back(arg_storage.back().c_str()); + + if (!cvars::config.empty()) { + arg_storage.push_back("--config=" + cvars::config); + argv.push_back(arg_storage.back().c_str()); + } + // Append to log file instead of overwriting + arg_storage.push_back("--log_append=true"); + argv.push_back(arg_storage.back().c_str()); + if (!launch_module.empty()) { + arg_storage.push_back("--launch_module=" + launch_module); + argv.push_back(arg_storage.back().c_str()); + } + if (launch_flags != 0) { + arg_storage.push_back(fmt::format("--launch_flags={}", launch_flags)); + argv.push_back(arg_storage.back().c_str()); + } + if (!launch_data.empty()) { + arg_storage.push_back("--launch_data=" + launch_data); + argv.push_back(arg_storage.back().c_str()); + } + if (!host_path.empty()) { + arg_storage.push_back(host_path); + argv.push_back(arg_storage.back().c_str()); + } + argv.push_back(nullptr); + + if (cvars::use_gamemode) { + execvp(gamemode_cmd.c_str(), const_cast(argv.data())); + } else { + execv(executable_path.c_str(), const_cast(argv.data())); + } + std::exit(1); + } else if (pid < 0) { + XELOGE("Failed to fork process"); + return; + } +#endif + // Exit the current process + std::quick_exit(0); + }); + + // Register callback for disc swap to update title bar + emulator_->set_on_disc_swap([this](uint8_t new_disc_number) { + swapped_disc_number_ = new_disc_number; + app_context_.CallInUIThread([this]() { UpdateTitle(); }); + }); } void EmulatorWindow::EmulatorWindowListener::OnClosing(ui::UIEvent& e) { @@ -1918,7 +2008,11 @@ void EmulatorWindow::UpdateTitle() { auto executable_module = emulator()->kernel_state()->GetExecutableModule(); if (executable_module) { if (executable_module->is_multi_disc_title()) { - sb.AppendFormat(" Disc {}", executable_module->disc_number()); + // Use swapped disc number if set, otherwise use XEX header value + uint8_t disc_number = swapped_disc_number_ != 0 + ? swapped_disc_number_ + : executable_module->disc_number(); + sb.AppendFormat(" Disc {}", disc_number); } // Show XEX name if it's not default.xex @@ -2411,63 +2505,13 @@ std::string EmulatorWindow::CanonicalizeFileExtension( } void EmulatorWindow::LaunchTitleInNewProcess( - const std::filesystem::path& path_to_file, bool for_launch_data) { + const std::filesystem::path& path_to_file) { // Get the path to the current executable std::filesystem::path executable_path = xe::filesystem::GetExecutablePath(); - // Handle launch_data.txt if present - std::filesystem::path actual_path = path_to_file; - std::string launch_module_arg; - - std::string launch_flags_arg; - std::string launch_data_arg; - - if (for_launch_data) { - // Read launch_data.txt to get all launch parameters - std::filesystem::path file_path(kernel::xam::kXamModuleLoaderDataFileName); - std::ifstream file(file_path); - if (!file.is_open()) { - XELOGE("launch_data.txt not found"); - return; - } - - std::string host_path; - std::string launch_path; - std::string line; - - while (std::getline(file, line)) { - size_t eq_pos = line.find('='); - if (eq_pos == std::string::npos) { - continue; - } - - std::string key = line.substr(0, eq_pos); - std::string value = line.substr(eq_pos + 1); - - if (key == "host_path") { - host_path = value; - } else if (key == "launch_path") { - launch_path = value; - } else if (key == "launch_flags") { - launch_flags_arg = value; - } else if (key == "launch_data") { - launch_data_arg = value; - } - } - - file.close(); - - // Delete launch_data.txt - we pass everything via command line - std::filesystem::remove(kernel::xam::kXamModuleLoaderDataFileName); - - // Use the host_path as the target and launch_path as --launch_module - actual_path = host_path; - launch_module_arg = launch_path; - } - // Verify the file exists - if (!actual_path.empty() && !std::filesystem::exists(actual_path)) { - XELOGE("Cannot launch title - file not found: {}", actual_path.string()); + if (!path_to_file.empty() && !std::filesystem::exists(path_to_file)) { + XELOGE("Cannot launch title - file not found: {}", path_to_file.string()); return; } @@ -2483,25 +2527,9 @@ void EmulatorWindow::LaunchTitleInNewProcess( cmd_line += u" --config=\"" + xe::to_utf16(cvars::config) + u"\""; } - // Add --launch_module if specified - if (!launch_module_arg.empty()) { - cmd_line += - u" --launch_module=\"" + xe::to_utf16(launch_module_arg) + u"\""; - } - - // Add --launch_flags if specified (for title-to-title launches) - if (!launch_flags_arg.empty()) { - cmd_line += u" --launch_flags=" + xe::to_utf16(launch_flags_arg); - } - - // Add --launch_data if specified (for title-to-title launches) - if (!launch_data_arg.empty()) { - cmd_line += u" --launch_data=" + xe::to_utf16(launch_data_arg); - } - // Add the target game file - if (!actual_path.empty()) { - auto game_path_u16 = xe::path_to_utf16(actual_path); + if (!path_to_file.empty()) { + auto game_path_u16 = xe::path_to_utf16(path_to_file); cmd_line += u" \"" + game_path_u16 + u"\""; } @@ -2560,31 +2588,10 @@ void EmulatorWindow::LaunchTitleInNewProcess( argv.push_back(config_arg.c_str()); } - // Add --launch_module if specified - std::string launch_module_arg_str; - if (!launch_module_arg.empty()) { - launch_module_arg_str = "--launch_module=" + launch_module_arg; - argv.push_back(launch_module_arg_str.c_str()); - } - - // Add --launch_flags if specified (for title-to-title launches) - std::string launch_flags_arg_str; - if (!launch_flags_arg.empty()) { - launch_flags_arg_str = "--launch_flags=" + launch_flags_arg; - argv.push_back(launch_flags_arg_str.c_str()); - } - - // Add --launch_data if specified (for title-to-title launches) - std::string launch_data_arg_str; - if (!launch_data_arg.empty()) { - launch_data_arg_str = "--launch_data=" + launch_data_arg; - argv.push_back(launch_data_arg_str.c_str()); - } - // Add the target game file std::string target_arg; - if (!actual_path.empty()) { - target_arg = actual_path.string(); + if (!path_to_file.empty()) { + target_arg = path_to_file.string(); argv.push_back(target_arg.c_str()); } argv.push_back(nullptr); @@ -2610,11 +2617,7 @@ void EmulatorWindow::LaunchTitleInNewProcess( child_processes_.push_back(pid); #endif - if (for_launch_data) { - XELOGI("Launched new process for launch_data.txt"); - } else { - XELOGI("Launched title in new process: {}", path_to_file.string()); - } + XELOGI("Launched title in new process: {}", path_to_file.string()); // Start periodic checking now that we have a child ScheduleChildProcessCheck(); @@ -2705,36 +2708,6 @@ void EmulatorWindow::CheckChildProcessStatus() { game_list_dialog_qt_->UpdateProfileButtonState(); XELOGI("Game list dialog refreshed"); } - - // Check for launch_data.txt - FILE* file = xe::filesystem::OpenFile( - kernel::xam::kXamModuleLoaderDataFileName, "r"); - if (file) { - fclose(file); - XELOGI( - "launch_data.txt exists - cleaning up old child and launching new " - "instance"); - - // Force kill any remaining child processes before launching new one - // (they should have exited but may be stuck) -#if XE_PLATFORM_WIN32 - for (auto it = child_processes_.begin(); it != child_processes_.end();) { - TerminateProcess(*it, 0); - CloseHandle(*it); - it = child_processes_.erase(it); - } -#else - for (auto it = child_processes_.begin(); it != child_processes_.end();) { - XELOGI("Force killing stuck child process {}", *it); - kill(*it, SIGKILL); - int status; - waitpid(*it, &status, WNOHANG); - it = child_processes_.erase(it); - } -#endif - - LaunchTitleInNewProcess(std::filesystem::path(), true); - } } had_child_last_check = has_child_now; diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index 23edbe549..d8eb7eec4 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -84,8 +84,7 @@ class EmulatorWindow { void OnEmulatorInitialized(); - void LaunchTitleInNewProcess(const std::filesystem::path& path_to_file, - bool for_launch_data = false); + void LaunchTitleInNewProcess(const std::filesystem::path& path_to_file); xe::X_STATUS RunTitle(const std::filesystem::path& path_to_file); void UpdateTitle(); bool HasRunningChildProcess(); @@ -289,6 +288,8 @@ class EmulatorWindow { std::string base_title_; bool initializing_shader_storage_ = false; + // Disc number after disc swap (0 = use XEX header value) + uint8_t swapped_disc_number_ = 0; QPointer postprocessing_dialog_qt_; QPointer performance_tuning_dialog_qt_; diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index f20f63389..4484fa9df 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -589,16 +589,8 @@ bool EmulatorApp::OnInitialize() { emulator_ = std::make_unique("", storage_root, content_root, cache_root); - // Check if this is a game process (has target or launch_data.txt) or UI - // process - bool has_launch_data = false; - FILE* launch_data_file = - xe::filesystem::OpenFile(kernel::xam::kXamModuleLoaderDataFileName, "r"); - if (launch_data_file) { - has_launch_data = true; - fclose(launch_data_file); - } - bool is_game_process = !cvars::target.empty() || has_launch_data; + // Check if this is a game process (has target) or UI process + bool is_game_process = !cvars::target.empty(); #if XE_PLATFORM_WIN32 && XE_ARCH_AMD64 == 1 // Apply ntdll rdrand patch for game process only @@ -853,8 +845,7 @@ void EmulatorApp::EmulatorThread(bool is_game_process) { return; } - // Store the host path in loader_data for potential restart with - // launch_data.txt + // Store the host path in loader_data for title-to-title launches auto xam_for_path = emulator_->kernel_state()->GetKernelModule( "xam.xex"); @@ -872,32 +863,20 @@ void EmulatorApp::EmulatorThread(bool is_game_process) { auto xam = emulator_->kernel_state()->GetKernelModule( "xam.xex"); - if (xam) { - // Check if launch data was passed via command line (from UI process) - // This takes precedence over launch_data.txt - if (cvars::launch_flags != 0 || !cvars::launch_data.empty()) { - auto& loader_data = xam->loader_data(); - loader_data.launch_data_present = true; - loader_data.launch_flags = cvars::launch_flags; + // Check if launch data was passed via command line (for title-to-title) + if (xam && (cvars::launch_flags != 0 || !cvars::launch_data.empty())) { + auto& loader_data = xam->loader_data(); + loader_data.launch_data_present = true; + loader_data.launch_flags = cvars::launch_flags; - // Decode hex-encoded launch_data - if (!cvars::launch_data.empty()) { - loader_data.launch_data.clear(); - const std::string& hex = cvars::launch_data; - for (size_t i = 0; i + 1 < hex.length(); i += 2) { - std::string byte_str = hex.substr(i, 2); - uint8_t byte = - static_cast(std::stoul(byte_str, nullptr, 16)); - loader_data.launch_data.push_back(byte); - } - } - } else { - // Fall back to loading from file (legacy behavior) - xam->LoadLoaderData(); - - if (xam->loader_data().launch_data_present) { - const std::filesystem::path host_path = xam->loader_data().host_path; - emulator_->LaunchPath(host_path); + // Decode hex-encoded launch_data + if (!cvars::launch_data.empty()) { + loader_data.launch_data.clear(); + const std::string& hex = cvars::launch_data; + for (size_t i = 0; i + 1 < hex.length(); i += 2) { + std::string byte_str = hex.substr(i, 2); + uint8_t byte = static_cast(std::stoul(byte_str, nullptr, 16)); + loader_data.launch_data.push_back(byte); } } } diff --git a/src/xenia/base/logging.cc b/src/xenia/base/logging.cc index a7968855a..d38f405ee 100644 --- a/src/xenia/base/logging.cc +++ b/src/xenia/base/logging.cc @@ -47,6 +47,10 @@ DEFINE_bool(log_to_logcat, true, "Write log output to Android Logcat.", "Logging"); #else DEFINE_path(log_file, "", "Logs are written to the given file", "Logging"); +DEFINE_transient_bool(log_append, false, + "Append to existing log file instead of overwriting. " + "Used for title-to-title launches.", + "Logging"); DEFINE_bool(log_to_stdout, true, "Write log output to stdout", "Logging"); DEFINE_bool(log_to_debugprint, false, "Dump the log to DebugPrint.", "Logging"); #endif // XE_PLATFORM_ANDROID @@ -447,15 +451,17 @@ void InitializeLogging(const std::string_view app_name, bool is_game_process) { // Only enable file logging for game processes, not the UI process if (is_game_process) { FILE* log_file = nullptr; + // Use append mode for title-to-title launches to preserve log history + const char* file_mode = cvars::log_append ? "at" : "wt"; if (cvars::log_file.empty()) { // Default log file name for game process std::string file_name = fmt::format("{}.log", app_name); auto file_path = xe::filesystem::GetExecutableFolder() / file_name; - log_file = xe::filesystem::OpenFile(file_path, "wt"); + log_file = xe::filesystem::OpenFile(file_path, file_mode); } else { // User specified log file - use as-is for game process xe::filesystem::CreateParentFolder(cvars::log_file); - log_file = xe::filesystem::OpenFile(cvars::log_file, "wt"); + log_file = xe::filesystem::OpenFile(cvars::log_file, file_mode); } logger_->AddLogSink(std::make_unique(log_file, true)); } diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index 02dc3f40f..57af46c72 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -326,13 +326,24 @@ class Emulator { xe::Delegate<> on_terminate; xe::Delegate<> on_exit; - // Called when XamLoaderLaunchTitle requests a restart with launch data. - // The UI layer should show an appropriate dialog and handle termination. - std::function on_launch_data_restart() const { - return on_launch_data_restart_; + // 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) + using LaunchNewTitleCallback = std::function; + LaunchNewTitleCallback on_launch_new_title() const { + return on_launch_new_title_; } - void set_on_launch_data_restart(std::function callback) { - on_launch_data_restart_ = std::move(callback); + void set_on_launch_new_title(LaunchNewTitleCallback callback) { + on_launch_new_title_ = std::move(callback); + } + + // Called when XamSwapDisc successfully swaps to a new disc. + // Parameters: new_disc_number + using DiscSwapCallback = std::function; + DiscSwapCallback on_disc_swap() const { return on_disc_swap_; } + void set_on_disc_swap(DiscSwapCallback callback) { + on_disc_swap_ = std::move(callback); } private: @@ -383,7 +394,8 @@ class Emulator { bool restoring_; threading::Fence restore_fence_; // Fired on restore finish. - std::function on_launch_data_restart_; + LaunchNewTitleCallback on_launch_new_title_; + DiscSwapCallback on_disc_swap_; }; } // namespace xe diff --git a/src/xenia/kernel/xam/xam_content.cc b/src/xenia/kernel/xam/xam_content.cc index 5406e17d0..2d351adc6 100644 --- a/src/xenia/kernel/xam/xam_content.cc +++ b/src/xenia/kernel/xam/xam_content.cc @@ -651,8 +651,10 @@ dword_result_t XamSwapDisc_entry( } auto filesystem = kernel_state()->file_system(); - auto mount_path = "\\Device\\LauncherData"; + // Mount to Cdrom0 so the game: symlink points to the new disc + auto mount_path = "\\Device\\Cdrom0"; + // Unmount the current disc if (filesystem->ResolvePath(mount_path) != NULL) { filesystem->UnregisterDevice(mount_path); } @@ -779,6 +781,12 @@ dword_result_t XamSwapDisc_entry( xam->loader_data().host_path = xe::path_to_utf8(new_disc_path); } + // Notify UI of disc swap for title bar update + auto on_disc_swap = kernel_state()->emulator()->on_disc_swap(); + if (on_disc_swap) { + on_disc_swap(exec_info.disc_number); + } + // Success - break out of the loop break; } @@ -847,24 +855,11 @@ dword_result_t XamContentLaunchImageFromFileInternal_entry( entry, kernel_state()->emulator()->content_root(), progress, true); } - auto xam = kernel_state()->GetKernelModule("xam.xex"); - - auto& loader_data = xam->loader_data(); - loader_data.host_path = xe::path_to_utf8(host_path); - loader_data.launch_path = xex_name_; - - xam->SaveLoaderData(); - - auto display_window = kernel_state()->emulator()->display_window(); - auto imgui_drawer = kernel_state()->emulator()->imgui_drawer(); - - if (display_window && imgui_drawer) { - display_window->app_context().CallInUIThreadSynchronous([imgui_drawer]() { - xe::ui::ImGuiDialog::ShowMessageBox( - imgui_drawer, "Launching new title!", - "Launching new title. \nPlease close Xenia and launch it again. Game " - "should load automatically."); - }); + auto on_launch_new_title = kernel_state()->emulator()->on_launch_new_title(); + if (on_launch_new_title) { + XELOGI("XamContentLaunchImageFromFileInternal: spawning new title process"); + on_launch_new_title(xe::path_to_utf8(host_path), xex_name_, 0, ""); + // Callback calls quick_exit, so we don't reach here } kernel_state()->TerminateTitle(); @@ -903,24 +898,11 @@ dword_result_t XamContentLaunchImageInternal_entry(lpvoid_t content_data_ptr, entry, kernel_state()->emulator()->content_root(), progress, true); } - auto xam = kernel_state()->GetKernelModule("xam.xex"); - - auto& loader_data = xam->loader_data(); - loader_data.host_path = xe::path_to_utf8(host_path); - loader_data.launch_path = xex_path.value(); - - xam->SaveLoaderData(); - - auto display_window = kernel_state()->emulator()->display_window(); - auto imgui_drawer = kernel_state()->emulator()->imgui_drawer(); - - if (display_window && imgui_drawer) { - display_window->app_context().CallInUIThreadSynchronous([imgui_drawer]() { - xe::ui::ImGuiDialog::ShowMessageBox( - imgui_drawer, "Launching new title!", - "Launching new title. \nPlease close Xenia and launch it again. Game " - "should load automatically."); - }); + auto on_launch_new_title = kernel_state()->emulator()->on_launch_new_title(); + if (on_launch_new_title) { + XELOGI("XamContentLaunchImageInternal: spawning new title process"); + on_launch_new_title(xe::path_to_utf8(host_path), xex_path.value(), 0, ""); + // Callback calls quick_exit, so we don't reach here } kernel_state()->TerminateTitle(); diff --git a/src/xenia/kernel/xam/xam_info.cc b/src/xenia/kernel/xam/xam_info.cc index 56b08b84d..6f9b841ae 100644 --- a/src/xenia/kernel/xam/xam_info.cc +++ b/src/xenia/kernel/xam/xam_info.cc @@ -385,26 +385,55 @@ void XamLoaderLaunchTitle_entry(lpstring_t raw_name_ptr, dword_t flags) { if (raw_name_ptr) { auto path = raw_name_ptr.value(); if (path.empty()) { - // Empty path means exit to dashboard - don't save loader data + // Empty path means exit to dashboard loader_data.launch_path = "game:\\default.xex"; } else { - // Non-empty path means launching another title - save loader data - loader_data.launch_path = xe::path_to_utf8(path); + // Non-empty path means launching another title loader_data.launch_data_present = true; - xam->SaveLoaderData(); - } - if (loader_data.launch_data_present) { - // Notify the UI that a restart with launch data is requested. - // The callback can show a notification, but we terminate immediately - // to prevent the game from calling this function repeatedly. - auto on_launch_data_restart = - kernel_state()->emulator()->on_launch_data_restart(); - if (on_launch_data_restart) { - on_launch_data_restart(); + // Normalize the paths + std::filesystem::path host_path = loader_data.host_path; + std::string launch_path = xe::path_to_utf8(path); + + XELOGI("XamLoaderLaunchTitle: original host_path={}, launch_path={}", + loader_data.host_path, launch_path); + + // Remove common guest path prefixes + auto remove_prefix = [&launch_path](std::string_view prefix) { + if (launch_path.compare(0, prefix.length(), prefix) == 0) { + launch_path = launch_path.substr(prefix.length()); + } + }; + remove_prefix("game:\\"); + remove_prefix("d:\\"); + + // If host_path points to a .xex, combine with launch_path + if (host_path.extension() == ".xex") { + host_path.remove_filename(); + host_path = host_path / launch_path; + launch_path = ""; } - // Terminate immediately when launch data is present + XELOGI("XamLoaderLaunchTitle: normalized host_path={}, launch_path={}", + xe::path_to_utf8(host_path), launch_path); + + // Convert launch_data to hex string + std::string launch_data_hex; + for (uint8_t byte : loader_data.launch_data) { + launch_data_hex += fmt::format("{:02X}", byte); + } + + // Call the callback to spawn the new process directly + auto on_launch_new_title = + kernel_state()->emulator()->on_launch_new_title(); + if (on_launch_new_title) { + XELOGI("XamLoaderLaunchTitle: spawning new title process"); + on_launch_new_title(xe::path_to_utf8(host_path), launch_path, + loader_data.launch_flags, launch_data_hex); + // Callback calls quick_exit, so we don't reach here + } + + // Terminate if callback wasn't set XELOGI("XamLoaderLaunchTitle: terminating to launch new title"); kernel_state()->TerminateTitle(); // This function does not return diff --git a/src/xenia/kernel/xam/xam_module.cc b/src/xenia/kernel/xam/xam_module.cc index 9493a7c9e..7dcf0d23f 100644 --- a/src/xenia/kernel/xam/xam_module.cc +++ b/src/xenia/kernel/xam/xam_module.cc @@ -67,104 +67,6 @@ void XamModule::RegisterExportTable(xe::cpu::ExportResolver* export_resolver) { XamModule::~XamModule() {} -void XamModule::LoadLoaderData() { - std::filesystem::path file_path(kXamModuleLoaderDataFileName); - std::ifstream file(file_path); - - if (!file.is_open()) { - loader_data_.launch_data_present = false; - return; - } - - loader_data_.launch_data_present = true; - - std::string line; - while (std::getline(file, line)) { - // Find the '=' delimiter - size_t eq_pos = line.find('='); - if (eq_pos == std::string::npos) { - continue; // Skip malformed lines - } - - std::string key = line.substr(0, eq_pos); - std::string value = line.substr(eq_pos + 1); - - if (key == "host_path") { - loader_data_.host_path = value; - } else if (key == "launch_path") { - loader_data_.launch_path = value; - } else if (key == "launch_flags") { - loader_data_.launch_flags = std::stoul(value); - } else if (key == "launch_data") { - // Convert hex string back to bytes - if (!value.empty()) { - loader_data_.launch_data.clear(); - for (size_t i = 0; i < value.length(); i += 2) { - if (i + 1 < value.length()) { - std::string byte_str = value.substr(i, 2); - uint8_t byte = - static_cast(std::stoul(byte_str, nullptr, 16)); - loader_data_.launch_data.push_back(byte); - } - } - } - } - } - - file.close(); - // We read launch data. Let's remove it till next request. - std::filesystem::remove(kXamModuleLoaderDataFileName); -} - -void XamModule::SaveLoaderData() { - std::filesystem::path file_path(kXamModuleLoaderDataFileName); - std::ofstream file(file_path); - - if (!file.is_open()) { - return; - } - - std::filesystem::path host_path = loader_data_.host_path; - std::string launch_path = loader_data_.launch_path; - - auto remove_prefix = [&launch_path](std::string_view prefix) { - if (launch_path.compare(0, prefix.length(), prefix) == 0) { - launch_path = launch_path.substr(prefix.length()); - } - }; - - remove_prefix("game:\\"); - remove_prefix("d:\\"); - - if (host_path.extension() == ".xex") { - host_path.remove_filename(); - host_path = host_path / launch_path; - launch_path = ""; - } - - const std::string host_path_as_string = xe::path_to_utf8(host_path); - - // Write text format: one field per line - file << "host_path=" << host_path_as_string << "\n"; - file << "launch_path=" << launch_path << "\n"; - file << "launch_flags=" << loader_data_.launch_flags << "\n"; - file << "title_id=" << std::hex << kernel_state()->title_id() << "\n"; - - // Convert launch_data bytes to hex string - if (!loader_data_.launch_data.empty()) { - file << "launch_data="; - for (uint8_t byte : loader_data_.launch_data) { - file << std::hex << std::setw(2) << std::setfill('0') - << static_cast(byte); - } - file << "\n"; - } else { - file << "launch_data=\n"; - } - - file.close(); -} - } // namespace xam } // namespace kernel } // namespace xe diff --git a/src/xenia/kernel/xam/xam_module.h b/src/xenia/kernel/xam/xam_module.h index 760bcba50..f306aa374 100644 --- a/src/xenia/kernel/xam/xam_module.h +++ b/src/xenia/kernel/xam/xam_module.h @@ -20,9 +20,6 @@ namespace xe { namespace kernel { namespace xam { -static constexpr std::string_view kXamModuleLoaderDataFileName = - "launch_data.txt"; - class XamModule : public KernelModule { public: XamModule(Emulator* emulator, KernelState* kernel_state); @@ -40,9 +37,6 @@ class XamModule : public KernelModule { std::vector launch_data; }; - void LoadLoaderData(); - void SaveLoaderData(); - const LoaderData& loader_data() const { return loader_data_; } LoaderData& loader_data() { return loader_data_; }