diff --git a/CMakeLists.txt b/CMakeLists.txt index a25885ff..4fa8a1a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/include/frontend_settings.hpp b/include/frontend_settings.hpp index 083b8c7d..fa98175f 100644 --- a/include/frontend_settings.hpp +++ b/include/frontend_settings.hpp @@ -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); diff --git a/include/panda_sdl/frontend_sdl.hpp b/include/panda_sdl/frontend_sdl.hpp index 3fc50af1..2a44dfc1 100644 --- a/include/panda_sdl/frontend_sdl.hpp +++ b/include/panda_sdl/frontend_sdl.hpp @@ -3,6 +3,8 @@ #include #include +#include +#include #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 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 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); }; \ No newline at end of file diff --git a/include/panda_sdl/imgui_layer.hpp b/include/panda_sdl/imgui_layer.hpp new file mode 100644 index 00000000..dbc7498a --- /dev/null +++ b/include/panda_sdl/imgui_layer.hpp @@ -0,0 +1,50 @@ +#pragma once + +#ifdef IMGUI_FRONTEND + +#include + +#include +#include + +#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 runGameSelector(); + + bool wantsCaptureKeyboard() const { return captureKeyboard; } + bool wantsCaptureMouse() const { return captureMouse; } + + void setPaused(bool paused) { isPaused = paused; } + void setPauseCallback(std::function callback) { onPauseChange = std::move(callback); } + void setVsyncCallback(std::function 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 onPauseChange; + std::function onVsyncChange; +}; + +#endif diff --git a/src/config.cpp b/src/config.cpp index 4d489c95..0baaf6a0 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -152,6 +152,7 @@ void EmulatorConfig::load() { frontendSettings.theme = FrontendSettings::themeFromString(toml::find_or(ui, "Theme", "dark")); frontendSettings.icon = FrontendSettings::iconFromString(toml::find_or(ui, "WindowIcon", "rpog")); frontendSettings.language = toml::find_or(ui, "Language", "en"); + frontendSettings.showImGuiDebugPanel = toml::find_or(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; diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 8033165a..37cbbf95 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -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(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,43 +126,117 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp ); if (needOpenGL) { - // 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); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, config.rendererType == RendererType::OpenGL ? 4 : 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, config.rendererType == RendererType::OpenGL ? 1 : 3); - window = SDL_CreateWindow(windowTitle, windowX, windowY, windowWidth, windowHeight, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); - - if (window == nullptr) { - Helpers::panic("Window creation failed: %s", SDL_GetError()); + #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"); } - glContext = SDL_GL_CreateContext(window); - if (glContext == nullptr) { - Helpers::warn("OpenGL context creation failed: %s\nTrying again with OpenGL ES.", SDL_GetError()); + window = existingWindow; + glContext = existingContext; + SDL_GetWindowSize(window, reinterpret_cast(&windowWidth), reinterpret_cast(&windowHeight)); - // Some low end devices (eg RPi, emulation handhelds) don't support desktop GL, but only OpenGL ES, so fall back to that if GL context - // creation failed - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); - glContext = SDL_GL_CreateContext(window); - if (glContext == nullptr) { - Helpers::panic("OpenGL context creation failed: %s", SDL_GetError()); - } + 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 (!gladLoadGLES2Loader(reinterpret_cast(SDL_GL_GetProcAddress))) { - Helpers::panic("OpenGL init failed"); - } + if (!gladLoadGLLoader(reinterpret_cast(SDL_GL_GetProcAddress))) { + #ifdef IMGUI_FRONTEND_DEBUG + printf("[IMGUI] gladLoadGLLoader failed\n"); + #endif + Helpers::panic("OpenGL init failed"); + } - emu.getRenderer()->setupGLES(); - } else { - if (!gladLoadGLLoader(reinterpret_cast(SDL_GL_GetProcAddress))) { - 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(renderer ? renderer : reinterpret_cast("(null)")), + reinterpret_cast(version ? version : reinterpret_cast("(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(&windowWidth), reinterpret_cast(&windowHeight)); + + if (SDL_GL_MakeCurrent(window, glContext) != 0) { + Helpers::panic("SDL_GL_MakeCurrent failed: %s", SDL_GetError()); + } + + if (!gladLoadGLLoader(reinterpret_cast(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); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, config.rendererType == RendererType::OpenGL ? 4 : 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, config.rendererType == RendererType::OpenGL ? 1 : 3); + window = SDL_CreateWindow(windowTitle, windowX, windowY, windowWidth, windowHeight, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); + + if (window == nullptr) { + Helpers::panic("Window creation failed: %s", SDL_GetError()); + } + + glContext = SDL_GL_CreateContext(window); + if (glContext == nullptr) { + Helpers::warn("OpenGL context creation failed: %s\nTrying again with OpenGL ES.", SDL_GetError()); + + // Some low end devices (eg RPi, emulation handhelds) don't support desktop GL, but only OpenGL ES, so fall back to that if GL context + // creation failed + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); + glContext = SDL_GL_CreateContext(window); + if (glContext == nullptr) { + Helpers::panic("OpenGL context creation failed: %s", SDL_GetError()); + } + + if (!gladLoadGLES2Loader(reinterpret_cast(SDL_GL_GetProcAddress))) { + Helpers::panic("OpenGL init failed"); + } + emu.getRenderer()->setupGLES(); + usingGLES = true; + } + + if (!usingGLES) { + if (!gladLoadGLLoader(reinterpret_cast(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 @@ -123,10 +260,26 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp #endif emu.initGraphicsContext(window); + +#ifdef IMGUI_FRONTEND + imgui = std::make_unique(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 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) { @@ -481,4 +686,27 @@ void FrontendSDL::handleLeftClick(int mouseX, int mouseY) { } else { hid.releaseTouchScreen(); } -} \ No newline at end of file +} + +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; \ No newline at end of file diff --git a/src/panda_sdl/imgui_layer.cpp b/src/panda_sdl/imgui_layer.cpp new file mode 100644 index 00000000..3225fbb6 --- /dev/null +++ b/src/panda_sdl/imgui_layer.cpp @@ -0,0 +1,624 @@ +#ifdef IMGUI_FRONTEND + +#include "panda_sdl/imgui_layer.hpp" + +#include + +#include +#include +#include +#include + +#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 scanGamesInDirectory(const std::filesystem::path& dir) { + std::vector 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 scanAllGames() { + std::vector 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(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 ImGuiLayer::runGameSelector() { + std::vector games = scanAllGames(); + bool showNoRom = games.empty(); + bool inSettings = false; + int selected = 0; + bool selectionMade = false; + std::optional 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(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 diff --git a/src/panda_sdl/main.cpp b/src/panda_sdl/main.cpp index a832430f..876193c9 100644 --- a/src/panda_sdl/main.cpp +++ b/src/panda_sdl/main.cpp @@ -1,7 +1,39 @@ #include "panda_sdl/frontend_sdl.hpp" +#ifdef IMGUI_FRONTEND +#include +#include + +#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();