ppsspp/SDL/SDLMain.cpp
Henrik Rydgård 19e4de5088 Change global UI messages to use an enum instead of strings.
Makes it easier to add new ones and delete outdated ones without missing
any uses.
2023-09-30 11:37:02 +02:00

1606 lines
43 KiB
C++

// SDL/EGL implementation of the framework.
// This is quite messy due to platform-specific implementations and #ifdef's.
// If your platform is not supported, it is suggested to use Qt instead.
#include <cstdlib>
#include <unistd.h>
#include <pwd.h>
#include "ppsspp_config.h"
#if PPSSPP_PLATFORM(MAC)
#include "SDL2/SDL.h"
#include "SDL2/SDL_syswm.h"
#else
#include "SDL.h"
#include "SDL_syswm.h"
#endif
#include "SDL/SDLJoystick.h"
SDLJoystick *joystick = NULL;
#if PPSSPP_PLATFORM(RPI)
#include <bcm_host.h>
#endif
#include <atomic>
#include <algorithm>
#include <cmath>
#include <csignal>
#include <thread>
#include <locale>
#include "Common/System/Display.h"
#include "Common/System/System.h"
#include "Common/System/Request.h"
#include "Common/System/NativeApp.h"
#include "ext/glslang/glslang/Public/ShaderLang.h"
#include "Common/Data/Format/PNGLoad.h"
#include "Common/Net/Resolve.h"
#include "Common/File/FileUtil.h"
#include "NKCodeFromSDL.h"
#include "Common/Math/math_util.h"
#include "Common/GPU/OpenGL/GLRenderManager.h"
#include "Common/Profiler/Profiler.h"
#if defined(VK_USE_PLATFORM_XLIB_KHR)
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#elif defined(VK_USE_PLATFORM_XCB_KHR)
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xlib-xcb.h>
#endif
#include "Common/GraphicsContext.h"
#include "Common/TimeUtil.h"
#include "Common/Input/InputState.h"
#include "Common/Input/KeyCodes.h"
#include "Common/Data/Collections/ConstMap.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/Thread/ThreadUtil.h"
#include "Core/System.h"
#include "Core/Core.h"
#include "Core/Config.h"
#include "Core/ConfigValues.h"
#include "SDLGLGraphicsContext.h"
#include "SDLVulkanGraphicsContext.h"
#if PPSSPP_PLATFORM(MAC)
#include "SDL2/SDL_vulkan.h"
#else
#include "SDL_vulkan.h"
#endif
#if PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS)
#include "UI/DarwinFileSystemServices.h"
#endif
#if PPSSPP_PLATFORM(MAC)
#include "CocoaBarItems.h"
#endif
#if PPSSPP_PLATFORM(SWITCH)
#define LIBNX_SWKBD_LIMIT 500 // enforced by HOS
extern u32 __nx_applet_type; // Not exposed through a header?
#endif
GlobalUIState lastUIState = UISTATE_MENU;
GlobalUIState GetUIState();
static bool g_QuitRequested = false;
static bool g_RestartRequested = false;
static int g_DesktopWidth = 0;
static int g_DesktopHeight = 0;
static float g_DesktopDPI = 1.0f;
static float g_ForcedDPI = 0.0f; // if this is 0.0f, use g_DesktopDPI
static float g_RefreshRate = 60.f;
static int g_sampleRate = 44100;
static bool g_rebootEmuThread = false;
static SDL_AudioSpec g_retFmt;
// Window state to be transferred to the main SDL thread.
static std::mutex g_mutexWindow;
struct WindowState {
std::string title;
bool toggleFullScreenNextFrame;
int toggleFullScreenType;
bool clipboardDataAvailable;
std::string clipboardString;
bool update;
};
static WindowState g_windowState;
int getDisplayNumber(void) {
int displayNumber = 0;
char * displayNumberStr;
//get environment
displayNumberStr=getenv("SDL_VIDEO_FULLSCREEN_HEAD");
if (displayNumberStr) {
displayNumber = atoi(displayNumberStr);
}
return displayNumber;
}
void sdl_mixaudio_callback(void *userdata, Uint8 *stream, int len) {
NativeMix((short *)stream, len / (2 * 2), g_sampleRate);
}
static SDL_AudioDeviceID audioDev = 0;
// Must be called after NativeInit().
static void InitSDLAudioDevice(const std::string &name = "") {
SDL_AudioSpec fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.freq = g_sampleRate;
fmt.format = AUDIO_S16;
fmt.channels = 2;
fmt.samples = 256;
fmt.callback = &sdl_mixaudio_callback;
fmt.userdata = nullptr;
std::string startDevice = name;
if (startDevice.empty()) {
startDevice = g_Config.sAudioDevice;
}
audioDev = 0;
if (!startDevice.empty()) {
audioDev = SDL_OpenAudioDevice(startDevice.c_str(), 0, &fmt, &g_retFmt, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
if (audioDev <= 0) {
WARN_LOG(AUDIO, "Failed to open audio device: %s", startDevice.c_str());
}
}
if (audioDev <= 0) {
INFO_LOG(AUDIO, "SDL: Trying a different audio device");
audioDev = SDL_OpenAudioDevice(nullptr, 0, &fmt, &g_retFmt, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
}
if (audioDev <= 0) {
ERROR_LOG(AUDIO, "Failed to open audio device: %s", SDL_GetError());
} else {
if (g_retFmt.samples != fmt.samples) // Notify, but still use it
ERROR_LOG(AUDIO, "Output audio samples: %d (requested: %d)", g_retFmt.samples, fmt.samples);
if (g_retFmt.format != fmt.format || g_retFmt.channels != fmt.channels) {
ERROR_LOG(AUDIO, "Sound buffer format does not match requested format.");
ERROR_LOG(AUDIO, "Output audio freq: %d (requested: %d)", g_retFmt.freq, fmt.freq);
ERROR_LOG(AUDIO, "Output audio format: %d (requested: %d)", g_retFmt.format, fmt.format);
ERROR_LOG(AUDIO, "Output audio channels: %d (requested: %d)", g_retFmt.channels, fmt.channels);
ERROR_LOG(AUDIO, "Provided output format does not match requirement, turning audio off");
SDL_CloseAudioDevice(audioDev);
}
SDL_PauseAudioDevice(audioDev, 0);
}
}
static void StopSDLAudioDevice() {
if (audioDev > 0) {
SDL_PauseAudioDevice(audioDev, 1);
SDL_CloseAudioDevice(audioDev);
}
}
static void UpdateScreenDPI(SDL_Window *window) {
int drawable_width, window_width;
SDL_GetWindowSize(window, &window_width, NULL);
if (g_Config.iGPUBackend == (int)GPUBackend::OPENGL)
SDL_GL_GetDrawableSize(window, &drawable_width, NULL);
else if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN)
SDL_Vulkan_GetDrawableSize(window, &drawable_width, NULL);
// Round up a little otherwise there would be a gap sometimes
// in fractional scaling
g_DesktopDPI = ((float) drawable_width + 1.0f) / window_width;
// Temporary hack
#if PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS)
if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) {
g_DesktopDPI = 1.0f;
}
#endif
}
// Simple implementations of System functions
void System_Toast(const char *text) {
#ifdef _WIN32
std::wstring str = ConvertUTF8ToWString(text);
MessageBox(0, str.c_str(), L"Toast!", MB_ICONINFORMATION);
#else
puts(text);
#endif
}
void System_ShowKeyboard() {
// Irrelevant on PC
}
void System_Vibrate(int length_ms) {
// Ignore on PC
}
bool System_MakeRequest(SystemRequestType type, int requestId, const std::string &param1, const std::string &param2, int param3) {
switch (type) {
case SystemRequestType::RESTART_APP:
g_RestartRequested = true;
// TODO: Also save param1 and then split it into an argv.
return true;
case SystemRequestType::EXIT_APP:
// Do a clean exit
g_QuitRequested = true;
return true;
#if PPSSPP_PLATFORM(SWITCH)
case SystemRequestType::INPUT_TEXT_MODAL: {
// swkbd only works on "real" titles
if (__nx_applet_type != AppletType_Application && __nx_applet_type != AppletType_SystemApplication) {
g_requestManager.PostSystemFailure(requestId);
return true;
}
SwkbdConfig kbd;
Result rc = swkbdCreate(&kbd, 0);
if (R_SUCCEEDED(rc)) {
char buf[LIBNX_SWKBD_LIMIT] = {'\0'};
swkbdConfigMakePresetDefault(&kbd);
swkbdConfigSetHeaderText(&kbd, param1.c_str());
swkbdConfigSetInitialText(&kbd, param2.c_str());
rc = swkbdShow(&kbd, buf, sizeof(buf));
swkbdClose(&kbd);
g_requestManager.PostSystemSuccess(requestId, buf);
return true;
}
g_requestManager.PostSystemFailure(requestId);
return true;
}
#endif // PPSSPP_PLATFORM(SWITCH)
#if PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS)
case SystemRequestType::BROWSE_FOR_FILE:
{
DarwinDirectoryPanelCallback callback = [requestId] (bool success, Path path) {
if (success) {
g_requestManager.PostSystemSuccess(requestId, path.c_str());
} else {
g_requestManager.PostSystemFailure(requestId);
}
};
DarwinFileSystemServices services;
BrowseFileType fileType = (BrowseFileType)param3;
services.presentDirectoryPanel(callback, /* allowFiles = */ true, /* allowDirectories = */ false, fileType);
return true;
}
case SystemRequestType::BROWSE_FOR_FOLDER:
{
DarwinDirectoryPanelCallback callback = [requestId] (bool success, Path path) {
if (success) {
g_requestManager.PostSystemSuccess(requestId, path.c_str());
} else {
g_requestManager.PostSystemFailure(requestId);
}
};
DarwinFileSystemServices services;
services.presentDirectoryPanel(callback, /* allowFiles = */ false, /* allowDirectories = */ true);
return true;
}
#endif
case SystemRequestType::TOGGLE_FULLSCREEN_STATE:
{
std::lock_guard<std::mutex> guard(g_mutexWindow);
g_windowState.update = true;
g_windowState.toggleFullScreenNextFrame = true;
if (param1 == "1") {
g_windowState.toggleFullScreenType = 1;
} else if (param1 == "0") {
g_windowState.toggleFullScreenType = 0;
} else {
// Just toggle.
g_windowState.toggleFullScreenType = -1;
}
return true;
}
case SystemRequestType::SET_WINDOW_TITLE:
{
std::lock_guard<std::mutex> guard(g_mutexWindow);
const char *app_name = System_GetPropertyBool(SYSPROP_APP_GOLD) ? "PPSSPP Gold" : "PPSSPP";
g_windowState.title = param1.empty() ? app_name : param1;
g_windowState.update = true;
return true;
}
case SystemRequestType::COPY_TO_CLIPBOARD:
{
std::lock_guard<std::mutex> guard(g_mutexWindow);
g_windowState.clipboardString = param1;
g_windowState.clipboardDataAvailable = true;
g_windowState.update = true;
return true;
}
case SystemRequestType::SHOW_FILE_IN_FOLDER:
{
#if PPSSPP_PLATFORM(WINDOWS)
SFGAOF flags;
PIDLIST_ABSOLUTE pidl = nullptr;
HRESULT hr = SHParseDisplayName(ConvertUTF8ToWString(ReplaceAll(path, "/", "\\")).c_str(), nullptr, &pidl, 0, &flags);
if (pidl) {
if (SUCCEEDED(hr))
SHOpenFolderAndSelectItems(pidl, 0, NULL, 0);
CoTaskMemFree(pidl);
}
#elif PPSSPP_PLATFORM(MAC)
OSXShowInFinder(param1.c_str());
#elif (PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID))
pid_t pid = fork();
if (pid < 0)
return true;
if (pid == 0) {
execlp("xdg-open", "xdg-open", param1.c_str(), nullptr);
exit(1);
}
#endif /* PPSSPP_PLATFORM(WINDOWS) */
return true;
}
default:
return false;
}
}
void System_AskForPermission(SystemPermission permission) {}
PermissionStatus System_GetPermissionStatus(SystemPermission permission) { return PERMISSION_STATUS_GRANTED; }
void System_LaunchUrl(LaunchUrlType urlType, const char *url) {
switch (urlType) {
case LaunchUrlType::BROWSER_URL:
case LaunchUrlType::MARKET_URL:
{
#if PPSSPP_PLATFORM(SWITCH)
Uuid uuid = { 0 };
WebWifiConfig conf;
webWifiCreate(&conf, NULL, url, uuid, 0);
webWifiShow(&conf, NULL);
#elif defined(MOBILE_DEVICE)
INFO_LOG(SYSTEM, "Would have gone to %s but LaunchBrowser is not implemented on this platform", url);
#elif defined(_WIN32)
std::wstring wurl = ConvertUTF8ToWString(url);
ShellExecute(NULL, L"open", wurl.c_str(), NULL, NULL, SW_SHOWNORMAL);
#elif defined(__APPLE__)
OSXOpenURL(url);
#else
std::string command = std::string("xdg-open ") + url;
int err = system(command.c_str());
if (err) {
INFO_LOG(SYSTEM, "Would have gone to %s but xdg-utils seems not to be installed", url);
}
#endif
break;
}
case LaunchUrlType::EMAIL_ADDRESS:
{
#if defined(MOBILE_DEVICE)
INFO_LOG(SYSTEM, "Would have opened your email client for %s but LaunchEmail is not implemented on this platform", url);
#elif defined(_WIN32)
std::wstring mailto = std::wstring(L"mailto:") + ConvertUTF8ToWString(url);
ShellExecute(NULL, L"open", mailto.c_str(), NULL, NULL, SW_SHOWNORMAL);
#elif defined(__APPLE__)
std::string mailToURL = std::string("mailto:") + url;
OSXOpenURL(mailToURL.c_str());
#else
std::string command = std::string("xdg-email ") + url;
int err = system(command.c_str());
if (err) {
INFO_LOG(SYSTEM, "Would have gone to %s but xdg-utils seems not to be installed", url);
}
#endif
break;
}
}
}
std::string System_GetProperty(SystemProperty prop) {
switch (prop) {
case SYSPROP_NAME:
#ifdef _WIN32
return "SDL:Windows";
#elif __linux__
return "SDL:Linux";
#elif __APPLE__
return "SDL:macOS";
#elif PPSSPP_PLATFORM(SWITCH)
return "SDL:Horizon";
#else
return "SDL:";
#endif
case SYSPROP_LANGREGION: {
// Get user-preferred locale from OS
setlocale(LC_ALL, "");
std::string locale(setlocale(LC_ALL, NULL));
// Set c and c++ strings back to POSIX
std::locale::global(std::locale("POSIX"));
if (!locale.empty()) {
// Technically, this is an opaque string, but try to find the locale code.
size_t messagesPos = locale.find("LC_MESSAGES=");
if (messagesPos != std::string::npos) {
messagesPos += strlen("LC_MESSAGES=");
size_t semi = locale.find(';', messagesPos);
locale = locale.substr(messagesPos, semi - messagesPos);
}
if (locale.find("_", 0) != std::string::npos) {
if (locale.find(".", 0) != std::string::npos) {
return locale.substr(0, locale.find(".",0));
}
return locale;
}
}
return "en_US";
}
case SYSPROP_CLIPBOARD_TEXT:
return SDL_HasClipboardText() ? SDL_GetClipboardText() : "";
case SYSPROP_AUDIO_DEVICE_LIST:
{
std::string result;
for (int i = 0; i < SDL_GetNumAudioDevices(0); ++i) {
const char *name = SDL_GetAudioDeviceName(i, 0);
if (!name) {
continue;
}
if (i == 0) {
result = name;
} else {
result.append(1, '\0');
result.append(name);
}
}
return result;
}
case SYSPROP_BUILD_VERSION:
return PPSSPP_GIT_VERSION;
case SYSPROP_USER_DOCUMENTS_DIR:
{
const char *home = getenv("HOME");
return home ? std::string(home) : "/";
}
default:
return "";
}
}
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {
std::vector<std::string> result;
switch (prop) {
case SYSPROP_TEMP_DIRS:
if (getenv("TMPDIR") && strlen(getenv("TMPDIR")) != 0)
result.push_back(getenv("TMPDIR"));
if (getenv("TMP") && strlen(getenv("TMP")) != 0)
result.push_back(getenv("TMP"));
if (getenv("TEMP") && strlen(getenv("TEMP")) != 0)
result.push_back(getenv("TEMP"));
return result;
default:
return result;
}
}
int System_GetPropertyInt(SystemProperty prop) {
switch (prop) {
case SYSPROP_AUDIO_SAMPLE_RATE:
return g_retFmt.freq;
case SYSPROP_AUDIO_FRAMES_PER_BUFFER:
return g_retFmt.samples;
case SYSPROP_DEVICE_TYPE:
#if defined(MOBILE_DEVICE)
return DEVICE_TYPE_MOBILE;
#else
return DEVICE_TYPE_DESKTOP;
#endif
case SYSPROP_DISPLAY_COUNT:
return SDL_GetNumVideoDisplays();
case SYSPROP_KEYBOARD_LAYOUT:
{
char q, w, y;
q = SDL_GetKeyFromScancode(SDL_SCANCODE_Q);
w = SDL_GetKeyFromScancode(SDL_SCANCODE_W);
y = SDL_GetKeyFromScancode(SDL_SCANCODE_Y);
if (q == 'a' && w == 'z' && y == 'y')
return KEYBOARD_LAYOUT_AZERTY;
else if (q == 'q' && w == 'w' && y == 'z')
return KEYBOARD_LAYOUT_QWERTZ;
return KEYBOARD_LAYOUT_QWERTY;
}
case SYSPROP_DISPLAY_XRES:
return g_DesktopWidth;
case SYSPROP_DISPLAY_YRES:
return g_DesktopHeight;
default:
return -1;
}
}
float System_GetPropertyFloat(SystemProperty prop) {
switch (prop) {
case SYSPROP_DISPLAY_REFRESH_RATE:
return g_RefreshRate;
case SYSPROP_DISPLAY_DPI:
return (g_ForcedDPI == 0.0f ? g_DesktopDPI : g_ForcedDPI) * 96.0;
case SYSPROP_DISPLAY_SAFE_INSET_LEFT:
case SYSPROP_DISPLAY_SAFE_INSET_RIGHT:
case SYSPROP_DISPLAY_SAFE_INSET_TOP:
case SYSPROP_DISPLAY_SAFE_INSET_BOTTOM:
return 0.0f;
default:
return -1;
}
}
bool System_GetPropertyBool(SystemProperty prop) {
switch (prop) {
case SYSPROP_CAN_SHOW_FILE:
#if PPSSPP_PLATFORM(WINDOWS) || PPSSPP_PLATFORM(MAC) || (PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID))
return true;
#else
return false;
#endif
case SYSPROP_HAS_OPEN_DIRECTORY:
#if PPSSPP_PLATFORM(WINDOWS)
return true;
#elif PPSSPP_PLATFORM(MAC) || (PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID))
return true;
#endif
case SYSPROP_HAS_BACK_BUTTON:
return true;
#if PPSSPP_PLATFORM(SWITCH)
case SYSPROP_HAS_TEXT_INPUT_DIALOG:
return __nx_applet_type == AppletType_Application || __nx_applet_type != AppletType_SystemApplication;
case SYSPROP_HAS_KEYBOARD:
return true;
#endif
case SYSPROP_APP_GOLD:
#ifdef GOLD
return true;
#else
return false;
#endif
case SYSPROP_CAN_JIT:
return true;
case SYSPROP_SUPPORTS_OPEN_FILE_IN_EDITOR:
return true; // FileUtil.cpp: OpenFileInEditor
#ifndef HTTPS_NOT_AVAILABLE
case SYSPROP_SUPPORTS_HTTPS:
return !g_Config.bDisableHTTPS;
#endif
#if PPSSPP_PLATFORM(MAC)
case SYSPROP_HAS_FOLDER_BROWSER:
case SYSPROP_HAS_FILE_BROWSER:
return true;
#endif
default:
return false;
}
}
void System_Notify(SystemNotification notification) {
switch (notification) {
case SystemNotification::AUDIO_RESET_DEVICE:
StopSDLAudioDevice();
InitSDLAudioDevice();
break;
default:
break;
}
}
// returns -1 on failure
static int parseInt(const char *str) {
int val;
int retval = sscanf(str, "%d", &val);
printf("%i = scanf %s\n", retval, str);
if (retval != 1) {
return -1;
} else {
return val;
}
}
static float parseFloat(const char *str) {
float val;
int retval = sscanf(str, "%f", &val);
printf("%i = sscanf %s\n", retval, str);
if (retval != 1) {
return -1.0f;
} else {
return val;
}
}
void UpdateWindowState(SDL_Window *window) {
SDL_SetWindowTitle(window, g_windowState.title.c_str());
if (g_windowState.toggleFullScreenNextFrame) {
g_windowState.toggleFullScreenNextFrame = false;
Uint32 window_flags = SDL_GetWindowFlags(window);
if (g_windowState.toggleFullScreenType == -1) {
window_flags ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
} else if (g_windowState.toggleFullScreenType == 1) {
window_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
} else {
window_flags &= ~SDL_WINDOW_FULLSCREEN_DESKTOP;
}
SDL_SetWindowFullscreen(window, window_flags);
}
if (g_windowState.clipboardDataAvailable) {
SDL_SetClipboardText(g_windowState.clipboardString.c_str());
g_windowState.clipboardDataAvailable = false;
g_windowState.clipboardString.clear();
}
g_windowState.update = false;
}
enum class EmuThreadState {
DISABLED,
START_REQUESTED,
RUNNING,
QUIT_REQUESTED,
STOPPED,
};
static std::thread emuThread;
static std::atomic<int> emuThreadState((int)EmuThreadState::DISABLED);
static void EmuThreadFunc(GraphicsContext *graphicsContext) {
SetCurrentThreadName("Emu");
// There's no real requirement that NativeInit happen on this thread.
// We just call the update/render loop here.
emuThreadState = (int)EmuThreadState::RUNNING;
NativeInitGraphics(graphicsContext);
while (emuThreadState != (int)EmuThreadState::QUIT_REQUESTED) {
double startTime = time_now_d();
UpdateRunLoop(graphicsContext);
// Simple throttling to not burn the GPU in the menu.
if (GetUIState() != UISTATE_INGAME || !PSP_IsInited()) {
double diffTime = time_now_d() - startTime;
int sleepTime = (int)(1000.0 / 60.0) - (int)(diffTime * 1000.0);
if (sleepTime > 0)
sleep_ms(sleepTime);
}
}
emuThreadState = (int)EmuThreadState::STOPPED;
graphicsContext->StopThread();
NativeShutdownGraphics();
}
static void EmuThreadStart(GraphicsContext *context) {
emuThreadState = (int)EmuThreadState::START_REQUESTED;
emuThread = std::thread(&EmuThreadFunc, context);
}
static void EmuThreadStop(const char *reason) {
emuThreadState = (int)EmuThreadState::QUIT_REQUESTED;
}
static void EmuThreadJoin() {
emuThread.join();
emuThread = std::thread();
}
struct InputStateTracker {
void TranslateMouseWheel() {
// SDL2 doesn't consider the mousewheel a button anymore
// so let's send the KEY_UP if it was moved after some frames
if (mouseWheelMovedUpFrames > 0) {
mouseWheelMovedUpFrames--;
if (mouseWheelMovedUpFrames == 0) {
KeyInput key;
key.deviceId = DEVICE_ID_MOUSE;
key.keyCode = NKCODE_EXT_MOUSEWHEEL_UP;
key.flags = KEY_UP;
NativeKey(key);
}
}
if (mouseWheelMovedDownFrames > 0) {
mouseWheelMovedDownFrames--;
if (mouseWheelMovedDownFrames == 0) {
KeyInput key;
key.deviceId = DEVICE_ID_MOUSE;
key.keyCode = NKCODE_EXT_MOUSEWHEEL_DOWN;
key.flags = KEY_UP;
NativeKey(key);
}
}
}
void MouseControl() {
// Disabled by default, needs a workaround to map to psp keys.
if (g_Config.bMouseControl) {
float scaleFactor_x = g_display.dpi_scale_x * 0.1 * g_Config.fMouseSensitivity;
float scaleFactor_y = g_display.dpi_scale_y * 0.1 * g_Config.fMouseSensitivity;
AxisInput axis[2];
axis[0].axisId = JOYSTICK_AXIS_MOUSE_REL_X;
axis[0].deviceId = DEVICE_ID_MOUSE;
axis[0].value = std::max(-1.0f, std::min(1.0f, mouseDeltaX * scaleFactor_x));
axis[1].axisId = JOYSTICK_AXIS_MOUSE_REL_Y;
axis[1].deviceId = DEVICE_ID_MOUSE;
axis[1].value = std::max(-1.0f, std::min(1.0f, mouseDeltaY * scaleFactor_y));
if (GetUIState() == UISTATE_INGAME || g_Config.bMapMouse) {
NativeAxis(axis, 2);
}
mouseDeltaX *= g_Config.fMouseSmoothing;
mouseDeltaY *= g_Config.fMouseSmoothing;
}
}
void MouseCaptureControl() {
bool captureMouseCondition = g_Config.bMouseControl && ((GetUIState() == UISTATE_INGAME && g_Config.bMouseConfine) || g_Config.bMapMouse);
if (mouseCaptured != captureMouseCondition) {
mouseCaptured = captureMouseCondition;
if (captureMouseCondition)
SDL_SetRelativeMouseMode(SDL_TRUE);
else
SDL_SetRelativeMouseMode(SDL_FALSE);
}
}
bool mouseDown;
float mouseDeltaX;
float mouseDeltaY;
int mouseWheelMovedUpFrames;
int mouseWheelMovedDownFrames;
bool mouseCaptured;
};
static void ProcessSDLEvent(SDL_Window *window, const SDL_Event &event, InputStateTracker *inputTracker) {
// We have to juggle around 3 kinds of "DPI spaces" if a logical DPI is
// provided (through --dpi, it is equal to system DPI if unspecified):
// - SDL gives us motion events in "system DPI" points
// - UpdateScreenScale expects pixels, so in a way "96 DPI" points
// - The UI code expects motion events in "logical DPI" points
float mx = event.motion.x * g_DesktopDPI * g_display.dpi_scale_x;
float my = event.motion.y * g_DesktopDPI * g_display.dpi_scale_x;
switch (event.type) {
case SDL_QUIT:
g_QuitRequested = 1;
break;
#if !defined(MOBILE_DEVICE)
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_SIZE_CHANGED: // better than RESIZED, more general
{
int new_width = event.window.data1;
int new_height = event.window.data2;
// The size given by SDL is in point-units, convert these to
// pixels before passing to UpdateScreenScale()
int new_width_px = new_width * g_DesktopDPI;
int new_height_px = new_height * g_DesktopDPI;
Core_NotifyWindowHidden(false);
Uint32 window_flags = SDL_GetWindowFlags(window);
bool fullscreen = (window_flags & SDL_WINDOW_FULLSCREEN);
// This one calls NativeResized if the size changed.
UpdateScreenScale(new_width_px, new_height_px);
// Set variable here in case fullscreen was toggled by hotkey
if (g_Config.UseFullScreen() != fullscreen) {
g_Config.bFullScreen = fullscreen;
g_Config.iForceFullScreen = -1;
} else {
// It is possible for the monitor to change DPI, so recalculate
// DPI on each resize event.
UpdateScreenDPI(window);
}
if (!g_Config.bFullScreen) {
g_Config.iWindowWidth = new_width;
g_Config.iWindowHeight = new_height;
}
// Hide/Show cursor correctly toggling fullscreen
if (lastUIState == UISTATE_INGAME && fullscreen && !g_Config.bShowTouchControls) {
SDL_ShowCursor(SDL_DISABLE);
} else if (lastUIState != UISTATE_INGAME || !fullscreen) {
SDL_ShowCursor(SDL_ENABLE);
}
break;
}
case SDL_WINDOWEVENT_MOVED:
{
Uint32 window_flags = SDL_GetWindowFlags(window);
bool fullscreen = (window_flags & SDL_WINDOW_FULLSCREEN);
if (!fullscreen) {
g_Config.iWindowX = (int)event.window.data1;
g_Config.iWindowY = (int)event.window.data2;
}
break;
}
case SDL_WINDOWEVENT_MINIMIZED:
case SDL_WINDOWEVENT_HIDDEN:
Core_NotifyWindowHidden(true);
break;
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SHOWN:
Core_NotifyWindowHidden(false);
break;
default:
break;
}
break;
#endif
case SDL_KEYDOWN:
{
if (event.key.repeat > 0) { break;}
int k = event.key.keysym.sym;
KeyInput key;
key.flags = KEY_DOWN;
auto mapped = KeyMapRawSDLtoNative.find(k);
if (mapped == KeyMapRawSDLtoNative.end() || mapped->second == NKCODE_UNKNOWN) {
break;
}
key.keyCode = mapped->second;
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
#ifdef _DEBUG
if (k == SDLK_F7) {
printf("f7 pressed - rebooting emuthread\n");
g_rebootEmuThread = true;
}
#endif
// Convenience subset of what
// "Enable standard shortcut keys"
// does on Windows.
if(g_Config.bSystemControls) {
bool ctrl = bool(event.key.keysym.mod & KMOD_CTRL);
if (ctrl && (k == SDLK_w))
{
if (Core_IsStepping())
Core_EnableStepping(false);
Core_Stop();
System_PostUIMessage(UIMessage::REQUEST_GAME_STOP);
// NOTE: Unlike Windows version, this
// does not need Core_WaitInactive();
// since SDL does not have a separate
// UI thread.
}
if (ctrl && (k == SDLK_b))
{
System_PostUIMessage(UIMessage::REQUEST_GAME_RESET);
Core_EnableStepping(false);
}
}
break;
}
case SDL_KEYUP:
{
if (event.key.repeat > 0) { break;}
int k = event.key.keysym.sym;
KeyInput key;
key.flags = KEY_UP;
auto mapped = KeyMapRawSDLtoNative.find(k);
if (mapped == KeyMapRawSDLtoNative.end() || mapped->second == NKCODE_UNKNOWN) {
break;
}
key.keyCode = mapped->second;
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
break;
}
case SDL_TEXTINPUT:
{
int pos = 0;
int c = u8_nextchar(event.text.text, &pos);
KeyInput key;
key.flags = KEY_CHAR;
key.unicodeChar = c;
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
break;
}
// This behavior doesn't feel right on a macbook with a touchpad.
#if !PPSSPP_PLATFORM(MAC)
case SDL_FINGERMOTION:
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
TouchInput input;
input.id = event.tfinger.fingerId;
input.x = event.tfinger.x * w * g_DesktopDPI * g_display.dpi_scale_x;
input.y = event.tfinger.y * h * g_DesktopDPI * g_display.dpi_scale_x;
input.flags = TOUCH_MOVE;
input.timestamp = event.tfinger.timestamp;
NativeTouch(input);
break;
}
case SDL_FINGERDOWN:
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
TouchInput input;
input.id = event.tfinger.fingerId;
input.x = event.tfinger.x * w * g_DesktopDPI * g_display.dpi_scale_x;
input.y = event.tfinger.y * h * g_DesktopDPI * g_display.dpi_scale_x;
input.flags = TOUCH_DOWN;
input.timestamp = event.tfinger.timestamp;
NativeTouch(input);
KeyInput key;
key.deviceId = DEVICE_ID_MOUSE;
key.keyCode = NKCODE_EXT_MOUSEBUTTON_1;
key.flags = KEY_DOWN;
NativeKey(key);
break;
}
case SDL_FINGERUP:
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
TouchInput input;
input.id = event.tfinger.fingerId;
input.x = event.tfinger.x * w * g_DesktopDPI * g_display.dpi_scale_x;
input.y = event.tfinger.y * h * g_DesktopDPI * g_display.dpi_scale_x;
input.flags = TOUCH_UP;
input.timestamp = event.tfinger.timestamp;
NativeTouch(input);
KeyInput key;
key.deviceId = DEVICE_ID_MOUSE;
key.keyCode = NKCODE_EXT_MOUSEBUTTON_1;
key.flags = KEY_UP;
NativeKey(key);
break;
}
#endif
case SDL_MOUSEBUTTONDOWN:
switch (event.button.button) {
case SDL_BUTTON_LEFT:
{
inputTracker->mouseDown = true;
TouchInput input{};
input.x = mx;
input.y = my;
input.flags = TOUCH_DOWN | TOUCH_MOUSE;
input.id = 0;
NativeTouch(input);
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_1, KEY_DOWN);
NativeKey(key);
}
break;
case SDL_BUTTON_RIGHT:
{
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_2, KEY_DOWN);
NativeKey(key);
}
break;
case SDL_BUTTON_MIDDLE:
{
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_3, KEY_DOWN);
NativeKey(key);
}
break;
case SDL_BUTTON_X1:
{
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_4, KEY_DOWN);
NativeKey(key);
}
break;
case SDL_BUTTON_X2:
{
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_5, KEY_DOWN);
NativeKey(key);
}
break;
}
break;
case SDL_MOUSEWHEEL:
{
KeyInput key;
key.deviceId = DEVICE_ID_MOUSE;
key.flags = KEY_DOWN;
#if SDL_VERSION_ATLEAST(2, 0, 18)
if (event.wheel.preciseY != 0.0f) {
// Should the scale be DPI-driven?
const float scale = 30.0f;
key.keyCode = event.wheel.preciseY > 0 ? NKCODE_EXT_MOUSEWHEEL_UP : NKCODE_EXT_MOUSEWHEEL_DOWN;
key.flags |= KEY_HASWHEELDELTA;
int wheelDelta = event.wheel.preciseY * scale;
if (event.wheel.preciseY < 0) {
wheelDelta = -wheelDelta;
}
key.flags |= wheelDelta << 16;
NativeKey(key);
break;
}
#endif
if (event.wheel.y > 0) {
key.keyCode = NKCODE_EXT_MOUSEWHEEL_UP;
inputTracker->mouseWheelMovedUpFrames = 5;
NativeKey(key);
} else if (event.wheel.y < 0) {
key.keyCode = NKCODE_EXT_MOUSEWHEEL_DOWN;
inputTracker->mouseWheelMovedDownFrames = 5;
NativeKey(key);
}
break;
}
case SDL_MOUSEMOTION:
if (inputTracker->mouseDown) {
TouchInput input{};
input.x = mx;
input.y = my;
input.flags = TOUCH_MOVE | TOUCH_MOUSE;
input.id = 0;
NativeTouch(input);
}
inputTracker->mouseDeltaX += event.motion.xrel;
inputTracker->mouseDeltaY += event.motion.yrel;
break;
case SDL_MOUSEBUTTONUP:
switch (event.button.button) {
case SDL_BUTTON_LEFT:
{
inputTracker->mouseDown = false;
TouchInput input{};
input.x = mx;
input.y = my;
input.flags = TOUCH_UP | TOUCH_MOUSE;
input.id = 0;
NativeTouch(input);
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_1, KEY_UP);
NativeKey(key);
}
break;
case SDL_BUTTON_RIGHT:
{
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_2, KEY_UP);
NativeKey(key);
}
break;
case SDL_BUTTON_MIDDLE:
{
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_3, KEY_UP);
NativeKey(key);
}
break;
case SDL_BUTTON_X1:
{
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_4, KEY_UP);
NativeKey(key);
}
break;
case SDL_BUTTON_X2:
{
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_5, KEY_UP);
NativeKey(key);
}
break;
}
break;
#if SDL_VERSION_ATLEAST(2, 0, 4)
case SDL_AUDIODEVICEADDED:
// Automatically switch to the new device.
if (event.adevice.iscapture == 0) {
const char *name = SDL_GetAudioDeviceName(event.adevice.which, 0);
if (!name) {
break;
}
// Don't start auto switching for a second, because some devices init on start.
bool doAutoSwitch = g_Config.bAutoAudioDevice && time_now_d() > 1.0f;
if (doAutoSwitch || g_Config.sAudioDevice == name) {
StopSDLAudioDevice();
InitSDLAudioDevice(name ? name : "");
}
}
break;
case SDL_AUDIODEVICEREMOVED:
if (event.adevice.iscapture == 0 && event.adevice.which == audioDev) {
StopSDLAudioDevice();
InitSDLAudioDevice();
}
break;
#endif
default:
if (joystick) {
joystick->ProcessInput(event);
}
break;
}
}
void UpdateSDLCursor() {
#if !defined(MOBILE_DEVICE)
if (lastUIState != GetUIState()) {
lastUIState = GetUIState();
if (lastUIState == UISTATE_INGAME && g_Config.UseFullScreen() && !g_Config.bShowTouchControls)
SDL_ShowCursor(SDL_DISABLE);
if (lastUIState != UISTATE_INGAME || !g_Config.UseFullScreen())
SDL_ShowCursor(SDL_ENABLE);
}
#endif
}
#ifdef _WIN32
#undef main
#endif
int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--version")) {
printf("%s\n", PPSSPP_GIT_VERSION);
return 0;
}
}
#ifdef HAVE_LIBNX
socketInitializeDefault();
nxlinkStdio();
#else // HAVE_LIBNX
// Ignore sigpipe.
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
perror("Unable to ignore SIGPIPE");
}
#endif // HAVE_LIBNX
PROFILE_INIT();
glslang::InitializeProcess();
#if PPSSPP_PLATFORM(RPI)
bcm_host_init();
#endif
putenv((char*)"SDL_VIDEO_CENTERED=1");
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
#ifdef SDL_HINT_TOUCH_MOUSE_EVENTS
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
#endif
if (VulkanMayBeAvailable()) {
printf("DEBUG: Vulkan might be available.\n");
} else {
printf("DEBUG: Vulkan is not available, not using Vulkan.\n");
}
SDL_version compiled;
SDL_version linked;
int set_xres = -1;
int set_yres = -1;
bool portrait = false;
bool set_ipad = false;
float set_dpi = 0.0f;
float set_scale = 1.0f;
// Produce a new set of arguments with the ones we skip.
int remain_argc = 1;
const char *remain_argv[256] = { argv[0] };
Uint32 mode = 0;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i],"--fullscreen")) {
mode |= SDL_WINDOW_FULLSCREEN_DESKTOP;
g_Config.iForceFullScreen = 1;
} else if (set_xres == -2)
set_xres = parseInt(argv[i]);
else if (set_yres == -2)
set_yres = parseInt(argv[i]);
else if (set_dpi == -2)
set_dpi = parseFloat(argv[i]);
else if (set_scale == -2)
set_scale = parseFloat(argv[i]);
else if (!strcmp(argv[i],"--xres"))
set_xres = -2;
else if (!strcmp(argv[i],"--yres"))
set_yres = -2;
else if (!strcmp(argv[i],"--dpi"))
set_dpi = -2;
else if (!strcmp(argv[i],"--scale"))
set_scale = -2;
else if (!strcmp(argv[i],"--ipad"))
set_ipad = true;
else if (!strcmp(argv[i],"--portrait"))
portrait = true;
else {
remain_argv[remain_argc++] = argv[i];
}
}
std::string app_name;
std::string app_name_nice;
std::string version;
bool landscape;
NativeGetAppInfo(&app_name, &app_name_nice, &landscape, &version);
bool joystick_enabled = true;
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO) < 0) {
fprintf(stderr, "Failed to initialize SDL with joystick support. Retrying without.\n");
joystick_enabled = false;
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
fprintf(stderr, "Unable to initialize SDL: %s\n", SDL_GetError());
return 1;
}
}
SDL_VERSION(&compiled);
SDL_GetVersion(&linked);
printf("Info: We compiled against SDL version %d.%d.%d", compiled.major, compiled.minor, compiled.patch);
if (compiled.minor != linked.minor || compiled.patch != linked.patch) {
printf(", but we are linking against SDL version %d.%d.%d., be aware that this can lead to unexpected behaviors\n", linked.major, linked.minor, linked.patch);
} else {
printf(" and we are linking against SDL version %d.%d.%d. :)\n", linked.major, linked.minor, linked.patch);
}
// Get the video info before doing anything else, so we don't get skewed resolution results.
// TODO: support multiple displays correctly
SDL_DisplayMode displayMode;
int should_be_zero = SDL_GetCurrentDisplayMode(0, &displayMode);
if (should_be_zero != 0) {
fprintf(stderr, "Could not get display mode: %s\n", SDL_GetError());
return 1;
}
g_DesktopWidth = displayMode.w;
g_DesktopHeight = displayMode.h;
g_RefreshRate = displayMode.refresh_rate;
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
// Force fullscreen if the resolution is too low to run windowed.
if (g_DesktopWidth < 480 * 2 && g_DesktopHeight < 272 * 2) {
mode |= SDL_WINDOW_FULLSCREEN_DESKTOP;
}
// If we're on mobile, don't try for windowed either.
#if defined(MOBILE_DEVICE) && !PPSSPP_PLATFORM(SWITCH)
mode |= SDL_WINDOW_FULLSCREEN;
#elif defined(USING_FBDEV) || PPSSPP_PLATFORM(SWITCH)
mode |= SDL_WINDOW_FULLSCREEN_DESKTOP;
#else
mode |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
#endif
if (mode & SDL_WINDOW_FULLSCREEN_DESKTOP) {
g_display.pixel_xres = g_DesktopWidth;
g_display.pixel_yres = g_DesktopHeight;
if (g_Config.iForceFullScreen == -1)
g_Config.bFullScreen = true;
} else {
// set a sensible default resolution (2x)
g_display.pixel_xres = 480 * 2 * set_scale;
g_display.pixel_yres = 272 * 2 * set_scale;
if (portrait) {
std::swap(g_display.pixel_xres, g_display.pixel_yres);
}
if (g_Config.iForceFullScreen == -1)
g_Config.bFullScreen = false;
}
if (set_ipad) {
g_display.pixel_xres = 1024;
g_display.pixel_yres = 768;
}
if (!landscape) {
std::swap(g_display.pixel_xres, g_display.pixel_yres);
}
if (set_xres > 0) {
g_display.pixel_xres = set_xres;
}
if (set_yres > 0) {
g_display.pixel_yres = set_yres;
}
if (set_dpi > 0) {
g_ForcedDPI = set_dpi;
}
// Mac / Linux
char path[2048];
#if PPSSPP_PLATFORM(SWITCH)
strcpy(path, "/switch/ppsspp/");
#else
const char *the_path = getenv("HOME");
if (!the_path) {
struct passwd *pwd = getpwuid(getuid());
if (pwd)
the_path = pwd->pw_dir;
}
if (the_path)
strcpy(path, the_path);
#endif
if (strlen(path) > 0 && path[strlen(path) - 1] != '/')
strcat(path, "/");
#if PPSSPP_PLATFORM(MAC)
std::string external_dir_str;
if (SDL_GetBasePath())
external_dir_str = std::string(SDL_GetBasePath()) + "/assets";
else
external_dir_str = "/tmp";
const char *external_dir = external_dir_str.c_str();
#else
const char *external_dir = "/tmp";
#endif
NativeInit(remain_argc, (const char **)remain_argv, path, external_dir, nullptr);
// Use the setting from the config when initing the window.
if (g_Config.UseFullScreen())
mode |= SDL_WINDOW_FULLSCREEN_DESKTOP;
int x = SDL_WINDOWPOS_UNDEFINED_DISPLAY(getDisplayNumber());
int y = SDL_WINDOWPOS_UNDEFINED;
int w = g_display.pixel_xres;
int h = g_display.pixel_yres;
if (!g_Config.bFullScreen) {
if (g_Config.iWindowX != -1)
x = g_Config.iWindowX;
if (g_Config.iWindowY != -1)
y = g_Config.iWindowY;
if (g_Config.iWindowWidth > 0)
w = g_Config.iWindowWidth;
if (g_Config.iWindowHeight > 0)
h = g_Config.iWindowHeight;
}
GraphicsContext *graphicsContext = nullptr;
SDL_Window *window = nullptr;
std::string error_message;
if (g_Config.iGPUBackend == (int)GPUBackend::OPENGL) {
SDLGLGraphicsContext *ctx = new SDLGLGraphicsContext();
if (ctx->Init(window, x, y, w, h, mode, &error_message) != 0) {
printf("GL init error '%s'\n", error_message.c_str());
}
graphicsContext = ctx;
#if !PPSSPP_PLATFORM(SWITCH)
} else if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) {
SDLVulkanGraphicsContext *ctx = new SDLVulkanGraphicsContext();
if (!ctx->Init(window, x, y, w, h, mode | SDL_WINDOW_VULKAN, &error_message)) {
printf("Vulkan init error '%s' - falling back to GL\n", error_message.c_str());
g_Config.iGPUBackend = (int)GPUBackend::OPENGL;
SetGPUBackend((GPUBackend)g_Config.iGPUBackend);
delete ctx;
SDLGLGraphicsContext *glctx = new SDLGLGraphicsContext();
glctx->Init(window, x, y, w, h, mode, &error_message);
graphicsContext = glctx;
} else {
graphicsContext = ctx;
}
#endif
}
#if PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS)
if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) {
g_ForcedDPI = 1.0f;
}
#endif
UpdateScreenDPI(window);
float dpi_scale = 1.0f / (g_ForcedDPI == 0.0f ? g_DesktopDPI : g_ForcedDPI);
UpdateScreenScale(w * g_DesktopDPI, h * g_DesktopDPI);
bool mainThreadIsRender = g_Config.iGPUBackend == (int)GPUBackend::OPENGL;
SDL_SetWindowTitle(window, (app_name_nice + " " + PPSSPP_GIT_VERSION).c_str());
char iconPath[PATH_MAX];
#if defined(ASSETS_DIR)
snprintf(iconPath, PATH_MAX, "%sicon_regular_72.png", ASSETS_DIR);
if (access(iconPath, F_OK) != 0)
snprintf(iconPath, PATH_MAX, "%sassets/icon_regular_72.png", SDL_GetBasePath() ? SDL_GetBasePath() : "");
#else
snprintf(iconPath, PATH_MAX, "%sassets/icon_regular_72.png", SDL_GetBasePath() ? SDL_GetBasePath() : "");
#endif
int width = 0, height = 0;
unsigned char *imageData;
if (pngLoad(iconPath, &width, &height, &imageData) == 1) {
SDL_Surface *surface = SDL_CreateRGBSurface(0, width, height, 32,
0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000);
memcpy(surface->pixels, imageData, width*height*4);
SDL_SetWindowIcon(window, surface);
SDL_FreeSurface(surface);
free(imageData);
imageData = NULL;
}
// Since we render from the main thread, there's nothing done here, but we call it to avoid confusion.
if (!graphicsContext->InitFromRenderThread(&error_message)) {
printf("Init from thread error: '%s'\n", error_message.c_str());
}
#ifdef MOBILE_DEVICE
SDL_ShowCursor(SDL_DISABLE);
#endif
// Ensure that the swap interval is set after context creation (needed for kmsdrm)
SDL_GL_SetSwapInterval(1);
InitSDLAudioDevice();
if (joystick_enabled) {
joystick = new SDLJoystick();
} else {
joystick = nullptr;
}
EnableFZ();
EmuThreadStart(graphicsContext);
graphicsContext->ThreadStart();
InputStateTracker inputTracker{};
#if PPSSPP_PLATFORM(MAC)
// setup menu items for macOS
initializeOSXExtras();
#endif
bool waitOnExit = g_Config.iGPUBackend == (int)GPUBackend::OPENGL;
if (!mainThreadIsRender) {
// We should only be a message pump
while (true) {
inputTracker.TranslateMouseWheel();
SDL_Event event;
while (SDL_PollEvent(&event)) {
ProcessSDLEvent(window, event, &inputTracker);
}
if (g_QuitRequested || g_RestartRequested)
break;
UpdateSDLCursor();
inputTracker.MouseControl();
inputTracker.MouseCaptureControl();
{
std::lock_guard<std::mutex> guard(g_mutexWindow);
if (g_windowState.update) {
UpdateWindowState(window);
}
}
}
} else while (true) {
inputTracker.TranslateMouseWheel();
{
SDL_Event event;
while (SDL_PollEvent(&event)) {
ProcessSDLEvent(window, event, &inputTracker);
}
}
if (g_QuitRequested || g_RestartRequested)
break;
if (emuThreadState == (int)EmuThreadState::DISABLED) {
UpdateRunLoop(graphicsContext);
}
if (g_QuitRequested || g_RestartRequested)
break;
UpdateSDLCursor();
inputTracker.MouseControl();
inputTracker.MouseCaptureControl();
bool renderThreadPaused = Core_IsWindowHidden() && g_Config.bPauseWhenMinimized && emuThreadState != (int)EmuThreadState::DISABLED;
if (emuThreadState != (int)EmuThreadState::DISABLED && !renderThreadPaused) {
if (!graphicsContext->ThreadFrame())
break;
}
{
std::lock_guard<std::mutex> guard(g_mutexWindow);
if (g_windowState.update) {
UpdateWindowState(window);
}
}
if (g_rebootEmuThread) {
printf("rebooting emu thread");
g_rebootEmuThread = false;
EmuThreadStop("shutdown");
// Skipping GL calls, the old context is gone.
while (graphicsContext->ThreadFrame()) {
INFO_LOG(SYSTEM, "graphicsContext->ThreadFrame executed to clear buffers");
}
EmuThreadJoin();
graphicsContext->ThreadEnd();
graphicsContext->ShutdownFromRenderThread();
printf("OK, shutdown complete. starting up graphics again.\n");
if (g_Config.iGPUBackend == (int)GPUBackend::OPENGL) {
SDLGLGraphicsContext *ctx = (SDLGLGraphicsContext *)graphicsContext;
if (!ctx->Init(window, x, y, w, h, mode, &error_message)) {
printf("Failed to reinit graphics.\n");
}
}
if (!graphicsContext->InitFromRenderThread(&error_message)) {
System_Toast("Graphics initialization failed. Quitting.");
return 1;
}
EmuThreadStart(graphicsContext);
graphicsContext->ThreadStart();
}
}
EmuThreadStop("shutdown");
if (waitOnExit) {
while (graphicsContext->ThreadFrame()) {
// Need to keep eating frames to allow the EmuThread to exit correctly.
continue;
}
}
EmuThreadJoin();
delete joystick;
graphicsContext->ThreadEnd();
NativeShutdown();
// Destroys Draw, which is used in NativeShutdown to shutdown.
graphicsContext->ShutdownFromRenderThread();
graphicsContext->Shutdown();
delete graphicsContext;
if (audioDev > 0) {
SDL_PauseAudioDevice(audioDev, 1);
SDL_CloseAudioDevice(audioDev);
}
SDL_Quit();
#if PPSSPP_PLATFORM(RPI)
bcm_host_deinit();
#endif
glslang::FinalizeProcess();
printf("Leaving main\n");
#ifdef HAVE_LIBNX
socketExit();
#endif
// If a restart was requested (and supported on this platform), respawn the executable.
if (g_RestartRequested) {
#if PPSSPP_PLATFORM(MAC)
RestartMacApp();
#elif PPSSPP_PLATFORM(LINUX)
// Hackery from https://unix.stackexchange.com/questions/207935/how-to-restart-or-reset-a-running-process-in-linux,
char *exec_argv[] = { argv[0], nullptr };
execv("/proc/self/exe", exec_argv);
#endif
}
return 0;
}