Files
melonDS/src/frontend/qt_sdl/EmuThread.cpp

686 lines
21 KiB
C++

/*
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 <stdlib.h>
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <optional>
#include <vector>
#include <string>
#include <algorithm>
#include <SDL2/SDL.h>
#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<DSi*>(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<DSi*>(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<SoftRenderer>());
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<SoftRenderer&>(emuInstance->nds->GPU.GetRenderer3D()).SetThreaded(
cfg.GetBool("3D.Soft.Threaded"),
emuInstance->nds->GPU);
break;
case renderer3D_OpenGL:
static_cast<GLRenderer&>(emuInstance->nds->GPU.GetRenderer3D()).SetRenderSettings(
cfg.GetBool("3D.GL.BetterPolygons"),
cfg.GetInt("3D.GL.ScaleFactor"));
break;
case renderer3D_OpenGLCompute:
static_cast<ComputeRenderer&>(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);
}