feat: add ImGui frontend support with game selection and debug panels
This commit is contained in:
@@ -70,6 +70,13 @@ option(IOS_SIMULATOR_BUILD "Compiling for IOS simulator (Set to off if compiling
|
||||
option(UWP_BUILD "Build as a UWP application for use on Windows/Xbox." OFF)
|
||||
option(IMGUI_FRONTEND "Build the imgui frontend. Forces OpenGL. " OFF)
|
||||
|
||||
if(IMGUI_FRONTEND)
|
||||
set(ENABLE_OPENGL ON)
|
||||
set(ENABLE_VULKAN OFF)
|
||||
set(ENABLE_METAL OFF)
|
||||
add_compile_definitions(IMGUI_FRONTEND IMGUI_IMPL_OPENGL_LOADER_GLAD2)
|
||||
endif()
|
||||
|
||||
if(UWP_BUILD)
|
||||
set(MINGW_UWP_DIR "${CMAKE_CURRENT_LIST_DIR}/uwp")
|
||||
set(ENABLE_OPENGL ON)
|
||||
@@ -889,9 +896,26 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE)
|
||||
else()
|
||||
set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp src/panda_sdl/mappings.cpp)
|
||||
set(FRONTEND_HEADER_FILES "include/panda_sdl/frontend_sdl.hpp")
|
||||
|
||||
if(IMGUI_FRONTEND)
|
||||
list(APPEND FRONTEND_SOURCE_FILES
|
||||
src/panda_sdl/imgui_layer.cpp
|
||||
third_party/imgui/backends/imgui_impl_sdl.cpp
|
||||
third_party/imgui/backends/imgui_impl_opengl3.cpp
|
||||
)
|
||||
list(APPEND FRONTEND_HEADER_FILES
|
||||
"include/panda_sdl/imgui_layer.hpp"
|
||||
)
|
||||
if(WIN32)
|
||||
list(APPEND FRONTEND_LIBRARIES imm32)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_link_libraries(Alber PRIVATE AlberCore)
|
||||
if(FRONTEND_LIBRARIES)
|
||||
target_link_libraries(Alber PRIVATE ${FRONTEND_LIBRARIES})
|
||||
endif()
|
||||
target_sources(Alber PRIVATE ${FRONTEND_SOURCE_FILES} ${FRONTEND_HEADER_FILES} ${GL_CONTEXT_SOURCE_FILES} ${APP_RESOURCES})
|
||||
elseif(BUILD_HYDRA_CORE)
|
||||
target_compile_definitions(AlberCore PRIVATE PANDA3DS_HYDRA_CORE=1)
|
||||
|
||||
@@ -27,6 +27,7 @@ struct FrontendSettings {
|
||||
Theme theme = Theme::Dark;
|
||||
WindowIcon icon = WindowIcon::Rpog;
|
||||
std::string language = "en";
|
||||
bool showImGuiDebugPanel = true;
|
||||
|
||||
static Theme themeFromString(std::string inString);
|
||||
static const char* themeToString(Theme theme);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <SDL.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "emulator.hpp"
|
||||
#include "input_mappings.hpp"
|
||||
@@ -13,11 +15,26 @@ class FrontendSDL {
|
||||
#ifdef PANDA3DS_ENABLE_OPENGL
|
||||
SDL_GLContext glContext;
|
||||
#endif
|
||||
#ifdef IMGUI_FRONTEND
|
||||
std::unique_ptr<class ImGuiLayer> imgui;
|
||||
#endif
|
||||
|
||||
public:
|
||||
FrontendSDL();
|
||||
#ifdef IMGUI_FRONTEND
|
||||
FrontendSDL(SDL_Window* existingWindow, SDL_GLContext existingContext);
|
||||
#endif
|
||||
~FrontendSDL();
|
||||
#ifdef IMGUI_FRONTEND
|
||||
struct ImGuiWindowContext {
|
||||
SDL_Window* window = nullptr;
|
||||
SDL_GLContext context = nullptr;
|
||||
};
|
||||
static ImGuiWindowContext createImGuiWindowContext(const EmulatorConfig& bootConfig, const char* windowTitle);
|
||||
#endif
|
||||
bool loadROM(const std::filesystem::path& path);
|
||||
void run();
|
||||
std::optional<std::filesystem::path> selectGame();
|
||||
u32 getMapping(InputMappings::Scancode scancode) { return keyboardMappings.getMapping(scancode); }
|
||||
|
||||
SDL_Window* window = nullptr;
|
||||
@@ -42,8 +59,12 @@ class FrontendSDL {
|
||||
// And so the user can still use the keyboard to control the analog
|
||||
bool keyboardAnalogX = false;
|
||||
bool keyboardAnalogY = false;
|
||||
bool emuPaused = false;
|
||||
|
||||
private:
|
||||
void setupControllerSensors(SDL_GameController* controller);
|
||||
void handleLeftClick(int mouseX, int mouseY);
|
||||
void setPaused(bool paused);
|
||||
void togglePaused();
|
||||
void initialize(SDL_Window* existingWindow, SDL_GLContext existingContext, bool useExternalContext);
|
||||
};
|
||||
50
include/panda_sdl/imgui_layer.hpp
Normal file
50
include/panda_sdl/imgui_layer.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef IMGUI_FRONTEND
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
#include "emulator.hpp"
|
||||
|
||||
class ImGuiLayer {
|
||||
public:
|
||||
ImGuiLayer(SDL_Window* window, SDL_GLContext context, Emulator& emu);
|
||||
void init();
|
||||
void shutdown();
|
||||
void processEvent(const SDL_Event& event);
|
||||
void beginFrame();
|
||||
void render();
|
||||
void handleHotkey(const SDL_Event& event);
|
||||
std::optional<std::filesystem::path> runGameSelector();
|
||||
|
||||
bool wantsCaptureKeyboard() const { return captureKeyboard; }
|
||||
bool wantsCaptureMouse() const { return captureMouse; }
|
||||
|
||||
void setPaused(bool paused) { isPaused = paused; }
|
||||
void setPauseCallback(std::function<void(bool)> callback) { onPauseChange = std::move(callback); }
|
||||
void setVsyncCallback(std::function<void(bool)> callback) { onVsyncChange = std::move(callback); }
|
||||
|
||||
private:
|
||||
void drawDebugPanel();
|
||||
void drawPausePanel();
|
||||
void drawSettingsPanel();
|
||||
|
||||
SDL_Window* window = nullptr;
|
||||
SDL_GLContext glContext = nullptr;
|
||||
Emulator& emu;
|
||||
|
||||
bool showDebug = true;
|
||||
bool showPauseMenu = false;
|
||||
bool showSettings = false;
|
||||
bool isPaused = false;
|
||||
bool captureKeyboard = false;
|
||||
bool captureMouse = false;
|
||||
|
||||
std::function<void(bool)> onPauseChange;
|
||||
std::function<void(bool)> onVsyncChange;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -152,6 +152,7 @@ void EmulatorConfig::load() {
|
||||
frontendSettings.theme = FrontendSettings::themeFromString(toml::find_or<std::string>(ui, "Theme", "dark"));
|
||||
frontendSettings.icon = FrontendSettings::iconFromString(toml::find_or<std::string>(ui, "WindowIcon", "rpog"));
|
||||
frontendSettings.language = toml::find_or<std::string>(ui, "Language", "en");
|
||||
frontendSettings.showImGuiDebugPanel = toml::find_or<toml::boolean>(ui, "ShowImGuiDebugPanel", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,6 +221,7 @@ void EmulatorConfig::save() {
|
||||
data["UI"]["Theme"] = std::string(FrontendSettings::themeToString(frontendSettings.theme));
|
||||
data["UI"]["WindowIcon"] = std::string(FrontendSettings::iconToString(frontendSettings.icon));
|
||||
data["UI"]["Language"] = frontendSettings.language;
|
||||
data["UI"]["ShowImGuiDebugPanel"] = frontendSettings.showImGuiDebugPanel;
|
||||
|
||||
std::ofstream file(path, std::ios::out);
|
||||
file << data;
|
||||
|
||||
@@ -6,7 +6,67 @@
|
||||
#include "sdl_sensors.hpp"
|
||||
#include "version.hpp"
|
||||
|
||||
#ifdef IMGUI_FRONTEND
|
||||
#include "panda_sdl/imgui_layer.hpp"
|
||||
#endif
|
||||
|
||||
FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMappings()) {
|
||||
initialize(nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
#ifdef IMGUI_FRONTEND
|
||||
FrontendSDL::FrontendSDL(SDL_Window* existingWindow, SDL_GLContext existingContext) : keyboardMappings(InputMappings::defaultKeyboardMappings()) {
|
||||
initialize(existingWindow, existingContext, true);
|
||||
}
|
||||
|
||||
FrontendSDL::ImGuiWindowContext FrontendSDL::createImGuiWindowContext(const EmulatorConfig& bootConfig, const char* windowTitle) {
|
||||
int windowX = SDL_WINDOWPOS_CENTERED;
|
||||
int windowY = SDL_WINDOWPOS_CENTERED;
|
||||
int windowW = 400;
|
||||
int windowH = 480;
|
||||
if (bootConfig.windowSettings.rememberPosition) {
|
||||
windowX = bootConfig.windowSettings.x;
|
||||
windowY = bootConfig.windowSettings.y;
|
||||
windowW = bootConfig.windowSettings.width;
|
||||
windowH = bootConfig.windowSettings.height;
|
||||
}
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
|
||||
SDL_Window* window = SDL_CreateWindow(windowTitle, windowX, windowY, windowW, windowH, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
|
||||
if (!window) {
|
||||
Helpers::panic("Window creation failed: %s", SDL_GetError());
|
||||
}
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
printf("[IMGUI] SDL_CreateWindow -> %p\n", window);
|
||||
#endif
|
||||
|
||||
SDL_GLContext glContext = SDL_GL_CreateContext(window);
|
||||
if (!glContext) {
|
||||
Helpers::panic("OpenGL context creation failed: %s", SDL_GetError());
|
||||
}
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
printf("[IMGUI] SDL_GL_CreateContext -> %p\n", glContext);
|
||||
#endif
|
||||
|
||||
if (SDL_GL_MakeCurrent(window, glContext) != 0) {
|
||||
Helpers::panic("SDL_GL_MakeCurrent failed: %s", SDL_GetError());
|
||||
}
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
printf("[IMGUI] SDL_GL_MakeCurrent OK. Current window=%p context=%p\n", SDL_GL_GetCurrentWindow(), SDL_GL_GetCurrentContext());
|
||||
#endif
|
||||
|
||||
if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
|
||||
Helpers::panic("OpenGL init failed");
|
||||
}
|
||||
SDL_GL_SetSwapInterval(bootConfig.vsyncEnabled ? 1 : 0);
|
||||
|
||||
return {window, glContext};
|
||||
}
|
||||
#endif
|
||||
|
||||
void FrontendSDL::initialize(SDL_Window* existingWindow, SDL_GLContext existingContext, bool useExternalContext) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) {
|
||||
Helpers::panic("Failed to initialize SDL2");
|
||||
}
|
||||
@@ -28,7 +88,10 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp
|
||||
setupControllerSensors(gameController);
|
||||
}
|
||||
|
||||
const EmulatorConfig& config = emu.getConfig();
|
||||
EmulatorConfig& config = emu.getConfig();
|
||||
#ifdef IMGUI_FRONTEND
|
||||
config.rendererType = RendererType::OpenGL;
|
||||
#endif
|
||||
// We need OpenGL for software rendering/null renderer or for the OpenGL renderer if it's enabled.
|
||||
bool needOpenGL = config.rendererType == RendererType::Software || config.rendererType == RendererType::Null;
|
||||
#ifdef PANDA3DS_ENABLE_OPENGL
|
||||
@@ -63,6 +126,72 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp
|
||||
);
|
||||
|
||||
if (needOpenGL) {
|
||||
#ifdef IMGUI_FRONTEND
|
||||
// IMGUI_FRONTEND must reuse an existing GL 4.1 context and window.
|
||||
if (!useExternalContext || existingWindow == nullptr || existingContext == nullptr) {
|
||||
existingWindow = SDL_GL_GetCurrentWindow();
|
||||
existingContext = SDL_GL_GetCurrentContext();
|
||||
useExternalContext = existingWindow != nullptr && existingContext != nullptr;
|
||||
}
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
printf("[IMGUI] FrontendSDL init: existingWindow=%p existingContext=%p currentWindow=%p currentContext=%p\n",
|
||||
existingWindow, existingContext, SDL_GL_GetCurrentWindow(), SDL_GL_GetCurrentContext());
|
||||
#endif
|
||||
if (!useExternalContext) {
|
||||
Helpers::panic("IMGUI_FRONTEND requires an existing OpenGL window/context");
|
||||
}
|
||||
|
||||
window = existingWindow;
|
||||
glContext = existingContext;
|
||||
SDL_GetWindowSize(window, reinterpret_cast<int*>(&windowWidth), reinterpret_cast<int*>(&windowHeight));
|
||||
|
||||
if (SDL_GL_MakeCurrent(window, glContext) != 0) {
|
||||
Helpers::panic("SDL_GL_MakeCurrent failed: %s", SDL_GetError());
|
||||
}
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
printf("[IMGUI] SDL_GL_MakeCurrent OK. Current window=%p context=%p\n", SDL_GL_GetCurrentWindow(), SDL_GL_GetCurrentContext());
|
||||
#endif
|
||||
|
||||
if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
printf("[IMGUI] gladLoadGLLoader failed\n");
|
||||
#endif
|
||||
Helpers::panic("OpenGL init failed");
|
||||
}
|
||||
|
||||
int major = 0;
|
||||
int minor = 0;
|
||||
glGetIntegerv(GL_MAJOR_VERSION, &major);
|
||||
glGetIntegerv(GL_MINOR_VERSION, &minor);
|
||||
const GLubyte* renderer = glGetString(GL_RENDERER);
|
||||
const GLubyte* version = glGetString(GL_VERSION);
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
printf("[IMGUI] GL version %d.%d, renderer=%s, versionStr=%s\n", major, minor,
|
||||
reinterpret_cast<const char*>(renderer ? renderer : reinterpret_cast<const GLubyte*>("(null)")),
|
||||
reinterpret_cast<const char*>(version ? version : reinterpret_cast<const GLubyte*>("(null)")));
|
||||
#endif
|
||||
if (major < 4 || (major == 4 && minor < 1)) {
|
||||
Helpers::panic("IMGUI_FRONTEND requires a single OpenGL 4.1+ core context (got %d.%d)", major, minor);
|
||||
}
|
||||
|
||||
SDL_GL_SetSwapInterval(config.vsyncEnabled ? 1 : 0);
|
||||
#else
|
||||
bool usingGLES = false;
|
||||
if (useExternalContext && existingWindow && existingContext) {
|
||||
window = existingWindow;
|
||||
glContext = existingContext;
|
||||
SDL_GetWindowSize(window, reinterpret_cast<int*>(&windowWidth), reinterpret_cast<int*>(&windowHeight));
|
||||
|
||||
if (SDL_GL_MakeCurrent(window, glContext) != 0) {
|
||||
Helpers::panic("SDL_GL_MakeCurrent failed: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
|
||||
Helpers::panic("OpenGL init failed");
|
||||
}
|
||||
|
||||
SDL_GL_SetSwapInterval(config.vsyncEnabled ? 1 : 0);
|
||||
} else {
|
||||
// Demand 4.1 core for OpenGL renderer (max available on MacOS), 3.3 for the software & null renderers
|
||||
// MacOS gets mad if we don't explicitly demand a core profile
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
@@ -91,16 +220,24 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp
|
||||
if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
|
||||
Helpers::panic("OpenGL init failed");
|
||||
}
|
||||
|
||||
emu.getRenderer()->setupGLES();
|
||||
} else {
|
||||
usingGLES = true;
|
||||
}
|
||||
|
||||
if (!usingGLES) {
|
||||
if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
|
||||
Helpers::panic("OpenGL init failed");
|
||||
}
|
||||
}
|
||||
|
||||
if (SDL_GL_MakeCurrent(window, glContext) != 0) {
|
||||
Helpers::panic("SDL_GL_MakeCurrent failed: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
SDL_GL_SetSwapInterval(config.vsyncEnabled ? 1 : 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef PANDA3DS_ENABLE_VULKAN
|
||||
if (config.rendererType == RendererType::Vulkan) {
|
||||
@@ -123,10 +260,26 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp
|
||||
#endif
|
||||
|
||||
emu.initGraphicsContext(window);
|
||||
|
||||
#ifdef IMGUI_FRONTEND
|
||||
imgui = std::make_unique<ImGuiLayer>(window, glContext, emu);
|
||||
imgui->init();
|
||||
imgui->setPauseCallback([this](bool paused) { setPaused(paused); });
|
||||
imgui->setVsyncCallback([this](bool enabled) { SDL_GL_SetSwapInterval(enabled ? 1 : 0); });
|
||||
#endif
|
||||
}
|
||||
|
||||
bool FrontendSDL::loadROM(const std::filesystem::path& path) { return emu.loadROM(path); }
|
||||
|
||||
std::optional<std::filesystem::path> FrontendSDL::selectGame() {
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui) {
|
||||
return imgui->runGameSelector();
|
||||
}
|
||||
#endif
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void FrontendSDL::run() {
|
||||
programRunning = true;
|
||||
keyboardAnalogX = false;
|
||||
@@ -143,6 +296,13 @@ void FrontendSDL::run() {
|
||||
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui) {
|
||||
imgui->processEvent(event);
|
||||
imgui->handleHotkey(event);
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace Keys = HID::Keys;
|
||||
|
||||
switch (event.type) {
|
||||
@@ -154,10 +314,21 @@ void FrontendSDL::run() {
|
||||
auto& windowSettings = emu.getConfig().windowSettings;
|
||||
SDL_GetWindowPosition(window, &windowSettings.x, &windowSettings.y);
|
||||
SDL_GetWindowSize(window, &windowSettings.width, &windowSettings.height);
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui) {
|
||||
imgui->shutdown();
|
||||
imgui.reset();
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
case SDL_KEYDOWN: {
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui && imgui->wantsCaptureKeyboard()) {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
if (emu.romType == ROMType::None) break;
|
||||
|
||||
u32 key = getMapping(event.key.keysym.sym);
|
||||
@@ -186,7 +357,7 @@ void FrontendSDL::run() {
|
||||
// Use the F4 button as a hot-key to pause or resume the emulator
|
||||
// We can't use the audio play/pause buttons because it's annoying
|
||||
case SDLK_F4: {
|
||||
emu.togglePause();
|
||||
togglePaused();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -232,6 +403,11 @@ void FrontendSDL::run() {
|
||||
}
|
||||
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui && imgui->wantsCaptureMouse()) {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
if (emu.romType == ROMType::None) break;
|
||||
|
||||
if (event.button.button == SDL_BUTTON_LEFT) {
|
||||
@@ -243,6 +419,11 @@ void FrontendSDL::run() {
|
||||
break;
|
||||
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui && imgui->wantsCaptureMouse()) {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
if (emu.romType == ROMType::None) break;
|
||||
|
||||
if (event.button.button == SDL_BUTTON_LEFT) {
|
||||
@@ -301,6 +482,11 @@ void FrontendSDL::run() {
|
||||
|
||||
// Detect mouse motion events for gyroscope emulation
|
||||
case SDL_MOUSEMOTION: {
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui && imgui->wantsCaptureMouse()) {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
if (emu.romType == ROMType::None) break;
|
||||
|
||||
// Handle "dragging" across the touchscreen
|
||||
@@ -347,6 +533,11 @@ void FrontendSDL::run() {
|
||||
}
|
||||
|
||||
case SDL_DROPFILE: {
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui && imgui->wantsCaptureMouse()) {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
char* droppedDir = event.drop.file;
|
||||
|
||||
if (droppedDir) {
|
||||
@@ -382,6 +573,13 @@ void FrontendSDL::run() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui) {
|
||||
imgui->beginFrame();
|
||||
imgui->render();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Update controller analog sticks and HID service
|
||||
if (emu.romType != ROMType::None) {
|
||||
// Update circlepad/c-stick/ZL/ZR if a controller is plugged in
|
||||
@@ -440,6 +638,13 @@ void FrontendSDL::run() {
|
||||
|
||||
SDL_GL_SwapWindow(window);
|
||||
}
|
||||
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui) {
|
||||
imgui->shutdown();
|
||||
imgui.reset();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void FrontendSDL::setupControllerSensors(SDL_GameController* controller) {
|
||||
@@ -482,3 +687,26 @@ void FrontendSDL::handleLeftClick(int mouseX, int mouseY) {
|
||||
hid.releaseTouchScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void FrontendSDL::setPaused(bool paused) {
|
||||
if (emuPaused == paused) {
|
||||
return;
|
||||
}
|
||||
emuPaused = paused;
|
||||
if (paused) {
|
||||
emu.pause();
|
||||
} else {
|
||||
emu.resume();
|
||||
}
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (imgui) {
|
||||
imgui->setPaused(emuPaused);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void FrontendSDL::togglePaused() {
|
||||
setPaused(!emuPaused);
|
||||
}
|
||||
|
||||
FrontendSDL::~FrontendSDL() = default;
|
||||
624
src/panda_sdl/imgui_layer.cpp
Normal file
624
src/panda_sdl/imgui_layer.cpp
Normal file
@@ -0,0 +1,624 @@
|
||||
#ifdef IMGUI_FRONTEND
|
||||
|
||||
#include "panda_sdl/imgui_layer.hpp"
|
||||
|
||||
#include <glad/gl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "backends/imgui_impl_opengl3.h"
|
||||
#include "backends/imgui_impl_sdl.h"
|
||||
#include "version.hpp"
|
||||
|
||||
namespace {
|
||||
constexpr int kDebugPadding = 10;
|
||||
|
||||
struct InstalledGame {
|
||||
std::string title;
|
||||
std::string id;
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
std::vector<InstalledGame> scanGamesInDirectory(const std::filesystem::path& dir) {
|
||||
std::vector<InstalledGame> games;
|
||||
if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) return games;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir)) {
|
||||
if (entry.is_regular_file()) {
|
||||
auto ext = entry.path().extension().string();
|
||||
if (ext == ".cci" || ext == ".3ds" || ext == ".cxi" || ext == ".app" || ext == ".ncch" || ext == ".elf" || ext == ".axf" ||
|
||||
ext == ".3dsx") {
|
||||
InstalledGame game;
|
||||
game.title = entry.path().stem().string();
|
||||
game.id = entry.path().filename().string();
|
||||
game.path = entry.path();
|
||||
games.push_back(game);
|
||||
}
|
||||
}
|
||||
}
|
||||
return games;
|
||||
}
|
||||
|
||||
std::vector<InstalledGame> scanAllGames() {
|
||||
std::vector<InstalledGame> allGames;
|
||||
std::filesystem::path eRoot("E:/");
|
||||
{
|
||||
auto games = scanGamesInDirectory(eRoot);
|
||||
allGames.insert(allGames.end(), games.begin(), games.end());
|
||||
}
|
||||
{
|
||||
std::filesystem::path ePanda = eRoot / "PANDA3DS";
|
||||
auto games = scanGamesInDirectory(ePanda);
|
||||
allGames.insert(allGames.end(), games.begin(), games.end());
|
||||
}
|
||||
std::filesystem::path rootPath;
|
||||
#ifdef __WINRT__
|
||||
{
|
||||
char* prefPath = SDL_GetPrefPath(nullptr, nullptr);
|
||||
if (prefPath) {
|
||||
rootPath = std::filesystem::path(prefPath);
|
||||
SDL_free(prefPath);
|
||||
}
|
||||
}
|
||||
#else
|
||||
{
|
||||
char* basePath = SDL_GetBasePath();
|
||||
if (basePath) {
|
||||
rootPath = std::filesystem::path(basePath);
|
||||
SDL_free(basePath);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!rootPath.empty()) {
|
||||
auto games = scanGamesInDirectory(rootPath);
|
||||
allGames.insert(allGames.end(), games.begin(), games.end());
|
||||
std::filesystem::path pandaPath = rootPath / "PANDA3DS";
|
||||
auto games2 = scanGamesInDirectory(pandaPath);
|
||||
allGames.insert(allGames.end(), games2.begin(), games2.end());
|
||||
}
|
||||
return allGames;
|
||||
}
|
||||
|
||||
std::string primaryScanRootLabel() {
|
||||
#ifdef __WINRT__
|
||||
char* prefPath = SDL_GetPrefPath(nullptr, nullptr);
|
||||
if (!prefPath) return "[SDL Pref Path]";
|
||||
std::string out(prefPath);
|
||||
SDL_free(prefPath);
|
||||
return out;
|
||||
#else
|
||||
char* basePath = SDL_GetBasePath();
|
||||
if (!basePath) return "[SDL Base Path]";
|
||||
std::string out(basePath);
|
||||
SDL_free(basePath);
|
||||
return out;
|
||||
#endif
|
||||
}
|
||||
|
||||
struct VersionInfo {
|
||||
std::string appVersion;
|
||||
std::string gitRevision;
|
||||
};
|
||||
|
||||
bool isHexRevision(const std::string& value) {
|
||||
if (value.size() != 7) {
|
||||
return false;
|
||||
}
|
||||
for (char c : value) {
|
||||
if (!std::isxdigit(static_cast<unsigned char>(c))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
VersionInfo splitVersionString(const char* versionString) {
|
||||
VersionInfo info{versionString ? versionString : "", ""};
|
||||
if (info.appVersion.empty()) {
|
||||
info.gitRevision = "unknown";
|
||||
return info;
|
||||
}
|
||||
|
||||
const auto dot = info.appVersion.find_last_of('.');
|
||||
if (dot != std::string::npos) {
|
||||
const std::string maybeRev = info.appVersion.substr(dot + 1);
|
||||
if (isHexRevision(maybeRev)) {
|
||||
info.gitRevision = maybeRev;
|
||||
info.appVersion = info.appVersion.substr(0, dot);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
info.gitRevision = "embedded";
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiLayer::ImGuiLayer(SDL_Window* window, SDL_GLContext context, Emulator& emu) : window(window), glContext(context), emu(emu) {}
|
||||
|
||||
void ImGuiLayer::init() {
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.IniFilename = nullptr;
|
||||
showDebug = emu.getConfig().frontendSettings.showImGuiDebugPanel;
|
||||
|
||||
const bool sdlInitOk = ImGui_ImplSDL2_InitForOpenGL(window, glContext);
|
||||
const bool glInitOk = ImGui_ImplOpenGL3_Init("#version 410");
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
printf("[IMGUI] ImGui init: SDL2=%s OpenGL3=%s window=%p context=%p\n", sdlInitOk ? "ok" : "fail",
|
||||
glInitOk ? "ok" : "fail", window, glContext);
|
||||
if (!sdlInitOk || !glInitOk) {
|
||||
printf("[IMGUI] ImGui backend init failed\n");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImGuiLayer::shutdown() {
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
}
|
||||
|
||||
void ImGuiLayer::processEvent(const SDL_Event& event) {
|
||||
ImGui_ImplSDL2_ProcessEvent(&event);
|
||||
}
|
||||
|
||||
void ImGuiLayer::beginFrame() {
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame(window);
|
||||
ImGui::NewFrame();
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
captureKeyboard = io.WantCaptureKeyboard;
|
||||
captureMouse = io.WantCaptureMouse;
|
||||
}
|
||||
|
||||
void ImGuiLayer::render() {
|
||||
int drawableW = 0;
|
||||
int drawableH = 0;
|
||||
SDL_GL_GetDrawableSize(window, &drawableW, &drawableH);
|
||||
if (drawableW == 0 || drawableH == 0) {
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
static bool warnedOnce = false;
|
||||
if (!warnedOnce) {
|
||||
warnedOnce = true;
|
||||
printf("[IMGUI] render: drawable size is %dx%d\n", drawableW, drawableH);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
glViewport(0, 0, drawableW, drawableH);
|
||||
}
|
||||
|
||||
drawDebugPanel();
|
||||
drawPausePanel();
|
||||
drawSettingsPanel();
|
||||
|
||||
ImGui::Render();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
GLenum err = glGetError();
|
||||
if (err != GL_NO_ERROR) {
|
||||
printf("[IMGUI] render: glGetError=0x%X\n", err);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImGuiLayer::handleHotkey(const SDL_Event& event) {
|
||||
if (event.type != SDL_KEYDOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key.keysym.sym) {
|
||||
case SDLK_F1:
|
||||
showDebug = !showDebug;
|
||||
emu.getConfig().frontendSettings.showImGuiDebugPanel = showDebug;
|
||||
break;
|
||||
case SDLK_F2:
|
||||
showSettings = !showSettings;
|
||||
break;
|
||||
case SDLK_F3:
|
||||
case SDLK_ESCAPE:
|
||||
showPauseMenu = !showPauseMenu;
|
||||
if (onPauseChange) {
|
||||
isPaused = showPauseMenu;
|
||||
onPauseChange(isPaused);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> ImGuiLayer::runGameSelector() {
|
||||
std::vector<InstalledGame> games = scanAllGames();
|
||||
bool showNoRom = games.empty();
|
||||
bool inSettings = false;
|
||||
int selected = 0;
|
||||
bool selectionMade = false;
|
||||
std::optional<std::filesystem::path> selectedPath;
|
||||
|
||||
while (!selectionMade) {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
ImGui_ImplSDL2_ProcessEvent(&event);
|
||||
if (event.type == SDL_QUIT) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!inSettings && event.type == SDL_CONTROLLERBUTTONDOWN) {
|
||||
if (event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_UP && selected > 0) selected--;
|
||||
if (event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN && selected < (int)games.size() - 1) selected++;
|
||||
if (event.cbutton.button == SDL_CONTROLLER_BUTTON_A && !games.empty()) selectionMade = true;
|
||||
}
|
||||
if (!inSettings && event.type == SDL_KEYDOWN) {
|
||||
if (event.key.keysym.sym == SDLK_UP && selected > 0) selected--;
|
||||
if (event.key.keysym.sym == SDLK_DOWN && selected < (int)games.size() - 1) selected++;
|
||||
if (event.key.keysym.sym == SDLK_RETURN && !games.empty()) selectionMade = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame(window);
|
||||
ImGui::NewFrame();
|
||||
|
||||
int drawableW = 0;
|
||||
int drawableH = 0;
|
||||
SDL_GL_GetDrawableSize(window, &drawableW, &drawableH);
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
if (drawableW == 0 || drawableH == 0) {
|
||||
static bool warnedOnce = false;
|
||||
if (!warnedOnce) {
|
||||
warnedOnce = true;
|
||||
printf("[IMGUI] selector: drawable size is %dx%d\n", drawableW, drawableH);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
||||
|
||||
if (showNoRom) {
|
||||
std::string rootLabel = primaryScanRootLabel();
|
||||
ImGui::SetNextWindowPos(ImVec2(drawableW * 0.5f, drawableH * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(620, 260), ImGuiCond_Always);
|
||||
ImGui::Begin("No ROMs Found", nullptr, flags);
|
||||
ImGui::TextWrapped(
|
||||
"No ROM inserted!\n\n"
|
||||
"Please add ROMs (supported formats: .cci, .3ds, .cxi, .app, .ncch, .elf, .axf, .3dsx) to:\n"
|
||||
"E:/\nE:/PANDA3DS\n%s\n%s/PANDA3DS",
|
||||
rootLabel.c_str(), rootLabel.c_str()
|
||||
);
|
||||
ImGui::Dummy(ImVec2(0, 8));
|
||||
if (ImGui::Button("Retry", ImVec2(120, 0))) {
|
||||
games = scanAllGames();
|
||||
showNoRom = games.empty();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Settings", ImVec2(120, 0))) {
|
||||
inSettings = true;
|
||||
showSettings = true;
|
||||
}
|
||||
ImGui::End();
|
||||
} else if (!inSettings) {
|
||||
ImGui::SetNextWindowPos(ImVec2(drawableW * 0.5f, drawableH * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_Always);
|
||||
ImGui::Begin("Select Game", nullptr, flags);
|
||||
|
||||
for (int i = 0; i < (int)games.size(); i++) {
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), "%d: %s (ID: %s)", i + 1, games[i].title.c_str(), games[i].id.c_str());
|
||||
if (ImGui::Selectable(buf, selected == i)) selected = i;
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, 8));
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy(ImVec2(0, 8));
|
||||
ImGui::SetCursorPosX((800 - 120) * 0.5f);
|
||||
if (ImGui::Button("Settings", ImVec2(120, 0))) {
|
||||
inSettings = true;
|
||||
showSettings = true;
|
||||
}
|
||||
ImGui::End();
|
||||
} else {
|
||||
drawSettingsPanel();
|
||||
if (!showSettings) {
|
||||
inSettings = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionMade && !games.empty()) {
|
||||
selectedPath = games[selected].path;
|
||||
}
|
||||
|
||||
ImGui::Render();
|
||||
SDL_GL_MakeCurrent(window, glContext);
|
||||
if (drawableW > 0 && drawableH > 0) {
|
||||
glViewport(0, 0, drawableW, drawableH);
|
||||
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
#ifdef IMGUI_FRONTEND_DEBUG
|
||||
GLenum err = glGetError();
|
||||
if (err != GL_NO_ERROR) {
|
||||
printf("[IMGUI] selector: glGetError=0x%X\n", err);
|
||||
}
|
||||
#endif
|
||||
SDL_GL_SwapWindow(window);
|
||||
SDL_Delay(16);
|
||||
}
|
||||
|
||||
return selectedPath;
|
||||
}
|
||||
|
||||
void ImGuiLayer::drawDebugPanel() {
|
||||
if (!showDebug) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(float(kDebugPadding), float(kDebugPadding)), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowBgAlpha(0.35f);
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings;
|
||||
|
||||
ImGui::Begin("##DebugOverlay", nullptr, flags);
|
||||
int major = 0;
|
||||
int minor = 0;
|
||||
glGetIntegerv(GL_MAJOR_VERSION, &major);
|
||||
glGetIntegerv(GL_MINOR_VERSION, &minor);
|
||||
const VersionInfo versionInfo = splitVersionString(PANDA3DS_VERSION);
|
||||
ImGui::Text("Context : GL %d.%d", major, minor);
|
||||
ImGui::Text("Driver : %s", glGetString(GL_RENDERER));
|
||||
ImGui::Text("Version : %s", versionInfo.appVersion.c_str());
|
||||
ImGui::Text("Revision: %s", versionInfo.gitRevision.c_str());
|
||||
ImGui::Text("FPS : %.1f", ImGui::GetIO().Framerate);
|
||||
ImGui::Text("Paused : %s", isPaused ? "Yes" : "No");
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ImGuiLayer::drawPausePanel() {
|
||||
if (!showPauseMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
int winW = 0;
|
||||
int winH = 0;
|
||||
SDL_GL_GetDrawableSize(window, &winW, &winH);
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(260, 160), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowPos(ImVec2(winW * 0.5f, winH * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||
ImGui::Begin("##PauseMenu", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize);
|
||||
|
||||
ImGui::TextUnformatted("Game Paused");
|
||||
ImGui::Dummy(ImVec2(0, 8));
|
||||
|
||||
if (ImGui::Button("Resume", ImVec2(-1, 0))) {
|
||||
showPauseMenu = false;
|
||||
if (onPauseChange) {
|
||||
isPaused = false;
|
||||
onPauseChange(false);
|
||||
}
|
||||
}
|
||||
if (ImGui::Button("Settings", ImVec2(-1, 0))) {
|
||||
showSettings = true;
|
||||
}
|
||||
if (ImGui::Button("Quit", ImVec2(-1, 0))) {
|
||||
SDL_Event quit{};
|
||||
quit.type = SDL_QUIT;
|
||||
SDL_PushEvent(&quit);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ImGuiLayer::drawSettingsPanel() {
|
||||
if (!showSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
int winW = 0;
|
||||
int winH = 0;
|
||||
SDL_GL_GetDrawableSize(window, &winW, &winH);
|
||||
const float width = 520.0f;
|
||||
const float height = 640.0f * 0.65f;
|
||||
ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowPos(ImVec2(winW * 0.5f, winH * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||
|
||||
EmulatorConfig& cfg = emu.getConfig();
|
||||
bool reloadSettings = false;
|
||||
bool saveConfig = false;
|
||||
const char* currentLang = EmulatorConfig::languageCodeToString(cfg.systemLanguage);
|
||||
|
||||
ImGui::Begin("Settings", &showSettings, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove);
|
||||
|
||||
if (ImGui::CollapsingHeader("General", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
reloadSettings |= ImGui::Checkbox("Enable Discord RPC", &cfg.discordRpcEnabled);
|
||||
reloadSettings |= ImGui::Checkbox("Print App Version", &cfg.printAppVersion);
|
||||
reloadSettings |= ImGui::Checkbox("Enable Circle Pad Pro", &cfg.circlePadProEnabled);
|
||||
reloadSettings |= ImGui::Checkbox("Enable Fastmem", &cfg.fastmemEnabled);
|
||||
reloadSettings |= ImGui::Checkbox("Use Portable Build", &cfg.usePortableBuild);
|
||||
|
||||
struct LanguageOption {
|
||||
const char* label;
|
||||
const char* code;
|
||||
};
|
||||
static const LanguageOption languageOptions[] = {
|
||||
{"English (en)", "en"}, {"Japanese (ja)", "ja"}, {"French (fr)", "fr"},
|
||||
{"German (de)", "de"}, {"Italian (it)", "it"}, {"Spanish (es)", "es"},
|
||||
{"Chinese (zh)", "zh"}, {"Korean (ko)", "ko"}, {"Dutch (nl)", "nl"},
|
||||
{"Portuguese (pt)", "pt"}, {"Russian (ru)", "ru"}, {"Taiwanese (tw)", "tw"},
|
||||
};
|
||||
int langIndex = 0;
|
||||
for (int i = 0; i < (int)std::size(languageOptions); i++) {
|
||||
if (std::strcmp(languageOptions[i].code, currentLang) == 0) {
|
||||
langIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ImGui::Combo("System Language", &langIndex, [](void* data, int idx, const char** outText) {
|
||||
const auto* options = static_cast<const LanguageOption*>(data);
|
||||
*outText = options[idx].label;
|
||||
return true;
|
||||
}, (void*)languageOptions, (int)std::size(languageOptions))) {
|
||||
cfg.systemLanguage = EmulatorConfig::languageCodeFromString(languageOptions[langIndex].code);
|
||||
reloadSettings = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("Window")) {
|
||||
reloadSettings |= ImGui::Checkbox("Show App Version", &cfg.windowSettings.showAppVersion);
|
||||
reloadSettings |= ImGui::Checkbox("Remember Position", &cfg.windowSettings.rememberPosition);
|
||||
ImGui::InputInt("Pos X", &cfg.windowSettings.x);
|
||||
ImGui::InputInt("Pos Y", &cfg.windowSettings.y);
|
||||
ImGui::InputInt("Width", &cfg.windowSettings.width);
|
||||
ImGui::InputInt("Height", &cfg.windowSettings.height);
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("UI")) {
|
||||
static const char* themeLabels[] = {"System", "Light", "Dark", "Greetings Cat", "Cream", "OLED"};
|
||||
static const FrontendSettings::Theme themeValues[] = {
|
||||
FrontendSettings::Theme::System, FrontendSettings::Theme::Light, FrontendSettings::Theme::Dark,
|
||||
FrontendSettings::Theme::GreetingsCat, FrontendSettings::Theme::Cream, FrontendSettings::Theme::Oled,
|
||||
};
|
||||
int themeIndex = 0;
|
||||
for (int i = 0; i < (int)std::size(themeValues); i++) {
|
||||
if (cfg.frontendSettings.theme == themeValues[i]) {
|
||||
themeIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ImGui::Combo("Theme", &themeIndex, themeLabels, (int)std::size(themeLabels))) {
|
||||
cfg.frontendSettings.theme = themeValues[themeIndex];
|
||||
reloadSettings = true;
|
||||
}
|
||||
|
||||
static const char* iconLabels[] = {"Rpog", "Rsyn", "Rnap", "Rcow", "SkyEmu", "Runpog"};
|
||||
static const FrontendSettings::WindowIcon iconValues[] = {
|
||||
FrontendSettings::WindowIcon::Rpog, FrontendSettings::WindowIcon::Rsyn, FrontendSettings::WindowIcon::Rnap,
|
||||
FrontendSettings::WindowIcon::Rcow, FrontendSettings::WindowIcon::SkyEmu, FrontendSettings::WindowIcon::Runpog,
|
||||
};
|
||||
int iconIndex = 0;
|
||||
for (int i = 0; i < (int)std::size(iconValues); i++) {
|
||||
if (cfg.frontendSettings.icon == iconValues[i]) {
|
||||
iconIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ImGui::Combo("Window Icon", &iconIndex, iconLabels, (int)std::size(iconLabels))) {
|
||||
cfg.frontendSettings.icon = iconValues[iconIndex];
|
||||
reloadSettings = true;
|
||||
}
|
||||
|
||||
bool showDebugPanel = cfg.frontendSettings.showImGuiDebugPanel;
|
||||
if (ImGui::Checkbox("Show ImGui Debug Panel", &showDebugPanel)) {
|
||||
cfg.frontendSettings.showImGuiDebugPanel = showDebugPanel;
|
||||
showDebug = showDebugPanel;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("Graphics")) {
|
||||
ImGui::TextUnformatted("Renderer: OpenGL 4.1");
|
||||
if (ImGui::Checkbox("Enable VSync", &cfg.vsyncEnabled)) {
|
||||
if (onVsyncChange) {
|
||||
onVsyncChange(cfg.vsyncEnabled);
|
||||
}
|
||||
}
|
||||
reloadSettings |= ImGui::Checkbox("Enable Shader JIT", &cfg.shaderJitEnabled);
|
||||
reloadSettings |= ImGui::Checkbox("Use Ubershaders", &cfg.useUbershaders);
|
||||
reloadSettings |= ImGui::Checkbox("Accurate Shader Mul", &cfg.accurateShaderMul);
|
||||
reloadSettings |= ImGui::Checkbox("Accelerate Shaders", &cfg.accelerateShaders);
|
||||
reloadSettings |= ImGui::Checkbox("Force Shadergen for Lighting", &cfg.forceShadergenForLights);
|
||||
reloadSettings |= ImGui::InputInt("Shadergen Light Threshold", &cfg.lightShadergenThreshold);
|
||||
cfg.lightShadergenThreshold = std::clamp(cfg.lightShadergenThreshold, 0, 8);
|
||||
reloadSettings |= ImGui::Checkbox("Hash Textures", &cfg.hashTextures);
|
||||
reloadSettings |= ImGui::Checkbox("Enable Renderdoc", &cfg.enableRenderdoc);
|
||||
|
||||
static const char* layoutLabels[] = {"Default", "Default Flipped", "Side By Side", "Side By Side Flipped"};
|
||||
static const ScreenLayout::Layout layoutValues[] = {
|
||||
ScreenLayout::Layout::Default, ScreenLayout::Layout::DefaultFlipped, ScreenLayout::Layout::SideBySide,
|
||||
ScreenLayout::Layout::SideBySideFlipped,
|
||||
};
|
||||
int layoutIndex = 0;
|
||||
for (int i = 0; i < (int)std::size(layoutValues); i++) {
|
||||
if (cfg.screenLayout == layoutValues[i]) {
|
||||
layoutIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ImGui::Combo("Screen Layout", &layoutIndex, layoutLabels, (int)std::size(layoutLabels))) {
|
||||
cfg.screenLayout = layoutValues[layoutIndex];
|
||||
reloadSettings = true;
|
||||
}
|
||||
reloadSettings |= ImGui::SliderFloat("Top Screen Size", &cfg.topScreenSize, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("Audio")) {
|
||||
reloadSettings |= ImGui::Checkbox("Enable Audio", &cfg.audioEnabled);
|
||||
reloadSettings |= ImGui::Checkbox("Mute Audio", &cfg.audioDeviceConfig.muteAudio);
|
||||
reloadSettings |= ImGui::SliderFloat("Volume", &cfg.audioDeviceConfig.volumeRaw, 0.0f, 2.0f);
|
||||
reloadSettings |= ImGui::Checkbox("Enable AAC Audio", &cfg.aacEnabled);
|
||||
reloadSettings |= ImGui::Checkbox("Print DSP Firmware", &cfg.printDSPFirmware);
|
||||
|
||||
static const char* dspLabels[] = {"Null", "Teakra", "HLE"};
|
||||
static const Audio::DSPCore::Type dspValues[] = {Audio::DSPCore::Type::Null, Audio::DSPCore::Type::Teakra, Audio::DSPCore::Type::HLE};
|
||||
int dspIndex = 0;
|
||||
for (int i = 0; i < (int)std::size(dspValues); i++) {
|
||||
if (cfg.dspType == dspValues[i]) {
|
||||
dspIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ImGui::Combo("DSP Emulation", &dspIndex, dspLabels, (int)std::size(dspLabels))) {
|
||||
cfg.dspType = dspValues[dspIndex];
|
||||
reloadSettings = true;
|
||||
}
|
||||
|
||||
static const char* curveLabels[] = {"Cubic", "Linear"};
|
||||
static const AudioDeviceConfig::VolumeCurve curveValues[] = {AudioDeviceConfig::VolumeCurve::Cubic, AudioDeviceConfig::VolumeCurve::Linear};
|
||||
int curveIndex = 0;
|
||||
for (int i = 0; i < (int)std::size(curveValues); i++) {
|
||||
if (cfg.audioDeviceConfig.volumeCurve == curveValues[i]) {
|
||||
curveIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ImGui::Combo("Volume Curve", &curveIndex, curveLabels, (int)std::size(curveLabels))) {
|
||||
cfg.audioDeviceConfig.volumeCurve = curveValues[curveIndex];
|
||||
reloadSettings = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("Battery")) {
|
||||
reloadSettings |= ImGui::Checkbox("Charger Plugged", &cfg.chargerPlugged);
|
||||
reloadSettings |= ImGui::SliderInt("Battery %", &cfg.batteryPercentage, 0, 100);
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("SD")) {
|
||||
reloadSettings |= ImGui::Checkbox("Use Virtual SD", &cfg.sdCardInserted);
|
||||
reloadSettings |= ImGui::Checkbox("Write Protect SD", &cfg.sdWriteProtected);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Save")) {
|
||||
saveConfig = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Close")) {
|
||||
showSettings = false;
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
if (reloadSettings) {
|
||||
emu.reloadSettings();
|
||||
}
|
||||
if (saveConfig) {
|
||||
cfg.save();
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,7 +1,39 @@
|
||||
#include "panda_sdl/frontend_sdl.hpp"
|
||||
|
||||
#ifdef IMGUI_FRONTEND
|
||||
#include <SDL.h>
|
||||
#include <glad/gl.h>
|
||||
|
||||
#include "config.hpp"
|
||||
#include "version.hpp"
|
||||
#endif
|
||||
|
||||
int emu_main(int argc, char *argv[]) {
|
||||
#ifdef IMGUI_FRONTEND
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) {
|
||||
Helpers::panic("Failed to initialize SDL2");
|
||||
}
|
||||
|
||||
std::filesystem::path configPath = std::filesystem::current_path() / "config.toml";
|
||||
if (!std::filesystem::exists(configPath)) {
|
||||
#ifdef __WINRT__
|
||||
char* prefPath = SDL_GetPrefPath(nullptr, nullptr);
|
||||
#else
|
||||
char* prefPath = SDL_GetPrefPath(nullptr, "Alber");
|
||||
#endif
|
||||
if (prefPath) {
|
||||
configPath = std::filesystem::path(prefPath) / "config.toml";
|
||||
SDL_free(prefPath);
|
||||
}
|
||||
}
|
||||
|
||||
EmulatorConfig bootConfig(configPath);
|
||||
const char* windowTitle = bootConfig.windowSettings.showAppVersion ? ("Alber v" PANDA3DS_VERSION) : "Alber";
|
||||
auto bootstrap = FrontendSDL::createImGuiWindowContext(bootConfig, windowTitle);
|
||||
FrontendSDL app(bootstrap.window, bootstrap.context);
|
||||
#else
|
||||
FrontendSDL app;
|
||||
#endif
|
||||
|
||||
if (argc > 1) {
|
||||
auto romPath = std::filesystem::current_path() / argv[1];
|
||||
@@ -10,7 +42,17 @@ int emu_main(int argc, char *argv[]) {
|
||||
Helpers::panic("Failed to load ROM file: %s", romPath.string().c_str());
|
||||
}
|
||||
} else {
|
||||
#ifdef IMGUI_FRONTEND
|
||||
auto selected = app.selectGame();
|
||||
if (!selected.has_value()) {
|
||||
return 0;
|
||||
}
|
||||
if (!app.loadROM(*selected)) {
|
||||
Helpers::panic("Failed to load ROM file: %s", selected->string().c_str());
|
||||
}
|
||||
#else
|
||||
printf("No ROM inserted! Load a ROM by dragging and dropping it into the emulator window!\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
app.run();
|
||||
|
||||
Reference in New Issue
Block a user