diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 667716320..680ef5d9e 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -9,6 +9,9 @@ #include "xenia/app/emulator_window.h" +#include +#include +#include #include "third_party/imgui/imgui.h" #include "third_party/stb/stb_image_write.h" #include "third_party/tomlplusplus/toml.hpp" @@ -17,11 +20,56 @@ #include "xenia/base/clock.h" #include "xenia/base/cvar.h" #include "xenia/base/debugging.h" +#include "xenia/base/filesystem.h" #include "xenia/base/logging.h" #include "xenia/base/platform.h" #include "xenia/base/profiling.h" #include "xenia/base/system.h" #include "xenia/base/threading.h" +#include "xenia/config.h" + +#if XE_PLATFORM_WIN32 +#include +#else +#include +#include +#include +#include +#endif + +#if XE_PLATFORM_LINUX +#include +#include +#include +#include +// X11 headers - include before other headers that might define None +#include +#include +#include +#include +#include +#include +#include +#include +#include // for usleep +// Undefine X11 macros that conflict with our code +#ifdef None +#undef None +#endif +#ifdef Success +#undef Success +#endif +#ifdef CursorShape +#undef CursorShape +#endif +#ifdef Status +#undef Status +#endif +#ifdef Bool +#undef Bool +#endif +#endif + #include "xenia/cpu/processor.h" #include "xenia/emulator.h" #include "xenia/gpu/command_processor.h" @@ -50,6 +98,7 @@ DECLARE_bool(debug); DECLARE_string(hid); DECLARE_bool(guide_button); +DECLARE_string(config); DECLARE_bool(clear_memory_page_state); @@ -168,9 +217,11 @@ constexpr std::string_view kBaseTitle = "Xenia-edge"; EmulatorWindow::EmulatorWindow(Emulator* emulator, ui::WindowedAppContext& app_context, - uint32_t width, uint32_t height) + uint32_t width, uint32_t height, + bool is_game_process) : emulator_(emulator), app_context_(app_context), + is_game_process_(is_game_process), window_listener_(*this), window_(ui::Window::Create(app_context, kBaseTitle, width, height)), imgui_drawer_( @@ -198,10 +249,10 @@ EmulatorWindow::EmulatorWindow(Emulator* emulator, std::unique_ptr EmulatorWindow::Create( Emulator* emulator, ui::WindowedAppContext& app_context, uint32_t width, - uint32_t height) { + uint32_t height, bool is_game_process) { assert_true(app_context.IsInUIThread()); - std::unique_ptr emulator_window( - new EmulatorWindow(emulator, app_context, width, height)); + std::unique_ptr emulator_window(new EmulatorWindow( + emulator, app_context, width, height, is_game_process)); if (!emulator_window->Initialize()) { return nullptr; } @@ -209,6 +260,38 @@ std::unique_ptr EmulatorWindow::Create( } EmulatorWindow::~EmulatorWindow() { + // Kill all child processes when the parent window is destroyed +#if XE_PLATFORM_WIN32 + for (HANDLE process : child_processes_) { + DWORD exit_code; + // Check if process is still running + if (GetExitCodeProcess(process, &exit_code) && exit_code == STILL_ACTIVE) { + // Terminate the process + TerminateProcess(process, 0); + // Wait briefly for it to exit + WaitForSingleObject(process, 1000); + } + // Close the handle regardless + CloseHandle(process); + } +#else + for (pid_t pid : child_processes_) { + // Check if process still exists before trying to kill it + if (kill(pid, 0) == 0) { + // Process exists, send SIGTERM to gracefully terminate + kill(pid, SIGTERM); + // Give it a moment to exit gracefully + usleep(100000); // 100ms + // If still running, force kill + kill(pid, SIGKILL); + } + // Try to reap it if it's a zombie + int status; + waitpid(pid, &status, WNOHANG); + } +#endif + child_processes_.clear(); + // Notify the ImGui drawer that the immediate drawer is being destroyed. ShutdownGraphicsSystemPresenterPainting(); } @@ -263,6 +346,16 @@ void EmulatorWindow::OnEmulatorInitialized() { disable_hotkeys_ = true; } + // Show recently played games list when the app starts + if (!recently_launched_titles_.empty() && !is_game_process_) { + recent_titles_ui_ = new RecentTitlesUI(imgui_drawer_.get(), this); + } + + // Start periodic child process checking if this is UI process + if (!is_game_process_) { + ScheduleChildProcessCheck(); + } + emulator_initialized_ = true; window_->SetMainMenuEnabled(true); // When the user can see that the emulator isn't initializing anymore (the @@ -284,6 +377,36 @@ void EmulatorWindow::OnEmulatorInitialized() { } void EmulatorWindow::EmulatorWindowListener::OnClosing(ui::UIEvent& e) { + // Game process: exit immediately without cleanup to avoid Vulkan hangs + if (emulator_window_.is_game_process_) { + std::quick_exit(0); + } + + // UI process: kill all child processes when the parent window is closing +#if XE_PLATFORM_WIN32 + for (HANDLE process : emulator_window_.child_processes_) { + DWORD exit_code; + if (GetExitCodeProcess(process, &exit_code)) { + if (exit_code == STILL_ACTIVE) { + XELOGI("Terminating child process"); + TerminateProcess(process, 0); + // Don't wait, just close handle + } + } + CloseHandle(process); + } +#else + for (pid_t pid : emulator_window_.child_processes_) { + // Check if process is still alive + if (kill(pid, 0) == 0) { + XELOGI("Terminating child process {}", pid); + // Send SIGKILL directly for immediate termination + kill(pid, SIGKILL); + } + } +#endif + + emulator_window_.child_processes_.clear(); emulator_window_.app_context_.QuitFromUIThread(); } @@ -291,6 +414,15 @@ void EmulatorWindow::EmulatorWindowListener::OnFileDrop(ui::FileDropEvent& e) { emulator_window_.FileDrop(e.filename()); } +void EmulatorWindow::EmulatorWindowListener::OnGotFocus(ui::UISetupEvent& e) { + // Check child process status when window regains focus + // This handles the case where a child process exits while our window doesn't + // have focus + if (!emulator_window_.is_game_process_) { + emulator_window_.CheckChildProcessStatus(); + } +} + void EmulatorWindow::EmulatorWindowListener::OnKeyDown(ui::KeyEvent& e) { emulator_window_.OnKeyDown(e); } @@ -320,9 +452,9 @@ void EmulatorWindow::DisplayConfigDialog::OnDraw(ImGuiIO& io) { ImGui::SetNextWindowPos(ImVec2(20, 20), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(20, 20), ImGuiCond_FirstUseEver); // Alpha from Dear ImGui tooltips (0.35 from the overlay provides too low - // visibility). Translucent so some effect of the changes can still be seen - // through it. - ImGui::SetNextWindowBgAlpha(0.6f); + // visibility). Slightly translucent so some effect of the changes can still + // be seen through it, but more opaque for better readability. + ImGui::SetNextWindowBgAlpha(0.85f); bool dialog_open = true; if (!ImGui::Begin("Post-processing", &dialog_open, ImGuiWindowFlags_NoCollapse | @@ -569,171 +701,298 @@ bool EmulatorWindow::Initialize() { window_->AddListener(&window_listener_); window_->AddInputListener(&window_listener_, kZOrderEmulatorWindowInput); - // Main menu. - // FIXME: This code is really messy. - auto main_menu = MenuItem::Create(MenuItem::Type::kNormal); - auto file_menu = MenuItem::Create(MenuItem::Type::kPopup, "&File"); - auto recent_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Open Recent"); - auto zar_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Zar Package"); - FillRecentlyLaunchedTitlesMenu(recent_menu.get()); - { - file_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "&Open...", "Ctrl+O", - std::bind(&EmulatorWindow::FileOpen, this))); - file_menu->AddChild(std::move(recent_menu)); - file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - file_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "Install Content...", - std::bind(&EmulatorWindow::InstallContent, this))); - zar_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "Create", - std::bind(&EmulatorWindow::CreateZarchive, this))); - zar_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "Extract", - std::bind(&EmulatorWindow::ExtractZarchive, this))); - file_menu->AddChild(std::move(zar_menu)); + // Set up callback to notify child processes when config is saved + if (!is_game_process_) { + config::SetConfigSavedCallback( + [this]() { SendCommandToChild("reload_config"); }); + } + +#if XE_PLATFORM_LINUX + // If this is a game process, create a named pipe for IPC + if (is_game_process_) { + pid_t pid = getpid(); + std::string pipe_path = fmt::format("/tmp/xenia_ipc_{}", pid); + + // Remove any existing pipe + unlink(pipe_path.c_str()); + + // Create the named pipe + if (mkfifo(pipe_path.c_str(), 0666) == 0) { + XELOGI("Created IPC pipe at: {}", pipe_path); + + // Start a thread to listen for commands + std::thread ipc_thread([this, pipe_path]() { + while (!emulator_initialized_) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + XELOGI("Starting IPC listener thread"); + char buffer[256]; + while (true) { + int fd = open(pipe_path.c_str(), O_RDONLY); + if (fd == -1) { + XELOGW("Failed to open pipe for reading: {}", strerror(errno)); + break; + } + + ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1); + if (bytes_read > 0) { + buffer[bytes_read] = '\0'; + std::string command(buffer); + // Remove trailing newline + if (!command.empty() && command.back() == '\n') { + command.pop_back(); + } + + XELOGI("Received IPC command: '{}'", command); + + // Execute command directly - if we get crashes, we'll need to use + // UI thread + if (command == "toggle_fullscreen") { + ToggleFullscreen(); + } else if (command == "take_screenshot") { + TakeScreenshot(); + } else if (command == "toggle_profiler") { + Profiler::ToggleDisplay(); + } else if (command == "gpu_trace_frame") { + GpuTraceFrame(); + } else if (command == "gpu_clear_caches") { + GpuClearCaches(); + } else if (command == "toggle_display_config") { + ToggleDisplayConfigDialog(); + } else if (command == "cpu_time_scalar_reset") { + CpuTimeScalarReset(); + } else if (command == "cpu_time_scalar_half") { + CpuTimeScalarSetHalf(); + } else if (command == "cpu_time_scalar_double") { + CpuTimeScalarSetDouble(); + } else if (command == "break_debugger") { + CpuBreakIntoDebugger(); + } else if (command == "reload_config") { + XELOGI("Reloading config from parent process request"); + config::ReloadConfig(); + // Sync the profile login state with the reloaded config + if (emulator_ && emulator_->kernel_state() && + emulator_->kernel_state()->xam_state() && + emulator_->kernel_state()->xam_state()->profile_manager()) { + emulator_->kernel_state() + ->xam_state() + ->profile_manager() + ->SyncProfilesWithConfig(); + XELOGI("Profile login state synchronized with config"); + } + } else { + XELOGW("Unknown IPC command: {}", command); + } + } + close(fd); + + // Check if we should exit + if (bytes_read <= 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + + // Clean up pipe on exit + unlink(pipe_path.c_str()); + XELOGI("IPC listener thread exiting"); + }); + ipc_thread.detach(); + } else { + XELOGW("Failed to create IPC pipe: {}", strerror(errno)); + } + } +#endif + + // Main menu - only create for UI process, not game process + if (!is_game_process_) { + // UI process - create the menu bar + // FIXME: This code is really messy. + auto main_menu = MenuItem::Create(MenuItem::Type::kNormal); + + // Create File menu with a callback that checks child status when clicked + auto file_menu = + MenuItem::Create(MenuItem::Type::kPopup, "&File", "", [this]() { + // Check child process status when File menu is clicked + CheckChildProcessStatus(); + }); + file_menu_ = file_menu.get(); + auto recent_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Open Recent"); + auto zar_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Zar Package"); + FillRecentlyLaunchedTitlesMenu(recent_menu.get()); + { + auto open_item = MenuItem::Create(MenuItem::Type::kString, "&Open...", + "Ctrl+O", [this]() { + // Check child process status when + // menu item is clicked + CheckChildProcessStatus(); + FileOpen(); + }); + file_open_item_ = open_item.get(); + file_menu->AddChild(std::move(open_item)); + file_open_recent_menu_ = recent_menu.get(); + file_menu->AddChild(std::move(recent_menu)); + file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + file_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "Install Content...", + std::bind(&EmulatorWindow::InstallContent, this))); + zar_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "Create", + std::bind(&EmulatorWindow::CreateZarchive, this))); + zar_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "Extract", + std::bind(&EmulatorWindow::ExtractZarchive, this))); + file_menu->AddChild(std::move(zar_menu)); #ifdef DEBUG - file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - file_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "Close", - std::bind(&EmulatorWindow::FileClose, this))); + file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + file_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "Close", + std::bind(&EmulatorWindow::FileClose, this))); #endif // #ifdef DEBUG - file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - file_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "Show content directory...", - std::bind(&EmulatorWindow::ShowContentDirectory, this))); - file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - file_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "&Configuration Manager", - std::bind(&EmulatorWindow::ShowConfigDialog, this))); - file_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "&Manage patches", - std::bind(&EmulatorWindow::ShowPatchesDialog, this))); - file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - file_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "E&xit", "Alt+F4", - [this]() { window_->RequestClose(); })); - } - main_menu->AddChild(std::move(file_menu)); + file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + file_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "Show content directory...", + std::bind(&EmulatorWindow::ShowContentDirectory, this))); + file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + file_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "&Configuration Manager", + std::bind(&EmulatorWindow::ShowConfigDialog, this))); + file_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Manage patches", + std::bind(&EmulatorWindow::ShowPatchesDialog, this))); + file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + file_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "E&xit", "Alt+F4", + [this]() { window_->RequestClose(); })); + } + main_menu->AddChild(std::move(file_menu)); - // Profile Menu - auto profile_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Profile"); - { - profile_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "&Show Profile Menu", "", - std::bind(&EmulatorWindow::ToggleProfilesConfigDialog, this))); - } - main_menu->AddChild(std::move(profile_menu)); + // Profile Menu + auto profile_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Profile"); + { + profile_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Show Profile Menu", "", + std::bind(&EmulatorWindow::ToggleProfilesConfigDialog, this))); + } + main_menu->AddChild(std::move(profile_menu)); - // CPU menu. - auto cpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&CPU"); - { - cpu_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "&Reset Time Scalar", "Num+*", - std::bind(&EmulatorWindow::CpuTimeScalarReset, this))); - cpu_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "Time Scalar /= 2", "Num+-", - std::bind(&EmulatorWindow::CpuTimeScalarSetHalf, this))); - cpu_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "Time Scalar *= 2", "Num++", - std::bind(&EmulatorWindow::CpuTimeScalarSetDouble, this))); - } - cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - { - cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kString, - "Toggle Profiler &Display", "F3", - []() { Profiler::ToggleDisplay(); })); - cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kString, - "&Pause/Resume Profiler", "`", - []() { Profiler::TogglePause(); })); - } - cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - { - cpu_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "&Break and Show Guest Debugger", - "Pause/Break", std::bind(&EmulatorWindow::CpuBreakIntoDebugger, this))); - cpu_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "&Break into Host Debugger", - "Ctrl+Pause/Break", - std::bind(&EmulatorWindow::CpuBreakIntoHostDebugger, this))); - } - main_menu->AddChild(std::move(cpu_menu)); + // CPU menu. + auto cpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&CPU"); + { + cpu_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Reset Time Scalar", "Numpad *", + std::bind(&EmulatorWindow::CpuTimeScalarReset, this))); + cpu_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "Time Scalar /= 2", "Numpad -", + std::bind(&EmulatorWindow::CpuTimeScalarSetHalf, this))); + cpu_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "Time Scalar *= 2", "Numpad +", + std::bind(&EmulatorWindow::CpuTimeScalarSetDouble, this))); + } + cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + { + cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kString, + "Toggle Profiler &Display", "F3", + []() { Profiler::ToggleDisplay(); })); + cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kString, + "&Pause/Resume Profiler", "`", + []() { Profiler::TogglePause(); })); + } + cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + { + cpu_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Break and Show Guest Debugger", + "Pause/Break", + std::bind(&EmulatorWindow::CpuBreakIntoDebugger, this))); + cpu_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Break into Host Debugger", + "Ctrl+Pause/Break", + std::bind(&EmulatorWindow::CpuBreakIntoHostDebugger, this))); + } + main_menu->AddChild(std::move(cpu_menu)); - // GPU menu. - auto gpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&GPU"); - { - gpu_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "&Trace Frame", "F4", - std::bind(&EmulatorWindow::GpuTraceFrame, this))); - } - gpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - { - gpu_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "&Clear Runtime Caches", "F5", - std::bind(&EmulatorWindow::GpuClearCaches, this))); - } - main_menu->AddChild(std::move(gpu_menu)); + // GPU menu. + auto gpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&GPU"); + { + gpu_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Trace Frame", "F4", [this]() { + ExecuteOrForward(std::bind(&EmulatorWindow::GpuTraceFrame, this), + ui::VirtualKey::kF4); + })); + } + gpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + { + gpu_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Clear Runtime Caches", "F5", + std::bind(&EmulatorWindow::GpuClearCaches, this))); + } + main_menu->AddChild(std::move(gpu_menu)); - // Display menu. - auto display_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Display"); - { - display_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "&Post-processing settings", "F6", - std::bind(&EmulatorWindow::ToggleDisplayConfigDialog, this))); - } - display_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - { - display_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "&Fullscreen", "F11", - std::bind(&EmulatorWindow::ToggleFullscreen, this))); - display_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "&Take Screenshot", "F12", - std::bind(&EmulatorWindow::TakeScreenshot, this))); - } - main_menu->AddChild(std::move(display_menu)); + // Display menu. + auto display_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Display"); + { + display_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Post-processing settings", "F6", + std::bind(&EmulatorWindow::ToggleDisplayConfigDialog, this))); + } + display_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + { + display_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Fullscreen", "F11", [this]() { + ExecuteOrForward(std::bind(&EmulatorWindow::ToggleFullscreen, this), + ui::VirtualKey::kF11); + })); + display_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Take Screenshot", "F12", [this]() { + ExecuteOrForward(std::bind(&EmulatorWindow::TakeScreenshot, this), + ui::VirtualKey::kF12); + })); + } + main_menu->AddChild(std::move(display_menu)); - // HID menu. - auto hid_menu = MenuItem::Create(MenuItem::Type::kPopup, "&HID"); - { - hid_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "&Toggle controller vibration", "", - std::bind(&EmulatorWindow::ToggleControllerVibration, this))); - hid_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "&Display controller hotkeys", "", - std::bind(&EmulatorWindow::DisplayHotKeysConfig, this))); - } - main_menu->AddChild(std::move(hid_menu)); + // HID menu. + auto hid_menu = MenuItem::Create(MenuItem::Type::kPopup, "&HID"); + { + hid_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Toggle controller vibration", "", + std::bind(&EmulatorWindow::ToggleControllerVibration, this))); + hid_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&Display controller hotkeys", "", + std::bind(&EmulatorWindow::DisplayHotKeysConfig, this))); + } + main_menu->AddChild(std::move(hid_menu)); - // Help menu. - auto help_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Help"); - { - help_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "FA&Q...", "F1", - std::bind(&EmulatorWindow::ShowFAQ, this))); - help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - help_menu->AddChild( - MenuItem::Create(MenuItem::Type::kString, "Game &compatibility...", - std::bind(&EmulatorWindow::ShowCompatibility, this))); - help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - help_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "Build commit on GitHub...", "F2", - std::bind(&EmulatorWindow::ShowBuildCommit, this))); - help_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "Recent changes on GitHub...", []() { - LaunchWebBrowser( - "https://github.com/has207/xenia-edge/" - "compare/" XE_BUILD_COMMIT "..." XE_BUILD_BRANCH); - })); - help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); - help_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, "&About...", - []() { LaunchWebBrowser("https://xenia.jp/about/"); })); - } - main_menu->AddChild(std::move(help_menu)); + // Help menu. + auto help_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Help"); + { + help_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "FA&Q...", "F1", + std::bind(&EmulatorWindow::ShowFAQ, this))); + help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + help_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "Game &compatibility...", + std::bind(&EmulatorWindow::ShowCompatibility, this))); + help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + help_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "Build commit on GitHub...", "F2", + std::bind(&EmulatorWindow::ShowBuildCommit, this))); + help_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "Recent changes on GitHub...", []() { + LaunchWebBrowser( + "https://github.com/has207/xenia-edge/" + "compare/" XE_BUILD_COMMIT "..." XE_BUILD_BRANCH); + })); + help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); + help_menu->AddChild(MenuItem::Create( + MenuItem::Type::kString, "&About...", + []() { LaunchWebBrowser("https://xenia.jp/about/"); })); + } + main_menu->AddChild(std::move(help_menu)); - window_->SetMainMenu(std::move(main_menu)); + window_->SetMainMenu(std::move(main_menu)); - window_->SetMainMenuEnabled(false); + window_->SetMainMenuEnabled(false); + } // End of menu creation for UI process UpdateTitle(); @@ -839,6 +1098,38 @@ void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) { return; } + // If we're the UI process and have a child process running, forward certain + // keys + if (!is_game_process_ && HasRunningChildProcess()) { + // Forward keys that should be handled by the game process + switch (e.virtual_key()) { + case ui::VirtualKey::kF11: + case ui::VirtualKey::kF12: + case ui::VirtualKey::kMultiply: // Numpad * + case ui::VirtualKey::kSubtract: // Numpad - + case ui::VirtualKey::kAdd: // Numpad + + case ui::VirtualKey::kF3: + case ui::VirtualKey::kF4: + case ui::VirtualKey::kF5: + case ui::VirtualKey::kF6: + SendKeyToChild(e.virtual_key(), e.is_ctrl_pressed(), e.is_alt_pressed(), + e.is_shift_pressed()); + e.set_handled(true); + return; + case ui::VirtualKey::kPause: + if (e.is_ctrl_pressed() && e.is_shift_pressed()) { + SendKeyToChild(e.virtual_key(), e.is_ctrl_pressed(), + e.is_alt_pressed(), e.is_shift_pressed()); + e.set_handled(true); + return; + } + break; + default: + // Let other keys fall through to be handled locally + break; + } + } + switch (e.virtual_key()) { case ui::VirtualKey::kO: { if (!e.is_ctrl_pressed()) { @@ -1047,7 +1338,8 @@ void EmulatorWindow::FileDrop(const std::filesystem::path& path) { return; } - RunTitle(path); + // Launch the title in a new process instead of loading it here + LaunchTitleInNewProcess(path); } void EmulatorWindow::FileOpen() { @@ -1071,8 +1363,10 @@ void EmulatorWindow::FileOpen() { if (!selected_files.empty()) { path = selected_files[0]; } - // Only run the title if a file is selected - RunTitle(path); + // Launch the title in a new process instead of loading it here + if (!path.empty()) { + LaunchTitleInNewProcess(path); + } } } @@ -1675,7 +1969,7 @@ EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey( if (selected_title_index < recently_launched_titles_.size()) { app_context().CallInUIThread([this]() { - RunTitle( + LaunchTitleInNewProcess( recently_launched_titles_[selected_title_index].path_to_file); }); } @@ -1951,6 +2245,350 @@ std::string EmulatorWindow::CanonicalizeFileExtension( return xe::utf8::lower_ascii(xe::path_to_utf8(path.extension())); } +void EmulatorWindow::LaunchTitleInNewProcess( + const std::filesystem::path& path_to_file, bool for_launch_data) { + // 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()); + return; + } + +#if XE_PLATFORM_WIN32 + // On Windows, build command line using Xenia's Unicode path handling + auto exe_path_u16 = xe::path_to_utf16(executable_path); + + // Build full command line with quotes for paths that may contain spaces + std::u16string cmd_line = u"\"" + exe_path_u16 + u"\""; + + // Pass the config file path if one is being used + if (!cvars::config.empty()) { + 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); + cmd_line += u" \"" + game_path_u16 + u"\""; + } + + STARTUPINFOW si = {}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi = {}; + + if (!CreateProcessW(nullptr, // Application name (use command line) + const_cast(reinterpret_cast( + cmd_line.c_str())), // Command line + nullptr, // Process attributes + nullptr, // Thread attributes + FALSE, // Inherit handles + CREATE_NEW_CONSOLE, // Creation flags + nullptr, // Environment + nullptr, // Current directory + &si, // Startup info + &pi)) { // Process information + XELOGE("Failed to launch new process: {}", GetLastError()); + return; + } + + // Store the process handle so we can terminate it later if needed + child_processes_.push_back(pi.hProcess); + + // Close thread handle as we don't need it + CloseHandle(pi.hThread); +#else + // On Linux/Unix, use fork/exec for proper process creation + pid_t pid = fork(); + + if (pid == 0) { + // Child process + std::vector argv; + argv.push_back(executable_path.c_str()); + + // Pass the config file if one is being used + std::string config_arg; + if (!cvars::config.empty()) { + config_arg = "--config=" + cvars::config; + argv.push_back(config_arg.c_str()); + } + + // Add the target game file (unless launching for launch_data) + std::string target_arg; + if (!for_launch_data) { + target_arg = path_to_file.string(); + argv.push_back(target_arg.c_str()); + } + argv.push_back(nullptr); + + // Execute the new process + execv(executable_path.c_str(), const_cast(argv.data())); + + // If execv returns, it failed + XELOGE("Failed to execute: {}", executable_path.string()); + std::exit(1); + } else if (pid < 0) { + // Fork failed + XELOGE("Failed to fork process"); + return; + } + // Parent process continues - store child PID + child_processes_.push_back(pid); +#endif + + if (for_launch_data) { + XELOGI("Launched new process for launch_data.bin"); + } else { + XELOGI("Launched title in new process: {}", path_to_file.string()); + } + + // Start periodic checking now that we have a child + ScheduleChildProcessCheck(); +} + +bool EmulatorWindow::HasRunningChildProcess() { + // Simple check if any child processes are still alive +#if XE_PLATFORM_WIN32 + for (const auto& process : child_processes_) { + DWORD exit_code; + if (GetExitCodeProcess(process, &exit_code) && exit_code == STILL_ACTIVE) { + return true; + } + } +#else + // Clean up any zombies first + for (auto it = child_processes_.begin(); it != child_processes_.end();) { + int status; + pid_t result = waitpid(*it, &status, WNOHANG); + if (result > 0) { + // Process has exited, remove from list + XELOGI("Reaped child process {}", *it); + it = child_processes_.erase(it); + } else if (result < 0 && errno == ECHILD) { + // Process doesn't exist anymore + XELOGI("Child process {} no longer exists", *it); + it = child_processes_.erase(it); + } else { + // Process still running + ++it; + } + } + + return !child_processes_.empty(); +#endif + + return false; +} + +void EmulatorWindow::CheckChildProcessStatus() { + static bool had_child_last_check = false; + bool has_child_now = HasRunningChildProcess(); + + // Detect transition from having child to no child + if (had_child_last_check && !has_child_now) { + XELOGI("Child process exited, reloading recent titles"); + LoadRecentlyLaunchedTitles(); + XELOGI("Recent titles reloaded, count: {}", + recently_launched_titles_.size()); + + // Reload the recent titles UI dialog if it's open + if (recent_titles_ui_) { + recent_titles_ui_->LoadRecentTitles(); + XELOGI("Recent titles UI dialog refreshed"); + } + + // Check for launch_data.bin + FILE* file = xe::filesystem::OpenFile( + kernel::xam::kXamModuleLoaderDataFileName, "rb"); + if (file) { + fclose(file); + XELOGI( + "launch_data.bin 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; + + // Update menu items based on whether a child process is running + if (file_open_item_) { + file_open_item_->SetEnabled(!has_child_now); + } + if (file_open_recent_menu_) { + file_open_recent_menu_->SetEnabled(!has_child_now); + } + + // Keep checking periodically if this is the UI process + if (!is_game_process_) { + ScheduleChildProcessCheck(); + } +} + +void EmulatorWindow::ScheduleChildProcessCheck() { + // Schedule check to run in 500ms on a background thread + std::thread([this]() { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + app_context_.CallInUIThread([this]() { CheckChildProcessStatus(); }); + }).detach(); +} + +void EmulatorWindow::SendCommandToChild(const std::string& command) { +#if XE_PLATFORM_LINUX + if (!child_processes_.empty()) { + // Use a named pipe to send commands to child process + std::string pipe_path = + fmt::format("/tmp/xenia_ipc_{}", child_processes_[0]); + + int fd = open(pipe_path.c_str(), O_WRONLY | O_NONBLOCK); + if (fd != -1) { + std::string message = command + "\n"; + ssize_t written = write(fd, message.c_str(), message.length()); + close(fd); + + if (written > 0) { + XELOGI("Sent command '{}' to child process via pipe", command); + } else { + XELOGW("Failed to write command to pipe"); + } + } else { + XELOGW("Could not open pipe {} for writing: {}", pipe_path, + strerror(errno)); + } + } +#elif XE_PLATFORM_WIN32 + // Windows implementation could use named pipes or other IPC + XELOGW("Command forwarding not yet implemented for Windows"); +#endif +} + +void EmulatorWindow::SendKeyToChild(ui::VirtualKey key, bool ctrl, bool alt, + bool shift) { +#if XE_PLATFORM_LINUX + // Convert key combination to command string + std::string command; + switch (key) { + case ui::VirtualKey::kF3: + command = "toggle_profiler"; + break; + case ui::VirtualKey::kF4: + command = "gpu_trace_frame"; + break; + case ui::VirtualKey::kF5: + command = "gpu_clear_caches"; + break; + case ui::VirtualKey::kF6: + command = "toggle_display_config"; + break; + case ui::VirtualKey::kF11: + command = "toggle_fullscreen"; + break; + case ui::VirtualKey::kF12: + command = "take_screenshot"; + break; + case ui::VirtualKey::kMultiply: + command = "cpu_time_scalar_reset"; + break; + case ui::VirtualKey::kSubtract: + command = "cpu_time_scalar_half"; + break; + case ui::VirtualKey::kAdd: + command = "cpu_time_scalar_double"; + break; + case ui::VirtualKey::kPause: + if (ctrl && shift) command = "break_debugger"; + break; + default: + XELOGW("Unknown key combination for command forwarding"); + return; + } + + if (!command.empty()) { + SendCommandToChild(command); + } +#endif + +#if XE_PLATFORM_WIN32 + // Windows implementation + if (!child_processes_.empty()) { + // Get the process ID from our first child + DWORD pid = GetProcessId(child_processes_[0]); + + // Find windows belonging to this process + struct EnumData { + DWORD pid; + HWND hwnd; + } data = {pid, nullptr}; + + EnumWindows( + [](HWND hwnd, LPARAM lParam) -> BOOL { + auto* data = reinterpret_cast(lParam); + DWORD window_pid; + GetWindowThreadProcessId(hwnd, &window_pid); + if (window_pid == data->pid && IsWindowVisible(hwnd)) { + data->hwnd = hwnd; + return FALSE; // Stop enumeration + } + return TRUE; // Continue enumeration + }, + reinterpret_cast(&data)); + + if (data.hwnd) { + XELOGI("Found child window handle: {}", + reinterpret_cast(data.hwnd)); + + // Send the key events + if (ctrl) PostMessage(data.hwnd, WM_KEYDOWN, VK_CONTROL, 0); + if (alt) PostMessage(data.hwnd, WM_KEYDOWN, VK_MENU, 0); + if (shift) PostMessage(data.hwnd, WM_KEYDOWN, VK_SHIFT, 0); + + // Convert VirtualKey to Windows VK code (they should mostly match) + WPARAM vk = static_cast(key); + PostMessage(data.hwnd, WM_KEYDOWN, vk, 0); + PostMessage(data.hwnd, WM_KEYUP, vk, 0); + + if (shift) PostMessage(data.hwnd, WM_KEYUP, VK_SHIFT, 0); + if (alt) PostMessage(data.hwnd, WM_KEYUP, VK_MENU, 0); + if (ctrl) PostMessage(data.hwnd, WM_KEYUP, VK_CONTROL, 0); + } else { + XELOGW("Could not find window for child process"); + } + } +#endif +} + +void EmulatorWindow::ExecuteOrForward(std::function local_action, + ui::VirtualKey key, bool ctrl, bool alt, + bool shift) { + if (HasRunningChildProcess()) { + XELOGI("Has child process - forwarding key instead of executing action"); + SendKeyToChild(key, ctrl, alt, shift); + } else { + XELOGI("No child process - executing action locally"); + local_action(); + } +} + xe::X_STATUS EmulatorWindow::RunTitle( const std::filesystem::path& path_to_file) { std::error_code ec = {}; @@ -2044,7 +2682,7 @@ xe::X_STATUS EmulatorWindow::RunTitle( void EmulatorWindow::RunPreviouslyPlayedTitle() { if (recently_launched_titles_.size() >= 1) { - RunTitle(recently_launched_titles_[0].path_to_file); + LaunchTitleInNewProcess(recently_launched_titles_[0].path_to_file); } } @@ -2058,13 +2696,21 @@ void EmulatorWindow::FillRecentlyLaunchedTitlesMenu( ? entry.path_to_file.string() : entry.title_name; - recent_menu->AddChild(MenuItem::Create( - MenuItem::Type::kString, item_text, hotkey, - std::bind(&EmulatorWindow::RunTitle, this, entry.path_to_file))); + recent_menu->AddChild(MenuItem::Create(MenuItem::Type::kString, item_text, + hotkey, + [this, path = entry.path_to_file]() { + // Check child process status when + // menu item is clicked + CheckChildProcessStatus(); + LaunchTitleInNewProcess(path); + })); } } void EmulatorWindow::LoadRecentlyLaunchedTitles() { + // Clear existing titles before loading + recently_launched_titles_.clear(); + std::ifstream file(emulator()->storage_root() / kRecentlyPlayedTitlesFilename); if (!file.is_open()) { diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index fd8c426c8..7a7d9f4dc 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -15,6 +15,7 @@ #include "xenia/app/patches_dialog.h" #include "xenia/app/profile_dialogs.h" +#include "xenia/app/recent_titles_ui.h" #include "xenia/emulator.h" #include "xenia/gpu/command_processor.h" #include "xenia/ui/imgui_dialog.h" @@ -55,7 +56,7 @@ class EmulatorWindow { static std::unique_ptr Create( Emulator* emulator, ui::WindowedAppContext& app_context, uint32_t width, - uint32_t height); + uint32_t height, bool is_game_process = false); std::unique_ptr Gamepad_HotKeys_Listener; @@ -82,8 +83,25 @@ class EmulatorWindow { void OnEmulatorInitialized(); + void LaunchTitleInNewProcess(const std::filesystem::path& path_to_file, + bool for_launch_data = false); xe::X_STATUS RunTitle(const std::filesystem::path& path_to_file); void UpdateTitle(); + bool HasRunningChildProcess(); + void CheckChildProcessStatus(); + void ScheduleChildProcessCheck(); + + // Keyboard forwarding for child processes + void SendKeyToChild(ui::VirtualKey key, bool ctrl = false, bool alt = false, + bool shift = false); + void SendCommandToChild(const std::string& command); + void ExecuteOrForward(std::function local_action, ui::VirtualKey key, + bool ctrl = false, bool alt = false, + bool shift = false); + + void AddRecentlyLaunchedTitle(std::filesystem::path path_to_file, + std::string title_name); + void SetFullscreen(bool fullscreen); void ToggleFullscreen(); void SetInitializingShaderStorage(bool initializing); @@ -95,6 +113,10 @@ class EmulatorWindow { void ToggleProfilesConfigDialog(); void SetHotkeysState(bool enabled) { disable_hotkeys_ = !enabled; } + void FileOpen(); + const std::vector& GetRecentlyLaunchedTitles() const { + return recently_launched_titles_; + } // Types of button functions for hotkeys. enum class ButtonFunctions { @@ -144,6 +166,7 @@ class EmulatorWindow { void OnClosing(ui::UIEvent& e) override; void OnFileDrop(ui::FileDropEvent& e) override; + void OnGotFocus(ui::UISetupEvent& e) override; void OnKeyDown(ui::KeyEvent& e) override; @@ -183,7 +206,7 @@ class EmulatorWindow { explicit EmulatorWindow(Emulator* emulator, ui::WindowedAppContext& app_context, uint32_t width, - uint32_t height); + uint32_t height, bool is_game_process = false); bool Initialize(); @@ -208,7 +231,6 @@ class EmulatorWindow { void ToggleFullscreenOnDoubleClick(); void FileDrop(const std::filesystem::path& filename); void OnMouseUp(const ui::MouseEvent& e); - void FileOpen(); void FileClose(); void InstallContent(); void ExtractZarchive(); @@ -242,14 +264,21 @@ class EmulatorWindow { void RunPreviouslyPlayedTitle(); void FillRecentlyLaunchedTitlesMenu(xe::ui::MenuItem* recent_menu); void LoadRecentlyLaunchedTitles(); - void AddRecentlyLaunchedTitle(std::filesystem::path path_to_file, - std::string title_name); void ClearDialogs(); + class RecentTitlesUI* recent_titles_ui_ = nullptr; + Emulator* emulator_; ui::WindowedAppContext& app_context_; + bool is_game_process_; EmulatorWindowListener window_listener_; + +#if XE_PLATFORM_LINUX + std::vector child_processes_; +#elif XE_PLATFORM_WIN32 + std::vector child_processes_; +#endif std::unique_ptr window_; std::unique_ptr imgui_drawer_; std::unique_ptr @@ -270,6 +299,11 @@ class EmulatorWindow { std::unique_ptr profile_config_dialog_; std::vector recently_launched_titles_; + + // Menu items that need to be enabled/disabled based on child process state + ui::MenuItem* file_menu_ = nullptr; + ui::MenuItem* file_open_item_ = nullptr; + ui::MenuItem* file_open_recent_menu_ = nullptr; }; } // namespace app diff --git a/src/xenia/app/recent_titles_ui.cc b/src/xenia/app/recent_titles_ui.cc new file mode 100644 index 000000000..772444641 --- /dev/null +++ b/src/xenia/app/recent_titles_ui.cc @@ -0,0 +1,401 @@ +/** + ****************************************************************************** + * 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/recent_titles_ui.h" +#include +#include +#include "third_party/fmt/include/fmt/format.h" +#include "xenia/app/emulator_window.h" +#include "xenia/base/logging.h" +#include "xenia/base/string_util.h" +#include "xenia/base/system.h" +#include "xenia/base/utf8.h" +#include "xenia/emulator.h" +#include "xenia/kernel/kernel_state.h" +#include "xenia/kernel/xam/profile_manager.h" +#include "xenia/kernel/xam/user_tracker.h" +#include "xenia/kernel/xam/xam_state.h" +#include "xenia/ui/imgui_guest_notification.h" + +namespace xe { +namespace app { + +RecentTitlesUI::RecentTitlesUI(ui::ImGuiDrawer* imgui_drawer, + EmulatorWindow* emulator_window) + : ui::ImGuiDialog(imgui_drawer), emulator_window_(emulator_window) { + LoadRecentTitles(); +} + +RecentTitlesUI::~RecentTitlesUI() { + for (auto& entry : title_icons_) { + entry.second.release(); + } +} + +void RecentTitlesUI::LoadRecentTitles() { + recent_titles_.clear(); + selected_title_ = 0; // Reset selection when reloading + + if (!emulator_window_) { + return; + } + + const auto& emulator_recent_titles = + emulator_window_->GetRecentlyLaunchedTitles(); + + for (const auto& entry : emulator_recent_titles) { + recent_titles_.push_back( + {entry.title_name, entry.path_to_file, entry.last_run_time, 0, {}}); + } +} + +void RecentTitlesUI::TryLoadIcons() { + if (!emulator_window_ || !emulator_window_->emulator()) { + return; + } + + auto kernel_state = emulator_window_->emulator()->kernel_state(); + if (!kernel_state) { + return; + } + + auto xam_state = kernel_state->xam_state(); + if (!xam_state) { + return; + } + + auto user_tracker = xam_state->user_tracker(); + auto profile_manager = xam_state->profile_manager(); + + if (!user_tracker || !profile_manager) { + return; + } + + ui::IconsData icon_data; + + // Check all logged in profiles + for (uint8_t user_index = 0; user_index < 4; user_index++) { + const auto profile = profile_manager->GetProfile(user_index); + if (!profile) { + continue; + } + + // Get all played titles for this profile + auto played_titles = user_tracker->GetPlayedTitles(profile->xuid()); + + // Match each recent title with played titles by name + for (auto& recent_title : recent_titles_) { + for (const auto& played_title : played_titles) { + std::string played_name = xe::to_utf8(played_title.title_name); + // Remove null terminator if present + if (!played_name.empty() && played_name.back() == '\0') { + played_name.pop_back(); + } + std::string trimmed_played = xe::string_util::trim(played_name); + std::string trimmed_recent = + xe::string_util::trim(recent_title.title_name); + + if (trimmed_played == trimmed_recent) { + if (!played_title.icon.empty()) { + recent_title.icon = std::vector(played_title.icon.begin(), + played_title.icon.end()); + // Update the title ID from the played title + recent_title.title_id = played_title.id; + icon_data[recent_title.title_id] = recent_title.icon; + } + break; // Found match for this recent title + } + } + } + } + + if (!icon_data.empty() && imgui_drawer()) { + // Load icons + for (const auto& [title_id, icon_data_entry] : icon_data) { + try { + auto texture = imgui_drawer()->LoadImGuiIcon(icon_data_entry); + if (texture) { + title_icons_[title_id] = std::move(texture); + } + } catch (const std::exception& e) { + XELOGE("Failed to load icon for title {}: {}", title_id, e.what()); + // Continue with other icons + } catch (...) { + XELOGE("Failed to load icon for title {}: unknown error", title_id); + // Continue with other icons + } + } + } +} + +void RecentTitlesUI::RefreshIcons() { + // Clear existing icons and force reload + for (auto& entry : title_icons_) { + entry.second.release(); + } + title_icons_.clear(); + + // Reset the title IDs to force re-matching + for (auto& title : recent_titles_) { + title.title_id = 0; + title.icon.clear(); + } +} + +void RecentTitlesUI::DrawTitleEntry(ImGuiIO& io, RecentTitleDisplay& entry, + size_t index) { + // First Column - Icon + ImGui::TableSetColumnIndex(0); + const auto start_position = ImGui::GetCursorPos(); + + auto icon_it = title_icons_.find(entry.title_id); + if (icon_it != title_icons_.end() && icon_it->second) { + ImGui::Image(reinterpret_cast(icon_it->second.get()), + ui::default_image_icon_size); + } else { + if (!has_logged_in_profile_) { + // Show "Not logged in" text when no profile is logged in + ImVec2 pos = ImGui::GetCursorPos(); + + // Create a child region to contain the text within icon bounds + ImGui::BeginChild(fmt::format("##NoIcon{}", index).c_str(), + ui::default_image_icon_size, false, + ImGuiWindowFlags_NoScrollbar); + + // Draw each line centered individually + const char* lines[] = {"Not", "logged", "in"}; + float line_height = ImGui::GetTextLineHeight(); + float total_height = line_height * 3; + float start_y = (ui::default_image_icon_size.y - total_height) * 0.5f; + + ImGui::PushStyleColor(ImGuiCol_Text, + ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + + for (int i = 0; i < 3; i++) { + ImVec2 line_size = ImGui::CalcTextSize(lines[i]); + float x_pos = (ui::default_image_icon_size.x - line_size.x) * 0.5f; + float y_pos = start_y + (i * line_height); + ImGui::SetCursorPos(ImVec2(x_pos, y_pos)); + ImGui::TextUnformatted(lines[i]); + } + + ImGui::PopStyleColor(); + + ImGui::EndChild(); + } else { + // Just show empty space if logged in but no icon found + ImGui::Dummy(ui::default_image_icon_size); + } + } + + // Second Column - Title Info + ImGui::TableNextColumn(); + + // Use full width of the column for text + float column_width = ImGui::GetContentRegionAvail().x; + + ImGui::PushFont(imgui_drawer()->GetTitleFont()); + ImGui::TextUnformatted(entry.title_name.c_str()); + ImGui::PopFont(); + + // Show file path + std::string display_path = entry.path_to_file.string(); + float text_width = ImGui::CalcTextSize(display_path.c_str()).x; + + // Only truncate if path is too long for the column + if (text_width > column_width) { + // Calculate how many characters we can fit + std::string ellipsis = "..."; + float ellipsis_width = ImGui::CalcTextSize(ellipsis.c_str()).x; + float available_width = column_width - ellipsis_width; + + // Search for the right substring length + size_t path_len = display_path.length(); + size_t keep_chars = path_len; + for (size_t i = path_len; i > 0; i--) { + std::string test_path = display_path.substr(path_len - i); + if (ImGui::CalcTextSize(test_path.c_str()).x <= available_width) { + keep_chars = i; + break; + } + } + display_path = ellipsis + display_path.substr(path_len - keep_chars); + } + ImGui::TextUnformatted(display_path.c_str()); + + ImGui::SetCursorPosY(start_position.y + ui::default_image_icon_size.y - + ImGui::GetTextLineHeight()); + + if (entry.last_run_time != 0) { + ImGui::TextUnformatted( + fmt::format("Last played: {:%Y-%m-%d %H:%M}", + std::chrono::system_clock::time_point( + std::chrono::seconds(entry.last_run_time))) + .c_str()); + } else { + ImGui::TextUnformatted("Last played: Unknown"); + } + + // Create invisible selectable over the entire row + ImGui::SetCursorPos(start_position); + + // Use index for unique ID instead of title_id which might be 0 + if (ImGui::Selectable(fmt::format("##RecentTitle{}Selectable", index).c_str(), + selected_title_ == entry.title_id, + ImGuiSelectableFlags_SpanAllColumns | + ImGuiSelectableFlags_AllowOverlap, + ImVec2(0, ui::default_image_icon_size.y))) { + selected_title_ = entry.title_id; + LaunchTitle(entry.path_to_file); + } + + if (ImGui::BeginPopupContextItem( + fmt::format("Recent Title Menu {}", index).c_str())) { + selected_title_ = entry.title_id; + + if (ImGui::MenuItem("Launch")) { + LaunchTitle(entry.path_to_file); + } + + if (ImGui::MenuItem("Open containing folder")) { + std::filesystem::path folder = entry.path_to_file.parent_path(); + std::thread path_open(LaunchFileExplorer, folder); + path_open.detach(); + } + + ImGui::EndPopup(); + } +} + +void RecentTitlesUI::LaunchTitle(const std::filesystem::path& path) { + if (emulator_window_) { + if (emulator_window_->HasRunningChildProcess()) { + return; + } + emulator_window_->LaunchTitleInNewProcess(path); + } +} + +void RecentTitlesUI::OnDraw(ImGuiIO& io) { + // Check if the number of logged-in profiles has changed + has_logged_in_profile_ = false; + if (emulator_window_ && emulator_window_->emulator()) { + auto kernel_state = emulator_window_->emulator()->kernel_state(); + if (kernel_state) { + auto xam_state = kernel_state->xam_state(); + if (xam_state) { + auto profile_manager = xam_state->profile_manager(); + if (profile_manager) { + // Count currently logged-in profiles + int current_logged_in_count = 0; + for (uint8_t i = 0; i < 4; i++) { + if (profile_manager->GetProfile(i)) { + current_logged_in_count++; + has_logged_in_profile_ = true; + } + } + + // If the count changed, refresh icons + if (current_logged_in_count != last_logged_in_count_) { + XELOGI( + "Logged-in profile count changed from {} to {}, refreshing " + "icons", + last_logged_in_count_, current_logged_in_count); + last_logged_in_count_ = current_logged_in_count; + + // Refresh icons when profiles change (login or logout) + RefreshIcons(); + } + } + } + } + } + + TryLoadIcons(); + + // Make the window take up the entire visible area + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(1.0f); + + if (!ImGui::Begin("##RecentlyPlayedGames", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBringToFrontOnFocus)) { + ImGui::End(); + return; + } + + if (!recent_titles_.empty()) { + if (recent_titles_.size() > 5) { + ImGui::Text("Search: "); + ImGui::SameLine(); + ImGui::InputText("##Search", title_name_filter_, title_name_filter_size); + ImGui::Separator(); + } + + if (ImGui::BeginTable( + "", 2, ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_PadOuterX)) { + // Set the icon column to fixed width (icon + small padding) + ImGui::TableSetupColumn("Icon", ImGuiTableColumnFlags_WidthFixed, + ui::default_image_icon_size.x + 10.0f); + // The details column takes the remaining space + ImGui::TableSetupColumn("Details", ImGuiTableColumnFlags_WidthStretch); + + size_t display_index = 0; + bool first_item = true; + for (auto& entry : recent_titles_) { + std::string filter(title_name_filter_); + if (!filter.empty()) { + bool contains_filter = + utf8::lower_ascii(entry.title_name) + .find(utf8::lower_ascii(filter)) != std::string::npos || + utf8::lower_ascii(entry.path_to_file.string()) + .find(utf8::lower_ascii(filter)) != std::string::npos; + + if (!contains_filter) { + continue; + } + } + // Add row with vertical padding + ImGui::TableNextRow(0, ui::default_image_icon_size.y + 4.0f); + DrawTitleEntry(io, entry, display_index++); + first_item = false; + } + ImGui::EndTable(); + } + } else { + // Align text to the center + std::string no_entries_message = "No recently played games found."; + + ImGui::PushFont(imgui_drawer()->GetTitleFont()); + float windowWidth = ImGui::GetContentRegionAvail().x; + ImVec2 textSize = ImGui::CalcTextSize(no_entries_message.c_str()); + float textOffsetX = (windowWidth - textSize.x) * 0.5f; + if (textOffsetX > 0.0f) { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + textOffsetX); + } + + ImGui::Text("%s", no_entries_message.c_str()); + ImGui::PopFont(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Open Game...")) { + emulator_window_->FileOpen(); + } + } + + ImGui::End(); +} + +} // namespace app +} // namespace xe diff --git a/src/xenia/app/recent_titles_ui.h b/src/xenia/app/recent_titles_ui.h new file mode 100644 index 000000000..1cc6b8521 --- /dev/null +++ b/src/xenia/app/recent_titles_ui.h @@ -0,0 +1,69 @@ +/** + ****************************************************************************** + * 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_RECENT_TITLES_UI_H_ +#define XENIA_APP_RECENT_TITLES_UI_H_ + +#include +#include +#include +#include +#include "xenia/ui/imgui_dialog.h" +#include "xenia/ui/imgui_drawer.h" + +namespace xe { +namespace app { + +class EmulatorWindow; + +struct RecentTitleDisplay { + std::string title_name; + std::filesystem::path path_to_file; + time_t last_run_time; + uint32_t title_id; + std::vector icon; +}; + +class RecentTitlesUI final : public ui::ImGuiDialog { + public: + RecentTitlesUI(ui::ImGuiDrawer* imgui_drawer, + EmulatorWindow* emulator_window); + + ~RecentTitlesUI(); + + public: + void LoadRecentTitles(); + + protected: + void OnDraw(ImGuiIO& io) override; + + private: + void TryLoadIcons(); + void DrawTitleEntry(ImGuiIO& io, RecentTitleDisplay& entry, size_t index); + void LaunchTitle(const std::filesystem::path& path); + + public: + void RefreshIcons(); + + static constexpr uint8_t title_name_filter_size = 15; + + char title_name_filter_[title_name_filter_size] = ""; + uint32_t selected_title_ = 0; + int last_logged_in_count_ = 0; + bool has_logged_in_profile_ = false; + + EmulatorWindow* emulator_window_; + std::vector recent_titles_; + std::map> title_icons_; +}; + +} // namespace app +} // namespace xe + +#endif diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index 33883d08a..9e1251604 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -266,7 +266,7 @@ class EmulatorApp final : public xe::ui::WindowedApp { static std::vector> CreateInputDrivers( ui::Window* window); - void EmulatorThread(); + void EmulatorThread(bool is_game_process); void ShutdownEmulatorThreadFromUIThread(); DebugWindowClosedListener debug_window_closed_listener_; @@ -527,12 +527,34 @@ bool EmulatorApp::OnInitialize() { emulator_ = std::make_unique("", storage_root, content_root, cache_root); - // Determine window size based on user setting. - auto res = xe::gpu::GraphicsSystem::GetInternalDisplayResolution(); + // Check if this is a game process (has target or launch_data.bin) or UI + // process + bool has_launch_data = false; + FILE* launch_data_file = + xe::filesystem::OpenFile(kernel::xam::kXamModuleLoaderDataFileName, "rb"); + if (launch_data_file) { + has_launch_data = true; + fclose(launch_data_file); + } + bool is_game_process = !cvars::target.empty() || has_launch_data; + + // Determine window size based on process type + uint32_t window_width, window_height; + if (is_game_process) { + // Game process - use full resolution from settings + auto res = xe::gpu::GraphicsSystem::GetInternalDisplayResolution(); + window_width = res.first; + window_height = res.second; + } else { + // UI process - sized to comfortably hold configuration manager (900x700) + window_width = 950; + window_height = 750; + } // Main emulator display window. - emulator_window_ = EmulatorWindow::Create(emulator_.get(), app_context(), - res.first, res.second); + emulator_window_ = + EmulatorWindow::Create(emulator_.get(), app_context(), window_width, + window_height, is_game_process); if (!emulator_window_) { XELOGE("Failed to create the main emulator window"); return false; @@ -542,7 +564,8 @@ bool EmulatorApp::OnInitialize() { emulator_thread_quit_requested_.store(false, std::memory_order_relaxed); emulator_thread_event_ = xe::threading::Event::CreateAutoResetEvent(false); assert_not_null(emulator_thread_event_); - emulator_thread_ = std::thread(&EmulatorApp::EmulatorThread, this); + emulator_thread_ = + std::thread(&EmulatorApp::EmulatorThread, this, is_game_process); return true; } @@ -566,7 +589,7 @@ void EmulatorApp::OnDestroy() { std::quick_exit(EXIT_SUCCESS); } -void EmulatorApp::EmulatorThread() { +void EmulatorApp::EmulatorThread(bool is_game_process) { assert_not_null(emulator_thread_event_); xe::threading::set_name("Emulator"); @@ -574,9 +597,11 @@ void EmulatorApp::EmulatorThread() { // Setup and initialize all subsystems. If we can't do something // (unsupported system, memory issues, etc) this will fail early. + // Only load input drivers if this is a game process X_STATUS result = emulator_->Setup( emulator_window_->window(), emulator_window_->imgui_drawer(), true, - CreateAudioSystem, CreateGraphicsSystem, CreateInputDrivers); + CreateAudioSystem, CreateGraphicsSystem, + is_game_process ? CreateInputDrivers : nullptr); if (XFAILED(result)) { XELOGE("Failed to setup emulator: {:08X}", result); app_context().RequestDeferredQuit(); @@ -719,13 +744,28 @@ void EmulatorApp::EmulatorThread() { // Normalize the path and make absolute. auto abs_path = std::filesystem::absolute(path); - result = app_context().CallInUIThread( - [this, abs_path]() { return emulator_window_->RunTitle(abs_path); }); + // TODO(has207): Add archive format check like in RunTitle? + result = emulator_->LaunchPath(abs_path); if (XFAILED(result)) { xe::FatalError(fmt::format("Failed to launch target: {:08X}", result)); app_context().RequestDeferredQuit(); return; } + + // Store the host path in loader_data for potential restart with + // launch_data.bin + auto xam_for_path = + emulator_->kernel_state()->GetKernelModule( + "xam.xex"); + if (xam_for_path) { + xam_for_path->loader_data().host_path = xe::path_to_utf8(abs_path); + } + + // Add to recent titles if this is a game process + if (is_game_process && emulator_window_) { + emulator_window_->AddRecentlyLaunchedTitle(abs_path, + emulator_->title_name()); + } } auto xam = emulator_->kernel_state()->GetKernelModule( @@ -736,9 +776,7 @@ void EmulatorApp::EmulatorThread() { if (xam->loader_data().launch_data_present) { const std::filesystem::path host_path = xam->loader_data().host_path; - app_context().CallInUIThread([this, host_path]() { - return emulator_window_->RunTitle(host_path); - }); + emulator_->LaunchPath(host_path); } } diff --git a/src/xenia/base/logging.cc b/src/xenia/base/logging.cc index d38043996..2d34fb2a2 100644 --- a/src/xenia/base/logging.cc +++ b/src/xenia/base/logging.cc @@ -427,7 +427,7 @@ class Logger { } }; -void InitializeLogging(const std::string_view app_name) { +void InitializeLogging(const std::string_view app_name, bool is_game_process) { auto mem = memory::AlignedAlloc(0x10); logger_ = new (mem) Logger(app_name); @@ -440,13 +440,29 @@ void InitializeLogging(const std::string_view app_name) { #else FILE* log_file = nullptr; if (cvars::log_file.empty()) { - // Default to app name. - auto file_name = fmt::format("{}.log", app_name); + // Default log file name based on process type + std::string file_name; + if (is_game_process) { + file_name = fmt::format("{}_game.log", app_name); + } else { + file_name = fmt::format("{}.log", app_name); + } auto file_path = xe::filesystem::GetExecutableFolder() / file_name; log_file = xe::filesystem::OpenFile(file_path, "wt"); } else { - xe::filesystem::CreateParentFolder(cvars::log_file); - log_file = xe::filesystem::OpenFile(cvars::log_file, "wt"); + // User specified log file + if (is_game_process) { + // Game process with explicit log file - prepend "game_" + std::filesystem::path log_path(cvars::log_file); + std::string filename = "game_" + log_path.filename().string(); + auto modified_path = log_path.parent_path() / filename; + xe::filesystem::CreateParentFolder(modified_path); + log_file = xe::filesystem::OpenFile(modified_path, "wt"); + } else { + // UI process uses log file as-is + xe::filesystem::CreateParentFolder(cvars::log_file); + log_file = xe::filesystem::OpenFile(cvars::log_file, "wt"); + } } logger_->AddLogSink(std::make_unique(log_file, true)); diff --git a/src/xenia/base/logging.h b/src/xenia/base/logging.h index 709aaf3a5..d9bbd89e2 100644 --- a/src/xenia/base/logging.h +++ b/src/xenia/base/logging.h @@ -78,7 +78,8 @@ class DebugPrintLogSink final : public LogSink { // Initializes the logging system and any outputs requested. // Must be called on startup. -void InitializeLogging(const std::string_view app_name); +void InitializeLogging(const std::string_view app_name, + bool is_game_process = false); void ShutdownLogging(); namespace logging { diff --git a/src/xenia/config.cc b/src/xenia/config.cc index 4e12e8fef..bdeb3cbb3 100644 --- a/src/xenia/config.cc +++ b/src/xenia/config.cc @@ -34,6 +34,7 @@ std::string config_name = "xenia-edge.config.toml"; std::filesystem::path config_folder; std::filesystem::path config_path; std::string game_config_suffix = ".config.toml"; +std::function config_saved_callback; bool sortCvar(cvar::IConfigVar* a, cvar::IConfigVar* b) { if (a->category() < b->category()) return true; @@ -139,6 +140,17 @@ void ReadGameConfig(const std::filesystem::path& file_path) { XELOGI("Loaded game config: {}", file_path); } +void ReloadConfig() { + if (config_path.empty()) { + return; + } + + if (std::filesystem::exists(config_path)) { + ReadConfig(config_path, false); + XELOGI("Reloaded config from: {}", xe::path_to_utf8(config_path)); + } +} + void SaveConfig() { if (config_path.empty()) { return; @@ -241,6 +253,15 @@ void SaveConfig() { fwrite(sb.buffer(), 1, sb.length(), handle); fclose(handle); } + + // Notify that config was saved + if (config_saved_callback) { + config_saved_callback(); + } +} + +void SetConfigSavedCallback(std::function callback) { + config_saved_callback = callback; } void SetupConfig(const std::filesystem::path& config_folder) { diff --git a/src/xenia/config.h b/src/xenia/config.h index 907d5ab2c..8227f545d 100644 --- a/src/xenia/config.h +++ b/src/xenia/config.h @@ -11,6 +11,7 @@ #define XENIA_CONFIG_H_ #include +#include #include "third_party/tomlplusplus/toml.hpp" toml::parse_result ParseFile(const std::filesystem::path& filename); @@ -19,6 +20,8 @@ namespace config { void SetupConfig(const std::filesystem::path& config_folder); void LoadGameConfig(const std::string_view title_id); void SaveConfig(); +void ReloadConfig(); +void SetConfigSavedCallback(std::function callback); } // namespace config #endif // XENIA_CONFIG_H_ diff --git a/src/xenia/kernel/xam/profile_manager.cc b/src/xenia/kernel/xam/profile_manager.cc index 719743cd8..611fa9931 100644 --- a/src/xenia/kernel/xam/profile_manager.cc +++ b/src/xenia/kernel/xam/profile_manager.cc @@ -10,6 +10,7 @@ #include "xenia/kernel/xam/profile_manager.h" #include "xenia/base/logging.h" +#include "xenia/config.h" #include "xenia/emulator.h" #include "xenia/hid/input_system.h" #include "xenia/kernel/kernel_state.h" @@ -149,6 +150,42 @@ void ProfileManager::ReloadProfiles() { } } +void ProfileManager::SyncProfilesWithConfig() { + // First reload all accounts from disk to pick up any new/deleted profiles + accounts_.clear(); + for (const auto account_xuid : FindProfiles()) { + LoadAccount(account_xuid); + } + + // Then logout all currently logged in profiles + std::vector slots_to_logout; + for (const auto& [slot, profile] : logged_profiles_) { + slots_to_logout.push_back(slot); + } + for (uint8_t slot : slots_to_logout) { + Logout(slot, false); + } + + // Now login profiles based on the current cvar values + const std::string* profile_cvars[4] = { + &cvars::logged_profile_slot_0_xuid, &cvars::logged_profile_slot_1_xuid, + &cvars::logged_profile_slot_2_xuid, &cvars::logged_profile_slot_3_xuid}; + + for (uint8_t slot = 0; slot < 4; slot++) { + if (!profile_cvars[slot]->empty()) { + uint64_t xuid = + xe::string_util::from_string(*profile_cvars[slot], true); + if (xuid != 0) { + Login(xuid, slot, false); + } + } + } + + // Send a single notification after all changes + kernel_state_->BroadcastNotification(kXNotificationSystemSignInChanged, + GetUsedUserSlots().to_ulong()); +} + UserProfile* ProfileManager::GetProfile(const uint64_t xuid) const { const uint8_t user_index = GetUserIndexAssignedToProfile(xuid); if (user_index >= XUserMaxUserCount) { @@ -577,6 +614,9 @@ void ProfileManager::UpdateConfig(const uint64_t xuid, const uint8_t slot) { default: break; } + + // Save config immediately to persist login/logout changes + config::SaveConfig(); return; } diff --git a/src/xenia/kernel/xam/profile_manager.h b/src/xenia/kernel/xam/profile_manager.h index a769912e7..18f0ece73 100644 --- a/src/xenia/kernel/xam/profile_manager.h +++ b/src/xenia/kernel/xam/profile_manager.h @@ -80,6 +80,7 @@ class ProfileManager { void ReloadProfiles(); void ReloadProfile(const uint64_t xuid); + void SyncProfilesWithConfig(); UserProfile* GetProfile(const uint64_t xuid) const; UserProfile* GetProfile(const uint8_t user_index) const; diff --git a/src/xenia/kernel/xam/xam_info.cc b/src/xenia/kernel/xam/xam_info.cc index 5af9a8cdb..be736dfc6 100644 --- a/src/xenia/kernel/xam/xam_info.cc +++ b/src/xenia/kernel/xam/xam_info.cc @@ -399,14 +399,60 @@ void XamLoaderLaunchTitle_entry(lpstring_t raw_name_ptr, dword_t flags) { auto imgui_drawer = kernel_state()->emulator()->imgui_drawer(); if (display_window && imgui_drawer) { + // Show a dialog and wait for user to click OK before terminating + // The parent UI will detect the launch_data.bin file and automatically + // relaunch without a game argument when this process exits display_window->app_context().CallInUIThreadSynchronous( - [imgui_drawer]() { - xe::ui::ImGuiDialog::ShowMessageBox( - imgui_drawer, "Title was restarted", - "Title closed with new launch data. \nPlease restart Xenia. " - "Game will be loaded automatically."); + [imgui_drawer, display_window, kernel_state = kernel_state()]() { + class LaunchDataRestartDialog : public xe::ui::ImGuiDialog { + public: + LaunchDataRestartDialog(ui::ImGuiDrawer* imgui_drawer, + ui::Window* display_window, + kernel::KernelState* kernel_state) + : ImGuiDialog(imgui_drawer), + display_window_(display_window), + kernel_state_(kernel_state) {} + + protected: + void OnDraw(ImGuiIO& io) override { + bool dialog_open = true; + ImGui::OpenPopup("Title Restart Required"); + if (ImGui::BeginPopupModal( + "Title Restart Required", &dialog_open, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextUnformatted( + "Title is restarting with new launch data.\n" + "Click OK to continue. Game will be loaded " + "automatically."); + ImGui::Spacing(); + if (ImGui::Button("OK", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + Close(); + // Terminate the title and quit after user clicks OK + kernel_state_->TerminateTitle(); + display_window_->app_context().QuitFromUIThread(); + } + ImGui::EndPopup(); + } + if (!dialog_open) { + Close(); + kernel_state_->TerminateTitle(); + display_window_->app_context().QuitFromUIThread(); + } + } + + private: + ui::Window* display_window_; + kernel::KernelState* kernel_state_; + }; + + new LaunchDataRestartDialog(imgui_drawer, display_window, + kernel_state); }); } + // Don't call TerminateTitle here - the dialog will do it when user clicks + // OK + return; } } else { assert_always("Game requested exit to dashboard via XamLoaderLaunchTitle"); diff --git a/src/xenia/ui/imgui_drawer.cc b/src/xenia/ui/imgui_drawer.cc index 0426b4eb0..6ce8a12f9 100644 --- a/src/xenia/ui/imgui_drawer.cc +++ b/src/xenia/ui/imgui_drawer.cc @@ -252,9 +252,12 @@ std::unique_ptr ImGuiDrawer::LoadImGuiIcon( return {}; } - return immediate_drawer_->CreateTexture( + auto texture = immediate_drawer_->CreateTexture( width, height, ImmediateTextureFilter::kLinear, true, reinterpret_cast(image_data)); + + stbi_image_free(image_data); // Free the image data after creating texture + return texture; } std::map> ImGuiDrawer::LoadIcons( diff --git a/src/xenia/ui/windowed_app_main_qt.cc b/src/xenia/ui/windowed_app_main_qt.cc index a2db24357..ff75a2a66 100644 --- a/src/xenia/ui/windowed_app_main_qt.cc +++ b/src/xenia/ui/windowed_app_main_qt.cc @@ -30,6 +30,18 @@ int main(int argc, char** argv) { QApplication qt_app(argc, argv); + // Set different application name for game processes so they show as separate + // dock entries. Check if we have a target file argument (game process) or not + // (UI process). + bool is_game_process = argc > 1; + if (is_game_process) { + qt_app.setApplicationName("Xbox 360 Game"); + qt_app.setDesktopFileName("xenia-game"); + } else { + qt_app.setApplicationName("Xenia Edge"); + qt_app.setDesktopFileName("xenia-edge"); + } + // Use Qt's own menu bar instead of native on all platforms qt_app.setAttribute(Qt::AA_DontUseNativeMenuBar);