[XConfig] Implementation of XConfig

This commit is contained in:
Gliniak
2026-05-17 21:30:10 +02:00
committed by Radosław Gliński
parent f88bfbe41e
commit 99ea6da18a
31 changed files with 2020 additions and 846 deletions

View File

@@ -0,0 +1,426 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/app/emulator_window.h"
#include "xenia/app/profile_dialogs.h"
#include "xenia/base/png_utils.h"
#include "xenia/base/system.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xam/xam_ui.h"
#include "xenia/ui/file_picker.h"
#include "xenia/app/console_settings_dialog.h"
#include "third_party/fmt/include/fmt/format.h"
namespace xe {
namespace app {
static const std::map<uint32_t, std::string> kLanguageMap = {
{1, "English"}, {2, "Japanese"},
{3, "German"}, {4, "French"},
{5, "Spanish"}, {6, "Italian"},
{7, "Korean"}, {8, "Traditional Chinese"},
{9, "Portuguese"}, {10, "Simplified Chinese"},
{11, "Polish"}, {12, "Russian"}};
static const std::map<uint8_t, std::string> kCountryMap = {
{1, "AE"}, {2, "AL"}, {3, "AM"}, {4, "AR"}, {5, "AT"},
{6, "AU"}, {7, "AZ"}, {8, "BE"}, {9, "BG"}, {10, "BH"},
{11, "BN"}, {12, "BO"}, {13, "BR"}, {14, "BY"}, {15, "BZ"},
{16, "CA"}, {18, "CH"}, {19, "CL"}, {20, "CN"}, {21, "CO"},
{22, "CR"}, {23, "CZ"}, {24, "DE"}, {25, "DK"}, {26, "DO"},
{27, "DZ"}, {28, "EC"}, {29, "EE"}, {30, "EG"}, {31, "ES"},
{32, "FI"}, {33, "FO"}, {34, "FR"}, {35, "GB"}, {36, "GE"},
{37, "GR"}, {38, "GT"}, {39, "HK"}, {40, "HN"}, {41, "HR"},
{42, "HU"}, {43, "ID"}, {44, "IE"}, {45, "IL"}, {46, "IN"},
{47, "IQ"}, {48, "IR"}, {49, "IS"}, {50, "IT"}, {51, "JM"},
{52, "JO"}, {53, "JP"}, {54, "KE"}, {55, "KG"}, {56, "KR"},
{57, "KW"}, {58, "KZ"}, {59, "LB"}, {60, "LI"}, {61, "LT"},
{62, "LU"}, {63, "LV"}, {64, "LY"}, {65, "MA"}, {66, "MC"},
{67, "MK"}, {68, "MN"}, {69, "MO"}, {70, "MV"}, {71, "MX"},
{72, "MY"}, {73, "NI"}, {74, "NL"}, {75, "NO"}, {76, "NZ"},
{77, "OM"}, {78, "PA"}, {79, "PE"}, {80, "PH"}, {81, "PK"},
{82, "PL"}, {83, "PR"}, {84, "PT"}, {85, "PY"}, {86, "QA"},
{87, "RO"}, {88, "RU"}, {89, "SA"}, {90, "SE"}, {91, "SG"},
{92, "SI"}, {93, "SK"}, {95, "SV"}, {96, "SY"}, {97, "TH"},
{98, "TN"}, {99, "TR"}, {100, "TT"}, {101, "TW"}, {102, "UA"},
{103, "US"}, {104, "UY"}, {105, "UZ"}, {106, "VE"}, {107, "VN"},
{108, "YE"}, {109, "ZA"},
};
static const std::map<uint32_t, std::string> kAVRegion = {
{0x00400100, "NTSC"},
{0x00400200, "NTSC-J"},
{0x00400400, "PAL"},
{0x00800300, "PAL 50Hz"}};
template <typename T>
void DrawCombobox(ImGuiIO& io, std::string_view combobox_name,
const std::map<T, std::string>& options, xe::be<T>& value) {
const char* preview = "Unknown";
for (const auto& opt : options) {
if (opt.first == value.get()) {
preview = opt.second.data();
break;
}
}
if (ImGui::BeginCombo(combobox_name.data(), preview)) {
for (const auto& opt : options) {
const bool selected = (opt.first == value.get());
if (ImGui::Selectable(opt.second.data(), selected)) {
value = opt.first;
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
}
void DrawResolutionCombobox(ImGuiIO& io,
const std::span<const kernel::Resolution> options,
xe::be<int32_t>& resolution,
xe::be<uint32_t>& video_flags) {
const char* preview = "Unknown";
for (const auto& opt : options) {
if (opt.to_host() == resolution.get()) {
preview = opt.name_.c_str();
break;
}
}
if (ImGui::BeginCombo("Resolution", preview)) {
for (const auto& opt : options) {
const bool selected = (opt.to_host() == resolution.get());
if (ImGui::Selectable(opt.name_.c_str(), selected)) {
resolution = opt.to_host();
if (opt.is_widescreen()) {
video_flags = video_flags | static_cast<uint32_t>(
kernel::X_VIDEO_FLAGS::Widescreen);
} else {
video_flags = video_flags & ~static_cast<uint32_t>(
kernel::X_VIDEO_FLAGS::Widescreen);
}
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
}
void DrawTimezoneCombobox(ImGuiIO& io, kernel::XConfigData* xdata) {
const kernel::TimeZone current_tz = {
xdata->user.time_zone_bias, xdata->user.tz_std_name,
xdata->user.tz_dlt_name, xdata->user.tz_std_date,
xdata->user.tz_dlt_date, xdata->user.tz_std_bias,
xdata->user.tz_dlt_bias};
size_t index = 0x19; // Index of GMT+00
auto it = std::find(kernel::kTimezones.cbegin(), kernel::kTimezones.cend(),
current_tz);
if (it != kernel::kTimezones.cend()) {
index = std::distance(kernel::kTimezones.cbegin(), it);
}
if (ImGui::BeginCombo("Timezone", kernel::kTimezones[index].name.c_str())) {
for (size_t i = 0; i < kernel::kTimezones.size(); ++i) {
const bool is_selected =
(kernel::kTimezones[i] == kernel::kTimezones[index]);
if (ImGui::Selectable(kernel::kTimezones[i].name.c_str(), is_selected)) {
index = i;
// Update timezone values
const auto& tz = kernel::kTimezones[index];
xdata->user.time_zone_bias = tz.timezone_bias;
memcpy(xdata->user.tz_std_name.data(), tz.tz_std_name.data(), 4);
memcpy(xdata->user.tz_dlt_name.data(), tz.tz_dlt_name.data(), 4);
memcpy(xdata->user.tz_std_date.data(), tz.tz_std_date.data(), 4);
memcpy(xdata->user.tz_dlt_date.data(), tz.tz_dlt_date.data(), 4);
xdata->user.tz_std_bias = tz.tz_std_bias;
xdata->user.tz_dlt_bias = tz.tz_dlt_bias;
}
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
}
template <typename T>
static bool FlagCheckbox(const char* label, xe::be<T>& field, T value) {
bool set = (field.get() & value) != 0;
if (ImGui::Checkbox(label, &set)) {
if (set) {
field = field.get() | value;
} else {
field = field.get() & ~value;
}
return true;
}
return false;
}
template <typename T>
static void ByteArray(const char* label, T* array, size_t count) {
for (size_t i = 0; i < count; i++) {
ImGui::SetNextItemWidth(ImGui::CalcTextSize("CC_").x);
ImGui::InputScalar(fmt::format("##{}{}", label, i).c_str(),
ImGuiDataType_U8, &array[i], nullptr, nullptr, "%02X");
if (i + 1 < count) {
ImGui::SameLine();
}
}
}
void GroupBox(const char* label, auto draw_fn) {
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 text_size = ImGui::CalcTextSize(label);
float border_offset = text_size.y * 0.5f;
ImGuiStyle& style = ImGui::GetStyle();
// Push content down so it doesn't overlap the label
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + border_offset);
ImGui::Indent(8.0f);
ImGui::BeginGroup();
ImGui::Dummy(ImVec2(ImGui::GetContentRegionAvail().x - 16.0f, 0));
ImGui::Spacing();
draw_fn();
ImGui::Spacing();
ImGui::EndGroup();
ImGui::Unindent(8.0f);
// NOW we know the rect
ImVec2 rect_min = ImGui::GetItemRectMin();
ImVec2 rect_max = ImGui::GetItemRectMax();
// Expand rect a bit for padding
rect_min.x -= 8.0f;
rect_max.x += 8.0f;
rect_max.y += style.ItemSpacing.y;
// Move the top of the border up to account for label offset
rect_min.y -= border_offset;
ImU32 border_color = ImGui::GetColorU32(ImGuiCol_Border);
float label_start = rect_min.x + 8.0f;
float label_end = label_start + text_size.x + 4.0f;
// Top edge with gap for label
draw_list->AddLine(ImVec2(rect_min.x, rect_min.y),
ImVec2(label_start, rect_min.y), border_color);
draw_list->AddLine(ImVec2(label_end, rect_min.y),
ImVec2(rect_max.x, rect_min.y), border_color);
// Left, right, bottom
draw_list->AddLine(rect_min, ImVec2(rect_min.x, rect_max.y), border_color);
draw_list->AddLine(ImVec2(rect_max.x, rect_min.y),
ImVec2(rect_max.x, rect_max.y), border_color);
draw_list->AddLine(ImVec2(rect_min.x, rect_max.y), rect_max, border_color);
// Label
draw_list->AddText(ImVec2(label_start + 2.0f, rect_min.y - border_offset),
ImGui::GetColorU32(ImGuiCol_Text), label);
ImGui::SetCursorPosY(rect_max.y + style.ItemSpacing.y);
}
void ConsoleSettingsDialog::OnDraw(ImGuiIO& io) {
ImGui::SetNextWindowPos(ImVec2(20, 20), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(20, 20), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowBgAlpha(0.90f);
bool dialog_open = true;
if (!ImGui::Begin("Console settings", &dialog_open,
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoMove)) {
ImGui::End();
return;
}
if (!dialog_open) {
Close();
ImGui::End();
emulator_window_.ToggleConsoleSettingsDialog();
return;
}
if (ImGui::BeginTabBar("##Categories")) {
if (ImGui::BeginTabItem("User")) {
ImGui::BeginDisabled(emulator_window_.emulator()->is_title_open());
ImGui::Dummy(ImVec2(2.f, 2.f));
GroupBox("Time", [&]() {
DrawTimezoneCombobox(io, &xconfig_data_);
FlagCheckbox("Disable Daylight-Saving Time",
xconfig_data_.user.retail_flags,
static_cast<uint32_t>(kernel::X_RETAIL_FLAGS::DSTOff));
FlagCheckbox(
"24H Time", xconfig_data_.user.retail_flags,
static_cast<uint32_t>(kernel::X_RETAIL_FLAGS::TwentyFourHourClock));
});
GroupBox("Locale", [&]() {
DrawCombobox(io, "Language", kLanguageMap, xconfig_data_.user.language);
DrawCombobox(io, "Country", kCountryMap, xconfig_data_.user.country);
});
GroupBox("Profile", [&]() {
DrawCombobox(io, "Default Profile", profiles_,
xconfig_data_.user.default_profile);
FlagCheckbox("Parental Control",
xconfig_data_.user.parental_control_flags,
static_cast<uint8_t>(kernel::X_PC_FLAGS::PCEnabled));
});
ImGui::Dummy(ImVec2(2.f, 2.f));
GroupBox("Retail Options", [&]() {
FlagCheckbox("Dashboard Initialized", xconfig_data_.user.retail_flags,
static_cast<uint32_t>(
kernel::X_RETAIL_FLAGS::DashboardInitialized));
FlagCheckbox(
"IPTV Initialized", xconfig_data_.user.retail_flags,
static_cast<uint32_t>(kernel::X_RETAIL_FLAGS::IPTVEnabled));
FlagCheckbox(
"DVR Initialized", xconfig_data_.user.retail_flags,
static_cast<uint32_t>(kernel::X_RETAIL_FLAGS::IPTVDVREnabled));
FlagCheckbox(
"Kinect Initialized", xconfig_data_.user.retail_flags,
static_cast<uint32_t>(kernel::X_RETAIL_FLAGS::KinectInitialized));
});
ImGui::EndDisabled();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("System")) {
ImGui::BeginDisabled(emulator_window_.emulator()->is_title_open());
ImGui::Dummy(ImVec2(2.f, 2.f));
GroupBox("Video Options", [&]() {
DrawCombobox(io, "AV Region", kAVRegion,
xconfig_data_.secured.av_region);
DrawResolutionCombobox(
io, {kernel::XVGAResolution.data(), kernel::XVGAResolution.size()},
xconfig_data_.user.av_pack_hdmi_sz, xconfig_data_.user.video_flags);
const auto res =
kernel::Resolution(xconfig_data_.user.av_pack_hdmi_sz.get());
ImGui::BeginDisabled(res.is_widescreen());
FlagCheckbox("Widescreen", xconfig_data_.user.video_flags,
static_cast<uint32_t>(kernel::X_VIDEO_FLAGS::Widescreen));
ImGui::EndDisabled();
});
GroupBox("Audio Options", [&]() {
FlagCheckbox("Mono", xconfig_data_.user.audio_flags,
static_cast<uint32_t>(kernel::X_AUDIO_FLAGS::AnalogMono));
FlagCheckbox(
"Dolby Pro Logic", xconfig_data_.user.audio_flags,
static_cast<uint32_t>(kernel::X_AUDIO_FLAGS::DolbyProLogic));
FlagCheckbox(
"Dolby Digital", xconfig_data_.user.audio_flags,
static_cast<uint32_t>(kernel::X_AUDIO_FLAGS::DolbyDigital));
FlagCheckbox("Dolby Digital WMA PRO", xconfig_data_.user.audio_flags,
static_cast<uint32_t>(
kernel::X_AUDIO_FLAGS::DolbyDigitalWithWMAPRO));
FlagCheckbox("Low Latency (unsupported)",
xconfig_data_.user.audio_flags,
static_cast<uint32_t>(kernel::X_AUDIO_FLAGS::LowLatency));
float volume = xconfig_data_.user.music_volume.get();
if (ImGui::SliderFloat("Audio player volume", &volume, 0.0f, 1.0f,
"%.2f")) {
xconfig_data_.user.music_volume = volume;
}
});
GroupBox("Network", [&]() {
if (ImGui::BeginTable("##NetworkTable", 2)) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("MAC Address: ");
ImGui::TableNextColumn();
ByteArray("mac", xconfig_data_.secured.mac_address.data(),
xconfig_data_.secured.mac_address.size());
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("Network ID: ");
ImGui::TableNextColumn();
ByteArray("netid", xconfig_data_.secured.online_network_id.data(),
xconfig_data_.secured.online_network_id.size());
ImGui::EndTable();
}
});
ImGui::EndDisabled();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::Dummy(ImVec2(2.f, 2.f));
// Bottom
ImGui::Separator();
ImGui::BeginDisabled(emulator_window_.emulator()->is_title_open());
if (ImGui::Button("Save")) {
SaveConfig();
save_confirmation_disappearance_ = ImGui::GetTime() + 3.0;
}
if (save_confirmation_disappearance_ > ImGui::GetTime()) {
ImGui::SameLine();
ImGui::Text("Settings Saved!");
}
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() +
ImGui::GetContentRegionAvail().x - 55.f);
if (ImGui::Button("Reset", ImVec2(55.f, 0.0f))) {
xconfig_->SetDefaults();
xconfig_data_ = *xconfig_->GetXConfig();
save_confirmation_disappearance_ = ImGui::GetTime() + 3.0;
}
ImGui::EndDisabled();
ImGui::End();
}
void ConsoleSettingsDialog::SaveConfig() {
xconfig_->WriteXConfig(&xconfig_data_);
}
} // namespace app
} // namespace xe

View File

@@ -0,0 +1,62 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_APP_CONSOLE_SETTING_DIALOG_H_
#define XENIA_APP_CONSOLE_SETTING_DIALOG_H_
#include "xenia/ui/imgui_dialog.h"
#include "xenia/ui/imgui_drawer.h"
#include "xenia/xbox.h"
#include "xenia/kernel/xconfig.h"
namespace xe {
namespace app {
class EmulatorWindow;
class ConsoleSettingsDialog final : public ui::ImGuiDialog {
public:
ConsoleSettingsDialog(ui::ImGuiDrawer* imgui_drawer,
EmulatorWindow& emulator_window,
kernel::XConfig* xconfig)
: ui::ImGuiDialog(imgui_drawer),
emulator_window_(emulator_window),
xconfig_(xconfig),
xconfig_data_(*xconfig->GetXConfig()) {
const auto profiles = emulator_window.emulator()
->kernel_state()
->xam_state()
->profile_manager()
->GetAccounts();
for (const auto& [xuid, profile] : *profiles) {
profiles_.insert({xuid, profile.GetGamertagString()});
}
}
protected:
void OnDraw(ImGuiIO& io) override;
private:
void SaveConfig();
EmulatorWindow& emulator_window_;
kernel::XConfig* xconfig_ = nullptr;
kernel::XConfigData xconfig_data_{};
std::map<uint64_t, std::string> profiles_;
// UI specific variables
double save_confirmation_disappearance_ = 0.0;
};
} // namespace app
} // namespace xe
#endif

View File

@@ -19,6 +19,8 @@
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
#include "xenia/app/console_settings_dialog.h"
#include "xenia/base/assert.h"
#include "xenia/base/clock.h"
#include "xenia/base/cvar.h"
@@ -36,6 +38,7 @@
#include "xenia/kernel/xam/profile_manager.h"
#include "xenia/kernel/xam/xam_module.h"
#include "xenia/kernel/xam/xam_state.h"
#include "xenia/kernel/xconfig.h"
#include "xenia/ui/file_picker.h"
#include "xenia/ui/graphics_provider.h"
#include "xenia/ui/imgui_dialog.h"
@@ -725,7 +728,8 @@ void EmulatorWindow::XMPConfigDialog::OnDraw(ImGuiIO& io) {
volume_ =
emulator_window_.emulator_->audio_media_player()->GetVolume()->load();
if (ImGui::SliderFloat("Audio player volume", &volume_, 0.0f, 1.0f)) {
if (ImGui::SliderFloat("Audio player volume", &volume_, 0.0f, 1.0f,
"%.2f")) {
audio_player->SetVolume(volume_);
}
}
@@ -882,6 +886,15 @@ bool EmulatorWindow::Initialize() {
}
main_menu->AddChild(std::move(xmp_menu));
// Console menu
auto console_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Console");
{
console_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "&Open console settings", "",
std::bind(&EmulatorWindow::ToggleConsoleSettingsDialog, this)));
}
main_menu->AddChild(std::move(console_menu));
// Help menu.
auto help_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Help");
{
@@ -1541,12 +1554,15 @@ void EmulatorWindow::GpuClearCaches() {
emulator()->graphics_system()->ClearCaches();
}
void EmulatorWindow::SetFullscreen(bool fullscreen) {
if (window_->IsFullscreen() == fullscreen) {
void EmulatorWindow::SetFullscreen(bool fullscreen_) {
if (window_->IsFullscreen() == fullscreen_) {
return;
}
window_->SetFullscreen(fullscreen);
window_->SetCursorVisibility(fullscreen
OVERRIDE_bool(fullscreen, fullscreen_);
window_->SetFullscreen(fullscreen_);
window_->SetCursorVisibility(fullscreen_
? ui::Window::CursorVisibility::kAutoHidden
: ui::Window::CursorVisibility::kVisible);
}
@@ -1596,6 +1612,20 @@ void EmulatorWindow::ToggleXMPConfigDialog() {
}
}
void EmulatorWindow::ToggleConsoleSettingsDialog() {
if (!console_settings_dialog_) {
console_settings_dialog_ =
std::unique_ptr<ConsoleSettingsDialog>(new ConsoleSettingsDialog(
imgui_drawer_.get(), *this, emulator_->kernel_state()->xconfig()));
} else {
if (console_settings_dialog_->IsClosing()) {
console_settings_dialog_.release();
} else {
console_settings_dialog_.reset();
}
}
}
void EmulatorWindow::ToggleControllerVibration() {
auto input_sys = emulator()->input_system();
if (input_sys) {
@@ -2341,6 +2371,10 @@ void EmulatorWindow::ClearDialogs() {
display_config_dialog_.reset();
}
if (console_settings_dialog_) {
console_settings_dialog_.reset();
}
imgui_drawer_.get()->ClearDialogs();
emulator_->kernel_state()->xam_state()->xam_dialogs_shown_ = 0;
}

View File

@@ -29,6 +29,8 @@
namespace xe {
namespace app {
class ConsoleSettingsDialog;
struct RecentTitleEntry {
std::string title_name;
std::filesystem::path path_to_file;
@@ -94,6 +96,8 @@ class EmulatorWindow {
void ToggleProfilesConfigDialog();
void ToggleXMPConfigDialog();
void ToggleConsoleSettingsDialog();
void SetHotkeysState(bool enabled) { disable_hotkeys_ = !enabled; }
// Types of button functions for hotkeys.
@@ -313,6 +317,7 @@ class EmulatorWindow {
bool initializing_shader_storage_ = false;
std::unique_ptr<DisplayConfigDialog> display_config_dialog_;
std::unique_ptr<ConsoleSettingsDialog> console_settings_dialog_;
// Storing pointers and toggling dialog state is useful for broadcasting
// messages back to guest.

View File

@@ -130,7 +130,8 @@ DECLARE_bool(debug);
DEFINE_bool(discord, true, "Enable Discord rich presence", "General");
DECLARE_bool(widescreen);
DECLARE_int32(window_size_x);
DECLARE_int32(window_size_y);
namespace xe {
namespace app {
@@ -538,12 +539,10 @@ 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();
// Main emulator display window.
emulator_window_ = EmulatorWindow::Create(emulator_.get(), app_context(),
res.first, res.second);
emulator_window_ =
EmulatorWindow::Create(emulator_.get(), app_context(),
cvars::window_size_x, cvars::window_size_y);
if (!emulator_window_) {
XELOGE("Failed to create the main emulator window");
return false;