#include #include #include #include #include #include KaizenGui::KaizenGui() noexcept : window("Kaizen " KAIZEN_VERSION_STR, 1280, 720), settingsWindow(window), vulkanWidget(window.getHandle()), emuThread(fpsCounter, settingsWindow) { gui::Initialize(n64::Core::GetInstance().parallel.wsi, window.getHandle()); SDL_InitSubSystem(SDL_INIT_GAMEPAD); SDL_AddGamepadMapping(gamecontrollerdb_str); } KaizenGui::~KaizenGui() { gui::Cleanup(); SDL_Quit(); } void KaizenGui::QueryDevices(const SDL_Event &event) { switch (event.type) { case SDL_EVENT_GAMEPAD_ADDED: if (!gamepad) { const auto index = event.gdevice.which; gamepad = SDL_OpenGamepad(index); info("Found controller!"); info("Name: {}", SDL_GetGamepadName(gamepad)); info("Vendor: {}", SDL_GetGamepadVendor(gamepad)); } break; case SDL_EVENT_GAMEPAD_REMOVED: if (gamepad) SDL_CloseGamepad(gamepad); break; default: break; } } void KaizenGui::HandleInput(const SDL_Event &event) { const n64::Core &core = n64::Core::GetInstance(); n64::PIF &pif = n64::Core::GetMem().mmio.si.pif; switch (event.type) { case SDL_EVENT_GAMEPAD_AXIS_MOTION: if (!gamepad) break; { pif.UpdateButton(0, n64::Controller::Key::Z, SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) == SDL_JOYSTICK_AXIS_MAX); pif.UpdateButton(0, n64::Controller::Key::CUp, SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY) <= -127); pif.UpdateButton(0, n64::Controller::Key::CDown, SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY) >= 127); pif.UpdateButton(0, n64::Controller::Key::CLeft, SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX) <= -127); pif.UpdateButton(0, n64::Controller::Key::CRight, SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX) >= 127); float xclamped = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX); if (xclamped < 0) { xclamped /= static_cast(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(std::abs(SDL_JOYSTICK_AXIS_MIN)); } else { yclamped /= SDL_JOYSTICK_AXIS_MAX; } yclamped *= 86; pif.UpdateAxis(0, n64::Controller::Axis::Y, static_cast(-yclamped)); pif.UpdateAxis(0, n64::Controller::Axis::X, static_cast(xclamped)); } break; case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: if (!gamepad) break; pif.UpdateButton(0, n64::Controller::Key::A, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_SOUTH)); pif.UpdateButton(0, n64::Controller::Key::B, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_WEST)); pif.UpdateButton(0, n64::Controller::Key::Start, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_START)); pif.UpdateButton(0, n64::Controller::Key::DUp, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_UP)); pif.UpdateButton(0, n64::Controller::Key::DDown, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_DOWN)); pif.UpdateButton(0, n64::Controller::Key::DLeft, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_LEFT)); pif.UpdateButton(0, n64::Controller::Key::DRight, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT)); pif.UpdateButton(0, n64::Controller::Key::LT, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)); pif.UpdateButton(0, n64::Controller::Key::RT, SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)); break; case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: { const auto keys = SDL_GetKeyboardState(nullptr); if ((keys[SDL_SCANCODE_LCTRL] || keys[SDL_SCANCODE_RCTRL]) && keys[SDL_SCANCODE_O]) { fileDialogOpen = true; } fastForward = keys[SDL_SCANCODE_SPACE]; if (!unlockFramerate) core.parallel.SetFramerateUnlocked(fastForward); if (core.romLoaded) { if (keys[SDL_SCANCODE_P]) { emuThread.TogglePause(); } if (keys[SDL_SCANCODE_R]) { emuThread.Reset(); } if (keys[SDL_SCANCODE_Q]) { emuThread.Stop(); } } if (gamepad) break; pif.UpdateButton(0, n64::Controller::Key::Z, keys[SDL_SCANCODE_Z]); pif.UpdateButton(0, n64::Controller::Key::CUp, keys[SDL_SCANCODE_HOME]); pif.UpdateButton(0, n64::Controller::Key::CDown, keys[SDL_SCANCODE_END]); pif.UpdateButton(0, n64::Controller::Key::CLeft, keys[SDL_SCANCODE_DELETE]); pif.UpdateButton(0, n64::Controller::Key::CRight, keys[SDL_SCANCODE_PAGEDOWN]); pif.UpdateButton(0, n64::Controller::Key::A, keys[SDL_SCANCODE_X]); pif.UpdateButton(0, n64::Controller::Key::B, keys[SDL_SCANCODE_C]); pif.UpdateButton(0, n64::Controller::Key::Start, keys[SDL_SCANCODE_RETURN]); pif.UpdateButton(0, n64::Controller::Key::DUp, keys[SDL_SCANCODE_I]); pif.UpdateButton(0, n64::Controller::Key::DDown, keys[SDL_SCANCODE_K]); pif.UpdateButton(0, n64::Controller::Key::DLeft, keys[SDL_SCANCODE_J]); pif.UpdateButton(0, n64::Controller::Key::DRight, keys[SDL_SCANCODE_L]); pif.UpdateButton(0, n64::Controller::Key::LT, keys[SDL_SCANCODE_A]); pif.UpdateButton(0, n64::Controller::Key::RT, keys[SDL_SCANCODE_S]); float x = 0, y = 0; if (keys[SDL_SCANCODE_UP]) y = 86; if (keys[SDL_SCANCODE_DOWN]) y = -86; if (keys[SDL_SCANCODE_LEFT]) x = -86; if (keys[SDL_SCANCODE_RIGHT]) x = 86; pif.UpdateAxis(0, n64::Controller::Axis::X, x); pif.UpdateAxis(0, n64::Controller::Axis::Y, y); } break; default: break; } } void KaizenGui::RenderUI() { n64::Core &core = n64::Core::GetInstance(); gui::StartFrame(); if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Open", "Ctrl-O")) { fileDialogOpen = true; } if (ImGui::MenuItem("Exit")) { quit = true; emuThread.Stop(); } ImGui::EndMenu(); } if (ImGui::BeginMenu("Emulation")) { ImGui::BeginDisabled(!core.romLoaded); if (ImGui::MenuItem(core.pause ? "Resume" : "Pause", "P")) { emuThread.TogglePause(); } if (ImGui::MenuItem("Reset", "R")) { emuThread.Reset(); } if (ImGui::MenuItem("Stop", "Q")) { emuThread.Stop(); core.romLoaded = false; } if (ImGui::Checkbox("Unlock framerate", &unlockFramerate)) { core.parallel.SetFramerateUnlocked(unlockFramerate); } ImGui::EndDisabled(); if (ImGui::MenuItem("Options")) { settingsWindow.isOpen = true; } ImGui::EndMenu(); } if (ImGui::BeginMenu("Help")) { if (ImGui::MenuItem("About")) { aboutOpen = true; } ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } if (settingsWindow.isOpen) { ImGui::OpenPopup("Settings", ImGuiPopupFlags_None); } if (aboutOpen) { ImGui::OpenPopup("About Kaizen"); } settingsWindow.render(); const ImVec2 center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); if (ImGui::BeginPopupModal("About Kaizen", &aboutOpen, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Kaizen is a Nintendo 64 emulator that strives"); ImGui::Text("to offer a friendly user experience and compatibility."); ImGui::Text("Kaizen is licensed under the BSD 3-clause license."); ImGui::Text("Nintendo 64 is a registered trademark of Nintendo Co., Ltd."); ImGui::Separator(); ImGui::Text("Kaizen %s%s", KAIZEN_USE_HASH ? "dev build " : "", KAIZEN_VERSION_STR); ImGui::Separator(); if (ImGui::Button("OK")) { aboutOpen = false; ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } if (ImGui::BeginMainStatusBar()) { ImGui::Text("FPS: %.2f", ImGui::GetIO().Framerate); ImGui::EndMainStatusBar(); } if (shouldDisplaySpinner) { ImGui::SetNextWindowPos({static_cast(width) * 0.5f, static_cast(height) * 0.5f}, 0, ImVec2(0.5f, 0.5f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, IM_COL32_BLACK_TRANS); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::Begin("##spinnerContainer", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration); ImGui::Spinner("##spinner", 10.f, 4.f, ImGui::GetColorU32(ImGui::GetStyle().Colors[ImGuiCol_TitleBgActive])); ImGui::SameLine(); ImGui::PushFont(nullptr, ImGui::GetStyle().FontSizeBase * 2.f); ImGui::Text("Loading \"%s\"...", fs::path(fileToLoad).filename().string().c_str()); ImGui::PopFont(); ImGui::End(); ImGui::PopStyleVar(); ImGui::PopStyleColor(); } ImGui::Render(); if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); } if (fileDialogOpen) { fileDialogOpen = false; constexpr SDL_DialogFileFilter filters[] = {{"All files", "*"}, {"Nintendo 64 executable", "n64;z64;v64"}, {"Nintendo 64 executable archive", "rar;tar;zip;7z"}}; SDL_ShowOpenFileDialog( [](void *userdata, const char *const *filelist, int) { auto kaizen = static_cast(userdata); if (!filelist) { panic("An error occured: {}", SDL_GetError()); } if (!*filelist) { warn("The user did not select any file."); warn("Most likely, the dialog was canceled."); return; } kaizen->fileToLoad = *filelist; kaizen->shouldDisplaySpinner = true; std::thread fileWorker(&KaizenGui::FileWorker, kaizen); fileWorker.detach(); }, this, window.getHandle(), filters, 3, nullptr, false); } if (minimized) return; if (core.romLoaded) { core.parallel.UpdateScreen(); return; } core.parallel.UpdateScreen(); } 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; SDL_SetWindowTitle(window.getHandle(), ("Kaizen " KAIZEN_VERSION_STR " - " + gameNameDB).c_str()); } void KaizenGui::run() { while (!quit) { SDL_Event e; while (SDL_PollEvent(&e)) { ImGui_ImplSDL3_ProcessEvent(&e); switch (e.type) { case SDL_EVENT_QUIT: quit = true; emuThread.Stop(); break; case SDL_EVENT_WINDOW_MINIMIZED: minimized = true; break; case SDL_EVENT_WINDOW_RESTORED: minimized = false; break; default: } QueryDevices(e); HandleInput(e); } SDL_GetWindowSize(window.getHandle(), &width, &height); emuThread.run(); RenderUI(); } } void KaizenGui::LoadTAS(const std::string &path) noexcept { n64::Core::GetInstance().LoadTAS(fs::path(path)); }