mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-11 01:25:07 +02:00
4413c7b10f
This option was Windows exclusive and this commit implements it on SDL as well. It is hidden on mobile devices because it makes no sense there.
1950 lines
56 KiB
C++
1950 lines
56 KiB
C++
#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"
|
|
#include "SDL2/SDL_mouse.h"
|
|
#else
|
|
#include "SDL.h"
|
|
#include "SDL_syswm.h"
|
|
#include "SDL_mouse.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 "ext/portable-file-dialogs/portable-file-dialogs.h"
|
|
|
|
#include "ext/imgui/imgui.h"
|
|
#include "ext/imgui/imgui_impl_platform.h"
|
|
#include "Common/System/Display.h"
|
|
#include "Common/System/System.h"
|
|
#include "Common/System/Request.h"
|
|
#include "Common/System/NativeApp.h"
|
|
#include "Common/Audio/AudioBackend.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"
|
|
#include "Common/Log/LogManager.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 "Common/StringUtils.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 "Core/Util/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;
|
|
|
|
static bool g_textFocusChanged;
|
|
static bool g_textFocus;
|
|
double g_audioStartTime = 0.0;
|
|
|
|
// Window state to be transferred to the main SDL thread.
|
|
static std::mutex g_mutexWindow;
|
|
struct WindowState {
|
|
std::string title;
|
|
bool applyFullScreenNextFrame;
|
|
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, userdata);
|
|
}
|
|
|
|
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 = std::max(g_Config.iSDLAudioBufferSize, 128);
|
|
fmt.callback = &sdl_mixaudio_callback;
|
|
fmt.userdata = nullptr;
|
|
|
|
std::string startDevice = name;
|
|
if (startDevice.empty()) {
|
|
startDevice = g_Config.sAudioDevice;
|
|
}
|
|
|
|
// List available audio devices before trying to open, for debugging purposes.
|
|
const int deviceCount = SDL_GetNumAudioDevices(0);
|
|
if (deviceCount > 0) {
|
|
INFO_LOG(Log::Audio, "Available audio devices:");
|
|
for (int i = 0; i < deviceCount; i++) {
|
|
const char *deviceName = SDL_GetAudioDeviceName(i, 0);
|
|
INFO_LOG(Log::Audio, " * '%s'", deviceName);
|
|
}
|
|
} else {
|
|
INFO_LOG(Log::Audio, "Failed to list audio devices: retval=%d", deviceCount);
|
|
}
|
|
|
|
audioDev = 0;
|
|
if (!startDevice.empty()) {
|
|
INFO_LOG(Log::Audio, "Opening audio device: '%s'", startDevice.c_str());
|
|
audioDev = SDL_OpenAudioDevice(startDevice.c_str(), 0, &fmt, &g_retFmt, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
|
|
if (audioDev <= 0) {
|
|
WARN_LOG(Log::Audio, "Failed to open audio device '%s'", startDevice.c_str());
|
|
}
|
|
}
|
|
if (audioDev <= 0) {
|
|
if (audioDev < 0) {
|
|
WARN_LOG(Log::Audio, "SDL: Error: '%s'. Trying the default audio device", SDL_GetError());
|
|
} else {
|
|
INFO_LOG(Log::Audio, "Opening default audio device");
|
|
}
|
|
audioDev = SDL_OpenAudioDevice(nullptr, 0, &fmt, &g_retFmt, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
|
|
}
|
|
if (audioDev <= 0) {
|
|
ERROR_LOG(Log::Audio, "Failed to open audio device '%s', second try. Giving up.", SDL_GetError());
|
|
} else {
|
|
if (g_retFmt.samples != fmt.samples) // Notify, but still use it
|
|
ERROR_LOG(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(Log::Audio, "Sound buffer format does not match requested format.");
|
|
ERROR_LOG(Log::Audio, "Output audio freq: %d (requested: %d)", g_retFmt.freq, fmt.freq);
|
|
ERROR_LOG(Log::Audio, "Output audio format: %d (requested: %d)", g_retFmt.format, fmt.format);
|
|
ERROR_LOG(Log::Audio, "Output audio channels: %d (requested: %d)", g_retFmt.channels, fmt.channels);
|
|
ERROR_LOG(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, window_height;
|
|
SDL_GetWindowSize(window, &window_width, &window_height);
|
|
|
|
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);
|
|
else {
|
|
// If we add SDL support for more platforms, we'll end up here.
|
|
g_DesktopDPI = 1.0f;
|
|
return;
|
|
}
|
|
// Round up a little otherwise there would be a gap sometimes
|
|
// in fractional scaling
|
|
g_DesktopDPI = ((float) drawable_width + 1.0f) / window_width;
|
|
}
|
|
|
|
// Simple implementations of System functions
|
|
|
|
void System_Toast(std::string_view text) {
|
|
#ifdef _WIN32
|
|
std::wstring str = ConvertUTF8ToWString(text);
|
|
MessageBox(0, str.c_str(), L"Toast!", MB_ICONINFORMATION);
|
|
#else
|
|
fprintf(stderr, "%*.s", (int)text.length(), text.data());
|
|
#endif
|
|
}
|
|
|
|
void System_ShowKeyboard() {
|
|
// Irrelevant on PC
|
|
}
|
|
|
|
void System_Vibrate(int length_ms) {
|
|
// Ignore on PC
|
|
}
|
|
|
|
AudioBackend *System_CreateAudioBackend() {
|
|
// Use legacy mechanisms.
|
|
return nullptr;
|
|
}
|
|
|
|
static void InitializeFilters(std::vector<std::string> &filters, BrowseFileType type) {
|
|
switch (type) {
|
|
case BrowseFileType::BOOTABLE:
|
|
filters.push_back("All supported file types (*.iso *.cso *.chd *.pbp *.elf *.prx *.zip *.ppdmp)");
|
|
filters.push_back("*.pbp *.elf *.iso *.cso *.chd *.prx *.zip *.ppdmp");
|
|
break;
|
|
case BrowseFileType::INI:
|
|
filters.push_back("Ini files");
|
|
filters.push_back("*.ini");
|
|
break;
|
|
case BrowseFileType::ZIP:
|
|
filters.push_back("ZIP files");
|
|
filters.push_back("*.zip");
|
|
break;
|
|
case BrowseFileType::DB:
|
|
filters.push_back("Cheat db files");
|
|
filters.push_back("*.db");
|
|
break;
|
|
case BrowseFileType::SOUND_EFFECT:
|
|
filters.push_back("Sound effect files (wav, mp3)");
|
|
filters.push_back("*.wav *.mp3");
|
|
break;
|
|
case BrowseFileType::SYMBOL_MAP:
|
|
filters.push_back("PPSSPP Symbol Map files (ppmap)");
|
|
filters.push_back("*.ppmap");
|
|
break;
|
|
case BrowseFileType::SYMBOL_MAP_NOCASH:
|
|
filters.push_back("No$ symbol Map files (sym)");
|
|
filters.push_back("*.sym");
|
|
break;
|
|
case BrowseFileType::ATRAC3:
|
|
filters.push_back("Atrac3 files (at3)");
|
|
filters.push_back("*.at3");
|
|
break;
|
|
case BrowseFileType::IMAGE:
|
|
filters.push_back("Pictures (jpg, png)");
|
|
filters.push_back("*.jpg *.png");
|
|
break;
|
|
case BrowseFileType::ANY:
|
|
break;
|
|
}
|
|
filters.push_back("All files (*.*)");
|
|
filters.push_back("*");
|
|
}
|
|
|
|
bool System_MakeRequest(SystemRequestType type, int requestId, const std::string ¶m1, const std::string ¶m2, int64_t param3, int64_t param4) {
|
|
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);
|
|
}
|
|
};
|
|
BrowseFileType fileType = (BrowseFileType)param3;
|
|
DarwinFileSystemServices::presentDirectoryPanel(callback, /* allowFiles = */ true, /* allowDirectories = */ false, fileType);
|
|
return true;
|
|
}
|
|
case SystemRequestType::BROWSE_FOR_IMAGE:
|
|
{
|
|
DarwinDirectoryPanelCallback callback = [requestId] (bool success, Path path) {
|
|
if (success) {
|
|
g_requestManager.PostSystemSuccess(requestId, path.c_str());
|
|
} else {
|
|
g_requestManager.PostSystemFailure(requestId);
|
|
}
|
|
};
|
|
BrowseFileType fileType = BrowseFileType::IMAGE;
|
|
DarwinFileSystemServices::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::presentDirectoryPanel(callback, /* allowFiles = */ false, /* allowDirectories = */ true);
|
|
return true;
|
|
}
|
|
#else
|
|
case SystemRequestType::BROWSE_FOR_IMAGE:
|
|
{
|
|
// TODO: Add non-blocking support.
|
|
const std::string &title = param1;
|
|
std::vector<std::string> filters;
|
|
InitializeFilters(filters, BrowseFileType::IMAGE);
|
|
std::vector<std::string> result = pfd::open_file(title, "", filters).result();
|
|
if (!result.empty()) {
|
|
g_requestManager.PostSystemSuccess(requestId, result[0]);
|
|
} else {
|
|
g_requestManager.PostSystemFailure(requestId);
|
|
}
|
|
return true;
|
|
}
|
|
case SystemRequestType::BROWSE_FOR_FILE:
|
|
case SystemRequestType::BROWSE_FOR_FILE_SAVE:
|
|
{
|
|
// TODO: Add non-blocking support.
|
|
const BrowseFileType browseType = (BrowseFileType)param3;
|
|
std::string initialFilename = param2;
|
|
const std::string &title = param1;
|
|
std::vector<std::string> filters;
|
|
InitializeFilters(filters, browseType);
|
|
if (type == SystemRequestType::BROWSE_FOR_FILE) {
|
|
std::vector<std::string> result = pfd::open_file(title, initialFilename, filters).result();
|
|
if (!result.empty()) {
|
|
g_requestManager.PostSystemSuccess(requestId, result[0]);
|
|
} else {
|
|
g_requestManager.PostSystemFailure(requestId);
|
|
}
|
|
} else {
|
|
std::string result = pfd::save_file(title, initialFilename, filters).result();
|
|
if (!result.empty()) {
|
|
g_requestManager.PostSystemSuccess(requestId, result);
|
|
} else {
|
|
g_requestManager.PostSystemFailure(requestId);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case SystemRequestType::BROWSE_FOR_FOLDER:
|
|
{
|
|
// TODO: Add non-blocking support.
|
|
std::string result = pfd::select_folder(param1, param2).result();
|
|
if (!result.empty()) {
|
|
g_requestManager.PostSystemSuccess(requestId, result);
|
|
} else {
|
|
g_requestManager.PostSystemFailure(requestId);
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
case SystemRequestType::APPLY_FULLSCREEN_STATE:
|
|
{
|
|
std::lock_guard<std::mutex> guard(g_mutexWindow);
|
|
g_windowState.update = true;
|
|
g_windowState.applyFullScreenNextFrame = true;
|
|
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;
|
|
}
|
|
case SystemRequestType::NOTIFY_UI_EVENT:
|
|
{
|
|
switch ((UIEventNotification)param3) {
|
|
case UIEventNotification::TEXT_GOTFOCUS:
|
|
g_textFocus = true;
|
|
g_textFocusChanged = true;
|
|
break;
|
|
case UIEventNotification::POPUP_CLOSED:
|
|
case UIEventNotification::TEXT_LOSTFOCUS:
|
|
g_textFocus = false;
|
|
g_textFocusChanged = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
case SystemRequestType::SET_KEEP_SCREEN_BRIGHT:
|
|
INFO_LOG(Log::UI, "SET_KEEP_SCREEN_BRIGHT not implemented.");
|
|
return true;
|
|
default:
|
|
INFO_LOG(Log::UI, "Unhandled system request %s", RequestTypeAsString(type));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void System_AskForPermission(SystemPermission permission) {}
|
|
PermissionStatus System_GetPermissionStatus(SystemPermission permission) { return PERMISSION_STATUS_GRANTED; }
|
|
|
|
void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) {
|
|
switch (urlType) {
|
|
case LaunchUrlType::BROWSER_URL:
|
|
case LaunchUrlType::MARKET_URL:
|
|
{
|
|
#if PPSSPP_PLATFORM(SWITCH)
|
|
Uuid uuid = { 0 };
|
|
WebWifiConfig conf;
|
|
webWifiCreate(&conf, NULL, std::string(url).c_str(), uuid, 0);
|
|
webWifiShow(&conf, NULL);
|
|
#elif defined(MOBILE_DEVICE)
|
|
INFO_LOG(Log::System, "Would have gone to %.*s but LaunchBrowser is not implemented on this platform", STR_VIEW(url));
|
|
#elif defined(_WIN32)
|
|
std::wstring wurl = ConvertUTF8ToWString(url);
|
|
ShellExecute(NULL, L"open", wurl.c_str(), NULL, NULL, SW_SHOWNORMAL);
|
|
#elif defined(__APPLE__)
|
|
OSXOpenURL(std::string(url).c_str());
|
|
#else
|
|
std::string command = join("xdg-open ", url);
|
|
int err = system(command.c_str());
|
|
if (err) {
|
|
INFO_LOG(Log::System, "Would have gone to %.*s but xdg-utils seems not to be installed", STR_VIEW(url));
|
|
}
|
|
#endif
|
|
break;
|
|
}
|
|
case LaunchUrlType::EMAIL_ADDRESS:
|
|
{
|
|
#if defined(MOBILE_DEVICE)
|
|
INFO_LOG(Log::System, "Would have opened your email client for %.*s but LaunchEmail is not implemented on this platform", STR_VIEW(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__)
|
|
OSXOpenURL(join("mailto:", url).c_str());
|
|
#else
|
|
int err = system(join("xdg-email ", url).c_str());
|
|
if (err) {
|
|
INFO_LOG(Log::System, "Would have gone to %.*s but xdg-utils seems not to be installed", STR_VIEW(url));
|
|
}
|
|
#endif
|
|
break;
|
|
}
|
|
case LaunchUrlType::LOCAL_FILE:
|
|
case LaunchUrlType::LOCAL_FOLDER:
|
|
#if defined(__APPLE__)
|
|
// If it's a folder and we're on a mac, open it in finder.
|
|
OSXShowInFinder(std::string(url).c_str());
|
|
#endif
|
|
// INFO_LOG(Log::System, "LaunchUrlType::LOCAL_FILE not implemented on this platform");
|
|
break;
|
|
default:
|
|
INFO_LOG(Log::System, "Unhandled LaunchUrlType %d", (int)urlType);
|
|
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;
|
|
}
|
|
}
|
|
|
|
#if PPSSPP_PLATFORM(MAC)
|
|
extern "C" {
|
|
int Apple_GetCurrentBatteryCapacity();
|
|
}
|
|
#endif
|
|
|
|
int64_t 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;
|
|
case SYSPROP_BATTERY_PERCENTAGE:
|
|
#if PPSSPP_PLATFORM(MAC)
|
|
// Let's keep using the old code on Mac for safety. Evaluate later if to be deleted.
|
|
return Apple_GetCurrentBatteryCapacity();
|
|
#else
|
|
{
|
|
int seconds = 0;
|
|
int percentage = 0;
|
|
SDL_GetPowerInfo(&seconds, &percentage);
|
|
return percentage;
|
|
}
|
|
#endif
|
|
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_HAS_TEXT_CLIPBOARD:
|
|
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;
|
|
#endif
|
|
case SYSPROP_HAS_KEYBOARD:
|
|
return true;
|
|
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
|
|
case SYSPROP_HAS_FOLDER_BROWSER:
|
|
case SYSPROP_HAS_FILE_BROWSER:
|
|
#if PPSSPP_PLATFORM(MAC)
|
|
return true;
|
|
#else
|
|
return pfd::settings::available();
|
|
#endif
|
|
case SYSPROP_HAS_ACCELEROMETER:
|
|
#if defined(MOBILE_DEVICE)
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
case SYSPROP_CAN_READ_BATTERY_PERCENTAGE:
|
|
return true;
|
|
case SYSPROP_ENOUGH_RAM_FOR_FULL_ISO:
|
|
#if PPSSPP_ARCH(64BIT) && !defined(MOBILE_DEVICE)
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
// hack for testing - do not commit
|
|
case SYSPROP_USE_IAP:
|
|
return false;
|
|
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);
|
|
fprintf(stderr, "%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);
|
|
fprintf(stderr, "%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.applyFullScreenNextFrame) {
|
|
g_windowState.applyFullScreenNextFrame = false;
|
|
|
|
Uint32 window_flags = SDL_GetWindowFlags(window);
|
|
if (g_Config.bFullScreen) {
|
|
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("EmuThread");
|
|
|
|
// 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) {
|
|
NativeFrame(graphicsContext);
|
|
}
|
|
emuThreadState = (int)EmuThreadState::STOPPED;
|
|
|
|
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 MouseCaptureControl() {
|
|
bool captureMouseCondition = g_Config.bMouseControl && ((GetUIState() == UISTATE_INGAME && g_Config.bMouseConfine) || g_IsMappingMouseInput);
|
|
if (mouseCaptured != captureMouseCondition) {
|
|
mouseCaptured = captureMouseCondition;
|
|
if (captureMouseCondition)
|
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
|
else
|
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
|
}
|
|
}
|
|
|
|
int mouseDown; // bitflags
|
|
bool mouseCaptured;
|
|
};
|
|
|
|
SDL_Cursor *g_builtinCursors[SDL_NUM_SYSTEM_CURSORS];
|
|
|
|
static SDL_SystemCursor GetSDLCursorFromImgui(ImGuiMouseCursor cursor) {
|
|
switch (cursor) {
|
|
case ImGuiMouseCursor_Arrow: return SDL_SYSTEM_CURSOR_ARROW; break;
|
|
case ImGuiMouseCursor_TextInput: return SDL_SYSTEM_CURSOR_IBEAM; break;
|
|
case ImGuiMouseCursor_ResizeAll: return SDL_SYSTEM_CURSOR_SIZEALL; break;
|
|
case ImGuiMouseCursor_ResizeEW: return SDL_SYSTEM_CURSOR_SIZEWE; break;
|
|
case ImGuiMouseCursor_ResizeNS: return SDL_SYSTEM_CURSOR_SIZENS; break;
|
|
case ImGuiMouseCursor_ResizeNESW: return SDL_SYSTEM_CURSOR_SIZENESW; break;
|
|
case ImGuiMouseCursor_ResizeNWSE: return SDL_SYSTEM_CURSOR_SIZENWSE; break;
|
|
case ImGuiMouseCursor_Hand: return SDL_SYSTEM_CURSOR_HAND; break;
|
|
case ImGuiMouseCursor_NotAllowed: return SDL_SYSTEM_CURSOR_NO; break;
|
|
default: return SDL_SYSTEM_CURSOR_ARROW; break;
|
|
}
|
|
}
|
|
|
|
void UpdateCursor() {
|
|
static SDL_SystemCursor curCursor = SDL_SYSTEM_CURSOR_ARROW;
|
|
auto cursor = ImGui_ImplPlatform_GetCursor();
|
|
SDL_SystemCursor sysCursor = GetSDLCursorFromImgui(cursor);
|
|
if (sysCursor != curCursor) {
|
|
curCursor = sysCursor;
|
|
if (!g_builtinCursors[(int)curCursor]) {
|
|
g_builtinCursors[(int)curCursor] = SDL_CreateSystemCursor(curCursor);
|
|
}
|
|
}
|
|
SDL_SetCursor(g_builtinCursors[(int)curCursor]);
|
|
}
|
|
|
|
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
|
|
// - Native_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 Native_UpdateScreenScale()
|
|
int new_width_px = new_width * g_DesktopDPI;
|
|
int new_height_px = new_height * g_DesktopDPI;
|
|
|
|
Native_NotifyWindowHidden(false);
|
|
|
|
Uint32 window_flags = SDL_GetWindowFlags(window);
|
|
bool fullscreen = (window_flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP));
|
|
|
|
// This one calls NativeResized if the size changed.
|
|
Native_UpdateScreenScale(new_width_px, new_height_px, UIScaleFactorToMultiplier(g_Config.iUIScaleFactor));
|
|
|
|
// Set variable here in case fullscreen was toggled by hotkey
|
|
if (g_Config.bFullScreen != fullscreen) {
|
|
g_Config.bFullScreen = fullscreen;
|
|
} 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 | SDL_WINDOW_FULLSCREEN_DESKTOP));
|
|
if (!fullscreen) {
|
|
g_Config.iWindowX = (int)event.window.data1;
|
|
g_Config.iWindowY = (int)event.window.data2;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SDL_WINDOWEVENT_FOCUS_LOST:
|
|
{
|
|
if (g_Config.bPauseOnLostFocus && GetUIState() == UISTATE_INGAME) {
|
|
Core_Break(BreakReason::UIFocus, 0);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SDL_WINDOWEVENT_FOCUS_GAINED:
|
|
{
|
|
if (Core_BreakReason() == BreakReason::UIFocus) {
|
|
Core_Resume();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SDL_WINDOWEVENT_MINIMIZED:
|
|
case SDL_WINDOWEVENT_HIDDEN:
|
|
Native_NotifyWindowHidden(true);
|
|
break;
|
|
case SDL_WINDOWEVENT_EXPOSED:
|
|
case SDL_WINDOWEVENT_SHOWN:
|
|
Native_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 = KeyInputFlags::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) {
|
|
fprintf(stderr, "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_Resume();
|
|
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.
|
|
}
|
|
|
|
/*
|
|
// TODO: Enable this?
|
|
if (k == SDLK_F11) {
|
|
#if !defined(MOBILE_DEVICE)
|
|
g_Config.bFullScreen = !g_Config.bFullScreen;
|
|
System_applyFullscreenState("");
|
|
#endif
|
|
}
|
|
*/
|
|
}
|
|
break;
|
|
}
|
|
case SDL_KEYUP:
|
|
{
|
|
if (event.key.repeat > 0) { break;}
|
|
int k = event.key.keysym.sym;
|
|
KeyInput key;
|
|
key.flags = KeyInputFlags::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, strlen(event.text.text));
|
|
KeyInput key;
|
|
key.flags = KeyInputFlags::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 = TouchInputFlags::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 = TouchInputFlags::DOWN;
|
|
input.timestamp = event.tfinger.timestamp;
|
|
NativeTouch(input);
|
|
|
|
KeyInput key{};
|
|
key.deviceId = DEVICE_ID_MOUSE;
|
|
key.keyCode = NKCODE_EXT_MOUSEBUTTON_1;
|
|
key.flags = KeyInputFlags::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 = TouchInputFlags::UP;
|
|
input.timestamp = event.tfinger.timestamp;
|
|
NativeTouch(input);
|
|
|
|
KeyInput key;
|
|
key.deviceId = DEVICE_ID_MOUSE;
|
|
key.keyCode = NKCODE_EXT_MOUSEBUTTON_1;
|
|
key.flags = KeyInputFlags::UP;
|
|
NativeKey(key);
|
|
break;
|
|
}
|
|
#endif
|
|
case SDL_MOUSEBUTTONDOWN:
|
|
switch (event.button.button) {
|
|
case SDL_BUTTON_LEFT:
|
|
{
|
|
inputTracker->mouseDown |= 1;
|
|
TouchInput input{};
|
|
input.x = mx;
|
|
input.y = my;
|
|
input.flags = TouchInputFlags::DOWN | TouchInputFlags::MOUSE;
|
|
input.buttons = 1;
|
|
input.id = 0;
|
|
NativeTouch(input);
|
|
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_1, KeyInputFlags::DOWN);
|
|
NativeKey(key);
|
|
}
|
|
break;
|
|
case SDL_BUTTON_RIGHT:
|
|
{
|
|
inputTracker->mouseDown |= 2;
|
|
TouchInput input{};
|
|
input.x = mx;
|
|
input.y = my;
|
|
input.flags = TouchInputFlags::DOWN | TouchInputFlags::MOUSE;
|
|
input.buttons = 2;
|
|
input.id = 0;
|
|
NativeTouch(input);
|
|
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_2, KeyInputFlags::DOWN);
|
|
NativeKey(key);
|
|
}
|
|
break;
|
|
case SDL_BUTTON_MIDDLE:
|
|
{
|
|
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_3, KeyInputFlags::DOWN);
|
|
NativeKey(key);
|
|
}
|
|
break;
|
|
case SDL_BUTTON_X1:
|
|
{
|
|
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_4, KeyInputFlags::DOWN);
|
|
NativeKey(key);
|
|
}
|
|
break;
|
|
case SDL_BUTTON_X2:
|
|
{
|
|
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_5, KeyInputFlags::DOWN);
|
|
NativeKey(key);
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case SDL_MOUSEWHEEL:
|
|
{
|
|
KeyInput key{};
|
|
key.deviceId = DEVICE_ID_MOUSE;
|
|
key.flags = KeyInputFlags::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 |= KeyInputFlags::HAS_WHEEL_DELTA;
|
|
int wheelDelta = event.wheel.preciseY * scale;
|
|
if (event.wheel.preciseY < 0) {
|
|
wheelDelta = -wheelDelta;
|
|
}
|
|
key.flags = (KeyInputFlags)((u32)key.flags | (wheelDelta << 16));
|
|
NativeKey(key);
|
|
break;
|
|
}
|
|
#endif
|
|
if (event.wheel.y > 0) {
|
|
key.keyCode = NKCODE_EXT_MOUSEWHEEL_UP;
|
|
NativeKey(key);
|
|
} else if (event.wheel.y < 0) {
|
|
key.keyCode = NKCODE_EXT_MOUSEWHEEL_DOWN;
|
|
NativeKey(key);
|
|
}
|
|
break;
|
|
}
|
|
case SDL_MOUSEMOTION:
|
|
{
|
|
TouchInput input{};
|
|
input.x = mx;
|
|
input.y = my;
|
|
input.flags = TouchInputFlags::MOVE | TouchInputFlags::MOUSE;
|
|
input.buttons = inputTracker->mouseDown;
|
|
input.id = 0;
|
|
NativeTouch(input);
|
|
NativeMouseDelta(event.motion.xrel, event.motion.yrel);
|
|
|
|
UpdateCursor();
|
|
break;
|
|
}
|
|
case SDL_MOUSEBUTTONUP:
|
|
switch (event.button.button) {
|
|
case SDL_BUTTON_LEFT:
|
|
{
|
|
inputTracker->mouseDown &= ~1;
|
|
TouchInput input{};
|
|
input.x = mx;
|
|
input.y = my;
|
|
input.flags = TouchInputFlags::UP | TouchInputFlags::MOUSE;
|
|
input.buttons = 1;
|
|
NativeTouch(input);
|
|
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_1, KeyInputFlags::UP);
|
|
NativeKey(key);
|
|
}
|
|
break;
|
|
case SDL_BUTTON_RIGHT:
|
|
{
|
|
inputTracker->mouseDown &= ~2;
|
|
// Right button only emits mouse move events. This is weird,
|
|
// but consistent with Windows. Needs cleanup.
|
|
TouchInput input{};
|
|
input.x = mx;
|
|
input.y = my;
|
|
input.flags = TouchInputFlags::UP | TouchInputFlags::MOUSE;
|
|
input.buttons = 2;
|
|
NativeTouch(input);
|
|
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_2, KeyInputFlags::UP);
|
|
NativeKey(key);
|
|
}
|
|
break;
|
|
case SDL_BUTTON_MIDDLE:
|
|
{
|
|
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_3, KeyInputFlags::UP);
|
|
NativeKey(key);
|
|
}
|
|
break;
|
|
case SDL_BUTTON_X1:
|
|
{
|
|
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_4, KeyInputFlags::UP);
|
|
NativeKey(key);
|
|
}
|
|
break;
|
|
case SDL_BUTTON_X2:
|
|
{
|
|
KeyInput key(DEVICE_ID_MOUSE, NKCODE_EXT_MOUSEBUTTON_5, KeyInputFlags::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) {
|
|
INFO_LOG(Log::Audio, "Got bogus new audio device notification");
|
|
break;
|
|
}
|
|
// Don't start auto switching for a couple of seconds, because some devices init on start.
|
|
bool doAutoSwitch = g_Config.bAutoSwitchAudioDevice;
|
|
if ((time_now_d() - g_audioStartTime) < 3.0) {
|
|
INFO_LOG(Log::Audio, "Ignoring new audio device: %s (current: %s)", name, g_Config.sAudioDevice.c_str());
|
|
doAutoSwitch = false;
|
|
}
|
|
if (doAutoSwitch || g_Config.sAudioDevice == name) {
|
|
StopSDLAudioDevice();
|
|
|
|
INFO_LOG(Log::Audio, "!!! Auto-switching to new audio device: '%s'", name);
|
|
|
|
InitSDLAudioDevice(name ? name : "");
|
|
}
|
|
}
|
|
break;
|
|
case SDL_AUDIODEVICEREMOVED:
|
|
if (event.adevice.iscapture == 0 && event.adevice.which == audioDev) {
|
|
StopSDLAudioDevice();
|
|
INFO_LOG(Log::Audio, "Audio device removed, reselecting");
|
|
InitSDLAudioDevice();
|
|
}
|
|
break;
|
|
#endif
|
|
|
|
default:
|
|
if (joystick) {
|
|
joystick->ProcessInput(event);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void UpdateTextFocus() {
|
|
if (g_textFocusChanged) {
|
|
INFO_LOG(Log::System, "Updating text focus: %d", g_textFocus);
|
|
if (g_textFocus) {
|
|
SDL_StartTextInput();
|
|
} else {
|
|
SDL_StopTextInput();
|
|
}
|
|
g_textFocusChanged = false;
|
|
}
|
|
}
|
|
|
|
void UpdateSDLCursor() {
|
|
#if !defined(MOBILE_DEVICE)
|
|
if (lastUIState != GetUIState()) {
|
|
lastUIState = GetUIState();
|
|
if (lastUIState == UISTATE_INGAME && g_Config.bFullScreen && !g_Config.bShowTouchControls)
|
|
SDL_ShowCursor(SDL_DISABLE);
|
|
if (lastUIState != UISTATE_INGAME || !g_Config.bFullScreen)
|
|
SDL_ShowCursor(SDL_ENABLE);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static int printUsage(const char *progname)
|
|
{
|
|
// NOTE: by convention, --help outputs to stdout,
|
|
// not to stderr, since it is intended output in this
|
|
// case (usage printed under different circumstances,
|
|
// say in response to error during parsing commandline,
|
|
// may go to stderr).
|
|
FILE *dst = stdout;
|
|
|
|
// NOTE: wording largely taken from
|
|
// https://www.ppsspp.org/docs/reference/command-line/
|
|
fprintf(dst, "PPSSPP - a PSP emulator (SDL build)\n");
|
|
fprintf(dst, "Usage: %s [options] [FILE]\n\n", progname);
|
|
fprintf(dst, "Launches FILE (e.g. ISO image) if present.\n");
|
|
fprintf(dst, "Options (some of these are specific to SDL backend):\n");
|
|
fprintf(dst, " -h, --help show this message and exit\n");
|
|
fprintf(dst, " --version show version information and exit\n");
|
|
|
|
fprintf(dst, " -d set the log level to debug\n");
|
|
fprintf(dst, " -v set the log level to verbose\n");
|
|
fprintf(dst, " --loglevel=INTEGER set the log level to specified value\n");
|
|
fprintf(dst, " --log=FILE output log to FILE\n");
|
|
fprintf(dst, " --state=FILE load state from FILE\n");
|
|
|
|
fprintf(dst, " -i use the interpreter\n");
|
|
fprintf(dst, " -r use IR interpreter\n");
|
|
fprintf(dst, " -j use JIT\n");
|
|
fprintf(dst, " -J use IR JIT\n");
|
|
|
|
fprintf(dst, " --fullscreen force full screen mode, ignoring saved configuration\n");
|
|
fprintf(dst, " --windowed force windowed mode, ignoring saved configuration\n");
|
|
fprintf(dst, " --xres PIXELS set X resolution\n");
|
|
fprintf(dst, " --yres PIXELS set Y resolution\n");
|
|
fprintf(dst, " --dpi FACTOR set DPI\n");
|
|
fprintf(dst, " --scale FACTOR set scale\n");
|
|
fprintf(dst, " --ipad set resolution to 1024x768\n");
|
|
fprintf(dst, " --portrait portrait mode\n");
|
|
fprintf(dst, " --graphics=BACKEND use a different gpu backend\n");
|
|
fprintf(dst, " options: gles, software, etc. (also opengl3.1, etc.)\n");
|
|
|
|
fprintf(dst, " --pause-menu-exit change \"Exit to menu\" in pause menu to \"Exit\"\n");
|
|
fprintf(dst, " --escape-exit escape key exits the application\n");
|
|
fprintf(dst, " --gamesettings go directly to settings\n");
|
|
fprintf(dst, " --touchscreentest go directly to the touchscreentest screen\n");
|
|
fprintf(dst, " --appendconfig=FILE merge config FILE into the current configuration\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
#undef main
|
|
#endif
|
|
int main(int argc, char *argv[]) {
|
|
for (int i = 1; i < argc; i++) {
|
|
if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h"))
|
|
return printUsage(argv[0]);
|
|
else if (!strcmp(argv[i], "--version")) {
|
|
printf("%s\n", PPSSPP_GIT_VERSION);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
TimeInit();
|
|
|
|
g_logManager.EnableOutput(LogOutput::Stdio);
|
|
|
|
#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
|
|
|
|
bool vulkanMayBeAvailable = false;
|
|
if (VulkanMayBeAvailable()) {
|
|
fprintf(stderr, "DEBUG: Vulkan might be available.\n");
|
|
vulkanMayBeAvailable = true;
|
|
} else {
|
|
fprintf(stderr, "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] };
|
|
|
|
// Option to force a specific OpenGL version (42="4.2",
|
|
// etc.; -1 means "try them all").
|
|
// Implemented as a workaround for https://github.com/hrydgard/ppsspp/issues/20687
|
|
// NOTE: this is currently not persistent (doesn't
|
|
// go to config), even though --graphics=openglX.Y
|
|
// also sets the GPU backend which does persist.
|
|
int force_gl_version = -1;
|
|
|
|
Uint32 mode = 0;
|
|
for (int i = 1; i < argc; i++) {
|
|
if (!strcmp(argv[i], "--fullscreen")) {
|
|
mode |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
|
g_Config.DoNotSaveSetting(&g_Config.bFullScreen);
|
|
} 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 if (!strncmp(argv[i], "--graphics=", strlen("--graphics="))) {
|
|
const char *restOfOption = argv[i] + strlen("--graphics=");
|
|
double val=-1.0; // Yes, floating point.
|
|
if (!strcmp(restOfOption, "vulkan")) {
|
|
g_Config.iGPUBackend = (int)GPUBackend::VULKAN;
|
|
g_Config.bSoftwareRendering = false;
|
|
} else if (!strcmp(restOfOption, "software")) {
|
|
// Same as on Windows, software presently implies OpenGL.
|
|
g_Config.iGPUBackend = (int)GPUBackend::OPENGL;
|
|
g_Config.bSoftwareRendering = true;
|
|
} else if (!strcmp(restOfOption, "gles") || !strcmp(restOfOption, "opengl")) {
|
|
// NOTE: OpenGL and GLES are treated the same for
|
|
// the purposes of option parsing.
|
|
g_Config.iGPUBackend = (int)GPUBackend::OPENGL;
|
|
g_Config.bSoftwareRendering = false;
|
|
} else if (sscanf(restOfOption, "gles%lg", &val) == 1 || sscanf(restOfOption, "opengl%lg", &val) == 1) {
|
|
g_Config.iGPUBackend = (int)GPUBackend::OPENGL;
|
|
g_Config.bSoftwareRendering = false;
|
|
force_gl_version = int(10.0 * val + 0.5);
|
|
}
|
|
} 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);
|
|
fprintf(stderr, "Info: We compiled against SDL version %d.%d.%d", compiled.major, compiled.minor, compiled.patch);
|
|
if (compiled.minor != linked.minor || compiled.patch != linked.patch) {
|
|
fprintf(stderr, ", 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 {
|
|
fprintf(stderr, " 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;
|
|
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);
|
|
}
|
|
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.bFullScreen) {
|
|
mode |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
|
g_display.pixel_xres = g_DesktopWidth;
|
|
g_display.pixel_yres = g_DesktopHeight;
|
|
}
|
|
|
|
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 && set_xres <= 0)
|
|
w = g_Config.iWindowWidth;
|
|
if (g_Config.iWindowHeight > 0 && set_yres <= 0)
|
|
h = g_Config.iWindowHeight;
|
|
}
|
|
|
|
GraphicsContext *graphicsContext = nullptr;
|
|
SDL_Window *window = nullptr;
|
|
|
|
// Switch away from Vulkan if not available.
|
|
if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN && !vulkanMayBeAvailable) {
|
|
g_Config.iGPUBackend = (int)GPUBackend::OPENGL;
|
|
}
|
|
|
|
std::string error_message;
|
|
if (g_Config.iGPUBackend == (int)GPUBackend::OPENGL) {
|
|
SDLGLGraphicsContext *glctx = new SDLGLGraphicsContext();
|
|
if (glctx->Init(window, x, y, w, h, mode, &error_message, force_gl_version) != 0) {
|
|
// Let's try the fallback once per process run.
|
|
fprintf(stderr, "GL init error '%s' - falling back to Vulkan\n", error_message.c_str());
|
|
g_Config.iGPUBackend = (int)GPUBackend::VULKAN;
|
|
SetGPUBackend((GPUBackend)g_Config.iGPUBackend);
|
|
delete glctx;
|
|
|
|
// NOTE : This should match the lines below in the Vulkan case.
|
|
SDLVulkanGraphicsContext *vkctx = new SDLVulkanGraphicsContext();
|
|
if (!vkctx->Init(window, x, y, w, h, mode | SDL_WINDOW_VULKAN, &error_message)) {
|
|
fprintf(stderr, "Vulkan fallback failed: %s\n", error_message.c_str());
|
|
return 1;
|
|
}
|
|
graphicsContext = vkctx;
|
|
} else {
|
|
graphicsContext = glctx;
|
|
}
|
|
#if !PPSSPP_PLATFORM(SWITCH)
|
|
} else if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) {
|
|
SDLVulkanGraphicsContext *vkctx = new SDLVulkanGraphicsContext();
|
|
if (!vkctx->Init(window, x, y, w, h, mode | SDL_WINDOW_VULKAN, &error_message)) {
|
|
// Let's try the fallback once per process run.
|
|
|
|
fprintf(stderr, "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 vkctx;
|
|
|
|
// NOTE : This should match the three lines above in the OpenGL case.
|
|
SDLGLGraphicsContext *glctx = new SDLGLGraphicsContext();
|
|
if (glctx->Init(window, x, y, w, h, mode, &error_message, force_gl_version) != 0) {
|
|
fprintf(stderr, "GL fallback failed: %s\n", error_message.c_str());
|
|
return 1;
|
|
}
|
|
graphicsContext = glctx;
|
|
} else {
|
|
graphicsContext = vkctx;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
UpdateScreenDPI(window);
|
|
|
|
float dpi_scale = 1.0f / (g_ForcedDPI == 0.0f ? g_DesktopDPI : g_ForcedDPI);
|
|
|
|
Native_UpdateScreenScale(w * g_DesktopDPI, h * g_DesktopDPI, UIScaleFactorToMultiplier(g_Config.iUIScaleFactor));
|
|
|
|
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, "%sui_images/icon.png", ASSETS_DIR);
|
|
if (access(iconPath, F_OK) != 0)
|
|
snprintf(iconPath, PATH_MAX, "%sassets/ui_images/icon.png", SDL_GetBasePath() ? SDL_GetBasePath() : "");
|
|
#else
|
|
snprintf(iconPath, PATH_MAX, "%sassets/ui_images/icon.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)) {
|
|
fprintf(stderr, "Init from thread error: '%s'\n", error_message.c_str());
|
|
return 1;
|
|
}
|
|
|
|
// OK, we have a valid graphics backend selected. Let's clear the failures.
|
|
g_Config.sFailedGPUBackends.clear();
|
|
|
|
#ifdef MOBILE_DEVICE
|
|
SDL_ShowCursor(SDL_DISABLE);
|
|
#endif
|
|
|
|
// Avoid the IME popup when holding keys. This doesn't affect all versions of SDL.
|
|
// Note: We re-enable it in text input fields! This is necessary otherwise we don't receive
|
|
// KeyInputFlags::CHAR events.
|
|
SDL_StopTextInput();
|
|
|
|
InitSDLAudioDevice();
|
|
g_audioStartTime = time_now_d();
|
|
|
|
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;
|
|
|
|
// Check if the path to a directory containing an unpacked ISO is passed as a command line argument
|
|
for (int i = 1; i < argc; i++) {
|
|
if (File::IsDirectory(Path(argv[i]))) {
|
|
// Display the toast warning
|
|
System_Toast("Warning: Playing unpacked games may cause issues.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!mainThreadIsRender) {
|
|
// Vulkan mode uses this.
|
|
// We should only be a message pump. This allows for lower latency
|
|
// input events, and so on.
|
|
while (true) {
|
|
SDL_Event event;
|
|
if (SDL_WaitEventTimeout(&event, 100)) {
|
|
do {
|
|
ProcessSDLEvent(window, event, &inputTracker);
|
|
|
|
if (g_QuitRequested || g_RestartRequested)
|
|
break;
|
|
} while (SDL_PollEvent(&event));
|
|
}
|
|
|
|
if (g_QuitRequested || g_RestartRequested)
|
|
break;
|
|
|
|
UpdateTextFocus();
|
|
UpdateSDLCursor();
|
|
|
|
inputTracker.MouseCaptureControl();
|
|
|
|
{
|
|
std::lock_guard<std::mutex> guard(g_mutexWindow);
|
|
if (g_windowState.update) {
|
|
UpdateWindowState(window);
|
|
}
|
|
}
|
|
}
|
|
} else while (true) {
|
|
{
|
|
SDL_Event event;
|
|
while (SDL_PollEvent(&event)) {
|
|
ProcessSDLEvent(window, event, &inputTracker);
|
|
}
|
|
}
|
|
if (g_QuitRequested || g_RestartRequested)
|
|
break;
|
|
if (emuThreadState == (int)EmuThreadState::DISABLED) {
|
|
NativeFrame(graphicsContext);
|
|
}
|
|
if (g_QuitRequested || g_RestartRequested)
|
|
break;
|
|
|
|
UpdateTextFocus();
|
|
UpdateSDLCursor();
|
|
|
|
inputTracker.MouseCaptureControl();
|
|
|
|
bool renderThreadPaused = Native_IsWindowHidden() && g_Config.bPauseWhenMinimized && emuThreadState != (int)EmuThreadState::DISABLED;
|
|
if (emuThreadState != (int)EmuThreadState::DISABLED && !renderThreadPaused) {
|
|
if (!graphicsContext->ThreadFrame(true))
|
|
break;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> guard(g_mutexWindow);
|
|
if (g_windowState.update) {
|
|
UpdateWindowState(window);
|
|
}
|
|
}
|
|
|
|
if (g_rebootEmuThread) {
|
|
fprintf(stderr, "rebooting emu thread");
|
|
g_rebootEmuThread = false;
|
|
EmuThreadStop("shutdown");
|
|
graphicsContext->ThreadFrameUntilCondition([]() {
|
|
return emuThreadState == (int)EmuThreadState::STOPPED || emuThreadState == (int)EmuThreadState::DISABLED;
|
|
});
|
|
EmuThreadJoin();
|
|
graphicsContext->ThreadEnd();
|
|
graphicsContext->ShutdownFromRenderThread();
|
|
|
|
fprintf(stderr, "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, force_gl_version)) {
|
|
fprintf(stderr, "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) {
|
|
graphicsContext->ThreadFrameUntilCondition([]() {
|
|
return emuThreadState == (int)EmuThreadState::STOPPED || emuThreadState == (int)EmuThreadState::DISABLED;
|
|
});
|
|
}
|
|
|
|
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();
|
|
fprintf(stderr, "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;
|
|
}
|