diff --git a/.gitmodules b/.gitmodules index 3fa5f9f563..c957c702d7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,9 +5,6 @@ [submodule "External/cpp-optparse"] path = Source/Common/cpp-optparse url = https://github.com/Sonicadvance1/cpp-optparse -[submodule "External/imgui"] - path = External/imgui - url = https://github.com/Sonicadvance1/imgui.git [submodule "External/xbyak"] shallow = true path = External/xbyak diff --git a/CMakeLists.txt b/CMakeLists.txt index ef122b0e55..f4b9108e53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ CHECK_INCLUDE_FILES ("gdb/jit-reader.h" HAVE_GDB_JIT_READER_H) option(BUILD_TESTS "Build unit tests to ensure sanity" TRUE) option(BUILD_FEX_LINUX_TESTS "Build FEXLinuxTests, requires x86 compiler" FALSE) option(BUILD_THUNKS "Build thunks" FALSE) -set(USE_FEXCONFIG_TOOLKIT "imgui" CACHE STRING "If set, build FEXConfig (qt or imgui)") +option(BUILD_FEXCONFIG "Build FEXConfig" TRUE) option(ENABLE_CLANG_THUNKS "Build thunks with clang" FALSE) option(ENABLE_IWYU "Enables include what you use program" FALSE) option(ENABLE_LTO "Enable LTO with compilation" TRUE) @@ -319,11 +319,6 @@ if (NOT fmt_FOUND) add_subdirectory(External/fmt/) endif() -if (USE_FEXCONFIG_TOOLKIT STREQUAL "imgui") - add_subdirectory(External/imgui/) - include_directories(External/imgui/) -endif() - add_subdirectory(External/tiny-json/) include_directories(External/tiny-json/) diff --git a/External/imgui b/External/imgui deleted file mode 160000 index 4c986ecb8d..0000000000 --- a/External/imgui +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4c986ecb8d2807087fd8e34894d1e7a138bc2f1d diff --git a/Source/Tools/CMakeLists.txt b/Source/Tools/CMakeLists.txt index ab785de3c6..0101bf319d 100644 --- a/Source/Tools/CMakeLists.txt +++ b/Source/Tools/CMakeLists.txt @@ -1,15 +1,13 @@ add_subdirectory(CommonTools) if (NOT MINGW_BUILD) - if (USE_FEXCONFIG_TOOLKIT STREQUAL "imgui") - add_subdirectory(FEXConfig/) - elseif (USE_FEXCONFIG_TOOLKIT STREQUAL "qt") + if (BUILD_FEXCONFIG) find_package(Qt6 COMPONENTS Qml Quick Widgets QUIET) if (NOT Qt6_FOUND) find_package(Qt5 COMPONENTS Qml Quick Widgets REQUIRED) endif() - add_subdirectory(FEXQonfig/) + add_subdirectory(FEXConfig/) endif() if (ENABLE_GDB_SYMBOLS) diff --git a/Source/Tools/FEXConfig/CMakeLists.txt b/Source/Tools/FEXConfig/CMakeLists.txt index 3afb233ed1..145a016347 100644 --- a/Source/Tools/FEXConfig/CMakeLists.txt +++ b/Source/Tools/FEXConfig/CMakeLists.txt @@ -1,31 +1,20 @@ -set(NAME FEXConfig) -set(SRCS Main.cpp - ${CMAKE_SOURCE_DIR}/External/imgui/examples/imgui_impl_sdl.cpp - ${CMAKE_SOURCE_DIR}/External/imgui/examples/imgui_impl_opengl3.cpp) - -find_library(EPOXY epoxy REQUIRED) -find_package(SDL2 REQUIRED) - -add_definitions(-DIMGUI_IMPL_OPENGL_LOADER_CUSTOM=) -add_executable(${NAME} ${SRCS}) - -target_include_directories(${NAME} PRIVATE ${CMAKE_SOURCE_DIR}/Source/) -target_include_directories(${NAME} PRIVATE ${CMAKE_SOURCE_DIR}/External/imgui/examples/) - -# Fix for SDL2 includes under Alpine Linux. -target_include_directories(${NAME} PRIVATE /usr/include/directfb/) - -if (TARGET SDL2::SDL2) - target_link_libraries(${NAME} PRIVATE SDL2::SDL2) +set(CMAKE_AUTOMOC ON) + +add_executable(FEXConfig) +target_sources(FEXConfig PRIVATE Main.cpp Main.h) +target_include_directories(FEXConfig PRIVATE ${CMAKE_SOURCE_DIR}/Source/) +target_link_libraries(FEXConfig PRIVATE Common) +if (Qt6_FOUND) + qt_add_resources(QT_RESOURCES qml6.qrc) + target_link_libraries(FEXConfig PRIVATE Qt6::Qml Qt6::Quick Qt6::Widgets) else() - target_include_directories(${NAME} PRIVATE ${SDL2_INCLUDE_DIRS}) - target_link_libraries(${NAME} PRIVATE ${SDL2_LIBRARIES}) + qt_add_resources(QT_RESOURCES qml5.qrc) + target_link_libraries(FEXConfig PRIVATE Qt5::Qml Qt5::Quick Qt5::Widgets) endif() - -target_link_libraries(${NAME} PRIVATE Common pthread epoxy X11 EGL imgui) +target_sources(FEXConfig PRIVATE ${QT_RESOURCES}) if (CMAKE_BUILD_TYPE MATCHES "RELEASE") - target_link_options(${NAME} + target_link_options(FEXConfig PRIVATE "LINKER:--gc-sections" "LINKER:--strip-all" @@ -33,7 +22,7 @@ if (CMAKE_BUILD_TYPE MATCHES "RELEASE") ) endif() -install(TARGETS ${NAME} +install(TARGETS FEXConfig RUNTIME DESTINATION bin COMPONENT runtime) diff --git a/Source/Tools/FEXConfig/Main.cpp b/Source/Tools/FEXConfig/Main.cpp index 2dcfced96f..5f17dfbd90 100644 --- a/Source/Tools/FEXConfig/Main.cpp +++ b/Source/Tools/FEXConfig/Main.cpp @@ -1,22 +1,19 @@ // SPDX-License-Identifier: MIT -#include "Common/Config.h" -#include "Common/FileFormatCheck.h" -#include "FEXCore/Config/Config.h" -#include "FEXCore/Utils/EnumUtils.h" -#include "Tools/CommonGUI/IMGui.h" - -#include -#include - -#include -#include -#include -#include -#include +#include "Main.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + #include -#include -#include -#include namespace fextl { // Helper to convert a std::filesystem::path to a fextl::string. @@ -24,999 +21,352 @@ inline fextl::string string_from_path(const std::filesystem::path& Path) { return Path.string().c_str(); } } // namespace fextl -namespace { -static std::chrono::time_point GlobalTime {}; -static bool ConfigOpen {}; -static bool ConfigChanged {}; -static int EnvironmentVariableSelected {}; -static int HostEnvironmentVariableSelected {}; -static int NamedRootFSSelected {-1}; - -static fextl::string ConfigFilename {}; static fextl::unique_ptr LoadedConfig {}; +static fextl::map> ConfigToNameLookup; +static fextl::map NameToConfigLookup; -static const char EnvironmentPopupName[] = "#New Environment Variable"; -static const char HostEnvironmentPopupName[] = "#New Host Environment Variable"; -static const char SavedPopupAppName[] = "#SavedApp"; -static const char OpenedPopupAppName[] = "#OpenedApp"; - -static bool OpenMsgPopup {}; -static bool SaveMsgIsOpen {}; -static std::string MsgMessage {}; -static const char MsgPopupName[] = "#Msg"; -static std::chrono::high_resolution_clock::time_point MsgTimerStart {}; - -static bool SelectedOpenFile {}; -static bool SelectedSaveFileAs {}; -static ImGuiFs::Dialog DialogSaveAs {}; -static ImGuiFs::Dialog DialogOpen {}; - -// Named rootfs -static std::vector NamedRootFS {}; -static std::mutex NamedRootFSUpdator {}; - -static std::atomic INotifyFD {-1}; -static int INotifyFolderFD {}; -static std::thread INotifyThreadHandle {}; -static std::atomic_bool INotifyShutdown {}; - -void OpenMsgMessagePopup(fextl::string Message) { - OpenMsgPopup = true; - MsgMessage = Message; - MsgTimerStart = std::chrono::high_resolution_clock::now(); - FEX::GUI::HadUpdate(); -} - -void LoadDefaultSettings() { - ConfigOpen = true; - ConfigFilename = {}; - LoadedConfig = fextl::make_unique(); -#define OPT_BASE(type, group, enum, json, default) LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(default)); -#define OPT_STR(group, enum, json, default) LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, default); -#define OPT_STRARRAY(group, enum, json, default) // Do nothing -#define OPT_STRENUM(group, enum, json, default) \ - LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(FEXCore::ToUnderlying(default))); -#include - - // Erase unnamed options which shouldn't be set - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS_INTERPRETER); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_INTERPRETER_INSTALLED); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_FILENAME); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_CONFIG_NAME); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS64BIT_MODE); +ConfigModel::ConfigModel() { + setItemRoleNames(QHash {{Qt::DisplayRole, "display"}, {Qt::UserRole + 1, "optionType"}, {Qt::UserRole + 2, "optionValue"}}); + Reload(); } -bool OpenFile(fextl::string Filename, bool LoadDefault = false) { - std::error_code ec {}; - if (!std::filesystem::exists(Filename, ec)) { - if (LoadDefault) { - LoadDefaultSettings(); - ConfigFilename = Filename; - OpenMsgMessagePopup("Opened with default options: " + Filename); - return true; - } - OpenMsgMessagePopup("Couldn't open: " + Filename); - return false; - } - ConfigOpen = true; - ConfigFilename = Filename; - LoadedConfig = FEX::Config::CreateMainLayer(&Filename); - LoadedConfig->Load(); - - // Load default options and only overwrite only if the option didn't exist -#define OPT_BASE(type, group, enum, json, default) \ - if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \ - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(default)); \ - } -#define OPT_STR(group, enum, json, default) \ - if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \ - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, default); \ - } -#define OPT_STRARRAY(group, enum, json, default) // Do nothing -#define OPT_STRENUM(group, enum, json, default) \ - if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \ - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(FEXCore::ToUnderlying(default))); \ - } -#include - - // Erase unnamed options which shouldn't be set - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS_INTERPRETER); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_INTERPRETER_INSTALLED); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_FILENAME); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_CONFIG_NAME); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS64BIT_MODE); - - return true; -} - -void LoadNamedRootFSFolder() { - std::scoped_lock lk {NamedRootFSUpdator}; - NamedRootFS.clear(); - fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/"; - std::error_code ec {}; - if (!std::filesystem::exists(RootFS, ec)) { - // Doesn't exist, create the the folder as a user convenience - if (!std::filesystem::create_directories(RootFS, ec)) { - // Well I guess we failed - return; - } - } - for (auto& it : std::filesystem::directory_iterator(RootFS)) { - if (it.is_directory()) { - NamedRootFS.emplace_back(it.path().filename()); - } else if (it.is_regular_file()) { - // If it is a regular file then we need to check if it is a valid archive - if (it.path().extension() == ".sqsh" && FEX::FormatCheck::IsSquashFS(fextl::string_from_path(it.path()))) { - NamedRootFS.emplace_back(it.path().filename()); - } else if (it.path().extension() == ".ero" && FEX::FormatCheck::IsEroFS(fextl::string_from_path(it.path()))) { - NamedRootFS.emplace_back(it.path().filename()); - } - } - } - std::sort(NamedRootFS.begin(), NamedRootFS.end()); -} - -void INotifyThread() { - while (!INotifyShutdown) { - constexpr size_t DATA_SIZE = (16 * (sizeof(struct inotify_event) + NAME_MAX + 1)); - char buf[DATA_SIZE]; - int Ret {}; - do { - fd_set Set {}; - FD_ZERO(&Set); - FD_SET(INotifyFD, &Set); - struct timeval tv {}; - // 50 ms - tv.tv_usec = 50000; - Ret = select(INotifyFD + 1, &Set, nullptr, nullptr, &tv); - } while (Ret == 0 && INotifyFD != -1); +void ConfigModel::Reload() { + auto Options = LoadedConfig->GetOptionMap(); - if (Ret == -1 || INotifyFD == -1) { - // Just return on error - INotifyShutdown = true; - return; + beginResetModel(); + removeRows(0, rowCount()); + for (auto& Option : Options) { + if (!LoadedConfig->OptionExists(Option.first)) { + continue; } - // Spin through the events, we don't actually care what they are - while (read(INotifyFD, buf, DATA_SIZE) > 0) - ; - - // Now update the named vector - LoadNamedRootFSFolder(); + auto& [Name, TypeId] = ConfigToNameLookup.find(Option.first)->second; + auto Item = new QStandardItem(QString::fromStdString(Name)); - FEX::GUI::HadUpdate(); + const char* OptionType = TypeId.data(); + Item->setData(OptionType, Qt::UserRole + 1); + Item->setData(QString::fromStdString(Option.second.front().c_str()), Qt::UserRole + 2); + appendRow(Item); } + endResetModel(); } -void SetupINotify() { - if (INotifyFD != -1) { - // Already setup - return; - } - - INotifyFD = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); - INotifyShutdown = false; - - fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/"; - INotifyFolderFD = inotify_add_watch(INotifyFD, RootFS.c_str(), IN_CREATE | IN_DELETE); - if (INotifyFolderFD != -1) { - INotifyThreadHandle = std::thread(INotifyThread); - } +bool ConfigModel::has(const QString& Name, bool) const { + auto Options = LoadedConfig->GetOptionMap(); + return LoadedConfig->OptionExists(NameToConfigLookup.at(Name.toStdString())); } -void ShutdownINotify() { - close(INotifyFD); - INotifyFD = -1; - if (INotifyThreadHandle.joinable()) { - INotifyThreadHandle.join(); - } +void ConfigModel::erase(const QString& Name) { + assert(has(Name, false)); + auto Options = LoadedConfig->GetOptionMap(); + LoadedConfig->Erase(NameToConfigLookup.at(Name.toStdString())); + Reload(); } -void SaveFile(fextl::string Filename) { - if (SaveMsgIsOpen) { - // Don't try saving a file while the message is already open. - // Stops us from spam saving the file to the filesystem. - return; - } +bool ConfigModel::getBool(const QString& Name, bool) const { + auto Options = LoadedConfig->GetOptionMap(); - if (!ConfigOpen) { - OpenMsgMessagePopup("Can't save file when config isn't open"); - return; + auto ret = LoadedConfig->Get(NameToConfigLookup.at(Name.toStdString())); + if (!ret || !*ret) { + throw std::runtime_error("Could not find setting"); } - - FEX::Config::SaveLayerToJSON(Filename, LoadedConfig.get()); - ConfigChanged = false; - ConfigFilename = Filename; - OpenMsgMessagePopup("Config Saved to: '" + Filename + "'"); - SaveMsgIsOpen = true; - - // Output in terminal as well - printf("Config Saved to: '%s'\n", ConfigFilename.c_str()); + return **ret == "1"; } -void CloseConfig() { - ConfigOpen = false; - ConfigFilename = {}; - ConfigChanged = false; - LoadedConfig.reset(); +void ConfigModel::setBool(const QString& Name, bool Value) { + auto Options = LoadedConfig->GetOptionMap(); + LoadedConfig->EraseSet(NameToConfigLookup.at(Name.toStdString()), Value ? "1" : "0"); + Reload(); } -void FillCPUConfig() { - char BlockSize[32] {}; - - if (ImGui::BeginTabItem("CPU")) { - std::optional Value {}; - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_MAXINST); - if (Value.has_value() && !(*Value)->empty()) { - strncpy(BlockSize, &(*Value)->at(0), 32); - } - if (ImGui::InputText("Block Size:", BlockSize, 32, ImGuiInputTextFlags_EnterReturnsTrue)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_MAXINST, BlockSize); - ConfigChanged = true; - } - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_MULTIBLOCK); - bool Multiblock = Value.has_value() && **Value == "1"; - if (ImGui::Checkbox("Multiblock", &Multiblock)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_MULTIBLOCK, Multiblock ? "1" : "0"); - ConfigChanged = true; - } - - ImGui::EndTabItem(); - } +void ConfigModel::setString(const QString& Name, const QString& Value) { + auto Options = LoadedConfig->GetOptionMap(); + LoadedConfig->EraseSet(NameToConfigLookup.at(Name.toStdString()), Value.toStdString()); + Reload(); } -template -bool EnvironmentVariableFiller(void* data, int idx, const char** out_text) { - static char TmpString[256]; - auto Value = LoadedConfig->All(Option); - if (Value.has_value()) { - auto List = (*Value); - auto it = List->begin(); - - // Since this is a list, we don't have a linear allocator that we can just jump to an element - // Just do a quick spin - for (int i = 0; i < idx; ++i) { - ++it; - } - - snprintf(TmpString, 256, "%s", it->c_str()); - *out_text = TmpString; +void ConfigModel::setStringList(const QString& Name, const QStringList& Values) { + auto Options = LoadedConfig->GetOptionMap(); - return true; + const auto& Option = NameToConfigLookup.at(Name.toStdString()); + LoadedConfig->Erase(Option); + for (auto& Value : Values) { + LoadedConfig->Set(Option, Value.toStdString().c_str()); } - - return false; + Reload(); } -bool NamedRootFSVariableFiller(void* data, int idx, const char** out_text) { - std::scoped_lock lk {NamedRootFSUpdator}; - static char TmpString[256]; - if (idx >= 0 && idx < NamedRootFS.size()) { - // Since this is a list, we don't have a linear allocator that we can just jump to an element - // Just do a quick spin - snprintf(TmpString, 256, "%s", NamedRootFS.at(idx).c_str()); - *out_text = TmpString; - - return true; - } - - return false; +void ConfigModel::setInt(const QString& Name, int Value) { + auto Options = LoadedConfig->GetOptionMap(); + LoadedConfig->EraseSet(NameToConfigLookup.at(Name.toStdString()), std::to_string(Value)); + Reload(); } +QString ConfigModel::getString(const QString& Name, bool) const { + auto Options = LoadedConfig->GetOptionMap(); -template -void DeleteEnvironmentVariable(int idx) { - auto Value = LoadedConfig->All(Option); - auto List = (*Value); - auto it = List->begin(); - - // Since this is a list, we don't have a linear allocator that we can just jump to an element - // Just do a quick spin - for (int i = 0; i < idx; ++i) { - ++it; + auto ret = LoadedConfig->Get(NameToConfigLookup.at(Name.toStdString())); + if (!ret || !*ret) { + throw std::runtime_error("Could not find setting"); } - - List->erase(it); - ConfigChanged = true; + return QString::fromUtf8((*ret)->c_str()); } -void AddNewEnvironmentVariable() { - char Environment[256] {}; - char HostEnvironment[256] {}; - - if (ImGui::BeginPopup(EnvironmentPopupName)) { - if (ImGui::InputText("New Environment", Environment, 256, ImGuiInputTextFlags_EnterReturnsTrue)) { - LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_ENV, Environment); - ImGui::CloseCurrentPopup(); - ConfigChanged = true; - } +QStringList ConfigModel::getStringList(const QString& Name, bool) const { + auto Options = LoadedConfig->GetOptionMap(); - ImGui::EndPopup(); + auto Values = LoadedConfig->All(NameToConfigLookup.at(Name.toStdString())); + if (!Values || !*Values) { + return {}; } - - ImGui::PushID(1); - if (ImGui::BeginPopup(HostEnvironmentPopupName)) { - if (ImGui::InputText("New Environment", HostEnvironment, 256, ImGuiInputTextFlags_EnterReturnsTrue)) { - LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_HOSTENV, HostEnvironment); - ImGui::CloseCurrentPopup(); - ConfigChanged = true; - } - - ImGui::EndPopup(); + QStringList Ret; + for (auto& Value : **Values) { + Ret.append(Value.c_str()); } - ImGui::PopID(); + return Ret; } -void FillEmulationConfig() { - char RootFS[256] {}; - char ThunkHostPath[256] {}; - char ThunkGuestPath[256] {}; - char ThunkConfigPath[256] {}; - - int NumEnvironmentVariables {}; - int NumHostEnvironmentVariables {}; - int NumRootFSPaths = NamedRootFS.size(); - - if (ImGui::BeginTabItem("Emulation")) { - auto Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_ROOTFS); - if (Value.has_value() && !(*Value)->empty()) { - strncpy(RootFS, &(*Value)->at(0), 256); - } - ImGui::Text("Available named RootFS folders: %d", NumRootFSPaths); - - if (ImGui::ListBox("Named RootFS folders", &NamedRootFSSelected, NamedRootFSVariableFiller, nullptr, NumRootFSPaths)) { - strncpy(RootFS, NamedRootFS.at(NamedRootFSSelected).c_str(), 256); - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_ROOTFS, RootFS); - ConfigChanged = true; - } - - if (ImGui::InputText("RootFS:", RootFS, 256, ImGuiInputTextFlags_EnterReturnsTrue)) { - NamedRootFSSelected = -1; - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_ROOTFS, RootFS); - ConfigChanged = true; - } - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_THUNKHOSTLIBS); - if (Value.has_value() && !(*Value)->empty()) { - strncpy(ThunkHostPath, &(*Value)->at(0), 256); - } - if (ImGui::InputText("Thunk Host library folder:", ThunkHostPath, 256, ImGuiInputTextFlags_EnterReturnsTrue)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_THUNKHOSTLIBS, ThunkHostPath); - ConfigChanged = true; - } - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_THUNKGUESTLIBS); - if (Value.has_value() && !(*Value)->empty()) { - strncpy(ThunkGuestPath, &(*Value)->at(0), 256); - } - if (ImGui::InputText("Thunk Guest library folder:", ThunkGuestPath, 256, ImGuiInputTextFlags_EnterReturnsTrue)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_THUNKGUESTLIBS, ThunkGuestPath); - ConfigChanged = true; - } - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_THUNKCONFIG); - if (Value.has_value() && !(*Value)->empty()) { - strncpy(ThunkConfigPath, &(*Value)->at(0), 256); - } - if (ImGui::InputText("Thunk Config file:", ThunkConfigPath, 256, ImGuiInputTextFlags_EnterReturnsTrue)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_THUNKCONFIG, ThunkConfigPath); - ConfigChanged = true; - } - - auto ValueList = LoadedConfig->All(FEXCore::Config::ConfigOption::CONFIG_ENV); - auto ValueHostList = LoadedConfig->All(FEXCore::Config::ConfigOption::CONFIG_HOSTENV); - if (ValueList.has_value()) { - NumEnvironmentVariables = (*ValueList)->size(); - } - - if (ValueHostList.has_value()) { - NumHostEnvironmentVariables = (*ValueHostList)->size(); - } - - ImGui::Text("Number of environment variables: %d", NumEnvironmentVariables); - - ImGui::ListBox("Environment variables", &EnvironmentVariableSelected, - EnvironmentVariableFiller, nullptr, NumEnvironmentVariables); - - if (ImGui::SmallButton("+")) { - ImGui::OpenPopup(EnvironmentPopupName); - } - - if (NumEnvironmentVariables) { - ImGui::SameLine(); - if (ImGui::SmallButton("-")) { - DeleteEnvironmentVariable(EnvironmentVariableSelected); - EnvironmentVariableSelected = std::max(0, EnvironmentVariableSelected - 1); - } - } - - - ImGui::PushID(1); - ImGui::Text("Number of Host environment variables: %d", NumHostEnvironmentVariables); - - ImGui::ListBox("Host Env variables", &HostEnvironmentVariableSelected, - EnvironmentVariableFiller, nullptr, NumHostEnvironmentVariables); - - if (ImGui::SmallButton("+")) { - ImGui::OpenPopup(HostEnvironmentPopupName); - } - - if (NumHostEnvironmentVariables) { - ImGui::SameLine(); - if (ImGui::SmallButton("-")) { - DeleteEnvironmentVariable(HostEnvironmentVariableSelected); - HostEnvironmentVariableSelected = std::max(0, HostEnvironmentVariableSelected - 1); - } - } - ImGui::PopID(); - - // Only draws if popup is open - AddNewEnvironmentVariable(); - - ImGui::Text("Debugging:"); - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_O0); - bool DisablePasses = Value.has_value() && **Value == "1"; - if (ImGui::Checkbox("Disable Optimization Passes", &DisablePasses)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_O0, DisablePasses ? "1" : "0"); - ConfigChanged = true; - } - - ImGui::Text("Ahead Of Time JIT Options:"); - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_AOTIRGENERATE); - bool AOTGenerate = Value.has_value() && **Value == "1"; - if (ImGui::Checkbox("Generate", &AOTGenerate)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_AOTIRGENERATE, AOTGenerate ? "1" : "0"); - ConfigChanged = true; - } - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_AOTIRCAPTURE); - bool AOTCapture = Value.has_value() && **Value == "1"; - if (ImGui::Checkbox("Capture", &AOTCapture)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_AOTIRCAPTURE, AOTCapture ? "1" : "0"); - ConfigChanged = true; - } - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_AOTIRLOAD); - bool AOTLoad = Value.has_value() && **Value == "1"; - if (ImGui::Checkbox("Load", &AOTLoad)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_AOTIRLOAD, AOTLoad ? "1" : "0"); - ConfigChanged = true; - } - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_CACHEOBJECTCODECOMPILATION); - - ImGui::Text("Cache JIT object code:"); - int CacheJITObjectCode = 0; - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_CACHEOBJECTCODECOMPILATION); - if (Value.has_value()) { - if (**Value == "0") { - CacheJITObjectCode = FEXCore::Config::ConfigObjectCodeHandler::CONFIG_NONE; - } else if (**Value == "1") { - CacheJITObjectCode = FEXCore::Config::ConfigObjectCodeHandler::CONFIG_READ; - } else if (**Value == "2") { - CacheJITObjectCode = FEXCore::Config::ConfigObjectCodeHandler::CONFIG_READWRITE; - } - } - - bool CacheChanged = false; - CacheChanged |= ImGui::RadioButton("Off", &CacheJITObjectCode, FEXCore::Config::ConfigObjectCodeHandler::CONFIG_NONE); - ImGui::SameLine(); - CacheChanged |= ImGui::RadioButton("Read-only", &CacheJITObjectCode, FEXCore::Config::ConfigObjectCodeHandler::CONFIG_READ); - ImGui::SameLine(); - CacheChanged |= ImGui::RadioButton("Read/Write", &CacheJITObjectCode, FEXCore::Config::ConfigObjectCodeHandler::CONFIG_READWRITE); - - if (CacheChanged) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_CACHEOBJECTCODECOMPILATION, std::to_string(CacheJITObjectCode)); - ConfigChanged = true; - } +int ConfigModel::getInt(const QString& Name, bool) const { + auto Options = LoadedConfig->GetOptionMap(); - ImGui::EndTabItem(); + auto ret = LoadedConfig->Get(NameToConfigLookup.at(Name.toStdString())); + if (!ret || !*ret) { + throw std::runtime_error("Could not find setting"); + } + int value; + auto res = std::from_chars(&*(*ret)->begin(), &*(*ret)->end(), value); + if (res.ptr != &*(*ret)->end()) { + throw std::runtime_error("Could not parse integer"); } + return value; } -void FillLoggingConfig() { - char LogFile[256] {}; - char IRDump[256] {}; +static void LoadDefaultSettings() { + LoadedConfig = fextl::make_unique(); +#define OPT_BASE(type, group, enum, json, default) LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(default)); +#define OPT_STR(group, enum, json, default) LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, default); +#define OPT_STRARRAY(group, enum, json, default) // Do nothing +#define OPT_STRENUM(group, enum, json, default) \ + LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(FEXCore::ToUnderlying(default))); +#include - if (ImGui::BeginTabItem("Logging")) { - auto Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_SILENTLOG); - bool SilentLog = Value.has_value() && **Value == "1"; - if (ImGui::Checkbox("Silent Logging", &SilentLog)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_SILENTLOG, SilentLog ? "1" : "0"); - ConfigChanged = true; - } + // Erase unnamed options which shouldn't be set + LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS_INTERPRETER); + LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_INTERPRETER_INSTALLED); + LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_FILENAME); + LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_CONFIG_NAME); + LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS64BIT_MODE); +} - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_OUTPUTLOG); - if (Value.has_value() && !(*Value)->empty()) { - strncpy(LogFile, &(*Value)->at(0), 256); - } - if (ImGui::InputText("Output log file:", LogFile, 256, ImGuiInputTextFlags_EnterReturnsTrue)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_OUTPUTLOG, LogFile); - ConfigChanged = true; - } +static void ConfigInit(fextl::string ConfigFilename) { +#define OPT_BASE(type, group, enum, json, default) \ + ConfigToNameLookup[FEXCore::Config::ConfigOption::CONFIG_##enum].first = #json; \ + ConfigToNameLookup[FEXCore::Config::ConfigOption::CONFIG_##enum].second = #type; \ + NameToConfigLookup[#json] = FEXCore::Config::ConfigOption::CONFIG_##enum; +#include +#undef OPT_BASE - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_DUMPIR); - if (Value.has_value() && !(*Value)->empty()) { - strncpy(IRDump, &(*Value)->at(0), 256); - } - if (ImGui::InputText("IR Dump location:", IRDump, 256, ImGuiInputTextFlags_EnterReturnsTrue)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_DUMPIR, IRDump); - ConfigChanged = true; + // Ensure config and RootFS directories exist + std::error_code ec {}; + std::filesystem::path Dirs[] = {std::filesystem::absolute(ConfigFilename).parent_path(), + std::filesystem::absolute(FEXCore::Config::GetDataDirectory()) / "RootFS/"}; + for (auto& Dir : Dirs) { + bool created = std::filesystem::create_directories(Dir, ec); + if (created) { + qInfo() << "Created folder" << Dir.c_str(); + } + if (ec) { + QMessageBox err(QMessageBox::Critical, "Failed to create directory", QString("Failed to create \"%1\" folder").arg(Dir.c_str()), + QMessageBox::Ok); + err.exec(); + std::exit(EXIT_FAILURE); + return; } - - ImGui::EndTabItem(); } } -void FillHackConfig() { - if (ImGui::BeginTabItem("Hacks")) { - auto Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_TSOENABLED); - auto VectorTSO = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_VECTORTSOENABLED); - auto MemcpyTSO = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_MEMCPYSETTSOENABLED); - auto StrictSplitLockAtomics = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_STRICTINPROCESSSPLITLOCKS); - auto HalfBarrierTSO = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_HALFBARRIERTSOENABLED); - - bool TSOEnabled = Value.has_value() && **Value == "1"; - bool VectorTSOEnabled = VectorTSO.has_value() && **VectorTSO == "1"; - bool MemcpyTSOEnabled = MemcpyTSO.has_value() && **MemcpyTSO == "1"; - bool StrictSplitLockAtomicsEnabled = StrictSplitLockAtomics.has_value() && **StrictSplitLockAtomics == "1"; - bool HalfBarrierTSOEnabled = HalfBarrierTSO.has_value() && **HalfBarrierTSO == "1"; - - if (ImGui::Checkbox("TSO Emulation Enabled", &TSOEnabled)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_TSOENABLED, TSOEnabled ? "1" : "0"); - ConfigChanged = true; - } - - if (TSOEnabled) { - if (ImGui::TreeNodeEx("TSO Emulation sub-options", ImGuiTreeNodeFlags_Leaf)) { - if (ImGui::Checkbox("Vector TSO Emulation Enabled", &VectorTSOEnabled)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_VECTORTSOENABLED, VectorTSOEnabled ? "1" : "0"); - ConfigChanged = true; - } - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("Disables TSO emulation on vector load/store instructions"); - ImGui::EndTooltip(); - } - - if (ImGui::Checkbox("Memcpy TSO Emulation Enabled", &MemcpyTSOEnabled)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_MEMCPYSETTSOENABLED, MemcpyTSOEnabled ? "1" : "0"); - ConfigChanged = true; - } - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("Disables TSO emulation on memcpy/memset instructions"); - ImGui::EndTooltip(); - } - - if (ImGui::Checkbox("Strict in-process split-lock atomics Enabled", &StrictSplitLockAtomicsEnabled)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_STRICTINPROCESSSPLITLOCKS, StrictSplitLockAtomicsEnabled ? "1" : "0"); - ConfigChanged = true; - } - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("Disables strict in-process split-lock atomics using a mutex"); - ImGui::EndTooltip(); - } - - if (ImGui::Checkbox("Unaligned Half-Barrier TSO Emulation Enabled", &HalfBarrierTSOEnabled)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_HALFBARRIERTSOENABLED, HalfBarrierTSOEnabled ? "1" : "0"); - ConfigChanged = true; - } - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("Disables half-barrier TSO emulation on unaligned load/store instructions"); - ImGui::EndTooltip(); - } - - ImGui::TreePop(); - } - } - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_PARANOIDTSO); - bool ParanoidTSOEnabled = Value.has_value() && **Value == "1"; - if (ImGui::Checkbox("Paranoid TSO Enabled", &ParanoidTSOEnabled)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_PARANOIDTSO, ParanoidTSOEnabled ? "1" : "0"); - ConfigChanged = true; - } - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_X87REDUCEDPRECISION); - bool X87ReducedPrecision = Value.has_value() && **Value == "1"; - if (ImGui::Checkbox("X87 Reduced Precision", &X87ReducedPrecision)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_X87REDUCEDPRECISION, X87ReducedPrecision ? "1" : "0"); - ConfigChanged = true; - } - - ImGui::Text("SMC Checks: "); - int SMCChecks = FEXCore::Config::CONFIG_SMC_MTRACK; - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_SMCCHECKS); - if (Value.has_value()) { - if (**Value == "0") { - SMCChecks = FEXCore::Config::CONFIG_SMC_NONE; - } else if (**Value == "1") { - SMCChecks = FEXCore::Config::CONFIG_SMC_MTRACK; - } else if (**Value == "2") { - SMCChecks = FEXCore::Config::CONFIG_SMC_FULL; - } - } - - bool SMCChanged = false; - SMCChanged |= ImGui::RadioButton("None", &SMCChecks, FEXCore::Config::CONFIG_SMC_NONE); - ImGui::SameLine(); - SMCChanged |= ImGui::RadioButton("MTrack (Default)", &SMCChecks, FEXCore::Config::CONFIG_SMC_MTRACK); - ImGui::SameLine(); - SMCChanged |= ImGui::RadioButton("Full", &SMCChecks, FEXCore::Config::CONFIG_SMC_FULL); - - if (SMCChanged) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_SMCCHECKS, std::to_string(SMCChecks)); - } - - Value = LoadedConfig->Get(FEXCore::Config::ConfigOption::CONFIG_ABILOCALFLAGS); - bool UnsafeLocalFlags = Value.has_value() && **Value == "1"; - if (ImGui::Checkbox("Unsafe local flags optimization", &UnsafeLocalFlags)) { - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_ABILOCALFLAGS, UnsafeLocalFlags ? "1" : "0"); - ConfigChanged = true; - } +RootFSModel::RootFSModel() { + INotifyFD = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); - ImGui::EndTabItem(); + fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/"; + FolderFD = inotify_add_watch(INotifyFD, RootFS.c_str(), IN_CREATE | IN_DELETE); + if (FolderFD != -1) { + Thread = std::thread {&RootFSModel::INotifyThreadFunc, this}; + } else { + qWarning() << "Could not set up inotify. RootFS folder won't be monitored for changes."; } -} - -static const std::map ConfigToNameLookup = {{ -#define OPT_BASE(type, group, enum, json, default) {FEXCore::Config::ConfigOption::CONFIG_##enum, #json}, -#include -}}; -struct TmpString { - char Str[256]; - std::string Name; -}; -static std::vector> AdvancedOptions {}; + // Load initial data + Reload(); +} -void UpdateAdvancedOptionsVector() { - AdvancedOptions.clear(); - // Push everything in to our vector table that we can modify instead of the map - auto Options = LoadedConfig->GetOptionMap(); - AdvancedOptions.resize(Options.size()); +RootFSModel::~RootFSModel() { + close(INotifyFD); + INotifyFD = -1; - size_t i = 0; - for (auto& Option : Options) { - auto ConfigName = ConfigToNameLookup.find(Option.first); - auto& AdvancedOption = AdvancedOptions.at(i); - AdvancedOption.resize(Option.second.size()); - size_t j = 0; - for (auto& OptionList : Option.second) { - strcpy(AdvancedOption[j].Str, OptionList.c_str()); - AdvancedOption[j].Name = ConfigName->second; - if (Option.second.size() > 1) { - AdvancedOption[j].Name += " " + std::to_string(j); - } - ++j; - } - ++i; - } + ExitRequest.count_down(); + Thread.join(); } -void FillAdvancedConfig() { - if (ImGui::BeginTabItem("Advanced")) { - auto Options = LoadedConfig->GetOptionMap(); +void RootFSModel::Reload() { + beginResetModel(); + removeRows(0, rowCount()); - if (ImGui::SmallButton("Refresh") || AdvancedOptions.size() != Options.size()) { - UpdateAdvancedOptionsVector(); - } - - if (Options.size()) { - ImGui::Columns(2); - size_t i = 0; - for (auto& Option : Options) { - auto ConfigName = ConfigToNameLookup.find(Option.first); - ImGui::Text("%s", ConfigName->second.c_str()); - ImGui::NextColumn(); - - auto& AdvancedOption = AdvancedOptions.at(i); - size_t j = 0; - - bool Stop = false; - for (auto& OptionList : AdvancedOption) { - // ImGui::Text("%s", OptionList.Str); - if (ImGui::InputText(OptionList.Name.c_str(), OptionList.Str, sizeof(TmpString), ImGuiInputTextFlags_EnterReturnsTrue)) { - if (Option.second.size() == 1) { - LoadedConfig->EraseSet(Option.first, OptionList.Str); - } else { - auto& All = Option.second; - auto Iter = All.begin(); - std::advance(Iter, j); - *Iter = OptionList.Str; - - LoadedConfig->Erase(Option.first); - for (auto& Value : All) { - LoadedConfig->Set(Option.first, Value); - } - } - ConfigChanged = true; - UpdateAdvancedOptionsVector(); - } - - ImGui::SameLine(); - - ImGui::PushID(OptionList.Name.c_str()); - if (ImGui::SmallButton("-")) { - if (Option.second.size() == 1) { - LoadedConfig->Erase(Option.first); - } else { - auto& All = Option.second; - auto Iter = All.begin(); - std::advance(Iter, j); - All.erase(Iter); - - LoadedConfig->Erase(Option.first); - for (auto& Value : All) { - LoadedConfig->Set(Option.first, Value); - } - } - Stop = true; - ConfigChanged = true; - UpdateAdvancedOptionsVector(); - } - ImGui::PopID(); - - ++j; - } - - ImGui::NextColumn(); - ++i; - if (Stop) { - break; - } + fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/"; + std::vector NamedRootFS {}; + for (auto& it : std::filesystem::directory_iterator(RootFS)) { + if (it.is_directory()) { + NamedRootFS.push_back(QString::fromStdString(it.path().filename())); + } else if (it.is_regular_file()) { + // If it is a regular file then we need to check if it is a valid archive + if (it.path().extension() == ".sqsh" && FEX::FormatCheck::IsSquashFS(fextl::string_from_path(it.path()))) { + NamedRootFS.push_back(QString::fromStdString(it.path().filename())); + } else if (it.path().extension() == ".ero" && FEX::FormatCheck::IsEroFS(fextl::string_from_path(it.path()))) { + NamedRootFS.push_back(QString::fromStdString(it.path().filename())); } } - ImGui::EndTabItem(); } + std::sort(NamedRootFS.begin(), NamedRootFS.end(), [](const QString& a, const QString& b) { return QString::localeAwareCompare(a, b) < 0; }); + for (auto& Entry : NamedRootFS) { + appendRow(new QStandardItem(Entry)); + } + + endResetModel(); } -void FillConfigWindow() { - ImGui::BeginTabBar("Config"); - FillCPUConfig(); - FillEmulationConfig(); - FillLoggingConfig(); - FillHackConfig(); - FillAdvancedConfig(); - ImGui::EndTabBar(); +bool RootFSModel::hasItem(const QString& Name) const { + return !findItems(Name, Qt::MatchExactly).empty(); } -bool DrawUI() { - ImGuiIO& io = ImGui::GetIO(); - auto current_time = std::chrono::high_resolution_clock::now(); - auto Diff = std::chrono::duration_cast(current_time - GlobalTime); - io.DeltaTime = Diff.count() > 0 ? Diff.count() : 1.0f / 60.0f; - GlobalTime = current_time; - - ImGui::NewFrame(); - - // We are using the ImGuiWindowFlags_NoDocking flag to make the parent window not dockable into, - // because it would be confusing to have two docking targets within each others. - ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking; - ImGuiViewport* viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(viewport->Pos); - ImGui::SetNextWindowSize(viewport->Size); - ImGui::SetNextWindowViewport(viewport->ID); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; - window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - ImGui::Begin("DockSpace", nullptr, window_flags); - ImGui::PopStyleVar(3); - - struct { - bool Open {}; - bool OpenDefault {}; - bool OpenAppProfile {}; - bool Save {}; - bool SaveAs {}; - bool SaveDefault {}; - bool Close {}; - bool Quit {}; - } Selected; - - char AppName[256] {}; - - if (ImGui::BeginMenuBar()) { - if (ImGui::BeginMenu("File")) { - ImGui::MenuItem("Open", "CTRL+O", &Selected.Open, true); - ImGui::MenuItem("Open from default location", "CTRL+SHIFT+O", &Selected.OpenDefault, true); - ImGui::MenuItem("Open App profile", "CTRL+I", &Selected.OpenAppProfile, true); - - ImGui::MenuItem("Save", "CTRL+S", &Selected.Save, true); - ImGui::MenuItem("Save As", "CTRL+SHIFT+S", &Selected.SaveAs, true); - ImGui::MenuItem("Save As App profile", "CTRL+E", nullptr, true); - ImGui::MenuItem("Save Default", "CTRL+SHIFT+P", &Selected.SaveDefault, true); - - ImGui::MenuItem("Close", "CTRL+W", &Selected.Close, true); - ImGui::MenuItem("Quit", "CTRL+Q", &Selected.Quit, true); - - ImGui::EndMenu(); - } +QUrl RootFSModel::getBaseUrl() const { + return QUrl::fromLocalFile(QString::fromStdString(FEXCore::Config::GetDataDirectory().c_str()) + "RootFS/"); +} - ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::GetFrameHeight()); +void RootFSModel::INotifyThreadFunc() { + while (!ExitRequest.try_wait()) { + constexpr size_t DATA_SIZE = (16 * (sizeof(struct inotify_event) + NAME_MAX + 1)); + char buf[DATA_SIZE]; + int Ret {}; + do { + fd_set Set {}; + FD_ZERO(&Set); + FD_SET(INotifyFD, &Set); + struct timeval tv {}; + // 50 ms + tv.tv_usec = 50000; + Ret = select(INotifyFD + 1, &Set, nullptr, nullptr, &tv); + } while (Ret == 0 && INotifyFD != -1); - if (ConfigOpen) { - if (ConfigChanged) { - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(1.0, 1.0, 0.0, 1.0)); - ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(1.0, 1.0, 0.0, 1.0)); - } else { - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.0, 1.0, 0.0, 1.0)); - ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.0, 1.0, 0.0, 1.0)); - } - } else { - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(1.0, 0.0, 0.0, 1.0)); - ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(1.0, 0.0, 0.0, 1.0)); + if (Ret == -1 || INotifyFD == -1) { + // Just return on error + return; } - ImGui::RadioButton("", true); - ImGui::PopStyleColor(2); + // Spin through the events, we don't actually care what they are + while (read(INotifyFD, buf, DATA_SIZE) > 0) + ; - ImGui::EndMenuBar(); + // Queue update to the data model + QMetaObject::invokeMethod(this, "Reload"); } +} - if (ConfigOpen) { - if (ImGui::BeginChild("#Config")) { - FillConfigWindow(); - } - - if (ImGui::IsKeyPressed(SDL_SCANCODE_E) && io.KeyCtrl && !io.KeyShift) { - ImGui::OpenPopup(SavedPopupAppName); - } - - ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x / 2, viewport->Pos.y + viewport->Size.y / 2), ImGuiCond_Appearing, - ImVec2(0.5f, 0.5f)); - if (ImGui::BeginPopupModal(SavedPopupAppName)) { - ImGui::SetKeyboardFocusHere(); - if (ImGui::InputText("App name", AppName, 256, ImGuiInputTextFlags_EnterReturnsTrue)) { - fextl::string AppNameString = AppName; - fextl::string Filename = FEXCore::Config::GetApplicationConfig(AppNameString, false); - SaveFile(Filename); - ImGui::CloseCurrentPopup(); - } - - if (ImGui::IsKeyPressed(SDL_SCANCODE_ESCAPE)) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - - ImGui::EndChild(); +// Returns true on success +static bool OpenFile(fextl::string Filename) { + std::error_code ec {}; + if (!std::filesystem::exists(Filename, ec)) { + return false; } - // Need this frame delay loop since ImGui doesn't allow us to enable a popup near the end of the frame - if (OpenMsgPopup) { - ImGui::OpenPopup(MsgPopupName); - OpenMsgPopup = false; - } + LoadedConfig = FEX::Config::CreateMainLayer(&Filename); + LoadedConfig->Load(); - if (Selected.OpenAppProfile || (ImGui::IsKeyPressed(SDL_SCANCODE_I) && io.KeyCtrl && !io.KeyShift)) { - ImGui::OpenPopup(OpenedPopupAppName); + // Load default options and only overwrite only if the option didn't exist +#define OPT_BASE(type, group, enum, json, default) \ + if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \ + LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(default)); \ } - - // Center the saved popup in the center of the window - ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x / 2, viewport->Pos.y + viewport->Size.y / 2), ImGuiCond_Appearing, - ImVec2(0.5f, 0.5f)); - - if (ImGui::BeginPopup(MsgPopupName)) { - ImGui::Text("%s", MsgMessage.c_str()); - if ((std::chrono::high_resolution_clock::now() - MsgTimerStart) >= std::chrono::seconds(2)) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } else if (SaveMsgIsOpen) { - SaveMsgIsOpen = false; +#define OPT_STR(group, enum, json, default) \ + if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \ + LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, default); \ } - - ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x / 2, viewport->Pos.y + viewport->Size.y / 2), ImGuiCond_Appearing, - ImVec2(0.5f, 0.5f)); - if (ImGui::BeginPopupModal(OpenedPopupAppName)) { - - ImGui::SetKeyboardFocusHere(); - if (ImGui::InputText("App name", AppName, 256, ImGuiInputTextFlags_EnterReturnsTrue)) { - fextl::string AppNameString = AppName; - fextl::string Filename = FEXCore::Config::GetApplicationConfig(AppNameString, false); - OpenFile(Filename, false); - ImGui::CloseCurrentPopup(); - } - - if (ImGui::IsKeyPressed(SDL_SCANCODE_ESCAPE)) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); +#define OPT_STRARRAY(group, enum, json, default) // Do nothing +#define OPT_STRENUM(group, enum, json, default) \ + if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \ + LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(FEXCore::ToUnderlying(default))); \ } +#include - if (Selected.Open || (ImGui::IsKeyPressed(SDL_SCANCODE_O) && io.KeyCtrl && !io.KeyShift)) { - SelectedOpenFile = true; - } - if (Selected.OpenDefault || (ImGui::IsKeyPressed(SDL_SCANCODE_O) && io.KeyCtrl && io.KeyShift)) { - if (OpenFile(FEXCore::Config::GetConfigFileLocation(), true)) { - LoadNamedRootFSFolder(); - SetupINotify(); - } - } + // Erase unnamed options which shouldn't be set + LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS_INTERPRETER); + LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_INTERPRETER_INSTALLED); + LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_FILENAME); + LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_CONFIG_NAME); + LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS64BIT_MODE); - if (Selected.Save || (ImGui::IsKeyPressed(SDL_SCANCODE_S) && io.KeyCtrl && !io.KeyShift)) { - SaveFile(ConfigFilename); - } - if (Selected.SaveAs || (ImGui::IsKeyPressed(SDL_SCANCODE_S) && io.KeyCtrl && io.KeyShift)) { - SelectedSaveFileAs = true; - } + return true; +} - if (Selected.SaveDefault || (ImGui::IsKeyPressed(SDL_SCANCODE_P) && io.KeyCtrl && io.KeyShift)) { - SaveFile(FEXCore::Config::GetConfigFileLocation()); - } - if (Selected.Close || (ImGui::IsKeyPressed(SDL_SCANCODE_W) && io.KeyCtrl && !io.KeyShift)) { - CloseConfig(); - ShutdownINotify(); - } +ConfigRuntime::ConfigRuntime(const QString& ConfigFilename) { + qmlRegisterSingletonInstance("FEX.ConfigModel", 1, 0, "ConfigModel", &ConfigModelInst); + qmlRegisterSingletonInstance("FEX.RootFSModel", 1, 0, "RootFSModel", &RootFSList); + Engine.load(QUrl("qrc:/main.qml")); - if (Selected.Quit || (ImGui::IsKeyPressed(SDL_SCANCODE_Q) && io.KeyCtrl && !io.KeyShift)) { - Selected.Quit = true; + Window = qobject_cast(Engine.rootObjects().first()); + if (!ConfigFilename.isEmpty()) { + Window->setProperty("configFilename", QUrl::fromLocalFile(ConfigFilename)); + } else { + Window->setProperty("configFilename", QUrl::fromLocalFile(FEXCore::Config::GetConfigFileLocation().c_str())); + Window->setProperty("configDirty", true); + Window->setProperty("loadedDefaults", true); } - ImGui::End(); // End dockspace - - const char* InitialPath; - const char* File; + ConfigRuntime::connect(Window, SIGNAL(selectedConfigFile(const QUrl&)), this, SLOT(onLoad(const QUrl&))); + ConfigRuntime::connect(Window, SIGNAL(triggeredSave(const QUrl&)), this, SLOT(onSave(const QUrl&))); + ConfigRuntime::connect(&ConfigModelInst, SIGNAL(modelReset()), Window, SLOT(refreshUI())); +} - InitialPath = DialogOpen.chooseFileDialog(SelectedOpenFile, "./", ".json", "#Chose a config to load"); - File = DialogOpen.getChosenPath(); - if (strlen(InitialPath) > 0 && strlen(File) > 0) { - OpenFile(File); - } +void ConfigRuntime::onSave(const QUrl& Filename) { + qInfo() << "Saving to" << Filename.toLocalFile().toStdString().c_str(); + FEX::Config::SaveLayerToJSON(Filename.toLocalFile().toStdString().c_str(), LoadedConfig.get()); +} - InitialPath = DialogSaveAs.saveFileDialog(SelectedSaveFileAs, "./", "Config.json", ".json", "#Choose where to save a config"); - File = DialogSaveAs.getChosenPath(); - if (strlen(InitialPath) > 0 && strlen(File) > 0) { - SaveFile(File); +void ConfigRuntime::onLoad(const QUrl& Filename) { + // TODO: Distinguish between "load" and "overlay". + // Currently, the new configuration is overlaid on top of the previous one. + + if (!OpenFile(Filename.toLocalFile().toStdString().c_str())) { + // This basically never happens because OpenFile performs no actual syntax checks. + // Treat as fatal since the UI state wouldn't be consistent after ignoring the error. + QMessageBox err(QMessageBox::Critical, tr("Could not load config file"), tr("Failed to load \"%1\"").arg(Filename.toLocalFile()), + QMessageBox::Ok); + err.exec(); + QApplication::exit(); + return; } - SelectedOpenFile = false; - SelectedSaveFileAs = false; - - ImGui::Render(); + ConfigModelInst.Reload(); + RootFSList.Reload(); - // Return true to keep rendering - return !Selected.Quit; + QMetaObject::invokeMethod(Window, "refreshUI"); } -} // namespace - -int main(int argc, char** argv) { - FEX::Config::InitializeConfigs(FEX::Config::PortableInformation {}); - fextl::string ImGUIConfig = FEXCore::Config::GetConfigDirectory(false) + "FEXConfig_imgui.ini"; - auto [window, gl_context] = FEX::GUI::SetupIMGui("#FEXConfig", ImGUIConfig); +int main(int Argc, char** Argv) { + QApplication App(Argc, Argv); - GlobalTime = std::chrono::high_resolution_clock::now(); + FEX::Config::InitializeConfigs(FEX::Config::PortableInformation {}); + fextl::string ConfigFilename = Argc > 1 ? Argv[1] : FEXCore::Config::GetConfigFileLocation(); + ConfigInit(ConfigFilename); - // Attempt to open the config passed in - if (argc > 1) { - if (OpenFile(argv[1], true)) { - LoadNamedRootFSFolder(); - SetupINotify(); - } - } else { - if (OpenFile(FEXCore::Config::GetConfigFileLocation(), true)) { - LoadNamedRootFSFolder(); - SetupINotify(); - } + qInfo() << "Opening" << ConfigFilename.c_str(); + if (!OpenFile(ConfigFilename)) { + // Load defaults if not found + ConfigFilename.clear(); + LoadDefaultSettings(); } - FEX::GUI::DrawUI(window, DrawUI); - - ShutdownINotify(); + ConfigRuntime Runtime(ConfigFilename.c_str()); - // Cleanup - FEX::GUI::Shutdown(window, gl_context); - return 0; + return App.exec(); } diff --git a/Source/Tools/FEXQonfig/Main.h b/Source/Tools/FEXConfig/Main.h similarity index 100% rename from Source/Tools/FEXQonfig/Main.h rename to Source/Tools/FEXConfig/Main.h diff --git a/Source/Tools/FEXQonfig/main.qml b/Source/Tools/FEXConfig/main.qml similarity index 100% rename from Source/Tools/FEXQonfig/main.qml rename to Source/Tools/FEXConfig/main.qml diff --git a/Source/Tools/FEXQonfig/qml5.qrc b/Source/Tools/FEXConfig/qml5.qrc similarity index 100% rename from Source/Tools/FEXQonfig/qml5.qrc rename to Source/Tools/FEXConfig/qml5.qrc diff --git a/Source/Tools/FEXQonfig/qml6.qrc b/Source/Tools/FEXConfig/qml6.qrc similarity index 100% rename from Source/Tools/FEXQonfig/qml6.qrc rename to Source/Tools/FEXConfig/qml6.qrc diff --git a/Source/Tools/FEXQonfig/qt5/FileDialog.qml b/Source/Tools/FEXConfig/qt5/FileDialog.qml similarity index 100% rename from Source/Tools/FEXQonfig/qt5/FileDialog.qml rename to Source/Tools/FEXConfig/qt5/FileDialog.qml diff --git a/Source/Tools/FEXQonfig/qt5/FolderDialog.qml b/Source/Tools/FEXConfig/qt5/FolderDialog.qml similarity index 100% rename from Source/Tools/FEXQonfig/qt5/FolderDialog.qml rename to Source/Tools/FEXConfig/qt5/FolderDialog.qml diff --git a/Source/Tools/FEXQonfig/qt5/MessageDialog.qml b/Source/Tools/FEXConfig/qt5/MessageDialog.qml similarity index 100% rename from Source/Tools/FEXQonfig/qt5/MessageDialog.qml rename to Source/Tools/FEXConfig/qt5/MessageDialog.qml diff --git a/Source/Tools/FEXQonfig/qt6/FileDialog.qml b/Source/Tools/FEXConfig/qt6/FileDialog.qml similarity index 100% rename from Source/Tools/FEXQonfig/qt6/FileDialog.qml rename to Source/Tools/FEXConfig/qt6/FileDialog.qml diff --git a/Source/Tools/FEXQonfig/qt6/FolderDialog.qml b/Source/Tools/FEXConfig/qt6/FolderDialog.qml similarity index 100% rename from Source/Tools/FEXQonfig/qt6/FolderDialog.qml rename to Source/Tools/FEXConfig/qt6/FolderDialog.qml diff --git a/Source/Tools/FEXQonfig/qt6/MessageDialog.qml b/Source/Tools/FEXConfig/qt6/MessageDialog.qml similarity index 100% rename from Source/Tools/FEXQonfig/qt6/MessageDialog.qml rename to Source/Tools/FEXConfig/qt6/MessageDialog.qml diff --git a/Source/Tools/FEXQonfig/CMakeLists.txt b/Source/Tools/FEXQonfig/CMakeLists.txt deleted file mode 100644 index 145a016347..0000000000 --- a/Source/Tools/FEXQonfig/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -set(CMAKE_AUTOMOC ON) - -add_executable(FEXConfig) -target_sources(FEXConfig PRIVATE Main.cpp Main.h) -target_include_directories(FEXConfig PRIVATE ${CMAKE_SOURCE_DIR}/Source/) -target_link_libraries(FEXConfig PRIVATE Common) -if (Qt6_FOUND) - qt_add_resources(QT_RESOURCES qml6.qrc) - target_link_libraries(FEXConfig PRIVATE Qt6::Qml Qt6::Quick Qt6::Widgets) -else() - qt_add_resources(QT_RESOURCES qml5.qrc) - target_link_libraries(FEXConfig PRIVATE Qt5::Qml Qt5::Quick Qt5::Widgets) -endif() -target_sources(FEXConfig PRIVATE ${QT_RESOURCES}) - -if (CMAKE_BUILD_TYPE MATCHES "RELEASE") - target_link_options(FEXConfig - PRIVATE - "LINKER:--gc-sections" - "LINKER:--strip-all" - "LINKER:--as-needed" - ) -endif() - -install(TARGETS FEXConfig - RUNTIME - DESTINATION bin - COMPONENT runtime) diff --git a/Source/Tools/FEXQonfig/Main.cpp b/Source/Tools/FEXQonfig/Main.cpp deleted file mode 100644 index 5f17dfbd90..0000000000 --- a/Source/Tools/FEXQonfig/Main.cpp +++ /dev/null @@ -1,372 +0,0 @@ -// SPDX-License-Identifier: MIT -#include "Main.h" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include - -namespace fextl { -// Helper to convert a std::filesystem::path to a fextl::string. -inline fextl::string string_from_path(const std::filesystem::path& Path) { - return Path.string().c_str(); -} -} // namespace fextl - -static fextl::unique_ptr LoadedConfig {}; -static fextl::map> ConfigToNameLookup; -static fextl::map NameToConfigLookup; - -ConfigModel::ConfigModel() { - setItemRoleNames(QHash {{Qt::DisplayRole, "display"}, {Qt::UserRole + 1, "optionType"}, {Qt::UserRole + 2, "optionValue"}}); - Reload(); -} - -void ConfigModel::Reload() { - auto Options = LoadedConfig->GetOptionMap(); - - beginResetModel(); - removeRows(0, rowCount()); - for (auto& Option : Options) { - if (!LoadedConfig->OptionExists(Option.first)) { - continue; - } - - auto& [Name, TypeId] = ConfigToNameLookup.find(Option.first)->second; - auto Item = new QStandardItem(QString::fromStdString(Name)); - - const char* OptionType = TypeId.data(); - Item->setData(OptionType, Qt::UserRole + 1); - Item->setData(QString::fromStdString(Option.second.front().c_str()), Qt::UserRole + 2); - appendRow(Item); - } - endResetModel(); -} - -bool ConfigModel::has(const QString& Name, bool) const { - auto Options = LoadedConfig->GetOptionMap(); - return LoadedConfig->OptionExists(NameToConfigLookup.at(Name.toStdString())); -} - -void ConfigModel::erase(const QString& Name) { - assert(has(Name, false)); - auto Options = LoadedConfig->GetOptionMap(); - LoadedConfig->Erase(NameToConfigLookup.at(Name.toStdString())); - Reload(); -} - -bool ConfigModel::getBool(const QString& Name, bool) const { - auto Options = LoadedConfig->GetOptionMap(); - - auto ret = LoadedConfig->Get(NameToConfigLookup.at(Name.toStdString())); - if (!ret || !*ret) { - throw std::runtime_error("Could not find setting"); - } - return **ret == "1"; -} - -void ConfigModel::setBool(const QString& Name, bool Value) { - auto Options = LoadedConfig->GetOptionMap(); - LoadedConfig->EraseSet(NameToConfigLookup.at(Name.toStdString()), Value ? "1" : "0"); - Reload(); -} - -void ConfigModel::setString(const QString& Name, const QString& Value) { - auto Options = LoadedConfig->GetOptionMap(); - LoadedConfig->EraseSet(NameToConfigLookup.at(Name.toStdString()), Value.toStdString()); - Reload(); -} - -void ConfigModel::setStringList(const QString& Name, const QStringList& Values) { - auto Options = LoadedConfig->GetOptionMap(); - - const auto& Option = NameToConfigLookup.at(Name.toStdString()); - LoadedConfig->Erase(Option); - for (auto& Value : Values) { - LoadedConfig->Set(Option, Value.toStdString().c_str()); - } - Reload(); -} - -void ConfigModel::setInt(const QString& Name, int Value) { - auto Options = LoadedConfig->GetOptionMap(); - LoadedConfig->EraseSet(NameToConfigLookup.at(Name.toStdString()), std::to_string(Value)); - Reload(); -} - -QString ConfigModel::getString(const QString& Name, bool) const { - auto Options = LoadedConfig->GetOptionMap(); - - auto ret = LoadedConfig->Get(NameToConfigLookup.at(Name.toStdString())); - if (!ret || !*ret) { - throw std::runtime_error("Could not find setting"); - } - return QString::fromUtf8((*ret)->c_str()); -} - -QStringList ConfigModel::getStringList(const QString& Name, bool) const { - auto Options = LoadedConfig->GetOptionMap(); - - auto Values = LoadedConfig->All(NameToConfigLookup.at(Name.toStdString())); - if (!Values || !*Values) { - return {}; - } - QStringList Ret; - for (auto& Value : **Values) { - Ret.append(Value.c_str()); - } - return Ret; -} - -int ConfigModel::getInt(const QString& Name, bool) const { - auto Options = LoadedConfig->GetOptionMap(); - - auto ret = LoadedConfig->Get(NameToConfigLookup.at(Name.toStdString())); - if (!ret || !*ret) { - throw std::runtime_error("Could not find setting"); - } - int value; - auto res = std::from_chars(&*(*ret)->begin(), &*(*ret)->end(), value); - if (res.ptr != &*(*ret)->end()) { - throw std::runtime_error("Could not parse integer"); - } - return value; -} - -static void LoadDefaultSettings() { - LoadedConfig = fextl::make_unique(); -#define OPT_BASE(type, group, enum, json, default) LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(default)); -#define OPT_STR(group, enum, json, default) LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, default); -#define OPT_STRARRAY(group, enum, json, default) // Do nothing -#define OPT_STRENUM(group, enum, json, default) \ - LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(FEXCore::ToUnderlying(default))); -#include - - // Erase unnamed options which shouldn't be set - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS_INTERPRETER); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_INTERPRETER_INSTALLED); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_FILENAME); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_CONFIG_NAME); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS64BIT_MODE); -} - -static void ConfigInit(fextl::string ConfigFilename) { -#define OPT_BASE(type, group, enum, json, default) \ - ConfigToNameLookup[FEXCore::Config::ConfigOption::CONFIG_##enum].first = #json; \ - ConfigToNameLookup[FEXCore::Config::ConfigOption::CONFIG_##enum].second = #type; \ - NameToConfigLookup[#json] = FEXCore::Config::ConfigOption::CONFIG_##enum; -#include -#undef OPT_BASE - - // Ensure config and RootFS directories exist - std::error_code ec {}; - std::filesystem::path Dirs[] = {std::filesystem::absolute(ConfigFilename).parent_path(), - std::filesystem::absolute(FEXCore::Config::GetDataDirectory()) / "RootFS/"}; - for (auto& Dir : Dirs) { - bool created = std::filesystem::create_directories(Dir, ec); - if (created) { - qInfo() << "Created folder" << Dir.c_str(); - } - if (ec) { - QMessageBox err(QMessageBox::Critical, "Failed to create directory", QString("Failed to create \"%1\" folder").arg(Dir.c_str()), - QMessageBox::Ok); - err.exec(); - std::exit(EXIT_FAILURE); - return; - } - } -} - -RootFSModel::RootFSModel() { - INotifyFD = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); - - fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/"; - FolderFD = inotify_add_watch(INotifyFD, RootFS.c_str(), IN_CREATE | IN_DELETE); - if (FolderFD != -1) { - Thread = std::thread {&RootFSModel::INotifyThreadFunc, this}; - } else { - qWarning() << "Could not set up inotify. RootFS folder won't be monitored for changes."; - } - - // Load initial data - Reload(); -} - -RootFSModel::~RootFSModel() { - close(INotifyFD); - INotifyFD = -1; - - ExitRequest.count_down(); - Thread.join(); -} - -void RootFSModel::Reload() { - beginResetModel(); - removeRows(0, rowCount()); - - fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/"; - std::vector NamedRootFS {}; - for (auto& it : std::filesystem::directory_iterator(RootFS)) { - if (it.is_directory()) { - NamedRootFS.push_back(QString::fromStdString(it.path().filename())); - } else if (it.is_regular_file()) { - // If it is a regular file then we need to check if it is a valid archive - if (it.path().extension() == ".sqsh" && FEX::FormatCheck::IsSquashFS(fextl::string_from_path(it.path()))) { - NamedRootFS.push_back(QString::fromStdString(it.path().filename())); - } else if (it.path().extension() == ".ero" && FEX::FormatCheck::IsEroFS(fextl::string_from_path(it.path()))) { - NamedRootFS.push_back(QString::fromStdString(it.path().filename())); - } - } - } - std::sort(NamedRootFS.begin(), NamedRootFS.end(), [](const QString& a, const QString& b) { return QString::localeAwareCompare(a, b) < 0; }); - for (auto& Entry : NamedRootFS) { - appendRow(new QStandardItem(Entry)); - } - - endResetModel(); -} - -bool RootFSModel::hasItem(const QString& Name) const { - return !findItems(Name, Qt::MatchExactly).empty(); -} - -QUrl RootFSModel::getBaseUrl() const { - return QUrl::fromLocalFile(QString::fromStdString(FEXCore::Config::GetDataDirectory().c_str()) + "RootFS/"); -} - -void RootFSModel::INotifyThreadFunc() { - while (!ExitRequest.try_wait()) { - constexpr size_t DATA_SIZE = (16 * (sizeof(struct inotify_event) + NAME_MAX + 1)); - char buf[DATA_SIZE]; - int Ret {}; - do { - fd_set Set {}; - FD_ZERO(&Set); - FD_SET(INotifyFD, &Set); - struct timeval tv {}; - // 50 ms - tv.tv_usec = 50000; - Ret = select(INotifyFD + 1, &Set, nullptr, nullptr, &tv); - } while (Ret == 0 && INotifyFD != -1); - - if (Ret == -1 || INotifyFD == -1) { - // Just return on error - return; - } - - // Spin through the events, we don't actually care what they are - while (read(INotifyFD, buf, DATA_SIZE) > 0) - ; - - // Queue update to the data model - QMetaObject::invokeMethod(this, "Reload"); - } -} - -// Returns true on success -static bool OpenFile(fextl::string Filename) { - std::error_code ec {}; - if (!std::filesystem::exists(Filename, ec)) { - return false; - } - - LoadedConfig = FEX::Config::CreateMainLayer(&Filename); - LoadedConfig->Load(); - - // Load default options and only overwrite only if the option didn't exist -#define OPT_BASE(type, group, enum, json, default) \ - if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \ - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(default)); \ - } -#define OPT_STR(group, enum, json, default) \ - if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \ - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, default); \ - } -#define OPT_STRARRAY(group, enum, json, default) // Do nothing -#define OPT_STRENUM(group, enum, json, default) \ - if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \ - LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(FEXCore::ToUnderlying(default))); \ - } -#include - - // Erase unnamed options which shouldn't be set - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS_INTERPRETER); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_INTERPRETER_INSTALLED); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_FILENAME); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_CONFIG_NAME); - LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS64BIT_MODE); - - return true; -} - -ConfigRuntime::ConfigRuntime(const QString& ConfigFilename) { - qmlRegisterSingletonInstance("FEX.ConfigModel", 1, 0, "ConfigModel", &ConfigModelInst); - qmlRegisterSingletonInstance("FEX.RootFSModel", 1, 0, "RootFSModel", &RootFSList); - Engine.load(QUrl("qrc:/main.qml")); - - Window = qobject_cast(Engine.rootObjects().first()); - if (!ConfigFilename.isEmpty()) { - Window->setProperty("configFilename", QUrl::fromLocalFile(ConfigFilename)); - } else { - Window->setProperty("configFilename", QUrl::fromLocalFile(FEXCore::Config::GetConfigFileLocation().c_str())); - Window->setProperty("configDirty", true); - Window->setProperty("loadedDefaults", true); - } - - ConfigRuntime::connect(Window, SIGNAL(selectedConfigFile(const QUrl&)), this, SLOT(onLoad(const QUrl&))); - ConfigRuntime::connect(Window, SIGNAL(triggeredSave(const QUrl&)), this, SLOT(onSave(const QUrl&))); - ConfigRuntime::connect(&ConfigModelInst, SIGNAL(modelReset()), Window, SLOT(refreshUI())); -} - -void ConfigRuntime::onSave(const QUrl& Filename) { - qInfo() << "Saving to" << Filename.toLocalFile().toStdString().c_str(); - FEX::Config::SaveLayerToJSON(Filename.toLocalFile().toStdString().c_str(), LoadedConfig.get()); -} - -void ConfigRuntime::onLoad(const QUrl& Filename) { - // TODO: Distinguish between "load" and "overlay". - // Currently, the new configuration is overlaid on top of the previous one. - - if (!OpenFile(Filename.toLocalFile().toStdString().c_str())) { - // This basically never happens because OpenFile performs no actual syntax checks. - // Treat as fatal since the UI state wouldn't be consistent after ignoring the error. - QMessageBox err(QMessageBox::Critical, tr("Could not load config file"), tr("Failed to load \"%1\"").arg(Filename.toLocalFile()), - QMessageBox::Ok); - err.exec(); - QApplication::exit(); - return; - } - - ConfigModelInst.Reload(); - RootFSList.Reload(); - - QMetaObject::invokeMethod(Window, "refreshUI"); -} - -int main(int Argc, char** Argv) { - QApplication App(Argc, Argv); - - FEX::Config::InitializeConfigs(FEX::Config::PortableInformation {}); - fextl::string ConfigFilename = Argc > 1 ? Argv[1] : FEXCore::Config::GetConfigFileLocation(); - ConfigInit(ConfigFilename); - - qInfo() << "Opening" << ConfigFilename.c_str(); - if (!OpenFile(ConfigFilename)) { - // Load defaults if not found - ConfigFilename.clear(); - LoadDefaultSettings(); - } - - ConfigRuntime Runtime(ConfigFilename.c_str()); - - return App.exec(); -}