Files
melonDS/src/frontend/qt_sdl/main.cpp
Nadia Holmquist Pedersen 38b0d21c22 Support loading Zstandard-compressed ROMs (#1667)
This is different from the archive support in that the compressed ROMs
are standalone files, rather than archives, making it possible to use
them exactly as if they were regular ROMs, while saving a bunch of space
on disk. This is supported both for DS and GBA ROMs, though given GBA
ROMs' generally small size it's mostly useful for the former.
2023-04-28 19:19:58 +02:00

3681 lines
100 KiB
C++

/*
Copyright 2016-2022 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 <QProcess>
#include <QApplication>
#include <QMessageBox>
#include <QMenuBar>
#include <QMimeDatabase>
#include <QFileDialog>
#include <QInputDialog>
#include <QPaintEvent>
#include <QPainter>
#include <QKeyEvent>
#include <QMimeData>
#include <QVector>
#include <QCommandLineParser>
#ifndef _WIN32
#include <QGuiApplication>
#include <QSocketNotifier>
#include <unistd.h>
#include <sys/socket.h>
#include <signal.h>
#ifndef APPLE
#include <qpa/qplatformnativeinterface.h>
#endif
#endif
#include <SDL2/SDL.h>
#include "OpenGLSupport.h"
#include "duckstation/gl/context.h"
#include "main.h"
#include "Input.h"
#include "CheatsDialog.h"
#include "EmuSettingsDialog.h"
#include "InputConfig/InputConfigDialog.h"
#include "VideoSettingsDialog.h"
#include "CameraSettingsDialog.h"
#include "AudioSettingsDialog.h"
#include "FirmwareSettingsDialog.h"
#include "PathSettingsDialog.h"
#include "MPSettingsDialog.h"
#include "WifiSettingsDialog.h"
#include "InterfaceSettingsDialog.h"
#include "ROMInfoDialog.h"
#include "RAMInfoDialog.h"
#include "TitleManagerDialog.h"
#include "PowerManagement/PowerManagementDialog.h"
#include "types.h"
#include "version.h"
#include "FrontendUtil.h"
#include "OSD.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 "DSi_I2C.h"
#include "Savestate.h"
#include "main_shaders.h"
#include "ROMManager.h"
#include "ArchiveUtil.h"
#include "CameraManager.h"
#include "CLI.h"
// TODO: uniform variable spelling
const QString NdsRomMimeType = "application/x-nintendo-ds-rom";
const QStringList NdsRomExtensions { ".nds", ".srl", ".dsi", ".ids" };
const QString GbaRomMimeType = "application/x-gba-rom";
const QStringList GbaRomExtensions { ".gba", ".agb" };
// This list of supported archive formats is based on libarchive(3) version 3.6.2 (2022-12-09).
const QStringList ArchiveMimeTypes
{
#ifdef ARCHIVE_SUPPORT_ENABLED
"application/zip",
"application/x-7z-compressed",
"application/vnd.rar", // *.rar
"application/x-tar",
"application/x-compressed-tar", // *.tar.gz
"application/x-xz-compressed-tar",
"application/x-bzip-compressed-tar",
"application/x-lz4-compressed-tar",
"application/x-zstd-compressed-tar",
"application/x-tarz", // *.tar.Z
"application/x-lzip-compressed-tar",
"application/x-lzma-compressed-tar",
"application/x-lrzip-compressed-tar",
"application/x-tzo", // *.tar.lzo
#endif
};
const QStringList ArchiveExtensions
{
#ifdef ARCHIVE_SUPPORT_ENABLED
".zip", ".7z", ".rar", ".tar",
".tar.gz", ".tgz",
".tar.xz", ".txz",
".tar.bz2", ".tbz2",
".tar.lz4", ".tlz4",
".tar.zst", ".tzst",
".tar.Z", ".taz",
".tar.lz",
".tar.lzma", ".tlz",
".tar.lrz", ".tlrz",
".tar.lzo", ".tzo"
#endif
};
bool RunningSomething;
MainWindow* mainWindow;
EmuThread* emuThread;
int autoScreenSizing = 0;
int videoRenderer;
GPU::RenderSettings videoSettings;
bool videoSettingsDirty;
SDL_AudioDeviceID audioDevice;
int audioFreq;
bool audioMuted;
SDL_cond* audioSync;
SDL_mutex* audioSyncLock;
SDL_AudioDeviceID micDevice;
s16 micExtBuffer[2048];
u32 micExtBufferWritePos;
u32 micWavLength;
s16* micWavBuffer;
CameraManager* camManager[2];
bool camStarted[2];
const struct { int id; float ratio; const char* label; } aspectRatios[] =
{
{ 0, 1, "4:3 (native)" },
{ 4, (5.f / 3) / (4.f / 3), "5:3 (3DS)"},
{ 1, (16.f / 9) / (4.f / 3), "16:9" },
{ 2, (21.f / 9) / (4.f / 3), "21:9" },
{ 3, 0, "window" }
};
void micCallback(void* data, Uint8* stream, int len);
void audioCallback(void* data, Uint8* stream, int len)
{
len /= (sizeof(s16) * 2);
// resample incoming audio to match the output sample rate
int len_in = Frontend::AudioOut_GetNumSamples(len);
s16 buf_in[1024*2];
int num_in;
SDL_LockMutex(audioSyncLock);
num_in = SPU::ReadOutput(buf_in, len_in);
SDL_CondSignal(audioSync);
SDL_UnlockMutex(audioSyncLock);
if ((num_in < 1) || audioMuted)
{
memset(stream, 0, len*sizeof(s16)*2);
return;
}
int margin = 6;
if (num_in < len_in-margin)
{
int last = num_in-1;
for (int i = num_in; i < len_in-margin; i++)
((u32*)buf_in)[i] = ((u32*)buf_in)[last];
num_in = len_in-margin;
}
Frontend::AudioOut_Resample(buf_in, num_in, (s16*)stream, len, Config::AudioVolume);
}
void audioMute()
{
int inst = Platform::InstanceID();
audioMuted = false;
switch (Config::MPAudioMode)
{
case 1: // only instance 1
if (inst > 0) audioMuted = true;
break;
case 2: // only currently focused instance
if (mainWindow != nullptr)
audioMuted = !mainWindow->isActiveWindow();
break;
}
}
void micOpen()
{
if (Config::MicInputType != 1)
{
micDevice = 0;
return;
}
SDL_AudioSpec whatIwant, whatIget;
memset(&whatIwant, 0, sizeof(SDL_AudioSpec));
whatIwant.freq = 44100;
whatIwant.format = AUDIO_S16LSB;
whatIwant.channels = 1;
whatIwant.samples = 1024;
whatIwant.callback = micCallback;
micDevice = SDL_OpenAudioDevice(NULL, 1, &whatIwant, &whatIget, 0);
if (!micDevice)
{
printf("Mic init failed: %s\n", SDL_GetError());
}
else
{
SDL_PauseAudioDevice(micDevice, 0);
}
}
void micClose()
{
if (micDevice)
SDL_CloseAudioDevice(micDevice);
micDevice = 0;
}
void micLoadWav(const std::string& name)
{
SDL_AudioSpec format;
memset(&format, 0, sizeof(SDL_AudioSpec));
if (micWavBuffer) delete[] micWavBuffer;
micWavBuffer = nullptr;
micWavLength = 0;
u8* buf;
u32 len;
if (!SDL_LoadWAV(name.c_str(), &format, &buf, &len))
return;
const u64 dstfreq = 44100;
int srcinc = format.channels;
len /= ((SDL_AUDIO_BITSIZE(format.format) / 8) * srcinc);
micWavLength = (len * dstfreq) / format.freq;
if (micWavLength < 735) micWavLength = 735;
micWavBuffer = new s16[micWavLength];
float res_incr = len / (float)micWavLength;
float res_timer = 0;
int res_pos = 0;
for (int i = 0; i < micWavLength; i++)
{
u16 val = 0;
switch (SDL_AUDIO_BITSIZE(format.format))
{
case 8:
val = buf[res_pos] << 8;
break;
case 16:
if (SDL_AUDIO_ISBIGENDIAN(format.format))
val = (buf[res_pos*2] << 8) | buf[res_pos*2 + 1];
else
val = (buf[res_pos*2 + 1] << 8) | buf[res_pos*2];
break;
case 32:
if (SDL_AUDIO_ISFLOAT(format.format))
{
u32 rawval;
if (SDL_AUDIO_ISBIGENDIAN(format.format))
rawval = (buf[res_pos*4] << 24) | (buf[res_pos*4 + 1] << 16) | (buf[res_pos*4 + 2] << 8) | buf[res_pos*4 + 3];
else
rawval = (buf[res_pos*4 + 3] << 24) | (buf[res_pos*4 + 2] << 16) | (buf[res_pos*4 + 1] << 8) | buf[res_pos*4];
float fval = *(float*)&rawval;
s32 ival = (s32)(fval * 0x8000);
ival = std::clamp(ival, -0x8000, 0x7FFF);
val = (s16)ival;
}
else if (SDL_AUDIO_ISBIGENDIAN(format.format))
val = (buf[res_pos*4] << 8) | buf[res_pos*4 + 1];
else
val = (buf[res_pos*4 + 3] << 8) | buf[res_pos*4 + 2];
break;
}
if (SDL_AUDIO_ISUNSIGNED(format.format))
val ^= 0x8000;
micWavBuffer[i] = val;
res_timer += res_incr;
while (res_timer >= 1.0)
{
res_timer -= 1.0;
res_pos += srcinc;
}
}
SDL_FreeWAV(buf);
}
void micCallback(void* data, Uint8* stream, int len)
{
s16* input = (s16*)stream;
len /= sizeof(s16);
int maxlen = sizeof(micExtBuffer) / sizeof(s16);
if ((micExtBufferWritePos + len) > maxlen)
{
u32 len1 = maxlen - micExtBufferWritePos;
memcpy(&micExtBuffer[micExtBufferWritePos], &input[0], len1*sizeof(s16));
memcpy(&micExtBuffer[0], &input[len1], (len - len1)*sizeof(s16));
micExtBufferWritePos = len - len1;
}
else
{
memcpy(&micExtBuffer[micExtBufferWritePos], input, len*sizeof(s16));
micExtBufferWritePos += len;
}
}
void micProcess()
{
int type = Config::MicInputType;
bool cmd = Input::HotkeyDown(HK_Mic);
if (type != 1 && !cmd)
{
type = 0;
}
switch (type)
{
case 0: // no mic
Frontend::Mic_FeedSilence();
break;
case 1: // host mic
case 3: // WAV
Frontend::Mic_FeedExternalBuffer();
break;
case 2: // white noise
Frontend::Mic_FeedNoise();
break;
}
}
EmuThread::EmuThread(QObject* parent) : QThread(parent)
{
EmuStatus = 0;
EmuRunning = 2;
EmuPause = 0;
RunningSomething = false;
connect(this, SIGNAL(windowUpdate()), mainWindow->panelWidget, SLOT(repaint()));
connect(this, SIGNAL(windowTitleChange(QString)), mainWindow, SLOT(onTitleUpdate(QString)));
connect(this, SIGNAL(windowEmuStart()), mainWindow, SLOT(onEmuStart()));
connect(this, SIGNAL(windowEmuStop()), mainWindow, SLOT(onEmuStop()));
connect(this, SIGNAL(windowEmuPause()), mainWindow->actPause, SLOT(trigger()));
connect(this, SIGNAL(windowEmuReset()), mainWindow->actReset, SLOT(trigger()));
connect(this, SIGNAL(windowEmuFrameStep()), mainWindow->actFrameStep, SLOT(trigger()));
connect(this, SIGNAL(windowLimitFPSChange()), mainWindow->actLimitFramerate, SLOT(trigger()));
connect(this, SIGNAL(screenLayoutChange()), mainWindow->panelWidget, SLOT(onScreenLayoutChanged()));
connect(this, SIGNAL(windowFullscreenToggle()), mainWindow, SLOT(onFullscreenToggled()));
connect(this, SIGNAL(swapScreensToggle()), mainWindow->actScreenSwap, SLOT(trigger()));
connect(this, SIGNAL(screenEmphasisToggle()), mainWindow, SLOT(onScreenEmphasisToggled()));
static_cast<ScreenPanelGL*>(mainWindow->panel)->transferLayout(this);
}
void EmuThread::updateScreenSettings(bool filter, const WindowInfo& windowInfo, int numScreens, int* screenKind, float* screenMatrix)
{
screenSettingsLock.lock();
if (lastScreenWidth != windowInfo.surface_width || lastScreenHeight != windowInfo.surface_height)
{
if (oglContext)
oglContext->ResizeSurface(windowInfo.surface_width, windowInfo.surface_height);
lastScreenWidth = windowInfo.surface_width;
lastScreenHeight = windowInfo.surface_height;
}
this->filter = filter;
this->windowInfo = windowInfo;
this->numScreens = numScreens;
memcpy(this->screenKind, screenKind, sizeof(int)*numScreens);
memcpy(this->screenMatrix, screenMatrix, sizeof(float)*numScreens*6);
screenSettingsLock.unlock();
}
void EmuThread::initOpenGL()
{
GL::Context* windowctx = mainWindow->getOGLContext();
oglContext = windowctx;
oglContext->MakeCurrent();
OpenGL::BuildShaderProgram(kScreenVS, kScreenFS, screenShaderProgram, "ScreenShader");
GLuint pid = screenShaderProgram[2];
glBindAttribLocation(pid, 0, "vPosition");
glBindAttribLocation(pid, 1, "vTexcoord");
glBindFragDataLocation(pid, 0, "oColor");
OpenGL::LinkShaderProgram(screenShaderProgram);
glUseProgram(pid);
glUniform1i(glGetUniformLocation(pid, "ScreenTex"), 0);
screenShaderScreenSizeULoc = glGetUniformLocation(pid, "uScreenSize");
screenShaderTransformULoc = glGetUniformLocation(pid, "uTransform");
// to prevent bleeding between both parts of the screen
// with bilinear filtering enabled
const int paddedHeight = 192*2+2;
const float padPixels = 1.f / paddedHeight;
const float vertices[] =
{
0.f, 0.f, 0.f, 0.f,
0.f, 192.f, 0.f, 0.5f - padPixels,
256.f, 192.f, 1.f, 0.5f - padPixels,
0.f, 0.f, 0.f, 0.f,
256.f, 192.f, 1.f, 0.5f - padPixels,
256.f, 0.f, 1.f, 0.f,
0.f, 0.f, 0.f, 0.5f + padPixels,
0.f, 192.f, 0.f, 1.f,
256.f, 192.f, 1.f, 1.f,
0.f, 0.f, 0.f, 0.5f + padPixels,
256.f, 192.f, 1.f, 1.f,
256.f, 0.f, 1.f, 0.5f + padPixels
};
glGenBuffers(1, &screenVertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, screenVertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glGenVertexArrays(1, &screenVertexArray);
glBindVertexArray(screenVertexArray);
glEnableVertexAttribArray(0); // position
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*4, (void*)(0));
glEnableVertexAttribArray(1); // texcoord
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4*4, (void*)(2*4));
glGenTextures(1, &screenTexture);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, screenTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, paddedHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
// fill the padding
u8 zeroData[256*4*4];
memset(zeroData, 0, sizeof(zeroData));
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 192, 256, 2, GL_RGBA, GL_UNSIGNED_BYTE, zeroData);
OSD::Init(true);
oglContext->SetSwapInterval(Config::ScreenVSync ? Config::ScreenVSyncInterval : 0);
}
void EmuThread::deinitOpenGL()
{
glDeleteTextures(1, &screenTexture);
glDeleteVertexArrays(1, &screenVertexArray);
glDeleteBuffers(1, &screenVertexBuffer);
OpenGL::DeleteShaderProgram(screenShaderProgram);
OSD::DeInit();
oglContext->DoneCurrent();
oglContext = nullptr;
lastScreenWidth = lastScreenHeight = -1;
}
void EmuThread::run()
{
u32 mainScreenPos[3];
NDS::Init();
mainScreenPos[0] = 0;
mainScreenPos[1] = 0;
mainScreenPos[2] = 0;
autoScreenSizing = 0;
videoSettingsDirty = false;
videoSettings.Soft_Threaded = Config::Threaded3D != 0;
videoSettings.GL_ScaleFactor = Config::GL_ScaleFactor;
videoSettings.GL_BetterPolygons = Config::GL_BetterPolygons;
if (mainWindow->hasOGL)
{
initOpenGL();
videoRenderer = Config::_3DRenderer;
}
else
{
videoRenderer = 0;
}
GPU::InitRenderer(videoRenderer);
GPU::SetRenderSettings(videoRenderer, videoSettings);
SPU::SetInterpolation(Config::AudioInterp);
Input::Init();
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];
while (EmuRunning != 0)
{
Input::Process();
if (Input::HotkeyPressed(HK_FastForwardToggle)) emit windowLimitFPSChange();
if (Input::HotkeyPressed(HK_Pause)) emit windowEmuPause();
if (Input::HotkeyPressed(HK_Reset)) emit windowEmuReset();
if (Input::HotkeyPressed(HK_FrameStep)) emit windowEmuFrameStep();
if (Input::HotkeyPressed(HK_FullscreenToggle)) emit windowFullscreenToggle();
if (Input::HotkeyPressed(HK_SwapScreens)) emit swapScreensToggle();
if (Input::HotkeyPressed(HK_SwapScreenEmphasis)) emit screenEmphasisToggle();
if (Input::HotkeyPressed(HK_SolarSensorDecrease))
{
int level = GBACart::SetInput(GBACart::Input_SolarSensorDown, true);
if (level != -1)
{
char msg[64];
sprintf(msg, "Solar sensor level: %d", level);
OSD::AddMessage(0, msg);
}
}
if (Input::HotkeyPressed(HK_SolarSensorIncrease))
{
int level = GBACart::SetInput(GBACart::Input_SolarSensorUp, true);
if (level != -1)
{
char msg[64];
sprintf(msg, "Solar sensor level: %d", level);
OSD::AddMessage(0, msg);
}
}
if (NDS::ConsoleType == 1)
{
double currentTime = SDL_GetPerformanceCounter() * perfCountsSec;
// Handle power button
if (Input::HotkeyDown(HK_PowerButton))
{
DSi_BPTWL::SetPowerButtonHeld(currentTime);
}
else if (Input::HotkeyReleased(HK_PowerButton))
{
DSi_BPTWL::SetPowerButtonReleased(currentTime);
}
// Handle volume buttons
if (Input::HotkeyDown(HK_VolumeUp))
{
DSi_BPTWL::SetVolumeSwitchHeld(DSi_BPTWL::volumeKey_Up);
}
else if (Input::HotkeyReleased(HK_VolumeUp))
{
DSi_BPTWL::SetVolumeSwitchReleased(DSi_BPTWL::volumeKey_Up);
}
if (Input::HotkeyDown(HK_VolumeDown))
{
DSi_BPTWL::SetVolumeSwitchHeld(DSi_BPTWL::volumeKey_Down);
}
else if (Input::HotkeyReleased(HK_VolumeDown))
{
DSi_BPTWL::SetVolumeSwitchReleased(DSi_BPTWL::volumeKey_Down);
}
DSi_BPTWL::ProcessVolumeSwitchInput(currentTime);
}
if (EmuRunning == 1 || EmuRunning == 3)
{
EmuStatus = 1;
if (EmuRunning == 3) EmuRunning = 2;
// update render settings if needed
// HACK:
// once the fast forward hotkey is released, we need to update vsync
// to the old setting again
if (videoSettingsDirty || Input::HotkeyReleased(HK_FastForward))
{
if (oglContext)
{
oglContext->SetSwapInterval(Config::ScreenVSync ? Config::ScreenVSyncInterval : 0);
videoRenderer = Config::_3DRenderer;
}
#ifdef OGLRENDERER_ENABLED
else
#endif
{
videoRenderer = 0;
}
videoRenderer = oglContext ? Config::_3DRenderer : 0;
videoSettingsDirty = false;
videoSettings.Soft_Threaded = Config::Threaded3D != 0;
videoSettings.GL_ScaleFactor = Config::GL_ScaleFactor;
videoSettings.GL_BetterPolygons = Config::GL_BetterPolygons;
GPU::SetRenderSettings(videoRenderer, videoSettings);
}
// process input and hotkeys
NDS::SetKeyMask(Input::InputMask);
if (Input::HotkeyPressed(HK_Lid))
{
bool lid = !NDS::IsLidClosed();
NDS::SetLidClosed(lid);
OSD::AddMessage(0, lid ? "Lid closed" : "Lid opened");
}
// microphone input
micProcess();
// auto screen layout
if (Config::ScreenSizing == screenSizing_Auto)
{
mainScreenPos[2] = mainScreenPos[1];
mainScreenPos[1] = mainScreenPos[0];
mainScreenPos[0] = 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 screenLayoutChange();
}
}
// emulate
u32 nlines = NDS::RunFrame();
if (ROMManager::NDSSave)
ROMManager::NDSSave->CheckFlush();
if (ROMManager::GBASave)
ROMManager::GBASave->CheckFlush();
if (!oglContext)
{
FrontBufferLock.lock();
FrontBuffer = GPU::FrontBuffer;
FrontBufferLock.unlock();
}
else
{
FrontBuffer = GPU::FrontBuffer;
drawScreenGL();
}
#ifdef MELONCAP
MelonCap::Update();
#endif // MELONCAP
if (EmuRunning == 0) break;
winUpdateCount++;
if (winUpdateCount >= winUpdateFreq && !oglContext)
{
emit windowUpdate();
winUpdateCount = 0;
}
bool fastforward = Input::HotkeyDown(HK_FastForward);
if (fastforward && oglContext && Config::ScreenVSync)
{
oglContext->SetSwapInterval(0);
}
if (Config::DSiVolumeSync && NDS::ConsoleType == 1)
{
u8 volumeLevel = DSi_BPTWL::GetVolumeLevel();
if (volumeLevel != dsiVolumeLevel)
{
dsiVolumeLevel = volumeLevel;
emit syncVolumeLevel();
}
Config::AudioVolume = volumeLevel * (256.0 / 31.0);
}
if (Config::AudioSync && !fastforward && audioDevice)
{
SDL_LockMutex(audioSyncLock);
while (SPU::GetOutputSize() > 1024)
{
int ret = SDL_CondWaitTimeout(audioSync, audioSyncLock, 500);
if (ret == SDL_MUTEX_TIMEDOUT) break;
}
SDL_UnlockMutex(audioSyncLock);
}
double frametimeStep = nlines / (60.0 * 263.0);
{
bool limitfps = Config::LimitFPS && !fastforward;
double practicalFramelimit = limitfps ? frametimeStep : 1.0 / 1000.0;
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 = Platform::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 = Platform::InstanceID();
if (inst == 0)
sprintf(melontitle, "melonDS " MELONDS_VERSION);
else
sprintf(melontitle, "melonDS (%d)", inst+1);
changeWindowTitle(melontitle);
SDL_Delay(75);
if (oglContext)
drawScreenGL();
int contextRequest = ContextRequest;
if (contextRequest == 1)
{
initOpenGL();
ContextRequest = 0;
}
else if (contextRequest == 2)
{
deinitOpenGL();
ContextRequest = 0;
}
}
}
EmuStatus = 0;
GPU::DeInitRenderer();
NDS::DeInit();
//Platform::LAN_DeInit();
}
void EmuThread::changeWindowTitle(char* title)
{
emit windowTitleChange(QString(title));
}
void EmuThread::emuRun()
{
EmuRunning = 1;
EmuPause = 0;
RunningSomething = true;
// checkme
emit windowEmuStart();
if (audioDevice) SDL_PauseAudioDevice(audioDevice, 0);
micOpen();
}
void EmuThread::initContext()
{
ContextRequest = 1;
while (ContextRequest != 0);
}
void EmuThread::deinitContext()
{
ContextRequest = 2;
while (ContextRequest != 0);
}
void EmuThread::emuPause()
{
EmuPause++;
if (EmuPause > 1) return;
PrevEmuStatus = EmuRunning;
EmuRunning = 2;
while (EmuStatus != 2);
if (audioDevice) SDL_PauseAudioDevice(audioDevice, 1);
micClose();
}
void EmuThread::emuUnpause()
{
if (EmuPause < 1) return;
EmuPause--;
if (EmuPause > 0) return;
EmuRunning = PrevEmuStatus;
if (audioDevice) SDL_PauseAudioDevice(audioDevice, 0);
micOpen();
}
void EmuThread::emuStop()
{
EmuRunning = 0;
EmuPause = 0;
if (audioDevice) SDL_PauseAudioDevice(audioDevice, 1);
micClose();
}
void EmuThread::emuFrameStep()
{
if (EmuPause < 1) emit windowEmuPause();
EmuRunning = 3;
}
bool EmuThread::emuIsRunning()
{
return (EmuRunning == 1);
}
bool EmuThread::emuIsActive()
{
return (RunningSomething == 1);
}
void EmuThread::drawScreenGL()
{
int w = windowInfo.surface_width;
int h = windowInfo.surface_height;
float factor = windowInfo.surface_scale;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDisable(GL_DEPTH_TEST);
glDepthMask(false);
glDisable(GL_BLEND);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_STENCIL_TEST);
glClear(GL_COLOR_BUFFER_BIT);
glViewport(0, 0, w, h);
glUseProgram(screenShaderProgram[2]);
glUniform2f(screenShaderScreenSizeULoc, w / factor, h / factor);
int frontbuf = FrontBuffer;
glActiveTexture(GL_TEXTURE0);
#ifdef OGLRENDERER_ENABLED
if (GPU::Renderer != 0)
{
// hardware-accelerated render
GPU::CurGLCompositor->BindOutputTexture(frontbuf);
}
else
#endif
{
// regular render
glBindTexture(GL_TEXTURE_2D, screenTexture);
if (GPU::Framebuffer[frontbuf][0] && GPU::Framebuffer[frontbuf][1])
{
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 256, 192, GL_RGBA,
GL_UNSIGNED_BYTE, GPU::Framebuffer[frontbuf][0]);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 192+2, 256, 192, GL_RGBA,
GL_UNSIGNED_BYTE, GPU::Framebuffer[frontbuf][1]);
}
}
screenSettingsLock.lock();
GLint filter = this->filter ? GL_LINEAR : GL_NEAREST;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
glBindBuffer(GL_ARRAY_BUFFER, screenVertexBuffer);
glBindVertexArray(screenVertexArray);
for (int i = 0; i < numScreens; i++)
{
glUniformMatrix2x3fv(screenShaderTransformULoc, 1, GL_TRUE, screenMatrix[i]);
glDrawArrays(GL_TRIANGLES, screenKind[i] == 0 ? 0 : 2*3, 2*3);
}
screenSettingsLock.unlock();
OSD::Update();
OSD::DrawGL(w, h);
oglContext->SwapBuffers();
}
ScreenHandler::ScreenHandler(QWidget* widget)
{
widget->setMouseTracking(true);
widget->setAttribute(Qt::WA_AcceptTouchEvents);
QTimer* mouseTimer = setupMouseTimer();
widget->connect(mouseTimer, &QTimer::timeout, [=] { if (Config::MouseHide) widget->setCursor(Qt::BlankCursor);});
}
ScreenHandler::~ScreenHandler()
{
mouseTimer->stop();
}
void ScreenHandler::screenSetupLayout(int w, int h)
{
int sizing = Config::ScreenSizing;
if (sizing == 3) sizing = autoScreenSizing;
float aspectTop, aspectBot;
for (auto ratio : aspectRatios)
{
if (ratio.id == Config::ScreenAspectTop)
aspectTop = ratio.ratio;
if (ratio.id == Config::ScreenAspectBot)
aspectBot = ratio.ratio;
}
if (aspectTop == 0)
aspectTop = ((float) w / h) / (4.f / 3.f);
if (aspectBot == 0)
aspectBot = ((float) w / h) / (4.f / 3.f);
Frontend::SetupScreenLayout(w, h,
Config::ScreenLayout,
Config::ScreenRotation,
sizing,
Config::ScreenGap,
Config::IntegerScaling != 0,
Config::ScreenSwap != 0,
aspectTop,
aspectBot);
numScreens = Frontend::GetScreenTransforms(screenMatrix[0], screenKind);
}
QSize ScreenHandler::screenGetMinSize(int factor = 1)
{
bool isHori = (Config::ScreenRotation == 1 || Config::ScreenRotation == 3);
int gap = Config::ScreenGap * factor;
int w = 256 * factor;
int h = 192 * factor;
if (Config::ScreenSizing == 4 || Config::ScreenSizing == 5)
{
return QSize(w, h);
}
if (Config::ScreenLayout == 0) // natural
{
if (isHori)
return QSize(h+gap+h, w);
else
return QSize(w, h+gap+h);
}
else if (Config::ScreenLayout == 1) // vertical
{
if (isHori)
return QSize(h, w+gap+w);
else
return QSize(w, h+gap+h);
}
else if (Config::ScreenLayout == 2) // horizontal
{
if (isHori)
return QSize(h+gap+h, w);
else
return QSize(w+gap+w, h);
}
else // hybrid
{
if (isHori)
return QSize(h+gap+h, 3*w + (int)ceil((4*gap) / 3.0));
else
return QSize(3*w + (int)ceil((4*gap) / 3.0), h+gap+h);
}
}
void ScreenHandler::screenOnMousePress(QMouseEvent* event)
{
event->accept();
if (event->button() != Qt::LeftButton) return;
int x = event->pos().x();
int y = event->pos().y();
if (Frontend::GetTouchCoords(x, y, false))
{
touching = true;
NDS::TouchScreen(x, y);
}
}
void ScreenHandler::screenOnMouseRelease(QMouseEvent* event)
{
event->accept();
if (event->button() != Qt::LeftButton) return;
if (touching)
{
touching = false;
NDS::ReleaseScreen();
}
}
void ScreenHandler::screenOnMouseMove(QMouseEvent* event)
{
event->accept();
showCursor();
if (!(event->buttons() & Qt::LeftButton)) return;
if (!touching) return;
int x = event->pos().x();
int y = event->pos().y();
if (Frontend::GetTouchCoords(x, y, true))
NDS::TouchScreen(x, y);
}
void ScreenHandler::screenHandleTablet(QTabletEvent* event)
{
event->accept();
switch(event->type())
{
case QEvent::TabletPress:
case QEvent::TabletMove:
{
int x = event->x();
int y = event->y();
if (Frontend::GetTouchCoords(x, y, event->type()==QEvent::TabletMove))
{
touching = true;
NDS::TouchScreen(x, y);
}
}
break;
case QEvent::TabletRelease:
if (touching)
{
NDS::ReleaseScreen();
touching = false;
}
break;
}
}
void ScreenHandler::screenHandleTouch(QTouchEvent* event)
{
event->accept();
switch(event->type())
{
case QEvent::TouchBegin:
case QEvent::TouchUpdate:
if (event->touchPoints().length() > 0)
{
QPointF lastPosition = event->touchPoints().first().lastPos();
int x = (int)lastPosition.x();
int y = (int)lastPosition.y();
if (Frontend::GetTouchCoords(x, y, event->type()==QEvent::TouchUpdate))
{
touching = true;
NDS::TouchScreen(x, y);
}
}
break;
case QEvent::TouchEnd:
if (touching)
{
NDS::ReleaseScreen();
touching = false;
}
break;
}
}
void ScreenHandler::showCursor()
{
mainWindow->panelWidget->setCursor(Qt::ArrowCursor);
mouseTimer->start();
}
QTimer* ScreenHandler::setupMouseTimer()
{
mouseTimer = new QTimer();
mouseTimer->setSingleShot(true);
mouseTimer->setInterval(Config::MouseHideSeconds*1000);
mouseTimer->start();
return mouseTimer;
}
ScreenPanelNative::ScreenPanelNative(QWidget* parent) : QWidget(parent), ScreenHandler(this)
{
screen[0] = QImage(256, 192, QImage::Format_RGB32);
screen[1] = QImage(256, 192, QImage::Format_RGB32);
screenTrans[0].reset();
screenTrans[1].reset();
OSD::Init(false);
}
ScreenPanelNative::~ScreenPanelNative()
{
OSD::DeInit();
}
void ScreenPanelNative::setupScreenLayout()
{
int w = width();
int h = height();
screenSetupLayout(w, h);
for (int i = 0; i < numScreens; i++)
{
float* mtx = screenMatrix[i];
screenTrans[i].setMatrix(mtx[0], mtx[1], 0.f,
mtx[2], mtx[3], 0.f,
mtx[4], mtx[5], 1.f);
}
}
void ScreenPanelNative::paintEvent(QPaintEvent* event)
{
QPainter painter(this);
// fill background
painter.fillRect(event->rect(), QColor::fromRgb(0, 0, 0));
if (emuThread->emuIsActive())
{
emuThread->FrontBufferLock.lock();
int frontbuf = emuThread->FrontBuffer;
if (!GPU::Framebuffer[frontbuf][0] || !GPU::Framebuffer[frontbuf][1])
{
emuThread->FrontBufferLock.unlock();
return;
}
memcpy(screen[0].scanLine(0), GPU::Framebuffer[frontbuf][0], 256 * 192 * 4);
memcpy(screen[1].scanLine(0), GPU::Framebuffer[frontbuf][1], 256 * 192 * 4);
emuThread->FrontBufferLock.unlock();
painter.setRenderHint(QPainter::SmoothPixmapTransform, Config::ScreenFilter != 0);
QRect screenrc(0, 0, 256, 192);
for (int i = 0; i < numScreens; i++)
{
painter.setTransform(screenTrans[i]);
painter.drawImage(screenrc, screen[screenKind[i]]);
}
}
OSD::Update();
OSD::DrawNative(painter);
}
void ScreenPanelNative::resizeEvent(QResizeEvent* event)
{
setupScreenLayout();
}
void ScreenPanelNative::mousePressEvent(QMouseEvent* event)
{
screenOnMousePress(event);
}
void ScreenPanelNative::mouseReleaseEvent(QMouseEvent* event)
{
screenOnMouseRelease(event);
}
void ScreenPanelNative::mouseMoveEvent(QMouseEvent* event)
{
screenOnMouseMove(event);
}
void ScreenPanelNative::tabletEvent(QTabletEvent* event)
{
screenHandleTablet(event);
}
bool ScreenPanelNative::event(QEvent* event)
{
if (event->type() == QEvent::TouchBegin
|| event->type() == QEvent::TouchEnd
|| event->type() == QEvent::TouchUpdate)
{
screenHandleTouch((QTouchEvent*)event);
return true;
}
return QWidget::event(event);
}
void ScreenPanelNative::onScreenLayoutChanged()
{
setMinimumSize(screenGetMinSize());
setupScreenLayout();
}
ScreenPanelGL::ScreenPanelGL(QWidget* parent) : QWidget(parent), ScreenHandler(this)
{
setAutoFillBackground(false);
setAttribute(Qt::WA_NativeWindow, true);
setAttribute(Qt::WA_NoSystemBackground, true);
setAttribute(Qt::WA_PaintOnScreen, true);
setAttribute(Qt::WA_KeyCompression, false);
setFocusPolicy(Qt::StrongFocus);
setMinimumSize(screenGetMinSize());
}
ScreenPanelGL::~ScreenPanelGL()
{}
bool ScreenPanelGL::createContext()
{
std::optional<WindowInfo> windowInfo = getWindowInfo();
std::array<GL::Context::Version, 2> versionsToTry = {
GL::Context::Version{GL::Context::Profile::Core, 4, 3},
GL::Context::Version{GL::Context::Profile::Core, 3, 2}};
if (windowInfo.has_value())
{
glContext = GL::Context::Create(*getWindowInfo(), versionsToTry);
glContext->DoneCurrent();
}
return glContext != nullptr;
}
qreal ScreenPanelGL::devicePixelRatioFromScreen() const
{
const QScreen* screen_for_ratio = window()->windowHandle()->screen();
if (!screen_for_ratio)
screen_for_ratio = QGuiApplication::primaryScreen();
return screen_for_ratio ? screen_for_ratio->devicePixelRatio() : static_cast<qreal>(1);
}
int ScreenPanelGL::scaledWindowWidth() const
{
return std::max(static_cast<int>(std::ceil(static_cast<qreal>(width()) * devicePixelRatioFromScreen())), 1);
}
int ScreenPanelGL::scaledWindowHeight() const
{
return std::max(static_cast<int>(std::ceil(static_cast<qreal>(height()) * devicePixelRatioFromScreen())), 1);
}
std::optional<WindowInfo> ScreenPanelGL::getWindowInfo()
{
WindowInfo wi;
// Windows and Apple are easy here since there's no display connection.
#if defined(_WIN32)
wi.type = WindowInfo::Type::Win32;
wi.window_handle = reinterpret_cast<void*>(winId());
#elif defined(__APPLE__)
wi.type = WindowInfo::Type::MacOS;
wi.window_handle = reinterpret_cast<void*>(winId());
#else
QPlatformNativeInterface* pni = QGuiApplication::platformNativeInterface();
const QString platform_name = QGuiApplication::platformName();
if (platform_name == QStringLiteral("xcb"))
{
wi.type = WindowInfo::Type::X11;
wi.display_connection = pni->nativeResourceForWindow("display", windowHandle());
wi.window_handle = reinterpret_cast<void*>(winId());
}
else if (platform_name == QStringLiteral("wayland"))
{
wi.type = WindowInfo::Type::Wayland;
QWindow* handle = windowHandle();
if (handle == nullptr)
return std::nullopt;
wi.display_connection = pni->nativeResourceForWindow("display", handle);
wi.window_handle = pni->nativeResourceForWindow("surface", handle);
}
else
{
qCritical() << "Unknown PNI platform " << platform_name;
return std::nullopt;
}
#endif
wi.surface_width = static_cast<u32>(scaledWindowWidth());
wi.surface_height = static_cast<u32>(scaledWindowHeight());
wi.surface_scale = static_cast<float>(devicePixelRatioFromScreen());
return wi;
}
QPaintEngine* ScreenPanelGL::paintEngine() const
{
return nullptr;
}
void ScreenPanelGL::setupScreenLayout()
{
int w = width();
int h = height();
screenSetupLayout(w, h);
if (emuThread)
transferLayout(emuThread);
}
void ScreenPanelGL::resizeEvent(QResizeEvent* event)
{
setupScreenLayout();
QWidget::resizeEvent(event);
}
void ScreenPanelGL::mousePressEvent(QMouseEvent* event)
{
screenOnMousePress(event);
}
void ScreenPanelGL::mouseReleaseEvent(QMouseEvent* event)
{
screenOnMouseRelease(event);
}
void ScreenPanelGL::mouseMoveEvent(QMouseEvent* event)
{
screenOnMouseMove(event);
}
void ScreenPanelGL::tabletEvent(QTabletEvent* event)
{
screenHandleTablet(event);
}
bool ScreenPanelGL::event(QEvent* event)
{
if (event->type() == QEvent::TouchBegin
|| event->type() == QEvent::TouchEnd
|| event->type() == QEvent::TouchUpdate)
{
screenHandleTouch((QTouchEvent*)event);
return true;
}
return QWidget::event(event);
}
void ScreenPanelGL::transferLayout(EmuThread* thread)
{
std::optional<WindowInfo> windowInfo = getWindowInfo();
if (windowInfo.has_value())
thread->updateScreenSettings(Config::ScreenFilter, *windowInfo, numScreens, screenKind, &screenMatrix[0][0]);
}
void ScreenPanelGL::onScreenLayoutChanged()
{
setMinimumSize(screenGetMinSize());
setupScreenLayout();
}
static bool FileExtensionInList(const QString& filename, const QStringList& extensions, Qt::CaseSensitivity cs = Qt::CaseInsensitive)
{
return std::any_of(extensions.cbegin(), extensions.cend(), [&](const auto& ext) {
return filename.endsWith(ext, cs);
});
}
static bool MimeTypeInList(const QMimeType& mimetype, const QStringList& superTypeNames)
{
return std::any_of(superTypeNames.cbegin(), superTypeNames.cend(), [&](const auto& superTypeName) {
return mimetype.inherits(superTypeName);
});
}
static bool NdsRomByExtension(const QString& filename)
{
return FileExtensionInList(filename, NdsRomExtensions);
}
static bool GbaRomByExtension(const QString& filename)
{
return FileExtensionInList(filename, GbaRomExtensions);
}
static bool SupportedArchiveByExtension(const QString& filename)
{
return FileExtensionInList(filename, ArchiveExtensions);
}
static bool NdsRomByMimetype(const QMimeType& mimetype)
{
return mimetype.inherits(NdsRomMimeType);
}
static bool GbaRomByMimetype(const QMimeType& mimetype)
{
return mimetype.inherits(GbaRomMimeType);
}
static bool SupportedArchiveByMimetype(const QMimeType& mimetype)
{
return MimeTypeInList(mimetype, ArchiveMimeTypes);
}
static bool ZstdNdsRomByExtension(const QString& filename)
{
if (filename.endsWith(".zst", Qt::CaseInsensitive))
return NdsRomByExtension(filename.left(filename.size() - 4));
}
static bool ZstdGbaRomByExtension(const QString& filename)
{
if (filename.endsWith(".zst", Qt::CaseInsensitive))
return GbaRomByExtension(filename.left(filename.size() - 4));
}
static bool FileIsSupportedFiletype(const QString& filename, bool insideArchive = false)
{
if (ZstdNdsRomByExtension(filename) || ZstdGbaRomByExtension(filename))
return true;
if (NdsRomByExtension(filename) || GbaRomByExtension(filename) || SupportedArchiveByExtension(filename))
return true;
const auto matchmode = insideArchive ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault;
const QMimeType mimetype = QMimeDatabase().mimeTypeForFile(filename, matchmode);
return NdsRomByMimetype(mimetype) || GbaRomByMimetype(mimetype) || SupportedArchiveByMimetype(mimetype);
}
#ifndef _WIN32
static int signalFd[2];
QSocketNotifier *signalSn;
static void signalHandler(int)
{
char a = 1;
write(signalFd[0], &a, sizeof(a));
}
#endif
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent)
{
#ifndef _WIN32
if (socketpair(AF_UNIX, SOCK_STREAM, 0, signalFd))
{
qFatal("Couldn't create socketpair");
}
signalSn = new QSocketNotifier(signalFd[1], QSocketNotifier::Read, this);
connect(signalSn, SIGNAL(activated(int)), this, SLOT(onQuit()));
struct sigaction sa;
sa.sa_handler = signalHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_flags |= SA_RESTART;
sigaction(SIGINT, &sa, 0);
#endif
oldW = Config::WindowWidth;
oldH = Config::WindowHeight;
oldMax = Config::WindowMaximized;
setWindowTitle("melonDS " MELONDS_VERSION);
setAttribute(Qt::WA_DeleteOnClose);
setAcceptDrops(true);
setFocusPolicy(Qt::ClickFocus);
int inst = Platform::InstanceID();
QMenuBar* menubar = new QMenuBar();
{
QMenu* menu = menubar->addMenu("File");
actOpenROM = menu->addAction("Open ROM...");
connect(actOpenROM, &QAction::triggered, this, &MainWindow::onOpenFile);
actOpenROM->setShortcut(QKeySequence(QKeySequence::StandardKey::Open));
/*actOpenROMArchive = menu->addAction("Open ROM inside archive...");
connect(actOpenROMArchive, &QAction::triggered, this, &MainWindow::onOpenFileArchive);
actOpenROMArchive->setShortcut(QKeySequence(Qt::Key_O | Qt::CTRL | Qt::SHIFT));*/
recentMenu = menu->addMenu("Open recent");
for (int i = 0; i < 10; ++i)
{
std::string item = Config::RecentROMList[i];
if (!item.empty())
recentFileList.push_back(QString::fromStdString(item));
}
updateRecentFilesMenu();
//actBootFirmware = menu->addAction("Launch DS menu");
actBootFirmware = menu->addAction("Boot firmware");
connect(actBootFirmware, &QAction::triggered, this, &MainWindow::onBootFirmware);
menu->addSeparator();
actCurrentCart = menu->addAction("DS slot: " + ROMManager::CartLabel());
actCurrentCart->setEnabled(false);
actInsertCart = menu->addAction("Insert cart...");
connect(actInsertCart, &QAction::triggered, this, &MainWindow::onInsertCart);
actEjectCart = menu->addAction("Eject cart");
connect(actEjectCart, &QAction::triggered, this, &MainWindow::onEjectCart);
menu->addSeparator();
actCurrentGBACart = menu->addAction("GBA slot: " + ROMManager::GBACartLabel());
actCurrentGBACart->setEnabled(false);
actInsertGBACart = menu->addAction("Insert ROM cart...");
connect(actInsertGBACart, &QAction::triggered, this, &MainWindow::onInsertGBACart);
{
QMenu* submenu = menu->addMenu("Insert add-on cart");
actInsertGBAAddon[0] = submenu->addAction("Memory expansion");
actInsertGBAAddon[0]->setData(QVariant(NDS::GBAAddon_RAMExpansion));
connect(actInsertGBAAddon[0], &QAction::triggered, this, &MainWindow::onInsertGBAAddon);
}
actEjectGBACart = menu->addAction("Eject cart");
connect(actEjectGBACart, &QAction::triggered, this, &MainWindow::onEjectGBACart);
menu->addSeparator();
actImportSavefile = menu->addAction("Import savefile");
connect(actImportSavefile, &QAction::triggered, this, &MainWindow::onImportSavefile);
menu->addSeparator();
{
QMenu* submenu = menu->addMenu("Save state");
for (int i = 1; i < 9; i++)
{
actSaveState[i] = submenu->addAction(QString("%1").arg(i));
actSaveState[i]->setShortcut(QKeySequence(Qt::ShiftModifier | (Qt::Key_F1+i-1)));
actSaveState[i]->setData(QVariant(i));
connect(actSaveState[i], &QAction::triggered, this, &MainWindow::onSaveState);
}
actSaveState[0] = submenu->addAction("File...");
actSaveState[0]->setShortcut(QKeySequence(Qt::ShiftModifier | Qt::Key_F9));
actSaveState[0]->setData(QVariant(0));
connect(actSaveState[0], &QAction::triggered, this, &MainWindow::onSaveState);
}
{
QMenu* submenu = menu->addMenu("Load state");
for (int i = 1; i < 9; i++)
{
actLoadState[i] = submenu->addAction(QString("%1").arg(i));
actLoadState[i]->setShortcut(QKeySequence(Qt::Key_F1+i-1));
actLoadState[i]->setData(QVariant(i));
connect(actLoadState[i], &QAction::triggered, this, &MainWindow::onLoadState);
}
actLoadState[0] = submenu->addAction("File...");
actLoadState[0]->setShortcut(QKeySequence(Qt::Key_F9));
actLoadState[0]->setData(QVariant(0));
connect(actLoadState[0], &QAction::triggered, this, &MainWindow::onLoadState);
}
actUndoStateLoad = menu->addAction("Undo state load");
actUndoStateLoad->setShortcut(QKeySequence(Qt::Key_F12));
connect(actUndoStateLoad, &QAction::triggered, this, &MainWindow::onUndoStateLoad);
menu->addSeparator();
actQuit = menu->addAction("Quit");
connect(actQuit, &QAction::triggered, this, &MainWindow::onQuit);
}
{
QMenu* menu = menubar->addMenu("System");
actPause = menu->addAction("Pause");
actPause->setCheckable(true);
connect(actPause, &QAction::triggered, this, &MainWindow::onPause);
actReset = menu->addAction("Reset");
connect(actReset, &QAction::triggered, this, &MainWindow::onReset);
actStop = menu->addAction("Stop");
connect(actStop, &QAction::triggered, this, &MainWindow::onStop);
actFrameStep = menu->addAction("Frame step");
connect(actFrameStep, &QAction::triggered, this, &MainWindow::onFrameStep);
menu->addSeparator();
actPowerManagement = menu->addAction("Power management");
connect(actPowerManagement, &QAction::triggered, this, &MainWindow::onOpenPowerManagement);
menu->addSeparator();
actEnableCheats = menu->addAction("Enable cheats");
actEnableCheats->setCheckable(true);
connect(actEnableCheats, &QAction::triggered, this, &MainWindow::onEnableCheats);
//if (inst == 0)
{
actSetupCheats = menu->addAction("Setup cheat codes");
actSetupCheats->setMenuRole(QAction::NoRole);
connect(actSetupCheats, &QAction::triggered, this, &MainWindow::onSetupCheats);
menu->addSeparator();
actROMInfo = menu->addAction("ROM info");
connect(actROMInfo, &QAction::triggered, this, &MainWindow::onROMInfo);
actRAMInfo = menu->addAction("RAM search");
connect(actRAMInfo, &QAction::triggered, this, &MainWindow::onRAMInfo);
actTitleManager = menu->addAction("Manage DSi titles");
connect(actTitleManager, &QAction::triggered, this, &MainWindow::onOpenTitleManager);
}
{
menu->addSeparator();
QMenu* submenu = menu->addMenu("Multiplayer");
actMPNewInstance = submenu->addAction("Launch new instance");
connect(actMPNewInstance, &QAction::triggered, this, &MainWindow::onMPNewInstance);
}
}
{
QMenu* menu = menubar->addMenu("Config");
actEmuSettings = menu->addAction("Emu settings");
connect(actEmuSettings, &QAction::triggered, this, &MainWindow::onOpenEmuSettings);
#ifdef __APPLE__
actPreferences = menu->addAction("Preferences...");
connect(actPreferences, &QAction::triggered, this, &MainWindow::onOpenEmuSettings);
actPreferences->setMenuRole(QAction::PreferencesRole);
#endif
actInputConfig = menu->addAction("Input and hotkeys");
connect(actInputConfig, &QAction::triggered, this, &MainWindow::onOpenInputConfig);
actVideoSettings = menu->addAction("Video settings");
connect(actVideoSettings, &QAction::triggered, this, &MainWindow::onOpenVideoSettings);
actCameraSettings = menu->addAction("Camera settings");
connect(actCameraSettings, &QAction::triggered, this, &MainWindow::onOpenCameraSettings);
actAudioSettings = menu->addAction("Audio settings");
connect(actAudioSettings, &QAction::triggered, this, &MainWindow::onOpenAudioSettings);
actMPSettings = menu->addAction("Multiplayer settings");
connect(actMPSettings, &QAction::triggered, this, &MainWindow::onOpenMPSettings);
actWifiSettings = menu->addAction("Wifi settings");
connect(actWifiSettings, &QAction::triggered, this, &MainWindow::onOpenWifiSettings);
actFirmwareSettings = menu->addAction("Firmware settings");
connect(actFirmwareSettings, &QAction::triggered, this, &MainWindow::onOpenFirmwareSettings);
actInterfaceSettings = menu->addAction("Interface settings");
connect(actInterfaceSettings, &QAction::triggered, this, &MainWindow::onOpenInterfaceSettings);
actPathSettings = menu->addAction("Path settings");
connect(actPathSettings, &QAction::triggered, this, &MainWindow::onOpenPathSettings);
{
QMenu* submenu = menu->addMenu("Savestate settings");
actSavestateSRAMReloc = submenu->addAction("Separate savefiles");
actSavestateSRAMReloc->setCheckable(true);
connect(actSavestateSRAMReloc, &QAction::triggered, this, &MainWindow::onChangeSavestateSRAMReloc);
}
menu->addSeparator();
{
QMenu* submenu = menu->addMenu("Screen size");
for (int i = 0; i < 4; i++)
{
int data = i+1;
actScreenSize[i] = submenu->addAction(QString("%1x").arg(data));
actScreenSize[i]->setData(QVariant(data));
connect(actScreenSize[i], &QAction::triggered, this, &MainWindow::onChangeScreenSize);
}
}
{
QMenu* submenu = menu->addMenu("Screen rotation");
grpScreenRotation = new QActionGroup(submenu);
for (int i = 0; i < 4; i++)
{
int data = i*90;
actScreenRotation[i] = submenu->addAction(QString("%1°").arg(data));
actScreenRotation[i]->setActionGroup(grpScreenRotation);
actScreenRotation[i]->setData(QVariant(i));
actScreenRotation[i]->setCheckable(true);
}
connect(grpScreenRotation, &QActionGroup::triggered, this, &MainWindow::onChangeScreenRotation);
}
{
QMenu* submenu = menu->addMenu("Screen gap");
grpScreenGap = new QActionGroup(submenu);
const int screengap[] = {0, 1, 8, 64, 90, 128};
for (int i = 0; i < 6; i++)
{
int data = screengap[i];
actScreenGap[i] = submenu->addAction(QString("%1 px").arg(data));
actScreenGap[i]->setActionGroup(grpScreenGap);
actScreenGap[i]->setData(QVariant(data));
actScreenGap[i]->setCheckable(true);
}
connect(grpScreenGap, &QActionGroup::triggered, this, &MainWindow::onChangeScreenGap);
}
{
QMenu* submenu = menu->addMenu("Screen layout");
grpScreenLayout = new QActionGroup(submenu);
const char* screenlayout[] = {"Natural", "Vertical", "Horizontal", "Hybrid"};
for (int i = 0; i < 4; i++)
{
actScreenLayout[i] = submenu->addAction(QString(screenlayout[i]));
actScreenLayout[i]->setActionGroup(grpScreenLayout);
actScreenLayout[i]->setData(QVariant(i));
actScreenLayout[i]->setCheckable(true);
}
connect(grpScreenLayout, &QActionGroup::triggered, this, &MainWindow::onChangeScreenLayout);
submenu->addSeparator();
actScreenSwap = submenu->addAction("Swap screens");
actScreenSwap->setCheckable(true);
connect(actScreenSwap, &QAction::triggered, this, &MainWindow::onChangeScreenSwap);
}
{
QMenu* submenu = menu->addMenu("Screen sizing");
grpScreenSizing = new QActionGroup(submenu);
const char* screensizing[] = {"Even", "Emphasize top", "Emphasize bottom", "Auto", "Top only", "Bottom only"};
for (int i = 0; i < screenSizing_MAX; i++)
{
actScreenSizing[i] = submenu->addAction(QString(screensizing[i]));
actScreenSizing[i]->setActionGroup(grpScreenSizing);
actScreenSizing[i]->setData(QVariant(i));
actScreenSizing[i]->setCheckable(true);
}
connect(grpScreenSizing, &QActionGroup::triggered, this, &MainWindow::onChangeScreenSizing);
submenu->addSeparator();
actIntegerScaling = submenu->addAction("Force integer scaling");
actIntegerScaling->setCheckable(true);
connect(actIntegerScaling, &QAction::triggered, this, &MainWindow::onChangeIntegerScaling);
}
{
QMenu* submenu = menu->addMenu("Aspect ratio");
grpScreenAspectTop = new QActionGroup(submenu);
grpScreenAspectBot = new QActionGroup(submenu);
actScreenAspectTop = new QAction*[sizeof(aspectRatios) / sizeof(aspectRatios[0])];
actScreenAspectBot = new QAction*[sizeof(aspectRatios) / sizeof(aspectRatios[0])];
for (int i = 0; i < 2; i++)
{
QActionGroup* group = grpScreenAspectTop;
QAction** actions = actScreenAspectTop;
if (i == 1)
{
group = grpScreenAspectBot;
submenu->addSeparator();
actions = actScreenAspectBot;
}
for (int j = 0; j < sizeof(aspectRatios) / sizeof(aspectRatios[0]); j++)
{
auto ratio = aspectRatios[j];
QString label = QString("%1 %2").arg(i ? "Bottom" : "Top", ratio.label);
actions[j] = submenu->addAction(label);
actions[j]->setActionGroup(group);
actions[j]->setData(QVariant(ratio.id));
actions[j]->setCheckable(true);
}
connect(group, &QActionGroup::triggered, this, &MainWindow::onChangeScreenAspect);
}
}
actScreenFiltering = menu->addAction("Screen filtering");
actScreenFiltering->setCheckable(true);
connect(actScreenFiltering, &QAction::triggered, this, &MainWindow::onChangeScreenFiltering);
actShowOSD = menu->addAction("Show OSD");
actShowOSD->setCheckable(true);
connect(actShowOSD, &QAction::triggered, this, &MainWindow::onChangeShowOSD);
menu->addSeparator();
actLimitFramerate = menu->addAction("Limit framerate");
actLimitFramerate->setCheckable(true);
connect(actLimitFramerate, &QAction::triggered, this, &MainWindow::onChangeLimitFramerate);
actAudioSync = menu->addAction("Audio sync");
actAudioSync->setCheckable(true);
connect(actAudioSync, &QAction::triggered, this, &MainWindow::onChangeAudioSync);
}
setMenuBar(menubar);
resize(Config::WindowWidth, Config::WindowHeight);
if (Config::FirmwareUsername == "Arisotura")
actMPNewInstance->setText("Fart");
#ifdef Q_OS_MAC
QPoint screenCenter = screen()->availableGeometry().center();
QRect frameGeo = frameGeometry();
frameGeo.moveCenter(screenCenter);
move(frameGeo.topLeft());
#endif
if (oldMax)
showMaximized();
else
show();
createScreenPanel();
actEjectCart->setEnabled(false);
actEjectGBACart->setEnabled(false);
if (Config::ConsoleType == 1)
{
actInsertGBACart->setEnabled(false);
for (int i = 0; i < 1; i++)
actInsertGBAAddon[i]->setEnabled(false);
}
for (int i = 0; i < 9; i++)
{
actSaveState[i]->setEnabled(false);
actLoadState[i]->setEnabled(false);
}
actUndoStateLoad->setEnabled(false);
actImportSavefile->setEnabled(false);
actPause->setEnabled(false);
actReset->setEnabled(false);
actStop->setEnabled(false);
actFrameStep->setEnabled(false);
actPowerManagement->setEnabled(false);
actSetupCheats->setEnabled(false);
actTitleManager->setEnabled(!Config::DSiNANDPath.empty());
actEnableCheats->setChecked(Config::EnableCheats);
actROMInfo->setEnabled(false);
actRAMInfo->setEnabled(false);
actSavestateSRAMReloc->setChecked(Config::SavestateRelocSRAM);
actScreenRotation[Config::ScreenRotation]->setChecked(true);
for (int i = 0; i < 6; i++)
{
if (actScreenGap[i]->data().toInt() == Config::ScreenGap)
{
actScreenGap[i]->setChecked(true);
break;
}
}
actScreenLayout[Config::ScreenLayout]->setChecked(true);
actScreenSizing[Config::ScreenSizing]->setChecked(true);
actIntegerScaling->setChecked(Config::IntegerScaling);
actScreenSwap->setChecked(Config::ScreenSwap);
for (int i = 0; i < sizeof(aspectRatios) / sizeof(aspectRatios[0]); i++)
{
if (Config::ScreenAspectTop == aspectRatios[i].id)
actScreenAspectTop[i]->setChecked(true);
if (Config::ScreenAspectBot == aspectRatios[i].id)
actScreenAspectBot[i]->setChecked(true);
}
actScreenFiltering->setChecked(Config::ScreenFilter);
actShowOSD->setChecked(Config::ShowOSD);
actLimitFramerate->setChecked(Config::LimitFPS);
actAudioSync->setChecked(Config::AudioSync);
if (inst > 0)
{
actEmuSettings->setEnabled(false);
actVideoSettings->setEnabled(false);
actMPSettings->setEnabled(false);
actWifiSettings->setEnabled(false);
actInterfaceSettings->setEnabled(false);
#ifdef __APPLE__
actPreferences->setEnabled(false);
#endif // __APPLE__
}
}
MainWindow::~MainWindow()
{
}
void MainWindow::closeEvent(QCloseEvent* event)
{
if (hasOGL)
{
// we intentionally don't unpause here
emuThread->emuPause();
emuThread->deinitContext();
}
QMainWindow::closeEvent(event);
}
void MainWindow::createScreenPanel()
{
hasOGL = (Config::ScreenUseGL != 0) || (Config::_3DRenderer != 0);
if (hasOGL)
{
ScreenPanelGL* panelGL = new ScreenPanelGL(this);
panelGL->show();
panel = panelGL;
panelWidget = panelGL;
panelGL->createContext();
}
if (!hasOGL)
{
ScreenPanelNative* panelNative = new ScreenPanelNative(this);
panel = panelNative;
panelWidget = panelNative;
panelWidget->show();
}
setCentralWidget(panelWidget);
connect(this, SIGNAL(screenLayoutChange()), panelWidget, SLOT(onScreenLayoutChanged()));
emit screenLayoutChange();
}
GL::Context* MainWindow::getOGLContext()
{
if (!hasOGL) return nullptr;
ScreenPanelGL* glpanel = static_cast<ScreenPanelGL*>(panel);
return glpanel->getContext();
}
void MainWindow::resizeEvent(QResizeEvent* event)
{
int w = event->size().width();
int h = event->size().height();
if (!isFullScreen())
{
// this is ugly
// thing is, when maximizing the window, we first receive the resizeEvent
// with a new size matching the screen, then the changeEvent telling us that
// the maximized flag was updated
oldW = Config::WindowWidth;
oldH = Config::WindowHeight;
oldMax = isMaximized();
Config::WindowWidth = w;
Config::WindowHeight = h;
}
}
void MainWindow::changeEvent(QEvent* event)
{
if (isMaximized() && !oldMax)
{
Config::WindowWidth = oldW;
Config::WindowHeight = oldH;
}
Config::WindowMaximized = isMaximized() ? 1:0;
}
void MainWindow::keyPressEvent(QKeyEvent* event)
{
if (event->isAutoRepeat()) return;
// TODO!! REMOVE ME IN RELEASE BUILDS!!
//if (event->key() == Qt::Key_F11) NDS::debug(0);
Input::KeyPress(event);
}
void MainWindow::keyReleaseEvent(QKeyEvent* event)
{
if (event->isAutoRepeat()) return;
Input::KeyRelease(event);
}
void MainWindow::dragEnterEvent(QDragEnterEvent* event)
{
if (!event->mimeData()->hasUrls()) return;
QList<QUrl> urls = event->mimeData()->urls();
if (urls.count() > 1) return; // not handling more than one file at once
QString filename = urls.at(0).toLocalFile();
if (FileIsSupportedFiletype(filename))
event->acceptProposedAction();
}
void MainWindow::dropEvent(QDropEvent* event)
{
if (!event->mimeData()->hasUrls()) return;
QList<QUrl> urls = event->mimeData()->urls();
if (urls.count() > 1) return; // not handling more than one file at once
emuThread->emuPause();
if (!verifySetup())
{
emuThread->emuUnpause();
return;
}
const QStringList file = splitArchivePath(urls.at(0).toLocalFile(), false);
if (file.isEmpty())
{
emuThread->emuUnpause();
return;
}
const QString filename = file.last();
const bool romInsideArchive = file.size() > 1;
const auto matchMode = romInsideArchive ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault;
const QMimeType mimetype = QMimeDatabase().mimeTypeForFile(filename, matchMode);
bool isNdsRom = NdsRomByExtension(filename) || NdsRomByMimetype(mimetype);
bool isGbaRom = GbaRomByExtension(filename) || GbaRomByMimetype(mimetype);
isNdsRom |= ZstdNdsRomByExtension(filename);
isGbaRom |= ZstdGbaRomByExtension(filename);
if (isNdsRom)
{
if (!ROMManager::LoadROM(file, true))
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the DS ROM.");
emuThread->emuUnpause();
return;
}
const QString barredFilename = file.join('|');
recentFileList.removeAll(barredFilename);
recentFileList.prepend(barredFilename);
updateRecentFilesMenu();
NDS::Start();
emuThread->emuRun();
updateCartInserted(false);
}
else if (isGbaRom)
{
if (!ROMManager::LoadGBAROM(file))
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the GBA ROM.");
emuThread->emuUnpause();
return;
}
emuThread->emuUnpause();
updateCartInserted(true);
}
else
{
QMessageBox::critical(this, "melonDS", "The file could not be recognized as a DS or GBA ROM.");
emuThread->emuUnpause();
return;
}
}
void MainWindow::focusInEvent(QFocusEvent* event)
{
audioMute();
}
void MainWindow::focusOutEvent(QFocusEvent* event)
{
audioMute();
}
void MainWindow::onAppStateChanged(Qt::ApplicationState state)
{
if (state == Qt::ApplicationInactive)
{
if (Config::PauseLostFocus && emuThread->emuIsRunning())
emuThread->emuPause();
}
else if (state == Qt::ApplicationActive)
{
if (Config::PauseLostFocus && !pausedManually)
emuThread->emuUnpause();
}
}
bool MainWindow::verifySetup()
{
QString res = ROMManager::VerifySetup();
if (!res.isEmpty())
{
QMessageBox::critical(this, "melonDS", res);
return false;
}
return true;
}
bool MainWindow::preloadROMs(QStringList file, QStringList gbafile, bool boot)
{
if (!verifySetup())
{
return false;
}
bool gbaloaded = false;
if (!gbafile.isEmpty())
{
if (!ROMManager::LoadGBAROM(gbafile))
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the GBA ROM.");
return false;
}
gbaloaded = true;
}
bool ndsloaded = false;
if (!file.isEmpty())
{
if (!ROMManager::LoadROM(file, true))
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the ROM.");
return false;
}
recentFileList.removeAll(file.join("|"));
recentFileList.prepend(file.join("|"));
updateRecentFilesMenu();
ndsloaded = true;
}
if (boot)
{
if (ndsloaded)
{
NDS::Start();
emuThread->emuRun();
}
else
{
onBootFirmware();
}
}
updateCartInserted(false);
if (gbaloaded)
{
updateCartInserted(true);
}
return true;
}
QStringList MainWindow::splitArchivePath(const QString& filename, bool useMemberSyntax)
{
if (filename.isEmpty()) return {};
#ifdef ARCHIVE_SUPPORT_ENABLED
if (useMemberSyntax)
{
const QStringList filenameParts = filename.split('|');
if (filenameParts.size() > 2)
{
QMessageBox::warning(this, "melonDS", "This path contains too many '|'.");
return {};
}
if (filenameParts.size() == 2)
{
const QString archive = filenameParts.at(0);
if (!QFileInfo(archive).exists())
{
QMessageBox::warning(this, "melonDS", "This archive does not exist.");
return {};
}
const QString subfile = filenameParts.at(1);
if (!Archive::ListArchive(archive).contains(subfile))
{
QMessageBox::warning(this, "melonDS", "This archive does not contain the desired file.");
return {};
}
return filenameParts;
}
}
#endif
if (!QFileInfo(filename).exists())
{
QMessageBox::warning(this, "melonDS", "This ROM file does not exist.");
return {};
}
#ifdef ARCHIVE_SUPPORT_ENABLED
if (SupportedArchiveByExtension(filename)
|| SupportedArchiveByMimetype(QMimeDatabase().mimeTypeForFile(filename)))
{
const QString subfile = pickFileFromArchive(filename);
if (subfile.isEmpty())
return {};
return { filename, subfile };
}
#endif
return { filename };
}
QString MainWindow::pickFileFromArchive(QString archiveFileName)
{
QVector<QString> archiveROMList = Archive::ListArchive(archiveFileName);
if (archiveROMList.size() <= 1)
{
if (!archiveROMList.isEmpty() && archiveROMList.at(0) == "OK")
QMessageBox::warning(this, "melonDS", "This archive is empty.");
else
QMessageBox::critical(this, "melonDS", "This archive could not be read. It may be corrupt or you don't have the permissions.");
return QString();
}
archiveROMList.removeFirst();
const auto notSupportedRom = [&](const auto& filename){
if (NdsRomByExtension(filename) || GbaRomByExtension(filename))
return false;
const QMimeType mimetype = QMimeDatabase().mimeTypeForFile(filename, QMimeDatabase::MatchExtension);
return !(NdsRomByMimetype(mimetype) || GbaRomByMimetype(mimetype));
};
archiveROMList.erase(std::remove_if(archiveROMList.begin(), archiveROMList.end(), notSupportedRom),
archiveROMList.end());
if (archiveROMList.isEmpty())
{
QMessageBox::warning(this, "melonDS", "This archive does not contain any supported ROMs.");
return QString();
}
if (archiveROMList.size() == 1)
return archiveROMList.first();
bool ok;
const QString toLoad = QInputDialog::getItem(
this, "melonDS",
"This archive contains multiple files. Select which ROM you want to load.",
archiveROMList.toList(), 0, false, &ok
);
if (ok) return toLoad;
// User clicked on cancel
return QString();
}
QStringList MainWindow::pickROM(bool gba)
{
const QString console = gba ? "GBA" : "DS";
const QStringList& romexts = gba ? GbaRomExtensions : NdsRomExtensions;
QString rawROMs = romexts.join(" *");
QString extraFilters = ";;" + console + " ROMs (*" + rawROMs;
QString allROMs = rawROMs;
QString zstdROMs = "*" + romexts.join(".zst *") + ".zst";
extraFilters += ");;Zstandard-compressed " + console + " ROMs (" + zstdROMs + ")";
allROMs += " " + zstdROMs;
#ifdef ARCHIVE_SUPPORT_ENABLED
QString archives = "*" + ArchiveExtensions.join(" *");
extraFilters += ";;Archives (" + archives + ")";
allROMs += " " + archives;
#endif
extraFilters += ";;All files (*.*)";
const QString filename = QFileDialog::getOpenFileName(
this, "Open " + console + " ROM",
QString::fromStdString(Config::LastROMFolder),
"All supported files (*" + allROMs + ")" + extraFilters
);
if (filename.isEmpty()) return {};
Config::LastROMFolder = QFileInfo(filename).dir().path().toStdString();
return splitArchivePath(filename, false);
}
void MainWindow::updateCartInserted(bool gba)
{
bool inserted;
if (gba)
{
inserted = ROMManager::GBACartInserted() && (Config::ConsoleType == 0);
actCurrentGBACart->setText("GBA slot: " + ROMManager::GBACartLabel());
actEjectGBACart->setEnabled(inserted);
}
else
{
inserted = ROMManager::CartInserted();
actCurrentCart->setText("DS slot: " + ROMManager::CartLabel());
actEjectCart->setEnabled(inserted);
actImportSavefile->setEnabled(inserted);
actSetupCheats->setEnabled(inserted);
actROMInfo->setEnabled(inserted);
actRAMInfo->setEnabled(inserted);
}
}
void MainWindow::onOpenFile()
{
emuThread->emuPause();
if (!verifySetup())
{
emuThread->emuUnpause();
return;
}
QStringList file = pickROM(false);
if (file.isEmpty())
{
emuThread->emuUnpause();
return;
}
if (!ROMManager::LoadROM(file, true))
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the ROM.");
emuThread->emuUnpause();
return;
}
QString filename = file.join('|');
recentFileList.removeAll(filename);
recentFileList.prepend(filename);
updateRecentFilesMenu();
NDS::Start();
emuThread->emuRun();
updateCartInserted(false);
}
void MainWindow::onClearRecentFiles()
{
recentFileList.clear();
for (int i = 0; i < 10; i++)
Config::RecentROMList[i] = "";
updateRecentFilesMenu();
}
void MainWindow::updateRecentFilesMenu()
{
recentMenu->clear();
for (int i = 0; i < recentFileList.size(); ++i)
{
if (i >= 10) break;
QString item_full = recentFileList.at(i);
QString item_display = item_full;
int itemlen = item_full.length();
const int maxlen = 100;
if (itemlen > maxlen)
{
int cut_start = 0;
while (item_full[cut_start] != '/' && item_full[cut_start] != '\\' &&
cut_start < itemlen)
cut_start++;
int cut_end = itemlen-1;
while (((item_full[cut_end] != '/' && item_full[cut_end] != '\\') ||
(cut_start+4+(itemlen-cut_end) < maxlen)) &&
cut_end > 0)
cut_end--;
item_display.truncate(cut_start+1);
item_display += "...";
item_display += QString(item_full).remove(0, cut_end);
}
QAction *actRecentFile_i = recentMenu->addAction(QString("%1. %2").arg(i+1).arg(item_display));
actRecentFile_i->setData(item_full);
connect(actRecentFile_i, &QAction::triggered, this, &MainWindow::onClickRecentFile);
Config::RecentROMList[i] = recentFileList.at(i).toStdString();
}
while (recentFileList.size() > 10)
recentFileList.removeLast();
recentMenu->addSeparator();
QAction *actClearRecentList = recentMenu->addAction("Clear");
connect(actClearRecentList, &QAction::triggered, this, &MainWindow::onClearRecentFiles);
if (recentFileList.empty())
actClearRecentList->setEnabled(false);
Config::Save();
}
void MainWindow::onClickRecentFile()
{
QAction *act = (QAction *)sender();
QString filename = act->data().toString();
emuThread->emuPause();
if (!verifySetup())
{
emuThread->emuUnpause();
return;
}
const QStringList file = splitArchivePath(filename, true);
if (file.isEmpty())
{
emuThread->emuUnpause();
return;
}
if (!ROMManager::LoadROM(file, true))
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the ROM.");
emuThread->emuUnpause();
return;
}
recentFileList.removeAll(filename);
recentFileList.prepend(filename);
updateRecentFilesMenu();
NDS::Start();
emuThread->emuRun();
updateCartInserted(false);
}
void MainWindow::onBootFirmware()
{
emuThread->emuPause();
if (!verifySetup())
{
emuThread->emuUnpause();
return;
}
if (!ROMManager::LoadBIOS())
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "This firmware is not bootable.");
emuThread->emuUnpause();
return;
}
NDS::Start();
emuThread->emuRun();
}
void MainWindow::onInsertCart()
{
emuThread->emuPause();
QStringList file = pickROM(false);
if (file.isEmpty())
{
emuThread->emuUnpause();
return;
}
if (!ROMManager::LoadROM(file, false))
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the ROM.");
emuThread->emuUnpause();
return;
}
emuThread->emuUnpause();
updateCartInserted(false);
}
void MainWindow::onEjectCart()
{
emuThread->emuPause();
ROMManager::EjectCart();
emuThread->emuUnpause();
updateCartInserted(false);
}
void MainWindow::onInsertGBACart()
{
emuThread->emuPause();
QStringList file = pickROM(true);
if (file.isEmpty())
{
emuThread->emuUnpause();
return;
}
if (!ROMManager::LoadGBAROM(file))
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the ROM.");
emuThread->emuUnpause();
return;
}
emuThread->emuUnpause();
updateCartInserted(true);
}
void MainWindow::onInsertGBAAddon()
{
QAction* act = (QAction*)sender();
int type = act->data().toInt();
emuThread->emuPause();
ROMManager::LoadGBAAddon(type);
emuThread->emuUnpause();
updateCartInserted(true);
}
void MainWindow::onEjectGBACart()
{
emuThread->emuPause();
ROMManager::EjectGBACart();
emuThread->emuUnpause();
updateCartInserted(true);
}
void MainWindow::onSaveState()
{
int slot = ((QAction*)sender())->data().toInt();
emuThread->emuPause();
std::string filename;
if (slot > 0)
{
filename = ROMManager::GetSavestateName(slot);
}
else
{
// TODO: specific 'last directory' for savestate files?
QString qfilename = QFileDialog::getSaveFileName(this,
"Save state",
QString::fromStdString(Config::LastROMFolder),
"melonDS savestates (*.mln);;Any file (*.*)");
if (qfilename.isEmpty())
{
emuThread->emuUnpause();
return;
}
filename = qfilename.toStdString();
}
if (ROMManager::SaveState(filename))
{
char msg[64];
if (slot > 0) sprintf(msg, "State saved to slot %d", slot);
else sprintf(msg, "State saved to file");
OSD::AddMessage(0, msg);
actLoadState[slot]->setEnabled(true);
}
else
{
OSD::AddMessage(0xFFA0A0, "State save failed");
}
emuThread->emuUnpause();
}
void MainWindow::onLoadState()
{
int slot = ((QAction*)sender())->data().toInt();
emuThread->emuPause();
std::string filename;
if (slot > 0)
{
filename = ROMManager::GetSavestateName(slot);
}
else
{
// TODO: specific 'last directory' for savestate files?
QString qfilename = QFileDialog::getOpenFileName(this,
"Load state",
QString::fromStdString(Config::LastROMFolder),
"melonDS savestates (*.ml*);;Any file (*.*)");
if (qfilename.isEmpty())
{
emuThread->emuUnpause();
return;
}
filename = qfilename.toStdString();
}
if (!Platform::FileExists(filename))
{
char msg[64];
if (slot > 0) sprintf(msg, "State slot %d is empty", slot);
else sprintf(msg, "State file does not exist");
OSD::AddMessage(0xFFA0A0, msg);
emuThread->emuUnpause();
return;
}
if (ROMManager::LoadState(filename))
{
char msg[64];
if (slot > 0) sprintf(msg, "State loaded from slot %d", slot);
else sprintf(msg, "State loaded from file");
OSD::AddMessage(0, msg);
actUndoStateLoad->setEnabled(true);
}
else
{
OSD::AddMessage(0xFFA0A0, "State load failed");
}
emuThread->emuUnpause();
}
void MainWindow::onUndoStateLoad()
{
emuThread->emuPause();
ROMManager::UndoStateLoad();
emuThread->emuUnpause();
OSD::AddMessage(0, "State load undone");
}
void MainWindow::onImportSavefile()
{
emuThread->emuPause();
QString path = QFileDialog::getOpenFileName(this,
"Select savefile",
QString::fromStdString(Config::LastROMFolder),
"Savefiles (*.sav *.bin *.dsv);;Any file (*.*)");
if (path.isEmpty())
{
emuThread->emuUnpause();
return;
}
FILE* f = Platform::OpenFile(path.toStdString(), "rb", true);
if (!f)
{
QMessageBox::critical(this, "melonDS", "Could not open the given savefile.");
emuThread->emuUnpause();
return;
}
if (RunningSomething)
{
if (QMessageBox::warning(this,
"melonDS",
"The emulation will be reset and the current savefile overwritten.",
QMessageBox::Ok, QMessageBox::Cancel) != QMessageBox::Ok)
{
emuThread->emuUnpause();
return;
}
ROMManager::Reset();
}
u32 len;
fseek(f, 0, SEEK_END);
len = (u32)ftell(f);
u8* data = new u8[len];
fseek(f, 0, SEEK_SET);
fread(data, len, 1, f);
NDS::LoadSave(data, len);
delete[] data;
fclose(f);
emuThread->emuUnpause();
}
void MainWindow::onQuit()
{
#ifndef _WIN32
signalSn->setEnabled(false);
#endif
QApplication::quit();
}
void MainWindow::onPause(bool checked)
{
if (!RunningSomething) return;
if (checked)
{
emuThread->emuPause();
OSD::AddMessage(0, "Paused");
pausedManually = true;
}
else
{
emuThread->emuUnpause();
OSD::AddMessage(0, "Resumed");
pausedManually = false;
}
}
void MainWindow::onReset()
{
if (!RunningSomething) return;
emuThread->emuPause();
actUndoStateLoad->setEnabled(false);
ROMManager::Reset();
OSD::AddMessage(0, "Reset");
emuThread->emuRun();
}
void MainWindow::onStop()
{
if (!RunningSomething) return;
emuThread->emuPause();
NDS::Stop();
}
void MainWindow::onFrameStep()
{
if (!RunningSomething) return;
emuThread->emuFrameStep();
}
void MainWindow::onEnableCheats(bool checked)
{
Config::EnableCheats = checked?1:0;
ROMManager::EnableCheats(Config::EnableCheats != 0);
}
void MainWindow::onSetupCheats()
{
emuThread->emuPause();
CheatsDialog* dlg = CheatsDialog::openDlg(this);
connect(dlg, &CheatsDialog::finished, this, &MainWindow::onCheatsDialogFinished);
}
void MainWindow::onCheatsDialogFinished(int res)
{
emuThread->emuUnpause();
}
void MainWindow::onROMInfo()
{
ROMInfoDialog* dlg = ROMInfoDialog::openDlg(this);
}
void MainWindow::onRAMInfo()
{
RAMInfoDialog* dlg = RAMInfoDialog::openDlg(this);
}
void MainWindow::onOpenTitleManager()
{
TitleManagerDialog* dlg = TitleManagerDialog::openDlg(this);
}
void MainWindow::onMPNewInstance()
{
//QProcess::startDetached(QApplication::applicationFilePath());
QProcess newinst;
newinst.setProgram(QApplication::applicationFilePath());
newinst.setArguments(QApplication::arguments().mid(1, QApplication::arguments().length()-1));
#ifdef __WIN32__
newinst.setCreateProcessArgumentsModifier([] (QProcess::CreateProcessArguments *args)
{
args->flags |= CREATE_NEW_CONSOLE;
});
#endif
newinst.startDetached();
}
void MainWindow::onOpenEmuSettings()
{
emuThread->emuPause();
EmuSettingsDialog* dlg = EmuSettingsDialog::openDlg(this);
connect(dlg, &EmuSettingsDialog::finished, this, &MainWindow::onEmuSettingsDialogFinished);
}
void MainWindow::onEmuSettingsDialogFinished(int res)
{
emuThread->emuUnpause();
if (Config::ConsoleType == 1)
{
actInsertGBACart->setEnabled(false);
for (int i = 0; i < 1; i++)
actInsertGBAAddon[i]->setEnabled(false);
actEjectGBACart->setEnabled(false);
}
else
{
actInsertGBACart->setEnabled(true);
for (int i = 0; i < 1; i++)
actInsertGBAAddon[i]->setEnabled(true);
actEjectGBACart->setEnabled(ROMManager::GBACartInserted());
}
if (EmuSettingsDialog::needsReset)
onReset();
actCurrentGBACart->setText("GBA slot: " + ROMManager::GBACartLabel());
if (!RunningSomething)
actTitleManager->setEnabled(!Config::DSiNANDPath.empty());
}
void MainWindow::onOpenPowerManagement()
{
PowerManagementDialog* dlg = PowerManagementDialog::openDlg(this);
}
void MainWindow::onOpenInputConfig()
{
emuThread->emuPause();
InputConfigDialog* dlg = InputConfigDialog::openDlg(this);
connect(dlg, &InputConfigDialog::finished, this, &MainWindow::onInputConfigFinished);
}
void MainWindow::onInputConfigFinished(int res)
{
emuThread->emuUnpause();
}
void MainWindow::onOpenVideoSettings()
{
VideoSettingsDialog* dlg = VideoSettingsDialog::openDlg(this);
connect(dlg, &VideoSettingsDialog::updateVideoSettings, this, &MainWindow::onUpdateVideoSettings);
}
void MainWindow::onOpenCameraSettings()
{
emuThread->emuPause();
camStarted[0] = camManager[0]->isStarted();
camStarted[1] = camManager[1]->isStarted();
if (camStarted[0]) camManager[0]->stop();
if (camStarted[1]) camManager[1]->stop();
CameraSettingsDialog* dlg = CameraSettingsDialog::openDlg(this);
connect(dlg, &CameraSettingsDialog::finished, this, &MainWindow::onCameraSettingsFinished);
}
void MainWindow::onCameraSettingsFinished(int res)
{
if (camStarted[0]) camManager[0]->start();
if (camStarted[1]) camManager[1]->start();
emuThread->emuUnpause();
}
void MainWindow::onOpenAudioSettings()
{
AudioSettingsDialog* dlg = AudioSettingsDialog::openDlg(this, emuThread->emuIsActive());
connect(emuThread, &EmuThread::syncVolumeLevel, dlg, &AudioSettingsDialog::onSyncVolumeLevel);
connect(emuThread, &EmuThread::windowEmuStart, dlg, &AudioSettingsDialog::onConsoleReset);
connect(dlg, &AudioSettingsDialog::updateAudioSettings, this, &MainWindow::onUpdateAudioSettings);
connect(dlg, &AudioSettingsDialog::finished, this, &MainWindow::onAudioSettingsFinished);
}
void MainWindow::onOpenFirmwareSettings()
{
emuThread->emuPause();
FirmwareSettingsDialog* dlg = FirmwareSettingsDialog::openDlg(this);
connect(dlg, &FirmwareSettingsDialog::finished, this, &MainWindow::onFirmwareSettingsFinished);
}
void MainWindow::onFirmwareSettingsFinished(int res)
{
if (FirmwareSettingsDialog::needsReset)
onReset();
emuThread->emuUnpause();
}
void MainWindow::onOpenPathSettings()
{
emuThread->emuPause();
PathSettingsDialog* dlg = PathSettingsDialog::openDlg(this);
connect(dlg, &PathSettingsDialog::finished, this, &MainWindow::onPathSettingsFinished);
}
void MainWindow::onPathSettingsFinished(int res)
{
if (PathSettingsDialog::needsReset)
onReset();
emuThread->emuUnpause();
}
void MainWindow::onUpdateAudioSettings()
{
SPU::SetInterpolation(Config::AudioInterp);
if (Config::AudioBitrate == 0)
SPU::SetDegrade10Bit(NDS::ConsoleType == 0);
else
SPU::SetDegrade10Bit(Config::AudioBitrate == 1);
}
void MainWindow::onAudioSettingsFinished(int res)
{
micClose();
SPU::SetInterpolation(Config::AudioInterp);
if (Config::MicInputType == 3)
{
micLoadWav(Config::MicWavPath);
Frontend::Mic_SetExternalBuffer(micWavBuffer, micWavLength);
}
else
{
delete[] micWavBuffer;
micWavBuffer = nullptr;
if (Config::MicInputType == 1)
Frontend::Mic_SetExternalBuffer(micExtBuffer, sizeof(micExtBuffer)/sizeof(s16));
else
Frontend::Mic_SetExternalBuffer(NULL, 0);
}
micOpen();
}
void MainWindow::onOpenMPSettings()
{
emuThread->emuPause();
MPSettingsDialog* dlg = MPSettingsDialog::openDlg(this);
connect(dlg, &MPSettingsDialog::finished, this, &MainWindow::onMPSettingsFinished);
}
void MainWindow::onMPSettingsFinished(int res)
{
audioMute();
LocalMP::SetRecvTimeout(Config::MPRecvTimeout);
emuThread->emuUnpause();
}
void MainWindow::onOpenWifiSettings()
{
emuThread->emuPause();
WifiSettingsDialog* dlg = WifiSettingsDialog::openDlg(this);
connect(dlg, &WifiSettingsDialog::finished, this, &MainWindow::onWifiSettingsFinished);
}
void MainWindow::onWifiSettingsFinished(int res)
{
Platform::LAN_DeInit();
Platform::LAN_Init();
if (WifiSettingsDialog::needsReset)
onReset();
emuThread->emuUnpause();
}
void MainWindow::onOpenInterfaceSettings()
{
emuThread->emuPause();
InterfaceSettingsDialog* dlg = InterfaceSettingsDialog::openDlg(this);
connect(dlg, &InterfaceSettingsDialog::finished, this, &MainWindow::onInterfaceSettingsFinished);
connect(dlg, &InterfaceSettingsDialog::updateMouseTimer, this, &MainWindow::onUpdateMouseTimer);
}
void MainWindow::onUpdateMouseTimer()
{
panel->mouseTimer->setInterval(Config::MouseHideSeconds*1000);
}
void MainWindow::onInterfaceSettingsFinished(int res)
{
emuThread->emuUnpause();
}
void MainWindow::onChangeSavestateSRAMReloc(bool checked)
{
Config::SavestateRelocSRAM = checked?1:0;
}
void MainWindow::onChangeScreenSize()
{
int factor = ((QAction*)sender())->data().toInt();
QSize diff = size() - panelWidget->size();
resize(panel->screenGetMinSize(factor) + diff);
}
void MainWindow::onChangeScreenRotation(QAction* act)
{
int rot = act->data().toInt();
Config::ScreenRotation = rot;
emit screenLayoutChange();
}
void MainWindow::onChangeScreenGap(QAction* act)
{
int gap = act->data().toInt();
Config::ScreenGap = gap;
emit screenLayoutChange();
}
void MainWindow::onChangeScreenLayout(QAction* act)
{
int layout = act->data().toInt();
Config::ScreenLayout = layout;
emit screenLayoutChange();
}
void MainWindow::onChangeScreenSwap(bool checked)
{
Config::ScreenSwap = checked?1:0;
// Swap between top and bottom screen when displaying one screen.
if (Config::ScreenSizing == screenSizing_TopOnly)
{
// Bottom Screen.
Config::ScreenSizing = screenSizing_BotOnly;
actScreenSizing[screenSizing_TopOnly]->setChecked(false);
actScreenSizing[Config::ScreenSizing]->setChecked(true);
}
else if (Config::ScreenSizing == screenSizing_BotOnly)
{
// Top Screen.
Config::ScreenSizing = screenSizing_TopOnly;
actScreenSizing[screenSizing_BotOnly]->setChecked(false);
actScreenSizing[Config::ScreenSizing]->setChecked(true);
}
emit screenLayoutChange();
}
void MainWindow::onChangeScreenSizing(QAction* act)
{
int sizing = act->data().toInt();
Config::ScreenSizing = sizing;
emit screenLayoutChange();
}
void MainWindow::onChangeScreenAspect(QAction* act)
{
int aspect = act->data().toInt();
QActionGroup* group = act->actionGroup();
if (group == grpScreenAspectTop)
{
Config::ScreenAspectTop = aspect;
}
else
{
Config::ScreenAspectBot = aspect;
}
emit screenLayoutChange();
}
void MainWindow::onChangeIntegerScaling(bool checked)
{
Config::IntegerScaling = checked?1:0;
emit screenLayoutChange();
}
void MainWindow::onChangeScreenFiltering(bool checked)
{
Config::ScreenFilter = checked?1:0;
emit screenLayoutChange();
}
void MainWindow::onChangeShowOSD(bool checked)
{
Config::ShowOSD = checked?1:0;
}
void MainWindow::onChangeLimitFramerate(bool checked)
{
Config::LimitFPS = checked?1:0;
}
void MainWindow::onChangeAudioSync(bool checked)
{
Config::AudioSync = checked?1:0;
}
void MainWindow::onTitleUpdate(QString title)
{
setWindowTitle(title);
}
void ToggleFullscreen(MainWindow* mainWindow)
{
if (!mainWindow->isFullScreen())
{
mainWindow->showFullScreen();
mainWindow->menuBar()->setFixedHeight(0); // Don't use hide() as menubar actions stop working
}
else
{
mainWindow->showNormal();
int menuBarHeight = mainWindow->menuBar()->sizeHint().height();
mainWindow->menuBar()->setFixedHeight(menuBarHeight);
}
}
void MainWindow::onFullscreenToggled()
{
ToggleFullscreen(this);
}
void MainWindow::onScreenEmphasisToggled()
{
int currentSizing = Config::ScreenSizing;
if (currentSizing == screenSizing_EmphTop)
{
Config::ScreenSizing = screenSizing_EmphBot;
}
else if (currentSizing == screenSizing_EmphBot)
{
Config::ScreenSizing = screenSizing_EmphTop;
}
emit screenLayoutChange();
}
void MainWindow::onEmuStart()
{
for (int i = 1; i < 9; i++)
{
actSaveState[i]->setEnabled(true);
actLoadState[i]->setEnabled(ROMManager::SavestateExists(i));
}
actSaveState[0]->setEnabled(true);
actLoadState[0]->setEnabled(true);
actUndoStateLoad->setEnabled(false);
actPause->setEnabled(true);
actPause->setChecked(false);
actReset->setEnabled(true);
actStop->setEnabled(true);
actFrameStep->setEnabled(true);
actPowerManagement->setEnabled(true);
actTitleManager->setEnabled(false);
}
void MainWindow::onEmuStop()
{
emuThread->emuPause();
for (int i = 0; i < 9; i++)
{
actSaveState[i]->setEnabled(false);
actLoadState[i]->setEnabled(false);
}
actUndoStateLoad->setEnabled(false);
actPause->setEnabled(false);
actReset->setEnabled(false);
actStop->setEnabled(false);
actFrameStep->setEnabled(false);
actPowerManagement->setEnabled(false);
actTitleManager->setEnabled(!Config::DSiNANDPath.empty());
}
void MainWindow::onUpdateVideoSettings(bool glchange)
{
if (glchange)
{
emuThread->emuPause();
if (hasOGL) emuThread->deinitContext();
delete panel;
createScreenPanel();
connect(emuThread, SIGNAL(windowUpdate()), panelWidget, SLOT(repaint()));
}
videoSettingsDirty = true;
if (glchange)
{
if (hasOGL) emuThread->initContext();
emuThread->emuUnpause();
}
}
void emuStop()
{
RunningSomething = false;
emit emuThread->windowEmuStop();
OSD::AddMessage(0xFFC040, "Shutdown");
}
MelonApplication::MelonApplication(int& argc, char** argv)
: QApplication(argc, argv)
{
setWindowIcon(QIcon(":/melon-icon"));
}
bool MelonApplication::event(QEvent *event)
{
if (event->type() == QEvent::FileOpen)
{
QFileOpenEvent *openEvent = static_cast<QFileOpenEvent*>(event);
emuThread->emuPause();
const QStringList file = mainWindow->splitArchivePath(openEvent->file(), true);
if (!mainWindow->preloadROMs(file, {}, true))
emuThread->emuUnpause();
}
return QApplication::event(event);
}
int main(int argc, char** argv)
{
srand(time(nullptr));
qputenv("QT_SCALE_FACTOR", "1");
printf("melonDS " MELONDS_VERSION "\n");
printf(MELONDS_URL "\n");
// easter egg - not worth checking other cases for something so dumb
if (argc != 0 && (!strcasecmp(argv[0], "derpDS") || !strcasecmp(argv[0], "./derpDS")))
printf("did you just call me a derp???\n");
Platform::Init(argc, argv);
MelonApplication melon(argc, argv);
CLI::CommandLineOptions* options = CLI::ManageArgs(melon);
// http://stackoverflow.com/questions/14543333/joystick-wont-work-using-sdl
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
if (SDL_Init(SDL_INIT_HAPTIC) < 0)
{
printf("SDL couldn't init rumble\n");
}
if (SDL_Init(SDL_INIT_JOYSTICK) < 0)
{
printf("SDL couldn't init joystick\n");
}
if (SDL_Init(SDL_INIT_AUDIO) < 0)
{
const char* err = SDL_GetError();
QString errorStr = "Failed to initialize SDL. This could indicate an issue with your audio driver.\n\nThe error was: ";
errorStr += err;
QMessageBox::critical(NULL, "melonDS", errorStr);
return 1;
}
SDL_JoystickEventState(SDL_ENABLE);
SDL_InitSubSystem(SDL_INIT_VIDEO);
SDL_EnableScreenSaver(); SDL_DisableScreenSaver();
Config::Load();
#define SANITIZE(var, min, max) { var = std::clamp(var, min, max); }
SANITIZE(Config::ConsoleType, 0, 1);
SANITIZE(Config::_3DRenderer,
0,
0 // Minimum, Software renderer
#ifdef OGLRENDERER_ENABLED
+ 1 // OpenGL Renderer
#endif
);
SANITIZE(Config::ScreenVSyncInterval, 1, 20);
SANITIZE(Config::GL_ScaleFactor, 1, 16);
SANITIZE(Config::AudioInterp, 0, 3);
SANITIZE(Config::AudioVolume, 0, 256);
SANITIZE(Config::MicInputType, 0, 3);
SANITIZE(Config::ScreenRotation, 0, 3);
SANITIZE(Config::ScreenGap, 0, 500);
SANITIZE(Config::ScreenLayout, 0, 3);
SANITIZE(Config::ScreenSizing, 0, (int)screenSizing_MAX);
SANITIZE(Config::ScreenAspectTop, 0, 4);
SANITIZE(Config::ScreenAspectBot, 0, 4);
#undef SANITIZE
audioMuted = false;
audioSync = SDL_CreateCond();
audioSyncLock = SDL_CreateMutex();
audioFreq = 48000; // TODO: make configurable?
SDL_AudioSpec whatIwant, whatIget;
memset(&whatIwant, 0, sizeof(SDL_AudioSpec));
whatIwant.freq = audioFreq;
whatIwant.format = AUDIO_S16LSB;
whatIwant.channels = 2;
whatIwant.samples = 1024;
whatIwant.callback = audioCallback;
audioDevice = SDL_OpenAudioDevice(NULL, 0, &whatIwant, &whatIget, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
if (!audioDevice)
{
printf("Audio init failed: %s\n", SDL_GetError());
}
else
{
audioFreq = whatIget.freq;
printf("Audio output frequency: %d Hz\n", audioFreq);
SDL_PauseAudioDevice(audioDevice, 1);
}
micDevice = 0;
memset(micExtBuffer, 0, sizeof(micExtBuffer));
micExtBufferWritePos = 0;
micWavBuffer = nullptr;
camStarted[0] = false;
camStarted[1] = false;
camManager[0] = new CameraManager(0, 640, 480, true);
camManager[1] = new CameraManager(1, 640, 480, true);
camManager[0]->setXFlip(Config::Camera[0].XFlip);
camManager[1]->setXFlip(Config::Camera[1].XFlip);
ROMManager::EnableCheats(Config::EnableCheats != 0);
Frontend::Init_Audio(audioFreq);
if (Config::MicInputType == 1)
{
Frontend::Mic_SetExternalBuffer(micExtBuffer, sizeof(micExtBuffer)/sizeof(s16));
}
else if (Config::MicInputType == 3)
{
micLoadWav(Config::MicWavPath);
Frontend::Mic_SetExternalBuffer(micWavBuffer, micWavLength);
}
Input::JoystickID = Config::JoystickID;
Input::OpenJoystick();
mainWindow = new MainWindow();
if (options->fullscreen)
ToggleFullscreen(mainWindow);
emuThread = new EmuThread();
emuThread->start();
emuThread->emuPause();
audioMute();
QObject::connect(&melon, &QApplication::applicationStateChanged, mainWindow, &MainWindow::onAppStateChanged);
bool memberSyntaxUsed = false;
const auto prepareRomPath = [&](const std::optional<QString>& romPath, const std::optional<QString>& romArchivePath) -> QStringList
{
if (!romPath.has_value())
return {};
if (romArchivePath.has_value())
return { *romPath, *romArchivePath };
const QStringList path = mainWindow->splitArchivePath(*romPath, true);
if (path.size() > 1) memberSyntaxUsed = true;
return path;
};
const QStringList dsfile = prepareRomPath(options->dsRomPath, options->dsRomArchivePath);
const QStringList gbafile = prepareRomPath(options->gbaRomPath, options->gbaRomArchivePath);
if (memberSyntaxUsed) printf("Warning: use the a.zip|b.nds format at your own risk!\n");
mainWindow->preloadROMs(dsfile, gbafile, options->boot);
int ret = melon.exec();
emuThread->emuStop();
emuThread->wait();
delete emuThread;
Input::CloseJoystick();
if (audioDevice) SDL_CloseAudioDevice(audioDevice);
micClose();
SDL_DestroyCond(audioSync);
SDL_DestroyMutex(audioSyncLock);
if (micWavBuffer) delete[] micWavBuffer;
delete camManager[0];
delete camManager[1];
Config::Save();
SDL_Quit();
Platform::DeInit();
return ret;
}
#ifdef __WIN32__
#include <windows.h>
int CALLBACK WinMain(HINSTANCE hinst, HINSTANCE hprev, LPSTR cmdline, int cmdshow)
{
int ret = main(__argc, __argv);
printf("\n\n>");
return ret;
}
#endif