Integrate ps2xStudio and improve elf_analyzer callback detection (#83)

* Integrate ps2xStudio and improve elf_analyzer callback detection

* Update CMakeLists.txt for ImGui libraries

* CMakeLists Fixes Again

Updated dependencies and improved comments for clarity.

* Again CMakeLists. Now it definitely works

* again CMakeLists

* Update CMakeLists.txt ps2xStudio & ps2xTest

* Update CMakeLists.txt

* Update CMakeLists.txt

* Update StudioState.hpp

* Update CMakeLists.txt
This commit is contained in:
Vova Vovchok
2026-03-01 20:41:33 +01:00
committed by GitHub
parent b3be2650b0
commit 765cbcca0f
13 changed files with 1941 additions and 2 deletions
+1
View File
@@ -49,3 +49,4 @@ add_subdirectory("ps2xRuntime")
add_subdirectory("ps2xAnalyzer")
add_subdirectory("ps2xTest")
add_subdirectory("ps2xStudio")
@@ -33,6 +33,8 @@ namespace ps2recomp
bool analyze();
bool generateToml(const std::string &outputPath);
bool importGhidraMap(const std::string &csvPath);
const std::vector<Function>& getFunctions() const;
bool isLibrarySymbolNameForHeuristics(const std::string &name) const;
static bool isReliableSymbolNameForHeuristics(const std::string &name);
static bool isSystemSymbolNameForHeuristics(const std::string &name);
+84
View File
@@ -2542,4 +2542,88 @@ namespace ps2recomp
return false;
}
}
bool ElfAnalyzer::importGhidraMap(const std::string &csvPath)
{
std::ifstream file(csvPath);
if (!file)
{
std::cerr << "Failed to open Ghidra CSV file: " << csvPath << std::endl;
return false;
}
std::string line;
int lineNum = 0;
int importedCount = 0;
while (std::getline(file, line))
{
lineNum++;
// Skip header line
if (lineNum == 1 && line.find("Name") != std::string::npos)
{
continue;
}
// Parse CSV line: Name,Start,End,Size
std::stringstream ss(line);
std::string name, startStr, endStr, sizeStr;
if (!std::getline(ss, name, ',') ||
!std::getline(ss, startStr, ',') ||
!std::getline(ss, endStr, ','))
{
continue; // Skip malformed lines
}
// Parse hex addresses (e.g., "0x00123456")
uint32_t startAddr = 0;
uint32_t endAddr = 0;
try
{
startAddr = std::stoul(startStr, nullptr, 16);
endAddr = std::stoul(endStr, nullptr, 16);
}
catch (...)
{
continue; // Skip lines with invalid addresses
}
// Update existing function or create new one
bool found = false;
for (auto &func : m_functions)
{
if (func.start == startAddr)
{
// Update with Ghidra's more accurate boundaries
func.name = name;
func.end = endAddr;
found = true;
importedCount++;
break;
}
}
// If not found, create new function from Ghidra data
if (!found)
{
Function newFunc;
newFunc.name = name;
newFunc.start = startAddr;
newFunc.end = endAddr;
m_functions.push_back(newFunc);
importedCount++;
}
}
std::cout << "Imported " << importedCount << " functions from Ghidra CSV: " << csvPath << std::endl;
return true;
}
const std::vector<Function>& ElfAnalyzer::getFunctions() const
{
return m_functions;
}
}
+130
View File
@@ -0,0 +1,130 @@
# Blackline Interactive
cmake_minimum_required(VERSION 3.21)
project(ps2xStudio)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(
imgui
GIT_REPOSITORY https://github.com/ocornut/imgui.git
GIT_TAG docking
GIT_SHALLOW TRUE
)
FetchContent_Declare(
imgui_colortextedit
GIT_REPOSITORY https://github.com/BalazsJako/ImGuiColorTextEdit.git
GIT_TAG master
GIT_SHALLOW TRUE
)
FetchContent_Declare(
imgui_club
GIT_REPOSITORY https://github.com/ocornut/imgui_club.git
GIT_TAG main
GIT_SHALLOW TRUE
)
FetchContent_Declare(
imgui_file_dialog
GIT_REPOSITORY https://github.com/aiekick/ImGuiFileDialog.git
GIT_TAG master
GIT_SHALLOW TRUE
)
FetchContent_Declare(
SDL2
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-2.30.9
GIT_SHALLOW TRUE
)
set(SDL_SHARED ON CACHE BOOL "" FORCE)
set(SDL_STATIC OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(imgui imgui_colortextedit imgui_club SDL2)
FetchContent_GetProperties(imgui_file_dialog)
if(NOT imgui_file_dialog_POPULATED)
FetchContent_Populate(imgui_file_dialog)
endif()
find_package(OpenGL REQUIRED)
set(IMGUI_SOURCES
${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_tables.cpp
${imgui_SOURCE_DIR}/imgui_widgets.cpp
${imgui_SOURCE_DIR}/imgui_demo.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_sdl2.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp
)
add_library(imgui_lib STATIC ${IMGUI_SOURCES})
target_include_directories(imgui_lib PUBLIC
${imgui_SOURCE_DIR}
${imgui_SOURCE_DIR}/backends
)
target_link_libraries(imgui_lib PUBLIC SDL2::SDL2 OpenGL::GL)
add_library(imgui_colortextedit_lib STATIC
${imgui_colortextedit_SOURCE_DIR}/TextEditor.cpp
)
target_include_directories(imgui_colortextedit_lib PUBLIC ${imgui_colortextedit_SOURCE_DIR})
target_link_libraries(imgui_colortextedit_lib PUBLIC imgui_lib)
add_library(imgui_memory_editor_lib INTERFACE)
target_include_directories(imgui_memory_editor_lib INTERFACE
${imgui_club_SOURCE_DIR}/imgui_memory_editor
)
target_link_libraries(imgui_memory_editor_lib INTERFACE imgui_lib)
add_library(imgui_filedialog_lib STATIC
${imgui_file_dialog_SOURCE_DIR}/ImGuiFileDialog.cpp
)
target_include_directories(imgui_filedialog_lib PUBLIC
${imgui_file_dialog_SOURCE_DIR}
${imgui_SOURCE_DIR}
)
target_link_libraries(imgui_filedialog_lib PUBLIC imgui_lib)
file(GLOB_RECURSE STUDIO_SOURCES "src/*.cpp")
file(GLOB_RECURSE STUDIO_HEADERS "include/*.hpp")
add_executable(ps2xStudio ${STUDIO_SOURCES} ${STUDIO_HEADERS})
target_include_directories(ps2xStudio PRIVATE
include
${CMAKE_CURRENT_SOURCE_DIR}/../ps2xAnalyzer/include
${CMAKE_CURRENT_SOURCE_DIR}/../ps2xRecomp/include
${CMAKE_CURRENT_SOURCE_DIR}/../ps2xTest/include
)
target_link_libraries(ps2xStudio PRIVATE
imgui_lib
imgui_colortextedit_lib
imgui_memory_editor_lib
imgui_filedialog_lib
ps2_recomp_lib
ps2_analyzer_lib
ps2_test_lib
SDL2::SDL2
OpenGL::GL
)
if(WIN32)
target_link_libraries(ps2xStudio PRIVATE SDL2::SDL2main)
endif()
Binary file not shown.
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "StudioState.hpp"
namespace GUI {
void DrawStudio(StudioState& state);
void ApplySettings(StudioState& state);
// Called from main loop BEFORE NewFrame when fonts need rebuilding
void RebuildFontsIfNeeded(StudioState& state);
// Notify GUI that config editor should resync
void SyncConfigEditor(StudioState& state);
// Returns true when user requested exit (File > Exit)
bool WantsQuit();
}
+570
View File
@@ -0,0 +1,570 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
#include <map>
#include <future>
#include <atomic>
#include <mutex>
#include <iostream>
#include <sstream>
#include <fstream>
#include <memory>
#include "ps2recomp/elf_analyzer.h"
#include "ps2recomp/config_manager.h"
#include "ps2recomp/ps2_recompiler.h"
#include "ps2recomp/types.h"
#include "../../ps2xTest/include/MiniTest.h"
extern void register_code_generator_tests();
extern void register_r5900_decoder_tests();
enum class OverrideStatus {
Default, Stub, Skip, ForceRecompile
};
enum class ThemeMode {
Dark, Light, Custom
};
struct AppSettings {
ThemeMode theme = ThemeMode::Dark;
float fontSize = 15.0f;
float uiScale = 1.0f;
std::string selectedFont = "Font_1.ttf";
int windowWidth = 1280;
int windowHeight = 720;
bool maximized = true;
// Custom theme colors
float customBgBase[4] = {0.08f, 0.08f, 0.08f, 1.00f};
float customAccent[4] = {0.00f, 0.48f, 0.80f, 1.00f};
};
class StreamRedirector : public std::stringbuf {
public:
StreamRedirector(std::vector<std::string>& target, std::mutex& mtx, std::atomic<size_t>& ver)
: logs(target), mutex(mtx), version(ver) {}
int sync() override {
std::lock_guard<std::mutex> lock(mutex);
std::string line = this->str();
if (!line.empty() && line.back() == '\n') line.pop_back();
if (!line.empty()) {
logs.push_back(line);
version.fetch_add(1);
}
this->str("");
return 0;
}
private:
std::vector<std::string>& logs;
std::mutex& mutex;
std::atomic<size_t>& version;
};
struct UIState {
std::string elfPath;
std::string outputPath = "output";
std::string customOutputPath;
std::string ghidraCSVPath;
std::string ghidraCSVContent;
std::string configTomlContent;
std::vector<uint8_t> rawElfData;
std::map<std::string, OverrideStatus> funcOverrides;
std::shared_ptr<ps2recomp::ElfAnalyzer> analyzer;
bool isAnalysisComplete = false;
// NEW: Support for multiple imported CSV files
std::vector<std::string> importedCSVFiles;
int selectedCSVIndex = -1;
};
class StudioState {
public:
UIState data;
AppSettings settings;
int selectedFuncIndex = -1;
std::atomic<bool> isBusy = false;
std::vector<std::string> logs;
std::mutex stateMutex;
std::streambuf* oldCout = nullptr;
std::streambuf* oldCerr = nullptr;
std::unique_ptr<StreamRedirector> redirector;
std::future<void> workerThread;
std::vector<std::string> availableFonts;
// Deferred font rebuild flag - fonts are rebuilt in the main loop BEFORE NewFrame
std::atomic<bool> pendingFontRebuild{false};
// Resolved absolute path to config.toml
std::string configTomlPath;
// Track log changes for efficient UI updates
std::atomic<size_t> logVersion{0};
// Thread-safe status message
void SetStatus(const std::string& msg) {
std::lock_guard<std::mutex> lock(statusMutex_);
statusMessage_ = msg;
}
std::string GetStatus() const {
std::lock_guard<std::mutex> lock(statusMutex_);
return statusMessage_;
}
StudioState() {
redirector = std::make_unique<StreamRedirector>(logs, stateMutex, logVersion);
oldCout = std::cout.rdbuf(redirector.get());
oldCerr = std::cerr.rdbuf(redirector.get());
try {
if (!std::filesystem::exists("output")) {
std::filesystem::create_directory("output");
}
data.outputPath = std::filesystem::absolute("output").string();
} catch(...) {}
LoadSettings();
ScanFonts();
// Load config.toml immediately and resolve its path
LoadConfigToml();
}
~StudioState() {
std::cout.rdbuf(oldCout);
std::cerr.rdbuf(oldCerr);
SaveSettings();
}
void Log(const std::string& msg) {
std::lock_guard<std::mutex> lock(stateMutex);
logs.push_back("[Studio] " + msg);
logVersion.fetch_add(1);
}
void LogRaw(const std::string& msg) {
std::lock_guard<std::mutex> lock(stateMutex);
logs.push_back(msg);
logVersion.fetch_add(1);
}
void ScanFonts() {
availableFonts.clear();
try {
std::filesystem::path fontDir = "external/Font";
if (std::filesystem::exists(fontDir)) {
for (const auto& entry : std::filesystem::directory_iterator(fontDir)) {
if (entry.is_regular_file()) {
std::string ext = entry.path().extension().string();
if (ext == ".ttf" || ext == ".TTF") {
availableFonts.push_back(entry.path().filename().string());
}
}
}
}
} catch(...) {}
if (availableFonts.empty()) {
availableFonts.push_back("Default");
}
}
void LoadSettings() {
try {
std::ifstream file("studio_settings.ini");
if (file) {
std::string line;
while (std::getline(file, line)) {
size_t pos = line.find('=');
if (pos != std::string::npos) {
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
if (key == "theme") {
int t = std::stoi(value);
settings.theme = static_cast<ThemeMode>(t);
} else if (key == "fontSize") {
float newSize = std::stof(value);
// Validate font size
if (newSize >= 10.0f && newSize <= 48.0f) {
settings.fontSize = newSize;
}
} else if (key == "uiScale") {
float newScale = std::stof(value);
if (newScale >= 0.5f && newScale <= 3.0f) {
settings.uiScale = newScale;
}
} else if (key == "selectedFont") {
settings.selectedFont = value;
} else if (key == "windowWidth") {
settings.windowWidth = std::stoi(value);
} else if (key == "windowHeight") {
settings.windowHeight = std::stoi(value);
} else if (key == "maximized") {
settings.maximized = (value == "1");
} else if (key == "customOutputPath") {
data.customOutputPath = value;
}
}
}
}
} catch(...) {}
}
void SaveSettings() {
try {
std::ofstream file("studio_settings.ini");
file << "theme=" << static_cast<int>(settings.theme) << "\n";
file << "fontSize=" << settings.fontSize << "\n";
file << "uiScale=" << settings.uiScale << "\n";
file << "selectedFont=" << settings.selectedFont << "\n";
file << "windowWidth=" << settings.windowWidth << "\n";
file << "windowHeight=" << settings.windowHeight << "\n";
file << "maximized=" << (settings.maximized ? "1" : "0") << "\n";
if (!data.customOutputPath.empty()) {
file << "customOutputPath=" << data.customOutputPath << "\n";
}
} catch(...) {}
}
void LoadELF(const std::string& path) {
data.elfPath = path;
data.isAnalysisComplete = false;
data.analyzer.reset();
data.funcOverrides.clear();
try {
std::ifstream file(path, std::ios::binary);
if (file) {
data.rawElfData.assign((std::istreambuf_iterator<char>(file)),
(std::istreambuf_iterator<char>()));
} else {
data.rawElfData.clear();
}
} catch (...) {
data.rawElfData.clear();
}
std::string filename = std::filesystem::path(path).filename().string();
SetStatus("Loaded: " + filename);
Log("File loaded: " + path);
}
void ImportGhidraCSV(const std::string& csvPath) {
if (!data.analyzer) {
Log("Error: Analyze ELF first before importing symbols.");
return;
}
data.ghidraCSVPath = csvPath;
try {
std::ifstream file(csvPath);
if (file) {
std::stringstream buffer;
buffer << file.rdbuf();
data.ghidraCSVContent = buffer.str();
}
} catch(...) {}
data.analyzer->importGhidraMap(csvPath);
Log("Ghidra symbols imported successfully from: " + csvPath);
}
void SetOutputDir(const std::string& path) {
if (path.empty()) return;
try {
data.customOutputPath = std::filesystem::absolute(path).string();
if (!std::filesystem::exists(data.customOutputPath)) {
std::filesystem::create_directories(data.customOutputPath);
}
Log("Custom output directory set to: " + data.customOutputPath);
} catch (const std::exception& e) {
Log(std::string("Error setting output dir: ") + e.what());
data.customOutputPath = std::filesystem::absolute("output").string();
}
}
void StartAnalysis(std::string path = "") {
if (isBusy) return;
if (!path.empty()) data.elfPath = path;
if (data.elfPath.empty()) {
Log("Error: No file selected.");
return;
}
isBusy = true;
SetStatus("Analyzing...");
std::string currentPath = data.elfPath;
workerThread = std::async(std::launch::async, [this, currentPath]() {
try {
auto newAnalyzer = std::make_shared<ps2recomp::ElfAnalyzer>(currentPath);
if (newAnalyzer->analyze()) {
std::lock_guard<std::mutex> lock(stateMutex);
data.analyzer = newAnalyzer;
data.isAnalysisComplete = true;
SetStatus("Analysis Complete");
logVersion.fetch_add(1);
} else {
SetStatus("Analysis Failed");
Log("Analysis failed for: " + currentPath);
}
} catch (const std::exception& e) {
SetStatus("Error");
Log(std::string("Analysis exception: ") + e.what());
}
isBusy = false;
});
}
void MoveOutputFiles() {
if (data.customOutputPath.empty() || data.customOutputPath == data.outputPath) {
return;
}
try {
std::filesystem::path srcDir = data.outputPath;
std::filesystem::path dstDir = data.customOutputPath;
if (!std::filesystem::exists(dstDir)) {
std::filesystem::create_directories(dstDir);
}
for (const auto& entry : std::filesystem::directory_iterator(srcDir)) {
if (entry.is_regular_file()) {
std::filesystem::path dst = dstDir / entry.path().filename();
std::filesystem::copy_file(entry.path(), dst,
std::filesystem::copy_options::overwrite_existing);
Log("Moved: " + entry.path().filename().string());
}
}
Log("All output files moved to: " + data.customOutputPath);
} catch (const std::exception& e) {
Log(std::string("Error moving files: ") + e.what());
}
}
void StartRecompilation() {
if (isBusy || !data.isAnalysisComplete) return;
// Set the effective output path BEFORE saving config
std::string effectiveOutputPath = data.customOutputPath.empty()
? data.outputPath : data.customOutputPath;
if (effectiveOutputPath.empty()) {
SetOutputDir("output");
effectiveOutputPath = data.outputPath;
}
// Ensure output directory exists
try {
if (!std::filesystem::exists(effectiveOutputPath)) {
std::filesystem::create_directories(effectiveOutputPath);
}
} catch (const std::exception& e) {
Log(std::string("Error creating output dir: ") + e.what());
return;
}
SaveConfigTOML();
isBusy = true;
SetStatus("Recompiling...");
// Use the resolved config path
std::string resolvedConfigPath = configTomlPath.empty() ? "config.toml" : configTomlPath;
workerThread = std::async(std::launch::async, [this, resolvedConfigPath]() {
try {
ps2recomp::PS2Recompiler recompiler(resolvedConfigPath);
if (recompiler.initialize() && recompiler.recompile()) {
recompiler.generateOutput();
SetStatus("Recompilation Success");
Log("Recompilation completed successfully");
LoadConfigToml();
} else {
SetStatus("Recompilation Failed");
Log("Recompilation failed");
}
} catch (const std::exception& e) {
SetStatus("Recompilation Error");
Log(std::string("Recompilation error: ") + e.what());
}
isBusy = false;
});
}
void RunDiagnostics() {
if (isBusy) return;
isBusy = true;
SetStatus("Running Diagnostics...");
workerThread = std::async(std::launch::async, [this]() {
try {
register_code_generator_tests();
register_r5900_decoder_tests();
std::cout << "\n--- DIAGNOSTICS ---\n" << std::endl;
int failed = MiniTest::Run();
if (failed == 0) {
SetStatus("System Healthy");
std::cout << "\n[SUCCESS] All systems operational." << std::endl;
} else {
SetStatus("Issues Found");
std::cerr << "\n[WARNING] " << failed << " tests failed!" << std::endl;
}
} catch (const std::exception& e) {
SetStatus("Diagnostics Error");
std::cerr << "Critical failure: " << e.what() << std::endl;
}
isBusy = false;
});
}
// Resolve and load config.toml with proper path tracking
void LoadConfigToml() {
bool loaded = false;
// Build search paths - prioritize custom output path and previously resolved path
std::vector<std::string> configPaths;
if (!configTomlPath.empty()) {
configPaths.push_back(configTomlPath); // previously resolved
}
configPaths.push_back("config.toml");
if (!data.customOutputPath.empty()) {
configPaths.push_back(data.customOutputPath + "/config.toml");
}
if (!data.outputPath.empty()) {
configPaths.push_back(data.outputPath + "/config.toml");
}
configPaths.push_back("../config.toml");
for (const auto& path : configPaths) {
if (path.empty()) continue;
try {
std::ifstream file(path);
if (file && file.good()) {
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
if (!content.empty()) {
data.configTomlContent = content;
configTomlPath = std::filesystem::absolute(path).string();
Log("Config loaded from: " + configTomlPath);
loaded = true;
break;
}
}
} catch(...) {
continue;
}
}
if (!loaded) {
// Create default config in CWD
configTomlPath = std::filesystem::absolute("config.toml").string();
CreateDefaultConfig();
}
}
// NEW: Create default config if none exists
void CreateDefaultConfig() {
try {
std::string defaultConfig = R"(# PS2Recomp Configuration File
# Generated by PS2Recomp Studio
[input]
path = ""
[output]
path = "output"
single_file = false
[functions]
stub = []
skip = []
force_recompile = []
[settings]
optimize = true
debug_info = false
)";
std::string savePath = configTomlPath.empty() ? "config.toml" : configTomlPath;
std::ofstream file(savePath);
if (file) {
file << defaultConfig;
file.flush();
data.configTomlContent = defaultConfig;
if (configTomlPath.empty()) {
configTomlPath = std::filesystem::absolute(savePath).string();
}
Log("Created default config at: " + configTomlPath);
}
} catch (const std::exception& e) {
Log(std::string("Error creating default config: ") + e.what());
}
}
void SaveConfigTOML() {
if (!data.isAnalysisComplete) return;
try {
ps2recomp::RecompilerConfig config;
config.inputPath = data.elfPath;
// Use the effective output path (custom if set, default otherwise)
config.outputPath = data.customOutputPath.empty()
? data.outputPath : data.customOutputPath;
config.singleFileOutput = false;
for (const auto& [name, status] : data.funcOverrides) {
if (status == OverrideStatus::Stub) {
config.stubImplementations.push_back(name);
} else if (status == OverrideStatus::Skip) {
config.skipFunctions.push_back(name);
}
}
std::string savePath = configTomlPath.empty() ? "config.toml" : configTomlPath;
ps2recomp::ConfigManager mgr(savePath);
mgr.saveConfig(config);
Log("Config saved to " + savePath);
LoadConfigToml();
} catch (const std::exception& e) {
Log(std::string("Error saving config: ") + e.what());
}
}
void SaveConfigTomlFromEditor(const std::string& newContent) {
try {
std::string savePath = configTomlPath.empty() ? "config.toml" : configTomlPath;
std::ofstream file(savePath);
if (file) {
file << newContent;
file.flush();
data.configTomlContent = newContent;
Log("config.toml saved to: " + savePath);
} else {
Log("Error: Cannot open " + savePath + " for writing");
}
} catch (const std::exception& e) {
Log(std::string("Error saving config.toml: ") + e.what());
}
}
// Get the effective output directory
std::string GetEffectiveOutputPath() const {
if (!data.customOutputPath.empty()) return data.customOutputPath;
return data.outputPath;
}
private:
mutable std::mutex statusMutex_;
std::string statusMessage_ = "Ready";
};
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include "imgui.h"
#include "../StudioState.hpp"
namespace StyleManager {
void ApplyTheme(ThemeMode mode, AppSettings& settings);
void ApplyDarkTheme();
void ApplyLightTheme();
void ApplyCustomTheme(AppSettings& settings);
void SetupFonts(AppSettings& settings);
}
+742
View File
@@ -0,0 +1,742 @@
#include "GUI.hpp"
#include "ui/StyleManager.hpp"
#include "imgui.h"
#include "imgui_internal.h"
#include "imgui_memory_editor.h"
#include "TextEditor.h"
#include "ImGuiFileDialog.h"
#include <sstream>
#include <iomanip>
#include <algorithm>
#include <cmath>
static MemoryEditor mem_edit;
static TextEditor code_editor;
static TextEditor config_editor;
static TextEditor ghidra_editor;
static TextEditor log_editor; // TextEditor for logs - supports text selection
static bool editors_initialized = false;
static bool show_settings_window = false;
static bool config_editor_needs_sync = false;
static size_t last_log_version = 0;
static bool s_wantsQuit = false;
// HEX view color highlighting
struct HexHighlightRange {
size_t start;
size_t end;
ImU32 color;
std::string label;
};
static std::vector<HexHighlightRange> hex_highlight_ranges;
// Global pointer for BgColorFn callback
static std::vector<HexHighlightRange>* g_hex_highlights = &hex_highlight_ranges;
std::string FormatHex(uint32_t val) {
std::stringstream ss;
ss << "0x" << std::hex << std::setw(8) << std::setfill('0') << val;
return ss.str();
}
// BgColorFn callback for MemoryEditor - provides per-byte background color
static ImU32 HexBgColorCallback(const ImU8* /*mem*/, size_t off, void* /*user_data*/) {
if (g_hex_highlights) {
for (const auto& hl : *g_hex_highlights) {
if (off >= hl.start && off < hl.end) {
return hl.color;
}
}
}
return 0; // no color (transparent)
}
void GUI::ApplySettings(StudioState& state) {
StyleManager::SetupFonts(state.settings);
StyleManager::ApplyTheme(state.settings.theme, state.settings);
}
void GUI::RebuildFontsIfNeeded(StudioState& state) {
if (state.pendingFontRebuild.exchange(false)) {
StyleManager::SetupFonts(state.settings);
}
}
void GUI::SyncConfigEditor(StudioState& state) {
config_editor_needs_sync = true;
}
bool GUI::WantsQuit() {
return s_wantsQuit;
}
// ---- Settings Window ----
static void DrawSettingsWindow(StudioState& state) {
if (!show_settings_window) return;
ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Settings", &show_settings_window)) {
if (ImGui::CollapsingHeader("Theme", ImGuiTreeNodeFlags_DefaultOpen)) {
const char* themes[] = { "Dark", "Light", "Custom" };
int current = static_cast<int>(state.settings.theme);
if (ImGui::Combo("Theme Mode", &current, themes, 3)) {
state.settings.theme = static_cast<ThemeMode>(current);
StyleManager::ApplyTheme(state.settings.theme, state.settings);
}
if (state.settings.theme == ThemeMode::Custom) {
ImGui::Spacing();
ImGui::ColorEdit4("Background", state.settings.customBgBase);
ImGui::ColorEdit4("Accent", state.settings.customAccent);
if (ImGui::Button("Apply Custom Colors")) {
StyleManager::ApplyTheme(state.settings.theme, state.settings);
}
}
}
if (ImGui::CollapsingHeader("Font & Size", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::BeginCombo("Font", state.settings.selectedFont.c_str())) {
for (const auto& font : state.availableFonts) {
bool selected = (state.settings.selectedFont == font);
if (ImGui::Selectable(font.c_str(), selected)) {
state.settings.selectedFont = font;
// Defer font rebuild to main loop (prevents crash)
state.pendingFontRebuild = true;
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
// Font size slider - changes are deferred to prevent crash
if (ImGui::SliderFloat("Font Size", &state.settings.fontSize, 10.0f, 24.0f, "%.1f")) {
// Clamp to safe range
state.settings.fontSize = std::clamp(state.settings.fontSize, 10.0f, 48.0f);
// Defer font rebuild to main loop - NOT during frame rendering
state.pendingFontRebuild = true;
}
}
if (ImGui::CollapsingHeader("UI Scale", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::SliderFloat("Scale", &state.settings.uiScale, 0.5f, 2.0f, "%.2f")) {
state.settings.uiScale = std::clamp(state.settings.uiScale, 0.5f, 3.0f);
StyleManager::ApplyTheme(state.settings.theme, state.settings);
}
}
if (ImGui::CollapsingHeader("Window", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Checkbox("Start Maximized", &state.settings.maximized);
ImGui::SliderInt("Default Width", &state.settings.windowWidth, 800, 3840);
ImGui::SliderInt("Default Height", &state.settings.windowHeight, 600, 2160);
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Rescan Fonts", ImVec2(150, 0))) {
state.ScanFonts();
}
ImGui::SameLine();
if (ImGui::Button("Reset to Defaults", ImVec2(150, 0))) {
state.settings = AppSettings();
state.pendingFontRebuild = true;
}
ImGui::SameLine();
if (ImGui::Button("Close", ImVec2(150, 0))) {
show_settings_window = false;
}
}
ImGui::End();
}
// ---- Main Draw ----
void GUI::DrawStudio(StudioState& state) {
if (!editors_initialized) {
code_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
code_editor.SetPalette(TextEditor::GetDarkPalette());
code_editor.SetReadOnly(true);
config_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
config_editor.SetPalette(TextEditor::GetDarkPalette());
config_editor.SetReadOnly(false);
config_editor.SetText(state.data.configTomlContent);
ghidra_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
ghidra_editor.SetPalette(TextEditor::GetDarkPalette());
ghidra_editor.SetReadOnly(true);
// Log editor: read-only TextEditor that supports text selection (Ctrl+A, Ctrl+C, mouse drag)
log_editor.SetPalette(TextEditor::GetDarkPalette());
log_editor.SetReadOnly(true);
log_editor.SetShowWhitespaces(false);
mem_edit.ReadOnly = true;
mem_edit.OptShowAscii = true;
mem_edit.OptGreyOutZeroes = true;
mem_edit.OptUpperCaseHex = false;
// Set BgColorFn for highlighting selected functions in hex view
mem_edit.BgColorFn = HexBgColorCallback;
mem_edit.UserData = nullptr;
editors_initialized = true;
}
// Sync config editor if requested
if (config_editor_needs_sync) {
config_editor.SetText(state.data.configTomlContent);
config_editor_needs_sync = false;
}
// Update log editor when logs change
{
size_t currentVersion = state.logVersion.load();
if (currentVersion != last_log_version) {
std::lock_guard<std::mutex> lock(state.stateMutex);
std::stringstream ss;
for (const auto& logLine : state.logs) {
ss << logLine << "\n";
}
log_editor.SetText(ss.str());
// Set error markers for lines containing Error/Failed
TextEditor::ErrorMarkers markers;
for (size_t i = 0; i < state.logs.size(); i++) {
const auto& line = state.logs[i];
if (line.find("Error") != std::string::npos ||
line.find("Failed") != std::string::npos ||
line.find("error") != std::string::npos) {
markers.insert(std::make_pair(static_cast<int>(i + 1), line));
}
}
log_editor.SetErrorMarkers(markers);
last_log_version = currentVersion;
}
}
// Dockspace
ImGuiID dockspace_id = ImGui::GetID("StudioDock");
ImGui::DockSpaceOverViewport(dockspace_id, ImGui::GetMainViewport());
static bool first_run = true;
if (first_run) {
first_run = false;
ImGui::DockBuilderRemoveNode(dockspace_id);
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspace_id, ImGui::GetMainViewport()->Size);
ImGuiID dock_left = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.22f, nullptr, &dockspace_id);
ImGuiID dock_right = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Right, 0.28f, nullptr, &dockspace_id);
ImGuiID dock_down = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Down, 0.25f, nullptr, &dockspace_id);
ImGui::DockBuilderDockWindow("Explorer", dock_left);
ImGui::DockBuilderDockWindow("Inspector", dock_right);
ImGui::DockBuilderDockWindow("Logs", dock_down);
ImGui::DockBuilderDockWindow("Workspace", dockspace_id);
ImGui::DockBuilderFinish(dockspace_id);
}
// File dialogs
ImGui::SetNextWindowSize(ImVec2(900, 600), ImGuiCond_FirstUseEver);
if (ImGuiFileDialog::Instance()->Display("ChooseFileDlgKey")) {
if (ImGuiFileDialog::Instance()->IsOk()) {
state.LoadELF(ImGuiFileDialog::Instance()->GetFilePathName());
}
ImGuiFileDialog::Instance()->Close();
}
if (ImGuiFileDialog::Instance()->Display("ImportGhidraKey")) {
if (ImGuiFileDialog::Instance()->IsOk()) {
std::string csvPath = ImGuiFileDialog::Instance()->GetFilePathName();
state.ImportGhidraCSV(csvPath);
// Add to imported CSV list (avoid duplicates)
bool alreadyImported = false;
for (const auto& existing : state.data.importedCSVFiles) {
if (existing == csvPath) { alreadyImported = true; break; }
}
if (!alreadyImported) {
state.data.importedCSVFiles.push_back(csvPath);
}
state.data.selectedCSVIndex = static_cast<int>(state.data.importedCSVFiles.size()) - 1;
ghidra_editor.SetText(state.data.ghidraCSVContent);
}
ImGuiFileDialog::Instance()->Close();
}
if (ImGuiFileDialog::Instance()->Display("ChooseDirDlgKey")) {
if (ImGuiFileDialog::Instance()->IsOk()) {
state.SetOutputDir(ImGuiFileDialog::Instance()->GetFilePathName());
}
ImGuiFileDialog::Instance()->Close();
}
// ---- Main Menu Bar ----
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("Open ELF...", "Ctrl+O")) {
IGFD::FileDialogConfig config;
config.path = ".";
ImGuiFileDialog::Instance()->OpenDialog("ChooseFileDlgKey", "Choose File", ".*,.elf", config);
}
if (ImGui::MenuItem("Import Ghidra CSV...")) {
IGFD::FileDialogConfig config;
config.path = ".";
ImGuiFileDialog::Instance()->OpenDialog("ImportGhidraKey", "Choose CSV", ".csv", config);
}
if (ImGui::MenuItem("Set Output Directory...")) {
IGFD::FileDialogConfig config;
config.path = ".";
ImGuiFileDialog::Instance()->OpenDialog("ChooseDirDlgKey", "Choose Output Dir", nullptr, config);
}
ImGui::Separator();
if (ImGui::MenuItem("Save Config", "Ctrl+S", false, state.data.isAnalysisComplete)) {
state.SaveConfigTOML();
config_editor_needs_sync = true;
}
if (ImGui::MenuItem("Reload Config from Disk")) {
state.LoadConfigToml();
config_editor_needs_sync = true;
}
ImGui::Separator();
if (ImGui::MenuItem("Exit")) s_wantsQuit = true;
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Tools")) {
bool busy = state.isBusy.load();
if (ImGui::MenuItem("Analyze", "F5", false, !busy && !state.data.elfPath.empty())) {
state.StartAnalysis();
}
if (ImGui::MenuItem("Recompile", "F7", false, !busy && state.data.isAnalysisComplete)) {
state.StartRecompilation();
}
ImGui::Separator();
if (ImGui::MenuItem("Run System Diagnostics", nullptr, false, !busy)) {
state.RunDiagnostics();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Settings")) {
if (ImGui::MenuItem("Open Settings Window")) {
show_settings_window = true;
}
ImGui::EndMenu();
}
// Show output path in menu bar
std::string pathDisplay = state.GetEffectiveOutputPath();
float pathWidth = ImGui::CalcTextSize(pathDisplay.c_str()).x;
if (ImGui::GetWindowWidth() > pathWidth + 300) {
ImGui::SameLine(ImGui::GetWindowWidth() - pathWidth - 20);
ImGui::TextDisabled("Output: %s", pathDisplay.c_str());
}
ImGui::EndMainMenuBar();
}
// ---- Keyboard Shortcuts ----
{
ImGuiIO& io = ImGui::GetIO();
bool noPopup = !ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId);
if (noPopup) {
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_O)) {
IGFD::FileDialogConfig config;
config.path = ".";
ImGuiFileDialog::Instance()->OpenDialog("ChooseFileDlgKey", "Choose File", ".*,.elf", config);
}
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S) && state.data.isAnalysisComplete) {
state.SaveConfigTOML();
config_editor_needs_sync = true;
}
bool busy = state.isBusy.load();
if (ImGui::IsKeyPressed(ImGuiKey_F5) && !busy && !state.data.elfPath.empty()) {
state.StartAnalysis();
}
if (ImGui::IsKeyPressed(ImGuiKey_F7) && !busy && state.data.isAnalysisComplete) {
state.StartRecompilation();
}
}
}
DrawSettingsWindow(state);
// ---- Explorer Panel ----
ImGui::Begin("Explorer");
ImGui::SetNextItemWidth(-1);
static char filterBuf[128] = "";
ImGui::InputTextWithHint("##Search", "Search functions...", filterBuf, 128);
ImGui::Separator();
if (state.data.isAnalysisComplete && state.data.analyzer) {
ImGui::BeginChild("FuncList", ImVec2(0, 0), false);
const auto& funcs = state.data.analyzer->getFunctions();
// Build filtered index list so clipper works correctly
std::string filterStr(filterBuf);
static std::vector<int> filteredIndices;
filteredIndices.clear();
filteredIndices.reserve(funcs.size());
for (int idx = 0; idx < static_cast<int>(funcs.size()); idx++) {
if (filterStr.empty() || funcs[idx].name.find(filterStr) != std::string::npos) {
filteredIndices.push_back(idx);
}
}
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(filteredIndices.size()));
while (clipper.Step()) {
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; row++) {
if (row < 0 || row >= static_cast<int>(filteredIndices.size())) continue;
int i = filteredIndices[row];
const auto& func = funcs[i];
OverrideStatus status = OverrideStatus::Default;
auto it = state.data.funcOverrides.find(func.name);
if (it != state.data.funcOverrides.end()) {
status = it->second;
}
ImVec4 color = ImGui::GetStyle().Colors[ImGuiCol_Text];
if (status == OverrideStatus::Stub) {
color = ImVec4(1.0f, 0.4f, 0.4f, 1.0f);
} else if (status == OverrideStatus::Skip) {
color = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
} else if (status == OverrideStatus::ForceRecompile) {
color = ImVec4(0.4f, 1.0f, 0.4f, 1.0f);
}
ImGui::PushStyleColor(ImGuiCol_Text, color);
bool isSelected = state.selectedFuncIndex == i;
if (ImGui::Selectable(func.name.c_str(), isSelected)) {
state.selectedFuncIndex = i;
std::stringstream codeSS;
codeSS << "// Function: " << func.name << "\n";
codeSS << "// Address: " << FormatHex(func.start) << "\n";
codeSS << "// Size: " << (func.end - func.start) << " bytes\n\n";
codeSS << "void " << func.name << "() {\n";
for (const auto& inst : func.instructions) {
codeSS << " // " << FormatHex(inst.address)
<< ": [0x" << std::hex << inst.opcode << "]\n";
}
codeSS << "}\n";
code_editor.SetText(codeSS.str());
// Update hex view to highlight and jump to this function
if (!state.data.rawElfData.empty() &&
func.start < state.data.rawElfData.size()) {
mem_edit.GotoAddrAndHighlight(func.start,
std::min(static_cast<size_t>(func.end), state.data.rawElfData.size()));
}
}
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Start: %s", FormatHex(func.start).c_str());
ImGui::Text("Size: %d bytes", (func.end - func.start));
ImGui::EndTooltip();
}
}
}
ImGui::EndChild();
} else {
float winW = ImGui::GetWindowSize().x;
float winH = ImGui::GetWindowSize().y;
if (state.data.elfPath.empty()) {
const char* txt = "No ELF loaded";
ImGui::SetCursorPos(ImVec2((winW - ImGui::CalcTextSize(txt).x) * 0.5f, winH * 0.4f));
ImGui::TextDisabled("%s", txt);
} else {
const char* txt = "Ready to Analyze";
ImGui::SetCursorPos(ImVec2((winW - ImGui::CalcTextSize(txt).x) * 0.5f, winH * 0.4f));
ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), "%s", txt);
ImGui::SetCursorPosX((winW - 120) * 0.5f);
if (ImGui::Button("Run Analysis", ImVec2(120, 30))) {
state.StartAnalysis();
}
}
}
ImGui::End();
// ---- Inspector Panel ----
ImGui::Begin("Inspector");
if (state.data.analyzer && state.selectedFuncIndex >= 0) {
const auto& funcs = state.data.analyzer->getFunctions();
if (state.selectedFuncIndex < static_cast<int>(funcs.size())) {
const auto& func = funcs[state.selectedFuncIndex];
if (ImGui::GetIO().Fonts->Fonts.Size > 0) {
ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]);
ImGui::TextColored(ImVec4(0.0f, 0.6f, 1.0f, 1.0f), "%s", func.name.c_str());
ImGui::PopFont();
} else {
ImGui::TextColored(ImVec4(0.0f, 0.6f, 1.0f, 1.0f), "%s", func.name.c_str());
}
ImGui::Separator();
ImGui::Spacing();
OverrideStatus current = OverrideStatus::Default;
auto it = state.data.funcOverrides.find(func.name);
if (it != state.data.funcOverrides.end()) {
current = it->second;
}
const char* options[] = { "Auto (Default)", "Stub (Plug)", "Skip (Ignore)", "Force Recompile" };
int idx = static_cast<int>(current);
ImGui::TextDisabled("Strategy:");
ImGui::SetNextItemWidth(-1);
if (ImGui::Combo("##Action", &idx, options, 4)) {
state.data.funcOverrides[func.name] = static_cast<OverrideStatus>(idx);
state.Log("Strategy changed for " + func.name);
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::BeginTable("MetaTable", 2)) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextDisabled("Address:");
ImGui::TableSetColumnIndex(1);
ImGui::Text("%s", FormatHex(func.start).c_str());
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextDisabled("Size:");
ImGui::TableSetColumnIndex(1);
ImGui::Text("%d bytes", (func.end - func.start));
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextDisabled("Instructions:");
ImGui::TableSetColumnIndex(1);
ImGui::Text("%zu", func.instructions.size());
ImGui::EndTable();
}
} else {
state.selectedFuncIndex = -1;
ImGui::TextDisabled("Select a function to inspect");
}
} else {
ImGui::TextDisabled("Select a function to inspect");
}
ImGui::End();
// ---- Workspace Panel (Tabs) ----
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.08f, 1.00f));
ImGui::Begin("Workspace", nullptr, ImGuiWindowFlags_NoTitleBar);
if (ImGui::BeginTabBar("Tabs", ImGuiTabBarFlags_Reorderable)) {
// C++ Preview Tab
if (ImGui::BeginTabItem(" C++ Preview ")) {
if (ImGui::BeginPopupContextItem("CppContext")) {
if (ImGui::MenuItem("Copy All")) {
ImGui::SetClipboardText(code_editor.GetText().c_str());
state.Log("C++ code copied to clipboard");
}
ImGui::EndPopup();
}
code_editor.Render("Editor");
ImGui::EndTabItem();
}
// Hex View Tab - with highlighting
if (ImGui::BeginTabItem(" Hex View ")) {
if (state.data.rawElfData.empty()) {
ImGui::TextDisabled("No binary data available (File not loaded or read error)");
} else {
// Setup hex highlights based on selected function
hex_highlight_ranges.clear();
if (state.data.analyzer && state.selectedFuncIndex >= 0) {
const auto& funcs = state.data.analyzer->getFunctions();
if (state.selectedFuncIndex < static_cast<int>(funcs.size())) {
const auto& func = funcs[state.selectedFuncIndex];
if (func.start < state.data.rawElfData.size()) {
HexHighlightRange hl;
hl.start = func.start;
hl.end = std::min(static_cast<size_t>(func.end), state.data.rawElfData.size());
hl.color = IM_COL32(0, 120, 215, 80);
hl.label = "Function: " + func.name;
hex_highlight_ranges.push_back(hl);
}
}
}
// Also highlight ELF header (first 52 bytes for 32-bit ELF)
if (state.data.rawElfData.size() >= 52) {
HexHighlightRange elfHeader;
elfHeader.start = 0;
elfHeader.end = 52;
elfHeader.color = IM_COL32(100, 180, 80, 40);
elfHeader.label = "ELF Header";
hex_highlight_ranges.push_back(elfHeader);
}
mem_edit.DrawContents(state.data.rawElfData.data(), state.data.rawElfData.size());
}
ImGui::EndTabItem();
}
// config.toml Tab
if (ImGui::BeginTabItem(" config.toml ")) {
// Toolbar for config editor
if (ImGui::Button("Save")) {
state.SaveConfigTomlFromEditor(config_editor.GetText());
}
ImGui::SameLine();
if (ImGui::Button("Reload from Disk")) {
state.LoadConfigToml();
config_editor.SetText(state.data.configTomlContent);
state.Log("config.toml reloaded from disk");
}
ImGui::SameLine();
if (ImGui::Button("Copy All")) {
ImGui::SetClipboardText(config_editor.GetText().c_str());
state.Log("config.toml copied to clipboard");
}
ImGui::SameLine();
if (!state.configTomlPath.empty()) {
ImGui::TextDisabled("Path: %s", state.configTomlPath.c_str());
}
ImGui::Separator();
config_editor.Render("ConfigEditor");
ImGui::EndTabItem();
}
// Imported CSV Files Tab - ALWAYS visible
if (ImGui::BeginTabItem(" Ghidra CSV ")) {
// Import button always available
if (ImGui::Button("Import CSV...")) {
IGFD::FileDialogConfig config;
config.path = ".";
ImGuiFileDialog::Instance()->OpenDialog("ImportGhidraKey", "Choose CSV", ".csv", config);
}
if (state.data.importedCSVFiles.empty()) {
ImGui::SameLine();
ImGui::TextDisabled("No CSV files imported yet");
} else {
ImGui::SameLine();
ImGui::TextDisabled("(%zu files)", state.data.importedCSVFiles.size());
}
ImGui::Separator();
// File list
if (!state.data.importedCSVFiles.empty()) {
ImGui::BeginChild("CSVList", ImVec2(0, 120), true);
for (size_t i = 0; i < state.data.importedCSVFiles.size(); i++) {
const auto& csvPath = state.data.importedCSVFiles[i];
std::string filename = std::filesystem::path(csvPath).filename().string();
ImGui::PushID(static_cast<int>(i));
bool isSelected = state.data.selectedCSVIndex == static_cast<int>(i);
if (ImGui::Selectable(filename.c_str(), isSelected)) {
state.data.selectedCSVIndex = static_cast<int>(i);
try {
std::ifstream file(csvPath);
if (file) {
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
ghidra_editor.SetText(content);
state.data.ghidraCSVPath = csvPath;
state.data.ghidraCSVContent = content;
}
} catch (...) {
state.Log("Error reading CSV: " + csvPath);
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", csvPath.c_str());
}
ImGui::SameLine();
if (ImGui::SmallButton("X")) {
state.data.importedCSVFiles.erase(state.data.importedCSVFiles.begin() + i);
if (state.data.selectedCSVIndex == static_cast<int>(i)) {
state.data.selectedCSVIndex = -1;
ghidra_editor.SetText("");
} else if (state.data.selectedCSVIndex > static_cast<int>(i)) {
state.data.selectedCSVIndex--;
}
ImGui::PopID();
break; // restart iteration next frame
}
ImGui::PopID();
}
ImGui::EndChild();
ImGui::Separator();
// Show content of selected CSV
if (state.data.selectedCSVIndex >= 0 &&
state.data.selectedCSVIndex < static_cast<int>(state.data.importedCSVFiles.size())) {
std::string selFileName = std::filesystem::path(
state.data.importedCSVFiles[state.data.selectedCSVIndex]).filename().string();
ImGui::TextDisabled("Content: %s", selFileName.c_str());
ImGui::Separator();
ghidra_editor.Render("GhidraEditor");
}
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
ImGui::PopStyleColor();
// ---- Logs Panel ----
ImGui::Begin("Logs");
// Status bar
{
std::string currentStatus = state.GetStatus();
if (state.isBusy.load()) {
float progress = std::fmod((float)ImGui::GetTime(), 1.0f);
ImGui::ProgressBar(progress, ImVec2(-1, 6), "");
ImGui::SameLine();
ImGui::Text("%s", currentStatus.c_str());
} else {
ImGui::TextDisabled("Status: %s", currentStatus.c_str());
}
}
// Log toolbar - right-aligned
{
float btnWidth = ImGui::CalcTextSize("Copy All").x + ImGui::CalcTextSize("Clear").x
+ ImGui::GetStyle().FramePadding.x * 4 + ImGui::GetStyle().ItemSpacing.x * 2 + 20;
ImGui::SameLine(std::max(0.0f, ImGui::GetWindowWidth() - btnWidth));
}
if (ImGui::SmallButton("Copy All")) {
std::lock_guard<std::mutex> lock(state.stateMutex);
std::stringstream allLogs;
for (const auto& log : state.logs) {
allLogs << log << "\n";
}
ImGui::SetClipboardText(allLogs.str().c_str());
}
ImGui::SameLine();
if (ImGui::SmallButton("Clear")) {
std::lock_guard<std::mutex> lock(state.stateMutex);
state.logs.clear();
state.logVersion.fetch_add(1);
}
ImGui::Separator();
// Log viewer using TextEditor - supports text selection, Ctrl+A, Ctrl+C, mouse drag
log_editor.Render("LogEditor");
ImGui::End();
}
+159
View File
@@ -0,0 +1,159 @@
#include "imgui.h"
#include "imgui_impl_sdl2.h"
#include "imgui_impl_opengl3.h"
#include <SDL.h>
#include <SDL_opengl.h>
#include "GUI.hpp"
#include "StudioState.hpp"
int main(int argc, char** argv) {
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) return -1;
SDL_DisplayMode displayMode;
SDL_GetCurrentDisplayMode(0, &displayMode);
int screenWidth = displayMode.w;
int screenHeight = displayMode.h;
int windowWidth = (int)(screenWidth * 0.8f);
int windowHeight = (int)(screenHeight * 0.8f);
SDL_WindowFlags window_flags = (SDL_WindowFlags)(
SDL_WINDOW_OPENGL |
SDL_WINDOW_RESIZABLE
);
// Pre-load window settings before StudioState construction
AppSettings tempSettings;
try {
std::ifstream file("studio_settings.ini");
if (file) {
std::string line;
while (std::getline(file, line)) {
size_t pos = line.find('=');
if (pos != std::string::npos) {
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
if (key == "maximized") {
tempSettings.maximized = (value == "1");
} else if (key == "windowWidth") {
tempSettings.windowWidth = std::stoi(value);
} else if (key == "windowHeight") {
tempSettings.windowHeight = std::stoi(value);
}
}
}
}
} catch(...) {}
if (tempSettings.maximized) {
window_flags = (SDL_WindowFlags)(window_flags | SDL_WINDOW_MAXIMIZED);
} else {
windowWidth = tempSettings.windowWidth;
windowHeight = tempSettings.windowHeight;
}
SDL_Window* window = SDL_CreateWindow(
"PS2Recomp Studio",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
windowWidth,
windowHeight,
window_flags
);
if (!window) {
SDL_Quit();
return -1;
}
SDL_GLContext gl_context = SDL_GL_CreateContext(window);
SDL_GL_MakeCurrent(window, gl_context);
SDL_GL_SetSwapInterval(1);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// Create StudioState AFTER ImGui context
StudioState state;
// Initial font and theme setup
GUI::ApplySettings(state);
ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
ImGui_ImplOpenGL3_Init("#version 130");
if (argc > 1) {
state.StartAnalysis(argv[1]);
}
bool done = false;
while (!done) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT) {
done = true;
}
if (event.type == SDL_WINDOWEVENT) {
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
state.settings.windowWidth = event.window.data1;
state.settings.windowHeight = event.window.data2;
}
if (event.window.event == SDL_WINDOWEVENT_MAXIMIZED) {
state.settings.maximized = true;
}
if (event.window.event == SDL_WINDOWEVENT_RESTORED) {
state.settings.maximized = false;
}
}
}
// Check GUI quit request (File > Exit)
if (GUI::WantsQuit()) {
done = true;
}
// CRITICAL: Handle deferred font rebuild BEFORE NewFrame
// This is the safe place to modify fonts - outside of the rendering frame
if (state.pendingFontRebuild.load()) {
// Rebuild fonts (clears atlas, adds fonts with new size)
GUI::RebuildFontsIfNeeded(state);
// Build the font atlas; the OpenGL3 backend will detect the
// change and upload the new texture automatically on NewFrame.
io.Fonts->Build();
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
GUI::DrawStudio(state);
ImGui::Render();
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(window);
}
// Save settings before shutdown
state.SaveSettings();
if (state.workerThread.valid()) {
state.workerThread.wait();
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
SDL_GL_DeleteContext(gl_context);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
+204
View File
@@ -0,0 +1,204 @@
#include "ui/StyleManager.hpp"
#include <filesystem>
#include <iostream>
#include <algorithm>
namespace StyleManager {
void SetupFonts(AppSettings& settings) {
if (ImGui::GetCurrentContext() == nullptr) {
return;
}
ImGuiIO& io = ImGui::GetIO();
// Validate and clamp font size
float safeFontSize = std::clamp(settings.fontSize, 10.0f, 48.0f);
settings.fontSize = safeFontSize;
// Clear existing fonts
io.Fonts->Clear();
std::string fontPath = "external/Font/" + settings.selectedFont;
ImFont* loadedFont = nullptr;
if (settings.selectedFont != "Default" && std::filesystem::exists(fontPath)) {
loadedFont = io.Fonts->AddFontFromFileTTF(fontPath.c_str(), safeFontSize);
if (!loadedFont) {
// TTF loading failed, use default
ImFontConfig config;
config.SizePixels = safeFontSize;
loadedFont = io.Fonts->AddFontDefault(&config);
}
} else {
ImFontConfig config;
config.SizePixels = safeFontSize;
loadedFont = io.Fonts->AddFontDefault(&config);
}
// Font atlas will be built automatically by the backend
}
void ApplyDarkTheme() {
if (ImGui::GetCurrentContext() == nullptr) return;
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
const ImVec4 bgBase = ImVec4(0.08f, 0.08f, 0.08f, 1.00f);
const ImVec4 bgPanel = ImVec4(0.13f, 0.13f, 0.14f, 1.00f);
const ImVec4 bgInput = ImVec4(0.18f, 0.18f, 0.19f, 1.00f);
const ImVec4 accentMain = ImVec4(0.00f, 0.48f, 0.80f, 1.00f);
const ImVec4 accentHover = ImVec4(0.10f, 0.55f, 0.90f, 1.00f);
const ImVec4 accentActive = ImVec4(0.00f, 0.40f, 0.70f, 1.00f);
const ImVec4 textBright = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
const ImVec4 textDim = ImVec4(0.70f, 0.70f, 0.70f, 1.00f);
colors[ImGuiCol_Text] = textBright;
colors[ImGuiCol_TextDisabled] = textDim;
colors[ImGuiCol_WindowBg] = bgBase;
colors[ImGuiCol_ChildBg] = bgPanel;
colors[ImGuiCol_PopupBg] = ImVec4(0.11f, 0.11f, 0.11f, 0.98f);
colors[ImGuiCol_Border] = ImVec4(0.30f, 0.30f, 0.30f, 0.60f);
colors[ImGuiCol_TitleBg] = ImVec4(0.06f, 0.06f, 0.06f, 1.00f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.05f, 0.05f, 0.05f, 1.00f);
colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.15f, 0.16f, 1.00f);
colors[ImGuiCol_ScrollbarBg] = bgBase;
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.35f, 0.35f, 0.35f, 1.00f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.45f, 0.45f, 0.45f, 1.00f);
colors[ImGuiCol_CheckMark] = accentMain;
colors[ImGuiCol_SliderGrab] = accentMain;
colors[ImGuiCol_SliderGrabActive] = accentActive;
colors[ImGuiCol_Button] = ImVec4(0.24f, 0.24f, 0.25f, 1.00f);
colors[ImGuiCol_ButtonHovered] = accentMain;
colors[ImGuiCol_ButtonActive] = accentActive;
colors[ImGuiCol_Header] = ImVec4(0.24f, 0.24f, 0.25f, 1.00f);
colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.28f, 0.29f, 1.00f);
colors[ImGuiCol_HeaderActive] = ImVec4(0.20f, 0.20f, 0.21f, 1.00f);
colors[ImGuiCol_FrameBg] = bgInput;
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.22f, 0.22f, 0.23f, 1.00f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0.24f, 0.24f, 0.25f, 1.00f);
colors[ImGuiCol_Tab] = ImVec4(0.08f, 0.08f, 0.08f, 1.00f);
colors[ImGuiCol_TabHovered] = bgPanel;
colors[ImGuiCol_TabActive] = bgPanel;
colors[ImGuiCol_TabUnfocused] = ImVec4(0.08f, 0.08f, 0.08f, 1.00f);
colors[ImGuiCol_TabUnfocusedActive] = bgPanel;
colors[ImGuiCol_DockingPreview] = accentMain;
colors[ImGuiCol_DockingEmptyBg] = bgBase;
colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.48f, 0.80f, 0.40f);
colors[ImGuiCol_NavHighlight] = accentMain;
}
void ApplyLightTheme() {
if (ImGui::GetCurrentContext() == nullptr) return;
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
const ImVec4 bgBase = ImVec4(0.95f, 0.95f, 0.95f, 1.00f);
const ImVec4 bgPanel = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
const ImVec4 bgInput = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
const ImVec4 accentMain = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
const ImVec4 accentHover = ImVec4(0.26f, 0.59f, 0.98f, 0.80f);
const ImVec4 accentActive = ImVec4(0.06f, 0.53f, 0.98f, 1.00f);
const ImVec4 textBright = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
const ImVec4 textDim = ImVec4(0.40f, 0.40f, 0.40f, 1.00f);
colors[ImGuiCol_Text] = textBright;
colors[ImGuiCol_TextDisabled] = textDim;
colors[ImGuiCol_WindowBg] = bgBase;
colors[ImGuiCol_ChildBg] = bgPanel;
colors[ImGuiCol_PopupBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.98f);
colors[ImGuiCol_Border] = ImVec4(0.70f, 0.70f, 0.70f, 0.60f);
colors[ImGuiCol_TitleBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.00f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.82f, 0.82f, 0.82f, 1.00f);
colors[ImGuiCol_MenuBarBg] = ImVec4(0.86f, 0.86f, 0.86f, 1.00f);
colors[ImGuiCol_ScrollbarBg] = bgBase;
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.69f, 0.69f, 0.69f, 1.00f);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.59f, 0.59f, 0.59f, 1.00f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.49f, 0.49f, 0.49f, 1.00f);
colors[ImGuiCol_CheckMark] = accentMain;
colors[ImGuiCol_SliderGrab] = accentMain;
colors[ImGuiCol_SliderGrabActive] = accentActive;
colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f);
colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f);
colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f);
colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
colors[ImGuiCol_FrameBg] = bgInput;
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f);
colors[ImGuiCol_Tab] = ImVec4(0.76f, 0.80f, 0.84f, 0.93f);
colors[ImGuiCol_TabHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f);
colors[ImGuiCol_TabActive] = ImVec4(0.60f, 0.73f, 0.88f, 1.00f);
colors[ImGuiCol_TabUnfocused] = ImVec4(0.92f, 0.93f, 0.94f, 0.98f);
colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.74f, 0.82f, 0.91f, 1.00f);
colors[ImGuiCol_DockingPreview] = accentMain;
colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f);
colors[ImGuiCol_NavHighlight] = accentMain;
}
void ApplyCustomTheme(AppSettings& settings) {
if (ImGui::GetCurrentContext() == nullptr) return;
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
ImVec4 bgBase(settings.customBgBase[0], settings.customBgBase[1],
settings.customBgBase[2], settings.customBgBase[3]);
ImVec4 accent(settings.customAccent[0], settings.customAccent[1],
settings.customAccent[2], settings.customAccent[3]);
colors[ImGuiCol_WindowBg] = bgBase;
colors[ImGuiCol_Button] = accent;
colors[ImGuiCol_ButtonHovered] = ImVec4(accent.x + 0.1f, accent.y + 0.1f, accent.z + 0.1f, accent.w);
colors[ImGuiCol_ButtonActive] = ImVec4(accent.x - 0.1f, accent.y - 0.1f, accent.z - 0.1f, accent.w);
}
void ApplyTheme(ThemeMode mode, AppSettings& settings) {
if (ImGui::GetCurrentContext() == nullptr) return;
ImGuiStyle& style = ImGui::GetStyle();
float safeScale = std::clamp(settings.uiScale, 0.5f, 3.0f);
style.WindowPadding = ImVec2(8 * safeScale, 8 * safeScale);
style.FramePadding = ImVec2(5 * safeScale, 4 * safeScale);
style.CellPadding = ImVec2(4 * safeScale, 4 * safeScale);
style.ItemSpacing = ImVec2(8 * safeScale, 6 * safeScale);
style.ItemInnerSpacing = ImVec2(6 * safeScale, 6 * safeScale);
style.IndentSpacing = 20 * safeScale;
style.ScrollbarSize = 12 * safeScale;
style.GrabMinSize = 10 * safeScale;
style.WindowBorderSize = 1 * safeScale;
style.ChildBorderSize = 1 * safeScale;
style.PopupBorderSize = 1 * safeScale;
style.FrameBorderSize = 0;
style.TabBorderSize = 0;
style.WindowRounding = 5.0f * safeScale;
style.ChildRounding = 5.0f * safeScale;
style.FrameRounding = 3.0f * safeScale;
style.PopupRounding = 5.0f * safeScale;
style.ScrollbarRounding = 9.0f * safeScale;
style.GrabRounding = 3.0f * safeScale;
style.TabRounding = 5.0f * safeScale;
switch(mode) {
case ThemeMode::Dark:
ApplyDarkTheme();
break;
case ThemeMode::Light:
ApplyLightTheme();
break;
case ThemeMode::Custom:
ApplyDarkTheme();
ApplyCustomTheme(settings);
break;
}
}
} // namespace StyleManager
+22 -2
View File
@@ -5,8 +5,8 @@ project(ps2xTest LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(ps2x_tests
src/main.cpp
# Static library with test logic (no main), used by ps2xStudio
add_library(ps2_test_lib STATIC
src/code_generator_tests.cpp
src/r5900_decoder_tests.cpp
src/elf_analyzer_tests.cpp
@@ -21,9 +21,28 @@ add_executable(ps2x_tests
src/ps2_runtime_expansion_tests.cpp
)
target_include_directories(ps2_test_lib PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/ps2xRecomp/include
${CMAKE_SOURCE_DIR}/ps2xAnalyzer/include
${CMAKE_SOURCE_DIR}/ps2xRuntime/include
)
target_link_libraries(ps2_test_lib PRIVATE
ps2_recomp_lib
ps2_analyzer_lib
ps2_runtime
)
add_executable(ps2x_tests
src/main.cpp
)
option(PRINT_GENERATED_CODE "Print generated code in tests" OFF)
if(PRINT_GENERATED_CODE)
target_compile_definitions(ps2x_tests PRIVATE PRINT_GENERATED_CODE)
target_compile_definitions(ps2_test_lib PRIVATE PRINT_GENERATED_CODE)
endif()
target_include_directories(ps2x_tests PRIVATE
@@ -34,6 +53,7 @@ target_include_directories(ps2x_tests PRIVATE
)
target_link_libraries(ps2x_tests PRIVATE
ps2_test_lib
ps2_recomp_lib
ps2_analyzer_lib
ps2_runtime