mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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();
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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", ¤t, 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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user