[App] Fix auto-relaunching games with launch_data.bin

Move all the actual logic into the UI process and launch the
new xex using --launch_module flag to the child process. The old
handling for launch_data.bin is still in the game process for
legacy reasons, in case that's started without a UI process for
instance, but it's no longer used normally (as it's actually
bugged though somewhat functional for some cases)
This commit is contained in:
Herman S.
2025-10-27 12:59:54 +09:00
parent 91af62c6f8
commit e043a40f5e
6 changed files with 133 additions and 275 deletions
+75 -14
View File
@@ -17,6 +17,7 @@
#include <QGridLayout>
#include <QMenu>
#include <QMessageBox>
#include <QTimer>
#include "third_party/imgui/imgui.h"
#include "third_party/stb/stb_image_write.h"
@@ -24,7 +25,6 @@
#include "xenia/app/config_dialog_qt.h"
#include "xenia/app/context_menu_widget_qt.h"
#include "xenia/app/game_list_dialog_qt.h"
#include "xenia/app/launch_data_restart_dialog_qt.h"
#include "xenia/app/notification_widget_qt.h"
#include "xenia/app/postprocessing_dialog_qt.h"
#include "xenia/app/profile_dialog_qt.h"
@@ -395,11 +395,22 @@ void EmulatorWindow::OnEmulatorInitialized() {
auto* qt_window = dynamic_cast<ui::QtWindow*>(window_.get());
if (qt_window) {
emulator_->set_on_launch_data_restart([this, qt_window]() {
// Show dialog in UI thread
// Show notification in UI thread
window_->app_context().CallInUIThread([this, qt_window]() {
auto* dialog = new app::LaunchDataRestartDialogQt(
qt_window->qwindow(), window_.get(), emulator_->kernel_state());
dialog->show();
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();
// Schedule terminate and exit after notification duration
QTimer::singleShot(5000, [this]() {
if (emulator_->kernel_state()) {
emulator_->kernel_state()->TerminateTitle();
}
std::quick_exit(0);
});
});
});
}
@@ -2123,9 +2134,46 @@ void EmulatorWindow::LaunchTitleInNewProcess(
// Get the path to the current executable
std::filesystem::path executable_path = xe::filesystem::GetExecutablePath();
// Verify the file exists (unless launching for launch_data)
if (!for_launch_data && !std::filesystem::exists(path_to_file)) {
XELOGE("Cannot launch title - file not found: {}", path_to_file.string());
// Handle launch_data.bin if present
std::filesystem::path actual_path = path_to_file;
std::string launch_module_arg;
if (for_launch_data) {
// Read launch_data.bin to get the host_path and launch_path
FILE* file = xe::filesystem::OpenFile(
kernel::xam::kXamModuleLoaderDataFileName, "rb");
if (!file) {
XELOGE("launch_data.bin not found");
return;
}
// Read host_path (the disc/package path)
uint16_t host_path_length = 0;
fread(&host_path_length, sizeof(host_path_length), 1, file);
std::string host_path;
host_path.resize(host_path_length);
fread(host_path.data(), host_path_length, 1, file);
// Read launch_path (the XEX to launch)
uint16_t launch_path_length = 0;
fread(&launch_path_length, sizeof(launch_path_length), 1, file);
std::string launch_path;
launch_path.resize(launch_path_length);
fread(launch_path.data(), launch_path_length, 1, file);
fclose(file);
// Delete launch_data.bin so new process doesn't try to process it
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());
return;
}
@@ -2141,9 +2189,15 @@ void EmulatorWindow::LaunchTitleInNewProcess(
cmd_line += u" --config=\"" + xe::to_utf16(cvars::config) + u"\"";
}
// Add the target game file (unless launching for launch_data)
if (!for_launch_data) {
auto game_path_u16 = xe::path_to_utf16(path_to_file);
// Add --launch_module if specified
if (!launch_module_arg.empty()) {
cmd_line +=
u" --launch_module=\"" + xe::to_utf16(launch_module_arg) + u"\"";
}
// Add the target game file
if (!actual_path.empty()) {
auto game_path_u16 = xe::path_to_utf16(actual_path);
cmd_line += u" \"" + game_path_u16 + u"\"";
}
@@ -2187,10 +2241,17 @@ void EmulatorWindow::LaunchTitleInNewProcess(
argv.push_back(config_arg.c_str());
}
// Add the target game file (unless launching for launch_data)
// 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 the target game file
std::string target_arg;
if (!for_launch_data) {
target_arg = path_to_file.string();
if (!actual_path.empty()) {
target_arg = actual_path.string();
argv.push_back(target_arg.c_str());
}
argv.push_back(nullptr);
@@ -1,79 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/app/launch_data_restart_dialog_qt.h"
#include <QLabel>
#include <QVBoxLayout>
#include <cstdlib>
#include "xenia/kernel/kernel_state.h"
#include "xenia/ui/window.h"
namespace xe {
namespace app {
LaunchDataRestartDialogQt::LaunchDataRestartDialogQt(
QWidget* parent, ui::Window* display_window,
kernel::KernelState* kernel_state)
: QDialog(parent),
display_window_(display_window),
kernel_state_(kernel_state) {
SetupUI();
}
LaunchDataRestartDialogQt::~LaunchDataRestartDialogQt() {
// Cleanup handled by Qt
}
void LaunchDataRestartDialogQt::SetupUI() {
setWindowTitle("Title Restart Required");
setModal(true);
setAttribute(Qt::WA_DeleteOnClose);
setMinimumWidth(400);
auto* main_layout = new QVBoxLayout(this);
// Message text
auto* message_label = new QLabel(
"Title is restarting with new launch data.\n"
"Click OK to continue. Game will be loaded automatically.",
this);
message_label->setWordWrap(true);
main_layout->addWidget(message_label);
main_layout->addSpacing(20);
// OK button
auto* button_layout = new QHBoxLayout();
button_layout->addStretch();
ok_button_ = new QPushButton("OK", this);
ok_button_->setMinimumWidth(120);
connect(ok_button_, &QPushButton::clicked, this,
&LaunchDataRestartDialogQt::OnOKClicked);
button_layout->addWidget(ok_button_);
button_layout->addStretch();
main_layout->addLayout(button_layout);
}
void LaunchDataRestartDialogQt::OnOKClicked() {
// Terminate the title and quit after user clicks OK
if (kernel_state_) {
kernel_state_->TerminateTitle();
}
accept();
// Use quick_exit to avoid cleanup issues
std::quick_exit(0);
}
} // namespace app
} // namespace xe
@@ -1,49 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_APP_LAUNCH_DATA_RESTART_DIALOG_QT_H_
#define XENIA_APP_LAUNCH_DATA_RESTART_DIALOG_QT_H_
#include <QDialog>
#include <QPushButton>
namespace xe {
namespace ui {
class Window;
} // namespace ui
namespace kernel {
class KernelState;
} // namespace kernel
namespace app {
class LaunchDataRestartDialogQt : public QDialog {
Q_OBJECT
public:
LaunchDataRestartDialogQt(QWidget* parent, ui::Window* display_window,
kernel::KernelState* kernel_state);
~LaunchDataRestartDialogQt() override;
private slots:
void OnOKClicked();
private:
void SetupUI();
ui::Window* display_window_;
kernel::KernelState* kernel_state_;
QPushButton* ok_button_;
};
} // namespace app
} // namespace xe
#endif // XENIA_APP_LAUNCH_DATA_RESTART_DIALOG_QT_H_
@@ -1,107 +0,0 @@
/****************************************************************************
** Meta object code from reading C++ file 'launch_data_restart_dialog_qt.h'
**
** Created by: The Qt Meta Object Compiler version 69 (Qt 6.9.2)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/
#include <QtCore/qmetatype.h>
#include "launch_data_restart_dialog_qt.h"
#include <QtCore/qtmochelpers.h>
#include <memory>
#include <QtCore/qxptype_traits.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error \
"The header file 'launch_data_restart_dialog_qt.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 69
#error "This file was generated using the moc from 6.9.2. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif
#ifndef Q_CONSTINIT
#define Q_CONSTINIT
#endif
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
QT_WARNING_DISABLE_GCC("-Wuseless-cast")
namespace {
struct qt_meta_tag_ZN2xe3app25LaunchDataRestartDialogQtE_t {};
} // unnamed namespace
template <>
constexpr inline auto
xe::app::LaunchDataRestartDialogQt::qt_create_metaobjectdata<
qt_meta_tag_ZN2xe3app25LaunchDataRestartDialogQtE_t>() {
namespace QMC = QtMocConstants;
QtMocHelpers::StringRefStorage qt_stringData{
"xe::app::LaunchDataRestartDialogQt", "OnOKClicked", ""};
QtMocHelpers::UintData qt_methods{
// Slot 'OnOKClicked'
QtMocHelpers::SlotData<void()>(1, 2, QMC::AccessPrivate, QMetaType::Void),
};
QtMocHelpers::UintData qt_properties{};
QtMocHelpers::UintData qt_enums{};
return QtMocHelpers::metaObjectData<
LaunchDataRestartDialogQt,
qt_meta_tag_ZN2xe3app25LaunchDataRestartDialogQtE_t>(
QMC::MetaObjectFlag{}, qt_stringData, qt_methods, qt_properties,
qt_enums);
}
Q_CONSTINIT const QMetaObject xe::app::LaunchDataRestartDialogQt::staticMetaObject = { {
QMetaObject::SuperData::link<QDialog::staticMetaObject>(),
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN2xe3app25LaunchDataRestartDialogQtE_t>.stringdata,
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN2xe3app25LaunchDataRestartDialogQtE_t>.data,
qt_static_metacall,
nullptr,
qt_staticMetaObjectRelocatingContent<qt_meta_tag_ZN2xe3app25LaunchDataRestartDialogQtE_t>.metaTypes,
nullptr
} };
void xe::app::LaunchDataRestartDialogQt::qt_static_metacall(
QObject* _o, QMetaObject::Call _c, int _id, void** _a) {
auto* _t = static_cast<LaunchDataRestartDialogQt*>(_o);
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
case 0:
_t->OnOKClicked();
break;
default:;
}
}
(void)_a;
}
const QMetaObject* xe::app::LaunchDataRestartDialogQt::metaObject() const {
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject()
: &staticMetaObject;
}
void* xe::app::LaunchDataRestartDialogQt::qt_metacast(const char* _clname) {
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_staticMetaObjectStaticContent<qt_meta_tag_ZN2xe3app25LaunchDataRestartDialogQtE_t>.strings))
return static_cast<void*>(this);
return QDialog::qt_metacast(_clname);
}
int xe::app::LaunchDataRestartDialogQt::qt_metacall(QMetaObject::Call _c,
int _id, void** _a) {
_id = QDialog::qt_metacall(_c, _id, _a);
if (_id < 0) return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 1) qt_static_metacall(this, _c, _id, _a);
_id -= 1;
}
if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 1) *reinterpret_cast<QMetaType*>(_a[0]) = QMetaType();
_id -= 1;
}
return _id;
}
QT_WARNING_POP
+57 -26
View File
@@ -1434,42 +1434,73 @@ void Emulator::RemoveGameConfigLoadCallback(GameConfigLoadCallback* callback) {
game_config_load_callbacks_.erase(it);
}
std::string Emulator::RemountAndResolveLaunchPath(
const std::string& launch_path) {
// Normalize path separators for the platform
std::string normalized_path = launch_path;
#if XE_PLATFORM_LINUX
// Convert backslashes to forward slashes for consistent paths on Linux
std::replace(normalized_path.begin(), normalized_path.end(), '\\', '/');
#endif
// Get the current game:\ symbolic link path
std::string symbolic_link_path;
if (!kernel_state_->file_system()->FindSymbolicLink(kDefaultGameSymbolicLink,
symbolic_link_path)) {
return "";
}
std::filesystem::path file_path = symbolic_link_path;
// Remove previous symbolic links.
// Some titles can provide root within specific directory.
kernel_state_->file_system()->UnregisterSymbolicLink(
kDefaultPartitionSymbolicLink);
kernel_state_->file_system()->UnregisterSymbolicLink(
kDefaultGameSymbolicLink);
file_path /= std::filesystem::path(normalized_path);
// Re-register symbolic links to point to the parent directory of the module
kernel_state_->file_system()->RegisterSymbolicLink(
kDefaultPartitionSymbolicLink, xe::path_to_utf8(file_path.parent_path()));
kernel_state_->file_system()->RegisterSymbolicLink(
kDefaultGameSymbolicLink, xe::path_to_utf8(file_path.parent_path()));
return xe::path_to_utf8(file_path);
}
std::string Emulator::FindLaunchModule() {
std::string path("game:\\");
auto xam = kernel_state()->GetKernelModule<kernel::xam::XamModule>("xam.xex");
if (!xam->loader_data().launch_path.empty()) {
std::string symbolic_link_path;
if (kernel_state_->file_system()->FindSymbolicLink(kDefaultGameSymbolicLink,
symbolic_link_path)) {
std::filesystem::path file_path = symbolic_link_path;
// Remove previous symbolic links.
// Some titles can provide root within specific directory.
kernel_state_->file_system()->UnregisterSymbolicLink(
kDefaultPartitionSymbolicLink);
kernel_state_->file_system()->UnregisterSymbolicLink(
kDefaultGameSymbolicLink);
std::string launch_path = xam->loader_data().launch_path;
#if XE_PLATFORM_LINUX
// Convert backslashes to forward slashes for consistent paths on Linux
std::replace(launch_path.begin(), launch_path.end(), '\\', '/');
#endif
file_path /= std::filesystem::path(launch_path);
kernel_state_->file_system()->RegisterSymbolicLink(
kDefaultPartitionSymbolicLink,
xe::path_to_utf8(file_path.parent_path()));
kernel_state_->file_system()->RegisterSymbolicLink(
kDefaultGameSymbolicLink, xe::path_to_utf8(file_path.parent_path()));
return xe::path_to_utf8(file_path);
std::string result =
RemountAndResolveLaunchPath(xam->loader_data().launch_path);
if (!result.empty()) {
return result;
}
}
if (!cvars::launch_module.empty()) {
return path + cvars::launch_module;
std::string launch_module = cvars::launch_module;
#if XE_PLATFORM_LINUX
// Convert backslashes to forward slashes for consistent paths on Linux
std::replace(launch_module.begin(), launch_module.end(), '\\', '/');
#endif
// If the module is in a subdirectory, remount game:\ to that subdirectory
std::filesystem::path module_path(launch_module);
if (module_path.has_parent_path() && module_path.parent_path() != ".") {
std::string result = RemountAndResolveLaunchPath(launch_module);
if (!result.empty()) {
return result;
}
}
// No subdirectory, just return as guest path
return path + launch_module;
}
return path + "default.xex";
+1
View File
@@ -367,6 +367,7 @@ class Emulator {
void AddGameConfigLoadCallback(GameConfigLoadCallback* callback);
void RemoveGameConfigLoadCallback(GameConfigLoadCallback* callback);
std::string RemountAndResolveLaunchPath(const std::string& launch_path);
std::string FindLaunchModule();
X_STATUS CompleteLaunch(const std::filesystem::path& path,