2292 lines
77 KiB
C++
2292 lines
77 KiB
C++
/**
|
||
******************************************************************************
|
||
* Xenia : Xbox 360 Emulator Research Project *
|
||
******************************************************************************
|
||
* Copyright 2022 Ben Vanik. All rights reserved. *
|
||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||
******************************************************************************
|
||
*/
|
||
|
||
#include "xenia/app/emulator_window.h"
|
||
|
||
#include "third_party/imgui/imgui.h"
|
||
#include "third_party/stb/stb_image_write.h"
|
||
#include "third_party/tomlplusplus/toml.hpp"
|
||
#include "xenia/base/assert.h"
|
||
#include "xenia/base/clock.h"
|
||
#include "xenia/base/cvar.h"
|
||
#include "xenia/base/debugging.h"
|
||
#include "xenia/base/logging.h"
|
||
#include "xenia/base/platform.h"
|
||
#include "xenia/base/profiling.h"
|
||
#include "xenia/base/system.h"
|
||
#include "xenia/base/threading.h"
|
||
#include "xenia/cpu/processor.h"
|
||
#include "xenia/emulator.h"
|
||
#include "xenia/gpu/command_processor.h"
|
||
#include "xenia/gpu/graphics_system.h"
|
||
#include "xenia/hid/input_system.h"
|
||
#include "xenia/kernel/xam/profile_manager.h"
|
||
#include "xenia/kernel/xam/xam_module.h"
|
||
#include "xenia/kernel/xam/xam_state.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/imgui_host_notification.h"
|
||
#include "xenia/ui/immediate_drawer.h"
|
||
#include "xenia/ui/presenter.h"
|
||
#include "xenia/ui/ui_event.h"
|
||
#include "xenia/ui/virtual_key.h"
|
||
|
||
// Autogenerated by `xb premake`.
|
||
#include "build/version.h"
|
||
|
||
DECLARE_bool(debug);
|
||
|
||
DECLARE_string(hid);
|
||
|
||
DECLARE_bool(guide_button);
|
||
|
||
DECLARE_bool(clear_memory_page_state);
|
||
|
||
DECLARE_bool(readback_resolve);
|
||
|
||
DECLARE_bool(readback_memexport);
|
||
|
||
DEFINE_bool(fullscreen, false, "Whether to launch the emulator in fullscreen.",
|
||
"Display");
|
||
|
||
DEFINE_bool(controller_hotkeys, false, "Hotkeys for Xbox and PS controllers.",
|
||
"General");
|
||
|
||
DEFINE_string(
|
||
postprocess_antialiasing, "",
|
||
"Post-processing anti-aliasing effect to apply to the image output of the "
|
||
"game.\n"
|
||
"Using post-process anti-aliasing is heavily recommended when AMD "
|
||
"FidelityFX Contrast Adaptive Sharpening or Super Resolution 1.0 is "
|
||
"active.\n"
|
||
"Use: [none, fxaa, fxaa_extreme]\n"
|
||
" none (or any value not listed here):\n"
|
||
" Don't alter the original image.\n"
|
||
" fxaa:\n"
|
||
" NVIDIA Fast Approximate Anti-Aliasing 3.11, normal quality preset (12)."
|
||
"\n"
|
||
" fxaa_extreme:\n"
|
||
" NVIDIA Fast Approximate Anti-Aliasing 3.11, extreme quality preset "
|
||
"(39).",
|
||
"Display");
|
||
DEFINE_string(
|
||
postprocess_scaling_and_sharpening, "",
|
||
"Post-processing effect to use for resampling and/or sharpening of the "
|
||
"final display output.\n"
|
||
"Use: [bilinear, cas, fsr]\n"
|
||
" bilinear (or any value not listed here):\n"
|
||
" Original image at 1:1, simple bilinear stretching for resampling.\n"
|
||
" cas:\n"
|
||
" Use AMD FidelityFX Contrast Adaptive Sharpening (CAS) for sharpening "
|
||
"at scaling factors of up to 2x2, with additional bilinear stretching for "
|
||
"larger factors.\n"
|
||
" fsr:\n"
|
||
" Use AMD FidelityFX Super Resolution 1.0 (FSR) for highest-quality "
|
||
"upscaling, or AMD FidelityFX Contrast Adaptive Sharpening for sharpening "
|
||
"while not scaling or downsampling.\n"
|
||
" For scaling by factors of more than 2x2, multiple FSR passes are done.",
|
||
"Display");
|
||
DEFINE_double(
|
||
postprocess_ffx_cas_additional_sharpness,
|
||
xe::ui::Presenter::GuestOutputPaintConfig::kCasAdditionalSharpnessDefault,
|
||
"Additional sharpness for AMD FidelityFX Contrast Adaptive Sharpening "
|
||
"(CAS), from 0 to 1.\n"
|
||
"Higher is sharper.",
|
||
"Display");
|
||
DEFINE_uint32(
|
||
postprocess_ffx_fsr_max_upsampling_passes,
|
||
xe::ui::Presenter::GuestOutputPaintConfig::kFsrMaxUpscalingPassesMax,
|
||
"Maximum number of upsampling passes performed in AMD FidelityFX Super "
|
||
"Resolution 1.0 (FSR) before falling back to bilinear stretching after the "
|
||
"final pass.\n"
|
||
"Each pass upscales only to up to 2x2 the previous size. If the game "
|
||
"outputs a 1280x720 image, 1 pass will upscale it to up to 2560x1440 "
|
||
"(below 4K), after 2 passes it will be upscaled to a maximum of 5120x2880 "
|
||
"(including 3840x2160 for 4K), and so on.\n"
|
||
"This variable has no effect if the display resolution isn't very high, "
|
||
"but may be reduced on resolutions like 4K or 8K in case the performance "
|
||
"impact of multiple FSR upsampling passes is too high, or if softer edges "
|
||
"are desired.\n"
|
||
"The default value is the maximum internally supported by Xenia.",
|
||
"Display");
|
||
DEFINE_double(
|
||
postprocess_ffx_fsr_sharpness_reduction,
|
||
xe::ui::Presenter::GuestOutputPaintConfig::kFsrSharpnessReductionDefault,
|
||
"Sharpness reduction for AMD FidelityFX Super Resolution 1.0 (FSR), in "
|
||
"stops.\n"
|
||
"Lower is sharper.",
|
||
"Display");
|
||
// Dithering to 8bpc is enabled by default since the effect is minor, only
|
||
// effects what can't be shown normally by host displays, and nothing is changed
|
||
// by it for 8bpc source without resampling.
|
||
DEFINE_bool(
|
||
postprocess_dither, true,
|
||
"Dither the final image output from the internal precision to 8 bits per "
|
||
"channel so gradients are smoother.\n"
|
||
"On a 10bpc display, the lower 2 bits will still be kept, but noise will "
|
||
"be added to them - disabling may be recommended for 10bpc, but it "
|
||
"depends on the 10bpc displaying capabilities of the actual display used.",
|
||
"Display");
|
||
|
||
DEFINE_int32(recent_titles_entry_amount, 10,
|
||
"Allows user to define how many titles is saved in list of "
|
||
"recently played titles.",
|
||
"General");
|
||
namespace xe {
|
||
namespace app {
|
||
|
||
using xe::ui::FileDropEvent;
|
||
using xe::ui::KeyEvent;
|
||
using xe::ui::MenuItem;
|
||
using xe::ui::UIEvent;
|
||
|
||
using namespace xe::hid;
|
||
using namespace xe::gpu;
|
||
|
||
constexpr std::string_view kRecentlyPlayedTitlesFilename = "recent.toml";
|
||
constexpr std::string_view kBaseTitle = "Xenia-canary";
|
||
|
||
EmulatorWindow::EmulatorWindow(Emulator* emulator,
|
||
ui::WindowedAppContext& app_context,
|
||
uint32_t width, uint32_t height)
|
||
: emulator_(emulator),
|
||
app_context_(app_context),
|
||
window_listener_(*this),
|
||
window_(ui::Window::Create(app_context, kBaseTitle, width, height)),
|
||
imgui_drawer_(
|
||
std::make_unique<ui::ImGuiDrawer>(window_.get(), kZOrderImGui)),
|
||
display_config_game_config_load_callback_(
|
||
new DisplayConfigGameConfigLoadCallback(*emulator, *this)) {
|
||
base_title_ = std::string(kBaseTitle) +
|
||
#ifdef DEBUG
|
||
#if _NO_DEBUG_HEAP == 1
|
||
" DEBUG"
|
||
#else
|
||
" CHECKED"
|
||
#endif
|
||
#endif
|
||
" ("
|
||
#ifdef XE_BUILD_IS_PR
|
||
"PR#" XE_BUILD_PR_NUMBER " - "
|
||
#endif
|
||
XE_BUILD_BRANCH "@" XE_BUILD_COMMIT_SHORT " on " XE_BUILD_DATE
|
||
")";
|
||
|
||
LoadRecentlyLaunchedTitles();
|
||
}
|
||
|
||
std::unique_ptr<EmulatorWindow> EmulatorWindow::Create(
|
||
Emulator* emulator, ui::WindowedAppContext& app_context, uint32_t width,
|
||
uint32_t height) {
|
||
assert_true(app_context.IsInUIThread());
|
||
std::unique_ptr<EmulatorWindow> emulator_window(
|
||
new EmulatorWindow(emulator, app_context, width, height));
|
||
if (!emulator_window->Initialize()) {
|
||
return nullptr;
|
||
}
|
||
return emulator_window;
|
||
}
|
||
|
||
EmulatorWindow::~EmulatorWindow() {
|
||
// Notify the ImGui drawer that the immediate drawer is being destroyed.
|
||
ShutdownGraphicsSystemPresenterPainting();
|
||
}
|
||
|
||
ui::Presenter* EmulatorWindow::GetGraphicsSystemPresenter() const {
|
||
gpu::GraphicsSystem* graphics_system = emulator_->graphics_system();
|
||
return graphics_system ? graphics_system->presenter() : nullptr;
|
||
}
|
||
|
||
void EmulatorWindow::SetupGraphicsSystemPresenterPainting() {
|
||
ShutdownGraphicsSystemPresenterPainting();
|
||
|
||
if (!window_) {
|
||
return;
|
||
}
|
||
|
||
ui::Presenter* presenter = GetGraphicsSystemPresenter();
|
||
if (!presenter) {
|
||
return;
|
||
}
|
||
|
||
ApplyDisplayConfigForCvars();
|
||
|
||
window_->SetPresenter(presenter);
|
||
|
||
immediate_drawer_ =
|
||
emulator_->graphics_system()->provider()->CreateImmediateDrawer();
|
||
if (immediate_drawer_) {
|
||
immediate_drawer_->SetPresenter(presenter);
|
||
imgui_drawer_->SetPresenterAndImmediateDrawer(presenter,
|
||
immediate_drawer_.get());
|
||
Profiler::SetUserIO(kZOrderProfiler, window_.get(), presenter,
|
||
immediate_drawer_.get());
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::ShutdownGraphicsSystemPresenterPainting() {
|
||
Profiler::SetUserIO(kZOrderProfiler, window_.get(), nullptr, nullptr);
|
||
imgui_drawer_->SetPresenterAndImmediateDrawer(nullptr, nullptr);
|
||
immediate_drawer_.reset();
|
||
if (window_) {
|
||
window_->SetPresenter(nullptr);
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::OnEmulatorInitialized() {
|
||
if (!emulator_->kernel_state()
|
||
->xam_state()
|
||
->profile_manager()
|
||
->GetAccountCount()) {
|
||
new NoProfileDialog(imgui_drawer_.get(), this);
|
||
disable_hotkeys_ = true;
|
||
}
|
||
|
||
emulator_initialized_ = true;
|
||
window_->SetMainMenuEnabled(true);
|
||
// When the user can see that the emulator isn't initializing anymore (the
|
||
// menu isn't disabled), enter fullscreen if requested.
|
||
if (cvars::fullscreen) {
|
||
SetFullscreen(true);
|
||
}
|
||
|
||
if (IsUseNexusForGameBarEnabled()) {
|
||
XELOGE(
|
||
"Xbox Gamebar Enabled, using BACK button instead of GUIDE for "
|
||
"controller hotkeys!!!");
|
||
}
|
||
|
||
// Create a thread to listen for controller hotkeys.
|
||
if (cvars::controller_hotkeys) {
|
||
Gamepad_HotKeys_Listener =
|
||
threading::Thread::Create({}, [&] { GamepadHotKeys(); });
|
||
Gamepad_HotKeys_Listener->set_name("Gamepad HotKeys Listener");
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::EmulatorWindowListener::OnClosing(ui::UIEvent& e) {
|
||
emulator_window_.app_context_.QuitFromUIThread();
|
||
}
|
||
|
||
void EmulatorWindow::EmulatorWindowListener::OnFileDrop(ui::FileDropEvent& e) {
|
||
emulator_window_.FileDrop(e.filename());
|
||
}
|
||
|
||
void EmulatorWindow::EmulatorWindowListener::OnKeyDown(ui::KeyEvent& e) {
|
||
emulator_window_.OnKeyDown(e);
|
||
}
|
||
|
||
void EmulatorWindow::EmulatorWindowListener::OnMouseDown(ui::MouseEvent& e) {
|
||
emulator_window_.OnMouseDown(e);
|
||
}
|
||
|
||
void EmulatorWindow::EmulatorWindowListener::OnMouseUp(ui::MouseEvent& e) {
|
||
emulator_window_.OnMouseUp(e);
|
||
}
|
||
|
||
void EmulatorWindow::DisplayConfigGameConfigLoadCallback::PostGameConfigLoad() {
|
||
emulator_window_.ApplyDisplayConfigForCvars();
|
||
}
|
||
|
||
void EmulatorWindow::DisplayConfigDialog::OnDraw(ImGuiIO& io) {
|
||
gpu::GraphicsSystem* graphics_system =
|
||
emulator_window_.emulator_->graphics_system();
|
||
if (!graphics_system) {
|
||
return;
|
||
}
|
||
|
||
// In the top-left corner so it's close to the menu bar from where it was
|
||
// opened.
|
||
// Origin Y coordinate 20 was taken from the Dear ImGui demo.
|
||
ImGui::SetNextWindowPos(ImVec2(20, 20), ImGuiCond_FirstUseEver);
|
||
ImGui::SetNextWindowSize(ImVec2(20, 20), ImGuiCond_FirstUseEver);
|
||
// Alpha from Dear ImGui tooltips (0.35 from the overlay provides too low
|
||
// visibility). Translucent so some effect of the changes can still be seen
|
||
// through it.
|
||
ImGui::SetNextWindowBgAlpha(0.6f);
|
||
bool dialog_open = true;
|
||
if (!ImGui::Begin("Post-processing", &dialog_open,
|
||
ImGuiWindowFlags_NoCollapse |
|
||
ImGuiWindowFlags_AlwaysAutoResize |
|
||
ImGuiWindowFlags_HorizontalScrollbar)) {
|
||
ImGui::End();
|
||
return;
|
||
}
|
||
|
||
// Even if the close button has been pressed, still paint everything not to
|
||
// have one frame with an empty window.
|
||
|
||
// Prevent user confusion which has been reported multiple times.
|
||
ImGui::TextUnformatted("All effects can be used on GPUs of any brand.");
|
||
ImGui::Spacing();
|
||
|
||
gpu::CommandProcessor* command_processor =
|
||
graphics_system->command_processor();
|
||
if (command_processor) {
|
||
if (ImGui::TreeNodeEx(
|
||
"Anti-aliasing",
|
||
ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen)) {
|
||
gpu::CommandProcessor::SwapPostEffect current_swap_post_effect =
|
||
command_processor->GetDesiredSwapPostEffect();
|
||
int new_swap_post_effect_index = int(current_swap_post_effect);
|
||
ImGui::RadioButton("None", &new_swap_post_effect_index,
|
||
int(gpu::CommandProcessor::SwapPostEffect::kNone));
|
||
ImGui::RadioButton(
|
||
"NVIDIA Fast Approximate Anti-Aliasing (FXAA) [Normal Quality]",
|
||
&new_swap_post_effect_index,
|
||
int(gpu::CommandProcessor::SwapPostEffect::kFxaa));
|
||
ImGui::RadioButton(
|
||
"NVIDIA Fast Approximate Anti-Aliasing (FXAA) [Extreme Quality]",
|
||
&new_swap_post_effect_index,
|
||
int(gpu::CommandProcessor::SwapPostEffect::kFxaaExtreme));
|
||
gpu::CommandProcessor::SwapPostEffect new_swap_post_effect =
|
||
gpu::CommandProcessor::SwapPostEffect(new_swap_post_effect_index);
|
||
if (current_swap_post_effect != new_swap_post_effect) {
|
||
command_processor->SetDesiredSwapPostEffect(new_swap_post_effect);
|
||
}
|
||
|
||
// Override the values in the cvars to save them to the config at exit if
|
||
// the user has set them to anything new.
|
||
if (GetSwapPostEffectForCvarValue(cvars::postprocess_antialiasing) !=
|
||
new_swap_post_effect) {
|
||
OVERRIDE_string(postprocess_antialiasing,
|
||
GetCvarValueForSwapPostEffect(new_swap_post_effect));
|
||
}
|
||
|
||
ImGui::TreePop();
|
||
}
|
||
}
|
||
|
||
ui::Presenter* presenter = graphics_system->presenter();
|
||
if (presenter) {
|
||
const ui::Presenter::GuestOutputPaintConfig& current_presenter_config =
|
||
presenter->GetGuestOutputPaintConfigFromUIThread();
|
||
ui::Presenter::GuestOutputPaintConfig new_presenter_config =
|
||
current_presenter_config;
|
||
|
||
if (ImGui::TreeNodeEx(
|
||
"Resampling and sharpening",
|
||
ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen)) {
|
||
// Filtering effect.
|
||
int new_effect_index = int(new_presenter_config.GetEffect());
|
||
ImGui::RadioButton(
|
||
"None / Bilinear", &new_effect_index,
|
||
int(ui::Presenter::GuestOutputPaintConfig::Effect::kBilinear));
|
||
ImGui::RadioButton(
|
||
"AMD FidelityFX Contrast Adaptive Sharpening (CAS)",
|
||
&new_effect_index,
|
||
int(ui::Presenter::GuestOutputPaintConfig::Effect::kCas));
|
||
ImGui::RadioButton(
|
||
"AMD FidelityFX Super Resolution 1.0 (FSR)", &new_effect_index,
|
||
int(ui::Presenter::GuestOutputPaintConfig::Effect::kFsr));
|
||
new_presenter_config.SetEffect(
|
||
ui::Presenter::GuestOutputPaintConfig::Effect(new_effect_index));
|
||
|
||
// effect_description must be one complete, but short enough, sentence per
|
||
// line, as TextWrapped doesn't work correctly in auto-resizing windows
|
||
// (in the initial frames, the window becomes extremely tall, and widgets
|
||
// added after the wrapped text have no effect on the width of the text).
|
||
const char* effect_description = nullptr;
|
||
switch (new_presenter_config.GetEffect()) {
|
||
case ui::Presenter::GuestOutputPaintConfig::Effect::kBilinear:
|
||
effect_description =
|
||
"Simple bilinear filtering is done if resampling is needed.\n"
|
||
"Otherwise, only anti-aliasing is done if enabled, or displaying "
|
||
"as is.";
|
||
break;
|
||
case ui::Presenter::GuestOutputPaintConfig::Effect::kCas:
|
||
effect_description =
|
||
"Sharpening and resampling to up to 2x2 to improve the fidelity "
|
||
"of details.\n"
|
||
"For scaling by more than 2x2, bilinear stretching is done "
|
||
"afterwards.";
|
||
break;
|
||
case ui::Presenter::GuestOutputPaintConfig::Effect::kFsr:
|
||
effect_description =
|
||
"High-quality edge-preserving upscaling to arbitrary target "
|
||
"resolutions.\n"
|
||
"For scaling by more than 2x2, multiple upsampling passes are "
|
||
"done.\n"
|
||
"If not upscaling, Contrast Adaptive Sharpening (CAS) is used "
|
||
"instead.";
|
||
break;
|
||
}
|
||
if (effect_description) {
|
||
ImGui::TextUnformatted(effect_description);
|
||
}
|
||
|
||
if (new_presenter_config.GetEffect() ==
|
||
ui::Presenter::GuestOutputPaintConfig::Effect::kCas ||
|
||
new_presenter_config.GetEffect() ==
|
||
ui::Presenter::GuestOutputPaintConfig::Effect::kFsr) {
|
||
if (effect_description) {
|
||
ImGui::Spacing();
|
||
}
|
||
|
||
ImGui::TextUnformatted(
|
||
"FXAA is highly recommended when using CAS or FSR.");
|
||
|
||
ImGui::Spacing();
|
||
|
||
// 2 decimal places is more or less enough precision for the sharpness
|
||
// given the minor visual effect of small changes, the width of the
|
||
// slider, and readability convenience (2 decimal places is like an
|
||
// integer percentage). However, because Dear ImGui parses the string
|
||
// representation of the number and snaps the value to it internally,
|
||
// 2 decimal places actually offer less precision than the slider itself
|
||
// does. This is especially prominent in the low range of the non-linear
|
||
// FSR sharpness reduction slider. 3 decimal places are optimal in this
|
||
// case.
|
||
|
||
if (new_presenter_config.GetEffect() ==
|
||
ui::Presenter::GuestOutputPaintConfig::Effect::kFsr) {
|
||
float fsr_sharpness_reduction =
|
||
new_presenter_config.GetFsrSharpnessReduction();
|
||
ImGui::TextUnformatted(
|
||
"FSR sharpness reduction when upscaling (lower is sharper):");
|
||
const auto label = fmt::format(
|
||
"{} %%", static_cast<int>(fsr_sharpness_reduction * 100));
|
||
// Power 2.0 scaling as the reduction is in stops, used in exp2.
|
||
fsr_sharpness_reduction = sqrt(2.f * fsr_sharpness_reduction);
|
||
ImGui::SliderFloat(
|
||
"##FSRSharpnessReduction", &fsr_sharpness_reduction,
|
||
ui::Presenter::GuestOutputPaintConfig::kFsrSharpnessReductionMin,
|
||
ui::Presenter::GuestOutputPaintConfig::kFsrSharpnessReductionMax,
|
||
label.c_str(), ImGuiSliderFlags_NoInput);
|
||
fsr_sharpness_reduction =
|
||
.5f * fsr_sharpness_reduction * fsr_sharpness_reduction;
|
||
ImGui::SameLine();
|
||
if (ImGui::Button("Reset##ResetFSRSharpnessReduction")) {
|
||
fsr_sharpness_reduction = ui::Presenter::GuestOutputPaintConfig ::
|
||
kFsrSharpnessReductionDefault;
|
||
}
|
||
new_presenter_config.SetFsrSharpnessReduction(
|
||
fsr_sharpness_reduction);
|
||
}
|
||
|
||
float cas_additional_sharpness =
|
||
new_presenter_config.GetCasAdditionalSharpness();
|
||
ImGui::TextUnformatted(
|
||
new_presenter_config.GetEffect() ==
|
||
ui::Presenter::GuestOutputPaintConfig::Effect::kFsr
|
||
? "CAS additional sharpness when not upscaling (higher is "
|
||
"sharper):"
|
||
: "CAS additional sharpness (higher is sharper):");
|
||
const auto label = fmt::format(
|
||
"{} %%", static_cast<int>(cas_additional_sharpness * 100));
|
||
ImGui::SliderFloat(
|
||
"##CASAdditionalSharpness", &cas_additional_sharpness,
|
||
ui::Presenter::GuestOutputPaintConfig::kCasAdditionalSharpnessMin,
|
||
ui::Presenter::GuestOutputPaintConfig::kCasAdditionalSharpnessMax,
|
||
label.c_str(), ImGuiSliderFlags_NoInput);
|
||
ImGui::SameLine();
|
||
if (ImGui::Button("Reset##ResetCASAdditionalSharpness")) {
|
||
cas_additional_sharpness = ui::Presenter::GuestOutputPaintConfig ::
|
||
kCasAdditionalSharpnessDefault;
|
||
}
|
||
new_presenter_config.SetCasAdditionalSharpness(
|
||
cas_additional_sharpness);
|
||
|
||
// There's no need to expose the setting for the maximum number of FSR
|
||
// EASU passes as it's largely meaningless if the user doesn't have a
|
||
// very high-resolution monitor compared to the original image size as
|
||
// most of the values of the slider will have no effect, and that's just
|
||
// very fine-grained performance control for a fixed-overhead pass only
|
||
// for huge screen resolutions.
|
||
}
|
||
|
||
ImGui::TreePop();
|
||
}
|
||
|
||
if (ImGui::TreeNodeEx("Dithering", ImGuiTreeNodeFlags_Framed |
|
||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||
bool dither = current_presenter_config.GetDither();
|
||
ImGui::Checkbox(
|
||
"Dither the final output to 8bpc to make gradients smoother",
|
||
&dither);
|
||
new_presenter_config.SetDither(dither);
|
||
|
||
ImGui::TreePop();
|
||
}
|
||
|
||
presenter->SetGuestOutputPaintConfigFromUIThread(new_presenter_config);
|
||
|
||
// Override the values in the cvars to save them to the config at exit if
|
||
// the user has set them to anything new.
|
||
ui::Presenter::GuestOutputPaintConfig cvars_presenter_config =
|
||
GetGuestOutputPaintConfigForCvars();
|
||
if (cvars_presenter_config.GetEffect() !=
|
||
new_presenter_config.GetEffect()) {
|
||
OVERRIDE_string(postprocess_scaling_and_sharpening,
|
||
GetCvarValueForGuestOutputPaintEffect(
|
||
new_presenter_config.GetEffect()));
|
||
}
|
||
if (cvars_presenter_config.GetCasAdditionalSharpness() !=
|
||
new_presenter_config.GetCasAdditionalSharpness()) {
|
||
OVERRIDE_double(postprocess_ffx_cas_additional_sharpness,
|
||
new_presenter_config.GetCasAdditionalSharpness());
|
||
}
|
||
if (cvars_presenter_config.GetFsrSharpnessReduction() !=
|
||
new_presenter_config.GetFsrSharpnessReduction()) {
|
||
OVERRIDE_double(postprocess_ffx_fsr_sharpness_reduction,
|
||
new_presenter_config.GetFsrSharpnessReduction());
|
||
}
|
||
if (cvars_presenter_config.GetDither() !=
|
||
new_presenter_config.GetDither()) {
|
||
OVERRIDE_bool(postprocess_dither, new_presenter_config.GetDither());
|
||
}
|
||
}
|
||
|
||
ImGui::End();
|
||
|
||
if (!dialog_open) {
|
||
emulator_window_.ToggleDisplayConfigDialog();
|
||
// `this` might have been destroyed by ToggleDisplayConfigDialog.
|
||
return;
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::ContentInstallDialog::OnDraw(ImGuiIO& io) {
|
||
ImGui::SetNextWindowPos(ImVec2(20, 20), ImGuiCond_FirstUseEver);
|
||
ImGui::SetNextWindowSize(ImVec2(20, 20), ImGuiCond_FirstUseEver);
|
||
|
||
bool dialog_open = true;
|
||
if (!ImGui::Begin(
|
||
fmt::format("Installation Progress###{}", window_id_).c_str(),
|
||
&dialog_open,
|
||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize |
|
||
ImGuiWindowFlags_HorizontalScrollbar)) {
|
||
Close();
|
||
ImGui::End();
|
||
return;
|
||
}
|
||
|
||
bool is_everything_installed = true;
|
||
for (const auto& entry : *installation_entries_) {
|
||
ImGui::BeginTable(fmt::format("table_{}", entry.name_).c_str(), 2);
|
||
ImGui::TableNextRow(0);
|
||
ImGui::TableSetColumnIndex(0);
|
||
if (entry.icon_) {
|
||
ImGui::Image(reinterpret_cast<ImTextureID>(entry.icon_.get()),
|
||
ui::default_image_icon_size);
|
||
} else {
|
||
ImGui::Dummy(ui::default_image_icon_size);
|
||
}
|
||
ImGui::TableNextColumn();
|
||
|
||
ImGui::Text("Name: %s", entry.name_.c_str());
|
||
ImGui::Text("Installation Path:");
|
||
ImGui::SameLine();
|
||
if (ImGui::TextLink(
|
||
xe::path_to_utf8(entry.data_installation_path_).c_str())) {
|
||
LaunchFileExplorer(emulator_window_.emulator_->content_root() /
|
||
entry.data_installation_path_);
|
||
}
|
||
|
||
if (entry.content_type_ != xe::XContentType::kInvalid) {
|
||
ImGui::Text("Content Type: %s",
|
||
XContentTypeMap.at(entry.content_type_).c_str());
|
||
}
|
||
|
||
std::string result = fmt::format(
|
||
"Status: {}", xe::Emulator::installStateStringName[static_cast<uint8_t>(
|
||
entry.installation_state_)]);
|
||
|
||
if (entry.installation_state_ == xe::Emulator::InstallState::failed) {
|
||
result += fmt::format(" - {} ({:08X})",
|
||
entry.installation_error_message_.c_str(),
|
||
entry.installation_result_);
|
||
}
|
||
|
||
ImGui::Text("%s", result.c_str());
|
||
ImGui::EndTable();
|
||
|
||
if (entry.content_size_ > 0) {
|
||
ImGui::ProgressBar(static_cast<float>(entry.currently_installed_size_) /
|
||
entry.content_size_);
|
||
|
||
if (entry.currently_installed_size_ != entry.content_size_ &&
|
||
entry.installation_result_ == X_ERROR_SUCCESS) {
|
||
is_everything_installed = false;
|
||
}
|
||
} else {
|
||
ImGui::ProgressBar(0.0f);
|
||
}
|
||
|
||
if (installation_entries_->size() > 1) {
|
||
ImGui::Separator();
|
||
}
|
||
}
|
||
ImGui::Spacing();
|
||
|
||
ImGui::BeginDisabled(!is_everything_installed);
|
||
if (ImGui::Button("Close")) {
|
||
ImGui::EndDisabled();
|
||
Close();
|
||
ImGui::End();
|
||
return;
|
||
}
|
||
ImGui::EndDisabled();
|
||
|
||
if (!dialog_open && is_everything_installed) {
|
||
Close();
|
||
ImGui::End();
|
||
return;
|
||
}
|
||
ImGui::End();
|
||
}
|
||
|
||
void EmulatorWindow::XMPConfigDialog::OnDraw(ImGuiIO& io) {
|
||
ImGui::SetNextWindowPos(ImVec2(20, 20), ImGuiCond_FirstUseEver);
|
||
ImGui::SetNextWindowSize(ImVec2(20, 20), ImGuiCond_FirstUseEver);
|
||
|
||
bool dialog_open = true;
|
||
if (!ImGui::Begin("Audio Player Menu", &dialog_open,
|
||
ImGuiWindowFlags_NoCollapse |
|
||
ImGuiWindowFlags_AlwaysAutoResize |
|
||
ImGuiWindowFlags_HorizontalScrollbar)) {
|
||
Close();
|
||
ImGui::End();
|
||
return;
|
||
}
|
||
|
||
auto audio_player = emulator_window_.emulator_->audio_media_player();
|
||
using xmp_state = kernel::xam::apps::XmpApp::State;
|
||
if (audio_player) {
|
||
ImGui::Text("Audio player status:");
|
||
ImGui::SameLine();
|
||
switch (audio_player->GetState()) {
|
||
case xmp_state::kIdle:
|
||
ImGui::Text("Idle");
|
||
break;
|
||
case xmp_state::kPaused:
|
||
ImGui::Text("Paused");
|
||
break;
|
||
case xmp_state::kPlaying:
|
||
ImGui::Text("Playing");
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
if (audio_player->IsPlaying()) {
|
||
if (ImGui::Button("Pause")) {
|
||
audio_player->Pause();
|
||
}
|
||
} else if (audio_player->IsPaused()) {
|
||
if (ImGui::Button("Resume")) {
|
||
audio_player->Continue();
|
||
}
|
||
}
|
||
|
||
volume_ =
|
||
emulator_window_.emulator_->audio_media_player()->GetVolume()->load();
|
||
|
||
if (ImGui::SliderFloat("Audio player volume", &volume_, 0.0f, 1.0f)) {
|
||
audio_player->SetVolume(volume_);
|
||
}
|
||
}
|
||
|
||
ImGui::End();
|
||
|
||
if (!dialog_open) {
|
||
emulator_window_.ToggleXMPConfigDialog();
|
||
return;
|
||
}
|
||
}
|
||
|
||
bool EmulatorWindow::Initialize() {
|
||
window_->AddListener(&window_listener_);
|
||
window_->AddInputListener(&window_listener_, kZOrderEmulatorWindowInput);
|
||
|
||
// Main menu.
|
||
// FIXME: This code is really messy.
|
||
auto main_menu = MenuItem::Create(MenuItem::Type::kNormal);
|
||
auto file_menu = MenuItem::Create(MenuItem::Type::kPopup, "&File");
|
||
auto recent_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Open Recent");
|
||
auto zar_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Zar Package");
|
||
FillRecentlyLaunchedTitlesMenu(recent_menu.get());
|
||
{
|
||
file_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "&Open...", "Ctrl+O",
|
||
std::bind(&EmulatorWindow::FileOpen, this)));
|
||
file_menu->AddChild(std::move(recent_menu));
|
||
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
file_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "Install Content...",
|
||
std::bind(&EmulatorWindow::InstallContent, this)));
|
||
zar_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "Create",
|
||
std::bind(&EmulatorWindow::CreateZarchive, this)));
|
||
zar_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "Extract",
|
||
std::bind(&EmulatorWindow::ExtractZarchive, this)));
|
||
file_menu->AddChild(std::move(zar_menu));
|
||
#ifdef DEBUG
|
||
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
file_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "Close",
|
||
std::bind(&EmulatorWindow::FileClose, this)));
|
||
#endif // #ifdef DEBUG
|
||
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
file_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "Show content directory...",
|
||
std::bind(&EmulatorWindow::ShowContentDirectory, this)));
|
||
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
file_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "E&xit", "Alt+F4",
|
||
[this]() { window_->RequestClose(); }));
|
||
}
|
||
main_menu->AddChild(std::move(file_menu));
|
||
|
||
// Profile Menu
|
||
auto profile_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Profile");
|
||
{
|
||
profile_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "&Show Profile Menu", "",
|
||
std::bind(&EmulatorWindow::ToggleProfilesConfigDialog, this)));
|
||
}
|
||
main_menu->AddChild(std::move(profile_menu));
|
||
|
||
// CPU menu.
|
||
auto cpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&CPU");
|
||
{
|
||
cpu_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "&Reset Time Scalar", "Numpad *",
|
||
std::bind(&EmulatorWindow::CpuTimeScalarReset, this)));
|
||
cpu_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "Time Scalar /= 2", "Numpad -",
|
||
std::bind(&EmulatorWindow::CpuTimeScalarSetHalf, this)));
|
||
cpu_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "Time Scalar *= 2", "Numpad +",
|
||
std::bind(&EmulatorWindow::CpuTimeScalarSetDouble, this)));
|
||
}
|
||
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
{
|
||
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kString,
|
||
"Toggle Profiler &Display", "F3",
|
||
[]() { Profiler::ToggleDisplay(); }));
|
||
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kString,
|
||
"&Pause/Resume Profiler", "`",
|
||
[]() { Profiler::TogglePause(); }));
|
||
}
|
||
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
{
|
||
cpu_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "&Break and Show Guest Debugger",
|
||
"Pause/Break", std::bind(&EmulatorWindow::CpuBreakIntoDebugger, this)));
|
||
cpu_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "&Break into Host Debugger",
|
||
"Ctrl+Pause/Break",
|
||
std::bind(&EmulatorWindow::CpuBreakIntoHostDebugger, this)));
|
||
}
|
||
main_menu->AddChild(std::move(cpu_menu));
|
||
|
||
// GPU menu.
|
||
auto gpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&GPU");
|
||
{
|
||
gpu_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "&Trace Frame", "F4",
|
||
std::bind(&EmulatorWindow::GpuTraceFrame, this)));
|
||
}
|
||
gpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
{
|
||
gpu_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "&Clear Runtime Caches", "F5",
|
||
std::bind(&EmulatorWindow::GpuClearCaches, this)));
|
||
}
|
||
main_menu->AddChild(std::move(gpu_menu));
|
||
|
||
// Display menu.
|
||
auto display_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Display");
|
||
{
|
||
display_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "&Post-processing settings", "F6",
|
||
std::bind(&EmulatorWindow::ToggleDisplayConfigDialog, this)));
|
||
}
|
||
display_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
{
|
||
display_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "&Fullscreen", "F11",
|
||
std::bind(&EmulatorWindow::ToggleFullscreen, this)));
|
||
display_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "&Take Screenshot", "F12",
|
||
std::bind(&EmulatorWindow::TakeScreenshot, this)));
|
||
}
|
||
main_menu->AddChild(std::move(display_menu));
|
||
|
||
// HID menu.
|
||
auto hid_menu = MenuItem::Create(MenuItem::Type::kPopup, "&HID");
|
||
{
|
||
hid_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "&Toggle controller vibration", "",
|
||
std::bind(&EmulatorWindow::ToggleControllerVibration, this)));
|
||
hid_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "&Display controller hotkeys", "",
|
||
std::bind(&EmulatorWindow::DisplayHotKeysConfig, this)));
|
||
}
|
||
main_menu->AddChild(std::move(hid_menu));
|
||
|
||
// XMP menu
|
||
auto xmp_menu = MenuItem::Create(MenuItem::Type::kPopup, "&XMP");
|
||
{
|
||
xmp_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "&Show XMP Menu", "",
|
||
std::bind(&EmulatorWindow::ToggleXMPConfigDialog, this)));
|
||
}
|
||
main_menu->AddChild(std::move(xmp_menu));
|
||
|
||
// Help menu.
|
||
auto help_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Help");
|
||
{
|
||
help_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "FA&Q...", "F1",
|
||
std::bind(&EmulatorWindow::ShowFAQ, this)));
|
||
help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
help_menu->AddChild(
|
||
MenuItem::Create(MenuItem::Type::kString, "Game &compatibility...",
|
||
std::bind(&EmulatorWindow::ShowCompatibility, this)));
|
||
help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
help_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "Build commit on GitHub...", "F2",
|
||
std::bind(&EmulatorWindow::ShowBuildCommit, this)));
|
||
help_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "Recent changes on GitHub...", []() {
|
||
LaunchWebBrowser(
|
||
"https://github.com/xenia-canary/xenia-canary/"
|
||
"compare/" XE_BUILD_COMMIT "..." XE_BUILD_BRANCH);
|
||
}));
|
||
help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||
help_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, "&About...",
|
||
[]() { LaunchWebBrowser("https://xenia.jp/about/"); }));
|
||
}
|
||
main_menu->AddChild(std::move(help_menu));
|
||
|
||
window_->SetMainMenu(std::move(main_menu));
|
||
|
||
window_->SetMainMenuEnabled(false);
|
||
|
||
UpdateTitle();
|
||
|
||
if (!window_->Open()) {
|
||
XELOGE("Failed to open the platform window");
|
||
return false;
|
||
}
|
||
|
||
Profiler::SetUserIO(kZOrderProfiler, window_.get(), nullptr, nullptr);
|
||
|
||
return true;
|
||
}
|
||
|
||
const char* EmulatorWindow::GetCvarValueForSwapPostEffect(
|
||
gpu::CommandProcessor::SwapPostEffect effect) {
|
||
switch (effect) {
|
||
case gpu::CommandProcessor::SwapPostEffect::kFxaa:
|
||
return "fxaa";
|
||
case gpu::CommandProcessor::SwapPostEffect::kFxaaExtreme:
|
||
return "fxaa_extreme";
|
||
default:
|
||
return "";
|
||
}
|
||
}
|
||
|
||
gpu::CommandProcessor::SwapPostEffect
|
||
EmulatorWindow::GetSwapPostEffectForCvarValue(const std::string& cvar_value) {
|
||
if (cvar_value == GetCvarValueForSwapPostEffect(
|
||
gpu::CommandProcessor::SwapPostEffect::kFxaa)) {
|
||
return gpu::CommandProcessor::SwapPostEffect::kFxaa;
|
||
}
|
||
if (cvar_value == GetCvarValueForSwapPostEffect(
|
||
gpu::CommandProcessor::SwapPostEffect::kFxaaExtreme)) {
|
||
return gpu::CommandProcessor::SwapPostEffect::kFxaaExtreme;
|
||
}
|
||
return gpu::CommandProcessor::SwapPostEffect::kNone;
|
||
}
|
||
|
||
const char* EmulatorWindow::GetCvarValueForGuestOutputPaintEffect(
|
||
ui::Presenter::GuestOutputPaintConfig::Effect effect) {
|
||
switch (effect) {
|
||
case ui::Presenter::GuestOutputPaintConfig::Effect::kCas:
|
||
return "cas";
|
||
case ui::Presenter::GuestOutputPaintConfig::Effect::kFsr:
|
||
return "fsr";
|
||
default:
|
||
return "";
|
||
}
|
||
}
|
||
|
||
ui::Presenter::GuestOutputPaintConfig::Effect
|
||
EmulatorWindow::GetGuestOutputPaintEffectForCvarValue(
|
||
const std::string& cvar_value) {
|
||
if (cvar_value == GetCvarValueForGuestOutputPaintEffect(
|
||
ui::Presenter::GuestOutputPaintConfig::Effect::kCas)) {
|
||
return ui::Presenter::GuestOutputPaintConfig::Effect::kCas;
|
||
}
|
||
if (cvar_value == GetCvarValueForGuestOutputPaintEffect(
|
||
ui::Presenter::GuestOutputPaintConfig::Effect::kFsr)) {
|
||
return ui::Presenter::GuestOutputPaintConfig::Effect::kFsr;
|
||
}
|
||
return ui::Presenter::GuestOutputPaintConfig::Effect::kBilinear;
|
||
}
|
||
|
||
ui::Presenter::GuestOutputPaintConfig
|
||
EmulatorWindow::GetGuestOutputPaintConfigForCvars() {
|
||
ui::Presenter::GuestOutputPaintConfig paint_config;
|
||
paint_config.SetAllowOverscanCutoff(true);
|
||
paint_config.SetEffect(GetGuestOutputPaintEffectForCvarValue(
|
||
cvars::postprocess_scaling_and_sharpening));
|
||
paint_config.SetCasAdditionalSharpness(
|
||
float(cvars::postprocess_ffx_cas_additional_sharpness));
|
||
paint_config.SetFsrMaxUpsamplingPasses(
|
||
cvars::postprocess_ffx_fsr_max_upsampling_passes);
|
||
paint_config.SetFsrSharpnessReduction(
|
||
float(cvars::postprocess_ffx_fsr_sharpness_reduction));
|
||
paint_config.SetDither(cvars::postprocess_dither);
|
||
return paint_config;
|
||
}
|
||
|
||
void EmulatorWindow::ApplyDisplayConfigForCvars() {
|
||
gpu::GraphicsSystem* graphics_system = emulator_->graphics_system();
|
||
if (!graphics_system) {
|
||
return;
|
||
}
|
||
|
||
gpu::CommandProcessor* command_processor =
|
||
graphics_system->command_processor();
|
||
if (command_processor) {
|
||
command_processor->SetDesiredSwapPostEffect(
|
||
GetSwapPostEffectForCvarValue(cvars::postprocess_antialiasing));
|
||
}
|
||
|
||
ui::Presenter* presenter = graphics_system->presenter();
|
||
if (presenter) {
|
||
presenter->SetGuestOutputPaintConfigFromUIThread(
|
||
GetGuestOutputPaintConfigForCvars());
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) {
|
||
if (!emulator_initialized_) {
|
||
return;
|
||
}
|
||
|
||
switch (e.virtual_key()) {
|
||
case ui::VirtualKey::kO: {
|
||
if (!e.is_ctrl_pressed()) {
|
||
return;
|
||
}
|
||
FileOpen();
|
||
} break;
|
||
case ui::VirtualKey::kMultiply: {
|
||
CpuTimeScalarReset();
|
||
} break;
|
||
case ui::VirtualKey::kSubtract: {
|
||
CpuTimeScalarSetHalf();
|
||
} break;
|
||
case ui::VirtualKey::kAdd: {
|
||
CpuTimeScalarSetDouble();
|
||
} break;
|
||
|
||
case ui::VirtualKey::kF3: {
|
||
Profiler::ToggleDisplay();
|
||
} break;
|
||
|
||
case ui::VirtualKey::kF4: {
|
||
GpuTraceFrame();
|
||
} break;
|
||
case ui::VirtualKey::kF5: {
|
||
GpuClearCaches();
|
||
} break;
|
||
|
||
case ui::VirtualKey::kF6: {
|
||
ToggleDisplayConfigDialog();
|
||
} break;
|
||
case ui::VirtualKey::kF11: {
|
||
ToggleFullscreen();
|
||
} break;
|
||
case ui::VirtualKey::kF12: {
|
||
TakeScreenshot();
|
||
} 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;
|
||
|
||
case ui::VirtualKey::kF9: {
|
||
RunPreviouslyPlayedTitle();
|
||
} break;
|
||
|
||
default:
|
||
return;
|
||
}
|
||
|
||
e.set_handled(true);
|
||
}
|
||
|
||
void EmulatorWindow::OnMouseDown(const ui::MouseEvent& e) {
|
||
if (e.button() == ui::MouseEvent::Button::kLeft) {
|
||
ToggleFullscreenOnDoubleClick();
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::OnMouseUp(const ui::MouseEvent& e) {
|
||
last_mouse_up = steady_clock::now();
|
||
}
|
||
|
||
void EmulatorWindow::TakeScreenshot() {
|
||
xe::ui::RawImage image;
|
||
|
||
imgui_drawer_->EnableNotifications(false);
|
||
|
||
if (!GetGraphicsSystemPresenter()->CaptureGuestOutput(image) ||
|
||
GetGraphicsSystemPresenter() == nullptr) {
|
||
XELOGE("Failed to capture guest output for screenshot");
|
||
return;
|
||
}
|
||
|
||
imgui_drawer_->EnableNotifications(true);
|
||
ExportScreenshot(image);
|
||
}
|
||
|
||
void EmulatorWindow::ExportScreenshot(const xe::ui::RawImage& image) {
|
||
auto t = std::time(nullptr);
|
||
|
||
// The format is: Year-Month-DayTHours-Minutes-Seconds based off ISO 8601
|
||
std::string datetime =
|
||
fmt::format("{:%Y-%m-%dT%H-%M-%S}", *std::localtime(&t));
|
||
|
||
// Get the title id of the game because some titles contain characters that
|
||
// cannot be used as a directory
|
||
std::string title_id;
|
||
if (emulator()->title_id()) {
|
||
title_id = fmt::format("{:08X}", emulator()->title_id());
|
||
} else {
|
||
XELOGE("Failed to get the current title id");
|
||
return;
|
||
}
|
||
|
||
// Find where xenia.exe or xenia_canary.exe is located and create a
|
||
// screenshots folder
|
||
auto screenshot_path =
|
||
(xe::filesystem::GetExecutableFolder() / "screenshots" / title_id);
|
||
|
||
if (!std::filesystem::exists(screenshot_path)) {
|
||
std::filesystem::create_directories(screenshot_path);
|
||
}
|
||
|
||
std::string filename = fmt::format("{} - {}.png", title_id, datetime);
|
||
SaveImage(screenshot_path / filename, image);
|
||
|
||
const std::string notification_text =
|
||
fmt::format("Screenshot saved: {}", filename);
|
||
|
||
app_context_.CallInUIThread([&, notification_text]() {
|
||
new xe::ui::HostNotificationWindow(imgui_drawer(), "Screenshot Created!",
|
||
notification_text, 0);
|
||
});
|
||
}
|
||
|
||
// Converts a RawImage into a PNG file
|
||
void EmulatorWindow::SaveImage(const std::filesystem::path& filepath,
|
||
const xe::ui::RawImage& image) {
|
||
auto file = std::ofstream(filepath, std::ios::binary);
|
||
if (!file.is_open()) {
|
||
XELOGE("Failed to open file for writing: {}", filepath);
|
||
return;
|
||
}
|
||
|
||
auto result = stbi_write_png_to_func(
|
||
[](void* context, void* data, int size) {
|
||
auto file = reinterpret_cast<std::ofstream*>(context);
|
||
file->write(reinterpret_cast<const char*>(data), size);
|
||
},
|
||
&file, image.width, image.height, 4, image.data.data(),
|
||
(int)image.stride);
|
||
if (result == 0) {
|
||
XELOGE("Failed to write PNG to file: {}", filepath);
|
||
return;
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::ToggleFullscreenOnDoubleClick() {
|
||
// this function tests if user has double clicked.
|
||
// if double click was achieved the fullscreen gets toggled
|
||
const auto now = steady_clock::now(); // current mouse event time
|
||
constexpr int16_t mouse_down_max_threshold = 250;
|
||
constexpr int16_t mouse_up_max_threshold = 250;
|
||
constexpr int16_t mouse_up_down_max_delta = 100;
|
||
// max delta to prevent 'chaining' of double clicks with next mouse events
|
||
|
||
const auto last_mouse_down_delta = diff_in_ms(now, last_mouse_down);
|
||
if (last_mouse_down_delta >= mouse_down_max_threshold) {
|
||
last_mouse_down = now;
|
||
return;
|
||
}
|
||
|
||
const auto last_mouse_up_delta = diff_in_ms(now, last_mouse_up);
|
||
const auto mouse_event_deltas = diff_in_ms(last_mouse_up, last_mouse_down);
|
||
if (last_mouse_up_delta >= mouse_up_max_threshold) {
|
||
return;
|
||
}
|
||
|
||
if (mouse_event_deltas < mouse_up_down_max_delta) {
|
||
ToggleFullscreen();
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::FileDrop(const std::filesystem::path& path) {
|
||
if (!emulator_initialized_) {
|
||
return;
|
||
}
|
||
|
||
RunTitle(path);
|
||
}
|
||
|
||
void EmulatorWindow::FileOpen() {
|
||
std::filesystem::path path;
|
||
|
||
auto file_picker = xe::ui::FilePicker::Create();
|
||
file_picker->set_mode(ui::FilePicker::Mode::kOpen);
|
||
file_picker->set_type(ui::FilePicker::Type::kFile);
|
||
file_picker->set_multi_selection(false);
|
||
file_picker->set_title("Select Content Package");
|
||
file_picker->set_extensions({
|
||
{"Supported Files", "*.iso;*.xex;*.zar;*.*"},
|
||
{"Disc Image (*.iso)", "*.iso"},
|
||
{"Disc Archive (*.zar)", "*.zar"},
|
||
{"Xbox Executable (*.xex)", "*.xex"},
|
||
//{"Content Package (*.xcp)", "*.xcp" },
|
||
{"All Files (*.*)", "*.*"},
|
||
});
|
||
if (file_picker->Show(window_.get())) {
|
||
auto selected_files = file_picker->selected_files();
|
||
if (!selected_files.empty()) {
|
||
path = selected_files[0];
|
||
}
|
||
// Only run the title if a file is selected
|
||
RunTitle(path);
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::FileClose() { emulator_->TerminateTitle(); }
|
||
|
||
void EmulatorWindow::InstallContent() {
|
||
std::vector<std::filesystem::path> paths;
|
||
|
||
auto file_picker = xe::ui::FilePicker::Create();
|
||
file_picker->set_mode(ui::FilePicker::Mode::kOpen);
|
||
file_picker->set_type(ui::FilePicker::Type::kFile);
|
||
file_picker->set_multi_selection(true);
|
||
file_picker->set_title("Select Content Package");
|
||
file_picker->set_extensions({
|
||
{"All Files (*.*)", "*.*"},
|
||
});
|
||
if (file_picker->Show(window_.get())) {
|
||
paths = file_picker->selected_files();
|
||
}
|
||
|
||
if (paths.empty()) {
|
||
return;
|
||
}
|
||
|
||
std::shared_ptr<std::vector<Emulator::ContentInstallEntry>>
|
||
content_installation_status =
|
||
std::make_shared<std::vector<Emulator::ContentInstallEntry>>();
|
||
|
||
for (const auto& path : paths) {
|
||
content_installation_status->push_back({path});
|
||
}
|
||
|
||
for (auto& entry : *content_installation_status) {
|
||
emulator_->ProcessContentPackageHeader(entry.path_, entry);
|
||
}
|
||
|
||
auto installationThread = std::thread([this, content_installation_status] {
|
||
for (auto& entry : *content_installation_status) {
|
||
emulator_->InstallContentPackage(entry.path_, entry);
|
||
}
|
||
});
|
||
installationThread.detach();
|
||
|
||
new ContentInstallDialog(imgui_drawer_.get(), *this,
|
||
content_installation_status);
|
||
}
|
||
|
||
void EmulatorWindow::ExtractZarchive() {
|
||
std::vector<std::filesystem::path> zarchive_files;
|
||
std::filesystem::path extract_dir;
|
||
|
||
auto file_picker = xe::ui::FilePicker::Create();
|
||
file_picker->set_mode(ui::FilePicker::Mode::kOpen);
|
||
file_picker->set_type(ui::FilePicker::Type::kFile);
|
||
file_picker->set_multi_selection(true);
|
||
file_picker->set_title("Select Zar Package");
|
||
file_picker->set_extensions({
|
||
{"Zarchive Files (*.zar)", "*.zar"},
|
||
});
|
||
|
||
if (file_picker->Show(window_.get())) {
|
||
zarchive_files = file_picker->selected_files();
|
||
}
|
||
|
||
if (zarchive_files.empty()) {
|
||
return;
|
||
}
|
||
|
||
file_picker->set_type(ui::FilePicker::Type::kDirectory);
|
||
file_picker->set_title("Select Directory to Extract");
|
||
|
||
if (file_picker->Show(window_.get())) {
|
||
extract_dir = file_picker->selected_files().front();
|
||
}
|
||
|
||
if (extract_dir.empty()) {
|
||
return;
|
||
}
|
||
|
||
std::string extract_overview = "";
|
||
|
||
for (auto& zarchive_file_path : zarchive_files) {
|
||
extract_overview += "\n" + path_to_utf8(zarchive_file_path);
|
||
}
|
||
|
||
app_context_.CallInUIThread([&]() {
|
||
new xe::ui::HostNotificationWindow(imgui_drawer(), "Extracting...",
|
||
string_util::trim(extract_overview), 0);
|
||
});
|
||
|
||
auto run = [this, extract_dir, zarchive_files]() -> void {
|
||
std::string summary = "";
|
||
|
||
for (auto& zarchive_file_path : zarchive_files) {
|
||
// Normalize the path and make absolute.
|
||
auto abs_path = std::filesystem::absolute(zarchive_file_path);
|
||
std::filesystem::path abs_extract_dir;
|
||
|
||
if (zarchive_files.size() > 1) {
|
||
abs_extract_dir =
|
||
std::filesystem::absolute((extract_dir / abs_path.stem()));
|
||
} else {
|
||
abs_extract_dir = std::filesystem::absolute(extract_dir);
|
||
}
|
||
|
||
XELOGI("Extracting zar package: {}\n",
|
||
zarchive_file_path.filename().string());
|
||
|
||
auto result =
|
||
emulator_->ExtractZarchivePackage(abs_path, abs_extract_dir);
|
||
|
||
if (result != X_STATUS_SUCCESS) {
|
||
std::error_code ec;
|
||
|
||
// delete incomplete output file
|
||
std::filesystem::remove(abs_extract_dir, ec);
|
||
|
||
summary += fmt::format("\nFailed: {}", zarchive_file_path);
|
||
|
||
XELOGE("Failed to extract Zarchive package.", result);
|
||
} else {
|
||
summary += fmt::format("\nSuccess: {}", abs_extract_dir);
|
||
}
|
||
}
|
||
|
||
new xe::ui::HostNotificationWindow(imgui_drawer(), "Zar Extraction Summary",
|
||
string_util::trim(summary), 0);
|
||
};
|
||
|
||
auto zarThread = std::thread(run);
|
||
zarThread.detach();
|
||
}
|
||
|
||
void EmulatorWindow::CreateZarchive() {
|
||
std::vector<std::filesystem::path> content_dirs;
|
||
std::filesystem::path zarchive_dir;
|
||
|
||
auto file_picker = xe::ui::FilePicker::Create();
|
||
file_picker->set_mode(ui::FilePicker::Mode::kOpen);
|
||
file_picker->set_type(ui::FilePicker::Type::kDirectory);
|
||
file_picker->set_multi_selection(true);
|
||
file_picker->set_title("Select Contents");
|
||
|
||
if (file_picker->Show(window_.get())) {
|
||
content_dirs = file_picker->selected_files();
|
||
}
|
||
|
||
if (content_dirs.empty()) {
|
||
return;
|
||
}
|
||
|
||
if (content_dirs.size() == 1) {
|
||
file_picker->set_mode(ui::FilePicker::Mode::kSave);
|
||
file_picker->set_type(ui::FilePicker::Type::kFile);
|
||
file_picker->set_multi_selection(false);
|
||
file_picker->set_file_name(content_dirs.front().stem().string());
|
||
file_picker->set_default_extension("zar");
|
||
file_picker->set_title("Zarchive File");
|
||
file_picker->set_extensions({
|
||
{"Zarchive File (*.zar)", "*.zar"},
|
||
});
|
||
} else {
|
||
file_picker->set_title("Output Directory");
|
||
}
|
||
|
||
if (file_picker->Show(window_.get())) {
|
||
zarchive_dir = file_picker->selected_files().front();
|
||
}
|
||
|
||
if (zarchive_dir.empty()) {
|
||
return;
|
||
}
|
||
|
||
std::string create_overview = "";
|
||
|
||
std::map<std::filesystem::path, std::filesystem::path> zarchive_files{};
|
||
|
||
for (auto& content_path : content_dirs) {
|
||
// Normalize the path and make absolute.
|
||
auto abs_content_dir = std::filesystem::absolute(content_path);
|
||
std::filesystem::path abs_zarchive_file;
|
||
|
||
if (content_dirs.size() > 1) {
|
||
abs_zarchive_file = std::filesystem::absolute(
|
||
(zarchive_dir / abs_content_dir.stem()).replace_extension("zar"));
|
||
} else {
|
||
abs_zarchive_file = std::filesystem::absolute(zarchive_dir);
|
||
}
|
||
|
||
zarchive_files[content_path] = abs_zarchive_file;
|
||
|
||
create_overview += "\n" + path_to_utf8(abs_zarchive_file);
|
||
}
|
||
|
||
app_context_.CallInUIThread([&]() {
|
||
new xe::ui::HostNotificationWindow(imgui_drawer(), "Creating...",
|
||
string_util::trim(create_overview), 0);
|
||
});
|
||
|
||
auto run = [this, zarchive_files]() -> void {
|
||
std::string summary = "";
|
||
|
||
for (auto const& [content_path, zarchive_file] : zarchive_files) {
|
||
// Normalize the path and make absolute.
|
||
auto abs_content_dir = std::filesystem::absolute(content_path);
|
||
|
||
XELOGI("Creating zar package: {}\n", zarchive_file.filename().string());
|
||
|
||
auto result =
|
||
emulator_->CreateZarchivePackage(abs_content_dir, zarchive_file);
|
||
|
||
if (result != X_ERROR_SUCCESS) {
|
||
std::error_code ec;
|
||
|
||
// delete incomplete output file
|
||
std::filesystem::remove(zarchive_file, ec);
|
||
|
||
summary += fmt::format("\nFailed: {}", abs_content_dir);
|
||
|
||
XELOGE("Failed to create Zarchive package.", result);
|
||
} else {
|
||
summary += fmt::format("\nSuccess: {}", zarchive_file);
|
||
}
|
||
}
|
||
|
||
new xe::ui::HostNotificationWindow(imgui_drawer(), "Zar Creation Summary",
|
||
string_util::trim(summary), 0);
|
||
};
|
||
|
||
auto zarThread = std::thread(run);
|
||
zarThread.detach();
|
||
}
|
||
|
||
void EmulatorWindow::ShowContentDirectory() {
|
||
auto content_root = emulator_->content_root();
|
||
|
||
if (!std::filesystem::exists(content_root)) {
|
||
std::filesystem::create_directories(content_root);
|
||
}
|
||
|
||
LaunchFileExplorer(content_root);
|
||
}
|
||
|
||
void EmulatorWindow::CpuTimeScalarReset() {
|
||
Clock::set_guest_time_scalar(1.0);
|
||
UpdateTitle();
|
||
}
|
||
|
||
void EmulatorWindow::CpuTimeScalarSetHalf() {
|
||
Clock::set_guest_time_scalar(Clock::guest_time_scalar() / 2.0);
|
||
UpdateTitle();
|
||
}
|
||
|
||
void EmulatorWindow::CpuTimeScalarSetDouble() {
|
||
Clock::set_guest_time_scalar(Clock::guest_time_scalar() * 2.0);
|
||
UpdateTitle();
|
||
}
|
||
|
||
void EmulatorWindow::CpuBreakIntoDebugger() {
|
||
if (!cvars::debug) {
|
||
xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(), "Xenia Debugger",
|
||
"Xenia must be launched with the "
|
||
"--debug flag in order to enable "
|
||
"debugging.");
|
||
return;
|
||
}
|
||
auto processor = emulator()->processor();
|
||
if (processor->execution_state() == cpu::ExecutionState::kRunning) {
|
||
// Currently running, so interrupt (and show the debugger).
|
||
processor->Pause();
|
||
} else {
|
||
// Not running, so just bring the debugger into focus.
|
||
processor->ShowDebugger();
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::CpuBreakIntoHostDebugger() { xe::debugging::Break(); }
|
||
|
||
void EmulatorWindow::GpuTraceFrame() {
|
||
emulator()->graphics_system()->RequestFrameTrace();
|
||
}
|
||
|
||
void EmulatorWindow::GpuClearCaches() {
|
||
emulator()->graphics_system()->ClearCaches();
|
||
}
|
||
|
||
void EmulatorWindow::SetFullscreen(bool fullscreen) {
|
||
if (window_->IsFullscreen() == fullscreen) {
|
||
return;
|
||
}
|
||
window_->SetFullscreen(fullscreen);
|
||
window_->SetCursorVisibility(fullscreen
|
||
? ui::Window::CursorVisibility::kAutoHidden
|
||
: ui::Window::CursorVisibility::kVisible);
|
||
}
|
||
|
||
void EmulatorWindow::ToggleFullscreen() {
|
||
SetFullscreen(!window_->IsFullscreen());
|
||
}
|
||
|
||
void EmulatorWindow::ToggleDisplayConfigDialog() {
|
||
if (!display_config_dialog_) {
|
||
display_config_dialog_ = std::unique_ptr<DisplayConfigDialog>(
|
||
new DisplayConfigDialog(imgui_drawer_.get(), *this));
|
||
} else {
|
||
display_config_dialog_.reset();
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::ToggleProfilesConfigDialog() {
|
||
if (!profile_config_dialog_) {
|
||
disable_hotkeys_ = true;
|
||
emulator_->kernel_state()->BroadcastNotification(kXNotificationSystemUI, 1);
|
||
profile_config_dialog_ =
|
||
std::make_unique<ProfileConfigDialog>(imgui_drawer_.get(), this);
|
||
emulator_->kernel_state()->xam_state()->xam_dialogs_shown_++;
|
||
} else {
|
||
disable_hotkeys_ = false;
|
||
emulator_->kernel_state()->BroadcastNotification(kXNotificationSystemUI, 0);
|
||
if (profile_config_dialog_->IsClosing()) {
|
||
profile_config_dialog_.release();
|
||
} else {
|
||
profile_config_dialog_.reset();
|
||
}
|
||
emulator_->kernel_state()->xam_state()->xam_dialogs_shown_--;
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::ToggleXMPConfigDialog() {
|
||
if (!xmp_config_dialog_) {
|
||
xmp_config_dialog_ = std::unique_ptr<XMPConfigDialog>(
|
||
new XMPConfigDialog(imgui_drawer_.get(), *this));
|
||
} else {
|
||
xmp_config_dialog_.reset();
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::ToggleControllerVibration() {
|
||
auto input_sys = emulator()->input_system();
|
||
if (input_sys) {
|
||
auto input_lock = input_sys->lock();
|
||
|
||
input_sys->ToggleVibration();
|
||
|
||
if (emulator_->kernel_state()) {
|
||
emulator_->kernel_state()->BroadcastNotification(
|
||
kXNotificationSystemProfileSettingChanged,
|
||
static_cast<uint32_t>(input_sys->GetConnectedSlots().count()));
|
||
}
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::ShowCompatibility() {
|
||
const std::string_view base_url =
|
||
"https://github.com/xenia-canary/game-compatibility/issues";
|
||
std::string url;
|
||
// Avoid searching for a title ID of "00000000".
|
||
uint32_t title_id = emulator_->title_id();
|
||
if (!title_id) {
|
||
url = base_url;
|
||
} else {
|
||
url = fmt::format("{}?q=is%3Aissue+is%3Aopen+{:08X}", base_url, title_id);
|
||
}
|
||
LaunchWebBrowser(url);
|
||
}
|
||
|
||
void EmulatorWindow::ShowFAQ() {
|
||
LaunchWebBrowser("https://github.com/xenia-canary/xenia-canary/wiki/FAQ");
|
||
}
|
||
|
||
void EmulatorWindow::ShowBuildCommit() {
|
||
#ifdef XE_BUILD_IS_PR
|
||
LaunchWebBrowser(
|
||
"https://github.com/xenia-canary/xenia-canary/pull/" XE_BUILD_PR_NUMBER);
|
||
#else
|
||
LaunchWebBrowser(
|
||
"https://github.com/xenia-canary/xenia-canary/commit/" XE_BUILD_COMMIT);
|
||
#endif
|
||
}
|
||
|
||
void EmulatorWindow::UpdateTitle() {
|
||
xe::StringBuffer sb;
|
||
sb.Append(base_title_);
|
||
|
||
// Title information, if available
|
||
if (emulator()->is_title_open()) {
|
||
sb.AppendFormat(" | [{:08X}", emulator()->title_id());
|
||
auto title_version = emulator()->title_version();
|
||
if (!title_version.empty()) {
|
||
sb.Append(" v");
|
||
sb.Append(title_version);
|
||
}
|
||
sb.Append("]");
|
||
|
||
auto title_name = emulator()->title_name();
|
||
if (!title_name.empty()) {
|
||
sb.Append(" ");
|
||
sb.Append(title_name);
|
||
}
|
||
}
|
||
|
||
// Graphics system name, if available
|
||
auto graphics_system = emulator()->graphics_system();
|
||
if (graphics_system) {
|
||
auto graphics_name = graphics_system->name();
|
||
if (!graphics_name.empty()) {
|
||
sb.Append(" <");
|
||
sb.Append(graphics_name);
|
||
sb.Append(">");
|
||
}
|
||
}
|
||
|
||
if (Clock::guest_time_scalar() != 1.0) {
|
||
sb.AppendFormat(" (@{:.2f}x)", Clock::guest_time_scalar());
|
||
}
|
||
|
||
if (initializing_shader_storage_) {
|
||
sb.Append(" (Preloading shaders\u2026)");
|
||
}
|
||
|
||
patcher::Patcher* patcher = emulator()->patcher();
|
||
if (patcher && patcher->IsAnyPatchApplied()) {
|
||
sb.Append(" [Patches Applied]");
|
||
}
|
||
|
||
patcher::PluginLoader* pluginloader = emulator()->plugin_loader();
|
||
if (pluginloader && pluginloader->IsAnyPluginLoaded()) {
|
||
sb.Append(" [Plugins Loaded]");
|
||
}
|
||
|
||
window_->SetTitle(sb.to_string_view());
|
||
}
|
||
|
||
void EmulatorWindow::SetInitializingShaderStorage(bool initializing) {
|
||
if (initializing_shader_storage_ == initializing) {
|
||
return;
|
||
}
|
||
initializing_shader_storage_ = initializing;
|
||
UpdateTitle();
|
||
}
|
||
|
||
// Notes:
|
||
// SDL and XInput both support the guide button
|
||
//
|
||
// Assumes titles do not use the guide button.
|
||
// For titles that do such as dashboards these titles could be excluded based on
|
||
// their title ID.
|
||
//
|
||
// Xbox Gamebar:
|
||
// If the Xbox Gamebar overlay is enabled Windows will consume the guide
|
||
// button's input, this can be seen using hid-demo.
|
||
//
|
||
// Workaround: Detect if the Xbox Gamebar overlay is enabled then use the BACK
|
||
// button instead of the GUIDE button. Therefore BACK and GUIDE are reserved
|
||
// buttons for hotkeys.
|
||
//
|
||
// This is not an issue with DualShock controllers because Windows will not
|
||
// open the gamebar overlay using the PlayStation menu button.
|
||
//
|
||
// Xbox One S Controller:
|
||
// The guide button on this controller is very buggy no idea why.
|
||
// Using xinput usually registers after a double tap.
|
||
// Doesn't work at all using SDL.
|
||
// Needs more testing.
|
||
//
|
||
// Steam:
|
||
// If guide button focus is enabled steam will open.
|
||
// Steam uses BACK + GUIDE to open an On-Screen keyboard, however this is not a
|
||
// problem since both these buttons are reserved.
|
||
const std::map<int, EmulatorWindow::ControllerHotKey> controller_hotkey_map = {
|
||
// Must use the Guide Button for all pass through hotkeys
|
||
{X_INPUT_GAMEPAD_A | X_INPUT_GAMEPAD_GUIDE,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::ReadbackResolve,
|
||
"A + Guide = Toggle Readback Resolve", true)},
|
||
{X_INPUT_GAMEPAD_B | X_INPUT_GAMEPAD_GUIDE,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::ToggleLogging,
|
||
"B + Guide = Toggle between loglevel set in config and the 'Disabled' "
|
||
"loglevel.",
|
||
true, true)},
|
||
{X_INPUT_GAMEPAD_Y | X_INPUT_GAMEPAD_GUIDE,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::ToggleFullscreen,
|
||
"Y + Guide = Toggle Fullscreen", true)},
|
||
{X_INPUT_GAMEPAD_X | X_INPUT_GAMEPAD_GUIDE,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::ClearMemoryPageState,
|
||
"X + Guide = Toggle Clear Memory Page State", true)},
|
||
|
||
{X_INPUT_GAMEPAD_RIGHT_SHOULDER | X_INPUT_GAMEPAD_GUIDE,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::ClearGPUCache,
|
||
"Right Shoulder + Guide = Clear GPU Cache", true)},
|
||
{X_INPUT_GAMEPAD_LEFT_SHOULDER | X_INPUT_GAMEPAD_GUIDE,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::ToggleControllerVibration,
|
||
"Left Shoulder + Guide = Toggle Controller Vibration", true)},
|
||
|
||
// CPU Time Scalar with no rumble feedback
|
||
{X_INPUT_GAMEPAD_DPAD_DOWN | X_INPUT_GAMEPAD_GUIDE,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::CpuTimeScalarSetHalf,
|
||
"D-PAD Down + Guide = Half CPU Scalar")},
|
||
{X_INPUT_GAMEPAD_DPAD_UP | X_INPUT_GAMEPAD_GUIDE,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::CpuTimeScalarSetDouble,
|
||
"D-PAD Up + Guide = Double CPU Scalar")},
|
||
{X_INPUT_GAMEPAD_DPAD_RIGHT | X_INPUT_GAMEPAD_GUIDE,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::CpuTimeScalarReset,
|
||
"D-PAD Right + Guide = Reset CPU Scalar")},
|
||
|
||
// non-pass through hotkeys
|
||
{X_INPUT_GAMEPAD_Y, EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::ToggleFullscreen,
|
||
"Y = Toggle Fullscreen", true, false)},
|
||
{X_INPUT_GAMEPAD_START, EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::RunTitle,
|
||
"Start = Run Selected Title", false, false)},
|
||
{X_INPUT_GAMEPAD_BACK | X_INPUT_GAMEPAD_START,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::ToggleLogging,
|
||
"Back + Start = Toggle between loglevel set in config and the "
|
||
"'Disabled' loglevel.",
|
||
false, false)},
|
||
{X_INPUT_GAMEPAD_DPAD_DOWN,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::IncTitleSelect,
|
||
"D-PAD Down = Title Selection +1", true, false)},
|
||
{X_INPUT_GAMEPAD_DPAD_UP,
|
||
EmulatorWindow::ControllerHotKey(
|
||
EmulatorWindow::ButtonFunctions::DecTitleSelect,
|
||
"D-PAD Up = Title Selection -1", true, false)}};
|
||
|
||
EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey(
|
||
int buttons) {
|
||
// Default return value
|
||
EmulatorWindow::ControllerHotKey Unknown_hotkey = {};
|
||
|
||
if (buttons == 0) {
|
||
return Unknown_hotkey;
|
||
}
|
||
|
||
if (disable_hotkeys_.load()) {
|
||
return Unknown_hotkey;
|
||
}
|
||
|
||
// Hotkey cool-down to prevent toggling too fast
|
||
constexpr std::chrono::milliseconds delay(75);
|
||
|
||
// If the Xbox Gamebar is enabled or the Guide button is disabled then
|
||
// replace the Guide button with the Back button without redeclaring the key
|
||
// mappings
|
||
if (IsUseNexusForGameBarEnabled() || !cvars::guide_button) {
|
||
if ((buttons & X_INPUT_GAMEPAD_BACK) == X_INPUT_GAMEPAD_BACK) {
|
||
buttons &= ~X_INPUT_GAMEPAD_BACK;
|
||
buttons |= X_INPUT_GAMEPAD_GUIDE;
|
||
}
|
||
}
|
||
|
||
auto it = controller_hotkey_map.find(buttons);
|
||
if (it == controller_hotkey_map.end()) {
|
||
return Unknown_hotkey;
|
||
}
|
||
|
||
// Do not activate hotkeys that are not intended for activation during
|
||
// gameplay
|
||
if (emulator_->is_title_open()) {
|
||
// If non-pass through (menu hoykeys) or hotkeys disabled then return
|
||
if (!it->second.title_passthru || !cvars::controller_hotkeys) {
|
||
return Unknown_hotkey;
|
||
}
|
||
}
|
||
|
||
std::string notificationTitle = "";
|
||
std::string notificationDesc = "";
|
||
|
||
EmulatorWindow::ControllerHotKey button_combination = it->second;
|
||
|
||
switch (button_combination.function) {
|
||
case ButtonFunctions::ToggleFullscreen:
|
||
app_context().CallInUIThread([this]() { ToggleFullscreen(); });
|
||
|
||
// Extra Sleep
|
||
xe::threading::Sleep(delay);
|
||
break;
|
||
case ButtonFunctions::RunTitle: {
|
||
if (selected_title_index == -1) {
|
||
selected_title_index++;
|
||
}
|
||
|
||
if (selected_title_index < recently_launched_titles_.size()) {
|
||
app_context().CallInUIThread([this]() {
|
||
RunTitle(
|
||
recently_launched_titles_[selected_title_index].path_to_file);
|
||
});
|
||
}
|
||
} break;
|
||
case ButtonFunctions::ClearMemoryPageState:
|
||
ToggleGPUSetting(GPUSetting::ClearMemoryPageState);
|
||
|
||
// Assume the user wants ClearCaches as well
|
||
if (cvars::clear_memory_page_state) {
|
||
GpuClearCaches();
|
||
}
|
||
|
||
notificationTitle = "Toggle Clear Memory Page State";
|
||
notificationDesc =
|
||
cvars::clear_memory_page_state ? "Enabled" : "Disabled";
|
||
|
||
// Extra Sleep
|
||
xe::threading::Sleep(delay);
|
||
break;
|
||
case ButtonFunctions::ReadbackResolve:
|
||
ToggleGPUSetting(GPUSetting::ReadbackResolve);
|
||
|
||
notificationTitle = "Toggle Readback Resolve";
|
||
notificationDesc = cvars::readback_resolve ? "Enabled" : "Disabled";
|
||
|
||
// Extra Sleep
|
||
xe::threading::Sleep(delay);
|
||
break;
|
||
case ButtonFunctions::CpuTimeScalarSetHalf:
|
||
CpuTimeScalarSetHalf();
|
||
|
||
notificationTitle = "Time Scalar";
|
||
notificationDesc =
|
||
fmt::format("Decreased to {}", Clock::guest_time_scalar());
|
||
break;
|
||
case ButtonFunctions::CpuTimeScalarSetDouble:
|
||
CpuTimeScalarSetDouble();
|
||
|
||
notificationTitle = "Time Scalar";
|
||
notificationDesc =
|
||
fmt::format("Increased to {}", Clock::guest_time_scalar());
|
||
break;
|
||
case ButtonFunctions::CpuTimeScalarReset:
|
||
CpuTimeScalarReset();
|
||
|
||
notificationTitle = "Time Scalar";
|
||
notificationDesc = fmt::format("Reset to {}", Clock::guest_time_scalar());
|
||
break;
|
||
case ButtonFunctions::ClearGPUCache:
|
||
GpuClearCaches();
|
||
|
||
notificationTitle = "Clear GPU Cache";
|
||
notificationDesc = "Complete";
|
||
|
||
// Extra Sleep
|
||
xe::threading::Sleep(delay);
|
||
break;
|
||
case ButtonFunctions::ToggleControllerVibration: {
|
||
ToggleControllerVibration();
|
||
|
||
bool vibration = false;
|
||
|
||
auto input_sys = emulator()->input_system();
|
||
if (input_sys) {
|
||
vibration = input_sys->GetVibrationCvar();
|
||
}
|
||
|
||
notificationTitle = "Toggle Controller Vibration";
|
||
notificationDesc = vibration ? "Enabled" : "Disabled";
|
||
|
||
// Extra Sleep
|
||
xe::threading::Sleep(delay);
|
||
} break;
|
||
case ButtonFunctions::IncTitleSelect:
|
||
selected_title_index++;
|
||
break;
|
||
case ButtonFunctions::DecTitleSelect:
|
||
selected_title_index--;
|
||
break;
|
||
case ButtonFunctions::ToggleLogging: {
|
||
logging::ToggleLogLevel();
|
||
|
||
notificationTitle = "Toggle Logging";
|
||
|
||
LogLevel level = static_cast<LogLevel>(logging::internal::GetLogLevel());
|
||
notificationDesc = level == LogLevel::Disabled ? "Disabled" : "Enabled";
|
||
} break;
|
||
case ButtonFunctions::Unknown:
|
||
default:
|
||
break;
|
||
}
|
||
|
||
if ((button_combination.function == ButtonFunctions::IncTitleSelect ||
|
||
button_combination.function == ButtonFunctions::DecTitleSelect) &&
|
||
recently_launched_titles_.size() > 0) {
|
||
selected_title_index =
|
||
std::clamp(selected_title_index, 0,
|
||
static_cast<int32_t>(recently_launched_titles_.size() - 1));
|
||
|
||
// Must clear dialogs to prevent stacking
|
||
ClearDialogs();
|
||
|
||
// Titles may contain Unicode characters such as At World’s End
|
||
// Must use ImGUI font that can render these Unicode characters
|
||
std::string title_name;
|
||
|
||
// Use filename if title name is empty
|
||
if (recently_launched_titles_[selected_title_index].title_name.empty()) {
|
||
title_name = recently_launched_titles_[selected_title_index]
|
||
.path_to_file.filename()
|
||
.string();
|
||
} else {
|
||
title_name = recently_launched_titles_[selected_title_index].title_name;
|
||
}
|
||
|
||
std::string title = fmt::format(
|
||
"{}: {}\n\n{}", selected_title_index + 1, title_name,
|
||
controller_hotkey_map.find(X_INPUT_GAMEPAD_START)->second.pretty);
|
||
|
||
xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(), "Title Selection",
|
||
title);
|
||
}
|
||
|
||
if (!notificationTitle.empty()) {
|
||
app_context_.CallInUIThread(
|
||
[imgui_drawer = imgui_drawer(), notificationTitle, notificationDesc]() {
|
||
new xe::ui::HostNotificationWindow(imgui_drawer, notificationTitle,
|
||
notificationDesc, 0);
|
||
});
|
||
}
|
||
|
||
xe::threading::Sleep(delay);
|
||
|
||
return it->second;
|
||
}
|
||
|
||
void EmulatorWindow::VibrateController(xe::hid::InputSystem* input_sys,
|
||
uint32_t user_index,
|
||
bool toggle_rumble) {
|
||
constexpr std::chrono::milliseconds rumble_duration(100);
|
||
|
||
// Hold lock while sleeping this thread for the duration of the rumble,
|
||
// otherwise the rumble may fail.
|
||
auto input_lock = input_sys->lock();
|
||
|
||
X_INPUT_VIBRATION vibration = {};
|
||
|
||
vibration.left_motor_speed = toggle_rumble ? UINT16_MAX : 0;
|
||
vibration.right_motor_speed = toggle_rumble ? UINT16_MAX : 0;
|
||
|
||
input_sys->SetState(user_index, &vibration);
|
||
|
||
// Vibration duration
|
||
if (toggle_rumble) {
|
||
xe::threading::Sleep(rumble_duration);
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::GamepadHotKeys() {
|
||
X_INPUT_STATE state;
|
||
|
||
constexpr std::chrono::milliseconds thread_delay(75);
|
||
|
||
auto input_sys = emulator_->input_system();
|
||
|
||
if (input_sys) {
|
||
while (true) {
|
||
// Collect controller states while holding the lock
|
||
std::array<std::pair<bool, X_INPUT_STATE>, XUserMaxUserCount>
|
||
controller_states;
|
||
{
|
||
auto input_lock = input_sys->lock();
|
||
for (uint32_t user_index = 0; user_index < XUserMaxUserCount;
|
||
++user_index) {
|
||
X_RESULT result = input_sys->GetState(
|
||
user_index, X_INPUT_FLAG::X_INPUT_FLAG_GAMEPAD, &state);
|
||
controller_states[user_index] = {result == X_ERROR_SUCCESS, state};
|
||
}
|
||
} // Lock is released here when input_lock goes out of scope
|
||
|
||
// Process hotkeys without holding the lock
|
||
for (uint32_t user_index = 0; user_index < XUserMaxUserCount;
|
||
++user_index) {
|
||
if (controller_states[user_index].first) {
|
||
if (ProcessControllerHotkey(
|
||
controller_states[user_index].second.gamepad.buttons)
|
||
.rumble) {
|
||
// Enable Vibration
|
||
VibrateController(input_sys, user_index, true);
|
||
|
||
// Disable Vibration
|
||
VibrateController(input_sys, user_index, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
xe::threading::Sleep(thread_delay);
|
||
}
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::ToggleGPUSetting(gpu::GPUSetting setting) {
|
||
switch (setting) {
|
||
case GPUSetting::ClearMemoryPageState:
|
||
SaveGPUSetting(GPUSetting::ClearMemoryPageState,
|
||
!cvars::clear_memory_page_state);
|
||
break;
|
||
case GPUSetting::ReadbackResolve:
|
||
SaveGPUSetting(GPUSetting::ReadbackResolve, !cvars::readback_resolve);
|
||
break;
|
||
case GPUSetting::ReadbackMemexport:
|
||
SaveGPUSetting(GPUSetting::ReadbackMemexport, !cvars::readback_memexport);
|
||
break;
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::DisplayHotKeysConfig() {
|
||
std::string msg = "";
|
||
std::string msg_passthru = "";
|
||
|
||
bool guide_enabled = !IsUseNexusForGameBarEnabled() && cvars::guide_button;
|
||
|
||
for (auto const& [key, val] : controller_hotkey_map) {
|
||
std::string pretty_text = val.pretty;
|
||
|
||
if (!guide_enabled) {
|
||
pretty_text = std::regex_replace(
|
||
pretty_text, std::regex("Guide", std::regex_constants::icase),
|
||
"Back");
|
||
}
|
||
|
||
if (emulator_->is_title_open() && !val.title_passthru) {
|
||
pretty_text += " (Disabled)";
|
||
}
|
||
|
||
if (val.title_passthru && !cvars::controller_hotkeys) {
|
||
pretty_text += " (Disabled)";
|
||
}
|
||
|
||
if (val.title_passthru) {
|
||
msg += pretty_text + "\n";
|
||
} else {
|
||
msg_passthru += pretty_text + "\n";
|
||
}
|
||
}
|
||
|
||
// Add Title
|
||
msg.insert(0, "Gameplay Hotkeys\n");
|
||
|
||
// Prepend non-passthru hotkeys
|
||
msg_passthru += "\n";
|
||
msg.insert(0, msg_passthru);
|
||
msg += "\n";
|
||
|
||
msg += "Readback Resolve: " +
|
||
xe::string_util::BoolToString(cvars::readback_resolve);
|
||
msg += "\n";
|
||
|
||
msg += "Clear Memory Page State: " +
|
||
xe::string_util::BoolToString(cvars::clear_memory_page_state);
|
||
msg += "\n";
|
||
|
||
msg += "Controller Hotkeys: " +
|
||
xe::string_util::BoolToString(cvars::controller_hotkeys);
|
||
|
||
ClearDialogs();
|
||
xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(), "Controller Hotkeys",
|
||
msg);
|
||
}
|
||
|
||
std::string EmulatorWindow::CanonicalizeFileExtension(
|
||
const std::filesystem::path& path) {
|
||
return xe::utf8::lower_ascii(xe::path_to_utf8(path.extension()));
|
||
}
|
||
|
||
xe::X_STATUS EmulatorWindow::RunTitle(
|
||
const std::filesystem::path& path_to_file) {
|
||
std::error_code ec = {};
|
||
bool titleExists = std::filesystem::exists(path_to_file, ec);
|
||
|
||
if (path_to_file.empty() || !titleExists) {
|
||
std::string log_msg =
|
||
fmt::format("Failed to launch title path is {}.",
|
||
path_to_file.empty() ? "empty" : "invalid");
|
||
|
||
if (!path_to_file.empty() && !titleExists) {
|
||
log_msg.append(fmt::format("\nProvided Path: {}", path_to_file));
|
||
}
|
||
|
||
if (ec) {
|
||
log_msg.append(fmt::format("\nExtended message info: {} ({:08X})",
|
||
ec.message(), ec.value()));
|
||
}
|
||
|
||
XELOGE("{}", log_msg);
|
||
|
||
ClearDialogs();
|
||
|
||
xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(),
|
||
"Title Launch Failed!", log_msg);
|
||
|
||
return X_STATUS_NO_SUCH_FILE;
|
||
}
|
||
|
||
if (emulator_->is_title_open()) {
|
||
// Terminate the current title and start a new title.
|
||
// if (emulator_->TerminateTitle() == X_STATUS_SUCCESS) {
|
||
// return RunTitle(path);
|
||
// }
|
||
|
||
return X_STATUS_UNSUCCESSFUL;
|
||
}
|
||
|
||
// Prevent crashing the emulator by not loading a game if a game is already
|
||
// loaded.
|
||
auto abs_path = std::filesystem::absolute(path_to_file);
|
||
|
||
auto extension = CanonicalizeFileExtension(abs_path);
|
||
|
||
if (extension == ".7z" || extension == ".zip" || extension == ".rar" ||
|
||
extension == ".tar" || extension == ".gz") {
|
||
xe::ShowSimpleMessageBox(
|
||
xe::SimpleMessageBoxType::Error,
|
||
fmt::format(
|
||
"Unsupported format!\n"
|
||
"Xenia does not support running software in an archived format."));
|
||
|
||
return X_STATUS_UNSUCCESSFUL;
|
||
}
|
||
|
||
auto result = emulator_->LaunchPath(abs_path);
|
||
|
||
disable_hotkeys_ = false;
|
||
|
||
if (profile_config_dialog_) {
|
||
profile_config_dialog_.reset();
|
||
emulator_->kernel_state()->xam_state()->xam_dialogs_shown_--;
|
||
}
|
||
|
||
if (display_config_dialog_) {
|
||
display_config_dialog_.reset();
|
||
}
|
||
|
||
ClearDialogs();
|
||
|
||
if (result) {
|
||
XELOGE("Failed to launch target: {:08X}", result);
|
||
|
||
xe::ui::ImGuiDialog::ShowMessageBox(
|
||
imgui_drawer_.get(), "Title Launch Failed!",
|
||
"Failed to launch title.\n\nCheck xenia.log for technical details.");
|
||
|
||
emulator_->file_system()->Clear();
|
||
} else {
|
||
AddRecentlyLaunchedTitle(path_to_file, emulator_->title_name());
|
||
|
||
auto xam =
|
||
emulator_->kernel_state()->GetKernelModule<kernel::xam::XamModule>(
|
||
"xam.xex");
|
||
|
||
xam->loader_data().host_path = xe::path_to_utf8(abs_path);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
void EmulatorWindow::RunPreviouslyPlayedTitle() {
|
||
if (recently_launched_titles_.size() >= 1) {
|
||
RunTitle(recently_launched_titles_[0].path_to_file);
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::FillRecentlyLaunchedTitlesMenu(
|
||
xe::ui::MenuItem* recent_menu) {
|
||
for (int i = 0; i < recently_launched_titles_.size(); ++i) {
|
||
std::string hotkey = (i == 0) ? "F9" : "";
|
||
|
||
const RecentTitleEntry& entry = recently_launched_titles_[i];
|
||
const std::string item_text = entry.title_name.empty()
|
||
? entry.path_to_file.string()
|
||
: entry.title_name;
|
||
|
||
recent_menu->AddChild(MenuItem::Create(
|
||
MenuItem::Type::kString, item_text, hotkey,
|
||
std::bind(&EmulatorWindow::RunTitle, this, entry.path_to_file)));
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::LoadRecentlyLaunchedTitles() {
|
||
std::ifstream file(emulator()->storage_root() /
|
||
kRecentlyPlayedTitlesFilename);
|
||
if (!file.is_open()) {
|
||
return;
|
||
}
|
||
|
||
toml::parse_result parsed_file;
|
||
try {
|
||
parsed_file = toml::parse(file);
|
||
} catch (toml::parse_error& exception) {
|
||
XELOGE("Cannot parse file: recent.toml. Error: {}", exception.what());
|
||
return;
|
||
}
|
||
|
||
if (parsed_file.is_table()) {
|
||
for (const auto& [index, entry] : *parsed_file.as_table()) {
|
||
if (!entry.is_table()) {
|
||
continue;
|
||
}
|
||
|
||
const toml::table* entry_table = entry.as_table();
|
||
|
||
std::string title_name =
|
||
entry_table->get_as<std::string>("title_name")->get();
|
||
std::string path = entry_table->get_as<std::string>("path")->get();
|
||
std::time_t last_run_time =
|
||
entry_table->get_as<int64_t>("last_run_time")->get();
|
||
|
||
std::error_code ec = {};
|
||
if (path.empty() || !std::filesystem::exists(path, ec)) {
|
||
continue;
|
||
}
|
||
|
||
recently_launched_titles_.push_back({title_name, path, last_run_time});
|
||
}
|
||
}
|
||
}
|
||
|
||
void EmulatorWindow::AddRecentlyLaunchedTitle(
|
||
std::filesystem::path path_to_file, std::string title_name) {
|
||
if (cvars::recent_titles_entry_amount <= 0) {
|
||
return;
|
||
}
|
||
|
||
// Check if game is already on list and pop it to front
|
||
auto entry_index = std::find_if(recently_launched_titles_.cbegin(),
|
||
recently_launched_titles_.cend(),
|
||
[&title_name](const RecentTitleEntry& entry) {
|
||
return entry.title_name == title_name;
|
||
});
|
||
if (entry_index != recently_launched_titles_.cend()) {
|
||
recently_launched_titles_.erase(entry_index);
|
||
}
|
||
|
||
recently_launched_titles_.insert(recently_launched_titles_.cbegin(),
|
||
{title_name, path_to_file, time(nullptr)});
|
||
// Serialize to toml
|
||
auto toml_table = toml::table();
|
||
|
||
uint8_t index = 0;
|
||
for (const RecentTitleEntry& entry : recently_launched_titles_) {
|
||
auto entry_table = toml::table();
|
||
|
||
// Fill entry under specific index.
|
||
std::string str_path = xe::path_to_utf8(entry.path_to_file);
|
||
entry_table.insert("title_name", entry.title_name);
|
||
entry_table.insert("path", str_path);
|
||
entry_table.insert("last_run_time", entry.last_run_time);
|
||
|
||
toml_table.insert(std::to_string(index++), entry_table);
|
||
|
||
if (index >= cvars::recent_titles_entry_amount) {
|
||
break;
|
||
}
|
||
}
|
||
// Open and write serialized data.
|
||
std::ofstream file(emulator()->storage_root() / kRecentlyPlayedTitlesFilename,
|
||
std::ofstream::trunc);
|
||
file << toml_table;
|
||
file.close();
|
||
}
|
||
|
||
void EmulatorWindow::ClearDialogs() {
|
||
if (profile_config_dialog_) {
|
||
profile_config_dialog_.reset();
|
||
}
|
||
|
||
if (display_config_dialog_) {
|
||
display_config_dialog_.reset();
|
||
}
|
||
|
||
imgui_drawer_.get()->ClearDialogs();
|
||
emulator_->kernel_state()->xam_state()->xam_dialogs_shown_ = 0;
|
||
}
|
||
|
||
} // namespace app
|
||
} // namespace xe
|