/* Copyright 2016-2023 melonDS team This file is part of melonDS. melonDS is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. melonDS is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with melonDS. If not, see http://www.gnu.org/licenses/. */ #include #include #include #include #include #include #include #include #include #include "main.h" #include "types.h" #include "version.h" #include "ScreenLayout.h" #include "Args.h" #include "NDS.h" #include "NDSCart.h" #include "GBACart.h" #include "GPU.h" #include "SPU.h" #include "Wifi.h" #include "Platform.h" #include "LocalMP.h" #include "Config.h" #include "RTC.h" #include "DSi.h" #include "DSi_I2C.h" #include "GPU3D_Soft.h" #include "GPU3D_OpenGL.h" #include "GPU3D_Compute.h" #include "Savestate.h" #include "EmuInstance.h" using namespace melonDS; EmuThread::EmuThread(EmuInstance* inst, QObject* parent) : QThread(parent) { emuInstance = inst; EmuStatus = emuStatus_Exit; EmuRunning = emuStatus_Paused; EmuPauseStack = EmuPauseStackRunning; emuActive = false; } void EmuThread::attachWindow(MainWindow* window) { connect(this, SIGNAL(windowUpdate()), window->panel, SLOT(repaint())); connect(this, SIGNAL(windowTitleChange(QString)), window, SLOT(onTitleUpdate(QString))); connect(this, SIGNAL(windowEmuStart()), window, SLOT(onEmuStart())); connect(this, SIGNAL(windowEmuStop()), window, SLOT(onEmuStop())); connect(this, SIGNAL(windowEmuPause(bool)), window, SLOT(onEmuPause(bool))); connect(this, SIGNAL(windowEmuReset()), window->actReset, SLOT(trigger())); connect(this, SIGNAL(windowEmuFrameStep()), window->actFrameStep, SLOT(trigger())); connect(this, SIGNAL(windowLimitFPSChange()), window->actLimitFramerate, SLOT(trigger())); connect(this, SIGNAL(autoScreenSizingChange(int)), window->panel, SLOT(onAutoScreenSizingChanged(int))); connect(this, SIGNAL(windowFullscreenToggle()), window, SLOT(onFullscreenToggled())); connect(this, SIGNAL(swapScreensToggle()), window->actScreenSwap, SLOT(trigger())); connect(this, SIGNAL(screenEmphasisToggle()), window, SLOT(onScreenEmphasisToggled())); } void EmuThread::detachWindow(MainWindow* window) { disconnect(this, SIGNAL(windowUpdate()), window->panel, SLOT(repaint())); disconnect(this, SIGNAL(windowTitleChange(QString)), window, SLOT(onTitleUpdate(QString))); disconnect(this, SIGNAL(windowEmuStart()), window, SLOT(onEmuStart())); disconnect(this, SIGNAL(windowEmuStop()), window, SLOT(onEmuStop())); disconnect(this, SIGNAL(windowEmuPause(bool)), window, SLOT(onEmuPause(bool))); disconnect(this, SIGNAL(windowEmuReset()), window->actReset, SLOT(trigger())); disconnect(this, SIGNAL(windowEmuFrameStep()), window->actFrameStep, SLOT(trigger())); disconnect(this, SIGNAL(windowLimitFPSChange()), window->actLimitFramerate, SLOT(trigger())); disconnect(this, SIGNAL(autoScreenSizingChange(int)), window->panel, SLOT(onAutoScreenSizingChanged(int))); disconnect(this, SIGNAL(windowFullscreenToggle()), window, SLOT(onFullscreenToggled())); disconnect(this, SIGNAL(swapScreensToggle()), window->actScreenSwap, SLOT(trigger())); disconnect(this, SIGNAL(screenEmphasisToggle()), window, SLOT(onScreenEmphasisToggled())); } void EmuThread::run() { Config::Table& globalCfg = emuInstance->getGlobalConfig(); u32 mainScreenPos[3]; Platform::FileHandle* file; emuInstance->updateConsole(nullptr, nullptr); // No carts are inserted when melonDS first boots mainScreenPos[0] = 0; mainScreenPos[1] = 0; mainScreenPos[2] = 0; autoScreenSizing = 0; videoSettingsDirty = false; if (emuInstance->usesOpenGL()) { emuInstance->initOpenGL(); useOpenGL = true; videoRenderer = globalCfg.GetInt("3D.Renderer"); } else { useOpenGL = false; videoRenderer = 0; } updateRenderer(); u32 nframes = 0; double perfCountsSec = 1.0 / SDL_GetPerformanceFrequency(); double lastTime = SDL_GetPerformanceCounter() * perfCountsSec; double frameLimitError = 0.0; double lastMeasureTime = lastTime; u32 winUpdateCount = 0, winUpdateFreq = 1; u8 dsiVolumeLevel = 0x1F; file = Platform::OpenLocalFile("rtc.bin", Platform::FileMode::Read); if (file) { RTC::StateData state; Platform::FileRead(&state, sizeof(state), 1, file); Platform::CloseFile(file); emuInstance->nds->RTC.SetState(state); } char melontitle[100]; while (EmuRunning != emuStatus_Exit) { emuInstance->inputProcess(); if (emuInstance->hotkeyPressed(HK_FastForwardToggle)) emit windowLimitFPSChange(); if (emuInstance->hotkeyPressed(HK_Pause)) emuTogglePause(); if (emuInstance->hotkeyPressed(HK_Reset)) emit windowEmuReset(); if (emuInstance->hotkeyPressed(HK_FrameStep)) emit windowEmuFrameStep(); if (emuInstance->hotkeyPressed(HK_FullscreenToggle)) emit windowFullscreenToggle(); if (emuInstance->hotkeyPressed(HK_SwapScreens)) emit swapScreensToggle(); if (emuInstance->hotkeyPressed(HK_SwapScreenEmphasis)) emit screenEmphasisToggle(); if (EmuRunning == emuStatus_Running || EmuRunning == emuStatus_FrameStep) { EmuStatus = emuStatus_Running; if (EmuRunning == emuStatus_FrameStep) EmuRunning = emuStatus_Paused; if (emuInstance->hotkeyPressed(HK_SolarSensorDecrease)) { int level = emuInstance->nds->GBACartSlot.SetInput(GBACart::Input_SolarSensorDown, true); if (level != -1) { emuInstance->osdAddMessage(0, "Solar sensor level: %d", level); } } if (emuInstance->hotkeyPressed(HK_SolarSensorIncrease)) { int level = emuInstance->nds->GBACartSlot.SetInput(GBACart::Input_SolarSensorUp, true); if (level != -1) { emuInstance->osdAddMessage(0, "Solar sensor level: %d", level); } } if (emuInstance->nds->ConsoleType == 1) { DSi* dsi = static_cast(emuInstance->nds); double currentTime = SDL_GetPerformanceCounter() * perfCountsSec; // Handle power button if (emuInstance->hotkeyDown(HK_PowerButton)) { dsi->I2C.GetBPTWL()->SetPowerButtonHeld(currentTime); } else if (emuInstance->hotkeyReleased(HK_PowerButton)) { dsi->I2C.GetBPTWL()->SetPowerButtonReleased(currentTime); } // Handle volume buttons if (emuInstance->hotkeyDown(HK_VolumeUp)) { dsi->I2C.GetBPTWL()->SetVolumeSwitchHeld(DSi_BPTWL::volumeKey_Up); } else if (emuInstance->hotkeyReleased(HK_VolumeUp)) { dsi->I2C.GetBPTWL()->SetVolumeSwitchReleased(DSi_BPTWL::volumeKey_Up); } if (emuInstance->hotkeyDown(HK_VolumeDown)) { dsi->I2C.GetBPTWL()->SetVolumeSwitchHeld(DSi_BPTWL::volumeKey_Down); } else if (emuInstance->hotkeyReleased(HK_VolumeDown)) { dsi->I2C.GetBPTWL()->SetVolumeSwitchReleased(DSi_BPTWL::volumeKey_Down); } dsi->I2C.GetBPTWL()->ProcessVolumeSwitchInput(currentTime); } if (useOpenGL) emuInstance->makeCurrentGL(); // update render settings if needed if (videoSettingsDirty) { if (useOpenGL) { emuInstance->setVSyncGL(true); videoRenderer = globalCfg.GetInt("3D.Renderer"); } #ifdef OGLRENDERER_ENABLED else #endif { videoRenderer = 0; } updateRenderer(); videoSettingsDirty = false; } // process input and hotkeys emuInstance->nds->SetKeyMask(emuInstance->inputMask); if (emuInstance->hotkeyPressed(HK_Lid)) { bool lid = !emuInstance->nds->IsLidClosed(); emuInstance->nds->SetLidClosed(lid); emuInstance->osdAddMessage(0, lid ? "Lid closed" : "Lid opened"); } // microphone input emuInstance->micProcess(); // auto screen layout { mainScreenPos[2] = mainScreenPos[1]; mainScreenPos[1] = mainScreenPos[0]; mainScreenPos[0] = emuInstance->nds->PowerControl9 >> 15; int guess; if (mainScreenPos[0] == mainScreenPos[2] && mainScreenPos[0] != mainScreenPos[1]) { // constant flickering, likely displaying 3D on both screens // TODO: when both screens are used for 2D only...??? guess = screenSizing_Even; } else { if (mainScreenPos[0] == 1) guess = screenSizing_EmphTop; else guess = screenSizing_EmphBot; } if (guess != autoScreenSizing) { autoScreenSizing = guess; emit autoScreenSizingChange(autoScreenSizing); } } // emulate u32 nlines; if (emuInstance->nds->GPU.GetRenderer3D().NeedsShaderCompile()) { compileShaders(); nlines = 1; } else { nlines = emuInstance->nds->RunFrame(); } if (emuInstance->ndsSave) emuInstance->ndsSave->CheckFlush(); if (emuInstance->gbaSave) emuInstance->gbaSave->CheckFlush(); if (emuInstance->firmwareSave) emuInstance->firmwareSave->CheckFlush(); if (!useOpenGL) { FrontBufferLock.lock(); FrontBuffer = emuInstance->nds->GPU.FrontBuffer; FrontBufferLock.unlock(); } else { FrontBuffer = emuInstance->nds->GPU.FrontBuffer; emuInstance->drawScreenGL(); } #ifdef MELONCAP MelonCap::Update(); #endif // MELONCAP if (EmuRunning == emuStatus_Exit) break; winUpdateCount++; if (winUpdateCount >= winUpdateFreq && !useOpenGL) { emit windowUpdate(); winUpdateCount = 0; } bool fastforward = emuInstance->hotkeyDown(HK_FastForward); if (useOpenGL) { // when using OpenGL: when toggling fast-forward, change the vsync interval if (emuInstance->hotkeyPressed(HK_FastForward)) { emuInstance->setVSyncGL(false); } else if (emuInstance->hotkeyReleased(HK_FastForward)) { emuInstance->setVSyncGL(true); } } if (emuInstance->audioDSiVolumeSync && emuInstance->nds->ConsoleType == 1) { DSi* dsi = static_cast(emuInstance->nds); u8 volumeLevel = dsi->I2C.GetBPTWL()->GetVolumeLevel(); if (volumeLevel != dsiVolumeLevel) { dsiVolumeLevel = volumeLevel; emit syncVolumeLevel(); } emuInstance->audioVolume = volumeLevel * (256.0 / 31.0); } if (emuInstance->doAudioSync && !fastforward) emuInstance->audioSync(); double frametimeStep = nlines / (60.0 * 263.0); { bool limitfps = emuInstance->doLimitFPS && !fastforward; double practicalFramelimit = limitfps ? frametimeStep : 1.0 / emuInstance->maxFPS; double curtime = SDL_GetPerformanceCounter() * perfCountsSec; frameLimitError += practicalFramelimit - (curtime - lastTime); if (frameLimitError < -practicalFramelimit) frameLimitError = -practicalFramelimit; if (frameLimitError > practicalFramelimit) frameLimitError = practicalFramelimit; if (round(frameLimitError * 1000.0) > 0.0) { SDL_Delay(round(frameLimitError * 1000.0)); double timeBeforeSleep = curtime; curtime = SDL_GetPerformanceCounter() * perfCountsSec; frameLimitError -= curtime - timeBeforeSleep; } lastTime = curtime; } nframes++; if (nframes >= 30) { double time = SDL_GetPerformanceCounter() * perfCountsSec; double dt = time - lastMeasureTime; lastMeasureTime = time; u32 fps = round(nframes / dt); nframes = 0; float fpstarget = 1.0/frametimeStep; winUpdateFreq = fps / (u32)round(fpstarget); if (winUpdateFreq < 1) winUpdateFreq = 1; int inst = emuInstance->instanceID; if (inst == 0) sprintf(melontitle, "[%d/%.0f] melonDS " MELONDS_VERSION, fps, fpstarget); else sprintf(melontitle, "[%d/%.0f] melonDS (%d)", fps, fpstarget, inst+1); changeWindowTitle(melontitle); } } else { // paused nframes = 0; lastTime = SDL_GetPerformanceCounter() * perfCountsSec; lastMeasureTime = lastTime; emit windowUpdate(); EmuStatus = EmuRunning; int inst = emuInstance->instanceID; if (inst == 0) sprintf(melontitle, "melonDS " MELONDS_VERSION); else sprintf(melontitle, "melonDS (%d)", inst+1); changeWindowTitle(melontitle); SDL_Delay(75); if (useOpenGL) { emuInstance->drawScreenGL(); } } handleMessages(); } file = Platform::OpenLocalFile("rtc.bin", Platform::FileMode::Write); if (file) { RTC::StateData state; emuInstance->nds->RTC.GetState(state); Platform::FileWrite(&state, sizeof(state), 1, file); Platform::CloseFile(file); } EmuStatus = emuStatus_Exit; NDS::Current = nullptr; } void EmuThread::sendMessage(Message msg) { msgMutex.lock(); msgQueue.enqueue(msg); msgMutex.unlock(); } void EmuThread::waitMessage() { if (QThread::currentThread() == this) return; msgSemaphore.acquire(); } void EmuThread::waitAllMessages() { if (QThread::currentThread() == this) return; msgSemaphore.acquire(msgSemaphore.available()); } void EmuThread::handleMessages() { msgMutex.lock(); while (!msgQueue.empty()) { Message msg = msgQueue.dequeue(); switch (msg.type) { case msg_EmuRun: EmuRunning = emuStatus_Running; EmuPauseStack = EmuPauseStackRunning; emuActive = true; emuInstance->audioEnable(); emit windowEmuStart(); break; case msg_EmuPause: EmuPauseStack++; if (EmuPauseStack > EmuPauseStackPauseThreshold) break; PrevEmuStatus = EmuRunning; EmuRunning = emuStatus_Paused; if (PrevEmuStatus != emuStatus_Paused) { emuInstance->audioDisable(); emit windowEmuPause(true); emuInstance->osdAddMessage(0, "Paused"); } break; case msg_EmuUnpause: if (EmuPauseStack < EmuPauseStackPauseThreshold) break; EmuPauseStack--; if (EmuPauseStack >= EmuPauseStackPauseThreshold) break; EmuRunning = PrevEmuStatus; if (EmuRunning != emuStatus_Paused) { emuInstance->audioEnable(); emit windowEmuPause(false); emuInstance->osdAddMessage(0, "Resumed"); } break; case msg_EmuStop: if (msg.stopExternal) emuInstance->nds->Stop(); EmuRunning = emuStatus_Paused; emuActive = false; emuInstance->audioDisable(); emit windowEmuStop(); break; case msg_InitGL: emuInstance->initOpenGL(); useOpenGL = true; break; case msg_DeInitGL: emuInstance->deinitOpenGL(); useOpenGL = false; break; } msgSemaphore.release(); } msgMutex.unlock(); } void EmuThread::changeWindowTitle(char* title) { emit windowTitleChange(QString(title)); } void EmuThread::initContext() { sendMessage(msg_InitGL); waitMessage(); } void EmuThread::deinitContext() { sendMessage(msg_DeInitGL); waitMessage(); } void EmuThread::emuRun() { sendMessage(msg_EmuRun); waitMessage(); } void EmuThread::emuPause() { sendMessage(msg_EmuPause); waitMessage(); } void EmuThread::emuUnpause() { sendMessage(msg_EmuUnpause); waitMessage(); } void EmuThread::emuTogglePause() { if (EmuRunning == emuStatus_Paused) emuUnpause(); else emuPause(); } void EmuThread::emuStop(bool external) { sendMessage({.type = msg_EmuStop, .stopExternal = external}); waitMessage(); } void EmuThread::emuExit() { EmuRunning = emuStatus_Exit; EmuPauseStack = EmuPauseStackRunning; emuInstance->audioDisable(); } void EmuThread::emuFrameStep() { //if (EmuPauseStack < EmuPauseStackPauseThreshold) emit windowEmuPause(); EmuRunning = emuStatus_FrameStep; } bool EmuThread::emuIsRunning() { return EmuRunning == emuStatus_Running; } bool EmuThread::emuIsActive() { return emuActive; } void EmuThread::updateRenderer() { if (videoRenderer != lastVideoRenderer) { printf("creating renderer %d\n", videoRenderer); switch (videoRenderer) { case renderer3D_Software: emuInstance->nds->GPU.SetRenderer3D(std::make_unique()); break; case renderer3D_OpenGL: emuInstance->nds->GPU.SetRenderer3D(GLRenderer::New()); break; case renderer3D_OpenGLCompute: emuInstance->nds->GPU.SetRenderer3D(ComputeRenderer::New()); break; default: __builtin_unreachable(); } } lastVideoRenderer = videoRenderer; auto& cfg = emuInstance->getGlobalConfig(); switch (videoRenderer) { case renderer3D_Software: static_cast(emuInstance->nds->GPU.GetRenderer3D()).SetThreaded( cfg.GetBool("3D.Soft.Threaded"), emuInstance->nds->GPU); break; case renderer3D_OpenGL: static_cast(emuInstance->nds->GPU.GetRenderer3D()).SetRenderSettings( cfg.GetBool("3D.GL.BetterPolygons"), cfg.GetInt("3D.GL.ScaleFactor")); break; case renderer3D_OpenGLCompute: static_cast(emuInstance->nds->GPU.GetRenderer3D()).SetRenderSettings( cfg.GetInt("3D.GL.ScaleFactor"), cfg.GetBool("3D.GL.HiresCoordinates")); break; default: __builtin_unreachable(); } } void EmuThread::compileShaders() { int currentShader, shadersCount; u64 startTime = SDL_GetPerformanceCounter(); // kind of hacky to look at the wallclock, though it is easier than // than disabling vsync do { emuInstance->nds->GPU.GetRenderer3D().ShaderCompileStep(currentShader, shadersCount); } while (emuInstance->nds->GPU.GetRenderer3D().NeedsShaderCompile() && (SDL_GetPerformanceCounter() - startTime) * perfCountsSec < 1.0 / 6.0); emuInstance->osdAddMessage(0, "Compiling shader %d/%d", currentShader+1, shadersCount); }