Merge branch 'master' into vulkan
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user