Merge branch 'master' into vulkan

This commit is contained in:
Triang3l
2022-02-03 21:12:10 +03:00
501 changed files with 56183 additions and 10196 deletions

View File

@@ -2,13 +2,20 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* 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/fmt/include/fmt/format.h"
#include "third_party/imgui/imgui.h"
#include "xenia/base/assert.h"
@@ -20,11 +27,17 @@
#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`.
@@ -32,13 +45,91 @@
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");
namespace xe {
namespace app {
using xe::ui::FileDropEvent;
using xe::ui::KeyEvent;
using xe::ui::MenuItem;
using xe::ui::MouseEvent;
using xe::ui::UIEvent;
const std::string kBaseTitle = "Xenia";
@@ -47,7 +138,12 @@ EmulatorWindow::EmulatorWindow(Emulator* emulator,
ui::WindowedAppContext& app_context)
: emulator_(emulator),
app_context_(app_context),
window_(ui::Window::Create(app_context, kBaseTitle)) {
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
@@ -76,107 +172,331 @@ std::unique_ptr<EmulatorWindow> EmulatorWindow::Create(
return emulator_window;
}
bool EmulatorWindow::Initialize() {
if (!window_->Initialize()) {
XELOGE("Failed to initialize platform window");
return false;
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;
}
UpdateTitle();
ui::Presenter* presenter = GetGraphicsSystemPresenter();
if (!presenter) {
return;
}
window_->on_closed.AddListener(
[this](UIEvent* e) { app_context_.QuitFromUIThread(); });
ApplyDisplayConfigForCvars();
window_->on_file_drop.AddListener(
[this](FileDropEvent* e) { FileDrop(e->filename()); });
window_->SetPresenter(presenter);
window_->on_key_down.AddListener([this](KeyEvent* e) {
bool handled = true;
switch (e->virtual_key()) {
case ui::VirtualKey::kO: {
if (e->is_ctrl_pressed()) {
FileOpen();
}
} break;
case ui::VirtualKey::kMultiply: {
CpuTimeScalarReset();
} break;
case ui::VirtualKey::kSubtract: {
CpuTimeScalarSetHalf();
} break;
case ui::VirtualKey::kAdd: {
CpuTimeScalarSetDouble();
} break;
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());
}
}
case ui::VirtualKey::kF3: {
Profiler::ToggleDisplay();
} break;
void EmulatorWindow::ShutdownGraphicsSystemPresenterPainting() {
Profiler::SetUserIO(kZOrderProfiler, window_.get(), nullptr, nullptr);
imgui_drawer_->SetPresenterAndImmediateDrawer(nullptr, nullptr);
immediate_drawer_.reset();
if (window_) {
window_->SetPresenter(nullptr);
}
}
case ui::VirtualKey::kF4: {
GpuTraceFrame();
} break;
case ui::VirtualKey::kF5: {
GpuClearCaches();
} break;
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;
case ui::VirtualKey::kF11: {
ToggleFullscreen();
} break;
case ui::VirtualKey::kEscape: {
// Allow users to escape fullscreen (but not enter it).
if (window_->is_fullscreen()) {
window_->ToggleFullscreen(false);
} else {
handled = false;
}
} break;
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) {
window_->SetFullscreen(true);
}
}
case ui::VirtualKey::kPause: {
CpuBreakIntoDebugger();
} break;
case ui::VirtualKey::kCancel: {
CpuBreakIntoHostDebugger();
} break;
void EmulatorWindow::EmulatorWindowListener::OnClosing(ui::UIEvent& e) {
emulator_window_.app_context_.QuitFromUIThread();
}
case ui::VirtualKey::kF1: {
ShowHelpWebsite();
} break;
void EmulatorWindow::EmulatorWindowListener::OnFileDrop(ui::FileDropEvent& e) {
emulator_window_.FileDrop(e.filename());
}
case ui::VirtualKey::kF2: {
ShowCommitID();
} break;
void EmulatorWindow::EmulatorWindowListener::OnKeyDown(ui::KeyEvent& e) {
emulator_window_.OnKeyDown(e);
}
default: {
handled = false;
} break;
}
e->set_handled(handled);
});
void EmulatorWindow::DisplayConfigGameConfigLoadCallback::PostGameConfigLoad() {
emulator_window_.ApplyDisplayConfigForCvars();
}
window_->on_mouse_move.AddListener([this](MouseEvent* e) {
if (window_->is_fullscreen() && (e->dx() > 2 || e->dy() > 2)) {
if (!window_->is_cursor_visible()) {
window_->set_cursor_visible(true);
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);
}
cursor_hide_time_ = Clock::QueryHostSystemTime() + 30000000;
// 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):");
// Power 2.0 as the reduction is in stops, used in exp2.
ImGui::SliderFloat(
"##FSRSharpnessReduction", &fsr_sharpness_reduction,
ui::Presenter::GuestOutputPaintConfig::kFsrSharpnessReductionMin,
ui::Presenter::GuestOutputPaintConfig::kFsrSharpnessReductionMax,
"%.3f stops", 2.0f);
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();
}
e->set_handled(false);
});
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);
window_->on_paint.AddListener([this](UIEvent* e) { CheckHideCursor(); });
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.
@@ -186,17 +506,19 @@ bool EmulatorWindow::Initialize() {
file_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, "&Open...", "Ctrl+O",
std::bind(&EmulatorWindow::FileOpen, this)));
#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_->Close(); }));
file_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, "E&xit", "Alt+F4",
[this]() { window_->RequestClose(); }));
}
main_menu->AddChild(std::move(file_menu));
@@ -249,21 +571,35 @@ bool EmulatorWindow::Initialize() {
}
main_menu->AddChild(std::move(gpu_menu));
// Window menu.
auto window_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Window");
// Display menu.
auto display_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Display");
{
window_menu->AddChild(
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(window_menu));
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, "Build commit on GitHub...",
"F2", std::bind(&EmulatorWindow::ShowCommitID, this)));
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(
@@ -271,25 +607,202 @@ bool EmulatorWindow::Initialize() {
"..." XE_BUILD_BRANCH);
}));
help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
help_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, "&Website...", "F1",
std::bind(&EmulatorWindow::ShowHelpWebsite, this)));
help_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "&About...",
[this]() { LaunchWebBrowser("https://xenia.jp/about/"); }));
}
main_menu->AddChild(std::move(help_menu));
window_->set_main_menu(std::move(main_menu));
window_->SetMainMenu(std::move(main_menu));
window_->Resize(1280, 720);
window_->SetMainMenuEnabled(false);
window_->DisableMainMenu();
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.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.
@@ -312,7 +825,7 @@ void EmulatorWindow::FileOpen() {
//{"Content Package (*.xcp)", "*.xcp" },
{"All Files (*.*)", "*.*"},
});
if (file_picker->Show(window_->native_handle())) {
if (file_picker->Show(window_.get())) {
auto selected_files = file_picker->selected_files();
if (!selected_files.empty()) {
path = selected_files[0];
@@ -357,17 +870,6 @@ void EmulatorWindow::ShowContentDirectory() {
LaunchFileExplorer(target_path);
}
void EmulatorWindow::CheckHideCursor() {
if (!window_->is_fullscreen()) {
// Only hide when fullscreen.
return;
}
if (Clock::QueryHostSystemTime() > cursor_hide_time_) {
window_->set_cursor_visible(false);
}
}
void EmulatorWindow::CpuTimeScalarReset() {
Clock::set_guest_time_scalar(1.0);
UpdateTitle();
@@ -385,7 +887,7 @@ void EmulatorWindow::CpuTimeScalarSetDouble() {
void EmulatorWindow::CpuBreakIntoDebugger() {
if (!cvars::debug) {
xe::ui::ImGuiDialog::ShowMessageBox(window_.get(), "Xenia Debugger",
xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(), "Xenia Debugger",
"Xenia must be launched with the "
"--debug flag in order to enable "
"debugging.");
@@ -411,19 +913,48 @@ void EmulatorWindow::GpuClearCaches() {
emulator()->graphics_system()->ClearCaches();
}
void EmulatorWindow::ToggleFullscreen() {
window_->ToggleFullscreen(!window_->is_fullscreen());
void EmulatorWindow::SetFullscreen(bool fullscreen) {
if (window_->IsFullscreen() == fullscreen) {
return;
}
window_->SetFullscreen(fullscreen);
window_->SetCursorVisibility(fullscreen
? ui::Window::CursorVisibility::kAutoHidden
: ui::Window::CursorVisibility::kVisible);
}
// Hide the cursor after a second if we're going fullscreen
cursor_hide_time_ = Clock::QueryHostSystemTime() + 30000000;
if (!window_->is_fullscreen()) {
window_->set_cursor_visible(true);
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::ShowHelpWebsite() { LaunchWebBrowser("https://xenia.jp"); }
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::ShowCommitID() {
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);
@@ -473,7 +1004,7 @@ void EmulatorWindow::UpdateTitle() {
sb.Append(u8" (Preloading shaders\u2026)");
}
window_->set_title(sb.to_string_view());
window_->SetTitle(sb.to_string_view());
}
void EmulatorWindow::SetInitializingShaderStorage(bool initializing) {

View File

@@ -2,7 +2,7 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
@@ -13,42 +13,124 @@
#include <memory>
#include <string>
#include "xenia/emulator.h"
#include "xenia/gpu/command_processor.h"
#include "xenia/ui/imgui_dialog.h"
#include "xenia/ui/imgui_drawer.h"
#include "xenia/ui/immediate_drawer.h"
#include "xenia/ui/menu_item.h"
#include "xenia/ui/presenter.h"
#include "xenia/ui/window.h"
#include "xenia/ui/window_listener.h"
#include "xenia/ui/windowed_app_context.h"
#include "xenia/xbox.h"
namespace xe {
class Emulator;
} // namespace xe
namespace xe {
namespace app {
class EmulatorWindow {
public:
enum : size_t {
// The UI is on top of the game and is open in special cases, so
// lowest-priority.
kZOrderHidInput,
kZOrderImGui,
kZOrderProfiler,
// Emulator window controls are expected to be always accessible by the
// user, so highest-priority.
kZOrderEmulatorWindowInput,
};
virtual ~EmulatorWindow();
static std::unique_ptr<EmulatorWindow> Create(
Emulator* emulator, ui::WindowedAppContext& app_context);
Emulator* emulator() const { return emulator_; }
ui::WindowedAppContext& app_context() const { return app_context_; }
ui::Window* window() const { return window_.get(); }
ui::ImGuiDrawer* imgui_drawer() const { return imgui_drawer_.get(); }
ui::Presenter* GetGraphicsSystemPresenter() const;
void SetupGraphicsSystemPresenterPainting();
void ShutdownGraphicsSystemPresenterPainting();
void OnEmulatorInitialized();
void UpdateTitle();
void SetFullscreen(bool fullscreen);
void ToggleFullscreen();
void SetInitializingShaderStorage(bool initializing);
private:
class EmulatorWindowListener final : public ui::WindowListener,
public ui::WindowInputListener {
public:
explicit EmulatorWindowListener(EmulatorWindow& emulator_window)
: emulator_window_(emulator_window) {}
void OnClosing(ui::UIEvent& e) override;
void OnFileDrop(ui::FileDropEvent& e) override;
void OnKeyDown(ui::KeyEvent& e) override;
private:
EmulatorWindow& emulator_window_;
};
class DisplayConfigGameConfigLoadCallback
: public Emulator::GameConfigLoadCallback {
public:
DisplayConfigGameConfigLoadCallback(Emulator& emulator,
EmulatorWindow& emulator_window)
: Emulator::GameConfigLoadCallback(emulator),
emulator_window_(emulator_window) {}
void PostGameConfigLoad() override;
private:
EmulatorWindow& emulator_window_;
};
class DisplayConfigDialog final : public ui::ImGuiDialog {
public:
DisplayConfigDialog(ui::ImGuiDrawer* imgui_drawer,
EmulatorWindow& emulator_window)
: ui::ImGuiDialog(imgui_drawer), emulator_window_(emulator_window) {}
protected:
void OnDraw(ImGuiIO& io) override;
private:
EmulatorWindow& emulator_window_;
};
explicit EmulatorWindow(Emulator* emulator,
ui::WindowedAppContext& app_context);
bool Initialize();
// For comparisons, use GetSwapPostEffectForCvarValue instead as the default
// fallback may be used for multiple values.
static const char* GetCvarValueForSwapPostEffect(
gpu::CommandProcessor::SwapPostEffect effect);
static gpu::CommandProcessor::SwapPostEffect GetSwapPostEffectForCvarValue(
const std::string& cvar_value);
// For comparisons, use GetGuestOutputPaintEffectForCvarValue instead as the
// default fallback may be used for multiple values.
static const char* GetCvarValueForGuestOutputPaintEffect(
ui::Presenter::GuestOutputPaintConfig::Effect effect);
static ui::Presenter::GuestOutputPaintConfig::Effect
GetGuestOutputPaintEffectForCvarValue(const std::string& cvar_value);
static ui::Presenter::GuestOutputPaintConfig
GetGuestOutputPaintConfigForCvars();
void ApplyDisplayConfigForCvars();
void OnKeyDown(ui::KeyEvent& e);
void FileDrop(const std::filesystem::path& filename);
void FileOpen();
void FileClose();
void ShowContentDirectory();
void CheckHideCursor();
void CpuTimeScalarReset();
void CpuTimeScalarSetHalf();
void CpuTimeScalarSetDouble();
@@ -56,15 +138,27 @@ class EmulatorWindow {
void CpuBreakIntoHostDebugger();
void GpuTraceFrame();
void GpuClearCaches();
void ShowHelpWebsite();
void ShowCommitID();
void ToggleDisplayConfigDialog();
void ShowCompatibility();
void ShowFAQ();
void ShowBuildCommit();
Emulator* emulator_;
ui::WindowedAppContext& app_context_;
EmulatorWindowListener window_listener_;
std::unique_ptr<ui::Window> window_;
std::unique_ptr<ui::ImGuiDrawer> imgui_drawer_;
std::unique_ptr<DisplayConfigGameConfigLoadCallback>
display_config_game_config_load_callback_;
// Creation may fail, in this case immediate drawer UI must not be drawn.
std::unique_ptr<ui::ImmediateDrawer> immediate_drawer_;
bool emulator_initialized_ = false;
std::string base_title_;
uint64_t cursor_hide_time_ = 0;
bool initializing_shader_storage_ = false;
std::unique_ptr<DisplayConfigDialog> display_config_dialog_;
};
} // namespace app

View File

@@ -2,7 +2,7 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
@@ -28,6 +28,7 @@
#include "xenia/emulator.h"
#include "xenia/ui/file_picker.h"
#include "xenia/ui/window.h"
#include "xenia/ui/window_listener.h"
#include "xenia/ui/windowed_app.h"
#include "xenia/ui/windowed_app_context.h"
#include "xenia/vfs/devices/host_path_device.h"
@@ -62,8 +63,6 @@ DEFINE_string(gpu, "any", "Graphics system. Use: [any, d3d12, vulkan, null]",
DEFINE_string(hid, "any", "Input system. Use: [any, nop, sdl, winkey, xinput]",
"HID");
DEFINE_bool(fullscreen, false, "Toggles fullscreen", "GPU");
DEFINE_path(
storage_root, "",
"Root path for persistent internal data storage (config, etc.), or empty "
@@ -192,6 +191,17 @@ class EmulatorApp final : public xe::ui::WindowedApp {
}
};
class DebugWindowClosedListener final : public xe::ui::WindowListener {
public:
explicit DebugWindowClosedListener(EmulatorApp& emulator_app)
: emulator_app_(emulator_app) {}
void OnClosing(xe::ui::UIEvent& e) override;
private:
EmulatorApp& emulator_app_;
};
explicit EmulatorApp(xe::ui::WindowedAppContext& app_context);
static std::unique_ptr<apu::AudioSystem> CreateAudioSystem(
@@ -203,6 +213,8 @@ class EmulatorApp final : public xe::ui::WindowedApp {
void EmulatorThread();
void ShutdownEmulatorThreadFromUIThread();
DebugWindowClosedListener debug_window_closed_listener_;
std::unique_ptr<Emulator> emulator_;
std::unique_ptr<EmulatorWindow> emulator_window_;
@@ -215,8 +227,15 @@ class EmulatorApp final : public xe::ui::WindowedApp {
std::thread emulator_thread_;
};
void EmulatorApp::DebugWindowClosedListener::OnClosing(xe::ui::UIEvent& e) {
EmulatorApp* emulator_app = &emulator_app_;
emulator_app->emulator_->processor()->set_debug_listener(nullptr);
emulator_app->debug_window_.reset();
}
EmulatorApp::EmulatorApp(xe::ui::WindowedAppContext& app_context)
: xe::ui::WindowedApp(app_context, "xenia", "[Path to .iso/.xex]") {
: xe::ui::WindowedApp(app_context, "xenia", "[Path to .iso/.xex]"),
debug_window_closed_listener_(*this) {
AddPositionalOption("target");
}
@@ -253,9 +272,10 @@ std::vector<std::unique_ptr<hid::InputDriver>> EmulatorApp::CreateInputDrivers(
ui::Window* window) {
std::vector<std::unique_ptr<hid::InputDriver>> drivers;
if (cvars::hid.compare("nop") == 0) {
drivers.emplace_back(xe::hid::nop::Create(window));
drivers.emplace_back(
xe::hid::nop::Create(window, EmulatorWindow::kZOrderHidInput));
} else {
Factory<hid::InputDriver, ui::Window*> factory;
Factory<hid::InputDriver, ui::Window*, size_t> factory;
#if XE_PLATFORM_WIN32
factory.Add("xinput", xe::hid::xinput::Create);
#endif // XE_PLATFORM_WIN32
@@ -264,14 +284,16 @@ std::vector<std::unique_ptr<hid::InputDriver>> EmulatorApp::CreateInputDrivers(
// WinKey input driver should always be the last input driver added!
factory.Add("winkey", xe::hid::winkey::Create);
#endif // XE_PLATFORM_WIN32
for (auto& driver : factory.CreateAll(cvars::hid, window)) {
for (auto& driver : factory.CreateAll(cvars::hid, window,
EmulatorWindow::kZOrderHidInput)) {
if (XSUCCEEDED(driver->Setup())) {
drivers.emplace_back(std::move(driver));
}
}
if (drivers.empty()) {
// Fallback to nop if none created.
drivers.emplace_back(xe::hid::nop::Create(window));
drivers.emplace_back(
xe::hid::nop::Create(window, EmulatorWindow::kZOrderHidInput));
}
}
return drivers;
@@ -366,6 +388,9 @@ void EmulatorApp::OnDestroy() {
// The profiler needs to shut down before the graphics context.
Profiler::Shutdown();
// Write all cvar overrides to the config.
config::SaveConfig();
// TODO(DrChat): Remove this code and do a proper exit.
XELOGI("Cheap-skate exit!");
std::quick_exit(EXIT_SUCCESS);
@@ -379,15 +404,18 @@ void EmulatorApp::EmulatorThread() {
// Setup and initialize all subsystems. If we can't do something
// (unsupported system, memory issues, etc) this will fail early.
X_STATUS result =
emulator_->Setup(emulator_window_->window(), CreateAudioSystem,
CreateGraphicsSystem, CreateInputDrivers);
X_STATUS result = emulator_->Setup(
emulator_window_->window(), emulator_window_->imgui_drawer(),
CreateAudioSystem, CreateGraphicsSystem, CreateInputDrivers);
if (XFAILED(result)) {
XELOGE("Failed to setup emulator: {:08X}", result);
app_context().RequestDeferredQuit();
return;
}
app_context().CallInUIThread(
[this]() { emulator_window_->SetupGraphicsSystemPresenterPainting(); });
if (cvars::mount_scratch) {
auto scratch_device = std::make_unique<xe::vfs::HostPathDevice>(
"\\SCRATCH", "scratch", false);
@@ -456,12 +484,8 @@ void EmulatorApp::EmulatorThread() {
app_context().CallInUIThreadSynchronous([this]() {
debug_window_ = xe::debug::ui::DebugWindow::Create(emulator_.get(),
app_context());
debug_window_->window()->on_closed.AddListener(
[this](xe::ui::UIEvent* e) {
emulator_->processor()->set_debug_listener(nullptr);
app_context().CallInUIThread(
[this]() { debug_window_.reset(); });
});
debug_window_->window()->AddListener(
&debug_window_closed_listener_);
});
// If failed to enqueue the UI thread call, this will just be null.
return debug_window_.get();
@@ -490,9 +514,9 @@ void EmulatorApp::EmulatorThread() {
}
});
// Enable the main menu now that the emulator is properly loaded
// Enable emulator input now that the emulator is properly loaded.
app_context().CallInUIThread(
[this]() { emulator_window_->window()->EnableMainMenu(); });
[this]() { emulator_window_->OnEmulatorInitialized(); });
// Grab path from the flag or unnamed argument.
std::filesystem::path path;
@@ -500,12 +524,6 @@ void EmulatorApp::EmulatorThread() {
path = cvars::target;
}
// Toggles fullscreen
if (cvars::fullscreen) {
app_context().CallInUIThread(
[this]() { emulator_window_->ToggleFullscreen(); });
}
if (!path.empty()) {
// Normalize the path and make absolute.
auto abs_path = std::filesystem::absolute(path);