#include #include #include #include #include #include KaizenGui::KaizenGui() noexcept : window("Kaizen", 800, 600), 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]); if (keys[SDL_SCANCODE_UP]) pif.UpdateAxis(0, n64::Controller::Axis::Y, 86); else pif.UpdateAxis(0, n64::Controller::Axis::Y, 0); if (keys[SDL_SCANCODE_DOWN]) pif.UpdateAxis(0, n64::Controller::Axis::Y, -86); else pif.UpdateAxis(0, n64::Controller::Axis::Y, 0); if (keys[SDL_SCANCODE_LEFT]) pif.UpdateAxis(0, n64::Controller::Axis::X, -86); else pif.UpdateAxis(0, n64::Controller::Axis::X, 0); if (keys[SDL_SCANCODE_RIGHT]) pif.UpdateAxis(0, n64::Controller::Axis::X, 86); else pif.UpdateAxis(0, n64::Controller::Axis::X, 0); } break; default: break; } } std::pair, std::optional> RenderErrorMessageDetails() { auto lastPC = Util::Error::GetLastPC(); if(lastPC.has_value()) { ImGui::Text("%s", std::format("Occurred @ PC = {:016X}", Util::Error::GetLastPC().value()).c_str()); } auto memoryAccess = Util::Error::GetMemoryAccess(); if(memoryAccess.has_value()) { const auto [is_write, size, address, written_val] = memoryAccess.value(); ImGui::Text("%s", std::format("{} {}-bit value @ {:08X}{}", is_write ? "Writing" : "Reading", static_cast(size), address, is_write ? std::format(" (value = 0x{:X})", written_val) : "") .c_str()); } return {lastPC, memoryAccess}; } 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); } if(ImGui::MenuItem("Open Debugger")) { debugger.Open(); } 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(!Util::Error::IsHandled()) { ImGui::OpenPopup(Util::Error::GetSeverity().as_c_str()); } if(settingsWindow.isOpen) { ImGui::OpenPopup("Settings", ImGuiPopupFlags_None); } if(aboutOpen) { ImGui::OpenPopup("About Kaizen"); } settingsWindow.render(); debugger.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."); if(ImGui::Button("OK")) { aboutOpen = false; ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); if (ImGui::BeginPopupModal(Util::Error::GetSeverity().as_c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { emuThread.TogglePause(); switch(Util::Error::GetSeverity().as_enum) { case Util::Error::Severity::WARN: { ImGui::PushStyleColor(ImGuiCol_TitleBg, 0x8054eae5); ImGui::PushStyleColor(ImGuiCol_Text, 0xff7be4e1); ImGui::Text("Warning of type: %s", Util::Error::GetType().as_c_str()); ImGui::PopStyleColor(); ImGui::PopStyleColor(); ImGui::Text(R"(Warning message: "%s")", Util::Error::GetError().c_str()); RenderErrorMessageDetails(); if(n64::Core::GetInstance().romLoaded && !n64::Core::GetInstance().pause) { const bool ignore = ImGui::Button("Try continuing"); ImGui::SameLine(); const bool stop = ImGui::Button("Stop emulation"); ImGui::SameLine(); const bool chooseAnother = ImGui::Button("Choose another ROM"); if(ignore || stop || chooseAnother) { Util::Error::SetHandled(); ImGui::CloseCurrentPopup(); } if(ignore) { emuThread.TogglePause(); } if(stop || chooseAnother) { emuThread.Stop(); } if(chooseAnother) { fileDialogOpen = true; } break; } if(ImGui::Button("OK")) ImGui::CloseCurrentPopup(); } break; case Util::Error::Severity::UNRECOVERABLE: { emuThread.Stop(); ImGui::PushStyleColor(ImGuiCol_TitleBg, 0x800000ff); ImGui::PushStyleColor(ImGuiCol_Text, 0xff3b3bbf); ImGui::Text("An unrecoverable error has occurred! Emulation has been stopped..."); ImGui::Text("Error of type: %s", Util::Error::GetType().as_c_str()); ImGui::PopStyleColor(); ImGui::PopStyleColor(); ImGui::Text(R"(Error message: "%s")", Util::Error::GetError().c_str()); RenderErrorMessageDetails(); if(ImGui::Button("OK")) ImGui::CloseCurrentPopup(); } break; case Util::Error::Severity::NON_FATAL: { ImGui::PushStyleColor(ImGuiCol_TitleBg, 0x800000ff); ImGui::PushStyleColor(ImGuiCol_Text, 0xff3b3bbf); ImGui::Text("An error has occurred!"); ImGui::Text("Error of type: %s", Util::Error::GetType().as_c_str()); ImGui::PopStyleColor(); ImGui::PopStyleColor(); ImGui::Text(R"(Error message: "%s")", Util::Error::GetError().c_str()); auto [lastPC, memoryAccess] = RenderErrorMessageDetails(); const bool ignore = ImGui::Button("Try continuing"); ImGui::SameLine(); const bool stop = ImGui::Button("Stop emulation"); ImGui::SameLine(); const bool chooseAnother = ImGui::Button("Choose another ROM"); const bool openInDebugger = lastPC.has_value() ? ImGui::Button("Add breakpoint at this PC and open the debugger") : false; if(ignore || stop || chooseAnother || openInDebugger) { Util::Error::SetHandled(); ImGui::CloseCurrentPopup(); } if(ignore) { emuThread.TogglePause(); } if(stop || chooseAnother) { emuThread.Stop(); } if(chooseAnother) { fileDialogOpen = true; } if(openInDebugger) { if(!n64::Core::GetInstance().breakpoints.contains(lastPC.value())) n64::Core::GetInstance().ToggleBreakpoint(lastPC.value()); debugger.Open(); emuThread.Reset(); } } break; default: break; } 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 - " + 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)); }