[UI] Separate UI and game processes and add a game list

This commit is contained in:
Herman S.
2025-10-08 16:00:31 +09:00
parent 567bd00016
commit f13a11c2e2
14 changed files with 1531 additions and 200 deletions
File diff suppressed because it is too large Load Diff
+39 -5
View File
@@ -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<EmulatorWindow> Create(
Emulator* emulator, ui::WindowedAppContext& app_context, uint32_t width,
uint32_t height);
uint32_t height, bool is_game_process = false);
std::unique_ptr<xe::threading::Thread> 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<void()> 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<RecentTitleEntry>& 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<pid_t> child_processes_;
#elif XE_PLATFORM_WIN32
std::vector<HANDLE> child_processes_;
#endif
std::unique_ptr<ui::Window> window_;
std::unique_ptr<ui::ImGuiDrawer> imgui_drawer_;
std::unique_ptr<DisplayConfigGameConfigLoadCallback>
@@ -270,6 +299,11 @@ class EmulatorWindow {
std::unique_ptr<ProfileConfigDialog> profile_config_dialog_;
std::vector<RecentTitleEntry> 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
+401
View File
@@ -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 <chrono>
#include <thread>
#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<uint8_t>(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<ImTextureID>(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
+69
View File
@@ -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 <filesystem>
#include <map>
#include <memory>
#include <vector>
#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<uint8_t> 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<RecentTitleDisplay> recent_titles_;
std::map<uint32_t, std::unique_ptr<ui::ImmediateTexture>> title_icons_;
};
} // namespace app
} // namespace xe
#endif
+51 -13
View File
@@ -266,7 +266,7 @@ class EmulatorApp final : public xe::ui::WindowedApp {
static std::vector<std::unique_ptr<hid::InputDriver>> 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<Emulator>("", 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<kernel::xam::XamModule>(
"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<kernel::xam::XamModule>(
@@ -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);
}
}
+21 -5
View File
@@ -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<Logger>(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<FileLogSink>(log_file, true));
+2 -1
View File
@@ -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 {
+21
View File
@@ -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<void()> 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<void()> callback) {
config_saved_callback = callback;
}
void SetupConfig(const std::filesystem::path& config_folder) {
+3
View File
@@ -11,6 +11,7 @@
#define XENIA_CONFIG_H_
#include <filesystem>
#include <functional>
#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<void()> callback);
} // namespace config
#endif // XENIA_CONFIG_H_
+40
View File
@@ -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<uint8_t> 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<uint64_t>(*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;
}
+1
View File
@@ -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;
+51 -5
View File
@@ -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");
+4 -1
View File
@@ -252,9 +252,12 @@ std::unique_ptr<ImmediateTexture> ImGuiDrawer::LoadImGuiIcon(
return {};
}
return immediate_drawer_->CreateTexture(
auto texture = immediate_drawer_->CreateTexture(
width, height, ImmediateTextureFilter::kLinear, true,
reinterpret_cast<uint8_t*>(image_data));
stbi_image_free(image_data); // Free the image data after creating texture
return texture;
}
std::map<uint32_t, std::unique_ptr<ImmediateTexture>> ImGuiDrawer::LoadIcons(
+12
View File
@@ -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);