mirror of
https://github.com/izzy2lost/xenia-edge.git
synced 2026-07-06 00:20:26 -07:00
[UI] Separate UI and game processes and add a game list
This commit is contained in:
+816
-170
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user