From c25e1d0617c75abd99a0d51885b5f808213364d0 Mon Sep 17 00:00:00 2001 From: iris Date: Tue, 9 Jun 2026 15:42:53 +0200 Subject: [PATCH] It works! --- CMakeLists.txt | 4 +- src/backend/Core.cpp | 4 +- src/backend/Scheduler.cpp | 13 ++- src/backend/Scheduler.hpp | 2 +- src/frontend/EmuThread.cpp | 25 ----- src/frontend/EmuThread.hpp | 22 ---- src/frontend/KaizenGui.cpp | 116 ++++++++++++++++++++-- src/frontend/KaizenGui.hpp | 34 ++++--- src/frontend/RenderWidget.cpp | 2 - src/frontend/RenderWidget.hpp | 4 + src/frontend/Settings/AudioSettings.cpp | 13 ++- src/frontend/Settings/AudioSettings.hpp | 5 + src/frontend/Settings/CPUSettings.cpp | 36 ++++--- src/frontend/Settings/CPUSettings.hpp | 5 + src/frontend/Settings/GeneralSettings.cpp | 10 +- src/frontend/Settings/GeneralSettings.hpp | 3 + src/frontend/SettingsWindow.hpp | 2 + src/utils/Options.cpp | 4 +- src/utils/Options.hpp | 6 +- 19 files changed, 203 insertions(+), 107 deletions(-) delete mode 100644 src/frontend/EmuThread.cpp delete mode 100644 src/frontend/EmuThread.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bb9b88f..b8ec4bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,14 +151,14 @@ set(CAPSTONE_MIPS_SUPPORT ON) set(CAPSTONE_X86_SUPPORT ON) add_subdirectory(external/capstone) +set(CMAKE_AUTOMOC ON) + qt_add_executable(kaizen src/frontend/main.cpp src/frontend/KaizenGui.hpp src/frontend/KaizenGui.cpp src/frontend/RenderWidget.cpp src/frontend/RenderWidget.hpp - src/frontend/EmuThread.hpp - src/frontend/EmuThread.cpp src/frontend/SettingsWindow.hpp src/frontend/SettingsWindow.cpp src/frontend/Settings/GeneralSettings.hpp diff --git a/src/backend/Core.cpp b/src/backend/Core.cpp index 090def5..9607528 100644 --- a/src/backend/Core.cpp +++ b/src/backend/Core.cpp @@ -6,9 +6,9 @@ namespace n64 { Core::Core() : interpreter(*mem, regs) { const auto selectedCpu = Options::GetCpuType(); - if (selectedCpu == "interpreter") { + if (selectedCpu == 0) { cpuType = PlainInterpreter; - } else if (selectedCpu == "cached_interpreter") { + } else if (selectedCpu == 1) { cpuType = CachedInterpreter; } else { panic("Unimplemented CPU type"); diff --git a/src/backend/Scheduler.cpp b/src/backend/Scheduler.cpp index 82f02b0..e4b8788 100644 --- a/src/backend/Scheduler.cpp +++ b/src/backend/Scheduler.cpp @@ -32,13 +32,24 @@ void Scheduler::SkipToNext() { ticks = events.top().time; } void Scheduler::Tick(const u64 t) { ticks += t; } void Scheduler::HandleEvents() { - n64::Mem &mem = n64::Core::GetMem(); + n64::Core &core = n64::Core::GetInstance(); + n64::Mem &mem = core.GetMem(); n64::MI &mi = mem.mmio.mi; n64::SI &si = mem.mmio.si; n64::PI &pi = mem.mmio.pi; while (ticks >= events.top().time) { switch (const auto type = events.top().type) { + case PAUSE: + core.TogglePause(); + break; + case STOP: + core.Stop(); + core.rom = {}; + break; + case RESET: + core.Reset(); + break; case SI_DMA: si.DMA(); break; diff --git a/src/backend/Scheduler.hpp b/src/backend/Scheduler.hpp index 51e43d5..5cb5b25 100644 --- a/src/backend/Scheduler.hpp +++ b/src/backend/Scheduler.hpp @@ -3,7 +3,7 @@ #include #include -enum EventType { NONE, PI_BUS_WRITE_COMPLETE, PI_DMA_COMPLETE, SI_DMA, IMPOSSIBLE }; +enum EventType { NONE, PAUSE, STOP, RESET, PI_BUS_WRITE_COMPLETE, PI_DMA_COMPLETE, SI_DMA, IMPOSSIBLE }; struct Event { u64 time; diff --git a/src/frontend/EmuThread.cpp b/src/frontend/EmuThread.cpp deleted file mode 100644 index 1108bbe..0000000 --- a/src/frontend/EmuThread.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include -#include -#include - -EmuThread::EmuThread() noexcept {} - -void EmuThread::run() const noexcept { - n64::Core &core = n64::Core::GetInstance(); - if (!core.romLoaded) - return; - - if (!core.pause) { - core.Run(); - } -} - -void EmuThread::TogglePause() const noexcept { n64::Core::GetInstance().TogglePause(); } - -void EmuThread::Reset() const noexcept { n64::Core::GetInstance().Reset(); } - -void EmuThread::Stop() const noexcept { - n64::Core &core = n64::Core::GetInstance(); - core.Stop(); - core.rom = {}; -} diff --git a/src/frontend/EmuThread.hpp b/src/frontend/EmuThread.hpp deleted file mode 100644 index c4f1bcb..0000000 --- a/src/frontend/EmuThread.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#include -#include -#include - -namespace n64 { -struct Core; -} - -class EmuThread final { - bool started = false; - - public: - explicit EmuThread() noexcept; - ~EmuThread() = default; - void run() const noexcept; - void TogglePause() const noexcept; - void Reset() const noexcept; - void Stop() const noexcept; - - bool interruptionRequested = false, parallelRDPInitialized = false; -}; diff --git a/src/frontend/KaizenGui.cpp b/src/frontend/KaizenGui.cpp index c2b824f..ad5f2da 100644 --- a/src/frontend/KaizenGui.cpp +++ b/src/frontend/KaizenGui.cpp @@ -1,32 +1,95 @@ #include -#include #include #include #include +#include +#include +#include #include +#include +#include -KaizenGui::KaizenGui() noexcept { +KaizenGui::KaizenGui() noexcept : settings(QSettings::UserScope) { SDL_InitSubSystem(SDL_INIT_GAMEPAD); hide(); vulkanWidget = new RenderWidget(); + cpuTypeLabel = new QLabel("Interpreter"); + if (Options::GetCpuType() == 1) + cpuTypeLabel->setText("Cached Interpreter"); + + fpsLabel = new QLabel("Not running"); + + statusBar()->addWidget(fpsLabel); + statusBar()->addWidget(cpuTypeLabel); + setWindowTitle("Kaizen " KAIZEN_VERSION_STR); setMinimumSize(640, 480); setCentralWidget(vulkanWidget); + statusBarTimer = new QTimer(); + + connect(statusBarTimer, &QTimer::timeout, this, [&] { + pause->setText("Pause"); + fpsLabel->setText(std::format("FPS: {:.2f}", 1000.f / elapsed).c_str()); + if (core.pause) { + pause->setText("Resume"); + fpsLabel->setText("Paused"); + } + + if (!core.romLoaded) { + pause->setDisabled(true); + reset->setDisabled(true); + stop->setDisabled(true); + fpsLabel->setText("Not running"); + } + }); + + statusBarTimer->start(); + auto fileMenu = menuBar()->addMenu("File"); auto open = fileMenu->addAction("Open"); + connect(open, &QAction::triggered, this, [&] { + auto fileToLoad = + QFileDialog::getOpenFileName(this, "Select a Nintendo 64 ROM", QDir::currentPath(), + "N64 ROM (*.z64 *.n64 *.v64)", nullptr, QFileDialog::DontUseNativeDialog) + .toStdString(); + if (!fileToLoad.empty()) + LoadROM(fileToLoad); + }); + auto exit = fileMenu->addAction("Exit"); auto emulationMenu = menuBar()->addMenu("Emulation"); auto settingsMenu = emulationMenu->addAction("Settings"); settingsWindow = new SettingsWindow(); connect(settingsMenu, &QAction::triggered, settingsWindow, &SettingsWindow::show); + connect(settingsWindow->cpu, &CPUSettings::cpuTypeChanged, this, [&] { + cpuTypeLabel->setText("Cached Interpreter"); + if (Options::GetCpuType() == 0) + cpuTypeLabel->setText("Interpreter"); + }); + emulationMenu->addSeparator(); - auto pause = emulationMenu->addAction("Pause"); - auto reset = emulationMenu->addAction("Reset"); - auto stop = emulationMenu->addAction("Stop"); + + pause->setDisabled(true); + emulationMenu->addAction(pause); + connect(pause, &QAction::triggered, this, [&] { + if (!core.pause) + Scheduler::GetInstance().EnqueueRelative(0, PAUSE); + else + core.TogglePause(); + }); + + reset->setDisabled(true); + emulationMenu->addAction(reset); + connect(reset, &QAction::triggered, this, [&] { Scheduler::GetInstance().EnqueueRelative(0, RESET); }); + + stop->setDisabled(true); + emulationMenu->addAction(stop); + connect(stop, &QAction::triggered, this, [&] { Scheduler::GetInstance().EnqueueRelative(0, STOP); }); + auto helpMenu = menuBar()->addMenu("Help"); auto about = helpMenu->addAction("About"); connect(about, &QAction::triggered, this, [&] { @@ -39,16 +102,53 @@ KaizenGui::KaizenGui() noexcept { QMessageBox::about(this, "About", text.c_str()); }); + + restoreGeometry(settings.value("geometry").toByteArray()); + restoreState(settings.value("windowState").toByteArray()); + show(); + + emuThread = QThread::create([&] { + core.parallel.Init(vulkanWidget->wsiPlatform, vulkanWidget->windowInfo, vulkanWidget->qtVkInstanceFactory.get(), + core.GetMem().GetRDRAMPtr()); + while (!emuThread->isInterruptionRequested()) { + if (!core.romLoaded) { + core.parallel.UpdateScreen(); + continue; + } + + if (!core.pause) { + auto timeStart = SDL_GetTicks(); + core.Run(); + core.parallel.UpdateScreen(); + elapsed = SDL_GetTicks() - timeStart; + continue; + } + } + }); + + emuThread->start(); } -KaizenGui::~KaizenGui() { SDL_Quit(); } +KaizenGui::~KaizenGui() { + SDL_Quit(); + emuThread->requestInterruption(); + emuThread->quit(); +} void KaizenGui::LoadROM(const std::string &path) noexcept { n64::Core &core = n64::Core::GetInstance(); core.LoadROM(path); - const auto gameNameDB = n64::Core::GetMem().rom.gameNameDB; - setWindowTitle(("Kaizen " KAIZEN_VERSION_STR " - " + gameNameDB).c_str()); + pause->setEnabled(true); + reset->setEnabled(true); + stop->setEnabled(true); + setWindowTitle(("Kaizen " KAIZEN_VERSION_STR " - " + n64::Core::GetMem().rom.gameNameDB).c_str()); } void KaizenGui::LoadTAS(const std::string &path) noexcept { n64::Core::GetInstance().LoadTAS(fs::path(path)); } + +void KaizenGui::closeEvent(QCloseEvent *event) { + settings.setValue("geometry", saveGeometry()); + settings.setValue("windowState", saveState()); + QMainWindow::closeEvent(event); +} diff --git a/src/frontend/KaizenGui.hpp b/src/frontend/KaizenGui.hpp index 4666b9d..cf12bb9 100644 --- a/src/frontend/KaizenGui.hpp +++ b/src/frontend/KaizenGui.hpp @@ -1,41 +1,49 @@ #pragma once #include -#include +#include #include #include +#include +#include + +class EmuThread; class KaizenGui final : QMainWindow { + Q_OBJECT public: explicit KaizenGui() noexcept; ~KaizenGui(); + void closeEvent(QCloseEvent *) override; + bool fastForward = false; bool unlockFramerate = false; bool minimized = false; SettingsWindow *settingsWindow; RenderWidget *vulkanWidget; - EmuThread emuThread; + QSettings settings; + QThread *emuThread; + QThread *fileWorker; + QTimer *statusBarTimer; + QLabel *fpsLabel; + QLabel *cpuTypeLabel; + QAction *pause = new QAction("Pause"); + QAction *reset = new QAction("Reset"); + QAction *stop = new QAction("Stop"); SDL_Gamepad *gamepad = nullptr; static void LoadTAS(const std::string &path) noexcept; void LoadROM(const std::string &path) noexcept; + signals: + void paused(); private: + float elapsed = 0.f; int width{}, height{}; bool aboutOpen = false; bool fileDialogOpen = false; bool quit = false; - bool shouldDisplaySpinner = false; - std::string fileToLoad = ""; - - void FileWorker() { - if (fileToLoad.empty()) - return; - - LoadROM(fileToLoad); - shouldDisplaySpinner = false; - fileToLoad = ""; - } + n64::Core &core = n64::Core::GetInstance(); }; diff --git a/src/frontend/RenderWidget.cpp b/src/frontend/RenderWidget.cpp index e6d67eb..536569c 100644 --- a/src/frontend/RenderWidget.cpp +++ b/src/frontend/RenderWidget.cpp @@ -44,6 +44,4 @@ RenderWidget::RenderWidget() { wsiPlatform = std::make_shared(windowHandle()); windowInfo = std::make_shared(windowHandle()); - n64::Core &core = n64::Core::GetInstance(); - core.parallel.Init(wsiPlatform, windowInfo, qtVkInstanceFactory.get(), core.GetMem().GetRDRAMPtr()); } diff --git a/src/frontend/RenderWidget.hpp b/src/frontend/RenderWidget.hpp index 6edcfce..a215522 100644 --- a/src/frontend/RenderWidget.hpp +++ b/src/frontend/RenderWidget.hpp @@ -3,6 +3,7 @@ #include #include #include +#include struct InputSettings; @@ -34,11 +35,14 @@ struct QtInstanceFactory : Vulkan::InstanceFactory { }; class RenderWidget final : public QWidget { + Q_OBJECT QVulkanInstance inst; public: explicit RenderWidget(); + QPaintEngine *paintEngine() const override { return nullptr; } + std::unique_ptr qtVkInstanceFactory; std::shared_ptr windowInfo; std::shared_ptr wsiPlatform; diff --git a/src/frontend/Settings/AudioSettings.cpp b/src/frontend/Settings/AudioSettings.cpp index f10938e..557593b 100644 --- a/src/frontend/Settings/AudioSettings.cpp +++ b/src/frontend/Settings/AudioSettings.cpp @@ -1,19 +1,22 @@ #include #include -#include -AudioSettings::AudioSettings() { - volume = new QSlider(); +AudioSettings::AudioSettings() : settings(QSettings::UserScope) { + volumePercent = new QLabel(); + volumePercent->setText(std::format("Volume: {:.2f}%", Options::GetVolume() * 100.f).c_str()); + volume = new QSlider(Qt::Horizontal); volume->setRange(0, 100); - volume->setValue(int(Options::GetVolume()) * 100.f); - QSettings settings; + volume->setValue(Options::GetVolume() * 100.f); connect(volume, &QSlider::valueChanged, this, [&] { float newValue = float(volume->value()) / 100.f; + volumePercent->setText(std::format("Volume: {:.2f}%", Options::GetVolume() * 100.f).c_str()); Options::SetVolume(newValue); settings.setValue("audio/volume", newValue); + settings.sync(); }); v = new QVBoxLayout(); v->addWidget(volume); + v->addWidget(volumePercent); setLayout(v); } diff --git a/src/frontend/Settings/AudioSettings.hpp b/src/frontend/Settings/AudioSettings.hpp index 3768927..70bee85 100644 --- a/src/frontend/Settings/AudioSettings.hpp +++ b/src/frontend/Settings/AudioSettings.hpp @@ -2,10 +2,15 @@ #include #include #include +#include +#include class AudioSettings final : public QWidget { + Q_OBJECT QVBoxLayout *v; QSlider *volume; + QLabel *volumePercent; + QSettings settings; public: explicit AudioSettings(); diff --git a/src/frontend/Settings/CPUSettings.cpp b/src/frontend/Settings/CPUSettings.cpp index ba998da..370d46d 100644 --- a/src/frontend/Settings/CPUSettings.cpp +++ b/src/frontend/Settings/CPUSettings.cpp @@ -1,9 +1,8 @@ #include #include #include -#include -CPUSettings::CPUSettings() { +CPUSettings::CPUSettings() : settings(QSettings::UserScope) { types = new QComboBox(); idleSkip = new QCheckBox("Idle skipping"); idleSkip->setToolTip("Whether to enable idle skipping.

" @@ -11,36 +10,41 @@ CPUSettings::CPUSettings() { "that enables skipping the execution of certain blocks of guest code
" "when it's determined that the aforementioned is used to wait on a certain
" "event to occur; the code gets skipped, the event is executed immediately by
" - "the emulator so that the game never actually waits, progressing immediately
" - "and making emulation much faster.

" + "the emulator so that the game never actually waits, progressing immediately and making " + "emulation much faster.

" "This feature is not available when the pure interpreter is selected
" - "because the information regarding instructions would be too limited to perform
" - "the evaluation above described."); + "because the information regarding instructions would be too limited to
" + "perform the evaluation described above."); + + idleSkip->setChecked(settings.value("cpu/idle_skip", false).value()); v = new QVBoxLayout(); h = new QHBoxLayout(); types->addItems({"Interpreter", "Cached Interpreter"}); + if (Options::GetCpuType() == 0) { + idleSkip->hide(); + } else { + idleSkip->show(); + } + types->setCurrentIndex(Options::GetCpuType()); - QSettings settings; connect(types, &QComboBox::currentIndexChanged, this, [&] { int index = types->currentIndex(); - QString newValue{}; - if (index == 0) { + if (index == 0) idleSkip->hide(); - newValue = "interpreter"; - } - if (index == 1) { + else idleSkip->show(); - newValue = "cached_interpreter"; - } - Options::SetCpuType(newValue.toStdString()); - settings.setValue("cpu/type", newValue); + Options::SetCpuType(index); + settings.setValue("cpu/type", index); + settings.sync(); + emit cpuTypeChanged(); }); connect(idleSkip, &QCheckBox::checkStateChanged, this, [&] { Options::SetIdleSkip(idleSkip->checkState()); settings.setValue("cpu/idle_skip", idleSkip->checkState()); + settings.sync(); }); h->addWidget(idleSkip); diff --git a/src/frontend/Settings/CPUSettings.hpp b/src/frontend/Settings/CPUSettings.hpp index 94dee75..bdead05 100644 --- a/src/frontend/Settings/CPUSettings.hpp +++ b/src/frontend/Settings/CPUSettings.hpp @@ -4,13 +4,18 @@ #include #include #include +#include class CPUSettings final : public QWidget { + Q_OBJECT QComboBox *types; QCheckBox *idleSkip; QVBoxLayout *v; QHBoxLayout *h; + QSettings settings; public: explicit CPUSettings(); + signals: + void cpuTypeChanged(); }; diff --git a/src/frontend/Settings/GeneralSettings.cpp b/src/frontend/Settings/GeneralSettings.cpp index 0e6984f..920ddd5 100644 --- a/src/frontend/Settings/GeneralSettings.cpp +++ b/src/frontend/Settings/GeneralSettings.cpp @@ -1,12 +1,10 @@ #include #include -#include #include #include #include -GeneralSettings::GeneralSettings() { - QSettings settings; +GeneralSettings::GeneralSettings() : settings(QSettings::UserScope) { description = new QLabel("Path:"); description->setToolTip("Path where game saves are stored."); selectedFolderLabel = new QLabel(Options::GetSavesPath().c_str()); @@ -15,10 +13,12 @@ GeneralSettings::GeneralSettings() { connect(folderSelectButton, &QPushButton::clicked, this, [&] { auto dir = QFileDialog::getExistingDirectory(this, tr("Open Directory"), QCoreApplication::applicationDirPath(), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks | + QFileDialog::DontUseNativeDialog); selectedFolderLabel->setText(dir); Options::SetSavesPath(dir.toStdString()); - settings.setValue("general/savesPath", dir); + settings.setValue("general/saves_path", dir); + settings.sync(); }); h = new QHBoxLayout(); diff --git a/src/frontend/Settings/GeneralSettings.hpp b/src/frontend/Settings/GeneralSettings.hpp index 38ccf6c..5a22727 100644 --- a/src/frontend/Settings/GeneralSettings.hpp +++ b/src/frontend/Settings/GeneralSettings.hpp @@ -3,13 +3,16 @@ #include #include #include +#include class GeneralSettings final : public QWidget { + Q_OBJECT QLabel *description; QPushButton *folderSelectButton; QLabel *selectedFolderLabel; QVBoxLayout *v; QHBoxLayout *h; + QSettings settings; public: explicit GeneralSettings(); diff --git a/src/frontend/SettingsWindow.hpp b/src/frontend/SettingsWindow.hpp index e98e698..a26c52d 100644 --- a/src/frontend/SettingsWindow.hpp +++ b/src/frontend/SettingsWindow.hpp @@ -8,6 +8,7 @@ #include class SettingsWindow final : public QWidget { + Q_OBJECT QTabWidget *categories; GeneralSettings *general; AudioSettings *audio; @@ -15,5 +16,6 @@ class SettingsWindow final : public QWidget { QVBoxLayout *v; public: + friend class KaizenGui; SettingsWindow(); }; diff --git a/src/utils/Options.cpp b/src/utils/Options.cpp index 54e6824..e24fd36 100644 --- a/src/utils/Options.cpp +++ b/src/utils/Options.cpp @@ -2,9 +2,9 @@ #include Options::Options() { - QSettings settings; + QSettings settings(QSettings::UserScope); volume = settings.value("audio/volume", 0.5f).toFloat(); - cpuType = settings.value("cpu/type", "interpreter").toString().toStdString(); + cpuType = settings.value("cpu/type", 0).toUInt(); idleSkip = settings.value("cpu/idle_skip", false).toBool(); savesPath = settings.value("general/saves_path", "saves").toString().toStdString(); } diff --git a/src/utils/Options.hpp b/src/utils/Options.hpp index a593d99..d7615bb 100644 --- a/src/utils/Options.hpp +++ b/src/utils/Options.hpp @@ -12,16 +12,16 @@ struct Options { static bool GetIdleSkip() { return GetInstance().idleSkip; } static float GetVolume() { return GetInstance().volume; } - static std::string GetCpuType() { return GetInstance().cpuType; } + static uint8_t GetCpuType() { return GetInstance().cpuType; } static std::string GetSavesPath() { return GetInstance().savesPath; } static void SetIdleSkip(bool v) { GetInstance().idleSkip = v; } static void SetVolume(float v) { GetInstance().volume = v; } - static void SetCpuType(const std::string &v) { GetInstance().cpuType = v; } + static void SetCpuType(uint8_t v) { GetInstance().cpuType = v; } static void SetSavesPath(const std::string &v) { GetInstance().savesPath = v; } private: bool idleSkip = false; float volume = 0.5; std::string savesPath = "saves"; - std::string cpuType = "interpreter"; + uint8_t cpuType = 0; };