xenia/src/xenia/app/emulator_window.cc
2025-08-24 23:45:51 +00:00

1149 lines
40 KiB
C++

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/app/emulator_window.h"
#include <filesystem>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include "third_party/cpptoml/include/cpptoml.h"
#include "third_party/fmt/include/fmt/format.h"
#include "third_party/imgui/imgui.h"
#include "xenia/base/assert.h"
#include "xenia/base/clock.h"
#include "xenia/base/cvar.h"
#include "xenia/base/debugging.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/cpu/processor.h"
#include "xenia/emulator.h"
#include "xenia/gpu/command_processor.h"
#include "xenia/gpu/graphics_system.h"
#include "xenia/ui/file_picker.h"
#include "xenia/ui/graphics_provider.h"
#include "xenia/ui/imgui_dialog.h"
#include "xenia/ui/imgui_drawer.h"
#include "xenia/ui/immediate_drawer.h"
#include "xenia/ui/presenter.h"
#include "xenia/ui/ui_event.h"
#include "xenia/ui/virtual_key.h"
// Autogenerated by `xb premake`.
#include "build/version.h"
DECLARE_bool(debug);
DEFINE_bool(fullscreen, false, "Whether to launch the emulator in fullscreen.",
"Display");
DEFINE_string(
postprocess_antialiasing, "",
"Post-processing anti-aliasing effect to apply to the image output of the "
"game.\n"
"Using post-process anti-aliasing is heavily recommended when AMD "
"FidelityFX Contrast Adaptive Sharpening or Super Resolution 1.0 is "
"active.\n"
"Use: [none, fxaa, fxaa_extreme]\n"
" none (or any value not listed here):\n"
" Don't alter the original image.\n"
" fxaa:\n"
" NVIDIA Fast Approximate Anti-Aliasing 3.11, normal quality preset (12)."
"\n"
" fxaa_extreme:\n"
" NVIDIA Fast Approximate Anti-Aliasing 3.11, extreme quality preset "
"(39).",
"Display");
DEFINE_string(
postprocess_scaling_and_sharpening, "",
"Post-processing effect to use for resampling and/or sharpening of the "
"final display output.\n"
"Use: [bilinear, cas, fsr]\n"
" bilinear (or any value not listed here):\n"
" Original image at 1:1, simple bilinear stretching for resampling.\n"
" cas:\n"
" Use AMD FidelityFX Contrast Adaptive Sharpening (CAS) for sharpening "
"at scaling factors of up to 2x2, with additional bilinear stretching for "
"larger factors.\n"
" fsr:\n"
" Use AMD FidelityFX Super Resolution 1.0 (FSR) for highest-quality "
"upscaling, or AMD FidelityFX Contrast Adaptive Sharpening for sharpening "
"while not scaling or downsampling.\n"
" For scaling by factors of more than 2x2, multiple FSR passes are done.",
"Display");
DEFINE_double(
postprocess_ffx_cas_additional_sharpness,
xe::ui::Presenter::GuestOutputPaintConfig::kCasAdditionalSharpnessDefault,
"Additional sharpness for AMD FidelityFX Contrast Adaptive Sharpening "
"(CAS), from 0 to 1.\n"
"Higher is sharper.",
"Display");
DEFINE_uint32(
postprocess_ffx_fsr_max_upsampling_passes,
xe::ui::Presenter::GuestOutputPaintConfig::kFsrMaxUpscalingPassesMax,
"Maximum number of upsampling passes performed in AMD FidelityFX Super "
"Resolution 1.0 (FSR) before falling back to bilinear stretching after the "
"final pass.\n"
"Each pass upscales only to up to 2x2 the previous size. If the game "
"outputs a 1280x720 image, 1 pass will upscale it to up to 2560x1440 "
"(below 4K), after 2 passes it will be upscaled to a maximum of 5120x2880 "
"(including 3840x2160 for 4K), and so on.\n"
"This variable has no effect if the display resolution isn't very high, "
"but may be reduced on resolutions like 4K or 8K in case the performance "
"impact of multiple FSR upsampling passes is too high, or if softer edges "
"are desired.\n"
"The default value is the maximum internally supported by Xenia.",
"Display");
DEFINE_double(
postprocess_ffx_fsr_sharpness_reduction,
xe::ui::Presenter::GuestOutputPaintConfig::kFsrSharpnessReductionDefault,
"Sharpness reduction for AMD FidelityFX Super Resolution 1.0 (FSR), in "
"stops.\n"
"Lower is sharper.",
"Display");
// Dithering to 8bpc is enabled by default since the effect is minor, only
// effects what can't be shown normally by host displays, and nothing is changed
// by it for 8bpc source without resampling.
DEFINE_bool(
postprocess_dither, true,
"Dither the final image output from the internal precision to 8 bits per "
"channel so gradients are smoother.\n"
"On a 10bpc display, the lower 2 bits will still be kept, but noise will "
"be added to them - disabling may be recommended for 10bpc, but it "
"depends on the 10bpc displaying capabilities of the actual display used.",
"Display");
DEFINE_int32(recent_titles_entry_amount, 10,
"Allows user to define how many titles is saved in list of "
"recently played titles.",
"General");
namespace xe {
namespace app {
using xe::ui::FileDropEvent;
using xe::ui::KeyEvent;
using xe::ui::MenuItem;
using xe::ui::UIEvent;
const std::string kRecentlyPlayedTitlesFilename = "recent.toml";
const std::string kBaseTitle = "Xenia";
EmulatorWindow::EmulatorWindow(Emulator* emulator,
ui::WindowedAppContext& app_context)
: emulator_(emulator),
app_context_(app_context),
window_listener_(*this),
window_(ui::Window::Create(app_context, kBaseTitle, 1280, 720)),
imgui_drawer_(
std::make_unique<ui::ImGuiDrawer>(window_.get(), kZOrderImGui)),
display_config_game_config_load_callback_(
new DisplayConfigGameConfigLoadCallback(*emulator, *this)) {
base_title_ = kBaseTitle +
#ifdef DEBUG
#if _NO_DEBUG_HEAP == 1
" DEBUG"
#else
" CHECKED"
#endif
#endif
" ("
#ifdef XE_BUILD_IS_PR
"PR#" XE_BUILD_PR_NUMBER " " XE_BUILD_PR_REPO
" " XE_BUILD_PR_BRANCH "@" XE_BUILD_PR_COMMIT_SHORT " against "
#endif
XE_BUILD_BRANCH "@" XE_BUILD_COMMIT_SHORT " on " XE_BUILD_DATE
")";
LoadRecentlyLaunchedTitles();
}
std::unique_ptr<EmulatorWindow> EmulatorWindow::Create(
Emulator* emulator, ui::WindowedAppContext& app_context) {
assert_true(app_context.IsInUIThread());
std::unique_ptr<EmulatorWindow> emulator_window(
new EmulatorWindow(emulator, app_context));
if (!emulator_window->Initialize()) {
return nullptr;
}
return emulator_window;
}
EmulatorWindow::~EmulatorWindow() {
// Notify the ImGui drawer that the immediate drawer is being destroyed.
ShutdownGraphicsSystemPresenterPainting();
}
ui::Presenter* EmulatorWindow::GetGraphicsSystemPresenter() const {
gpu::GraphicsSystem* graphics_system = emulator_->graphics_system();
return graphics_system ? graphics_system->presenter() : nullptr;
}
void EmulatorWindow::SetupGraphicsSystemPresenterPainting() {
ShutdownGraphicsSystemPresenterPainting();
if (!window_) {
return;
}
ui::Presenter* presenter = GetGraphicsSystemPresenter();
if (!presenter) {
return;
}
ApplyDisplayConfigForCvars();
window_->SetPresenter(presenter);
immediate_drawer_ =
emulator_->graphics_system()->provider()->CreateImmediateDrawer();
if (immediate_drawer_) {
immediate_drawer_->SetPresenter(presenter);
imgui_drawer_->SetPresenterAndImmediateDrawer(presenter,
immediate_drawer_.get());
Profiler::SetUserIO(kZOrderProfiler, window_.get(), presenter,
immediate_drawer_.get());
}
}
void EmulatorWindow::ShutdownGraphicsSystemPresenterPainting() {
Profiler::SetUserIO(kZOrderProfiler, window_.get(), nullptr, nullptr);
imgui_drawer_->SetPresenterAndImmediateDrawer(nullptr, nullptr);
immediate_drawer_.reset();
if (window_) {
window_->SetPresenter(nullptr);
}
}
void EmulatorWindow::OnEmulatorInitialized() {
emulator_initialized_ = true;
window_->SetMainMenuEnabled(true);
// When the user can see that the emulator isn't initializing anymore (the
// menu isn't disabled), enter fullscreen if requested.
if (cvars::fullscreen) {
SetFullscreen(true);
}
}
void EmulatorWindow::EmulatorWindowListener::OnClosing(ui::UIEvent& e) {
emulator_window_.app_context_.QuitFromUIThread();
}
void EmulatorWindow::EmulatorWindowListener::OnFileDrop(ui::FileDropEvent& e) {
emulator_window_.FileDrop(e.filename());
}
void EmulatorWindow::EmulatorWindowListener::OnKeyDown(ui::KeyEvent& e) {
emulator_window_.OnKeyDown(e);
}
void EmulatorWindow::DisplayConfigGameConfigLoadCallback::PostGameConfigLoad() {
emulator_window_.ApplyDisplayConfigForCvars();
}
void EmulatorWindow::DisplayConfigDialog::OnDraw(ImGuiIO& io) {
gpu::GraphicsSystem* graphics_system =
emulator_window_.emulator_->graphics_system();
if (!graphics_system) {
return;
}
// In the top-left corner so it's close to the menu bar from where it was
// opened.
// Origin Y coordinate 20 was taken from the Dear ImGui demo.
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);
bool dialog_open = true;
if (!ImGui::Begin("Post-processing", &dialog_open,
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_HorizontalScrollbar)) {
ImGui::End();
return;
}
// Even if the close button has been pressed, still paint everything not to
// have one frame with an empty window.
// Prevent user confusion which has been reported multiple times.
ImGui::TextUnformatted("All effects can be used on GPUs of any brand.");
ImGui::Spacing();
gpu::CommandProcessor* command_processor =
graphics_system->command_processor();
if (command_processor) {
if (ImGui::TreeNodeEx(
"Anti-aliasing",
ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen)) {
gpu::CommandProcessor::SwapPostEffect current_swap_post_effect =
command_processor->GetDesiredSwapPostEffect();
int new_swap_post_effect_index = int(current_swap_post_effect);
ImGui::RadioButton("None", &new_swap_post_effect_index,
int(gpu::CommandProcessor::SwapPostEffect::kNone));
ImGui::RadioButton(
"NVIDIA Fast Approximate Anti-Aliasing 3.11 (FXAA), normal quality",
&new_swap_post_effect_index,
int(gpu::CommandProcessor::SwapPostEffect::kFxaa));
ImGui::RadioButton(
"NVIDIA Fast Approximate Anti-Aliasing 3.11 (FXAA), extreme quality",
&new_swap_post_effect_index,
int(gpu::CommandProcessor::SwapPostEffect::kFxaaExtreme));
gpu::CommandProcessor::SwapPostEffect new_swap_post_effect =
gpu::CommandProcessor::SwapPostEffect(new_swap_post_effect_index);
if (current_swap_post_effect != new_swap_post_effect) {
command_processor->SetDesiredSwapPostEffect(new_swap_post_effect);
}
// Override the values in the cvars to save them to the config at exit if
// the user has set them to anything new.
if (GetSwapPostEffectForCvarValue(cvars::postprocess_antialiasing) !=
new_swap_post_effect) {
OVERRIDE_string(postprocess_antialiasing,
GetCvarValueForSwapPostEffect(new_swap_post_effect));
}
ImGui::TreePop();
}
}
ui::Presenter* presenter = graphics_system->presenter();
if (presenter) {
const ui::Presenter::GuestOutputPaintConfig& current_presenter_config =
presenter->GetGuestOutputPaintConfigFromUIThread();
ui::Presenter::GuestOutputPaintConfig new_presenter_config =
current_presenter_config;
if (ImGui::TreeNodeEx(
"Resampling and sharpening",
ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen)) {
// Filtering effect.
int new_effect_index = int(new_presenter_config.GetEffect());
ImGui::RadioButton(
"None / bilinear", &new_effect_index,
int(ui::Presenter::GuestOutputPaintConfig::Effect::kBilinear));
ImGui::RadioButton(
"AMD FidelityFX Contrast Adaptive Sharpening (CAS)",
&new_effect_index,
int(ui::Presenter::GuestOutputPaintConfig::Effect::kCas));
ImGui::RadioButton(
"AMD FidelityFX Super Resolution 1.0 (FSR)", &new_effect_index,
int(ui::Presenter::GuestOutputPaintConfig::Effect::kFsr));
new_presenter_config.SetEffect(
ui::Presenter::GuestOutputPaintConfig::Effect(new_effect_index));
// effect_description must be one complete, but short enough, sentence per
// line, as TextWrapped doesn't work correctly in auto-resizing windows
// (in the initial frames, the window becomes extremely tall, and widgets
// added after the wrapped text have no effect on the width of the text).
const char* effect_description = nullptr;
switch (new_presenter_config.GetEffect()) {
case ui::Presenter::GuestOutputPaintConfig::Effect::kBilinear:
effect_description =
"Simple bilinear filtering is done if resampling is needed.\n"
"Otherwise, only anti-aliasing is done if enabled, or displaying "
"as is.";
break;
case ui::Presenter::GuestOutputPaintConfig::Effect::kCas:
effect_description =
"Sharpening and resampling to up to 2x2 to improve the fidelity "
"of details.\n"
"For scaling by more than 2x2, bilinear stretching is done "
"afterwards.";
break;
case ui::Presenter::GuestOutputPaintConfig::Effect::kFsr:
effect_description =
"High-quality edge-preserving upscaling to arbitrary target "
"resolutions.\n"
"For scaling by more than 2x2, multiple upsampling passes are "
"done.\n"
"If not upscaling, Contrast Adaptive Sharpening (CAS) is used "
"instead.";
break;
}
if (effect_description) {
ImGui::TextUnformatted(effect_description);
}
if (new_presenter_config.GetEffect() ==
ui::Presenter::GuestOutputPaintConfig::Effect::kCas ||
new_presenter_config.GetEffect() ==
ui::Presenter::GuestOutputPaintConfig::Effect::kFsr) {
if (effect_description) {
ImGui::Spacing();
}
ImGui::TextUnformatted(
"FXAA is highly recommended when using CAS or FSR.");
ImGui::Spacing();
// 2 decimal places is more or less enough precision for the sharpness
// given the minor visual effect of small changes, the width of the
// slider, and readability convenience (2 decimal places is like an
// integer percentage). However, because Dear ImGui parses the string
// representation of the number and snaps the value to it internally,
// 2 decimal places actually offer less precision than the slider itself
// does. This is especially prominent in the low range of the non-linear
// FSR sharpness reduction slider. 3 decimal places are optimal in this
// case.
if (new_presenter_config.GetEffect() ==
ui::Presenter::GuestOutputPaintConfig::Effect::kFsr) {
float fsr_sharpness_reduction =
new_presenter_config.GetFsrSharpnessReduction();
ImGui::TextUnformatted(
"FSR sharpness reduction when upscaling (lower is sharper):");
const auto label =
fmt::format("{:.3f} stops", fsr_sharpness_reduction);
// Power 2.0 scaling as the reduction is in stops, used in exp2.
fsr_sharpness_reduction = sqrt(2.f * fsr_sharpness_reduction);
ImGui::SliderFloat(
"##FSRSharpnessReduction", &fsr_sharpness_reduction,
ui::Presenter::GuestOutputPaintConfig::kFsrSharpnessReductionMin,
ui::Presenter::GuestOutputPaintConfig::kFsrSharpnessReductionMax,
label.c_str(), ImGuiSliderFlags_NoInput);
fsr_sharpness_reduction =
.5f * fsr_sharpness_reduction * fsr_sharpness_reduction;
ImGui::SameLine();
if (ImGui::Button("Reset##ResetFSRSharpnessReduction")) {
fsr_sharpness_reduction = ui::Presenter::GuestOutputPaintConfig ::
kFsrSharpnessReductionDefault;
}
new_presenter_config.SetFsrSharpnessReduction(
fsr_sharpness_reduction);
}
float cas_additional_sharpness =
new_presenter_config.GetCasAdditionalSharpness();
ImGui::TextUnformatted(
new_presenter_config.GetEffect() ==
ui::Presenter::GuestOutputPaintConfig::Effect::kFsr
? "CAS additional sharpness when not upscaling (higher is "
"sharper):"
: "CAS additional sharpness (higher is sharper):");
ImGui::SliderFloat(
"##CASAdditionalSharpness", &cas_additional_sharpness,
ui::Presenter::GuestOutputPaintConfig::kCasAdditionalSharpnessMin,
ui::Presenter::GuestOutputPaintConfig::kCasAdditionalSharpnessMax,
"%.3f");
ImGui::SameLine();
if (ImGui::Button("Reset##ResetCASAdditionalSharpness")) {
cas_additional_sharpness = ui::Presenter::GuestOutputPaintConfig ::
kCasAdditionalSharpnessDefault;
}
new_presenter_config.SetCasAdditionalSharpness(
cas_additional_sharpness);
// There's no need to expose the setting for the maximum number of FSR
// EASU passes as it's largely meaningless if the user doesn't have a
// very high-resolution monitor compared to the original image size as
// most of the values of the slider will have no effect, and that's just
// very fine-grained performance control for a fixed-overhead pass only
// for huge screen resolutions.
}
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Dithering", ImGuiTreeNodeFlags_Framed |
ImGuiTreeNodeFlags_DefaultOpen)) {
bool dither = current_presenter_config.GetDither();
ImGui::Checkbox(
"Dither the final output to 8bpc to make gradients smoother",
&dither);
new_presenter_config.SetDither(dither);
ImGui::TreePop();
}
presenter->SetGuestOutputPaintConfigFromUIThread(new_presenter_config);
// Override the values in the cvars to save them to the config at exit if
// the user has set them to anything new.
ui::Presenter::GuestOutputPaintConfig cvars_presenter_config =
GetGuestOutputPaintConfigForCvars();
if (cvars_presenter_config.GetEffect() !=
new_presenter_config.GetEffect()) {
OVERRIDE_string(postprocess_scaling_and_sharpening,
GetCvarValueForGuestOutputPaintEffect(
new_presenter_config.GetEffect()));
}
if (cvars_presenter_config.GetCasAdditionalSharpness() !=
new_presenter_config.GetCasAdditionalSharpness()) {
OVERRIDE_double(postprocess_ffx_cas_additional_sharpness,
new_presenter_config.GetCasAdditionalSharpness());
}
if (cvars_presenter_config.GetFsrSharpnessReduction() !=
new_presenter_config.GetFsrSharpnessReduction()) {
OVERRIDE_double(postprocess_ffx_fsr_sharpness_reduction,
new_presenter_config.GetFsrSharpnessReduction());
}
if (cvars_presenter_config.GetDither() !=
new_presenter_config.GetDither()) {
OVERRIDE_bool(postprocess_dither, new_presenter_config.GetDither());
}
}
ImGui::End();
if (!dialog_open) {
emulator_window_.ToggleDisplayConfigDialog();
// `this` might have been destroyed by ToggleDisplayConfigDialog.
return;
}
}
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");
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));
#ifdef DEBUG
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, "E&xit", "Alt+F4",
[this]() { window_->RequestClose(); }));
}
main_menu->AddChild(std::move(file_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));
// 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)));
}
main_menu->AddChild(std::move(display_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...", [this]() {
LaunchWebBrowser(
"https://github.com/xenia-project/xenia/compare/" XE_BUILD_COMMIT
"..." XE_BUILD_BRANCH);
}));
help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
help_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "&About...",
[this]() { LaunchWebBrowser("https://xenia.jp/about/"); }));
}
main_menu->AddChild(std::move(help_menu));
window_->SetMainMenu(std::move(main_menu));
window_->SetMainMenuEnabled(false);
UpdateTitle();
if (!window_->Open()) {
XELOGE("Failed to open the platform window");
return false;
}
Profiler::SetUserIO(kZOrderProfiler, window_.get(), nullptr, nullptr);
return true;
}
const char* EmulatorWindow::GetCvarValueForSwapPostEffect(
gpu::CommandProcessor::SwapPostEffect effect) {
switch (effect) {
case gpu::CommandProcessor::SwapPostEffect::kFxaa:
return "fxaa";
case gpu::CommandProcessor::SwapPostEffect::kFxaaExtreme:
return "fxaa_extreme";
default:
return "";
}
}
gpu::CommandProcessor::SwapPostEffect
EmulatorWindow::GetSwapPostEffectForCvarValue(const std::string& cvar_value) {
if (cvar_value == GetCvarValueForSwapPostEffect(
gpu::CommandProcessor::SwapPostEffect::kFxaa)) {
return gpu::CommandProcessor::SwapPostEffect::kFxaa;
}
if (cvar_value == GetCvarValueForSwapPostEffect(
gpu::CommandProcessor::SwapPostEffect::kFxaaExtreme)) {
return gpu::CommandProcessor::SwapPostEffect::kFxaaExtreme;
}
return gpu::CommandProcessor::SwapPostEffect::kNone;
}
const char* EmulatorWindow::GetCvarValueForGuestOutputPaintEffect(
ui::Presenter::GuestOutputPaintConfig::Effect effect) {
switch (effect) {
case ui::Presenter::GuestOutputPaintConfig::Effect::kCas:
return "cas";
case ui::Presenter::GuestOutputPaintConfig::Effect::kFsr:
return "fsr";
default:
return "";
}
}
ui::Presenter::GuestOutputPaintConfig::Effect
EmulatorWindow::GetGuestOutputPaintEffectForCvarValue(
const std::string& cvar_value) {
if (cvar_value == GetCvarValueForGuestOutputPaintEffect(
ui::Presenter::GuestOutputPaintConfig::Effect::kCas)) {
return ui::Presenter::GuestOutputPaintConfig::Effect::kCas;
}
if (cvar_value == GetCvarValueForGuestOutputPaintEffect(
ui::Presenter::GuestOutputPaintConfig::Effect::kFsr)) {
return ui::Presenter::GuestOutputPaintConfig::Effect::kFsr;
}
return ui::Presenter::GuestOutputPaintConfig::Effect::kBilinear;
}
ui::Presenter::GuestOutputPaintConfig
EmulatorWindow::GetGuestOutputPaintConfigForCvars() {
ui::Presenter::GuestOutputPaintConfig paint_config;
paint_config.SetAllowOverscanCutoff(true);
paint_config.SetEffect(GetGuestOutputPaintEffectForCvarValue(
cvars::postprocess_scaling_and_sharpening));
paint_config.SetCasAdditionalSharpness(
float(cvars::postprocess_ffx_cas_additional_sharpness));
paint_config.SetFsrMaxUpsamplingPasses(
cvars::postprocess_ffx_fsr_max_upsampling_passes);
paint_config.SetFsrSharpnessReduction(
float(cvars::postprocess_ffx_fsr_sharpness_reduction));
paint_config.SetDither(cvars::postprocess_dither);
return paint_config;
}
void EmulatorWindow::ApplyDisplayConfigForCvars() {
gpu::GraphicsSystem* graphics_system = emulator_->graphics_system();
if (!graphics_system) {
return;
}
gpu::CommandProcessor* command_processor =
graphics_system->command_processor();
if (command_processor) {
command_processor->SetDesiredSwapPostEffect(
GetSwapPostEffectForCvarValue(cvars::postprocess_antialiasing));
}
ui::Presenter* presenter = graphics_system->presenter();
if (presenter) {
presenter->SetGuestOutputPaintConfigFromUIThread(
GetGuestOutputPaintConfigForCvars());
}
}
void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) {
if (!emulator_initialized_) {
return;
}
switch (e.virtual_key()) {
case ui::VirtualKey::kO: {
if (!e.is_ctrl_pressed()) {
return;
}
FileOpen();
} break;
case ui::VirtualKey::kMultiply: {
CpuTimeScalarReset();
} break;
case ui::VirtualKey::kSubtract: {
CpuTimeScalarSetHalf();
} break;
case ui::VirtualKey::kAdd: {
CpuTimeScalarSetDouble();
} break;
case ui::VirtualKey::kF3: {
Profiler::ToggleDisplay();
} break;
case ui::VirtualKey::kF4: {
GpuTraceFrame();
} break;
case ui::VirtualKey::kF5: {
GpuClearCaches();
} break;
case ui::VirtualKey::kF6: {
ToggleDisplayConfigDialog();
} break;
case ui::VirtualKey::kF11: {
ToggleFullscreen();
} break;
case ui::VirtualKey::kEscape: {
// Allow users to escape fullscreen (but not enter it).
if (!window_->IsFullscreen()) {
return;
}
SetFullscreen(false);
} break;
#ifdef DEBUG
case ui::VirtualKey::kF7: {
// Save to file
// TODO: Choose path based on user input, or from options
// TODO: Spawn a new thread to do this.
emulator()->SaveToFile("test.sav");
} break;
case ui::VirtualKey::kF8: {
// Restore from file
// TODO: Choose path from user
// TODO: Spawn a new thread to do this.
emulator()->RestoreFromFile("test.sav");
} break;
#endif // #ifdef DEBUG
case ui::VirtualKey::kPause: {
CpuBreakIntoDebugger();
} break;
case ui::VirtualKey::kCancel: {
CpuBreakIntoHostDebugger();
} break;
case ui::VirtualKey::kF1: {
ShowFAQ();
} break;
case ui::VirtualKey::kF2: {
ShowBuildCommit();
} break;
default:
return;
}
e.set_handled(true);
}
void EmulatorWindow::FileDrop(const std::filesystem::path& filename) {
if (!emulator_initialized_) {
return;
}
auto result = emulator_->LaunchPath(filename);
if (XFAILED(result)) {
// TODO: Display a message box.
XELOGE("Failed to launch target: {:08X}", result);
}
}
void EmulatorWindow::FileOpen() {
std::filesystem::path path;
auto file_picker = xe::ui::FilePicker::Create();
file_picker->set_mode(ui::FilePicker::Mode::kOpen);
file_picker->set_type(ui::FilePicker::Type::kFile);
file_picker->set_multi_selection(false);
file_picker->set_title("Select Content Package");
file_picker->set_extensions({
{"Supported Files", "*.iso;*.xex;*.*"},
{"Disc Image (*.iso)", "*.iso"},
{"Xbox Executable (*.xex)", "*.xex"},
//{"Content Package (*.xcp)", "*.xcp" },
{"All Files (*.*)", "*.*"},
});
if (file_picker->Show(window_.get())) {
auto selected_files = file_picker->selected_files();
if (!selected_files.empty()) {
path = selected_files[0];
}
}
if (!path.empty()) {
// Normalize the path and make absolute.
auto abs_path = std::filesystem::absolute(path);
auto result = emulator_->LaunchPath(abs_path);
if (XFAILED(result)) {
// TODO: Display a message box.
XELOGE("Failed to launch target: {:08X}", result);
} else {
AddRecentlyLaunchedTitle(abs_path, emulator_->title_name());
}
}
}
void EmulatorWindow::FileClose() {
if (emulator_->is_title_open()) {
emulator_->TerminateTitle();
}
}
void EmulatorWindow::ShowContentDirectory() {
std::filesystem::path target_path;
auto content_root = emulator_->content_root();
if (!emulator_->is_title_open() || !emulator_->kernel_state()) {
target_path = content_root;
} else {
// TODO(gibbed): expose this via ContentManager?
auto title_id =
fmt::format("{:08X}", emulator_->kernel_state()->title_id());
auto package_root = content_root / title_id;
target_path = package_root;
}
if (!std::filesystem::exists(target_path)) {
std::filesystem::create_directories(target_path);
}
LaunchFileExplorer(target_path);
}
void EmulatorWindow::CpuTimeScalarReset() {
Clock::set_guest_time_scalar(1.0);
UpdateTitle();
}
void EmulatorWindow::CpuTimeScalarSetHalf() {
Clock::set_guest_time_scalar(Clock::guest_time_scalar() / 2.0);
UpdateTitle();
}
void EmulatorWindow::CpuTimeScalarSetDouble() {
Clock::set_guest_time_scalar(Clock::guest_time_scalar() * 2.0);
UpdateTitle();
}
void EmulatorWindow::CpuBreakIntoDebugger() {
if (!cvars::debug) {
xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(), "Xenia Debugger",
"Xenia must be launched with the "
"--debug flag in order to enable "
"debugging.");
return;
}
auto processor = emulator()->processor();
if (processor->execution_state() == cpu::ExecutionState::kRunning) {
// Currently running, so interrupt (and show the debugger).
processor->Pause();
} else {
// Not running, so just bring the debugger into focus.
processor->ShowDebugger();
}
}
void EmulatorWindow::CpuBreakIntoHostDebugger() { xe::debugging::Break(); }
void EmulatorWindow::GpuTraceFrame() {
emulator()->graphics_system()->RequestFrameTrace();
}
void EmulatorWindow::GpuClearCaches() {
emulator()->graphics_system()->ClearCaches();
}
void EmulatorWindow::SetFullscreen(bool fullscreen) {
if (window_->IsFullscreen() == fullscreen) {
return;
}
window_->SetFullscreen(fullscreen);
window_->SetCursorVisibility(fullscreen
? ui::Window::CursorVisibility::kAutoHidden
: ui::Window::CursorVisibility::kVisible);
}
void EmulatorWindow::ToggleFullscreen() {
SetFullscreen(!window_->IsFullscreen());
}
void EmulatorWindow::ToggleDisplayConfigDialog() {
if (!display_config_dialog_) {
display_config_dialog_ = std::unique_ptr<DisplayConfigDialog>(
new DisplayConfigDialog(imgui_drawer_.get(), *this));
} else {
display_config_dialog_.reset();
}
}
void EmulatorWindow::ShowCompatibility() {
const std::string_view base_url =
"https://github.com/xenia-project/game-compatibility/issues";
std::string url;
// Avoid searching for a title ID of "00000000".
uint32_t title_id = emulator_->title_id();
if (!title_id) {
url = base_url;
} else {
url = fmt::format("{}?q=is%3Aissue+is%3Aopen+{:08X}", base_url, title_id);
}
LaunchWebBrowser(url);
}
void EmulatorWindow::ShowFAQ() {
LaunchWebBrowser("https://github.com/xenia-project/xenia/wiki/FAQ");
}
void EmulatorWindow::ShowBuildCommit() {
#ifdef XE_BUILD_IS_PR
LaunchWebBrowser(
"https://github.com/xenia-project/xenia/pull/" XE_BUILD_PR_NUMBER);
#else
LaunchWebBrowser(
"https://github.com/xenia-project/xenia/commit/" XE_BUILD_COMMIT);
#endif
}
void EmulatorWindow::UpdateTitle() {
xe::StringBuffer sb;
sb.Append(base_title_);
// Title information, if available
if (emulator()->is_title_open()) {
sb.AppendFormat(u8" | [{:08X}", emulator()->title_id());
auto title_version = emulator()->title_version();
if (!title_version.empty()) {
sb.Append(u8" v");
sb.Append(title_version);
}
sb.Append(u8"]");
auto title_name = emulator()->title_name();
if (!title_name.empty()) {
sb.Append(u8" ");
sb.Append(title_name);
}
}
// Graphics system name, if available
auto graphics_system = emulator()->graphics_system();
if (graphics_system) {
auto graphics_name = graphics_system->name();
if (!graphics_name.empty()) {
sb.Append(u8" <");
sb.Append(graphics_name);
sb.Append(u8">");
}
}
if (Clock::guest_time_scalar() != 1.0) {
sb.AppendFormat(u8" (@{:.2f}x)", Clock::guest_time_scalar());
}
if (initializing_shader_storage_) {
sb.Append(u8" (Preloading shaders\u2026)");
}
window_->SetTitle(sb.to_string_view());
}
void EmulatorWindow::SetInitializingShaderStorage(bool initializing) {
if (initializing_shader_storage_ == initializing) {
return;
}
initializing_shader_storage_ = initializing;
UpdateTitle();
}
void EmulatorWindow::RunRecentlyPlayedTitle(
std::filesystem::path path_to_file) {
if (path_to_file.empty()) {
return;
}
auto abs_path = std::filesystem::absolute(path_to_file);
auto result = emulator_->LaunchPath(abs_path);
if (XFAILED(result)) {
// TODO: Display a message box.
XELOGE("Failed to launch target: {:08X}", result);
return;
}
AddRecentlyLaunchedTitle(path_to_file, emulator_->title_name());
}
void EmulatorWindow::FillRecentlyLaunchedTitlesMenu(
xe::ui::MenuItem* recent_menu) {
for (const RecentTitleEntry& entry : recently_launched_titles_) {
std::string item_text = !entry.title_name.empty()
? entry.title_name
: entry.path_to_file.string();
recent_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, item_text,
std::bind(&EmulatorWindow::RunRecentlyPlayedTitle,
this, entry.path_to_file)));
}
}
void EmulatorWindow::LoadRecentlyLaunchedTitles() {
std::ifstream file(kRecentlyPlayedTitlesFilename);
if (!file.is_open()) {
return;
}
std::shared_ptr<cpptoml::table> parsed_file;
try {
cpptoml::parser p(file);
parsed_file = p.parse();
} catch (cpptoml::parse_exception exception) {
// TODO(Gliniak): Better handling of errors, but good enough for now.
return;
}
if (parsed_file->is_table()) {
for (const auto& [index, entry] : *parsed_file->as_table()) {
if (!entry->is_table()) {
continue;
}
const std::shared_ptr<cpptoml::table> entry_table = entry->as_table();
std::string title_name = *entry_table->get_as<std::string>("title_name");
std::string path = *entry_table->get_as<std::string>("path");
std::time_t last_run_time =
*entry_table->get_as<uint64_t>("last_run_time");
if (path.empty()) {
continue;
}
recently_launched_titles_.push_back({title_name, path, last_run_time});
}
}
}
void EmulatorWindow::AddRecentlyLaunchedTitle(
std::filesystem::path path_to_file, std::string title_name) {
// Check if game is already on list and pop it to front
auto entry_index = std::find_if(recently_launched_titles_.cbegin(),
recently_launched_titles_.cend(),
[&title_name](const RecentTitleEntry& entry) {
return entry.title_name == title_name;
});
if (entry_index != recently_launched_titles_.cend()) {
recently_launched_titles_.erase(entry_index);
}
recently_launched_titles_.insert(recently_launched_titles_.cbegin(),
{title_name, path_to_file, time(nullptr)});
// Serialize to toml
auto toml_table = cpptoml::make_table();
uint8_t index = 0;
for (const RecentTitleEntry& entry : recently_launched_titles_) {
auto entry_table = cpptoml::make_table();
// Fill entry under specific index.
std::string str_path = xe::path_to_utf8(entry.path_to_file);
entry_table->insert("title_name", entry.title_name);
entry_table->insert("path", str_path);
entry_table->insert("last_run_time", entry.last_run_time);
entry_table->end();
toml_table->insert(std::to_string(index++), entry_table);
if (index >= cvars::recent_titles_entry_amount) {
break;
}
}
toml_table->end();
// Open and write serialized data.
std::ofstream file(kRecentlyPlayedTitlesFilename, std::ofstream::trunc);
file << *toml_table;
file.close();
}
} // namespace app
} // namespace xe