/* Copyright 2016-2024 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_Paused; emuPauseStack = emuPauseStackRunning; emuActive = false; } void EmuThread::attachWindow(MainWindow* window) { 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, SLOT(onEmuReset())); connect(this, SIGNAL(autoScreenSizingChange(int)), window->panel, SLOT(onAutoScreenSizingChanged(int))); connect(this, SIGNAL(windowFullscreenToggle()), window, SLOT(onFullscreenToggled())); connect(this, SIGNAL(screenEmphasisToggle()), window, SLOT(onScreenEmphasisToggled())); if (window->winHasMenu()) { connect(this, SIGNAL(windowLimitFPSChange()), window->actLimitFramerate, SLOT(trigger())); connect(this, SIGNAL(swapScreensToggle()), window->actScreenSwap, SLOT(trigger())); } } void EmuThread::detachWindow(MainWindow* window) { 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, SLOT(onEmuReset())); disconnect(this, SIGNAL(autoScreenSizingChange(int)), window->panel, SLOT(onAutoScreenSizingChanged(int))); disconnect(this, SIGNAL(windowFullscreenToggle()), window, SLOT(onFullscreenToggled())); disconnect(this, SIGNAL(screenEmphasisToggle()), window, SLOT(onScreenEmphasisToggled())); if (window->winHasMenu()) { disconnect(this, SIGNAL(windowLimitFPSChange()), window->actLimitFramerate, SLOT(trigger())); disconnect(this, SIGNAL(swapScreensToggle()), window->actScreenSwap, SLOT(trigger())); } } void EmuThread::run() { Config::Table& globalCfg = emuInstance->getGlobalConfig(); u32 mainScreenPos[3]; //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(0); useOpenGL = true; videoRenderer = globalCfg.GetInt("3D.Renderer"); } else { useOpenGL = false; videoRenderer = 0; } //updateRenderer(); videoSettingsDirty = true; 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; char melontitle[100]; bool fastforward = false; bool slowmo = false; emuInstance->fastForwardToggled = false; emuInstance->slowmoToggled = false; while (emuStatus != emuStatus_Exit) { MPInterface::Get().Process(); emuInstance->inputProcess(); if (emuInstance->hotkeyPressed(HK_FrameLimitToggle)) emit windowLimitFPSChange(); if (emuInstance->hotkeyPressed(HK_Pause)) emuTogglePause(); if (emuInstance->hotkeyPressed(HK_Reset)) emuReset(); if (emuInstance->hotkeyPressed(HK_FrameStep)) emuFrameStep(); if (emuInstance->hotkeyPressed(HK_FullscreenToggle)) emit windowFullscreenToggle(); if (emuInstance->hotkeyPressed(HK_SwapScreens)) emit swapScreensToggle(); if (emuInstance->hotkeyPressed(HK_SwapScreenEmphasis)) emit screenEmphasisToggle(); if (emuStatus == emuStatus_Running || emuStatus == emuStatus_FrameStep) { if (emuStatus == emuStatus_FrameStep) emuStatus = 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 winUpdateCount++; if (winUpdateCount >= winUpdateFreq && !useOpenGL) { emit windowUpdate(); winUpdateCount = 0; } if (emuInstance->hotkeyPressed(HK_FastForwardToggle)) emuInstance->fastForwardToggled = !emuInstance->fastForwardToggled; if (emuInstance->hotkeyPressed(HK_SlowMoToggle)) emuInstance->slowmoToggled = !emuInstance->slowmoToggled; bool enablefastforward = emuInstance->hotkeyDown(HK_FastForward) | emuInstance->fastForwardToggled; bool enableslowmo = emuInstance->hotkeyDown(HK_SlowMo) | emuInstance->slowmoToggled; if (useOpenGL) { // when using OpenGL: when toggling fast-forward or slowmo, change the vsync interval if ((enablefastforward || enableslowmo) && !(fastforward || slowmo)) { emuInstance->setVSyncGL(false); } else if (!(enablefastforward || enableslowmo) && (fastforward || slowmo)) { emuInstance->setVSyncGL(true); } } fastforward = enablefastforward; slowmo = enableslowmo; if (slowmo) emuInstance->curFPS = emuInstance->slowmoFPS; else if (fastforward) emuInstance->curFPS = emuInstance->fastForwardFPS; else if (!emuInstance->doLimitFPS) emuInstance->curFPS = 1000.0; else emuInstance->curFPS = emuInstance->targetFPS; 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 || slowmo)) emuInstance->audioSync(); double frametimeStep = nlines / (emuInstance->curFPS * 263.0); if (frametimeStep < 0.001) frametimeStep = 0.001; { double curtime = SDL_GetPerformanceCounter() * perfCountsSec; frameLimitError += frametimeStep - (curtime - lastTime); if (frameLimitError < -frametimeStep) frameLimitError = -frametimeStep; if (frameLimitError > frametimeStep) frameLimitError = frametimeStep; 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; double actualfps = (59.8261 * 263.0) / nlines; int inst = emuInstance->instanceID; if (inst == 0) sprintf(melontitle, "[%d/%.0f] melonDS " MELONDS_VERSION, fps, actualfps); 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(); 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(); } } void EmuThread::sendMessage(Message msg) { msgMutex.lock(); msgQueue.enqueue(msg); msgMutex.unlock(); } void EmuThread::waitMessage(int num) { if (QThread::currentThread() == this) return; msgSemaphore.acquire(num); } 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_Exit: emuStatus = emuStatus_Exit; emuPauseStack = emuPauseStackRunning; emuInstance->audioDisable(); break; case msg_EmuRun: emuStatus = emuStatus_Running; emuPauseStack = emuPauseStackRunning; emuActive = true; emuInstance->audioEnable(); emit windowEmuStart(); break; case msg_EmuPause: emuPauseStack++; if (emuPauseStack > emuPauseStackPauseThreshold) break; prevEmuStatus = emuStatus; emuStatus = 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; emuStatus = prevEmuStatus; if (emuStatus != emuStatus_Paused) { emuInstance->audioEnable(); emit windowEmuPause(false); emuInstance->osdAddMessage(0, "Resumed"); } break; case msg_EmuStop: if (msg.param.value()) emuInstance->nds->Stop(); emuStatus = emuStatus_Paused; emuActive = false; emuInstance->audioDisable(); emit windowEmuStop(); break; case msg_EmuFrameStep: emuStatus = emuStatus_FrameStep; break; case msg_EmuReset: emuInstance->reset(); emuStatus = emuStatus_Running; emuPauseStack = emuPauseStackRunning; emuActive = true; emuInstance->audioEnable(); emit windowEmuReset(); emuInstance->osdAddMessage(0, "Reset"); break; case msg_InitGL: emuInstance->initOpenGL(msg.param.value()); useOpenGL = true; break; case msg_DeInitGL: emuInstance->deinitOpenGL(msg.param.value()); if (msg.param.value() == 0) useOpenGL = false; break; case msg_BootROM: msgResult = 0; if (!emuInstance->loadROM(msg.param.value(), true)) break; assert(emuInstance->nds != nullptr); emuInstance->nds->Start(); msgResult = 1; break; case msg_BootFirmware: msgResult = 0; if (!emuInstance->bootToMenu()) break; assert(emuInstance->nds != nullptr); emuInstance->nds->Start(); msgResult = 1; break; case msg_InsertCart: msgResult = 0; if (!emuInstance->loadROM(msg.param.value(), false)) break; msgResult = 1; break; case msg_EjectCart: emuInstance->ejectCart(); break; case msg_InsertGBACart: msgResult = 0; if (!emuInstance->loadGBAROM(msg.param.value())) break; msgResult = 1; break; case msg_InsertGBAAddon: msgResult = 0; emuInstance->loadGBAAddon(msg.param.value()); msgResult = 1; break; case msg_EjectGBACart: emuInstance->ejectGBACart(); break; case msg_SaveState: msgResult = emuInstance->saveState(msg.param.value().toStdString()); break; case msg_LoadState: msgResult = emuInstance->loadState(msg.param.value().toStdString()); break; case msg_UndoStateLoad: emuInstance->undoStateLoad(); msgResult = 1; break; case msg_ImportSavefile: { msgResult = 0; auto f = Platform::OpenFile(msg.param.value().toStdString(), Platform::FileMode::Read); if (!f) break; u32 len = FileLength(f); std::unique_ptr data = std::make_unique(len); Platform::FileRewind(f); Platform::FileRead(data.get(), len, 1, f); assert(emuInstance->nds != nullptr); emuInstance->nds->SetNDSSave(data.get(), len); CloseFile(f); msgResult = 1; } break; } msgSemaphore.release(); } msgMutex.unlock(); } void EmuThread::changeWindowTitle(char* title) { emit windowTitleChange(QString(title)); } void EmuThread::initContext(int win) { sendMessage({.type = msg_InitGL, .param = win}); waitMessage(); } void EmuThread::deinitContext(int win) { sendMessage({.type = msg_DeInitGL, .param = win}); waitMessage(); } void EmuThread::emuRun() { sendMessage(msg_EmuRun); waitMessage(); } void EmuThread::emuPause(bool broadcast) { sendMessage(msg_EmuPause); waitMessage(); if (broadcast) emuInstance->broadcastCommand(InstCmd_Pause); } void EmuThread::emuUnpause(bool broadcast) { sendMessage(msg_EmuUnpause); waitMessage(); if (broadcast) emuInstance->broadcastCommand(InstCmd_Unpause); } void EmuThread::emuTogglePause(bool broadcast) { if (emuStatus == emuStatus_Paused) emuUnpause(broadcast); else emuPause(broadcast); } void EmuThread::emuStop(bool external) { sendMessage({.type = msg_EmuStop, .param = external}); waitMessage(); } void EmuThread::emuExit() { sendMessage(msg_Exit); waitAllMessages(); } void EmuThread::emuFrameStep() { if (emuPauseStack < emuPauseStackPauseThreshold) sendMessage(msg_EmuPause); sendMessage(msg_EmuFrameStep); waitAllMessages(); } void EmuThread::emuReset() { sendMessage(msg_EmuReset); waitMessage(); } bool EmuThread::emuIsRunning() { return emuStatus == emuStatus_Running; } bool EmuThread::emuIsActive() { return emuActive; } int EmuThread::bootROM(const QStringList& filename) { sendMessage({.type = msg_BootROM, .param = filename}); waitMessage(); if (!msgResult) return msgResult; sendMessage(msg_EmuRun); waitMessage(); return msgResult; } int EmuThread::bootFirmware() { sendMessage(msg_BootFirmware); waitMessage(); if (!msgResult) return msgResult; sendMessage(msg_EmuRun); waitMessage(); return msgResult; } int EmuThread::insertCart(const QStringList& filename, bool gba) { MessageType msgtype = gba ? msg_InsertGBACart : msg_InsertCart; sendMessage({.type = msgtype, .param = filename}); waitMessage(); return msgResult; } void EmuThread::ejectCart(bool gba) { sendMessage(gba ? msg_EjectGBACart : msg_EjectCart); waitMessage(); } int EmuThread::insertGBAAddon(int type) { sendMessage({.type = msg_InsertGBAAddon, .param = type}); waitMessage(); return msgResult; } int EmuThread::saveState(const QString& filename) { sendMessage({.type = msg_SaveState, .param = filename}); waitMessage(); return msgResult; } int EmuThread::loadState(const QString& filename) { sendMessage({.type = msg_LoadState, .param = filename}); waitMessage(); return msgResult; } int EmuThread::undoStateLoad() { sendMessage(msg_UndoStateLoad); waitMessage(); return msgResult; } int EmuThread::importSavefile(const QString& filename) { sendMessage(msg_EmuReset); sendMessage({.type = msg_ImportSavefile, .param = filename}); waitMessage(2); return msgResult; } void EmuThread::updateRenderer() { if (videoRenderer != lastVideoRenderer) { 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); }