422 lines
16 KiB
C++
422 lines
16 KiB
C++
#include <KaizenGui.hpp>
|
|
#include <QMenuBar>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QCoreApplication>
|
|
#include <QStatusBar>
|
|
#include <QTimer>
|
|
#include <QMimeData>
|
|
#include <QHeaderView>
|
|
#include <resources/gamecontrollerdb.h>
|
|
#include <Options.hpp>
|
|
#include <Scheduler.hpp>
|
|
|
|
void KaizenGui::populateRomsList(const std::string &romsPath) {
|
|
centralWidget->setCurrentWidget(romPathNotSet);
|
|
if (!romsPath.empty()) {
|
|
int i = 0;
|
|
centralWidget->setCurrentWidget(romsList);
|
|
|
|
for (const auto &file : fs::recursive_directory_iterator{romsPath}) {
|
|
if (!file.is_regular_file())
|
|
continue;
|
|
|
|
auto filename = file.path().string();
|
|
|
|
bool isPlain = std::ranges::any_of(std::array{".n64", ".z64", ".v64"},
|
|
[&](const std::string &ext) { return file.path().extension() == ext; });
|
|
|
|
bool isArchive =
|
|
std::ranges::any_of(std::array{".zip", ".7z", ".rar", ".tar"},
|
|
[&](const std::string &ext) { return file.path().extension() == ext; });
|
|
|
|
if (!isArchive && !isPlain)
|
|
continue;
|
|
|
|
auto rom = n64::Mem::LoadROM(isArchive, filename);
|
|
auto regions = n64::GameDB::match(rom);
|
|
|
|
if (rom.gameNameDB.empty())
|
|
rom.gameNameDB = fs::path(filename).stem();
|
|
|
|
romsListPaths.push_back(filename);
|
|
romsList->insertRow(i);
|
|
|
|
romsList->setItem(
|
|
i, 0,
|
|
new QTableWidgetItem(std::format("{} ({}) (Rev {})", rom.gameNameDB,
|
|
n64::GameDB::regionCodeToReadable(rom.header.countryCode),
|
|
rom.header.version)
|
|
.c_str()));
|
|
romsList->setItem(i, 1, new QTableWidgetItem(regions.c_str()));
|
|
romsList->setItem(i, 2, new QTableWidgetItem("Never"));
|
|
romsList->setItem(i, 3, new QTableWidgetItem("0h 0m 0s"));
|
|
i++;
|
|
}
|
|
}
|
|
currentHomeWidget = centralWidget->currentWidget();
|
|
}
|
|
|
|
KaizenGui::KaizenGui() noexcept : QMainWindow(nullptr), settings(QSettings::UserScope) {
|
|
SDL_InitSubSystem(SDL_INIT_GAMEPAD);
|
|
|
|
SDL_AddGamepadMapping(gamecontrollerdb_str);
|
|
hide();
|
|
|
|
romsList->verticalHeader()->hide();
|
|
romsList->setSizePolicy({QSizePolicy::Maximum, QSizePolicy::Maximum});
|
|
romsList->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
romsList->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
romsList->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
romsList->setSortingEnabled(true);
|
|
romsList->setColumnCount(4);
|
|
romsList->setHorizontalHeaderItem(0, new QTableWidgetItem("Name"));
|
|
romsList->setHorizontalHeaderItem(1, new QTableWidgetItem("Regions"));
|
|
romsList->setHorizontalHeaderItem(2, new QTableWidgetItem("Last played"));
|
|
romsList->setHorizontalHeaderItem(3, new QTableWidgetItem("Time played"));
|
|
|
|
vulkanWidget = new RenderWidget();
|
|
vulkanWidget->hide();
|
|
|
|
centralWidget->addWidget(vulkanWidget);
|
|
centralWidget->addWidget(romsList);
|
|
centralWidget->addWidget(romPathNotSet);
|
|
|
|
populateRomsList(Options::GetRomsPath());
|
|
|
|
connect(romsList, &QTableWidget::cellDoubleClicked, this, [&](int row, int) {
|
|
auto fileToLoad = fs::path(Options::GetRomsPath()) / romsListPaths[row];
|
|
std::println("{}", fileToLoad.string());
|
|
LoadROM(fileToLoad.string());
|
|
});
|
|
|
|
installEventFilter(this);
|
|
|
|
restoreGeometry(settings.value("geometry").toByteArray());
|
|
restoreState(settings.value("windowState").toByteArray());
|
|
|
|
cpuTypeLabel = new QLabel("Interpreter");
|
|
if (Options::GetCpuType() == n64::CachedInterpreter)
|
|
cpuTypeLabel->setText("Cached Interpreter");
|
|
|
|
idleSkipLabel = new QLabel("Idle skipping");
|
|
if (!Options::GetIdleSkip() || Options::GetCpuType() == n64::PlainInterpreter)
|
|
idleSkipLabel->hide();
|
|
|
|
fpsLabel = new QLabel("Not running");
|
|
|
|
statusBar()->addWidget(fpsLabel);
|
|
statusBar()->addWidget(cpuTypeLabel);
|
|
statusBar()->addWidget(idleSkipLabel);
|
|
|
|
setWindowTitle("Kaizen " KAIZEN_VERSION_STR);
|
|
setMinimumSize(640, 480);
|
|
setCentralWidget(centralWidget);
|
|
|
|
setAcceptDrops(true);
|
|
|
|
SDLeventsTimer = new QTimer();
|
|
SDLeventsTimer->setInterval(1);
|
|
statusBarTimer = new QTimer();
|
|
statusBarTimer->setInterval(500);
|
|
|
|
connect(SDLeventsTimer, &QTimer::timeout, this, [&] {
|
|
SDL_Event e;
|
|
while (SDL_PollEvent(&e)) {
|
|
if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
|
|
if (!gamepad) {
|
|
const auto index = e.gdevice.which;
|
|
|
|
gamepad = SDL_OpenGamepad(index);
|
|
warn("Found controller!");
|
|
warn("Name: {}", SDL_GetGamepadName(gamepad));
|
|
warn("Vendor: {}", SDL_GetGamepadVendor(gamepad));
|
|
}
|
|
}
|
|
|
|
if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
|
if (gamepad) {
|
|
SDL_CloseGamepad(gamepad);
|
|
gamepad = nullptr;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
SDLeventsTimer->start();
|
|
|
|
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->setText("Pause");
|
|
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");
|
|
open->setShortcut(QKeyCombination(Qt::CTRL, Qt::Key_O));
|
|
connect(open, &QAction::triggered, this, [&] {
|
|
auto originPath = Options::GetRomsPath().empty() ? QDir::currentPath() : Options::GetRomsPath().c_str();
|
|
auto fileToLoad =
|
|
QFileDialog::getOpenFileName(this, "Select a Nintendo 64 ROM", originPath, "N64 ROM (*.z64 *.n64 *.v64)")
|
|
.toStdString();
|
|
if (!fileToLoad.empty())
|
|
LoadROM(fileToLoad);
|
|
});
|
|
|
|
auto exit = fileMenu->addAction("Exit");
|
|
connect(exit, &QAction::triggered, this, [&] { Scheduler::GetInstance().EnqueueRelative(0, STOP); });
|
|
connect(exit, &QAction::triggered, this, &QMainWindow::close);
|
|
auto emulationMenu = menuBar()->addMenu("Emulation");
|
|
auto settingsMenu = emulationMenu->addAction("Settings");
|
|
settingsWindow = new SettingsWindow();
|
|
connect(settingsMenu, &QAction::triggered, settingsWindow, &SettingsWindow::show);
|
|
connect(settingsWindow->general, &GeneralSettings::romFolderSelected, this,
|
|
[&] { populateRomsList(Options::GetRomsPath()); });
|
|
|
|
connect(settingsWindow->cpu, &CPUSettings::cpuTypeChanged, this, [&] {
|
|
core.cpuType = Options::GetCpuType();
|
|
cpuTypeLabel->setText("Cached Interpreter");
|
|
idleSkipLabel->setVisible(Options::GetCpuType() == n64::CachedInterpreter);
|
|
if (Options::GetCpuType() == n64::PlainInterpreter)
|
|
cpuTypeLabel->setText("Interpreter");
|
|
});
|
|
|
|
connect(settingsWindow->cpu, &CPUSettings::idleSkipChanged, this, [&] {
|
|
core.idleSkip = Options::GetIdleSkip();
|
|
idleSkipLabel->setVisible(Options::GetIdleSkip());
|
|
});
|
|
|
|
emulationMenu->addSeparator();
|
|
|
|
auto unlockFramerate = emulationMenu->addAction("Unlock framerate");
|
|
unlockFramerate->setCheckable(true);
|
|
|
|
connect(unlockFramerate, &QAction::triggered, this, [&](bool checked) { unlockFrameratePressed = checked; });
|
|
|
|
pause->setDisabled(true);
|
|
pause->setShortcut(QKeyCombination(Qt::CTRL, Qt::Key_P));
|
|
emulationMenu->addAction(pause);
|
|
connect(pause, &QAction::triggered, this, [&] {
|
|
if (!core.pause)
|
|
Scheduler::GetInstance().EnqueueRelative(0, PAUSE);
|
|
else
|
|
core.TogglePause();
|
|
});
|
|
|
|
|
|
reset->setDisabled(true);
|
|
reset->setShortcut(QKeyCombination(Qt::CTRL, Qt::Key_R));
|
|
emulationMenu->addAction(reset);
|
|
connect(reset, &QAction::triggered, this, [&] { Scheduler::GetInstance().EnqueueRelative(0, RESET); });
|
|
|
|
stop->setDisabled(true);
|
|
stop->setShortcut(QKeyCombination(Qt::CTRL, Qt::Key_X));
|
|
emulationMenu->addAction(stop);
|
|
connect(stop, &QAction::triggered, this, [&] {
|
|
Scheduler::GetInstance().EnqueueRelative(0, STOP);
|
|
centralWidget->setCurrentWidget(currentHomeWidget);
|
|
});
|
|
|
|
auto helpMenu = menuBar()->addMenu("Help");
|
|
auto about = helpMenu->addAction("About");
|
|
connect(about, &QAction::triggered, this, [&] {
|
|
auto text = std::format("<p>Kaizen is a Nintendo 64 emulator that strives<br>"
|
|
"to offer a friendly user experience and compatibility.<br>"
|
|
"Kaizen is licensed under the BSD 3-clause license.<br>"
|
|
"Nintendo 64 is a registered trademark of Nintendo Co., Ltd.</p><hr>"
|
|
"Kaizen {}{}",
|
|
KAIZEN_USE_HASH ? "dev build " : "", KAIZEN_VERSION_STR);
|
|
|
|
QMessageBox::about(this, "About", text.c_str());
|
|
});
|
|
|
|
show();
|
|
|
|
emuThread = QThread::create([&] {
|
|
core.parallel.Init(vulkanWidget->wsiPlatform, vulkanWidget->windowInfo, vulkanWidget->qtVkInstanceFactory.get(),
|
|
core.GetMem().GetRDRAMPtr());
|
|
while (!emuThread->isInterruptionRequested()) {
|
|
if (!core.romLoaded || core.pause)
|
|
continue;
|
|
|
|
updateKeys();
|
|
updateAxis();
|
|
|
|
core.parallel.SetFramerateUnlocked(unlockFrameratePressed);
|
|
|
|
auto timeStart = SDL_GetTicks();
|
|
core.Run();
|
|
core.parallel.UpdateScreen();
|
|
elapsed = SDL_GetTicks() - timeStart;
|
|
}
|
|
});
|
|
|
|
emuThread->start();
|
|
}
|
|
|
|
KaizenGui::~KaizenGui() { cleanup(); }
|
|
|
|
void KaizenGui::updateKeys() {
|
|
auto &pif = core.mem->mmio.si.pif;
|
|
if (gamepad) {
|
|
pif.UpdateButton(0, n64::Controller::Z,
|
|
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) == SDL_JOYSTICK_AXIS_MAX);
|
|
pif.UpdateButton(0, n64::Controller::A, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_SOUTH));
|
|
pif.UpdateButton(0, n64::Controller::B, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_WEST));
|
|
pif.UpdateButton(0, n64::Controller::LT, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER));
|
|
pif.UpdateButton(0, n64::Controller::RT, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER));
|
|
pif.UpdateButton(0, n64::Controller::Start, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_START));
|
|
pif.UpdateButton(0, n64::Controller::DUp, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_UP));
|
|
pif.UpdateButton(0, n64::Controller::DDown, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_DOWN));
|
|
pif.UpdateButton(0, n64::Controller::DLeft, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_LEFT));
|
|
pif.UpdateButton(0, n64::Controller::DRight, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT));
|
|
pif.UpdateButton(0, n64::Controller::CUp,
|
|
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY) == SDL_JOYSTICK_AXIS_MIN);
|
|
pif.UpdateButton(0, n64::Controller::CDown,
|
|
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY) == SDL_JOYSTICK_AXIS_MAX);
|
|
pif.UpdateButton(0, n64::Controller::CLeft,
|
|
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX) == SDL_JOYSTICK_AXIS_MIN);
|
|
pif.UpdateButton(0, n64::Controller::CRight,
|
|
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX) == SDL_JOYSTICK_AXIS_MAX);
|
|
return;
|
|
}
|
|
pif.UpdateButton(0, n64::Controller::Z, pressedKeys.test(0));
|
|
pif.UpdateButton(0, n64::Controller::A, pressedKeys.test(1));
|
|
pif.UpdateButton(0, n64::Controller::B, pressedKeys.test(2));
|
|
pif.UpdateButton(0, n64::Controller::LT, pressedKeys.test(3));
|
|
pif.UpdateButton(0, n64::Controller::RT, pressedKeys.test(4));
|
|
pif.UpdateButton(0, n64::Controller::Start, pressedKeys.test(5));
|
|
pif.UpdateButton(0, n64::Controller::DUp, pressedKeys.test(6));
|
|
pif.UpdateButton(0, n64::Controller::DDown, pressedKeys.test(7));
|
|
pif.UpdateButton(0, n64::Controller::DLeft, pressedKeys.test(8));
|
|
pif.UpdateButton(0, n64::Controller::DRight, pressedKeys.test(9));
|
|
pif.UpdateButton(0, n64::Controller::CUp, pressedKeys.test(10));
|
|
pif.UpdateButton(0, n64::Controller::CDown, pressedKeys.test(11));
|
|
pif.UpdateButton(0, n64::Controller::CLeft, pressedKeys.test(12));
|
|
pif.UpdateButton(0, n64::Controller::CRight, pressedKeys.test(13));
|
|
}
|
|
|
|
void KaizenGui::updateAxis() {
|
|
auto &pif = core.mem->mmio.si.pif;
|
|
|
|
if (gamepad) {
|
|
float xclamped = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
|
|
if (xclamped < 0) {
|
|
xclamped /= static_cast<float>(std::abs(SDL_JOYSTICK_AXIS_MAX));
|
|
} else {
|
|
xclamped /= SDL_JOYSTICK_AXIS_MAX;
|
|
}
|
|
|
|
xclamped *= 86;
|
|
|
|
float yclamped = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
|
|
if (yclamped < 0) {
|
|
yclamped /= static_cast<float>(std::abs(SDL_JOYSTICK_AXIS_MIN));
|
|
} else {
|
|
yclamped /= SDL_JOYSTICK_AXIS_MAX;
|
|
}
|
|
|
|
yclamped *= 86;
|
|
|
|
pif.UpdateAxis(0, n64::Controller::Axis::Y, static_cast<s8>(-yclamped));
|
|
pif.UpdateAxis(0, n64::Controller::Axis::X, static_cast<s8>(xclamped));
|
|
|
|
return;
|
|
}
|
|
|
|
s16 x = 0, y = 0;
|
|
if (pressedKeys.test(14)) // up
|
|
y += 86;
|
|
|
|
if (pressedKeys.test(15)) // down
|
|
y -= 86;
|
|
|
|
if (pressedKeys.test(16)) // left
|
|
x -= 86;
|
|
|
|
if (pressedKeys.test(17)) // right
|
|
x += 86;
|
|
|
|
core.mem->mmio.si.pif.UpdateAxis(0, n64::Controller::X, x);
|
|
core.mem->mmio.si.pif.UpdateAxis(0, n64::Controller::Y, y);
|
|
}
|
|
|
|
void KaizenGui::LoadROM(const std::string &path) noexcept {
|
|
core.LoadROM(path);
|
|
pause->setEnabled(true);
|
|
reset->setEnabled(true);
|
|
stop->setEnabled(true);
|
|
centralWidget->setCurrentWidget(vulkanWidget);
|
|
setWindowTitle(("Kaizen " KAIZEN_VERSION_STR " - " + core.mem->rom.gameNameDB).c_str());
|
|
}
|
|
|
|
void KaizenGui::LoadTAS(const std::string &path) noexcept { core.LoadTAS(fs::path(path)); }
|
|
|
|
void KaizenGui::cleanup() {
|
|
SDL_Quit();
|
|
core.Stop();
|
|
emuThread->requestInterruption();
|
|
emuThread->quit();
|
|
delete emuThread;
|
|
delete vulkanWidget;
|
|
delete settingsWindow;
|
|
settings.setValue("geometry", saveGeometry());
|
|
settings.setValue("windowState", saveState());
|
|
}
|
|
|
|
void KaizenGui::dropEvent(QDropEvent *event) {
|
|
if (event->mimeData()->hasUrls()) {
|
|
auto file = event->mimeData()->urls()[0].toLocalFile().toStdString();
|
|
LoadROM(file);
|
|
event->acceptProposedAction();
|
|
}
|
|
}
|
|
|
|
void KaizenGui::dragEnterEvent(QDragEnterEvent *event) {
|
|
if (event->mimeData()->hasUrls())
|
|
event->acceptProposedAction();
|
|
}
|
|
|
|
void KaizenGui::keyPressEvent(QKeyEvent *event) {
|
|
for (int i = 0; i < settingsWindow->input->mapping.size(); i++) {
|
|
if (settingsWindow->input->mapping[i] == event->key())
|
|
pressedKeys.set(i);
|
|
}
|
|
}
|
|
|
|
void KaizenGui::keyReleaseEvent(QKeyEvent *event) {
|
|
for (int i = 0; i < settingsWindow->input->mapping.size(); i++) {
|
|
if (settingsWindow->input->mapping[i] == event->key())
|
|
pressedKeys.reset(i);
|
|
}
|
|
}
|
|
|
|
bool KaizenGui::eventFilter(QObject *obj, QEvent *event) {
|
|
if (event->type() == QEvent::KeyPress) {
|
|
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
|
|
if (!settingsWindow->hasFocus())
|
|
return keyPressEvent(keyEvent), true;
|
|
}
|
|
|
|
if (event->type() == QEvent::KeyRelease) {
|
|
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
|
|
if (!settingsWindow->hasFocus())
|
|
return keyReleaseEvent(keyEvent), true;
|
|
}
|
|
|
|
return QMainWindow::eventFilter(obj, event);
|
|
}
|