It works!

This commit is contained in:
2026-06-09 15:42:53 +02:00
parent 74d9831d68
commit c25e1d0617
19 changed files with 203 additions and 107 deletions
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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");
+12 -1
View File
@@ -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;
+1 -1
View File
@@ -3,7 +3,7 @@
#include <log.hpp>
#include <queue>
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;
-25
View File
@@ -1,25 +0,0 @@
#include <EmuThread.hpp>
#include <KaizenGui.hpp>
#include <Core.hpp>
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 = {};
}
-22
View File
@@ -1,22 +0,0 @@
#pragma once
#include <RenderWidget.hpp>
#include <SettingsWindow.hpp>
#include <memory>
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;
};
+108 -8
View File
@@ -1,32 +1,95 @@
#include <KaizenGui.hpp>
#include <backend/Core.hpp>
#include <QMenuBar>
#include <QMenu>
#include <QMessageBox>
#include <QCoreApplication>
#include <QStatusBar>
#include <QTimer>
#include <resources/gamecontrollerdb.h>
#include <Options.hpp>
#include <Scheduler.hpp>
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<false>();
continue;
}
KaizenGui::~KaizenGui() { SDL_Quit(); }
if (!core.pause) {
auto timeStart = SDL_GetTicks();
core.Run();
core.parallel.UpdateScreen<true>();
elapsed = SDL_GetTicks() - timeStart;
continue;
}
}
});
emuThread->start();
}
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);
}
+21 -13
View File
@@ -1,41 +1,49 @@
#pragma once
#include <RenderWidget.hpp>
#include <EmuThread.hpp>
#include <SettingsWindow.hpp>
#include <SDL3/SDL_gamepad.h>
#include <QMainWindow>
#include <QFileDialog>
#include <Core.hpp>
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();
};
-2
View File
@@ -44,6 +44,4 @@ RenderWidget::RenderWidget() {
wsiPlatform = std::make_shared<QtWSIPlatform>(windowHandle());
windowInfo = std::make_shared<QtPrdpWindowInfo>(windowHandle());
n64::Core &core = n64::Core::GetInstance();
core.parallel.Init(wsiPlatform, windowInfo, qtVkInstanceFactory.get(), core.GetMem().GetRDRAMPtr());
}
+4
View File
@@ -3,6 +3,7 @@
#include <ParallelRDPWrapper.hpp>
#include <QVulkanWindow>
#include <QMainWindow>
#include <QThread>
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<QtInstanceFactory> qtVkInstanceFactory;
std::shared_ptr<ParallelRDP::WindowInfo> windowInfo;
std::shared_ptr<QtWSIPlatform> wsiPlatform;
+8 -5
View File
@@ -1,19 +1,22 @@
#include <AudioSettings.hpp>
#include <Options.hpp>
#include <QSettings>
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);
}
+5
View File
@@ -2,10 +2,15 @@
#include <QWidget>
#include <QVBoxLayout>
#include <QSlider>
#include <QSettings>
#include <QLabel>
class AudioSettings final : public QWidget {
Q_OBJECT
QVBoxLayout *v;
QSlider *volume;
QLabel *volumePercent;
QSettings settings;
public:
explicit AudioSettings();
+20 -16
View File
@@ -1,9 +1,8 @@
#include <CPUSettings.hpp>
#include <Options.hpp>
#include <log.hpp>
#include <QSettings>
CPUSettings::CPUSettings() {
CPUSettings::CPUSettings() : settings(QSettings::UserScope) {
types = new QComboBox();
idleSkip = new QCheckBox("Idle skipping");
idleSkip->setToolTip("Whether to enable idle skipping.<br><br>"
@@ -11,36 +10,41 @@ CPUSettings::CPUSettings() {
"that enables skipping the execution of certain blocks of guest code<br>"
"when it's determined that the aforementioned is used to wait on a certain<br>"
"event to occur; the code gets skipped, the event is executed immediately by<br>"
"the emulator so that the game never actually waits, progressing immediately<br>"
"and making emulation much faster.<br><br>"
"the emulator so that the game never actually waits, progressing immediately and making "
"emulation much faster.<br><br>"
"This feature is not available when the pure interpreter is selected<br>"
"because the information regarding instructions would be too limited to perform<br>"
"the evaluation above described.");
"because the information regarding instructions would be too limited to<br>"
"perform the evaluation described above.");
idleSkip->setChecked(settings.value("cpu/idle_skip", false).value<bool>());
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);
+5
View File
@@ -4,13 +4,18 @@
#include <QCheckBox>
#include <QVBoxLayout>
#include <QLabel>
#include <QSettings>
class CPUSettings final : public QWidget {
Q_OBJECT
QComboBox *types;
QCheckBox *idleSkip;
QVBoxLayout *v;
QHBoxLayout *h;
QSettings settings;
public:
explicit CPUSettings();
signals:
void cpuTypeChanged();
};
+5 -5
View File
@@ -1,12 +1,10 @@
#include <GeneralSettings.hpp>
#include <Options.hpp>
#include <QSettings>
#include <QFileDialog>
#include <QCoreApplication>
#include <log.hpp>
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();
@@ -3,13 +3,16 @@
#include <QPushButton>
#include <QVBoxLayout>
#include <QLabel>
#include <QSettings>
class GeneralSettings final : public QWidget {
Q_OBJECT
QLabel *description;
QPushButton *folderSelectButton;
QLabel *selectedFolderLabel;
QVBoxLayout *v;
QHBoxLayout *h;
QSettings settings;
public:
explicit GeneralSettings();
+2
View File
@@ -8,6 +8,7 @@
#include <CPUSettings.hpp>
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();
};
+2 -2
View File
@@ -2,9 +2,9 @@
#include <QSettings>
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();
}
+3 -3
View File
@@ -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;
};