feat: add ImGui frontend support with game selection and debug panels

This commit is contained in:
moonpower
2026-02-01 23:54:58 +03:00
parent 5b8081bc33
commit 25add6ada7
8 changed files with 1024 additions and 32 deletions

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);
};

View 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

View File

@@ -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;

View File

@@ -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;

View 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

View File

@@ -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();