Duckstation/src/duckstation-sdl/sdl_host_interface.cpp
2020-05-17 14:12:35 +10:00

1445 lines
43 KiB
C++

#include "sdl_host_interface.h"
#include "common/assert.h"
#include "common/byte_stream.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/string_util.h"
#include "core/controller.h"
#include "core/gpu.h"
#include "core/host_display.h"
#include "core/system.h"
#include "frontend-common/icon.h"
#include "frontend-common/imgui_styles.h"
#include "frontend-common/ini_settings_interface.h"
#include "frontend-common/sdl_audio_stream.h"
#include "frontend-common/sdl_controller_interface.h"
#include "imgui_impl_sdl.h"
#include "opengl_host_display.h"
#include "scmversion/scmversion.h"
#include "sdl_key_names.h"
#include <cinttypes>
#include <cmath>
#include <imgui.h>
#include <imgui_stdlib.h>
#include <nfd.h>
Log_SetChannel(SDLHostInterface);
#ifdef WIN32
#include "d3d11_host_display.h"
#endif
SDLHostInterface::SDLHostInterface()
{
m_run_later_event_id = SDL_RegisterEvents(1);
}
SDLHostInterface::~SDLHostInterface() = default;
const char* SDLHostInterface::GetFrontendName() const
{
return "DuckStation SDL/ImGui Frontend";
}
ALWAYS_INLINE static TinyString GetWindowTitle()
{
return TinyString::FromFormat("DuckStation %s (%s)", g_scm_tag_str, g_scm_branch_str);
}
float SDLHostInterface::GetDPIScaleFactor(SDL_Window* window)
{
#ifdef __APPLE__
static constexpr float DEFAULT_DPI = 72.0f;
#else
static constexpr float DEFAULT_DPI = 96.0f;
#endif
if (!window)
{
SDL_Window* dummy_window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1, 1,
SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
if (!dummy_window)
return 1.0f;
const float scale = GetDPIScaleFactor(dummy_window);
SDL_DestroyWindow(dummy_window);
return scale;
}
int display_index = SDL_GetWindowDisplayIndex(window);
float display_dpi = DEFAULT_DPI;
if (SDL_GetDisplayDPI(display_index, &display_dpi, nullptr, nullptr) != 0)
return 1.0f;
return display_dpi / DEFAULT_DPI;
}
bool SDLHostInterface::CreateSDLWindow()
{
static constexpr u32 DEFAULT_WINDOW_WIDTH = 900;
static constexpr u32 DEFAULT_WINDOW_HEIGHT = 700;
// Create window.
const u32 window_flags =
SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | (UseOpenGLRenderer() ? SDL_WINDOW_OPENGL : 0);
u32 window_width = DEFAULT_WINDOW_WIDTH;
u32 window_height = DEFAULT_WINDOW_HEIGHT;
// macOS does DPI scaling differently..
#ifndef __APPLE__
{
// scale by default monitor's DPI
float scale = GetDPIScaleFactor(nullptr);
window_width = static_cast<u32>(std::round(static_cast<float>(window_width) * scale));
window_height = static_cast<u32>(std::round(static_cast<float>(window_height) * scale));
}
#endif
m_window = SDL_CreateWindow(GetWindowTitle(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width,
window_height, window_flags);
if (!m_window)
return false;
// Set window icon.
SDL_Surface* icon_surface =
SDL_CreateRGBSurfaceFrom(const_cast<unsigned int*>(WINDOW_ICON_DATA), WINDOW_ICON_WIDTH, WINDOW_ICON_HEIGHT, 32,
WINDOW_ICON_WIDTH * sizeof(u32), UINT32_C(0x000000FF), UINT32_C(0x0000FF00),
UINT32_C(0x00FF0000), UINT32_C(0xFF000000));
if (icon_surface)
{
SDL_SetWindowIcon(m_window, icon_surface);
SDL_FreeSurface(icon_surface);
}
if (m_fullscreen)
SDL_SetWindowFullscreen(m_window, SDL_WINDOW_FULLSCREEN_DESKTOP);
return true;
}
void SDLHostInterface::DestroySDLWindow()
{
SDL_DestroyWindow(m_window);
m_window = nullptr;
}
bool SDLHostInterface::CreateDisplay()
{
const bool debug_device = m_settings.gpu_use_debug_device;
std::unique_ptr<HostDisplay> display;
#ifdef WIN32
display = UseOpenGLRenderer() ? OpenGLHostDisplay::Create(m_window, debug_device) :
D3D11HostDisplay::Create(m_window, debug_device);
#else
display = OpenGLHostDisplay::Create(m_window, debug_device);
#endif
if (!display)
return false;
m_app_icon_texture =
display->CreateTexture(APP_ICON_WIDTH, APP_ICON_HEIGHT, APP_ICON_DATA, APP_ICON_WIDTH * sizeof(u32));
if (!display)
return false;
display->SetDisplayTopMargin(m_fullscreen ? 0 : static_cast<int>(20.0f * ImGui::GetIO().DisplayFramebufferScale.x));
m_display = display.release();
return true;
}
void SDLHostInterface::DestroyDisplay()
{
m_app_icon_texture.reset();
delete m_display;
m_display = nullptr;
}
void SDLHostInterface::CreateImGuiContext()
{
const float framebuffer_scale = GetDPIScaleFactor(m_window);
ImGui::CreateContext();
ImGui::GetIO().IniFilename = nullptr;
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
ImGui::GetIO().DisplayFramebufferScale.x = framebuffer_scale;
ImGui::GetIO().DisplayFramebufferScale.y = framebuffer_scale;
ImGui::GetStyle().ScaleAllSizes(framebuffer_scale);
ImGui::StyleColorsDarker();
ImGui::AddRobotoRegularFont(15.0f * framebuffer_scale);
}
void SDLHostInterface::UpdateFramebufferScale()
{
const float framebuffer_scale = GetDPIScaleFactor(m_window);
ImGui::GetIO().DisplayFramebufferScale.x = framebuffer_scale;
ImGui::GetIO().DisplayFramebufferScale.y = framebuffer_scale;
}
bool SDLHostInterface::AcquireHostDisplay()
{
// Handle renderer switch if required on Windows.
#ifdef WIN32
const HostDisplay::RenderAPI render_api = m_display->GetRenderAPI();
const bool render_api_is_gl =
render_api == HostDisplay::RenderAPI::OpenGL || render_api == HostDisplay::RenderAPI::OpenGLES;
const bool render_api_wants_gl = UseOpenGLRenderer();
if (render_api_is_gl != render_api_wants_gl)
{
ImGui::EndFrame();
DestroyDisplay();
DestroySDLWindow();
if (!CreateSDLWindow())
Panic("Failed to recreate SDL window on GPU renderer switch");
if (!CreateDisplay())
Panic("Failed to recreate display on GPU renderer switch");
ImGui::NewFrame();
}
#endif
return true;
}
void SDLHostInterface::ReleaseHostDisplay()
{
if (m_fullscreen)
SetFullscreen(false);
// restore vsync, since we don't want to burn cycles at the menu
m_display->SetVSync(true);
}
std::unique_ptr<AudioStream> SDLHostInterface::CreateAudioStream(AudioBackend backend)
{
switch (backend)
{
case AudioBackend::Null:
return AudioStream::CreateNullAudioStream();
case AudioBackend::Cubeb:
return AudioStream::CreateCubebAudioStream();
case AudioBackend::SDL:
return SDLAudioStream::Create();
default:
return nullptr;
}
}
std::unique_ptr<ControllerInterface> SDLHostInterface::CreateControllerInterface()
{
return std::make_unique<SDLControllerInterface>();
}
std::optional<CommonHostInterface::HostKeyCode> SDLHostInterface::GetHostKeyCode(const std::string_view key_code) const
{
const std::optional<u32> code = SDLKeyNames::ParseKeyString(key_code);
if (!code)
return std::nullopt;
return static_cast<HostKeyCode>(*code);
}
void SDLHostInterface::UpdateInputMap()
{
CommonHostInterface::UpdateInputMap(*m_settings_interface.get());
}
void SDLHostInterface::OnSystemCreated()
{
CommonHostInterface::OnSystemCreated();
ClearImGuiFocus();
}
void SDLHostInterface::OnSystemPaused(bool paused)
{
CommonHostInterface::OnSystemPaused(paused);
if (!paused)
ClearImGuiFocus();
}
void SDLHostInterface::OnSystemDestroyed()
{
CommonHostInterface::OnSystemDestroyed();
}
void SDLHostInterface::OnRunningGameChanged()
{
CommonHostInterface::OnRunningGameChanged();
if (m_system && !m_system->GetRunningTitle().empty())
SDL_SetWindowTitle(m_window, m_system->GetRunningTitle().c_str());
else
SDL_SetWindowTitle(m_window, GetWindowTitle());
}
void SDLHostInterface::RequestExit()
{
m_quit_request = true;
}
void SDLHostInterface::RunLater(std::function<void()> callback)
{
SDL_Event ev = {};
ev.type = SDL_USEREVENT;
ev.user.code = m_run_later_event_id;
ev.user.data1 = new std::function<void()>(std::move(callback));
SDL_PushEvent(&ev);
}
void SDLHostInterface::SaveAndUpdateSettings()
{
m_settings_copy.Save(*m_settings_interface.get());
UpdateSettings(*m_settings_interface.get());
m_settings_interface->Save();
}
bool SDLHostInterface::IsFullscreen() const
{
return m_fullscreen;
}
bool SDLHostInterface::SetFullscreen(bool enabled)
{
if (m_fullscreen == enabled)
return true;
SDL_SetWindowFullscreen(m_window, enabled ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
// We set the margin only in windowed mode, the menu bar is not drawn fullscreen.
m_display->SetDisplayTopMargin(enabled ? 0 : static_cast<int>(20.0f * ImGui::GetIO().DisplayFramebufferScale.x));
int window_width, window_height;
SDL_GetWindowSize(m_window, &window_width, &window_height);
m_display->WindowResized(window_width, window_height);
m_fullscreen = enabled;
return true;
}
std::unique_ptr<SDLHostInterface> SDLHostInterface::Create()
{
return std::make_unique<SDLHostInterface>();
}
bool SDLHostInterface::Initialize()
{
if (!CommonHostInterface::Initialize())
return false;
// Change to the user directory so that all default/relative paths in the config are after this.
if (!FileSystem::SetWorkingDirectory(m_user_directory.c_str()))
Log_ErrorPrintf("Failed to set working directory to '%s'", m_user_directory.c_str());
if (!CreateSDLWindow())
{
Log_ErrorPrintf("Failed to create SDL window");
return false;
}
CreateImGuiContext();
if (!CreateDisplay())
{
Log_ErrorPrintf("Failed to create host display");
return false;
}
RegisterHotkeys();
ImGui::NewFrame();
// process events to pick up controllers before updating input map
ProcessEvents();
UpdateInputMap();
return true;
}
void SDLHostInterface::Shutdown()
{
DestroySystem();
if (m_display)
{
DestroyDisplay();
ImGui::DestroyContext();
}
if (m_window)
DestroySDLWindow();
CommonHostInterface::Shutdown();
}
void SDLHostInterface::LoadSettings()
{
// Settings need to be loaded prior to creating the window for OpenGL bits.
m_settings_interface = std::make_unique<INISettingsInterface>(GetSettingsFileName());
ApplySettings(*m_settings_interface.get());
m_settings_copy = m_settings;
}
void SDLHostInterface::ReportError(const char* message)
{
const bool was_fullscreen = IsFullscreen();
if (was_fullscreen)
SetFullscreen(false);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "DuckStation", message, m_window);
if (was_fullscreen)
SetFullscreen(true);
}
void SDLHostInterface::ReportMessage(const char* message)
{
AddOSDMessage(message, 2.0f);
}
bool SDLHostInterface::ConfirmMessage(const char* message)
{
const bool was_fullscreen = IsFullscreen();
if (was_fullscreen)
SetFullscreen(false);
SDL_MessageBoxData mbd = {};
mbd.flags = SDL_MESSAGEBOX_INFORMATION;
mbd.window = m_window;
mbd.title = "DuckStation";
mbd.message = message;
mbd.numbuttons = 2;
// Why the heck these are reversed I have no idea...
SDL_MessageBoxButtonData buttons[2] = {};
buttons[1].flags = SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT;
buttons[1].buttonid = 0;
buttons[1].text = "Yes";
buttons[0].flags = SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT;
buttons[0].buttonid = 1;
buttons[0].text = "No";
mbd.buttons = buttons;
mbd.numbuttons = countof(buttons);
int button_id = 0;
SDL_ShowMessageBox(&mbd, &button_id);
const bool result = (button_id == 0);
if (was_fullscreen)
SetFullscreen(true);
return result;
}
void SDLHostInterface::HandleSDLEvent(const SDL_Event* event)
{
ImGui_ImplSDL2_ProcessEvent(event);
if (m_controller_interface &&
static_cast<SDLControllerInterface*>(m_controller_interface.get())->ProcessSDLEvent(event))
{
return;
}
switch (event->type)
{
case SDL_WINDOWEVENT:
{
if (event->window.event == SDL_WINDOWEVENT_RESIZED)
{
m_display->WindowResized(event->window.data1, event->window.data2);
UpdateFramebufferScale();
}
else if (event->window.event == SDL_WINDOWEVENT_MOVED)
{
UpdateFramebufferScale();
}
}
break;
case SDL_QUIT:
m_quit_request = true;
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
{
if (!ImGui::GetIO().WantCaptureKeyboard && event->key.repeat == 0)
{
const HostKeyCode code = static_cast<HostKeyCode>(static_cast<u32>(event->key.keysym.sym) |
static_cast<u32>(event->key.keysym.mod) << 16);
const bool pressed = (event->type == SDL_KEYDOWN);
HandleHostKeyEvent(code, pressed);
}
}
break;
case SDL_MOUSEMOTION:
{
m_display->SetMousePosition(event->motion.x, event->motion.y);
}
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
{
if (!ImGui::GetIO().WantCaptureMouse)
{
const s32 button = static_cast<s32>(ZeroExtend32(event->button.button));
const bool pressed = (event->type == SDL_MOUSEBUTTONDOWN);
HandleHostMouseEvent(button, pressed);
}
}
break;
case SDL_USEREVENT:
{
if (static_cast<u32>(event->user.code) == m_run_later_event_id)
{
std::function<void()>* callback = static_cast<std::function<void()>*>(event->user.data1);
Assert(callback);
(*callback)();
delete callback;
}
}
break;
}
}
void SDLHostInterface::PollAndUpdate()
{
CommonHostInterface::PollAndUpdate();
ProcessEvents();
}
void SDLHostInterface::ProcessEvents()
{
for (;;)
{
SDL_Event ev;
if (SDL_PollEvent(&ev))
HandleSDLEvent(&ev);
else
break;
}
}
void SDLHostInterface::DrawImGuiWindows()
{
if (!m_fullscreen)
DrawMainMenuBar();
CommonHostInterface::DrawImGuiWindows();
if (!m_system)
DrawPoweredOffWindow();
if (m_settings_window_open)
DrawSettingsWindow();
if (m_about_window_open)
DrawAboutWindow();
ImGui::Render();
}
void SDLHostInterface::DrawMainMenuBar()
{
if (!ImGui::BeginMainMenuBar())
return;
const bool system_enabled = static_cast<bool>(m_system);
if (ImGui::BeginMenu("System"))
{
if (ImGui::MenuItem("Start Disc", nullptr, false, !system_enabled))
{
RunLater([this]() { DoStartDisc(); });
ClearImGuiFocus();
}
if (ImGui::MenuItem("Start BIOS", nullptr, false, !system_enabled))
{
RunLater([this]() {
SystemBootParameters boot_params;
BootSystem(boot_params);
});
ClearImGuiFocus();
}
ImGui::Separator();
if (ImGui::MenuItem("Power Off", nullptr, false, system_enabled))
{
RunLater([this]() { DestroySystem(); });
ClearImGuiFocus();
}
if (ImGui::MenuItem("Reset", nullptr, false, system_enabled))
{
RunLater([this]() { ResetSystem(); });
ClearImGuiFocus();
}
if (ImGui::MenuItem("Pause", nullptr, m_paused, system_enabled))
{
RunLater([this]() { PauseSystem(!m_paused); });
ClearImGuiFocus();
}
ImGui::Separator();
if (ImGui::MenuItem("Change Disc", nullptr, false, system_enabled))
{
RunLater([this]() { DoChangeDisc(); });
ClearImGuiFocus();
}
if (ImGui::MenuItem("Frame Step", nullptr, false, system_enabled))
{
RunLater([this]() { DoFrameStep(); });
ClearImGuiFocus();
}
ImGui::Separator();
if (ImGui::BeginMenu("Load State"))
{
for (u32 i = 1; i <= GLOBAL_SAVE_STATE_SLOTS; i++)
{
char buf[16];
std::snprintf(buf, sizeof(buf), "State %u", i);
if (ImGui::MenuItem(buf))
{
RunLater([this, i]() { LoadState(true, i); });
ClearImGuiFocus();
}
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Save State", system_enabled))
{
for (u32 i = 1; i <= GLOBAL_SAVE_STATE_SLOTS; i++)
{
char buf[16];
std::snprintf(buf, sizeof(buf), "State %u", i);
if (ImGui::MenuItem(buf))
{
RunLater([this, i]() { SaveState(true, i); });
ClearImGuiFocus();
}
}
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem("Exit"))
m_quit_request = true;
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Settings"))
{
if (ImGui::MenuItem("Change Settings..."))
m_settings_window_open = true;
ImGui::Separator();
DrawQuickSettingsMenu();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Debug", system_enabled))
{
DrawDebugMenu();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help"))
{
if (ImGui::MenuItem("GitHub Repository"))
{
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Add URL Opener", "https://github.com/stenzek/duckstation",
m_window);
}
ImGui::Separator();
if (ImGui::MenuItem("About"))
m_about_window_open = true;
ImGui::EndMenu();
}
if (m_system)
{
const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x;
if (!m_paused)
{
ImGui::SetCursorPosX(ImGui::GetIO().DisplaySize.x - (210.0f * framebuffer_scale));
const float speed = m_system->GetEmulationSpeed();
const u32 rounded_speed = static_cast<u32>(std::round(speed));
if (speed < 90.0f)
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%u%%", rounded_speed);
else if (speed < 110.0f)
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "%u%%", rounded_speed);
else
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "%u%%", rounded_speed);
ImGui::SetCursorPosX(ImGui::GetIO().DisplaySize.x - (165.0f * framebuffer_scale));
ImGui::Text("FPS: %.2f", m_system->GetFPS());
ImGui::SetCursorPosX(ImGui::GetIO().DisplaySize.x - (80.0f * framebuffer_scale));
ImGui::Text("VPS: %.2f", m_system->GetVPS());
}
else
{
ImGui::SetCursorPosX(ImGui::GetIO().DisplaySize.x - (50.0f * framebuffer_scale));
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Paused");
}
}
ImGui::EndMainMenuBar();
}
void SDLHostInterface::DrawQuickSettingsMenu()
{
bool settings_changed = false;
settings_changed |= ImGui::MenuItem("Enable Speed Limiter", nullptr, &m_settings_copy.speed_limiter_enabled);
ImGui::Separator();
if (ImGui::BeginMenu("CPU Execution Mode"))
{
const CPUExecutionMode current = m_settings.cpu_execution_mode;
for (u32 i = 0; i < static_cast<u32>(CPUExecutionMode::Count); i++)
{
if (ImGui::MenuItem(Settings::GetCPUExecutionModeDisplayName(static_cast<CPUExecutionMode>(i)), nullptr,
i == static_cast<u32>(current)))
{
m_settings_copy.cpu_execution_mode = static_cast<CPUExecutionMode>(i);
settings_changed = true;
}
}
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::BeginMenu("Renderer"))
{
const GPURenderer current = m_settings_copy.gpu_renderer;
for (u32 i = 0; i < static_cast<u32>(GPURenderer::Count); i++)
{
if (ImGui::MenuItem(Settings::GetRendererDisplayName(static_cast<GPURenderer>(i)), nullptr,
i == static_cast<u32>(current)))
{
m_settings_copy.gpu_renderer = static_cast<GPURenderer>(i);
settings_changed = true;
}
}
ImGui::EndMenu();
}
bool fullscreen = m_fullscreen;
if (ImGui::MenuItem("Fullscreen", nullptr, &fullscreen))
RunLater([this, fullscreen] { SetFullscreen(fullscreen); });
settings_changed |= ImGui::MenuItem("VSync", nullptr, &m_settings_copy.video_sync_enabled);
ImGui::Separator();
if (ImGui::BeginMenu("Resolution Scale"))
{
const u32 current_internal_resolution = m_settings_copy.gpu_resolution_scale;
for (u32 scale = 1; scale <= GPU::MAX_RESOLUTION_SCALE; scale++)
{
char buf[32];
std::snprintf(buf, sizeof(buf), "%ux (%ux%u)", scale, scale * GPU::VRAM_WIDTH, scale * GPU::VRAM_HEIGHT);
if (ImGui::MenuItem(buf, nullptr, current_internal_resolution == scale))
{
m_settings_copy.gpu_resolution_scale = scale;
settings_changed = true;
}
}
ImGui::EndMenu();
}
settings_changed |= ImGui::MenuItem("True (24-Bit) Color", nullptr, &m_settings_copy.gpu_true_color);
settings_changed |= ImGui::MenuItem("Scaled Dithering", nullptr, &m_settings_copy.gpu_scaled_dithering);
settings_changed |= ImGui::MenuItem("Texture Filtering", nullptr, &m_settings_copy.gpu_texture_filtering);
settings_changed |= ImGui::MenuItem("Display Linear Filtering", nullptr, &m_settings_copy.display_linear_filtering);
settings_changed |= ImGui::MenuItem("Display Integer Scaling", nullptr, &m_settings_copy.display_integer_scaling);
ImGui::Separator();
if (ImGui::MenuItem("Dump Audio", nullptr, IsDumpingAudio(), HasSystem()))
{
if (!IsDumpingAudio())
StartDumpingAudio();
else
StopDumpingAudio();
}
if (ImGui::MenuItem("Save Screenshot"))
RunLater([this]() { SaveScreenshot(); });
if (settings_changed)
RunLater([this]() { SaveAndUpdateSettings(); });
}
void SDLHostInterface::DrawDebugMenu()
{
Settings::DebugSettings& debug_settings = m_settings.debugging;
bool settings_changed = false;
settings_changed |= ImGui::MenuItem("Dump CPU to VRAM Copies", nullptr, &debug_settings.dump_cpu_to_vram_copies);
settings_changed |= ImGui::MenuItem("Dump VRAM to CPU Copies", nullptr, &debug_settings.dump_vram_to_cpu_copies);
ImGui::Separator();
settings_changed |= ImGui::MenuItem("Show VRAM", nullptr, &debug_settings.show_vram);
settings_changed |= ImGui::MenuItem("Show GPU State", nullptr, &debug_settings.show_gpu_state);
settings_changed |= ImGui::MenuItem("Show CDROM State", nullptr, &debug_settings.show_cdrom_state);
settings_changed |= ImGui::MenuItem("Show SPU State", nullptr, &debug_settings.show_spu_state);
settings_changed |= ImGui::MenuItem("Show Timers State", nullptr, &debug_settings.show_timers_state);
settings_changed |= ImGui::MenuItem("Show MDEC State", nullptr, &debug_settings.show_mdec_state);
if (settings_changed)
{
// have to apply it to the copy too, otherwise it won't save
Settings::DebugSettings& debug_settings_copy = m_settings_copy.debugging;
debug_settings_copy.show_gpu_state = debug_settings.show_gpu_state;
debug_settings_copy.show_vram = debug_settings.show_vram;
debug_settings_copy.dump_cpu_to_vram_copies = debug_settings.dump_cpu_to_vram_copies;
debug_settings_copy.dump_vram_to_cpu_copies = debug_settings.dump_vram_to_cpu_copies;
debug_settings_copy.show_cdrom_state = debug_settings.show_cdrom_state;
debug_settings_copy.show_spu_state = debug_settings.show_spu_state;
debug_settings_copy.show_timers_state = debug_settings.show_timers_state;
debug_settings_copy.show_mdec_state = debug_settings.show_mdec_state;
SaveAndUpdateSettings();
}
}
void SDLHostInterface::DrawPoweredOffWindow()
{
static constexpr int WINDOW_WIDTH = 400;
static constexpr int WINDOW_HEIGHT = 650;
static constexpr int BUTTON_WIDTH = 200;
static constexpr int BUTTON_HEIGHT = 40;
const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x;
ImGui::SetNextWindowSize(ImVec2(static_cast<float>(WINDOW_WIDTH) * framebuffer_scale,
static_cast<float>(WINDOW_HEIGHT) * framebuffer_scale));
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f),
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
if (!ImGui::Begin("Powered Off", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoBringToFrontOnFocus))
{
ImGui::End();
}
ImGui::SetCursorPosX(static_cast<float>((WINDOW_WIDTH - APP_ICON_WIDTH) / 2) * framebuffer_scale);
ImGui::Image(m_app_icon_texture->GetHandle(), ImVec2(static_cast<float>(APP_ICON_WIDTH) * framebuffer_scale,
static_cast<float>(APP_ICON_HEIGHT) * framebuffer_scale));
ImGui::SetCursorPosY(static_cast<float>(APP_ICON_HEIGHT + 32) * framebuffer_scale);
const ImVec2 button_size(static_cast<float>(BUTTON_WIDTH) * framebuffer_scale,
static_cast<float>(BUTTON_HEIGHT) * framebuffer_scale);
const float button_left = static_cast<float>((WINDOW_WIDTH - BUTTON_WIDTH) / 2) * framebuffer_scale;
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * framebuffer_scale);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f * framebuffer_scale);
ImGui::PushStyleColor(ImGuiCol_Button, 0xFF202020);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, 0xFF808080);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, 0xFF575757);
ImGui::SetCursorPosX(button_left);
if (ImGui::Button("Resume", button_size))
{
RunLater([this]() { ResumeSystemFromMostRecentState(); });
ClearImGuiFocus();
}
ImGui::NewLine();
ImGui::SetCursorPosX(button_left);
if (ImGui::Button("Start Disc", button_size))
{
RunLater([this]() { DoStartDisc(); });
ClearImGuiFocus();
}
ImGui::NewLine();
ImGui::SetCursorPosX(button_left);
if (ImGui::Button("Start BIOS", button_size))
{
RunLater([this]() {
SystemBootParameters boot_params;
BootSystem(boot_params);
});
ClearImGuiFocus();
}
ImGui::NewLine();
ImGui::SetCursorPosX(button_left);
if (ImGui::Button("Load State", button_size))
ImGui::OpenPopup("PowerOffWindow_LoadStateMenu");
if (ImGui::BeginPopup("PowerOffWindow_LoadStateMenu"))
{
for (u32 i = 1; i <= GLOBAL_SAVE_STATE_SLOTS; i++)
{
char buf[16];
std::snprintf(buf, sizeof(buf), "State %u", i);
if (ImGui::MenuItem(buf))
{
RunLater([this, i]() { LoadState(true, i); });
ClearImGuiFocus();
}
}
ImGui::EndPopup();
}
ImGui::NewLine();
ImGui::SetCursorPosX(button_left);
if (ImGui::Button("Settings", button_size))
m_settings_window_open = true;
ImGui::NewLine();
ImGui::SetCursorPosX(button_left);
if (ImGui::Button("Exit", button_size))
m_quit_request = true;
ImGui::NewLine();
ImGui::PopStyleColor(3);
ImGui::PopStyleVar(2);
ImGui::End();
}
static bool DrawSettingsSectionHeader(const char* title)
{
return ImGui::CollapsingHeader(title, ImGuiTreeNodeFlags_DefaultOpen /* | ImGuiTreeNodeFlags_Leaf*/);
}
void SDLHostInterface::DrawSettingsWindow()
{
const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x;
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f),
ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(500 * framebuffer_scale, 400.0f * framebuffer_scale), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Settings", &m_settings_window_open, ImGuiWindowFlags_NoResize))
{
ImGui::End();
return;
}
bool settings_changed = false;
if (ImGui::BeginTabBar("SettingsTabBar", 0))
{
const float indent = 150.0f * framebuffer_scale;
if (ImGui::BeginTabItem("General"))
{
if (DrawSettingsSectionHeader("Console"))
{
ImGui::Text("Region:");
ImGui::SameLine(indent);
int region = static_cast<int>(m_settings_copy.region);
if (ImGui::Combo(
"##region", &region,
[](void*, int index, const char** out_text) {
*out_text = Settings::GetConsoleRegionDisplayName(static_cast<ConsoleRegion>(index));
return true;
},
nullptr, static_cast<int>(ConsoleRegion::Count)))
{
m_settings_copy.region = static_cast<ConsoleRegion>(region);
settings_changed = true;
}
ImGui::Text("BIOS Path:");
ImGui::SameLine(indent);
settings_changed |= DrawFileChooser("##bios_path", &m_settings_copy.bios_path);
settings_changed |= ImGui::Checkbox("Enable TTY Output", &m_settings_copy.bios_patch_tty_enable);
settings_changed |= ImGui::Checkbox("Fast Boot", &m_settings_copy.bios_patch_fast_boot);
}
ImGui::NewLine();
if (DrawSettingsSectionHeader("Behavior"))
{
ImGui::Text("Emulation Speed:");
ImGui::SameLine(indent);
settings_changed |= ImGui::SliderFloat("##speed", &m_settings_copy.emulation_speed, 0.25f, 5.0f);
settings_changed |= ImGui::Checkbox("Enable Speed Limiter", &m_settings_copy.speed_limiter_enabled);
settings_changed |= ImGui::Checkbox("Increase Timer Resolution", &m_settings_copy.increase_timer_resolution);
settings_changed |= ImGui::Checkbox("Pause On Start", &m_settings_copy.start_paused);
settings_changed |= ImGui::Checkbox("Start Fullscreen", &m_settings_copy.start_fullscreen);
settings_changed |= ImGui::Checkbox("Save State On Exit", &m_settings_copy.save_state_on_exit);
}
ImGui::NewLine();
if (DrawSettingsSectionHeader("CDROM Emulation"))
{
settings_changed |= ImGui::Checkbox("Use Read Thread (Asynchronous)", &m_settings_copy.cdrom_read_thread);
settings_changed |= ImGui::Checkbox("Enable Region Check", &m_settings_copy.cdrom_region_check);
}
ImGui::NewLine();
if (DrawSettingsSectionHeader("Audio"))
{
ImGui::Text("Backend:");
ImGui::SameLine(indent);
int backend = static_cast<int>(m_settings_copy.audio_backend);
if (ImGui::Combo(
"##backend", &backend,
[](void*, int index, const char** out_text) {
*out_text = Settings::GetAudioBackendDisplayName(static_cast<AudioBackend>(index));
return true;
},
nullptr, static_cast<int>(AudioBackend::Count)))
{
m_settings_copy.audio_backend = static_cast<AudioBackend>(backend);
settings_changed = true;
}
settings_changed |= ImGui::Checkbox("Output Sync", &m_settings_copy.audio_sync_enabled);
settings_changed |= ImGui::Checkbox("Start Dumping On Boot", &m_settings_copy.audio_dump_on_boot);
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Ports"))
{
for (int i = 0; i < 2; i++)
{
char buf[32];
std::snprintf(buf, sizeof(buf), "Front Port %d", 1 + i);
if (DrawSettingsSectionHeader(buf))
{
ImGui::Text("Controller:");
ImGui::SameLine(indent);
int controller_type = static_cast<int>(m_settings_copy.controller_types[i]);
if (ImGui::Combo(
"##controller_type", &controller_type,
[](void*, int index, const char** out_text) {
*out_text = Settings::GetControllerTypeDisplayName(static_cast<ControllerType>(index));
return true;
},
nullptr, static_cast<int>(ControllerType::Count)))
{
m_settings_copy.controller_types[i] = static_cast<ControllerType>(controller_type);
settings_changed = true;
}
ImGui::Text("Memory Card Type:");
ImGui::SameLine(indent);
int memory_card_type = static_cast<int>(m_settings_copy.memory_card_types[i]);
if (ImGui::Combo(
"##memory_card_type", &memory_card_type,
[](void*, int index, const char** out_text) {
*out_text = Settings::GetMemoryCardTypeDisplayName(static_cast<MemoryCardType>(index));
return true;
},
nullptr, static_cast<int>(MemoryCardType::Count)))
{
m_settings_copy.memory_card_types[i] = static_cast<MemoryCardType>(memory_card_type);
settings_changed = true;
}
ImGui::Text("Shared Card Path:");
ImGui::SameLine(indent);
std::string* path_ptr = &m_settings_copy.memory_card_paths[i];
std::snprintf(buf, sizeof(buf), "##memcard_%c_path", 'a' + i);
settings_changed |= DrawFileChooser(buf, path_ptr);
}
ImGui::NewLine();
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("CPU"))
{
ImGui::Text("Execution Mode:");
ImGui::SameLine(indent);
int execution_mode = static_cast<int>(m_settings_copy.cpu_execution_mode);
if (ImGui::Combo(
"##execution_mode", &execution_mode,
[](void*, int index, const char** out_text) {
*out_text = Settings::GetCPUExecutionModeDisplayName(static_cast<CPUExecutionMode>(index));
return true;
},
nullptr, static_cast<int>(CPUExecutionMode::Count)))
{
m_settings_copy.cpu_execution_mode = static_cast<CPUExecutionMode>(execution_mode);
settings_changed = true;
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("GPU"))
{
if (DrawSettingsSectionHeader("Basic"))
{
ImGui::Text("Renderer:");
ImGui::SameLine(indent);
int gpu_renderer = static_cast<int>(m_settings_copy.gpu_renderer);
if (ImGui::Combo(
"##gpu_renderer", &gpu_renderer,
[](void*, int index, const char** out_text) {
*out_text = Settings::GetRendererDisplayName(static_cast<GPURenderer>(index));
return true;
},
nullptr, static_cast<int>(GPURenderer::Count)))
{
m_settings_copy.gpu_renderer = static_cast<GPURenderer>(gpu_renderer);
settings_changed = true;
}
}
ImGui::NewLine();
if (DrawSettingsSectionHeader("Display Output"))
{
ImGui::Text("Aspect Ratio:");
ImGui::SameLine(indent);
int display_aspect_ratio = static_cast<int>(m_settings_copy.display_aspect_ratio);
if (ImGui::Combo(
"##display_pixel_aspect_ratio", &display_aspect_ratio,
[](void*, int index, const char** out_text) {
*out_text = Settings::GetDisplayAspectRatioName(static_cast<DisplayAspectRatio>(index));
return true;
},
nullptr, static_cast<int>(DisplayAspectRatio::Count)))
{
m_settings_copy.display_aspect_ratio = static_cast<DisplayAspectRatio>(display_aspect_ratio);
settings_changed = true;
}
ImGui::Text("Crop:");
ImGui::SameLine(indent);
int display_crop_mode = static_cast<int>(m_settings_copy.display_crop_mode);
if (ImGui::Combo(
"##display_crop_mode", &display_crop_mode,
[](void*, int index, const char** out_text) {
*out_text = Settings::GetDisplayCropModeDisplayName(static_cast<DisplayCropMode>(index));
return true;
},
nullptr, static_cast<int>(DisplayCropMode::Count)))
{
m_settings_copy.display_crop_mode = static_cast<DisplayCropMode>(display_crop_mode);
settings_changed = true;
}
settings_changed |= ImGui::Checkbox("Use Debug Device", &m_settings_copy.gpu_use_debug_device);
settings_changed |= ImGui::Checkbox("Linear Filtering", &m_settings_copy.display_linear_filtering);
settings_changed |= ImGui::Checkbox("Integer Scaling", &m_settings_copy.display_integer_scaling);
settings_changed |= ImGui::Checkbox("VSync", &m_settings_copy.video_sync_enabled);
}
ImGui::NewLine();
if (DrawSettingsSectionHeader("Enhancements"))
{
ImGui::Text("Resolution Scale:");
ImGui::SameLine(indent);
static constexpr std::array<const char*, GPU::MAX_RESOLUTION_SCALE> resolutions = {{
"1x (1024x512)",
"2x (2048x1024)",
"3x (3072x1536)",
"4x (4096x2048)",
"5x (5120x2560)",
"6x (6144x3072)",
"7x (7168x3584)",
"8x (8192x4096)",
"9x (9216x4608)",
"10x (10240x5120)",
"11x (11264x5632)",
"12x (12288x6144)",
"13x (13312x6656)",
"14x (14336x7168)",
"15x (15360x7680)",
"16x (16384x8192)",
}};
int current_resolution_index = static_cast<int>(m_settings_copy.gpu_resolution_scale) - 1;
if (ImGui::Combo("##gpu_resolution_scale", &current_resolution_index, resolutions.data(),
static_cast<int>(resolutions.size())))
{
m_settings_copy.gpu_resolution_scale = static_cast<u32>(current_resolution_index + 1);
settings_changed = true;
}
settings_changed |= ImGui::Checkbox("True 24-bit Color (disables dithering)", &m_settings_copy.gpu_true_color);
settings_changed |= ImGui::Checkbox("Texture Filtering", &m_settings_copy.gpu_texture_filtering);
settings_changed |= ImGui::Checkbox("Disable Interlacing", &m_settings_copy.gpu_disable_interlacing);
settings_changed |= ImGui::Checkbox("Force NTSC Timings", &m_settings_copy.gpu_force_ntsc_timings);
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Advanced"))
{
ImGui::Text("These options are tweakable to improve performance/game compatibility.");
ImGui::Text("They will not be automatically saved to the settings INI file.");
ImGui::Text("Use at your own risk, modified values will not be supported.");
ImGui::NewLine();
ImGui::Text("DMA Max Slice Ticks:");
ImGui::SameLine(indent);
int dma_max_slice_ticks = static_cast<int>(m_settings_copy.dma_max_slice_ticks);
if (ImGui::SliderInt("##dma_max_slice_ticks", &dma_max_slice_ticks, 100, 10000))
{
m_settings_copy.dma_max_slice_ticks = dma_max_slice_ticks;
settings_changed = true;
}
ImGui::Text("DMA Halt Ticks:");
ImGui::SameLine(indent);
int dma_halt_ticks = static_cast<int>(m_settings_copy.dma_halt_ticks);
if (ImGui::SliderInt("##dma_halt_ticks", &dma_halt_ticks, 100, 10000))
{
m_settings_copy.dma_halt_ticks = dma_halt_ticks;
settings_changed = true;
}
ImGui::Text("FIFO Size:");
ImGui::SameLine(indent);
int gpu_fifo_size = static_cast<int>(m_settings_copy.gpu_fifo_size);
if (ImGui::SliderInt("##gpu_fifo_size", &gpu_fifo_size, 16, GPU::MAX_FIFO_SIZE))
{
m_settings_copy.gpu_fifo_size = gpu_fifo_size;
settings_changed = true;
}
ImGui::Text("Max Run-Ahead:");
ImGui::SameLine(indent);
int gpu_max_run_ahead = static_cast<int>(m_settings_copy.gpu_max_run_ahead);
if (ImGui::SliderInt("##gpu_max_run_ahead", &gpu_max_run_ahead, 0, 1000))
{
m_settings_copy.gpu_max_run_ahead = gpu_max_run_ahead;
settings_changed = true;
}
if (ImGui::Button("Reset"))
{
m_settings_copy.dma_max_slice_ticks = static_cast<TickCount>(Settings::DEFAULT_DMA_MAX_SLICE_TICKS);
m_settings_copy.dma_halt_ticks = static_cast<TickCount>(Settings::DEFAULT_DMA_HALT_TICKS);
m_settings_copy.gpu_fifo_size = Settings::DEFAULT_GPU_FIFO_SIZE;
m_settings_copy.gpu_max_run_ahead = static_cast<TickCount>(Settings::DEFAULT_GPU_MAX_RUN_AHEAD);
settings_changed = true;
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
if (settings_changed)
RunLater([this]() { SaveAndUpdateSettings(); });
}
void SDLHostInterface::DrawAboutWindow()
{
const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x;
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::OpenPopup("About DuckStation");
if (!ImGui::BeginPopupModal("About DuckStation", &m_about_window_open, ImGuiWindowFlags_NoResize))
return;
ImGui::Text("DuckStation");
ImGui::NewLine();
ImGui::Text("Authors:");
ImGui::Text(" Connor McLaughlin <stenzek@gmail.com>");
ImGui::NewLine();
ImGui::Text("Uses Dear ImGui (https://github.com/ocornut/imgui)");
ImGui::Text("Uses libcue (https://github.com/lipnitsk/libcue)");
ImGui::Text("Uses stb_image_write (https://github.com/nothings/stb)");
ImGui::Text("Uses simpleini (https://github.com/brofield/simpleini)");
ImGui::NewLine();
ImGui::Text("Duck icon by icons8 (https://icons8.com/icon/74847/platforms.undefined.short-title)");
ImGui::NewLine();
ImGui::SetCursorPosX((ImGui::GetWindowSize().x - (60.0f * framebuffer_scale)) / 2.0f);
if (ImGui::Button("Close", ImVec2(60.0f * framebuffer_scale, 20.0f * framebuffer_scale)))
m_about_window_open = false;
ImGui::EndPopup();
}
bool SDLHostInterface::DrawFileChooser(const char* label, std::string* path, const char* filter /* = nullptr */)
{
const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x;
ImGui::SetNextItemWidth(ImGui::CalcItemWidth() - (50.0f * framebuffer_scale));
bool result = ImGui::InputText(label, path);
ImGui::SameLine();
ImGui::SetNextItemWidth(50.0f * framebuffer_scale);
if (ImGui::Button("..."))
{
nfdchar_t* out_path = nullptr;
nfdresult_t nfd_result = NFD_OpenDialog(filter, path->c_str(), &out_path);
if (nfd_result == NFD_ERROR)
{
// try without the path - it might not be valid
nfd_result = NFD_OpenDialog(filter, nullptr, &out_path);
}
if (nfd_result == NFD_OKAY)
{
path->assign(out_path);
result = true;
}
}
return result;
}
void SDLHostInterface::ClearImGuiFocus()
{
ImGui::SetWindowFocus(nullptr);
}
void SDLHostInterface::RegisterHotkeys()
{
RegisterHotkey(StaticString("General"), StaticString("FrameStep"), StaticString("Frame Step"), [this](bool pressed) {
if (!pressed)
{
DoFrameStep();
}
});
}
void SDLHostInterface::DoStartDisc()
{
Assert(!m_system);
nfdchar_t* path = nullptr;
if (!NFD_OpenDialog("bin,img,cue,chd,exe,psexe,psf", nullptr, &path) || !path || std::strlen(path) == 0)
return;
AddFormattedOSDMessage(2.0f, "Starting disc from '%s'...", path);
SystemBootParameters boot_params;
boot_params.filename = path;
BootSystem(boot_params);
}
void SDLHostInterface::DoChangeDisc()
{
Assert(m_system);
nfdchar_t* path = nullptr;
if (!NFD_OpenDialog("bin,img,cue,chd", nullptr, &path) || !path || std::strlen(path) == 0)
return;
if (m_system->InsertMedia(path))
AddFormattedOSDMessage(2.0f, "Switched CD to '%s'", path);
else
AddOSDMessage("Failed to switch CD. The log may contain further information.");
m_system->ResetPerformanceCounters();
}
void SDLHostInterface::DoFrameStep()
{
if (!m_system)
return;
m_frame_step_request = true;
m_paused = false;
}
void SDLHostInterface::Run()
{
while (!m_quit_request)
{
PollAndUpdate();
if (m_system && !m_paused)
{
m_system->RunFrame();
UpdateControllerRumble();
if (m_frame_step_request)
{
m_frame_step_request = false;
PauseSystem(true);
}
}
// rendering
{
DrawImGuiWindows();
if (m_system)
m_system->GetGPU()->ResetGraphicsAPIState();
m_display->Render();
if (m_system)
{
m_system->GetGPU()->RestoreGraphicsAPIState();
m_system->UpdatePerformanceCounters();
if (m_speed_limiter_enabled)
m_system->Throttle();
}
}
}
// Save state on exit so it can be resumed
if (m_system)
{
if (m_settings.save_state_on_exit)
SaveResumeSaveState();
DestroySystem();
}
}