switch to INI for settings (easier), make settings a singleton thingy, slightly simplify core pause/stop/reset. Drop support for remappable inputs for now

This commit is contained in:
irisz64
2025-06-27 00:50:47 +02:00
parent 38129d8d88
commit 7ca337ca48
18 changed files with 2279 additions and 137 deletions

2
.gitignore vendored
View File

@@ -6,7 +6,7 @@ saves/
.vscode/ .vscode/
out/ out/
*.toml *.toml
*.ini imgui.ini
*.z64 *.z64
*.n64 *.n64
*.v64 *.v64

2168
resources/gamecontrollerdb.h Normal file

File diff suppressed because it is too large Load Diff

8
resources/options.ini Normal file
View File

@@ -0,0 +1,8 @@
[general]
savepath=
[audio]
volumel=0.74
volumer=0.74
lock=true
[cpu]
type=interpreter

View File

@@ -8,22 +8,22 @@ Core::Core() : cpu(std::make_unique<Interpreter>(parallel)) {}
void Core::Stop() { void Core::Stop() {
pause = true; pause = true;
romLoaded = false; romLoaded = false;
rom = {};
cpu->Reset(); cpu->Reset();
} }
bool Core::LoadTAS(const fs::path &path) const { return cpu->GetMem().mmio.si.pif.movie.Load(path); } bool Core::LoadTAS(const fs::path &path) const { return cpu->GetMem().mmio.si.pif.movie.Load(path); }
void Core::LoadROM(const std::string &rom_) { void Core::LoadROM(const std::string &rom_) {
pause = true; Stop();
rom = rom_; rom = rom_;
cpu->Reset(); cpu->Reset();
romLoaded = true;
std::string archive_types[] = {".zip", ".7z", ".rar", ".tar"}; std::string archive_types[] = {".zip", ".7z", ".rar", ".tar"};
auto extension = fs::path(rom).extension().string(); auto extension = fs::path(rom).extension().string();
const bool isArchive = std::ranges::any_of(archive_types, [&extension](const auto &e) { return e == extension; }); const bool isArchive = std::ranges::any_of(archive_types, [&extension](const auto &e) { return e == extension; });
cpu->GetMem().LoadROM(isArchive, rom); cpu->GetMem().LoadROM(isArchive, rom);
GameDB::match(cpu->GetMem()); GameDB::match(cpu->GetMem());
if (cpu->GetMem().rom.gameNameDB.empty()) { if (cpu->GetMem().rom.gameNameDB.empty()) {
@@ -37,6 +37,7 @@ void Core::LoadROM(const std::string &rom_) {
cpu->GetMem().LoadSRAM(cpu->GetMem().saveType, rom); cpu->GetMem().LoadSRAM(cpu->GetMem().saveType, rom);
cpu->GetMem().mmio.si.pif.Execute(); cpu->GetMem().mmio.si.pif.Execute();
pause = false; pause = false;
romLoaded = true;
} }
void Core::Run(float volumeL, float volumeR) { void Core::Run(float volumeL, float volumeR) {

View File

@@ -1,15 +1,15 @@
#include <AudioSettings.hpp> #include <AudioSettings.hpp>
AudioSettings::AudioSettings(nlohmann::json &settings) : settings(settings) { AudioSettings::AudioSettings() {
lockChannels.setChecked(JSONGetField<bool>(settings, "audio", "lock")); lockChannels.setChecked(Options::GetInstance().GetValue<bool>("audio", "lock"));
volumeL.setValue(JSONGetField<float>(settings, "audio", "volumeL") * 100); volumeL.setValue(Options::GetInstance().GetValue<float>("audio", "volumeL") * 100);
volumeR.setValue(JSONGetField<float>(settings, "audio", "volumeR") * 100); volumeR.setValue(Options::GetInstance().GetValue<float>("audio", "volumeR") * 100);
} }
bool AudioSettings::render() { bool AudioSettings::render() {
if(lockChannels.render()) { if(lockChannels.render()) {
auto isChecked = lockChannels.isChecked(); auto isChecked = lockChannels.isChecked();
JSONSetField(settings, "audio", "lock", isChecked); Options::GetInstance().SetValue("audio", "lock", isChecked);
if (isChecked) { if (isChecked) {
volumeR.setValue(volumeL.getValue()); volumeR.setValue(volumeL.getValue());
} }
@@ -21,10 +21,10 @@ bool AudioSettings::render() {
if(volumeL.render()) { if(volumeL.render()) {
float valueL = volumeL.getValue(); float valueL = volumeL.getValue();
JSONSetField(settings, "audio", "volumeL", float(valueL) / 100.f); Options::GetInstance().SetValue("audio", "volumeL", float(valueL) / 100.f);
if (lockChannels.isChecked()) { if (lockChannels.isChecked()) {
volumeR.setValue(valueL); volumeR.setValue(valueL);
JSONSetField(settings, "audio", "volumeR", float(valueL) / 100.f); Options::GetInstance().SetValue("audio", "volumeR", float(valueL) / 100.f);
} }
modified = true; modified = true;
@@ -34,7 +34,7 @@ bool AudioSettings::render() {
if(volumeR.render()) { if(volumeR.render()) {
if (!lockChannels.isChecked()) { if (!lockChannels.isChecked()) {
JSONSetField(settings, "audio", "volumeR", float(volumeR.getValue()) / 100.f); Options::GetInstance().SetValue("audio", "volumeR", float(volumeR.getValue()) / 100.f);
} }
modified = true; modified = true;

View File

@@ -1,5 +1,5 @@
#pragma once #pragma once
#include <JSONUtils.hpp> #include <Options.hpp>
#include <ImGuiImpl/Checkbox.hpp> #include <ImGuiImpl/Checkbox.hpp>
#include <ImGuiImpl/Slider.hpp> #include <ImGuiImpl/Slider.hpp>
@@ -9,9 +9,8 @@ class AudioSettings final {
public: public:
gui::SliderFloat volumeL{"Volume L", 0.f, 100.f, 0.f}; gui::SliderFloat volumeL{"Volume L", 0.f, 100.f, 0.f};
gui::SliderFloat volumeR{"Volume R", 0.f, 100.f, 0.f}; gui::SliderFloat volumeR{"Volume R", 0.f, 100.f, 0.f};
explicit AudioSettings(nlohmann::json &); explicit AudioSettings();
bool render(); bool render();
void setModified(bool v) { modified = v; } void setModified(bool v) { modified = v; }
bool getModified() { return modified; } bool getModified() { return modified; }
nlohmann::json &settings;
}; };

View File

@@ -49,6 +49,7 @@ include_directories(
../../external/imgui/backends ../../external/imgui/backends
../../external/nfd/src/include ../../external/nfd/src/include
../../external/cflags/include ../../external/cflags/include
../../external/mINI/src/
ImGuiImpl/ ImGuiImpl/
) )
@@ -109,6 +110,7 @@ add_subdirectory(../../external/unarr unarr)
add_subdirectory(../../external/SDL SDL) add_subdirectory(../../external/SDL SDL)
add_subdirectory(../../external/cflags cflags) add_subdirectory(../../external/cflags cflags)
add_subdirectory(../../external/imgui imgui) add_subdirectory(../../external/imgui imgui)
add_subdirectory(../../external/mINI mINI)
add_subdirectory(../../external/nfd nfd) add_subdirectory(../../external/nfd nfd)
set(CAPSTONE_ARCHITECTURE_DEFAULT OFF) set(CAPSTONE_ARCHITECTURE_DEFAULT OFF)
set(CAPSTONE_MIPS_SUPPORT ON) set(CAPSTONE_MIPS_SUPPORT ON)
@@ -127,10 +129,10 @@ add_executable(kaizen
SettingsWindow.cpp SettingsWindow.cpp
CPUSettings.hpp CPUSettings.hpp
CPUSettings.cpp CPUSettings.cpp
JSONUtils.hpp
AudioSettings.hpp AudioSettings.hpp
AudioSettings.cpp AudioSettings.cpp
NativeWindow.hpp NativeWindow.hpp
../utils/Options.cpp
Debugger.hpp Debugger.hpp
Debugger.cpp) Debugger.cpp)

View File

@@ -1,9 +1,9 @@
#include <CPUSettings.hpp> #include <CPUSettings.hpp>
#include <JSONUtils.hpp> #include <Options.hpp>
#include <log.hpp> #include <log.hpp>
CPUSettings::CPUSettings(nlohmann::json &settings) : settings(settings) { CPUSettings::CPUSettings() {
if (JSONGetField<std::string>(settings, "cpu_type") == "jit") { if (Options::GetInstance().GetValue<std::string>("cpu", "type") == "jit") {
cpuTypes.setCurrentIndex(1); cpuTypes.setCurrentIndex(1);
} else { } else {
cpuTypes.setCurrentIndex(0); cpuTypes.setCurrentIndex(0);
@@ -13,7 +13,7 @@ CPUSettings::CPUSettings(nlohmann::json &settings) : settings(settings) {
bool CPUSettings::render() { bool CPUSettings::render() {
if(cpuTypes.render()) { if(cpuTypes.render()) {
if(cpuTypes.getCurrentIndex() == 0) { if(cpuTypes.getCurrentIndex() == 0) {
JSONSetField(settings, "cpu_type", "interpreter"); Options::GetInstance().SetValue<std::string>("cpu", "type", "interpreter");
} else { } else {
Util::panic("JIT should not be selectable??"); Util::panic("JIT should not be selectable??");
} }

View File

@@ -1,5 +1,5 @@
#pragma once #pragma once
#include <JSONUtils.hpp> #include <Options.hpp>
#include <ImGuiImpl/Combobox.hpp> #include <ImGuiImpl/Combobox.hpp>
class CPUSettings final { class CPUSettings final {
@@ -7,8 +7,7 @@ class CPUSettings final {
bool modified = false; bool modified = false;
public: public:
bool render(); bool render();
explicit CPUSettings(nlohmann::json &); explicit CPUSettings();
void setModified(bool v) { modified = v; } void setModified(bool v) { modified = v; }
bool getModified() { return modified; } bool getModified() { return modified; }
nlohmann::json &settings;
}; };

View File

@@ -7,8 +7,8 @@ EmuThread::EmuThread(const std::shared_ptr<n64::Core> &core, double &fps, Render
renderWidget(renderWidget), core(core), settings(settings), fps(fps) {} renderWidget(renderWidget), core(core), settings(settings), fps(fps) {}
void EmuThread::run() noexcept { void EmuThread::run() noexcept {
isRunning = true; if(!core->romLoaded) return;
auto lastSample = std::chrono::high_resolution_clock::now(); auto lastSample = std::chrono::high_resolution_clock::now();
auto avgFps = 16.667; auto avgFps = 16.667;
auto sampledFps = 0; auto sampledFps = 0;
@@ -49,15 +49,11 @@ void EmuThread::TogglePause() const noexcept {
} }
void EmuThread::Reset() const noexcept { void EmuThread::Reset() const noexcept {
core->pause = true;
core->Stop(); core->Stop();
core->LoadROM(core->rom); core->LoadROM(core->rom);
core->pause = false;
} }
void EmuThread::Stop() const noexcept { void EmuThread::Stop() const noexcept {
Util::RPC::GetInstance().Update(Util::RPC::Idling); Util::RPC::GetInstance().Update(Util::RPC::Idling);
core->rom = {};
core->pause = true;
core->Stop(); core->Stop();
} }

View File

@@ -19,7 +19,7 @@ public:
void Reset() const noexcept; void Reset() const noexcept;
void Stop() const noexcept; void Stop() const noexcept;
bool interruptionRequested = false, isRunning = false, parallelRDPInitialized = false; bool interruptionRequested = false, parallelRDPInitialized = false;
std::shared_ptr<n64::Core> core; std::shared_ptr<n64::Core> core;
SettingsWindow &settings; SettingsWindow &settings;
double& fps; double& fps;

View File

@@ -1,54 +0,0 @@
#pragma once
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
#include <common.hpp>
namespace fs = std::filesystem;
static FORCE_INLINE nlohmann::json JSONOpenOrCreate(const std::string &path) {
auto fileExists = fs::exists(path);
std::fstream file;
nlohmann::json json;
if (fileExists) {
file = std::fstream(path, std::fstream::in | std::fstream::out);
json = nlohmann::json::parse(file);
file.close();
return json;
}
file = std::fstream(path, std::fstream::in | std::fstream::out | std::fstream::trunc);
json["general"]["savePath"] = "";
json["audio"]["volumeL"] = 0.5;
json["audio"]["volumeR"] = 0.5;
json["audio"]["lock"] = true;
json["cpu_type"] = "interpreter";
file << json;
file.close();
return json;
}
template <typename T>
static FORCE_INLINE void JSONSetField(nlohmann::json &json, const std::string &key, const std::string &field,
const T &value) {
json[key][field] = value;
}
template <typename T>
static FORCE_INLINE T JSONGetField(nlohmann::json &json, const std::string &key, const std::string &field) {
return json[key][field].get<T>();
}
template <typename T>
static FORCE_INLINE void JSONSetField(nlohmann::json &json, const std::string &key, const T &value) {
json[key] = value;
}
template <typename T>
static FORCE_INLINE T JSONGetField(nlohmann::json &json, const std::string &key) {
return json[key].get<T>();
}

View File

@@ -3,6 +3,7 @@
#include <backend/Core.hpp> #include <backend/Core.hpp>
#include <ImGuiImpl/StatusBar.hpp> #include <ImGuiImpl/StatusBar.hpp>
#include <ImGuiImpl/GUI.hpp> #include <ImGuiImpl/GUI.hpp>
#include <resources/gamecontrollerdb.h>
bool HandleInput(void *userdata, SDL_Event *event); bool HandleInput(void *userdata, SDL_Event *event);
bool QueryDevices(void *userdata, SDL_Event *event); bool QueryDevices(void *userdata, SDL_Event *event);
@@ -10,6 +11,7 @@ bool QueryDevices(void *userdata, SDL_Event *event);
KaizenGui::KaizenGui() noexcept : window("Kaizen", 800, 600), core(std::make_shared<n64::Core>()), vulkanWidget(core, window.getHandle()), emuThread(core, fpsCounter, vulkanWidget, settingsWindow) { KaizenGui::KaizenGui() noexcept : window("Kaizen", 800, 600), core(std::make_shared<n64::Core>()), vulkanWidget(core, window.getHandle()), emuThread(core, fpsCounter, vulkanWidget, settingsWindow) {
gui::Initialize(core->parallel.wsi, window.getHandle()); gui::Initialize(core->parallel.wsi, window.getHandle());
SDL_AddGamepadMapping(gamecontrollerdb_str);
SDL_AddEventWatch(HandleInput, this); SDL_AddEventWatch(HandleInput, this);
SDL_AddEventWatch(QueryDevices, this); SDL_AddEventWatch(QueryDevices, this);
@@ -59,6 +61,7 @@ KaizenGui::KaizenGui() noexcept : window("Kaizen", 800, 600), core(std::make_sha
if(result == NFD_CANCEL) return; if(result == NFD_CANCEL) return;
if(result == NFD_ERROR) if(result == NFD_ERROR)
Util::panic("Error: {}", NFD::GetError()); Util::panic("Error: {}", NFD::GetError());
LoadROM(path.get()); LoadROM(path.get());
}}, }},
{"Exit", [&]() { {"Exit", [&]() {
@@ -233,7 +236,6 @@ void KaizenGui::LoadROM(const std::string &path) noexcept {
void KaizenGui::run() { void KaizenGui::run() {
while(!quit) { while(!quit) {
emuThread.run();
SDL_Event e; SDL_Event e;
while (SDL_PollEvent(&e)) { while (SDL_PollEvent(&e)) {
ImGui_ImplSDL3_ProcessEvent(&e); ImGui_ImplSDL3_ProcessEvent(&e);
@@ -245,6 +247,7 @@ void KaizenGui::run() {
} }
} }
emuThread.run();
RenderUI(); RenderUI();
} }
} }

View File

@@ -25,10 +25,7 @@ private:
class SDLWSIPlatform final : public Vulkan::WSIPlatform { class SDLWSIPlatform final : public Vulkan::WSIPlatform {
public: public:
explicit SDLWSIPlatform(const std::shared_ptr<n64::Core> &core, SDL_Window* window) : window(window), core(core) {} explicit SDLWSIPlatform(const std::shared_ptr<n64::Core> &core, SDL_Window* window) : window(window), core(core) {}
~SDLWSIPlatform() { ~SDLWSIPlatform() = default;
if(gamepadConnected)
SDL_CloseGamepad(gamepad);
}
std::vector<const char *> get_instance_extensions() override { std::vector<const char *> get_instance_extensions() override {
auto vec = std::vector<const char *>(); auto vec = std::vector<const char *>();
@@ -71,7 +68,6 @@ public:
VkSurfaceKHR surface; VkSurfaceKHR surface;
private: private:
std::shared_ptr<n64::Core> core; std::shared_ptr<n64::Core> core;
SDL_Gamepad *gamepad{};
bool gamepadConnected = false; bool gamepadConnected = false;
}; };

View File

@@ -2,13 +2,13 @@
#include <fmt/core.h> #include <fmt/core.h>
#include <nfd.hpp> #include <nfd.hpp>
#include <log.hpp> #include <log.hpp>
#include <JSONUtils.hpp> #include <Options.hpp>
std::string savePath; std::string savePath;
SettingsWindow::SettingsWindow() : settings{JSONOpenOrCreate("resources/settings.json")}, cpuSettings{settings}, audioSettings{settings} { SettingsWindow::SettingsWindow() {
savesFolder.setName(fmt::format("Save path: {}", savesFolder.setName(fmt::format("Save path: {}",
JSONGetField<std::string>(settings, "general", "savePath"))); Options::GetInstance().GetValue<std::string>("general", "savePath")));
tabs.addTab({"General", [&]() { tabs.addTab({"General", [&]() {
if(savesFolder.render()) { if(savesFolder.render()) {
@@ -22,7 +22,7 @@ SettingsWindow::SettingsWindow() : settings{JSONOpenOrCreate("resources/settings
Util::panic("Error: {}", NFD::GetError()); Util::panic("Error: {}", NFD::GetError());
savesFolder.setName(fmt::format("Save path: {}", outPath.get())); savesFolder.setName(fmt::format("Save path: {}", outPath.get()));
JSONSetField(settings, "general", "savePath", outPath.get()); Options::GetInstance().SetValue<std::string>("general", "savePath", outPath.get());
apply.setEnabled(true); apply.setEnabled(true);
} }
}}); }});
@@ -48,9 +48,7 @@ SettingsWindow::SettingsWindow() : settings{JSONOpenOrCreate("resources/settings
if(apply.render()) { if(apply.render()) {
apply.setEnabled(false); apply.setEnabled(false);
std::ofstream file("resources/settings.json"); Options::GetInstance().Apply();
file << settings;
file.close();
} }
ImGui::SameLine(); ImGui::SameLine();

View File

@@ -9,7 +9,6 @@
class SettingsWindow final { class SettingsWindow final {
gui::PushButton apply{"Apply", "", false}, savesFolder{"Open", "Save path: {}"}; gui::PushButton apply{"Apply", "", false}, savesFolder{"Open", "Save path: {}"};
nlohmann::json settings;
CPUSettings cpuSettings; CPUSettings cpuSettings;
AudioSettings audioSettings; AudioSettings audioSettings;
public: public:

31
src/utils/Options.cpp Normal file
View File

@@ -0,0 +1,31 @@
#include <Options.hpp>
template <>
void Options::SetValue<std::string>(const std::string &key, const std::string &field, const std::string &value) {
structure[key][field] = value;
}
template <>
void Options::SetValue<float>(const std::string &key, const std::string &field, const float &value) {
structure[key][field] = fmt::format("{:.2f}", value);
}
template <>
void Options::SetValue<bool>(const std::string &key, const std::string &field, const bool &value) {
structure[key][field] = value ? "true" : "false";
}
template<>
std::string Options::GetValue<std::string>(const std::string &key, const std::string &field) {
return structure[key][field];
}
template<>
float Options::GetValue<float>(const std::string &key, const std::string &field) {
return std::stof(structure[key][field]);
}
template<>
bool Options::GetValue<bool>(const std::string &key, const std::string &field) {
return structure[key][field] == "true" ? true : false;
}

View File

@@ -2,48 +2,44 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <common.hpp> #include <common.hpp>
#include <mini/ini.h>
#include <log.hpp>
namespace fs = std::filesystem; namespace fs = std::filesystem;
static FORCE_INLINE mINI::INIStructure JSONOpenOrCreate(const std::string &path) { struct Options {
auto fileExists = fs::exists(path); Options() : file{"resources/options.ini"} {
auto fileExists = fs::exists("resources/options.ini");
if(fileExists) {
file.read(structure);
return;
}
mINI::INIStructure options; structure["general"]["savePath"] = "";
mINI::INIFile file(path); structure["audio"]["volumeL"] = "0.5";
structure["audio"]["volumeR"] = "0.5";
if (fileExists) { structure["audio"]["lock"] = "true";
file.read(options); structure["cpu"]["type"] = "interpreter";
return options;
}
options["general"]["savePath"] = ""; if(!file.generate(structure))
options["audio"]["volumeL"] = 0.5; Util::panic("Couldn't generate settings' INI!");
options["audio"]["volumeR"] = 0.5; }
options["audio"]["lock"] = true;
options["cpu"]["type"] = "interpreter";
file.write(options); static Options &GetInstance() {
static Options instance;
return instance;
}
return options; template <typename T>
} void SetValue(const std::string &key, const std::string &field, const T &value);
template <typename T>
T GetValue(const std::string &key, const std::string &field);
template <typename T> void Apply() {
static FORCE_INLINE void JSONSetField(mINI::INIStructure &options, const std::string &key, const std::string &field, if(!file.write(structure))
const T &value) { Util::panic("Could not modify options on disk!");
options[key][field] = value; }
} private:
mINI::INIFile file;
template <typename T> mINI::INIStructure structure;
static FORCE_INLINE T JSONGetField(mINI::INIStructure &options, const std::string &key, const std::string &field) { };
return options[key][field];
}
template <typename T>
static FORCE_INLINE void JSONSetField(mINI::INIStructure &options, const std::string &key, const T &value) {
options[key] = value;
}
template <typename T>
static FORCE_INLINE T JSONGetField(mINI::INIStructure &options, const std::string &key) {
return options[key];
}