diff --git a/.gitignore b/.gitignore
index c4e913ccf5..2e210617ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,10 +64,9 @@ PPSSPPControls.dat
.idea
*.iml
build
-build.ios
+/build*/
versionname.txt
versioncode.txt
-build*/
android/.cxx
# Temp file used by jenkins windows build (TODO: remove)
@@ -75,7 +74,7 @@ desc.txt
Logs
Memstick
-memstick*
+/memstick*
Cheats
/git-version.cpp
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f94d19d932..8648726fd7 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -89,6 +89,8 @@ libretro-build-linux-i686:
# MacOS 64-bit
libretro-build-osx-x64:
+ tags:
+ - macosx-packaging
extends:
- .libretro-osx-x64-make-default
- .core-defs
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1826721d7a..e2464852e1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -270,6 +270,9 @@ foreach (LANGUAGE C CXX)
endforeach()
if(NOT MSVC)
+ # NEON optimizations in libpng17 seem to cause PNG load errors, see #14485.
+ add_definitions(-DPNG_ARM_NEON_OPT=0)
+
if(ANDROID)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
endif()
@@ -499,6 +502,8 @@ add_library(Common STATIC
Common/File/VFS/VFS.cpp
Common/File/VFS/AssetReader.cpp
Common/File/VFS/AssetReader.h
+ Common/File/AndroidStorage.h
+ Common/File/AndroidStorage.cpp
Common/File/DiskFree.h
Common/File/DiskFree.cpp
Common/File/Path.h
@@ -603,12 +608,14 @@ add_library(Common STATIC
Common/Render/Text/draw_text_uwp.h
Common/System/Display.cpp
Common/System/Display.h
- Common/Thread/PrioritizedWorkQueue.cpp
- Common/Thread/PrioritizedWorkQueue.h
+ Common/Thread/Channel.h
+ Common/Thread/ParallelLoop.cpp
+ Common/Thread/ParallelLoop.h
+ Common/Thread/Promise.h
Common/Thread/ThreadUtil.cpp
Common/Thread/ThreadUtil.h
- Common/Thread/ThreadPool.cpp
- Common/Thread/ThreadPool.h
+ Common/Thread/ThreadManager.cpp
+ Common/Thread/ThreadManager.h
Common/UI/Root.cpp
Common/UI/Root.h
Common/UI/Screen.cpp
@@ -644,6 +651,8 @@ add_library(Common STATIC
Common/Log.cpp
Common/LogManager.cpp
Common/LogManager.h
+ Common/LogReporting.cpp
+ Common/LogReporting.h
Common/MakeUnique.h
Common/MemArenaAndroid.cpp
Common/MemArenaDarwin.cpp
@@ -1125,34 +1134,65 @@ list(APPEND NativeAppSource
android/jni/TestRunner.cpp
UI/DiscordIntegration.cpp
UI/NativeApp.cpp
+ UI/BackgroundAudio.h
UI/BackgroundAudio.cpp
+ UI/ChatScreen.h
UI/ChatScreen.cpp
UI/DevScreens.cpp
+ UI/DevScreens.h
UI/DisplayLayoutEditor.cpp
+ UI/DisplayLayoutEditor.h
UI/DisplayLayoutScreen.cpp
+ UI/DisplayLayoutScreen.h
+ UI/EmuScreen.h
UI/EmuScreen.cpp
+ UI/GameInfoCache.h
UI/GameInfoCache.cpp
+ UI/MainScreen.h
UI/MainScreen.cpp
+ UI/MiscScreens.h
UI/MiscScreens.cpp
+ UI/PauseScreen.h
UI/PauseScreen.cpp
+ UI/GameScreen.h
UI/GameScreen.cpp
+ UI/GameSettingsScreen.h
UI/GameSettingsScreen.cpp
+ UI/GPUDriverTestScreen.h
UI/GPUDriverTestScreen.cpp
+ UI/TiltAnalogSettingsScreen.h
UI/TiltAnalogSettingsScreen.cpp
+ UI/TiltEventProcessor.h
UI/TiltEventProcessor.cpp
+ UI/TouchControlLayoutScreen.h
UI/TouchControlLayoutScreen.cpp
+ UI/TouchControlVisibilityScreen.h
UI/TouchControlVisibilityScreen.cpp
+ UI/GamepadEmu.h
UI/GamepadEmu.cpp
+ UI/OnScreenDisplay.h
UI/OnScreenDisplay.cpp
+ UI/ControlMappingScreen.h
UI/ControlMappingScreen.cpp
+ UI/RemoteISOScreen.h
UI/RemoteISOScreen.cpp
+ UI/ReportScreen.h
UI/ReportScreen.cpp
+ UI/SavedataScreen.h
UI/SavedataScreen.cpp
+ UI/Store.h
UI/Store.cpp
+ UI/CwCheatScreen.h
UI/CwCheatScreen.cpp
+ UI/InstallZipScreen.h
UI/InstallZipScreen.cpp
+ UI/MemStickScreen.h
+ UI/MemStickScreen.cpp
+ UI/ProfilerDraw.h
UI/ProfilerDraw.cpp
+ UI/TextureUtil.h
UI/TextureUtil.cpp
+ UI/ComboKeyMappingScreen.h
UI/ComboKeyMappingScreen.cpp
)
@@ -1525,6 +1565,8 @@ add_library(${CoreLibName} ${CoreLinkType}
Core/Config.cpp
Core/Config.h
Core/ConfigValues.h
+ Core/ControlMapper.cpp
+ Core/ControlMapper.h
Core/Core.cpp
Core/Core.h
Core/Compatibility.cpp
@@ -2194,6 +2236,7 @@ if(UNITTEST)
unittest/TestArm64Emitter.cpp
unittest/TestX64Emitter.cpp
unittest/TestVertexJit.cpp
+ unittest/TestThreadManager.cpp
unittest/JitHarness.cpp
Core/MIPS/ARM/ArmRegCache.cpp
Core/MIPS/ARM/ArmRegCacheFPU.cpp
diff --git a/Common/ArmCPUDetect.cpp b/Common/ArmCPUDetect.cpp
index 3e59843e2b..46e3d50335 100644
--- a/Common/ArmCPUDetect.cpp
+++ b/Common/ArmCPUDetect.cpp
@@ -248,6 +248,7 @@ void CPUInfo::Detect()
Mode64bit = false;
#endif
vendor = VENDOR_ARM;
+ logical_cpu_count = 1;
// Get the information about the CPU
#if !PPSSPP_PLATFORM(LINUX)
diff --git a/Common/CPUDetect.h b/Common/CPUDetect.h
index 1318814070..c2985fc3c5 100644
--- a/Common/CPUDetect.h
+++ b/Common/CPUDetect.h
@@ -39,7 +39,10 @@ struct CPUInfo {
bool Mode64bit;
bool HTT;
+
+ // Number of real CPU cores.
int num_cores;
+ // Number of logical CPUs per core.
int logical_cpu_count;
bool bAtom;
diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj
index 476039ff0f..5bc0e2ab83 100644
--- a/Common/Common.vcxproj
+++ b/Common/Common.vcxproj
@@ -409,6 +409,7 @@
+
@@ -469,6 +470,7 @@
+
@@ -525,8 +527,11 @@
-
-
+
+
+
+
+
@@ -830,6 +835,7 @@
+
@@ -891,6 +897,7 @@
+
@@ -960,8 +967,8 @@
-
-
+
+
diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters
index cd5146e2cf..58ca55dbbf 100644
--- a/Common/Common.vcxproj.filters
+++ b/Common/Common.vcxproj.filters
@@ -90,12 +90,6 @@
ext\libpng17
-
- Thread
-
-
- Thread
-
Thread
@@ -394,6 +388,25 @@
File
+
+
+ File
+
+
+ Thread
+
+
+ Thread
+
+
+ Thread
+
+
+ Thread
+
+
+ Thread
+
@@ -489,12 +502,6 @@
ext\libpng17
-
- Thread
-
-
- Thread
-
Thread
@@ -760,6 +767,16 @@
File
+
+
+ File
+
+
+ Thread
+
+
+ Thread
+
diff --git a/Common/Data/Format/IniFile.cpp b/Common/Data/Format/IniFile.cpp
index 06ee1acd60..0900e28499 100644
--- a/Common/Data/Format/IniFile.cpp
+++ b/Common/Data/Format/IniFile.cpp
@@ -170,6 +170,10 @@ void Section::Set(const char* key, uint32_t newValue) {
Set(key, StringFromFormat("0x%08x", newValue).c_str());
}
+void Section::Set(const char* key, uint64_t newValue) {
+ Set(key, StringFromFormat("0x%016lx", newValue).c_str());
+}
+
void Section::Set(const char* key, float newValue) {
Set(key, StringFromFormat("%f", newValue).c_str());
}
@@ -311,6 +315,16 @@ bool Section::Get(const char* key, uint32_t* value, uint32_t defaultValue)
return false;
}
+bool Section::Get(const char* key, uint64_t* value, uint64_t defaultValue)
+{
+ std::string temp;
+ bool retval = Get(key, &temp, 0);
+ if (retval && TryParse(temp, value))
+ return true;
+ *value = defaultValue;
+ return false;
+}
+
bool Section::Get(const char* key, bool* value, bool defaultValue)
{
std::string temp;
@@ -659,6 +673,17 @@ bool IniFile::Get(const char* sectionName, const char* key, uint32_t* value, uin
}
}
+bool IniFile::Get(const char* sectionName, const char* key, uint64_t* value, uint64_t defaultValue)
+{
+ Section *section = GetSection(sectionName);
+ if (!section) {
+ *value = defaultValue;
+ return false;
+ } else {
+ return section->Get(key, value, defaultValue);
+ }
+}
+
bool IniFile::Get(const char* sectionName, const char* key, bool* value, bool defaultValue)
{
Section *section = GetSection(sectionName);
diff --git a/Common/Data/Format/IniFile.h b/Common/Data/Format/IniFile.h
index 6499562edb..87bd1d1830 100644
--- a/Common/Data/Format/IniFile.h
+++ b/Common/Data/Format/IniFile.h
@@ -35,6 +35,7 @@ public:
bool Get(const char* key, std::string* value, const char* defaultValue);
void Set(const char* key, uint32_t newValue);
+ void Set(const char* key, uint64_t newValue);
void Set(const char* key, float newValue);
void Set(const char* key, const float newValue, const float defaultValue);
void Set(const char* key, double newValue);
@@ -58,6 +59,7 @@ public:
bool Get(const char* key, int* value, int defaultValue = 0);
bool Get(const char* key, uint32_t* value, uint32_t defaultValue = 0);
+ bool Get(const char* key, uint64_t* value, uint64_t defaultValue = 0);
bool Get(const char* key, bool* value, bool defaultValue = false);
bool Get(const char* key, float* value, float defaultValue = false);
bool Get(const char* key, double* value, double defaultValue = false);
@@ -103,6 +105,9 @@ public:
void Set(const char* sectionName, const char* key, uint32_t newValue) {
GetOrCreateSection(sectionName)->Set(key, newValue);
}
+ void Set(const char* sectionName, const char* key, uint64_t newValue) {
+ GetOrCreateSection(sectionName)->Set(key, newValue);
+ }
void Set(const char* sectionName, const char* key, bool newValue) {
GetOrCreateSection(sectionName)->Set(key, newValue);
}
@@ -114,6 +119,7 @@ public:
bool Get(const char* sectionName, const char* key, std::string* value, const char* defaultValue = "");
bool Get(const char* sectionName, const char* key, int* value, int defaultValue = 0);
bool Get(const char* sectionName, const char* key, uint32_t* value, uint32_t defaultValue = 0);
+ bool Get(const char* sectionName, const char* key, uint64_t* value, uint64_t defaultValue = 0);
bool Get(const char* sectionName, const char* key, bool* value, bool defaultValue = false);
bool Get(const char* sectionName, const char* key, std::vector& values);
diff --git a/Common/Data/Text/Parsers.cpp b/Common/Data/Text/Parsers.cpp
index ab7c057339..5342b6fbcd 100644
--- a/Common/Data/Text/Parsers.cpp
+++ b/Common/Data/Text/Parsers.cpp
@@ -71,6 +71,26 @@ bool TryParse(const std::string &str, uint32_t *const output) {
return true;
}
+bool TryParse(const std::string &str, uint64_t *const output) {
+ char *endptr = NULL;
+
+ // Holy crap this is ugly.
+
+ // Reset errno to a value other than ERANGE
+ errno = 0;
+
+ uint64_t value = strtoul(str.c_str(), &endptr, 0);
+
+ if (!endptr || *endptr)
+ return false;
+
+ if (errno == ERANGE)
+ return false;
+
+ *output = value;
+ return true;
+}
+
bool TryParse(const std::string &str, bool *const output) {
if ("1" == str || !strcasecmp("true", str.c_str()))
*output = true;
diff --git a/Common/Data/Text/Parsers.h b/Common/Data/Text/Parsers.h
index 909e667178..bfa111774c 100644
--- a/Common/Data/Text/Parsers.h
+++ b/Common/Data/Text/Parsers.h
@@ -57,6 +57,7 @@ bool ParseMacAddress(std::string str, uint8_t macAddr[6]);
bool TryParse(const std::string &str, bool *const output);
bool TryParse(const std::string &str, uint32_t *const output);
+bool TryParse(const std::string &str, uint64_t *const output);
template
static bool TryParse(const std::string &str, N *const output) {
diff --git a/Common/File/AndroidStorage.cpp b/Common/File/AndroidStorage.cpp
new file mode 100644
index 0000000000..6ea6983f5e
--- /dev/null
+++ b/Common/File/AndroidStorage.cpp
@@ -0,0 +1,190 @@
+#include "Common/File/AndroidStorage.h"
+#include "Common/StringUtils.h"
+#include "Common/Log.h"
+#include "Common/TimeUtil.h"
+
+#include "android/jni/app-android.h"
+
+#if PPSSPP_PLATFORM(ANDROID) && !defined(__LIBRETRO__)
+
+static jmethodID openContentUri;
+static jmethodID listContentUriDir;
+static jmethodID contentUriCreateFile;
+static jmethodID contentUriCreateDirectory;
+static jmethodID contentUriRemoveFile;
+static jmethodID contentUriGetFileInfo;
+static jmethodID contentUriGetFreeStorageSpace;
+static jmethodID filePathGetFreeStorageSpace;
+
+static jobject g_nativeActivity;
+
+void Android_StorageSetNativeActivity(jobject nativeActivity) {
+ g_nativeActivity = nativeActivity;
+}
+
+void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj) {
+ openContentUri = env->GetMethodID(env->GetObjectClass(obj), "openContentUri", "(Ljava/lang/String;Ljava/lang/String;)I");
+ _dbg_assert_(openContentUri);
+ listContentUriDir = env->GetMethodID(env->GetObjectClass(obj), "listContentUriDir", "(Ljava/lang/String;)[Ljava/lang/String;");
+ _dbg_assert_(listContentUriDir);
+ contentUriCreateDirectory = env->GetMethodID(env->GetObjectClass(obj), "contentUriCreateDirectory", "(Ljava/lang/String;Ljava/lang/String;)Z");
+ _dbg_assert_(contentUriCreateDirectory);
+ contentUriCreateFile = env->GetMethodID(env->GetObjectClass(obj), "contentUriCreateFile", "(Ljava/lang/String;Ljava/lang/String;)Z");
+ _dbg_assert_(contentUriCreateFile);
+ contentUriRemoveFile = env->GetMethodID(env->GetObjectClass(obj), "contentUriRemoveFile", "(Ljava/lang/String;)Z");
+ _dbg_assert_(contentUriRemoveFile);
+ contentUriGetFileInfo = env->GetMethodID(env->GetObjectClass(obj), "contentUriGetFileInfo", "(Ljava/lang/String;)Ljava/lang/String;");
+ _dbg_assert_(contentUriGetFileInfo);
+ contentUriGetFreeStorageSpace = env->GetMethodID(env->GetObjectClass(obj), "contentUriGetFreeStorageSpace", "(Ljava/lang/String;)J");
+ _dbg_assert_(contentUriGetFreeStorageSpace);
+ filePathGetFreeStorageSpace = env->GetMethodID(env->GetObjectClass(obj), "filePathGetFreeStorageSpace", "(Ljava/lang/String;)J");
+ _dbg_assert_(filePathGetFreeStorageSpace);
+}
+
+bool Android_IsContentUri(const std::string &filename) {
+ return startsWith(filename, "content://");
+}
+
+int Android_OpenContentUriFd(const std::string &filename, Android_OpenContentUriMode mode) {
+ if (!g_nativeActivity) {
+ return -1;
+ }
+
+ std::string fname = filename;
+ // PPSSPP adds an ending slash to directories before looking them up.
+ // TODO: Fix that in the caller (or don't call this for directories).
+ if (fname.back() == '/')
+ fname.pop_back();
+
+ auto env = getEnv();
+ const char *modeStr = "";
+ switch (mode) {
+ case Android_OpenContentUriMode::READ: modeStr = "r"; break;
+ case Android_OpenContentUriMode::READ_WRITE: modeStr = "rw"; break;
+ case Android_OpenContentUriMode::READ_WRITE_TRUNCATE: modeStr = "rwt"; break;
+ }
+ jstring j_filename = env->NewStringUTF(fname.c_str());
+ jstring j_mode = env->NewStringUTF(modeStr);
+ int fd = env->CallIntMethod(g_nativeActivity, openContentUri, j_filename, j_mode);
+ return fd;
+}
+
+bool Android_CreateDirectory(const std::string &rootTreeUri, const std::string &dirName) {
+ if (!g_nativeActivity) {
+ return false;
+ }
+ auto env = getEnv();
+ jstring paramRoot = env->NewStringUTF(rootTreeUri.c_str());
+ jstring paramDirName = env->NewStringUTF(dirName.c_str());
+ return env->CallBooleanMethod(g_nativeActivity, contentUriCreateDirectory, paramRoot, paramDirName);
+}
+
+bool Android_CreateFile(const std::string &parentTreeUri, const std::string &fileName) {
+ if (!g_nativeActivity) {
+ return false;
+ }
+ auto env = getEnv();
+ jstring paramRoot = env->NewStringUTF(parentTreeUri.c_str());
+ jstring paramFileName = env->NewStringUTF(fileName.c_str());
+ return env->CallBooleanMethod(g_nativeActivity, contentUriCreateFile, paramRoot, paramFileName);
+}
+
+bool Android_RemoveFile(const std::string &fileUri) {
+ if (!g_nativeActivity) {
+ return false;
+ }
+ auto env = getEnv();
+ jstring paramFileName = env->NewStringUTF(fileUri.c_str());
+ return env->CallBooleanMethod(g_nativeActivity, contentUriRemoveFile, paramFileName);
+}
+
+static bool ParseFileInfo(const std::string &line, File::FileInfo *fileInfo) {
+ std::vector parts;
+ SplitString(line, '|', parts);
+ if (parts.size() != 5) {
+ ERROR_LOG(FILESYS, "Bad format: %s", line.c_str());
+ return false;
+ }
+ fileInfo->name = std::string(parts[2]);
+ fileInfo->isDirectory = parts[0][0] == 'D';
+ fileInfo->exists = true;
+ sscanf(parts[1].c_str(), "%" PRIu64, &fileInfo->size);
+ fileInfo->fullName = Path(parts[3]);
+ fileInfo->isWritable = true; // TODO: Should be passed as part of the string.
+ sscanf(parts[4].c_str(), "%" PRIu64, &fileInfo->lastModified);
+ return true;
+}
+
+bool Android_GetFileInfo(const std::string &fileUri, File::FileInfo *fileInfo) {
+ if (!g_nativeActivity) {
+ return false;
+ }
+ auto env = getEnv();
+ jstring paramFileUri = env->NewStringUTF(fileUri.c_str());
+
+ jstring str = (jstring)env->CallObjectMethod(g_nativeActivity, contentUriGetFileInfo, paramFileUri);
+ if (!str) {
+ return false;
+ }
+ const char *charArray = env->GetStringUTFChars(str, 0);
+ bool retval = ParseFileInfo(std::string(charArray), fileInfo);
+ env->DeleteLocalRef(str);
+ return retval && fileInfo->exists;
+}
+
+std::vector Android_ListContentUri(const std::string &path) {
+ if (!g_nativeActivity) {
+ return std::vector();
+ }
+ auto env = getEnv();
+
+ double start = time_now_d();
+
+ jstring param = env->NewStringUTF(path.c_str());
+ jobject retval = env->CallObjectMethod(g_nativeActivity, listContentUriDir, param);
+
+ jobjectArray fileList = (jobjectArray)retval;
+ std::vector items;
+ int size = env->GetArrayLength(fileList);
+ for (int i = 0; i < size; i++) {
+ jstring str = (jstring)env->GetObjectArrayElement(fileList, i);
+ const char *charArray = env->GetStringUTFChars(str, 0);
+ if (charArray) { // paranoia
+ File::FileInfo info;
+ if (ParseFileInfo(std::string(charArray), &info)) {
+ items.push_back(info);
+ }
+ }
+ env->ReleaseStringUTFChars(str, charArray);
+ env->DeleteLocalRef(str);
+ }
+ env->DeleteLocalRef(fileList);
+
+ double elapsed = time_now_d() - start;
+ if (elapsed > 0.1) {
+ INFO_LOG(FILESYS, "Listing directory on content URI took %0.3f s (%d files)", elapsed, (int)size);
+ }
+ return items;
+}
+
+int64_t Android_GetFreeSpaceByContentUri(const std::string &uri) {
+ if (!g_nativeActivity) {
+ return false;
+ }
+ auto env = getEnv();
+
+ jstring param = env->NewStringUTF(uri.c_str());
+ return env->CallLongMethod(g_nativeActivity, contentUriGetFreeStorageSpace, param);
+}
+
+int64_t Android_GetFreeSpaceByFilePath(const std::string &filePath) {
+ if (!g_nativeActivity) {
+ return false;
+ }
+ auto env = getEnv();
+
+ jstring param = env->NewStringUTF(filePath.c_str());
+ return env->CallLongMethod(g_nativeActivity, filePathGetFreeStorageSpace, param);
+}
+
+#endif
diff --git a/Common/File/AndroidStorage.h b/Common/File/AndroidStorage.h
new file mode 100644
index 0000000000..71211824df
--- /dev/null
+++ b/Common/File/AndroidStorage.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include
+#include
+
+#include "Common/File/DirListing.h"
+
+// To emphasize that Android storage mode strings are different, let's just use
+// an enum.
+enum class Android_OpenContentUriMode {
+ READ = 0, // "r"
+ READ_WRITE = 1, // "rw"
+ READ_WRITE_TRUNCATE = 2, // "rwt"
+};
+
+#if PPSSPP_PLATFORM(ANDROID) && !defined(__LIBRETRO__)
+
+#include
+
+extern std::string g_extFilesDir;
+
+void Android_StorageSetNativeActivity(jobject nativeActivity);
+
+bool Android_IsContentUri(const std::string &uri);
+int Android_OpenContentUriFd(const std::string &uri, const Android_OpenContentUriMode mode);
+bool Android_CreateDirectory(const std::string &parentTreeUri, const std::string &dirName);
+bool Android_CreateFile(const std::string &parentTreeUri, const std::string &fileName);
+bool Android_RemoveFile(const std::string &fileUri);
+bool Android_GetFileInfo(const std::string &fileUri, File::FileInfo *info);
+int64_t Android_GetFreeSpaceByContentUri(const std::string &uri);
+int64_t Android_GetFreeSpaceByFilePath(const std::string &filePath);
+
+std::vector Android_ListContentUri(const std::string &uri);
+
+void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj);
+
+#else
+
+// Stub out the Android Storage wrappers, so that we can avoid ifdefs everywhere.
+
+inline bool Android_IsContentUri(const std::string &uri) { return false; }
+inline int Android_OpenContentUriFd(const std::string &uri, const Android_OpenContentUriMode mode) { return -1; }
+inline bool Android_CreateDirectory(const std::string &parentTreeUri, const std::string &dirName) { return false; }
+inline bool Android_CreateFile(const std::string &parentTreeUri, const std::string &fileName) { return false; }
+inline bool Android_RemoveFile(const std::string &fileUri) { return false; }
+inline bool Android_GetFileInfo(const std::string &fileUri, File::FileInfo *info) { return false; }
+inline int64_t Android_GetFreeSpaceByContentUri(const std::string &uri) { return -1; }
+inline int64_t Android_GetFreeSpaceByFilePath(const std::string &filePath) { return -1; }
+inline std::vector Android_ListContentUri(const std::string &uri) {
+ return std::vector();
+}
+
+#endif
diff --git a/Common/File/DirListing.cpp b/Common/File/DirListing.cpp
index 36f05b9259..ea1f3a3040 100644
--- a/Common/File/DirListing.cpp
+++ b/Common/File/DirListing.cpp
@@ -20,8 +20,10 @@
#include "Common/Data/Encoding/Utf8.h"
#include "Common/StringUtils.h"
+#include "Common/Net/URL.h"
#include "Common/File/DirListing.h"
#include "Common/File/FileUtil.h"
+#include "Common/File/AndroidStorage.h"
#if !defined(__linux__) && !defined(_WIN32) && !defined(__QNX__)
#define stat64 stat
@@ -40,6 +42,8 @@ bool GetFileInfo(const Path &path, FileInfo * fileInfo) {
switch (path.Type()) {
case PathType::NATIVE:
break; // OK
+ case PathType::CONTENT_URI:
+ return Android_GetFileInfo(path.ToString(), fileInfo);
default:
return false;
}
@@ -129,7 +133,61 @@ bool FileInfo::operator <(const FileInfo & other) const {
return false;
}
+std::vector ApplyFilter(std::vector files, const char *filter) {
+ std::set filters;
+ if (filter) {
+ std::string tmp;
+ while (*filter) {
+ if (*filter == ':') {
+ filters.insert("." + tmp);
+ tmp.clear();
+ } else {
+ tmp.push_back(*filter);
+ }
+ filter++;
+ }
+ if (!tmp.empty())
+ filters.insert("." + tmp);
+ }
+
+ auto pred = [&](const File::FileInfo &info) {
+ if (info.isDirectory || !filter)
+ return false;
+ std::string ext = info.fullName.GetFileExtension();
+ return filters.find(ext) == filters.end();
+ };
+ files.erase(std::remove_if(files.begin(), files.end(), pred), files.end());
+ return files;
+}
+
size_t GetFilesInDir(const Path &directory, std::vector * files, const char *filter, int flags) {
+ if (directory.Type() == PathType::CONTENT_URI) {
+ std::vector fileList = Android_ListContentUri(directory.ToString());
+ *files = ApplyFilter(fileList, filter);
+ std::sort(files->begin(), files->end());
+ return true;
+ }
+
+#if PPSSPP_PLATFORM(WINDOWS)
+ if (directory.IsRoot()) {
+ // Special path that means root of file system.
+ std::vector drives = File::GetWindowsDrives();
+ for (auto drive = drives.begin(); drive != drives.end(); ++drive) {
+ if (*drive == "A:/" || *drive == "B:/")
+ continue;
+ File::FileInfo fake;
+ fake.fullName = Path(*drive);
+ fake.name = *drive;
+ fake.isDirectory = true;
+ fake.exists = true;
+ fake.size = 0;
+ fake.isWritable = false;
+ files->push_back(fake);
+ }
+ return files->size();
+ }
+#endif
+
size_t foundEntries = 0;
std::set filters;
if (filter) {
diff --git a/Common/File/DirListing.h b/Common/File/DirListing.h
index f175796a95..cea0fbb37a 100644
--- a/Common/File/DirListing.h
+++ b/Common/File/DirListing.h
@@ -3,7 +3,7 @@
#include
#include
-#include
+#include
#include
@@ -24,18 +24,22 @@ struct FileInfo {
uint64_t ctime;
uint32_t access; // st_mode & 0x1ff
+ // Currently only supported for Android storage files.
+ // Other places use different methods to get this.
+ uint64_t lastModified = 0;
+
bool operator <(const FileInfo &other) const;
};
bool GetFileInfo(const Path &path, FileInfo *fileInfo);
enum {
- GETFILES_GETHIDDEN = 1
+ GETFILES_GETHIDDEN = 1,
};
-
size_t GetFilesInDir(const Path &directory, std::vector *files, const char *filter = nullptr, int flags = 0);
int64_t GetDirectoryRecursiveSize(const Path &path, const char *filter = nullptr, int flags = 0);
+std::vector ApplyFilter(std::vector files, const char *filter);
#ifdef _WIN32
std::vector GetWindowsDrives();
diff --git a/Common/File/FileUtil.cpp b/Common/File/FileUtil.cpp
index 2536d4bdf7..bdd64d96c5 100644
--- a/Common/File/FileUtil.cpp
+++ b/Common/File/FileUtil.cpp
@@ -24,6 +24,9 @@
#include "ppsspp_config.h"
+#include "android/jni/app-android.h"
+#include "android/jni/AndroidContentURI.h"
+
#ifdef __MINGW32__
#include
#ifndef _POSIX_THREAD_SAFE_FUNCTIONS
@@ -35,6 +38,7 @@
#include
#include "Common/Log.h"
+#include "Common/LogReporting.h"
#include "Common/File/FileUtil.h"
#include "Common/StringUtils.h"
#include "Common/SysError.h"
@@ -97,18 +101,89 @@ FILE *OpenCFile(const Path &path, const char *mode) {
switch (path.Type()) {
case PathType::NATIVE:
break;
+ case PathType::CONTENT_URI:
+ // We're gonna need some error codes..
+ if (!strcmp(mode, "r") || !strcmp(mode, "rb")) {
+ INFO_LOG(COMMON, "Opening content file for read: '%s'", path.c_str());
+ // Read, let's support this - easy one.
+ int descriptor = Android_OpenContentUriFd(path.ToString(), Android_OpenContentUriMode::READ);
+ if (descriptor == -1) {
+ return nullptr;
+ }
+ return fdopen(descriptor, "rb");
+ } else if (!strcmp(mode, "w") || !strcmp(mode, "wb")) {
+ // Need to be able to create the file here if it doesn't exist.
+ // Not exactly sure which abstractions are best, let's start simple.
+ if (!File::Exists(path)) {
+ INFO_LOG(COMMON, "Opening content file '%s' for write. Doesn't exist, creating empty and reopening.", path.c_str());
+ std::string name = path.GetFilename();
+ if (path.CanNavigateUp()) {
+ Path parent = path.NavigateUp();
+ if (!Android_CreateFile(parent.ToString(), name)) {
+ WARN_LOG(COMMON, "Failed to create file '%s' in '%s'", name.c_str(), parent.c_str());
+ return nullptr;
+ }
+ } else {
+ INFO_LOG_REPORT_ONCE(openCFileFailedNavigateUp, COMMON, "Failed to navigate up to create file: %s", path.c_str());
+ return nullptr;
+ }
+ } else {
+ INFO_LOG(COMMON, "Opening file by fd for write");
+ }
+
+ // TODO: Support append modes and stuff... For now let's go with the most common one.
+ int descriptor = Android_OpenContentUriFd(path.ToString(), Android_OpenContentUriMode::READ_WRITE_TRUNCATE);
+ if (descriptor == -1) {
+ INFO_LOG(COMMON, "Opening '%s' for write failed", path.ToString().c_str());
+ return nullptr;
+ }
+ return fdopen(descriptor, "wb");
+ } else {
+ ERROR_LOG(COMMON, "OpenCFile(%s): Mode not yet supported: %s", path.c_str(), mode);
+ return nullptr;
+ }
+ break;
default:
- ERROR_LOG(COMMON, "OpenCFile(%s): Not yet supported", path.c_str());
+ ERROR_LOG(COMMON, "OpenCFile(%s): PathType not yet supported", path.c_str());
return nullptr;
}
#if defined(_WIN32) && defined(UNICODE)
return _wfopen(path.ToWString().c_str(), ConvertUTF8ToWString(mode).c_str());
#else
- return fopen(path.ToString().c_str(), mode);
+ return fopen(path.c_str(), mode);
#endif
}
+int OpenFD(const Path &path, OpenFlag flags) {
+ switch (path.Type()) {
+ case PathType::CONTENT_URI:
+ break;
+ default:
+ // Not yet supported.
+ return -1;
+ }
+
+ Android_OpenContentUriMode mode;
+ if (flags == OPEN_READ) {
+ mode = Android_OpenContentUriMode::READ;
+ } else if (flags & OPEN_WRITE) {
+ if (flags & OPEN_TRUNCATE) {
+ mode = Android_OpenContentUriMode::READ_WRITE;
+ } else {
+ mode = Android_OpenContentUriMode::READ_WRITE_TRUNCATE;
+ }
+ // TODO: Maybe better checking of additional flags here.
+ } else {
+ // TODO: Add support for more modes if possible.
+ ERROR_LOG_REPORT_ONCE(openFlagNotSupported, COMMON, "OpenFlag 0x%x not yet supported", flags);
+ return -1;
+ }
+
+ int descriptor = Android_OpenContentUriFd(path.ToString(), mode);
+ return descriptor;
+}
+
#ifdef _WIN32
static bool ResolvePathVista(const std::wstring &path, wchar_t *buf, DWORD bufSize) {
typedef DWORD(WINAPI *getFinalPathNameByHandleW_f)(HANDLE hFile, LPWSTR lpszFilePath, DWORD cchFilePath, DWORD dwFlags);
@@ -146,7 +221,13 @@ std::string ResolvePath(const std::string &path) {
if (startsWith(path, "http://") || startsWith(path, "https://")) {
return path;
}
-#if PPSSPP_PLATFORM(WINDOWS)
+
+ if (Android_IsContentUri(path)) {
+ // Nothing to do?
+ return path;
+ }
+
+#ifdef _WIN32
static const int BUF_SIZE = 32768;
wchar_t *buf = new wchar_t[BUF_SIZE] {};
@@ -190,39 +271,28 @@ std::string ResolvePath(const std::string &path) {
#endif
}
-// Remove any ending forward slashes from directory paths
-// Modifies argument.
-static void StripTailDirSlashes(std::string &fname) {
- if (fname.length() > 1) {
- size_t i = fname.length() - 1;
-#if PPSSPP_PLATFORM(WINDOWS)
- if (i == 2 && fname[1] == ':' && fname[2] == '\\')
- return;
-#endif
- while (strchr(DIR_SEP_CHRS, fname[i]))
- fname[i--] = '\0';
- }
- return;
-}
-
// Returns true if file filename exists. Will return true on directories.
bool ExistsInDir(const Path &path, const std::string &filename) {
return Exists(path / filename);
}
-bool Exists(const std::string &filename) {
- std::string fn = filename;
- StripTailDirSlashes(fn);
+bool Exists(const Path &path) {
+ if (path.Type() == PathType::CONTENT_URI) {
+ FileInfo info;
+ if (!Android_GetFileInfo(path.c_str(), &info)) {
+ return false;
+ }
+ return info.exists;
+ }
#if defined(_WIN32)
- std::wstring copy = ConvertUTF8ToWString(fn);
// Make sure Windows will no longer handle critical errors, which means no annoying "No disk" dialog
#if !PPSSPP_PLATFORM(UWP)
int OldMode = SetErrorMode(SEM_FAILCRITICALERRORS);
#endif
WIN32_FILE_ATTRIBUTE_DATA data{};
- if (!GetFileAttributesEx(copy.c_str(), GetFileExInfoStandard, &data) || data.dwFileAttributes == INVALID_FILE_ATTRIBUTES) {
+ if (!GetFileAttributesEx(path.ToWString().c_str(), GetFileExInfoStandard, &data) || data.dwFileAttributes == INVALID_FILE_ATTRIBUTES) {
return false;
}
#if !PPSSPP_PLATFORM(UWP)
@@ -231,43 +301,44 @@ bool Exists(const std::string &filename) {
return true;
#else
struct stat file_info;
- return stat(fn.c_str(), &file_info) == 0;
+ return stat(path.c_str(), &file_info) == 0;
#endif
}
-bool Exists(const Path &path) {
- // Temporary forward.
- return Exists(path.ToString());
-}
-
// Returns true if filename exists and is a directory
bool IsDirectory(const Path &filename) {
switch (filename.Type()) {
case PathType::NATIVE:
break; // OK
+ case PathType::CONTENT_URI:
+ {
+ FileInfo info;
+ if (!Android_GetFileInfo(filename.ToString(), &info)) {
+ return false;
+ }
+ return info.exists && info.isDirectory;
+ }
default:
return false;
}
- std::string fn = filename.ToString();
- StripTailDirSlashes(fn);
-
#if defined(_WIN32)
- std::wstring copy = ConvertUTF8ToWString(fn);
WIN32_FILE_ATTRIBUTE_DATA data{};
- if (!GetFileAttributesEx(copy.c_str(), GetFileExInfoStandard, &data) || data.dwFileAttributes == INVALID_FILE_ATTRIBUTES) {
+ if (!GetFileAttributesEx(filename.ToWString().c_str(), GetFileExInfoStandard, &data) || data.dwFileAttributes == INVALID_FILE_ATTRIBUTES) {
auto err = GetLastError();
- WARN_LOG(COMMON, "GetFileAttributes failed on %s: %08x %s", fn.c_str(), (uint32_t)err, GetStringErrorMsg(err).c_str());
+ if (err != ERROR_FILE_NOT_FOUND) {
+ WARN_LOG(COMMON, "GetFileAttributes failed on %s: %08x %s", filename.ToVisualString().c_str(), (uint32_t)err, GetStringErrorMsg(err).c_str());
+ }
return false;
}
DWORD result = data.dwFileAttributes;
return (result & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY;
#else
- std::string copy(fn);
+ std::string copy = filename.ToString();
struct stat file_info;
int result = stat(copy.c_str(), &file_info);
if (result < 0) {
- WARN_LOG(COMMON, "IsDirectory: stat failed on %s: %s", fn.c_str(), GetLastErrorMsg().c_str());
+ WARN_LOG(COMMON, "IsDirectory: stat failed on %s: %s", copy.c_str(), GetLastErrorMsg().c_str());
return false;
}
return S_ISDIR(file_info.st_mode);
@@ -280,6 +351,8 @@ bool Delete(const Path &filename) {
switch (filename.Type()) {
case PathType::NATIVE:
break; // OK
+ case PathType::CONTENT_URI:
+ return Android_RemoveFile(filename.ToString());
default:
return false;
}
@@ -289,13 +362,13 @@ bool Delete(const Path &filename) {
// Return true because we care about the file no
// being there, not the actual delete.
if (!Exists(filename)) {
- WARN_LOG(COMMON, "Delete: %s does not exists", filename.c_str());
+ WARN_LOG(COMMON, "Delete: '%s' already does not exist", filename.c_str());
return true;
}
// We can't delete a directory
if (IsDirectory(filename)) {
- WARN_LOG(COMMON, "Delete failed: %s is a directory", filename.c_str());
+ WARN_LOG(COMMON, "Delete failed: '%s' is a directory", filename.c_str());
return false;
}
@@ -320,103 +393,109 @@ bool CreateDir(const Path &path) {
switch (path.Type()) {
case PathType::NATIVE:
break; // OK
+ case PathType::CONTENT_URI:
+ {
+ // Convert it to a "CreateDirIn" call, if possible, since that's
+ // what we can do with the storage API.
+ AndroidContentURI uri(path.ToString());
+ std::string newDirName = uri.GetLastPart();
+ if (uri.NavigateUp()) {
+ INFO_LOG(COMMON, "Calling Android_CreateDirectory(%s, %s)", uri.ToString().c_str(), newDirName.c_str());
+ return Android_CreateDirectory(uri.ToString(), newDirName);
+ } else {
+ // Bad path - can't create this directory.
+ WARN_LOG(COMMON, "CreateDir failed: '%s'", path.c_str());
+ return false;
+ }
+ break;
+ }
default:
return false;
}
- std::string fn = path.ToString();
- DEBUG_LOG(COMMON, "CreateDir('%s')", fn.c_str());
+ DEBUG_LOG(COMMON, "CreateDir('%s')", path.c_str());
#ifdef _WIN32
- if (::CreateDirectory(ConvertUTF8ToWString(fn).c_str(), NULL))
+ if (::CreateDirectory(path.ToWString().c_str(), NULL))
return true;
DWORD error = GetLastError();
- if (error == ERROR_ALREADY_EXISTS)
- {
+ if (error == ERROR_ALREADY_EXISTS) {
WARN_LOG(COMMON, "CreateDir: CreateDirectory failed on %s: already exists", path.c_str());
return true;
}
ERROR_LOG(COMMON, "CreateDir: CreateDirectory failed on %s: %08x %s", path.c_str(), (uint32_t)error, GetStringErrorMsg(error).c_str());
return false;
#else
- if (mkdir(fn.c_str(), 0755) == 0)
- return true;
-
- int err = errno;
- if (err == EEXIST)
- {
- WARN_LOG(COMMON, "CreateDir: mkdir failed on %s: already exists", fn.c_str());
+ if (mkdir(path.ToString().c_str(), 0755) == 0) {
return true;
}
- ERROR_LOG(COMMON, "CreateDir: mkdir failed on %s: %s", fn.c_str(), strerror(err));
+ int err = errno;
+ if (err == EEXIST) {
+ WARN_LOG(COMMON, "CreateDir: mkdir failed on %s: already exists", path.c_str());
+ return true;
+ }
+
+ ERROR_LOG(COMMON, "CreateDir: mkdir failed on %s: %s", path.c_str(), strerror(err));
return false;
#endif
}
// Creates the full path of fullPath returns true on success
bool CreateFullPath(const Path &path) {
- switch (path.Type()) {
- case PathType::NATIVE:
- break; // OK
- default:
- return false;
- }
-
- std::string fullPath = path.ToString();
- int panicCounter = 100;
- VERBOSE_LOG(COMMON, "CreateFullPath: '%s'", fullPath.c_str());
-
- if (File::Exists(fullPath)) {
- DEBUG_LOG(COMMON, "CreateFullPath: path already exists: '%s'", fullPath.c_str());
+ if (File::Exists(path)) {
+ DEBUG_LOG(COMMON, "CreateFullPath: path exists %s", path.c_str());
return true;
}
- size_t position = 0;
-
-#ifdef _WIN32
- // Skip the drive letter when looking for slashes, no need to create C:\.
- if (path.IsAbsolute()) {
- position = 3;
+ switch (path.Type()) {
+ case PathType::NATIVE:
+ case PathType::CONTENT_URI:
+ break; // OK
+ default:
+ ERROR_LOG(COMMON, "CreateFullPath(%s): Not yet supported", path.c_str());
+ return false;
}
-#endif
- while (true) {
- // Find next sub path
- position = fullPath.find_first_of(DIR_SEP_CHRS, position);
- // we're done, yay!
- if (position == fullPath.npos) {
- if (!File::Exists(fullPath))
- return File::CreateDir(Path(fullPath));
- return true;
- }
- std::string subPath = fullPath.substr(0, position);
- if (position != 0 && !File::Exists(subPath)) {
- File::CreateDir(Path(subPath));
- }
+ // The below code is entirely agnostic of path format.
- // A safety check
- panicCounter--;
- if (panicCounter <= 0) {
- ERROR_LOG(COMMON, "CreateFullPath: directory structure too deep");
- return false;
- }
- position++;
+ Path root = path.GetRootVolume();
+
+ std::string diff = root.PathTo(path); // always with forward slashes
+
+ std::vector parts;
+ SplitString(diff, '/', parts);
+
+ // Probably not necessary sanity check, ported from the old code.
+ if (parts.size() > 100) {
+ ERROR_LOG(COMMON, "CreateFullPath: directory structure too deep");
+ return false;
}
+
+ Path curPath = root;
+ for (auto &part : parts) {
+ curPath /= part;
+ if (!File::Exists(curPath)) {
+ File::CreateDir(curPath);
+ }
+ }
+
+ return true;
}
-// Deletes a directory filename, returns true on success
+// Deletes an empty directory, returns true on success
bool DeleteDir(const Path &path) {
switch (path.Type()) {
case PathType::NATIVE:
break; // OK
+ case PathType::CONTENT_URI:
+ return Android_RemoveFile(path.ToString());
default:
return false;
}
INFO_LOG(COMMON, "DeleteDir: directory %s", path.c_str());
// check if a directory
- if (!File::IsDirectory(path))
- {
+ if (!File::IsDirectory(path)) {
ERROR_LOG(COMMON, "DeleteDir: Not a directory %s", path.c_str());
return false;
}
@@ -444,11 +523,15 @@ bool Rename(const Path &srcFilename, const Path &destFilename)
switch (srcFilename.Type()) {
case PathType::NATIVE:
break; // OK
+ case PathType::CONTENT_URI:
+ ERROR_LOG_REPORT_ONCE(renameUriNotSupported, COMMON, "Moving files by Android URI is not yet supported");
+ return false;
default:
return false;
}
INFO_LOG(COMMON, "Rename: %s --> %s", srcFilename.c_str(), destFilename.c_str());
+
#if defined(_WIN32) && defined(UNICODE)
std::wstring srcw = srcFilename.ToWString();
std::wstring destw = destFilename.ToWString();
@@ -470,12 +553,18 @@ bool Copy(const Path &srcFilename, const Path &destFilename)
switch (srcFilename.Type()) {
case PathType::NATIVE:
break; // OK
+ case PathType::CONTENT_URI:
+ ERROR_LOG_REPORT_ONCE(copyUriNotSupported, COMMON, "Copying files by Android URI is not yet supported");
+ break;
default:
return false;
}
switch (destFilename.Type()) {
case PathType::NATIVE:
break; // OK
+ case PathType::CONTENT_URI:
+ ERROR_LOG_REPORT_ONCE(copyUriNotSupported, COMMON, "Copying files by Android URI is not yet supported");
+ return false;
default:
return false;
}
@@ -563,6 +652,23 @@ bool Move(const Path &srcFilename, const Path &destFilename) {
// Returns the size of file (64bit)
// TODO: Add a way to return an error.
uint64_t GetFileSize(const Path &filename) {
+ switch (filename.Type()) {
+ case PathType::NATIVE:
+ break; // OK
+ case PathType::CONTENT_URI:
+ {
+ FileInfo info;
+ if (Android_GetFileInfo(filename.ToString(), &info)) {
+ return info.size;
+ } else {
+ return 0;
+ }
+ }
+ break;
+ default:
+ return false;
+ }
+
#if defined(_WIN32) && defined(UNICODE)
WIN32_FILE_ATTRIBUTE_DATA attr;
if (!GetFileAttributesEx(filename.ToWString().c_str(), GetFileExInfoStandard, &attr))
@@ -591,8 +697,7 @@ uint64_t GetFileSize(const Path &filename) {
#endif
}
-uint64_t GetFileSize(FILE *f)
-{
+uint64_t GetFileSize(FILE *f) {
// This will only support 64-bit when large file support is available.
// That won't be the case on some versions of Android, at least.
#if defined(__ANDROID__) || (defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS < 64)
@@ -632,11 +737,10 @@ uint64_t GetFileSize(FILE *f)
// creates an empty file filename, returns true on success
bool CreateEmptyFile(const Path &filename) {
-
INFO_LOG(COMMON, "CreateEmptyFile: %s", filename.c_str());
FILE *pFile = OpenCFile(filename, "wb");
if (!pFile) {
- ERROR_LOG(COMMON, "CreateEmptyFile: failed %s: %s", filename.c_str(), GetLastErrorMsg().c_str());
+ ERROR_LOG(COMMON, "CreateEmptyFile: failed to create '%s': %s", filename.c_str(), GetLastErrorMsg().c_str());
return false;
}
fclose(pFile);
@@ -644,72 +748,38 @@ bool CreateEmptyFile(const Path &filename) {
}
// Deletes the given directory and anything under it. Returns true on success.
-bool DeleteDirRecursively(const Path &directory) {
- //Removed check, it prevents the UWP from deleting store downloads
- INFO_LOG(COMMON, "DeleteDirRecursively: %s", directory.c_str());
-
-#ifdef _WIN32
-
- // Find the first file in the directory.
- WIN32_FIND_DATA ffd;
- HANDLE hFind = FindFirstFile((directory.ToWString() + L"\\*").c_str(), &ffd);
- if (hFind == INVALID_HANDLE_VALUE) {
+bool DeleteDirRecursively(const Path &directory) {
+ switch (directory.Type()) {
+ case PathType::CONTENT_URI:
+ case PathType::NATIVE:
+ break; // OK
+ default:
+ ERROR_LOG(COMMON, "DeleteDirRecursively: Path type not supported");
return false;
}
-
- // windows loop
- do {
- const std::string virtualName = ConvertWStringToUTF8(ffd.cFileName);
-#else
- struct dirent *result = NULL;
- DIR *dirp = opendir(directory.c_str());
- if (!dirp)
- return false;
- // non windows loop
- while ((result = readdir(dirp))) {
- const std::string virtualName = result->d_name;
-#endif
- // check for "." and ".."
- if (((virtualName[0] == '.') && (virtualName[1] == '\0')) ||
- ((virtualName[0] == '.') && (virtualName[1] == '.') &&
- (virtualName[2] == '\0')))
- continue;
-
- Path newPath = directory / virtualName;
- if (IsDirectory(newPath)) {
- if (!DeleteDirRecursively(newPath)) {
-#ifndef _WIN32
- closedir(dirp);
-#else
- FindClose(hFind);
-#endif
- return false;
- }
+ std::vector files;
+ GetFilesInDir(directory, &files, nullptr, GETFILES_GETHIDDEN);
+ for (const auto &file : files) {
+ if (file.isDirectory) {
+ DeleteDirRecursively(file.fullName);
+ } else {
+ Delete(file.fullName);
}
- else {
- if (!File::Delete(newPath)) {
-#ifndef _WIN32
- closedir(dirp);
-#else
- FindClose(hFind);
-#endif
- return false;
- }
- }
-
-#ifdef _WIN32
- } while (FindNextFile(hFind, &ffd) != 0);
- FindClose(hFind);
-#else
}
- closedir(dirp);
-#endif
- return File::DeleteDir(directory);
+ return DeleteDir(directory);
}
-void OpenFileInEditor(const Path &fileName) {
-#if defined(_WIN32)
+bool OpenFileInEditor(const Path &fileName) {
+ switch (fileName.Type()) {
+ case PathType::NATIVE:
+ break; // OK
+ default:
+ ERROR_LOG(COMMON, "OpenFileInEditor(%s): Path type not supported", fileName.c_str());
+ return false;
+ }
+
+#if PPSSPP_PLATFORM(WINDOWS)
#if PPSSPP_PLATFORM(UWP)
// Do nothing.
#else
@@ -729,6 +799,7 @@ void OpenFileInEditor(const Path &fileName) {
ERROR_LOG(COMMON, "Failed to launch ini file");
}
#endif
+ return true;
}
const Path &GetExeDirectory() {
@@ -791,32 +862,15 @@ const Path &GetExeDirectory() {
return ExePath;
}
-IOFile::IOFile() {}
-IOFile::IOFile(std::FILE* file)
- : m_file(file)
-{}
-
-IOFile::IOFile(const std::string &filename, const char openmode[]) {
+IOFile::IOFile(const Path &filename, const char openmode[]) {
Open(filename, openmode);
}
-IOFile::IOFile(const Path &filename, const char openmode[]) {
- Open(filename.ToString(), openmode);
-}
-
IOFile::~IOFile() {
Close();
}
-bool IOFile::Open(const std::string& filename, const char openmode[])
-{
- Close();
- m_file = File::OpenCFile(Path(filename), openmode);
- m_good = IsOpen();
- return m_good;
-}
-
bool IOFile::Open(const Path& filename, const char openmode[])
{
Close();
diff --git a/Common/File/FileUtil.h b/Common/File/FileUtil.h
index 1933f5893c..ab0782fb22 100644
--- a/Common/File/FileUtil.h
+++ b/Common/File/FileUtil.h
@@ -27,6 +27,8 @@
#include "Common/Common.h"
#include "Common/File/Path.h"
+// Some functions here support Android content URIs. These are marked as such.
+
#ifdef _MSC_VER
inline struct tm* localtime_r(const time_t *clock, struct tm *result) {
if (localtime_s(result, clock) == 0)
@@ -40,6 +42,21 @@ namespace File {
// Mostly to handle UTF-8 filenames better on Windows.
FILE *OpenCFile(const Path &filename, const char *mode);
+// Reminiscent of PSP's FileAccess enum, due to its use in emulating it.
+enum OpenFlag {
+ OPEN_NONE = 0,
+ OPEN_READ = 1,
+ OPEN_WRITE = 2,
+ OPEN_APPEND = 4,
+ OPEN_CREATE = 8,
+ OPEN_TRUNCATE = 16,
+ // EXCL?
+};
+
+// TODO: This currently only handles Android Content URIs, but might port the rest
+// of DirectoryFileSystem::Open here eventually for symmetry.
+int OpenFD(const Path &filename, OpenFlag flags);
+
// Resolves symlinks and similar.
std::string ResolvePath(const std::string &path);
@@ -97,7 +114,7 @@ bool CreateEmptyFile(const Path &filename);
// Opens ini file (cheats, texture replacements etc.)
// TODO: Belongs in System or something.
-void OpenFileInEditor(const Path &fileName);
+bool OpenFileInEditor(const Path &fileName);
// TODO: Belongs in System or something.
const Path &GetExeDirectory();
@@ -107,8 +124,7 @@ const Path &GetExeDirectory();
// and make forgetting an fclose() harder
class IOFile {
public:
- IOFile();
- IOFile(FILE* file);
+ IOFile() {}
IOFile(const Path &filename, const char openmode[]);
~IOFile();
@@ -173,9 +189,6 @@ public:
}
private:
- IOFile(const std::string &filename, const char openmode[]);
- bool Open(const std::string &filename, const char openmode[]);
-
std::FILE *m_file = nullptr;
bool m_good = true;
};
diff --git a/Common/File/Path.cpp b/Common/File/Path.cpp
index 0f340dda06..a45112599e 100644
--- a/Common/File/Path.cpp
+++ b/Common/File/Path.cpp
@@ -6,11 +6,16 @@
#include "Common/Log.h"
#include "Common/Data/Encoding/Utf8.h"
+#include "android/jni/app-android.h"
+#include "android/jni/AndroidContentURI.h"
+
Path::Path(const std::string &str) {
if (str.empty()) {
type_ = PathType::UNDEFINED;
} else if (startsWith(str, "http://") || startsWith(str, "https://")) {
type_ = PathType::HTTP;
+ } else if (Android_IsContentUri(str)) {
+ type_ = PathType::CONTENT_URI;
} else {
type_ = PathType::NATIVE;
}
@@ -46,6 +51,13 @@ void Path::Init(const std::string &str) {
// We always use forward slashes internally, we convert to backslash only when
// converted to a wstring.
Path Path::operator /(const std::string &subdir) const {
+ if (type_ == PathType::CONTENT_URI) {
+ AndroidContentURI uri(path_);
+ return Path(uri.WithComponent(subdir).ToString());
+ }
+
+ // Direct string manipulation.
+
if (subdir.empty()) {
return Path(path_);
}
@@ -66,11 +78,21 @@ void Path::operator /=(const std::string &subdir) {
}
Path Path::WithExtraExtension(const std::string &ext) const {
+ if (type_ == PathType::CONTENT_URI) {
+ AndroidContentURI uri(path_);
+ return Path(uri.WithExtraExtension(ext).ToString());
+ }
+
_dbg_assert_(!ext.empty() && ext[0] == '.');
return Path(path_ + ext);
}
Path Path::WithReplacedExtension(const std::string &oldExtension, const std::string &newExtension) const {
+ if (type_ == PathType::CONTENT_URI) {
+ AndroidContentURI uri(path_);
+ return Path(uri.WithReplacedExtension(oldExtension, newExtension).ToString());
+ }
+
_dbg_assert_(!oldExtension.empty() && oldExtension[0] == '.');
_dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
if (endsWithNoCase(path_, oldExtension)) {
@@ -92,6 +114,10 @@ Path Path::WithReplacedExtension(const std::string &newExtension) const {
}
std::string Path::GetFilename() const {
+ if (type_ == PathType::CONTENT_URI) {
+ AndroidContentURI uri(path_);
+ return uri.GetLastPart();
+ }
size_t pos = path_.rfind('/');
if (pos != std::string::npos) {
return path_.substr(pos + 1);
@@ -99,24 +125,39 @@ std::string Path::GetFilename() const {
return path_;
}
-std::string Path::GetFileExtension() const {
- size_t pos = path_.rfind(".");
+static std::string GetExtFromString(const std::string &str) {
+ size_t pos = str.rfind(".");
if (pos == std::string::npos) {
return "";
}
- size_t slash_pos = path_.rfind("/");
+ size_t slash_pos = str.rfind("/");
if (slash_pos != std::string::npos && slash_pos > pos) {
// Don't want to detect "df/file" from "/as.df/file"
return "";
}
- std::string ext = path_.substr(pos);
+ std::string ext = str.substr(pos);
for (size_t i = 0; i < ext.size(); i++) {
ext[i] = tolower(ext[i]);
}
return ext;
}
+std::string Path::GetFileExtension() const {
+ if (type_ == PathType::CONTENT_URI) {
+ AndroidContentURI uri(path_);
+ return GetExtFromString(uri.FilePath());
+ }
+ return GetExtFromString(path_);
+}
+
std::string Path::GetDirectory() const {
+ if (type_ == PathType::CONTENT_URI) {
+ // Unclear how meaningful this is.
+ AndroidContentURI uri(path_);
+ uri.NavigateUp();
+ return uri.ToString();
+ }
+
size_t pos = path_.rfind('/');
if (type_ == PathType::HTTP) {
// Things are a bit different for HTTP, because we probably ended with /.
@@ -151,7 +192,12 @@ std::string Path::GetDirectory() const {
}
bool Path::FilePathContains(const std::string &needle) const {
- const std::string &haystack = path_;
+ std::string haystack;
+ if (type_ == PathType::CONTENT_URI) {
+ haystack = AndroidContentURI(path_).FilePath();
+ } else {
+ haystack = path_;
+ }
return haystack.find(needle) != std::string::npos;
}
@@ -180,10 +226,17 @@ std::wstring Path::ToWString() const {
#endif
std::string Path::ToVisualString() const {
+ if (type_ == PathType::CONTENT_URI) {
+ return AndroidContentURI(path_).ToVisualString();
+ }
return path_;
}
bool Path::CanNavigateUp() const {
+ if (type_ == PathType::CONTENT_URI) {
+ return AndroidContentURI(path_).CanNavigateUp();
+ }
+
if (path_ == "/" || path_ == "") {
return false;
}
@@ -198,11 +251,44 @@ bool Path::CanNavigateUp() const {
}
Path Path::NavigateUp() const {
+ if (type_ == PathType::CONTENT_URI) {
+ AndroidContentURI uri(path_);
+ uri.NavigateUp();
+ return Path(uri.ToString());
+ }
std::string dir = GetDirectory();
return Path(dir);
}
+Path Path::GetRootVolume() const {
+ if (!IsAbsolute()) {
+ // Can't do anything
+ return Path(path_);
+ }
+
+ if (type_ == PathType::CONTENT_URI) {
+ AndroidContentURI uri(path_);
+ AndroidContentURI rootPath = uri.WithRootFilePath("");
+ return Path(rootPath.ToString());
+ }
+
+#if PPSSPP_PLATFORM(WINDOWS)
+ if (path_[1] == ':') {
+ // Windows path with drive letter
+ std::string path = path_.substr(0, 2);
+ return Path(path);
+ }
+#endif
+
+ return Path("/");
+}
+
bool Path::IsAbsolute() const {
+ if (type_ == PathType::CONTENT_URI) {
+ // These don't exist in relative form.
+ return true;
+ }
+
if (path_.empty())
return true;
else if (path_.front() == '/')
@@ -214,3 +300,28 @@ bool Path::IsAbsolute() const {
else
return false;
}
+
+std::string Path::PathTo(const Path &other) {
+ if (!other.StartsWith(*this)) {
+ // Can't do this. Should return an error.
+ return std::string();
+ }
+
+ std::string diff;
+
+ if (type_ == PathType::CONTENT_URI) {
+ AndroidContentURI a(path_);
+ AndroidContentURI b(other.path_);
+ if (a.RootPath() != b.RootPath()) {
+ // No common root, can't do anything
+ return std::string();
+ }
+ diff = a.PathTo(b);
+ } else if (path_ == "/") {
+ diff = other.path_.substr(1);
+ } else {
+ diff = other.path_.substr(path_.size() + 1);
+ }
+
+ return diff;
+}
diff --git a/Common/File/Path.h b/Common/File/Path.h
index 258287f8cb..53fc8745e2 100644
--- a/Common/File/Path.h
+++ b/Common/File/Path.h
@@ -6,9 +6,8 @@
enum class PathType {
UNDEFINED = 0,
- NATIVE = 1,
- // RELATIVE, // (do we need this?)
- // CONTENT_URI = 2, // Android only
+ NATIVE = 1, // Can be relative.
+ CONTENT_URI = 2, // Android only. Can only be absolute!
HTTP = 3, // http://, https://
};
@@ -44,7 +43,7 @@ public:
return path_.size();
}
- // WARNING: Unsafe usage.
+ // WARNING: Potentially unsafe usage, if it's not NATIVE.
const char *c_str() const {
return path_.c_str();
}
@@ -78,6 +77,12 @@ public:
bool CanNavigateUp() const;
Path NavigateUp() const;
+ // Navigates as far up as possible from this path. If not possible to navigate upwards, returns the same path.
+ // Not actually always the root of the volume, especially on systems like Mac and Linux where things are often mounted.
+ Path GetRootVolume() const;
+
+ std::string PathTo(const Path &child);
+
bool operator ==(const Path &other) const {
return path_ == other.path_ && type_ == other.type_;
}
diff --git a/Common/File/PathBrowser.cpp b/Common/File/PathBrowser.cpp
index 2ef8c092ec..f7f426544a 100644
--- a/Common/File/PathBrowser.cpp
+++ b/Common/File/PathBrowser.cpp
@@ -9,6 +9,7 @@
#include "Common/File/PathBrowser.h"
#include "Common/File/FileUtil.h"
+#include "Common/File/DirListing.h"
#include "Common/StringUtils.h"
#include "Common/TimeUtil.h"
#include "Common/Log.h"
@@ -16,6 +17,11 @@
#include "Core/System.h"
+#if PPSSPP_PLATFORM(ANDROID)
+#include "android/jni/app-android.h"
+#include "android/jni/AndroidContentURI.h"
+#endif
+
bool LoadRemoteFileList(const Path &url, bool *cancel, std::vector &files) {
_dbg_assert_(url.Type() == PathType::HTTP);
@@ -97,33 +103,6 @@ bool LoadRemoteFileList(const Path &url, bool *cancel, std::vector ApplyFilter(std::vector files, const char *filter) {
- std::set filters;
- if (filter) {
- std::string tmp;
- while (*filter) {
- if (*filter == ':') {
- filters.insert("." + tmp);
- tmp.clear();
- } else {
- tmp.push_back(*filter);
- }
- filter++;
- }
- if (!tmp.empty())
- filters.insert("." + tmp);
- }
-
- auto pred = [&](const File::FileInfo &info) {
- if (info.isDirectory || !filter)
- return false;
- std::string ext = info.fullName.GetFileExtension();
- return filters.find(ext) == filters.end();
- };
- files.erase(std::remove_if(files.begin(), files.end(), pred), files.end());
- return files;
-}
-
PathBrowser::~PathBrowser() {
std::unique_lock guard(pendingLock_);
pendingCancel_ = true;
@@ -149,13 +128,6 @@ void PathBrowser::HandlePath() {
return;
}
- if (path_.Type() != PathType::HTTP) {
- if (pendingActive_)
- ResetPending();
- ready_ = true;
- return;
- }
-
std::lock_guard guard(pendingLock_);
ready_ = false;
pendingActive_ = true;
@@ -184,6 +156,12 @@ void PathBrowser::HandlePath() {
results.clear();
success = LoadRemoteFileList(lastPath, &pendingCancel_, results);
guard.lock();
+ } else {
+ guard.unlock();
+ results.clear();
+ File::GetFilesInDir(lastPath, &results, nullptr);
+ success = true;
+ guard.lock();
}
if (pendingPath_ == lastPath) {
@@ -231,36 +209,12 @@ bool PathBrowser::GetListing(std::vector &fileInfo, const char *
while (!IsListingReady() && (!cancel || !*cancel)) {
// In case cancel changes, just sleep.
guard.unlock();
- sleep_ms(100);
+ sleep_ms(50);
guard.lock();
}
-#ifdef _WIN32
- if (path_.IsRoot()) {
- // Special path that means root of file system.
- std::vector drives = File::GetWindowsDrives();
- for (auto drive = drives.begin(); drive != drives.end(); ++drive) {
- if (*drive == "A:/" || *drive == "B:/")
- continue;
- File::FileInfo fake;
- fake.fullName = Path(*drive);
- fake.name = *drive;
- fake.isDirectory = true;
- fake.exists = true;
- fake.size = 0;
- fake.isWritable = false;
- fileInfo.push_back(fake);
- }
- }
-#endif
-
- if (path_.Type() == PathType::HTTP) {
- fileInfo = ApplyFilter(pendingFiles_, filter);
- return true;
- } else {
- File::GetFilesInDir(path_, &fileInfo, filter);
- return true;
- }
+ fileInfo = ApplyFilter(pendingFiles_, filter);
+ return true;
}
bool PathBrowser::CanNavigateUp() {
diff --git a/Common/GPU/D3D11/thin3d_d3d11.cpp b/Common/GPU/D3D11/thin3d_d3d11.cpp
index dff6367f36..9efb731922 100644
--- a/Common/GPU/D3D11/thin3d_d3d11.cpp
+++ b/Common/GPU/D3D11/thin3d_d3d11.cpp
@@ -26,6 +26,8 @@
namespace Draw {
+static constexpr int MAX_BOUND_TEXTURES = 8;
+
// A problem is that we can't get the D3Dcompiler.dll without using a later SDK than 7.1, which was the last that
// supported XP. A possible solution might be here:
// https://tedwvc.wordpress.com/2014/01/01/how-to-target-xp-with-vc2012-or-vc2013-and-continue-to-use-the-windows-8-x-sdk/
@@ -1162,6 +1164,7 @@ void D3D11DrawContext::UpdateBuffer(Buffer *buffer, const uint8_t *data, size_t
}
void D3D11DrawContext::BindVertexBuffers(int start, int count, Buffer **buffers, int *offsets) {
+ _assert_(start + count <= ARRAY_SIZE(nextVertexBuffers_));
// Lazy application
for (int i = 0; i < count; i++) {
D3D11Buffer *buf = (D3D11Buffer *)buffers[i];
@@ -1328,7 +1331,8 @@ Framebuffer *D3D11DrawContext::CreateFramebuffer(const FramebufferDesc &desc) {
void D3D11DrawContext::BindTextures(int start, int count, Texture **textures) {
// Collect the resource views from the textures.
- ID3D11ShaderResourceView *views[8];
+ ID3D11ShaderResourceView *views[MAX_BOUND_TEXTURES];
+ _assert_(start + count <= ARRAY_SIZE(views));
for (int i = 0; i < count; i++) {
D3D11Texture *tex = (D3D11Texture *)textures[i];
views[i] = tex ? tex->view : nullptr;
@@ -1337,7 +1341,8 @@ void D3D11DrawContext::BindTextures(int start, int count, Texture **textures) {
}
void D3D11DrawContext::BindSamplerStates(int start, int count, SamplerState **states) {
- ID3D11SamplerState *samplers[8];
+ ID3D11SamplerState *samplers[MAX_BOUND_TEXTURES];
+ _assert_(start + count <= ARRAY_SIZE(samplers));
for (int i = 0; i < count; i++) {
D3D11SamplerState *samp = (D3D11SamplerState *)states[i];
samplers[i] = samp->ss;
@@ -1613,6 +1618,7 @@ void D3D11DrawContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const Ren
}
void D3D11DrawContext::BindFramebufferAsTexture(Framebuffer *fbo, int binding, FBChannel channelBit, int attachment) {
+ _assert_(binding < MAX_BOUND_TEXTURES);
D3D11Framebuffer *fb = (D3D11Framebuffer *)fbo;
switch (channelBit) {
case FBChannel::FB_COLOR_BIT:
diff --git a/Common/GPU/D3D9/thin3d_d3d9.cpp b/Common/GPU/D3D9/thin3d_d3d9.cpp
index 79a0bb0dca..39a8b9d5b7 100644
--- a/Common/GPU/D3D9/thin3d_d3d9.cpp
+++ b/Common/GPU/D3D9/thin3d_d3d9.cpp
@@ -28,6 +28,8 @@
namespace Draw {
+static constexpr int MAX_BOUND_TEXTURES = 8;
+
// Could be declared as u8
static const D3DCMPFUNC compareToD3D9[] = {
D3DCMP_NEVER,
@@ -525,12 +527,14 @@ public:
void BindTextures(int start, int count, Texture **textures) override;
void BindSamplerStates(int start, int count, SamplerState **states) override {
+ _assert_(start + count <= MAX_BOUND_TEXTURES);
for (int i = 0; i < count; ++i) {
D3D9SamplerState *s = static_cast(states[i]);
s->Apply(device_, start + i);
}
}
void BindVertexBuffers(int start, int count, Buffer **buffers, int *offsets) override {
+ _assert_(start + count <= ARRAY_SIZE(curVBuffers_));
for (int i = 0; i < count; i++) {
curVBuffers_[i + start] = (D3D9Buffer *)buffers[i];
curVBufferOffsets_[i + start] = offsets ? offsets[i] : 0;
@@ -785,6 +789,7 @@ Texture *D3D9Context::CreateTexture(const TextureDesc &desc) {
}
void D3D9Context::BindTextures(int start, int count, Texture **textures) {
+ _assert_(start + count <= MAX_BOUND_TEXTURES);
for (int i = start; i < start + count; i++) {
D3D9Texture *tex = static_cast(textures[i - start]);
if (tex) {
@@ -1182,6 +1187,7 @@ uintptr_t D3D9Context::GetFramebufferAPITexture(Framebuffer *fbo, int channelBit
}
void D3D9Context::BindFramebufferAsTexture(Framebuffer *fbo, int binding, FBChannel channelBit, int color) {
+ _assert_(binding < MAX_BOUND_TEXTURES);
D3D9Framebuffer *fb = (D3D9Framebuffer *)fbo;
switch (channelBit) {
case FB_DEPTH_BIT:
diff --git a/Common/GPU/OpenGL/GLQueueRunner.cpp b/Common/GPU/OpenGL/GLQueueRunner.cpp
index 3f3dec4c4d..a2d66b9a86 100644
--- a/Common/GPU/OpenGL/GLQueueRunner.cpp
+++ b/Common/GPU/OpenGL/GLQueueRunner.cpp
@@ -17,7 +17,7 @@
#include "GLRenderManager.h"
#include "DataFormatGL.h"
-#define TEXCACHE_NAME_CACHE_SIZE 16
+static constexpr int TEXCACHE_NAME_CACHE_SIZE = 16;
#if PPSSPP_PLATFORM(IOS)
extern void bindDefaultFBO();
@@ -793,6 +793,7 @@ void GLQueueRunner::PerformRenderPass(const GLRStep &step, bool first, bool last
bool blendEnabled = false;
bool cullEnabled = false;
bool ditherEnabled = false;
+ bool depthClampEnabled = false;
#ifndef USING_GLES2
int logicOp = -1;
bool logicEnabled = false;
@@ -800,7 +801,7 @@ void GLQueueRunner::PerformRenderPass(const GLRStep &step, bool first, bool last
GLuint blendEqColor = (GLuint)-1;
GLuint blendEqAlpha = (GLuint)-1;
- GLRTexture *curTex[8]{};
+ GLRTexture *curTex[MAX_GL_TEXTURE_SLOTS]{};
CHECK_GL_ERROR_IF_DEBUG();
auto &commands = step.commands;
@@ -1283,6 +1284,17 @@ void GLQueueRunner::PerformRenderPass(const GLRStep &step, bool first, bool last
glDisable(GL_DITHER);
ditherEnabled = false;
}
+#ifndef USING_GLES2
+ if (c.raster.depthClampEnable) {
+ if (!depthClampEnabled) {
+ glEnable(GL_DEPTH_CLAMP);
+ depthClampEnabled = true;
+ }
+ } else if (!c.raster.depthClampEnable && depthClampEnabled) {
+ glDisable(GL_DEPTH_CLAMP);
+ depthClampEnabled = false;
+ }
+#endif
CHECK_GL_ERROR_IF_DEBUG();
break;
default:
@@ -1322,6 +1334,8 @@ void GLQueueRunner::PerformRenderPass(const GLRStep &step, bool first, bool last
if (cullEnabled)
glDisable(GL_CULL_FACE);
#ifndef USING_GLES2
+ if (depthClampEnabled)
+ glDisable(GL_DEPTH_CLAMP);
if (!gl_extensions.IsGLES && logicEnabled) {
glDisable(GL_COLOR_LOGIC_OP);
}
diff --git a/Common/GPU/OpenGL/GLQueueRunner.h b/Common/GPU/OpenGL/GLQueueRunner.h
index 07837421be..8dec753949 100644
--- a/Common/GPU/OpenGL/GLQueueRunner.h
+++ b/Common/GPU/OpenGL/GLQueueRunner.h
@@ -196,6 +196,7 @@ struct GLRRenderData {
GLenum frontFace;
GLenum cullFace;
GLboolean ditherEnable;
+ GLboolean depthClampEnable;
} raster;
};
};
diff --git a/Common/GPU/OpenGL/GLRenderManager.cpp b/Common/GPU/OpenGL/GLRenderManager.cpp
index 6229aa676a..1475d8d5ff 100644
--- a/Common/GPU/OpenGL/GLRenderManager.cpp
+++ b/Common/GPU/OpenGL/GLRenderManager.cpp
@@ -357,6 +357,7 @@ void GLRenderManager::BindFramebufferAsRenderTarget(GLRFramebuffer *fb, GLRRende
void GLRenderManager::BindFramebufferAsTexture(GLRFramebuffer *fb, int binding, int aspectBit, int attachment) {
_dbg_assert_(curRenderStep_ && curRenderStep_->stepType == GLRStepType::RENDER);
+ _dbg_assert_(binding < MAX_GL_TEXTURE_SLOTS);
GLRRenderData data{ GLRRenderCommand::BIND_FB_TEXTURE };
data.bind_fb_texture.slot = binding;
data.bind_fb_texture.framebuffer = fb;
diff --git a/Common/GPU/OpenGL/GLRenderManager.h b/Common/GPU/OpenGL/GLRenderManager.h
index 69cd0e9d37..b4c3378739 100644
--- a/Common/GPU/OpenGL/GLRenderManager.h
+++ b/Common/GPU/OpenGL/GLRenderManager.h
@@ -21,6 +21,8 @@ namespace Draw {
class DrawContext;
}
+constexpr int MAX_GL_TEXTURE_SLOTS = 8;
+
class GLRTexture {
public:
GLRTexture(int width, int height, int numMips);
@@ -571,8 +573,8 @@ public:
void BindTexture(int slot, GLRTexture *tex) {
_dbg_assert_(curRenderStep_ && curRenderStep_->stepType == GLRStepType::RENDER);
+ _dbg_assert_(slot < MAX_GL_TEXTURE_SLOTS);
GLRRenderData data{ GLRRenderCommand::BINDTEXTURE };
- _dbg_assert_(slot < 16);
data.texture.slot = slot;
data.texture.texture = tex;
curRenderStep_->commands.push_back(data);
@@ -811,19 +813,21 @@ public:
curRenderStep_->commands.push_back(data);
}
- void SetRaster(GLboolean cullEnable, GLenum frontFace, GLenum cullFace, GLboolean ditherEnable) {
+ void SetRaster(GLboolean cullEnable, GLenum frontFace, GLenum cullFace, GLboolean ditherEnable, GLboolean depthClamp) {
_dbg_assert_(curRenderStep_ && curRenderStep_->stepType == GLRStepType::RENDER);
GLRRenderData data{ GLRRenderCommand::RASTER };
data.raster.cullEnable = cullEnable;
data.raster.frontFace = frontFace;
data.raster.cullFace = cullFace;
data.raster.ditherEnable = ditherEnable;
+ data.raster.depthClampEnable = depthClamp;
curRenderStep_->commands.push_back(data);
}
// Modifies the current texture as per GL specs, not global state.
void SetTextureSampler(int slot, GLenum wrapS, GLenum wrapT, GLenum magFilter, GLenum minFilter, float anisotropy) {
_dbg_assert_(curRenderStep_ && curRenderStep_->stepType == GLRStepType::RENDER);
+ _dbg_assert_(slot < MAX_GL_TEXTURE_SLOTS);
GLRRenderData data{ GLRRenderCommand::TEXTURESAMPLER };
data.textureSampler.slot = slot;
data.textureSampler.wrapS = wrapS;
@@ -836,6 +840,7 @@ public:
void SetTextureLod(int slot, float minLod, float maxLod, float lodBias) {
_dbg_assert_(curRenderStep_ && curRenderStep_->stepType == GLRStepType::RENDER);
+ _dbg_assert_(slot < MAX_GL_TEXTURE_SLOTS);
GLRRenderData data{ GLRRenderCommand::TEXTURELOD};
data.textureLod.slot = slot;
data.textureLod.minLod = minLod;
diff --git a/Common/GPU/OpenGL/thin3d_gl.cpp b/Common/GPU/OpenGL/thin3d_gl.cpp
index 454cde20f5..d26f1ab8af 100644
--- a/Common/GPU/OpenGL/thin3d_gl.cpp
+++ b/Common/GPU/OpenGL/thin3d_gl.cpp
@@ -188,7 +188,7 @@ public:
class OpenGLRasterState : public RasterState {
public:
void Apply(GLRenderManager *render) {
- render->SetRaster(cullEnable, frontFace, cullMode, false);
+ render->SetRaster(cullEnable, frontFace, cullMode, GL_FALSE, GL_FALSE);
}
GLboolean cullEnable;
@@ -299,7 +299,7 @@ public:
// TODO: Optimize by getting the locations first and putting in a custom struct
UniformBufferDesc dynamicUniforms;
- GLint samplerLocs_[8]{};
+ GLint samplerLocs_[MAX_TEXTURE_SLOTS]{};
std::vector dynamicUniformLocs_;
GLRProgram *program_ = nullptr;
@@ -368,9 +368,7 @@ public:
void GetFramebufferDimensions(Framebuffer *fbo, int *w, int *h) override;
void BindSamplerStates(int start, int count, SamplerState **states) override {
- if (start + count > MAX_TEXTURE_SLOTS) {
- return;
- }
+ _assert_(start + count <= MAX_TEXTURE_SLOTS);
for (int i = 0; i < count; i++) {
int index = i + start;
boundSamplers_[index] = static_cast(states[i]);
@@ -402,6 +400,7 @@ public:
void BindTextures(int start, int count, Texture **textures) override;
void BindPipeline(Pipeline *pipeline) override;
void BindVertexBuffers(int start, int count, Buffer **buffers, int *offsets) override {
+ _assert_(start + count <= ARRAY_SIZE(curVBuffers_));
for (int i = 0; i < count; i++) {
curVBuffers_[i + start] = (OpenGLBuffer *)buffers[i];
curVBufferOffsets_[i + start] = offsets ? offsets[i] : 0;
@@ -534,6 +533,12 @@ OpenGLContext::OpenGLContext() {
}
caps_.framebufferBlitSupported = gl_extensions.NV_framebuffer_blit || gl_extensions.ARB_framebuffer_object;
caps_.framebufferDepthBlitSupported = caps_.framebufferBlitSupported;
+ caps_.depthClampSupported = gl_extensions.ARB_depth_clamp;
+
+ // Interesting potential hack for emulating GL_DEPTH_CLAMP (use a separate varying, force depth in fragment shader):
+ // This will induce a performance penalty on many architectures though so a blanket enable of this
+ // is probably not a good idea.
+ // https://stackoverflow.com/questions/5960757/how-to-emulate-gl-depth-clamp-nv
switch (gl_extensions.gpuVendor) {
case GPU_VENDOR_AMD: caps_.vendor = GPUVendor::VENDOR_AMD; break;
@@ -1070,9 +1075,7 @@ Pipeline *OpenGLContext::CreateGraphicsPipeline(const PipelineDesc &desc) {
}
void OpenGLContext::BindTextures(int start, int count, Texture **textures) {
- if (start + count > MAX_TEXTURE_SLOTS) {
- return;
- }
+ _assert_(start + count <= MAX_TEXTURE_SLOTS);
for (int i = start; i < start + count; i++) {
OpenGLTexture *glTex = static_cast(textures[i - start]);
if (!glTex) {
@@ -1151,12 +1154,14 @@ bool OpenGLPipeline::LinkShaders() {
std::vector queries;
queries.push_back({ &samplerLocs_[0], "sampler0" });
queries.push_back({ &samplerLocs_[1], "sampler1" });
+ queries.push_back({ &samplerLocs_[2], "sampler2" });
+ _assert_(queries.size() >= MAX_TEXTURE_SLOTS);
for (size_t i = 0; i < dynamicUniforms.uniforms.size(); ++i) {
queries.push_back({ &dynamicUniformLocs_[i], dynamicUniforms.uniforms[i].name });
}
std::vector initialize;
- initialize.push_back({ &samplerLocs_[0], 0, 0 });
- initialize.push_back({ &samplerLocs_[1], 0, 1 });
+ for (int i = 0; i < MAX_TEXTURE_SLOTS; ++i)
+ initialize.push_back({ &samplerLocs_[i], 0, i });
program_ = render_->CreateProgram(linkShaders, semantics, queries, initialize, false);
return true;
}
@@ -1354,6 +1359,7 @@ bool OpenGLContext::BlitFramebuffer(Framebuffer *fbsrc, int srcX1, int srcY1, in
void OpenGLContext::BindFramebufferAsTexture(Framebuffer *fbo, int binding, FBChannel channelBit, int color) {
OpenGLFramebuffer *fb = (OpenGLFramebuffer *)fbo;
+ _assert_(binding < MAX_TEXTURE_SLOTS);
GLuint aspect = 0;
if (channelBit & FB_COLOR_BIT) {
diff --git a/Common/GPU/ShaderTranslation.cpp b/Common/GPU/ShaderTranslation.cpp
index 5d92d2fd7d..34de213451 100644
--- a/Common/GPU/ShaderTranslation.cpp
+++ b/Common/GPU/ShaderTranslation.cpp
@@ -85,6 +85,7 @@ cbuffer data : register(b0) {
float2 u_texelDelta;
float2 u_pixelDelta;
float4 u_time;
+ float4 u_timeDelta;
float4 u_setting;
float u_video;
};
@@ -101,6 +102,7 @@ layout (std140, set = 0, binding = 0) uniform Data {
vec2 u_texelDelta;
vec2 u_pixelDelta;
vec4 u_time;
+ vec4 u_timeDelta;
vec4 u_setting;
float u_video;
};
@@ -111,8 +113,9 @@ float4 gl_HalfPixel : register(c0);
float2 u_texelDelta : register(c1);
float2 u_pixelDelta : register(c2);
float4 u_time : register(c3);
-float4 u_setting : register(c4);
-float u_video : register(c5);
+float4 u_timeDelta : register(c4);
+float4 u_setting : register(c5);
+float u_video : register(c6);
)";
// SPIRV-Cross' HLSL output has some deficiencies we need to work around.
@@ -135,12 +138,9 @@ std::string Postprocess(std::string code, ShaderLanguage lang, ShaderStage stage
std::string line;
std::stringstream instream(code);
while (std::getline(instream, line)) {
- if (line == "uniform sampler2D sampler0;" && lang == HLSL_D3D9) {
- out << "sampler2D sampler0 : register(s0);\n";
- continue;
- }
- if (line == "uniform sampler2D sampler1;" && lang == HLSL_D3D9) {
- out << "sampler2D sampler1 : register(s1);\n";
+ int num;
+ if (lang == HLSL_D3D9 && sscanf(line.c_str(), "uniform sampler2D sampler%d;", &num) == 1) {
+ out << "sampler2D sampler" << num << " : register(s" << num << ");\n";
continue;
}
if (line.find("uniform float") != std::string::npos) {
@@ -184,7 +184,9 @@ bool ConvertToVulkanGLSL(std::string *dest, TranslatedShaderMetadata *destMetada
if (line.find("uniform bool") != std::string::npos) {
continue;
} else if (line.find("uniform sampler2D") == 0) {
- if (line.find("sampler0") != line.npos)
+ if (sscanf(line.c_str(), "uniform sampler2D sampler%d", &num) == 1)
+ line = StringFromFormat("layout(set = 0, binding = %d) ", num + 1) + line;
+ else if (line.find("sampler0") != line.npos)
line = "layout(set = 0, binding = 1) " + line;
else
line = "layout(set = 0, binding = 2) " + line;
@@ -299,8 +301,11 @@ bool TranslateShader(std::string *dest, ShaderLanguage destLang, const ShaderLan
int i = 0;
for (auto &resource : resources.sampled_images) {
- // int location = hlsl.get_decoration(resource.id, spv::DecorationLocation);
- hlsl.set_decoration(resource.id, spv::DecorationLocation, i);
+ const std::string &name = hlsl.get_name(resource.id);
+ int num;
+ if (sscanf(name.c_str(), "sampler%d", &num) != 1)
+ num = i;
+ hlsl.set_decoration(resource.id, spv::DecorationBinding, num);
i++;
}
spirv_cross::CompilerHLSL::Options options{};
diff --git a/Common/GPU/Vulkan/VulkanContext.cpp b/Common/GPU/Vulkan/VulkanContext.cpp
index f0a28b484f..21bc687b42 100644
--- a/Common/GPU/Vulkan/VulkanContext.cpp
+++ b/Common/GPU/Vulkan/VulkanContext.cpp
@@ -565,33 +565,16 @@ void VulkanContext::ChooseDevice(int physical_device) {
}
deviceFeatures_.enabled = {};
-
// Enable a few safe ones if they are available.
- if (deviceFeatures_.available.dualSrcBlend) {
- deviceFeatures_.enabled.dualSrcBlend = true;
- }
- if (deviceFeatures_.available.largePoints) {
- deviceFeatures_.enabled.largePoints = true;
- }
- if (deviceFeatures_.available.wideLines) {
- deviceFeatures_.enabled.wideLines = true;
- }
- if (deviceFeatures_.available.logicOp) {
- deviceFeatures_.enabled.logicOp = true;
- }
- if (deviceFeatures_.available.depthClamp) {
- deviceFeatures_.enabled.depthClamp = true;
- }
- if (deviceFeatures_.available.depthBounds) {
- deviceFeatures_.enabled.depthBounds = true;
- }
- if (deviceFeatures_.available.samplerAnisotropy) {
- deviceFeatures_.enabled.samplerAnisotropy = true;
- }
+ deviceFeatures_.enabled.dualSrcBlend = deviceFeatures_.available.dualSrcBlend;
+ deviceFeatures_.enabled.largePoints = deviceFeatures_.available.largePoints;
+ deviceFeatures_.enabled.wideLines = deviceFeatures_.available.wideLines;
+ deviceFeatures_.enabled.logicOp = deviceFeatures_.available.logicOp;
+ deviceFeatures_.enabled.depthClamp = deviceFeatures_.available.depthClamp;
+ deviceFeatures_.enabled.depthBounds = deviceFeatures_.available.depthBounds;
+ deviceFeatures_.enabled.samplerAnisotropy = deviceFeatures_.available.samplerAnisotropy;
// For easy wireframe mode, someday.
- if (deviceFeatures_.available.fillModeNonSolid) {
- deviceFeatures_.enabled.fillModeNonSolid = true;
- }
+ deviceFeatures_.enabled.fillModeNonSolid = deviceFeatures_.available.fillModeNonSolid;
GetDeviceLayerExtensionList(nullptr, device_extension_properties_);
diff --git a/Common/GPU/Vulkan/thin3d_vulkan.cpp b/Common/GPU/Vulkan/thin3d_vulkan.cpp
index 054096f377..f68132f5e4 100644
--- a/Common/GPU/Vulkan/thin3d_vulkan.cpp
+++ b/Common/GPU/Vulkan/thin3d_vulkan.cpp
@@ -295,7 +295,7 @@ class VKBuffer;
class VKSamplerState;
enum {
- MAX_BOUND_TEXTURES = 2
+ MAX_BOUND_TEXTURES = MAX_TEXTURE_SLOTS,
};
struct DescriptorSetKey {
@@ -416,6 +416,7 @@ public:
// TODO: Make VKBuffers proper buffers, and do a proper binding model. This is just silly.
void BindVertexBuffers(int start, int count, Buffer **buffers, int *offsets) override {
+ _assert_(start + count <= ARRAY_SIZE(curVBuffers_));
for (int i = 0; i < count; i++) {
curVBuffers_[i + start] = (VKBuffer *)buffers[i];
curVBufferOffsets_[i + start] = offsets ? offsets[i] : 0;
@@ -689,6 +690,7 @@ RasterState *VKContext::CreateRasterState(const RasterStateDesc &desc) {
}
void VKContext::BindSamplerStates(int start, int count, SamplerState **state) {
+ _assert_(start + count <= MAX_BOUND_TEXTURES);
for (int i = start; i < start + count; i++) {
boundSamplers_[i] = (VKSamplerState *)state[i - start];
}
@@ -1271,6 +1273,7 @@ void VKContext::UpdateBuffer(Buffer *buffer, const uint8_t *data, size_t offset,
}
void VKContext::BindTextures(int start, int count, Texture **textures) {
+ _assert_(start + count <= MAX_BOUND_TEXTURES);
for (int i = start; i < start + count; i++) {
boundTextures_[i] = static_cast(textures[i - start]);
boundImageView_[i] = boundTextures_[i] ? boundTextures_[i]->GetImageView() : GetNullTexture()->GetImageView();
@@ -1544,6 +1547,7 @@ void VKContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPass
void VKContext::BindFramebufferAsTexture(Framebuffer *fbo, int binding, FBChannel channelBit, int attachment) {
VKFramebuffer *fb = (VKFramebuffer *)fbo;
+ _assert_(binding < MAX_BOUND_TEXTURES);
// TODO: There are cases where this is okay, actually. But requires layout transitions and stuff -
// we're not ready for this.
diff --git a/Common/GPU/thin3d.h b/Common/GPU/thin3d.h
index 9ee285c804..853ed27542 100644
--- a/Common/GPU/thin3d.h
+++ b/Common/GPU/thin3d.h
@@ -278,7 +278,7 @@ enum class Event {
PRESENTED,
};
-constexpr uint32_t MAX_TEXTURE_SLOTS = 2;
+constexpr uint32_t MAX_TEXTURE_SLOTS = 3;
struct FramebufferDesc {
int width;
diff --git a/Common/LogManager.cpp b/Common/LogManager.cpp
index 84f03a8aa6..d762636c5b 100644
--- a/Common/LogManager.cpp
+++ b/Common/LogManager.cpp
@@ -17,6 +17,12 @@
#include "ppsspp_config.h"
+#if PPSSPP_PLATFORM(ANDROID)
+
+#include
+
+#endif
+
#include
#include
@@ -51,8 +57,14 @@ void GenericLog(LogTypes::LOG_LEVELS level, LogTypes::LOG_TYPE type, const char
instance->Log(level, type, file, line, fmt, args);
} else {
// Fall back to printf if we're before the log manager has been initialized.
+#if PPSSPP_PLATFORM(ANDROID)
+ char temp[512];
+ vsnprintf(temp, sizeof(temp), fmt, args);
+ __android_log_print(ANDROID_LOG_INFO, "PPSSPP", "EARLY: %s", temp);
+#else
vprintf(fmt, args);
printf("\n");
+#endif
}
va_end(args);
}
diff --git a/Common/LogReporting.cpp b/Common/LogReporting.cpp
new file mode 100644
index 0000000000..2189b1c7e2
--- /dev/null
+++ b/Common/LogReporting.cpp
@@ -0,0 +1,92 @@
+// Copyright (c) 2012- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+#include
+#include
+#include
+#include
+#include "Common/LogReporting.h"
+
+namespace Reporting {
+
+// Keeps track of report-only-once identifiers. Since they're always constants, a pointer is okay.
+static std::unordered_map logNTimes;
+static std::mutex logNTimesLock;
+
+AllowedCallback allowedCallback = nullptr;
+MessageCallback messageCallback = nullptr;
+
+bool ShouldLogNTimes(const char *identifier, int count) {
+ // True if it wasn't there already -> so yes, log.
+ std::lock_guard lock(logNTimesLock);
+ auto iter = logNTimes.find(identifier);
+ if (iter == logNTimes.end()) {
+ logNTimes.insert(std::pair(identifier, 1));
+ return true;
+ } else {
+ if (iter->second >= count) {
+ return false;
+ } else {
+ iter->second++;
+ return true;
+ }
+ }
+}
+
+void ResetCounts() {
+ std::lock_guard lock(logNTimesLock);
+ logNTimes.clear();
+}
+
+void SetupCallbacks(AllowedCallback allowed, MessageCallback message) {
+ allowedCallback = allowed;
+ messageCallback = message;
+}
+
+void ReportMessage(const char *message, ...) {
+ if (!allowedCallback || !messageCallback) {
+ ERROR_LOG(SYSTEM, "Reporting not initialized, skipping: %s", message);
+ return;
+ }
+
+ if (!allowedCallback())
+ return;
+
+ const int MESSAGE_BUFFER_SIZE = 65536;
+ char temp[MESSAGE_BUFFER_SIZE];
+
+ va_list args;
+ va_start(args, message);
+ vsnprintf(temp, MESSAGE_BUFFER_SIZE - 1, message, args);
+ temp[MESSAGE_BUFFER_SIZE - 1] = '\0';
+ va_end(args);
+
+ messageCallback(message, temp);
+}
+
+void ReportMessageFormatted(const char *message, const char *formatted) {
+ if (!allowedCallback || !messageCallback) {
+ ERROR_LOG(SYSTEM, "Reporting not initialized, skipping: %s", message);
+ return;
+ }
+
+ if (!allowedCallback())
+ return;
+ messageCallback(message, formatted);
+}
+
+}
diff --git a/Common/LogReporting.h b/Common/LogReporting.h
new file mode 100644
index 0000000000..f4b0ea7903
--- /dev/null
+++ b/Common/LogReporting.h
@@ -0,0 +1,65 @@
+// Copyright (c) 2012- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+#pragma once
+
+#include "Common/CommonTypes.h"
+#include "Common/Log.h"
+
+#define DEBUG_LOG_REPORT(t,...) do { DEBUG_LOG(t, __VA_ARGS__); Reporting::ReportMessage(__VA_ARGS__); } while (false)
+#define ERROR_LOG_REPORT(t,...) do { ERROR_LOG(t, __VA_ARGS__); Reporting::ReportMessage(__VA_ARGS__); } while (false)
+#define WARN_LOG_REPORT(t,...) do { WARN_LOG(t, __VA_ARGS__); Reporting::ReportMessage(__VA_ARGS__); } while (false)
+#define NOTICE_LOG_REPORT(t,...) do { NOTICE_LOG(t, __VA_ARGS__); Reporting::ReportMessage(__VA_ARGS__); } while (false)
+#define INFO_LOG_REPORT(t,...) do { INFO_LOG(t, __VA_ARGS__); Reporting::ReportMessage(__VA_ARGS__); } while (false)
+
+#define DEBUG_LOG_REPORT_ONCE(n,t,...) do { if (Reporting::ShouldLogNTimes(#n, 1)) { DEBUG_LOG_REPORT(t, __VA_ARGS__); } } while (false)
+#define ERROR_LOG_REPORT_ONCE(n,t,...) do { if (Reporting::ShouldLogNTimes(#n, 1)) { ERROR_LOG_REPORT(t, __VA_ARGS__); } } while (false)
+#define WARN_LOG_REPORT_ONCE(n,t,...) do { if (Reporting::ShouldLogNTimes(#n, 1)) { WARN_LOG_REPORT(t, __VA_ARGS__); } } while (false)
+#define NOTICE_LOG_REPORT_ONCE(n,t,...) do { if (Reporting::ShouldLogNTimes(#n, 1)) { NOTICE_LOG_REPORT(t, __VA_ARGS__); } } while (false)
+#define INFO_LOG_REPORT_ONCE(n,t,...) do { if (Reporting::ShouldLogNTimes(#n, 1)) { INFO_LOG_REPORT(t, __VA_ARGS__); } } while (false)
+
+#define ERROR_LOG_ONCE(n,t,...) do { if (Reporting::ShouldLogNTimes(#n, 1)) { ERROR_LOG(t, __VA_ARGS__); } } while (false)
+#define WARN_LOG_ONCE(n,t,...) do { if (Reporting::ShouldLogNTimes(#n, 1)) { WARN_LOG(t, __VA_ARGS__); } } while (false)
+#define NOTICE_LOG_ONCE(n,t,...) do { if (Reporting::ShouldLogNTimes(#n, 1)) { NOTICE_LOG(t, __VA_ARGS__); } } while (false)
+#define INFO_LOG_ONCE(n,t,...) do { if (Reporting::ShouldLogNTimes(#n, 1)) { INFO_LOG(t, __VA_ARGS__); } } while (false)
+
+#define ERROR_LOG_N_TIMES(s,n,t,...) do { if (Reporting::ShouldLogNTimes(#s, n)) { ERROR_LOG(t, __VA_ARGS__); } } while (false)
+#define WARN_LOG_N_TIMES(s,n,t,...) do { if (Reporting::ShouldLogNTimes(#s, n)) { WARN_LOG(t, __VA_ARGS__); } } while (false)
+#define NOTICE_LOG_N_TIMES(s,n,t,...) do { if (Reporting::ShouldLogNTimes(#s, n)) { NOTICE_LOG(t, __VA_ARGS__); } } while (false)
+#define INFO_LOG_N_TIMES(s,n,t,...) do { if (Reporting::ShouldLogNTimes(#s, n)) { INFO_LOG(t, __VA_ARGS__); } } while (false)
+
+namespace Reporting {
+
+typedef bool(*AllowedCallback)();
+typedef void(*MessageCallback)(const char *message, const char *formatted);
+
+// Resets counts on any count-limited logs (see ShouldLogNTimes).
+void ResetCounts();
+
+// Returns true if that identifier has not been logged yet.
+bool ShouldLogNTimes(const char *identifier, int n);
+
+// Set callbacks for implementation of message reporting.
+void SetupCallbacks(AllowedCallback allowed, MessageCallback message);
+
+// Report a message string, using the format string as a key.
+void ReportMessage(const char *message, ...);
+
+// The same, but with a preformatted version (message is still the key.)
+void ReportMessageFormatted(const char *message, const char *formatted);
+
+}
diff --git a/Common/MemArena.h b/Common/MemArena.h
index 0c3b794546..b7409e16b9 100644
--- a/Common/MemArena.h
+++ b/Common/MemArena.h
@@ -36,7 +36,7 @@ struct MemArenaData;
class MemArena {
public:
size_t roundup(size_t x);
- void GrabLowMemSpace(size_t size);
+ bool GrabMemSpace(size_t size);
void ReleaseSpace();
void *CreateView(s64 offset, size_t size, void *base = 0);
void ReleaseView(void *view, size_t size);
diff --git a/Common/MemArenaAndroid.cpp b/Common/MemArenaAndroid.cpp
index c304f86435..ab156ed98b 100644
--- a/Common/MemArenaAndroid.cpp
+++ b/Common/MemArenaAndroid.cpp
@@ -98,7 +98,7 @@ size_t MemArena::roundup(size_t x) {
return x;
}
-void MemArena::GrabLowMemSpace(size_t size) {
+bool MemArena::GrabMemSpace(size_t size) {
// Use ashmem so we don't have to allocate a file on disk!
const char* name = "PPSSPP_RAM";
@@ -111,8 +111,9 @@ void MemArena::GrabLowMemSpace(size_t size) {
// Note that it appears that ashmem is pinned by default, so no need to pin.
if (fd < 0) {
ERROR_LOG(MEMMAP, "Failed to grab ashmem space of size: %08x errno: %d", (int)size, (int)(errno));
- return;
+ return false;
}
+ return true;
}
void MemArena::ReleaseSpace() {
diff --git a/Common/MemArenaDarwin.cpp b/Common/MemArenaDarwin.cpp
index acac2957a1..1950ccf2df 100644
--- a/Common/MemArenaDarwin.cpp
+++ b/Common/MemArenaDarwin.cpp
@@ -39,13 +39,15 @@ size_t MemArena::roundup(size_t x) {
return x;
}
-void MemArena::GrabLowMemSpace(size_t size) {
+bool MemArena::GrabMemSpace(size_t size) {
vm_size = size;
kern_return_t retval = vm_allocate(mach_task_self(), &vm_mem, size, VM_FLAGS_ANYWHERE);
if (retval != KERN_SUCCESS) {
ERROR_LOG(MEMMAP, "Failed to grab a block of virtual memory");
+ return false;
} else {
INFO_LOG(MEMMAP, "Successfully allocated %d bytes at %p", (int)size, (void *)vm_mem);
+ return true;
}
}
diff --git a/Common/MemArenaPosix.cpp b/Common/MemArenaPosix.cpp
index 3701f5c377..47698f1e1e 100644
--- a/Common/MemArenaPosix.cpp
+++ b/Common/MemArenaPosix.cpp
@@ -50,7 +50,7 @@ bool MemArena::NeedsProbing() {
return false;
}
-void MemArena::GrabLowMemSpace(size_t size) {
+bool MemArena::GrabMemSpace(size_t size) {
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
// Some platforms (like Raspberry Pi) end up flushing to disk.
@@ -61,15 +61,20 @@ void MemArena::GrabLowMemSpace(size_t size) {
if (fd >= 0) {
// Great, this definitely shouldn't flush to disk.
ram_temp_file = tmpfs_ram_temp_file;
+ } else {
+ INFO_LOG(MEMMAP, "Got tmpfs ram file: %s", tmpfs_ram_temp_file.c_str());
}
+ } else {
+ INFO_LOG(MEMMAP, "'%s' does not exist. Trying other ram file options.", tmpfs_location.c_str());
}
if (fd < 0) {
+ INFO_LOG(MEMMAP, "Trying '%s' as ram temp file", ram_temp_file.c_str());
fd = open(ram_temp_file.c_str(), O_RDWR | O_CREAT, mode);
}
if (fd < 0) {
- ERROR_LOG(MEMMAP, "Failed to grab memory space as a file: %s of size: %08x errno: %d", ram_temp_file.c_str(), (int)size, (int)(errno));
- return;
+ ERROR_LOG(MEMMAP, "Failed to grab memory space as a file: %s of size: %08x. Error: %s", ram_temp_file.c_str(), (int)size, strerror(errno));
+ return false;
}
// delete immediately, we keep the fd so it still lives
if (unlink(ram_temp_file.c_str()) != 0) {
@@ -77,8 +82,9 @@ void MemArena::GrabLowMemSpace(size_t size) {
}
if (ftruncate(fd, size) != 0) {
ERROR_LOG(MEMMAP, "Failed to ftruncate %d (%s) to size %08x", (int)fd, ram_temp_file.c_str(), (int)size);
+ // Should this be a failure?
}
- return;
+ return true;
}
void MemArena::ReleaseSpace() {
@@ -95,7 +101,7 @@ void *MemArena::CreateView(s64 offset, size_t size, void *base)
((base == 0) ? 0 : MAP_FIXED), fd, offset);
if (retval == MAP_FAILED) {
- NOTICE_LOG(MEMMAP, "mmap on %s (fd: %d) failed", ram_temp_file.c_str(), (int)fd);
+ NOTICE_LOG(MEMMAP, "mmap on %s (fd: %d) failed: %s", ram_temp_file.c_str(), (int)fd, strerror(errno));
return 0;
}
return retval;
@@ -122,7 +128,7 @@ u8* MemArena::Find4GBBase() {
return reinterpret_cast(aligned_base);
} else {
u8 *hardcoded_ptr = reinterpret_cast(0x2300000000ULL);
- INFO_LOG(MEMMAP, "Failed to anonymously map 8GB. Fall back to the hardcoded pointer %p.", hardcoded_ptr);
+ INFO_LOG(MEMMAP, "Failed to anonymously map 8GB (%s). Fall back to the hardcoded pointer %p.", strerror(errno), hardcoded_ptr);
// Just grab some random 4GB...
// This has been known to fail lately though, see issue #12249.
return hardcoded_ptr;
diff --git a/Common/MemArenaWin32.cpp b/Common/MemArenaWin32.cpp
index 437bfd33d9..39d0d6de70 100644
--- a/Common/MemArenaWin32.cpp
+++ b/Common/MemArenaWin32.cpp
@@ -28,13 +28,14 @@ size_t MemArena::roundup(size_t x) {
return (x + gran - 1) & ~(gran - 1);
}
-void MemArena::GrabLowMemSpace(size_t size) {
+bool MemArena::GrabMemSpace(size_t size) {
#if !PPSSPP_PLATFORM(UWP)
hMemoryMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, (DWORD)(size), NULL);
GetSystemInfo(&sysInfo);
#else
hMemoryMapping = 0;
#endif
+ return true;
}
void MemArena::ReleaseSpace() {
diff --git a/Common/MipsCPUDetect.cpp b/Common/MipsCPUDetect.cpp
index a7669edd51..e3b383dc7c 100644
--- a/Common/MipsCPUDetect.cpp
+++ b/Common/MipsCPUDetect.cpp
@@ -180,6 +180,7 @@ void CPUInfo::Detect()
Mode64bit = false;
#endif
vendor = VENDOR_OTHER;
+ logical_cpu_count = 1;
// Get the information about the CPU
#if !defined(__linux__)
diff --git a/Common/Net/HTTPClient.cpp b/Common/Net/HTTPClient.cpp
index e1b25bb073..edc202ed24 100644
--- a/Common/Net/HTTPClient.cpp
+++ b/Common/Net/HTTPClient.cpp
@@ -552,12 +552,12 @@ void Download::Do() {
}
if (resultCode == 200) {
- INFO_LOG(IO, "Completed downloading %s to %s", url_.c_str(), outfile_.empty() ? "memory" : outfile_.ToVisualString().c_str());
+ INFO_LOG(IO, "Completed downloading %s to %s", url_.c_str(), outfile_.empty() ? "memory" : outfile_.c_str());
if (!outfile_.empty() && !buffer_.FlushToFile(outfile_)) {
- ERROR_LOG(IO, "Failed writing download to %s", outfile_.ToVisualString().c_str());
+ ERROR_LOG(IO, "Failed writing download to '%s'", outfile_.c_str());
}
} else {
- ERROR_LOG(IO, "Error downloading %s to %s: %i", url_.c_str(), outfile_.ToVisualString().c_str(), resultCode);
+ ERROR_LOG(IO, "Error downloading '%s' to '%s': %i", url_.c_str(), outfile_.c_str(), resultCode);
}
resultCode_ = resultCode;
}
diff --git a/Common/Net/HTTPServer.cpp b/Common/Net/HTTPServer.cpp
index c787c3f829..e56a5b9a6a 100644
--- a/Common/Net/HTTPServer.cpp
+++ b/Common/Net/HTTPServer.cpp
@@ -33,14 +33,18 @@
#include
#include
+#include
#include "Common/Net/HTTPServer.h"
#include "Common/Net/NetBuffer.h"
#include "Common/Net/Sinks.h"
#include "Common/File/FileDescriptor.h"
+
+#include "Common/Buffer.h"
#include "Common/Log.h"
-void NewThreadExecutor::Run(std::function &&func) {
+
+void NewThreadExecutor::Run(std::function func) {
threads_.push_back(std::thread(func));
}
@@ -138,7 +142,7 @@ void Request::Close() {
}
Server::Server(NewThreadExecutor *executor)
- : executor_(executor) {
+ : port_(0), executor_(executor) {
RegisterHandler("/", std::bind(&Server::HandleListing, this, std::placeholders::_1));
SetFallbackHandler(std::bind(&Server::Handle404, this, std::placeholders::_1));
}
diff --git a/Common/Net/HTTPServer.h b/Common/Net/HTTPServer.h
index a88137398b..40e336bb5a 100644
--- a/Common/Net/HTTPServer.h
+++ b/Common/Net/HTTPServer.h
@@ -10,16 +10,18 @@
class NewThreadExecutor {
public:
~NewThreadExecutor();
- void Run(std::function &&func);
+ void Run(std::function func);
private:
std::vector threads_;
};
namespace net {
+
class InputSink;
class OutputSink;
-};
+
+} // namespace net
namespace http {
diff --git a/Common/Net/URL.cpp b/Common/Net/URL.cpp
index 1183022ed7..cc9d5fcacc 100644
--- a/Common/Net/URL.cpp
+++ b/Common/Net/URL.cpp
@@ -155,7 +155,7 @@ std::string UriDecode(const std::string & sSrc)
return sResult;
}
-// Only alphanum is safe.
+// Only alphanum and underscore is safe.
const char SAFE[256] =
{
/* 0 1 2 3 4 5 6 7 8 9 A B C D E F */
@@ -165,7 +165,7 @@ const char SAFE[256] =
/* 3 */ 1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0,
/* 4 */ 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
- /* 5 */ 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
+ /* 5 */ 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,1, // last here is underscore. it's ok.
/* 6 */ 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
/* 7 */ 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
diff --git a/Common/Render/Text/draw_text_uwp.cpp b/Common/Render/Text/draw_text_uwp.cpp
index 6f5088619f..d12a41275f 100644
--- a/Common/Render/Text/draw_text_uwp.cpp
+++ b/Common/Render/Text/draw_text_uwp.cpp
@@ -359,6 +359,11 @@ void TextDrawerUWP::DrawStringBitmap(std::vector &bitmapData, TextStrin
size.cx = metrics.width + 1;
size.cy = metrics.height + 1;
+ if (size.cx > MAX_TEXT_WIDTH)
+ size.cx = MAX_TEXT_WIDTH;
+ if (size.cy > MAX_TEXT_HEIGHT)
+ size.cy = MAX_TEXT_HEIGHT;
+
// Prevent zero-sized textures, which can occur. Not worth to avoid
// creating the texture altogether in this case. One example is a string
// containing only '\r\n', see issue #10764.
diff --git a/Common/Thread/Channel.h b/Common/Thread/Channel.h
new file mode 100644
index 0000000000..52f7601f36
--- /dev/null
+++ b/Common/Thread/Channel.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+// Named Channel.h because I originally intended to support a multi item channel as
+// well as a simple blocking mailbox. Let's see if we get there.
+
+// Single item mailbox.
+template
+struct Mailbox {
+ Mailbox() : refcount_(1) {}
+ ~Mailbox() {
+ assert(refcount_ == 0);
+ }
+
+ std::mutex mutex_;
+ std::condition_variable condvar_;
+ T *data_ = nullptr;
+
+ T *Wait() {
+ std::unique_lock lock(mutex_);
+ while (!data_) {
+ condvar_.wait(lock);
+ }
+ return data_;
+ }
+
+ bool Poll(T **data) {
+ std::unique_lock lock(mutex_);
+ if (data_) {
+ *data = data_;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ bool Send(T *data) {
+ std::unique_lock lock(mutex_);
+ if (!data_) {
+ data_ = data;
+ condvar_.notify_one();
+ return true;
+ } else {
+ // Already has value.
+ return false;
+ }
+ }
+
+ void AddRef() {
+ refcount_.fetch_add(1);
+ }
+
+ void Release() {
+ int count = refcount_.fetch_sub(1);
+ if (count == 1) { // was definitely decreased to 0
+ delete this;
+ }
+ }
+
+private:
+ std::atomic refcount_;
+};
diff --git a/Common/Thread/Event.h b/Common/Thread/Event.h
new file mode 100644
index 0000000000..3baf465373
--- /dev/null
+++ b/Common/Thread/Event.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "Common/Thread/ThreadManager.h"
+
+#include
+#include
+
+struct Event : public Waitable {
+public:
+ void Wait() override {
+ if (triggered_) {
+ return;
+ }
+ std::unique_lock lock;
+ cond_.wait(lock, [&] { return !triggered_; });
+ }
+
+ void Notify() {
+ std::unique_lock lock;
+ triggered_ = true;
+ cond_.notify_one();
+ }
+
+private:
+ std::condition_variable cond_;
+ std::mutex mutex_;
+ bool triggered_ = false;
+};
diff --git a/Common/Thread/ParallelLoop.cpp b/Common/Thread/ParallelLoop.cpp
new file mode 100644
index 0000000000..0212a84b8a
--- /dev/null
+++ b/Common/Thread/ParallelLoop.cpp
@@ -0,0 +1,134 @@
+#include
+#include
+
+#include "Common/Thread/ParallelLoop.h"
+#include "Common/CPUDetect.h"
+
+class LoopRangeTask : public Task {
+public:
+ LoopRangeTask(WaitableCounter *counter, const std::function &loop, int lower, int upper)
+ : counter_(counter), loop_(loop), lower_(lower), upper_(upper) {}
+
+ void Run() override {
+ loop_(lower_, upper_);
+ counter_->Count();
+ }
+
+ std::function loop_;
+ WaitableCounter *counter_;
+
+ int lower_;
+ int upper_;
+};
+
+WaitableCounter *ParallelRangeLoopWaitable(ThreadManager *threadMan, const std::function &loop, int lower, int upper, int minSize) {
+ if (minSize == -1) {
+ minSize = 1;
+ }
+
+ int numTasks = threadMan->GetNumLooperThreads();
+ int range = upper - lower;
+ if (range <= 0) {
+ // Nothing to do. A finished counter allocated to keep the API.
+ return new WaitableCounter(0);
+ } else if (range <= minSize) {
+ // Single background task.
+ WaitableCounter *waitableCounter = new WaitableCounter(1);
+ threadMan->EnqueueTaskOnThread(0, new LoopRangeTask(waitableCounter, loop, lower, upper), TaskType::CPU_COMPUTE);
+ return waitableCounter;
+ } else {
+ // Split the range between threads. Allow for some fractional bits.
+ const int fractionalBits = 8;
+
+ int64_t totalFrac = (int64_t)range << fractionalBits;
+ int64_t delta = totalFrac / numTasks;
+
+ delta = std::max(delta, (int64_t)minSize << fractionalBits);
+
+ // Now we can compute the actual number of tasks.
+ // Remember that stragglers are done on the current thread
+ // so we don't round up.
+ numTasks = (int)(totalFrac / delta);
+
+ WaitableCounter *waitableCounter = new WaitableCounter(numTasks);
+ int64_t counter = (int64_t)lower << fractionalBits;
+
+ // Split up tasks as equitable as possible.
+ for (int i = 0; i < numTasks; i++) {
+ int start = (int)(counter >> fractionalBits);
+ int end = (int)((counter + delta) >> fractionalBits);
+ if (end > upper) {
+ // Let's do the stragglers on the current thread.
+ break;
+ }
+ threadMan->EnqueueTaskOnThread(i, new LoopRangeTask(waitableCounter, loop, start, end), TaskType::CPU_COMPUTE);
+ counter += delta;
+ if ((counter >> fractionalBits) >= upper) {
+ break;
+ }
+ }
+
+ // Run stragglers on the calling thread directly.
+ // We might add a flag later to avoid this for some cases.
+ int stragglerStart = (int)(counter >> fractionalBits);
+ int stragglerEnd = upper;
+ if (stragglerStart < stragglerEnd) {
+ loop(stragglerStart, stragglerEnd);
+ }
+ return waitableCounter;
+ }
+}
+
+void ParallelRangeLoop(ThreadManager *threadMan, const std::function &loop, int lower, int upper, int minSize) {
+ if (cpu_info.num_cores == 1 || (minSize >= (upper - lower) && upper > lower)) {
+ // "Optimization" for single-core devices, or minSize larger than the range.
+ // No point in adding threading overhead, let's just do it inline (since this is the blocking variant).
+ loop(lower, upper);
+ return;
+ }
+
+ if (minSize < 1) {
+ // There's no obvious value to default to.
+ minSize = 1;
+ }
+
+ WaitableCounter *counter = ParallelRangeLoopWaitable(threadMan, loop, lower, upper, minSize);
+ // TODO: Optimize using minSize. We'll just compute whether there's a remainer, remove it from the call to ParallelRangeLoopWaitable,
+ // and process the remainder right here. If there's no remainer, we'll steal a whole chunk.
+ if (counter) {
+ counter->WaitAndRelease();
+ }
+}
+
+// NOTE: Supports a max of 2GB.
+void ParallelMemcpy(ThreadManager *threadMan, void *dst, const void *src, size_t bytes) {
+ // This threshold can probably be a lot bigger.
+ if (bytes < 512) {
+ memcpy(dst, src, bytes);
+ return;
+ }
+
+ // unknown's testing showed that 128kB is an appropriate minimum size.
+
+ char *d = (char *)dst;
+ const char *s = (const char *)src;
+ ParallelRangeLoop(threadMan, [&](int l, int h) {
+ memmove(d + l, s + l, h - l);
+ }, 0, (int)bytes, 128 * 1024);
+}
+
+// NOTE: Supports a max of 2GB.
+void ParallelMemset(ThreadManager *threadMan, void *dst, uint8_t value, size_t bytes) {
+ // This threshold can probably be a lot bigger.
+ if (bytes < 512) {
+ memset(dst, 0, bytes);
+ return;
+ }
+
+ // unknown's testing showed that 128kB is an appropriate minimum size.
+
+ char *d = (char *)dst;
+ ParallelRangeLoop(threadMan, [&](int l, int h) {
+ memset(d + l, value, h - l);
+ }, 0, (int)bytes, 128 * 1024);
+}
diff --git a/Common/Thread/ParallelLoop.h b/Common/Thread/ParallelLoop.h
new file mode 100644
index 0000000000..71c018ebce
--- /dev/null
+++ b/Common/Thread/ParallelLoop.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include
+#include
+#include
+
+#include "Common/Thread/ThreadManager.h"
+
+// Same as the latch from C++21.
+struct WaitableCounter : public Waitable {
+public:
+ WaitableCounter(int count) : count_(count) {}
+
+ void Count() {
+ std::unique_lock lock(mutex_);
+ if (count_ == 0) {
+ return;
+ }
+ count_--;
+ if (count_ == 0) {
+ // We were the last one to increment
+ cond_.notify_all();
+ }
+ }
+
+ void Wait() override {
+ std::unique_lock lock(mutex_);
+ while (count_ != 0) {
+ cond_.wait(lock);
+ }
+ }
+
+ int count_;
+ std::mutex mutex_;
+ std::condition_variable cond_;
+};
+
+// Note that upper bounds are non-inclusive: range is [lower, upper)
+WaitableCounter *ParallelRangeLoopWaitable(ThreadManager *threadMan, const std::function &loop, int lower, int upper, int minSize);
+
+// Note that upper bounds are non-inclusive: range is [lower, upper)
+void ParallelRangeLoop(ThreadManager *threadMan, const std::function &loop, int lower, int upper, int minSize);
+
+// Common utilities for large (!) memory copies.
+// Will only fall back to threads if it seems to make sense.
+// NOTE: These support a max of 2GB.
+void ParallelMemcpy(ThreadManager *threadMan, void *dst, const void *src, size_t bytes);
+void ParallelMemset(ThreadManager *threadMan, void *dst, uint8_t value, size_t bytes);
diff --git a/Common/Thread/PrioritizedWorkQueue.cpp b/Common/Thread/PrioritizedWorkQueue.cpp
deleted file mode 100644
index 973ef43ce4..0000000000
--- a/Common/Thread/PrioritizedWorkQueue.cpp
+++ /dev/null
@@ -1,140 +0,0 @@
-#include
-#include
-
-#include "Common/TimeUtil.h"
-#include "Common/Thread/PrioritizedWorkQueue.h"
-
-#include "Common/Log.h"
-
-PrioritizedWorkQueue::~PrioritizedWorkQueue() {
- if (!done_) {
- ERROR_LOG(SYSTEM, "PrioritizedWorkQueue destroyed but not done!");
- }
-}
-
-void PrioritizedWorkQueue::Add(PrioritizedWorkQueueItem *item) {
- std::lock_guard guard(mutex_);
- queue_.push_back(item);
- notEmpty_.notify_one();
-}
-
-void PrioritizedWorkQueue::Stop() {
- std::lock_guard guard(mutex_);
- done_ = true;
- notEmpty_.notify_one();
-}
-
-void PrioritizedWorkQueue::Flush() {
- std::lock_guard guard(mutex_);
- int flush_count = 0;
- for (auto iter = queue_.begin(); iter != queue_.end(); ++iter) {
- delete *iter;
- flush_count++;
- }
- queue_.clear();
- if (flush_count > 0) {
- INFO_LOG(SYSTEM, "PrioritizedWorkQueue: Flushed %d un-executed tasks", flush_count);
- }
-}
-
-bool PrioritizedWorkQueue::WaitUntilDone(bool all) {
- // We'll lock drain this entire time, so make sure you follow that lock ordering.
- std::unique_lock guard(drainMutex_);
- if (AllItemsDone()) {
- return true;
- }
-
- while (!AllItemsDone()) {
- drain_.wait(guard);
- if (!all) {
- // Return whether empty or not, something just drained.
- return AllItemsDone();
- }
- }
-
- return true;
-}
-
-void PrioritizedWorkQueue::NotifyDrain() {
- std::lock_guard guard(drainMutex_);
- drain_.notify_one();
-}
-
-bool PrioritizedWorkQueue::AllItemsDone() {
- std::lock_guard guard(mutex_);
- return queue_.empty() && !working_;
-}
-
-// The worker should simply call this in a loop. Will block when appropriate.
-PrioritizedWorkQueueItem *PrioritizedWorkQueue::Pop() {
- {
- std::lock_guard guard(mutex_);
- working_ = false; // The thread only calls Pop if it's done.
- }
-
- // Important: make sure mutex_ is not locked while draining.
- NotifyDrain();
-
- std::unique_lock guard(mutex_);
- if (done_) {
- return 0;
- }
-
- while (queue_.empty()) {
- notEmpty_.wait(guard);
- if (done_) {
- return 0;
- }
- }
-
- // Find the top priority item (lowest value).
- float best_prio = std::numeric_limits::infinity();
- std::vector::iterator best = queue_.end();
- for (auto iter = queue_.begin(); iter != queue_.end(); ++iter) {
- if ((*iter)->priority() < best_prio) {
- best = iter;
- best_prio = (*iter)->priority();
- }
- }
-
- if (best != queue_.end()) {
- PrioritizedWorkQueueItem *poppedItem = *best;
- queue_.erase(best);
- working_ = true; // This will be worked on.
- return poppedItem;
- } else {
- // Not really sure how this can happen, but let's be safe.
- return 0;
- }
-}
-
-// TODO: This feels ugly. Revisit later.
-
-static std::thread *workThread;
-
-static void threadfunc(PrioritizedWorkQueue *wq) {
- SetCurrentThreadName("PrioQueue");
- while (true) {
- PrioritizedWorkQueueItem *item = wq->Pop();
- if (!item) {
- if (wq->Done())
- break;
- } else {
- item->run();
- delete item;
- }
- }
-}
-
-void ProcessWorkQueueOnThreadWhile(PrioritizedWorkQueue *wq) {
- workThread = new std::thread([=](){threadfunc(wq);});
-}
-
-void StopProcessingWorkQueue(PrioritizedWorkQueue *wq) {
- wq->Stop();
- if (workThread) {
- workThread->join();
- delete workThread;
- }
- workThread = nullptr;
-}
diff --git a/Common/Thread/PrioritizedWorkQueue.h b/Common/Thread/PrioritizedWorkQueue.h
deleted file mode 100644
index 59231f48b1..0000000000
--- a/Common/Thread/PrioritizedWorkQueue.h
+++ /dev/null
@@ -1,65 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-
-#include "Common/Thread/ThreadUtil.h"
-
-#include "Common/Common.h"
-
-// Priorities can change dynamically.
-// Try to make priority() fast, it will be called a lot.
-
-class PrioritizedWorkQueueItem {
-public:
- PrioritizedWorkQueueItem() {}
- virtual ~PrioritizedWorkQueueItem() {}
- virtual void run() = 0;
- virtual float priority() = 0; // Low priority value = high priority!
-
-private:
- DISALLOW_COPY_AND_ASSIGN(PrioritizedWorkQueueItem);
-};
-
-class PrioritizedWorkQueue {
-public:
- PrioritizedWorkQueue() : done_(false), working_(false) {}
- ~PrioritizedWorkQueue();
- // Takes ownership.
- void Add(PrioritizedWorkQueueItem *item);
-
- // The worker should simply call this in a loop. Will block when appropriate.
- PrioritizedWorkQueueItem *Pop();
-
- void Flush();
- bool Done() { return done_; }
- void Stop();
- bool WaitUntilDone(bool all = true);
-
- bool IsWorking() {
- return working_;
- }
-
-private:
- void NotifyDrain();
- bool AllItemsDone();
-
- bool done_;
- bool working_;
- std::mutex mutex_;
- std::mutex drainMutex_;
- std::condition_variable notEmpty_;
- std::condition_variable drain_;
-
- std::vector queue_;
-
- DISALLOW_COPY_AND_ASSIGN(PrioritizedWorkQueue);
-};
-
-
-// Starts up a thread that keeps trying to run this workqueue.
-// TODO: This feels ugly. Revisit later.
-void ProcessWorkQueueOnThreadWhile(PrioritizedWorkQueue *wq);
-void StopProcessingWorkQueue(PrioritizedWorkQueue *wq);
diff --git a/Common/Thread/Promise.h b/Common/Thread/Promise.h
new file mode 100644
index 0000000000..e6fc471141
--- /dev/null
+++ b/Common/Thread/Promise.h
@@ -0,0 +1,87 @@
+#pragma once
+
+#include
+
+#include "Common/Thread/Channel.h"
+#include "Common/Thread/ThreadManager.h"
+#include "Common/Log.h"
+
+template
+class PromiseTask : public Task {
+public:
+ PromiseTask(std::function fun, Mailbox *tx) : fun_(fun), tx_(tx) {
+ tx_->AddRef();
+ }
+ ~PromiseTask() {
+ tx_->Release();
+ }
+
+ void Run() override {
+ T *value = fun_();
+ tx_->Send(value);
+ }
+
+ std::function fun_;
+ Mailbox *tx_;
+};
+
+// Represents pending or actual data.
+// Has ownership over the data. Single use.
+// TODO: Split Mailbox (rx_ and tx_) up into separate proxy objects.
+// NOTE: Poll/BlockUntilReady should only be used from one thread.
+template
+class Promise {
+public:
+ static Promise *Spawn(ThreadManager *threadman, std::function fun, TaskType taskType) {
+ Mailbox *mailbox = new Mailbox();
+
+ Promise *promise = new Promise();
+ promise->rx_ = mailbox;
+
+ PromiseTask *task = new PromiseTask(fun, mailbox);
+ threadman->EnqueueTask(task, taskType);
+ return promise;
+ }
+
+ ~Promise() {
+ // A promise should have been fulfilled before it's destroyed.
+ _assert_(ready_);
+ _assert_(!rx_);
+ delete data_;
+ }
+
+ // Returns *T if the data is ready, nullptr if it's not.
+ T *Poll() {
+ if (ready_) {
+ return data_;
+ } else {
+ if (rx_->Poll(&data_)) {
+ rx_->Release();
+ rx_ = nullptr;
+ ready_ = true;
+ return data_;
+ } else {
+ return nullptr;
+ }
+ }
+ }
+
+ T *BlockUntilReady() {
+ if (ready_) {
+ return data_;
+ } else {
+ data_ = rx_->Wait();
+ rx_->Release();
+ rx_ = nullptr;
+ ready_ = true;
+ return data_;
+ }
+ }
+
+private:
+ Promise() {}
+
+ T *data_ = nullptr;
+ bool ready_ = false;
+ Mailbox *rx_;
+};
diff --git a/Common/Thread/ThreadManager.cpp b/Common/Thread/ThreadManager.cpp
new file mode 100644
index 0000000000..8ac9ac0a22
--- /dev/null
+++ b/Common/Thread/ThreadManager.cpp
@@ -0,0 +1,176 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "Common/Log.h"
+#include "Common/Thread/ThreadUtil.h"
+#include "Common/Thread/ThreadManager.h"
+
+// Threads and task scheduling
+//
+// * The threadpool should contain a number of threads that's the the number of cores,
+// plus a fixed number more for I/O-limited background tasks.
+// * Parallel compute-limited loops should use as many threads as there are cores.
+// They should always be scheduled to the first N threads.
+// * For some tasks, splitting the input values up linearly between the threads
+// is not fair. However, we ignore that for now.
+
+const int MAX_CORES_TO_USE = 16;
+const int EXTRA_THREADS = 4; // For I/O limited tasks
+
+struct GlobalThreadContext {
+ std::mutex mutex; // associated with each respective condition variable
+ std::deque queue;
+ std::vector threads_;
+
+ int roundRobin = 0;
+};
+
+struct ThreadContext {
+ std::thread thread; // the worker thread
+ std::condition_variable cond; // used to signal new work
+ std::mutex mutex; // protects the local queue.
+ std::atomic queueSize;
+ int index;
+ std::atomic cancelled;
+ std::deque private_queue;
+};
+
+ThreadManager::ThreadManager() : global_(new GlobalThreadContext()) {
+
+}
+
+ThreadManager::~ThreadManager() {
+ delete global_;
+}
+
+void ThreadManager::Teardown() {
+ for (size_t i = 0; i < global_->threads_.size(); i++) {
+ global_->threads_[i]->cancelled = true;
+ global_->threads_[i]->cond.notify_one();
+ }
+ for (size_t i = 0; i < global_->threads_.size(); i++) {
+ global_->threads_[i]->thread.join();
+ delete global_->threads_[i];
+ }
+ global_->threads_.clear();
+}
+
+static void WorkerThreadFunc(GlobalThreadContext *global, ThreadContext *thread) {
+ char threadName[16];
+ snprintf(threadName, sizeof(threadName), "PoolWorker %d", thread->index);
+ SetCurrentThreadName(threadName);
+ while (!thread->cancelled) {
+ Task *task = nullptr;
+
+ // Check the global queue first, then check the private queue and wait if there's nothing to do.
+ {
+ // Grab one from the global queue if there is any.
+ std::unique_lock lock(global->mutex);
+ if (!global->queue.empty()) {
+ task = global->queue.front();
+ global->queue.pop_front();
+ }
+ }
+
+ if (!task) {
+ std::unique_lock lock(thread->mutex);
+ if (!thread->private_queue.empty()) {
+ task = thread->private_queue.front();
+ thread->private_queue.pop_front();
+ thread->queueSize.store((int)thread->private_queue.size());
+ } else {
+ thread->cond.wait(lock);
+ }
+ }
+ // The task itself takes care of notifying anyone waiting on it. Not the
+ // responsibility of the ThreadManager (although it could be!).
+ if (task) {
+ task->Run();
+ delete task;
+ }
+ }
+}
+
+void ThreadManager::Init(int numRealCores, int numLogicalCoresPerCpu) {
+ if (!global_->threads_.empty()) {
+ Teardown();
+ }
+
+ numComputeThreads_ = std::min(numRealCores * numLogicalCoresPerCpu, MAX_CORES_TO_USE);
+ int numThreads = numComputeThreads_ + EXTRA_THREADS;
+ numThreads_ = numThreads;
+
+ INFO_LOG(SYSTEM, "ThreadManager::Init(compute threads: %d, all: %d)", numComputeThreads_, numThreads_);
+
+ for (int i = 0; i < numThreads; i++) {
+ ThreadContext *thread = new ThreadContext();
+ thread->cancelled.store(false);
+ thread->thread = std::thread(&WorkerThreadFunc, global_, thread);
+ thread->index = i;
+ global_->threads_.push_back(thread);
+ }
+}
+
+void ThreadManager::EnqueueTask(Task *task, TaskType taskType) {
+ int maxThread;
+ int threadOffset = 0;
+ if (taskType == TaskType::CPU_COMPUTE) {
+ // only the threads reserved for heavy compute.
+ maxThread = numComputeThreads_;
+ threadOffset = 0;
+ } else {
+ // any free thread
+ maxThread = numThreads_;
+ threadOffset = numComputeThreads_;
+ }
+
+ // Find a thread with no outstanding work.
+ int threadNum = threadOffset;
+ for (int i = 0; i < maxThread; i++, threadNum++) {
+ if (threadNum >= global_->threads_.size()) {
+ threadNum = 0;
+ }
+ ThreadContext *thread = global_->threads_[threadNum];
+ if (thread->queueSize.load() == 0) {
+ std::unique_lock lock(thread->mutex);
+ thread->private_queue.push_back(task);
+ thread->queueSize.store((int)thread->private_queue.size());
+ thread->cond.notify_one();
+ // Found it - done.
+ return;
+ }
+ }
+
+ // Still not scheduled? Put it on the global queue and notify a thread chosen by round-robin.
+ // Not particularly scientific, but hopefully we should not run into this too much.
+ {
+ std::unique_lock lock(global_->mutex);
+ global_->queue.push_back(task);
+ global_->threads_[global_->roundRobin % maxThread]->cond.notify_one();
+ global_->roundRobin++;
+ }
+}
+
+void ThreadManager::EnqueueTaskOnThread(int threadNum, Task *task, TaskType taskType) {
+ _assert_(threadNum >= 0 && threadNum < (int)global_->threads_.size());
+ ThreadContext *thread = global_->threads_[threadNum];
+ {
+ std::unique_lock lock(thread->mutex);
+ thread->private_queue.push_back(task);
+ thread->cond.notify_one();
+ }
+}
+
+int ThreadManager::GetNumLooperThreads() const {
+ return numComputeThreads_;
+}
+
+void ThreadManager::TryCancelTask(uint64_t taskID) {
+ // Do nothing
+}
diff --git a/Common/Thread/ThreadManager.h b/Common/Thread/ThreadManager.h
new file mode 100644
index 0000000000..822997d921
--- /dev/null
+++ b/Common/Thread/ThreadManager.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include
+
+// The new threadpool.
+
+// To help future smart scheduling.
+enum class TaskType {
+ CPU_COMPUTE,
+ IO_BLOCKING,
+};
+
+// Implement this to make something that you can run on the thread manager.
+class Task {
+public:
+ virtual ~Task() {}
+ virtual void Run() = 0;
+ virtual bool Cancellable() { return false; }
+ virtual void Cancel() {}
+ virtual uint64_t id() { return 0; }
+};
+
+class Waitable {
+public:
+ virtual ~Waitable() {}
+
+ virtual void Wait() = 0;
+
+ void WaitAndRelease() {
+ Wait();
+ delete this;
+ }
+};
+
+struct ThreadContext;
+struct GlobalThreadContext;
+
+class ThreadManager {
+public:
+ ThreadManager();
+ ~ThreadManager();
+
+ // The distinction here is to be able to take hyper-threading into account.
+ // It gets even trickier when you think about mobile chips with BIG/LITTLE, but we'll
+ // just ignore it and let the OS handle it.
+ void Init(int numCores, int numLogicalCoresPerCpu);
+ void EnqueueTask(Task *task, TaskType taskType);
+ void EnqueueTaskOnThread(int threadNum, Task *task, TaskType taskType);
+ void Teardown();
+
+ // Currently does nothing. It will always be best-effort - maybe it cancels,
+ // maybe it doesn't. Note that the id is the id() returned by the task. You need to make that
+ // something meaningful yourself.
+ void TryCancelTask(uint64_t id);
+
+ // Parallel loops (assumed compute-limited) get one thread per logical core. We have a few extra threads too
+ // for I/O bounds tasks, that can be run concurrently with those.
+ int GetNumLooperThreads() const;
+
+private:
+ GlobalThreadContext *global_ = nullptr;
+
+ int numThreads_ = 0;
+ int numComputeThreads_ = 0;
+
+ friend struct ThreadContext;
+};
+
+extern ThreadManager g_threadManager;
diff --git a/Common/Thread/ThreadPool.cpp b/Common/Thread/ThreadPool.cpp
deleted file mode 100644
index e2560d17dd..0000000000
--- a/Common/Thread/ThreadPool.cpp
+++ /dev/null
@@ -1,142 +0,0 @@
-#include
-#include
-#include "Common/Thread/ThreadPool.h"
-#include "Common/Thread/ThreadUtil.h"
-
-#include "Common/Log.h"
-#include "Common/MakeUnique.h"
-
-///////////////////////////// WorkerThread
-
-WorkerThread::~WorkerThread() {
- {
- std::lock_guard guard(mutex);
- active = false;
- signal.notify_one();
- }
- if (thread.joinable()) {
- thread.join();
- }
-}
-
-void WorkerThread::StartUp() {
- thread = std::thread(std::bind(&WorkerThread::WorkFunc, this));
-}
-
-void WorkerThread::Process(std::function work) {
- std::lock_guard guard(mutex);
- work_ = std::move(work);
- jobsTarget = jobsDone + 1;
- signal.notify_one();
-}
-
-void WorkerThread::WaitForCompletion() {
- std::unique_lock guard(doneMutex);
- while (jobsDone < jobsTarget) {
- done.wait(guard);
- }
-}
-
-void WorkerThread::WorkFunc() {
- SetCurrentThreadName("Worker");
- std::unique_lock guard(mutex);
- while (active) {
- // 'active == false' is one of the conditions for signaling,
- // do not "optimize" it
- while (active && jobsTarget <= jobsDone) {
- signal.wait(guard);
- }
- if (active) {
- work_();
-
- std::lock_guard doneGuard(doneMutex);
- jobsDone++;
- done.notify_one();
- }
- }
-}
-
-void LoopWorkerThread::ProcessLoop(std::function work, int start, int end) {
- std::lock_guard guard(mutex);
- loopWork_ = std::move(work);
- work_ = [this]() {
- loopWork_(start_, end_);
- };
- start_ = start;
- end_ = end;
- jobsTarget = jobsDone + 1;
- signal.notify_one();
-}
-
-///////////////////////////// ThreadPool
-
-ThreadPool::ThreadPool(int numThreads) {
- if (numThreads <= 0) {
- numThreads_ = 1;
- INFO_LOG(JIT, "ThreadPool: Bad number of threads %d", numThreads);
- } else if (numThreads > 16) {
- INFO_LOG(JIT, "ThreadPool: Capping number of threads to 16 (was %d)", numThreads);
- numThreads_ = 16;
- } else {
- numThreads_ = numThreads;
- }
-}
-
-void ThreadPool::StartWorkers() {
- if (!workersStarted) {
- workers.reserve(numThreads_ - 1);
- for(int i = 0; i < numThreads_ - 1; ++i) { // create one less worker thread as the thread calling ParallelLoop will also do work
- auto workerPtr = make_unique();
- workerPtr->StartUp();
- workers.push_back(std::move(workerPtr));
- }
- workersStarted = true;
- }
-}
-
-void ThreadPool::ParallelLoop(const std::function &loop, int lower, int upper, int minSize) {
- // Don't parallelize tiny loops.
- if (minSize == -1)
- minSize = 4;
-
- int range = upper - lower;
- if (range >= minSize) {
- std::lock_guard guard(mutex);
- StartWorkers();
-
- // could do slightly better load balancing for the generic case,
- // but doesn't matter since all our loops are power of 2
- int chunk = std::max(minSize, range / numThreads_);
- int s = lower;
- for (auto &worker : workers) {
- // We'll do the last chunk on the current thread.
- if (s + chunk >= upper) {
- break;
- }
- worker->ProcessLoop(loop, s, s + chunk);
- s += chunk;
- }
- // This is the final chunk.
- if (s < upper)
- loop(s, upper);
- for (auto &worker : workers) {
- worker->WaitForCompletion();
- }
- } else {
- loop(lower, upper);
- }
-}
-
-void ThreadPool::ParallelMemcpy(void *dest, const void *src, int size) {
- static const int MIN_SIZE = 128 * 1024;
- ParallelLoop([&](int l, int h) {
- memmove((uint8_t *)dest + l, (const uint8_t *)src + l, h - l);
- }, 0, size, MIN_SIZE);
-}
-
-void ThreadPool::ParallelMemset(void *dest, uint8_t val, int size) {
- static const int MIN_SIZE = 128 * 1024;
- ParallelLoop([&](int l, int h) {
- memset((uint8_t *)dest + l, val, h - l);
- }, 0, size, MIN_SIZE);
-}
diff --git a/Common/Thread/ThreadPool.h b/Common/Thread/ThreadPool.h
deleted file mode 100644
index ceac3c39c6..0000000000
--- a/Common/Thread/ThreadPool.h
+++ /dev/null
@@ -1,78 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-// This is the simplest possible worker implementation I can think of
-// but entirely sufficient for the given purpose.
-// Only handles a single item of work at a time.
-class WorkerThread {
-public:
- WorkerThread() = default;
- virtual ~WorkerThread();
-
- void StartUp();
-
- // submit a new work item
- void Process(std::function work);
- // wait for a submitted work item to be completed
- void WaitForCompletion();
-
-protected:
- virtual void WorkFunc();
-
- std::thread thread; // the worker thread
- std::condition_variable signal; // used to signal new work
- std::condition_variable done; // used to signal work completion
- std::mutex mutex, doneMutex; // associated with each respective condition variable
- bool active = true;
- int jobsDone = 0;
- int jobsTarget = 0;
-
- std::function work_; // the work to be done by this thread
-
-private:
- WorkerThread(const WorkerThread& other) = delete; // prevent copies
- void operator =(const WorkerThread &other) = delete;
-};
-
-class LoopWorkerThread final : public WorkerThread {
-public:
- LoopWorkerThread() = default;
- void ProcessLoop(std::function work, int start, int end);
-
-private:
- int start_;
- int end_;
- std::function loopWork_; // the work to be done by this thread
-};
-
-// A thread pool manages a set of worker threads, and allows the execution of parallel loops on them
-// individual parallel loops are fully sequentialized to simplify synchronization, which should not
-// be a problem as they should each use the entire system
-class ThreadPool {
-public:
- ThreadPool(int numThreads);
- // don't need a destructor, "workers" is cleared on delete,
- // leading to the stopping and joining of all worker threads (RAII and all that)
-
- void ParallelLoop(const std::function &loop, int lower, int upper, int minSize);
- void ParallelMemcpy(void *dest, const void *src, int sz);
- void ParallelMemset(void *dest, uint8_t val, int sz);
-
-private:
- int numThreads_;
- std::vector> workers;
- std::mutex mutex; // used to sequentialize loop execution
-
- bool workersStarted = false;
- void StartWorkers();
-
- ThreadPool(const ThreadPool& other) = delete; // prevent copies
- void operator =(const ThreadPool &other) = delete;
-};
-
diff --git a/Common/Thread/ThreadUtil.cpp b/Common/Thread/ThreadUtil.cpp
index 8f35af321b..950ef3042a 100644
--- a/Common/Thread/ThreadUtil.cpp
+++ b/Common/Thread/ThreadUtil.cpp
@@ -1,11 +1,19 @@
-#ifdef _WIN32
+#include "ppsspp_config.h"
+
+#if PPSSPP_PLATFORM(WINDOWS)
+
#include
+
#ifdef __MINGW32__
#include
#endif
+
#define TLS_SUPPORTED
+
#elif defined(__ANDROID__)
+
#define TLS_SUPPORTED
+
#endif
#include
@@ -14,8 +22,15 @@
#include "Common/Log.h"
#include "Common/Thread/ThreadUtil.h"
-#if defined(__ANDROID__) || defined(__APPLE__) || (defined(__GLIBC__) && defined(_GNU_SOURCE))
+#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(LINUX)
+#define _GNU_SOURCE
+#endif
+
+#if !PPSSPP_PLATFORM(WINDOWS)
#include
+#include
+#include
+#include
#endif
#ifdef TLS_SUPPORTED
@@ -41,12 +56,14 @@ static EXCEPTION_DISPOSITION NTAPI ignore_handler(EXCEPTION_RECORD *rec,
#endif
void SetCurrentThreadName(const char* threadName) {
-#ifdef _WIN32
+#if PPSSPP_PLATFORM(WINDOWS)
// Set the debugger-visible threadname through an unholy magic hack
static const DWORD MS_VC_EXCEPTION = 0x406D1388;
#endif
-#if defined(_WIN32) && defined(__MINGW32__)
+ // TODO: Use the new function SetThreadDescription available since Windows 10, version 1607.
+
+#if PPSSPP_PLATFORM(WINDOWS) && defined(__MINGW32__)
// Thread information for VS compatible debugger. -1 sets current thread.
THREADNAME_INFO ti;
ti.dwType = 0x1000;
@@ -67,7 +84,7 @@ void SetCurrentThreadName(const char* threadName) {
// Pop exception handler
tib->ExceptionList = tib->ExceptionList->Next;
-#elif defined(_WIN32)
+#elif PPSSPP_PLATFORM(WINDOWS)
#pragma pack(push,8)
struct THREADNAME_INFO {
DWORD dwType; // must be 0x1000
@@ -98,12 +115,10 @@ void SetCurrentThreadName(const char* threadName) {
{}
#else
-#if defined(__ANDROID__) || (defined(__GLIBC__) && defined(_GNU_SOURCE))
+#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(LINUX)
pthread_setname_np(pthread_self(), threadName);
#elif defined(__APPLE__)
pthread_setname_np(threadName);
-// #else
-// pthread_setname_np(threadName);
#endif
// Do nothing
@@ -121,3 +136,23 @@ void AssertCurrentThreadName(const char *threadName) {
}
#endif
}
+
+int GetCurrentThreadIdForDebug() {
+#if __LIBRETRO__
+ // Not sure why gettid() would not be available, but it isn't.
+ // The return value of this function is only used in unit tests anyway...
+ return 1;
+#elif PPSSPP_PLATFORM(WINDOWS)
+ return (int)GetCurrentThreadId();
+#elif PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS) || defined(__OpenBSD__) || defined(__FreeBSD__)
+ uint64_t tid = 0;
+ pthread_threadid_np(NULL, &tid);
+ return (int)tid;
+#elif PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(LINUX)
+ // See issue 14545
+ return (int)syscall(__NR_gettid);
+ // return (int)gettid();
+#else
+ return 1;
+#endif
+}
diff --git a/Common/Thread/ThreadUtil.h b/Common/Thread/ThreadUtil.h
index 60e6f731fd..982de2a41c 100644
--- a/Common/Thread/ThreadUtil.h
+++ b/Common/Thread/ThreadUtil.h
@@ -1,6 +1,12 @@
#pragma once
+#include
+
// Note that name must be a global string that lives until the end of the process,
// for AssertCurrentThreadName to work.
void SetCurrentThreadName(const char *threadName);
void AssertCurrentThreadName(const char *threadName);
+
+// Just gets a cheap thread identifier so that you can see different threads in debug output,
+// exactly what it is is badly specified and not useful for anything.
+int GetCurrentThreadIdForDebug();
diff --git a/Common/UI/ViewGroup.h b/Common/UI/ViewGroup.h
index 5bbac5e060..730f74f10b 100644
--- a/Common/UI/ViewGroup.h
+++ b/Common/UI/ViewGroup.h
@@ -112,8 +112,9 @@ public:
: LayoutParams(w, h, LP_ANCHOR), left(l), top(t), right(r), bottom(b), center(c) {
}
+ // There's a small hack here to make this behave more intuitively - AnchorLayout ordinarily ignores FILL_PARENT.
AnchorLayoutParams(Size w, Size h, bool c = false)
- : LayoutParams(w, h, LP_ANCHOR), left(0), top(0), right(NONE), bottom(NONE), center(c) {
+ : LayoutParams(w, h, LP_ANCHOR), left(0), top(0), right(w == FILL_PARENT ? 0 : NONE), bottom(h == FILL_PARENT ? 0 : NONE), center(c) {
}
AnchorLayoutParams(float l, float t, float r, float b, bool c = false)
: LayoutParams(WRAP_CONTENT, WRAP_CONTENT, LP_ANCHOR), left(l), top(t), right(r), bottom(b), center(c) {}
diff --git a/Core/Config.cpp b/Core/Config.cpp
index 848137003c..7de910587f 100644
--- a/Core/Config.cpp
+++ b/Core/Config.cpp
@@ -68,46 +68,56 @@ struct ConfigSetting {
TYPE_BOOL,
TYPE_INT,
TYPE_UINT32,
+ TYPE_UINT64,
TYPE_FLOAT,
TYPE_STRING,
TYPE_TOUCH_POS,
TYPE_PATH,
+ TYPE_CUSTOM_BUTTON
};
union DefaultValue {
bool b;
int i;
uint32_t u;
+ uint64_t lu;
float f;
const char *s;
const char *p; // not sure how much point..
ConfigTouchPos touchPos;
+ ConfigCustomButton customButton;
};
union SettingPtr {
bool *b;
int *i;
uint32_t *u;
+ uint64_t *lu;
float *f;
std::string *s;
Path *p;
ConfigTouchPos *touchPos;
+ ConfigCustomButton *customButton;
};
typedef bool (*BoolDefaultCallback)();
typedef int (*IntDefaultCallback)();
typedef uint32_t (*Uint32DefaultCallback)();
+ typedef uint64_t (*Uint64DefaultCallback)();
typedef float (*FloatDefaultCallback)();
typedef const char *(*StringDefaultCallback)();
typedef ConfigTouchPos(*TouchPosDefaultCallback)();
typedef const char *(*PathDefaultCallback)();
+ typedef ConfigCustomButton (*CustomButtonDefaultCallback)();
union Callback {
BoolDefaultCallback b;
IntDefaultCallback i;
Uint32DefaultCallback u;
+ Uint64DefaultCallback lu;
FloatDefaultCallback f;
StringDefaultCallback s;
PathDefaultCallback p;
TouchPosDefaultCallback touchPos;
+ CustomButtonDefaultCallback customButton;
};
ConfigSetting(bool v)
@@ -144,6 +154,13 @@ struct ConfigSetting {
default_.u = def;
}
+ ConfigSetting(const char *ini, uint64_t *v, uint64_t def, bool save = true, bool perGame = false)
+ : iniKey_(ini), type_(TYPE_UINT64), report_(false), save_(save), perGame_(perGame) {
+ ptr_.lu = v;
+ cb_.lu = nullptr;
+ default_.lu = def;
+ }
+
ConfigSetting(const char *ini, float *v, float def, bool save = true, bool perGame = false)
: iniKey_(ini), type_(TYPE_FLOAT), report_(false), save_(save), perGame_(perGame) {
ptr_.f = v;
@@ -172,6 +189,13 @@ struct ConfigSetting {
default_.touchPos = def;
}
+ ConfigSetting(const char *iniKey, const char *iniImage, const char *iniShape, const char *iniToggle, ConfigCustomButton *v, ConfigCustomButton def, bool save = true, bool perGame = false)
+ : iniKey_(iniKey), ini2_(iniImage), ini3_(iniShape), ini4_(iniToggle), type_(TYPE_CUSTOM_BUTTON), report_(false), save_(save), perGame_(perGame) {
+ ptr_.customButton = v;
+ cb_.customButton = nullptr;
+ default_.customButton = def;
+ }
+
ConfigSetting(const char *ini, bool *v, BoolDefaultCallback def, bool save = true, bool perGame = false)
: iniKey_(ini), type_(TYPE_BOOL), report_(false), save_(save), perGame_(perGame) {
ptr_.b = v;
@@ -242,6 +266,11 @@ struct ConfigSetting {
default_.u = cb_.u();
}
return section->Get(iniKey_, ptr_.u, default_.u);
+ case TYPE_UINT64:
+ if (cb_.lu) {
+ default_.lu = cb_.lu();
+ }
+ return section->Get(iniKey_, ptr_.lu, default_.lu);
case TYPE_FLOAT:
if (cb_.f) {
default_.f = cb_.f();
@@ -277,6 +306,15 @@ struct ConfigSetting {
}
return result;
}
+ case TYPE_CUSTOM_BUTTON:
+ if (cb_.customButton) {
+ default_.customButton = cb_.customButton();
+ }
+ section->Get(iniKey_, &ptr_.customButton->key, default_.customButton.key);
+ section->Get(ini2_, &ptr_.customButton->image, default_.customButton.image);
+ section->Get(ini3_, &ptr_.customButton->shape, default_.customButton.shape);
+ section->Get(ini4_, &ptr_.customButton->toggle, default_.customButton.toggle);
+ return true;
default:
_dbg_assert_msg_(false, "Unexpected ini setting type");
return false;
@@ -298,6 +336,8 @@ struct ConfigSetting {
return section->Set(iniKey_, *ptr_.i);
case TYPE_UINT32:
return section->Set(iniKey_, *ptr_.u);
+ case TYPE_UINT64:
+ return section->Set(iniKey_, *ptr_.lu);
case TYPE_FLOAT:
return section->Set(iniKey_, *ptr_.f);
case TYPE_STRING:
@@ -312,6 +352,12 @@ struct ConfigSetting {
section->Set(ini4_, ptr_.touchPos->show);
}
return;
+ case TYPE_CUSTOM_BUTTON:
+ section->Set(iniKey_, ptr_.customButton->key);
+ section->Set(ini2_, ptr_.customButton->image);
+ section->Set(ini3_, ptr_.customButton->shape);
+ section->Set(ini4_, ptr_.customButton->toggle);
+ return;
default:
_dbg_assert_msg_(false, "Unexpected ini setting type");
return;
@@ -329,6 +375,8 @@ struct ConfigSetting {
return data.Add(prefix + iniKey_, *ptr_.i);
case TYPE_UINT32:
return data.Add(prefix + iniKey_, *ptr_.u);
+ case TYPE_UINT64:
+ return data.Add(prefix + iniKey_, *ptr_.lu);
case TYPE_FLOAT:
return data.Add(prefix + iniKey_, *ptr_.f);
case TYPE_STRING:
@@ -338,6 +386,9 @@ struct ConfigSetting {
case TYPE_TOUCH_POS:
// Doesn't report.
return;
+ case TYPE_CUSTOM_BUTTON:
+ // Doesn't report.
+ return;
default:
_dbg_assert_msg_(false, "Unexpected ini setting type");
return;
@@ -423,12 +474,6 @@ std::string CreateRandMAC() {
return randStream.str();
}
-static int DefaultNumWorkers() {
- // Let's cap the global thread pool at 16 threads. Nothing we do really should have much
- // use for more...
- return std::min(16, cpu_info.num_cores);
-}
-
static int DefaultCpuCore() {
#if PPSSPP_ARCH(ARM) || PPSSPP_ARCH(ARM64) || PPSSPP_ARCH(X86) || PPSSPP_ARCH(AMD64)
return (int)CPUCore::JIT;
@@ -477,7 +522,6 @@ static ConfigSetting generalSettings[] = {
ConfigSetting("DiscordPresence", &g_Config.bDiscordPresence, false, true, false), // Or maybe it makes sense to have it per-game? Race conditions abound...
ConfigSetting("UISound", &g_Config.bUISound, false, true, false),
- ReportedConfigSetting("NumWorkerThreads", &g_Config.iNumWorkerThreads, &DefaultNumWorkers, true, true),
ConfigSetting("AutoLoadSaveState", &g_Config.iAutoLoadSaveState, 0, true, true),
ReportedConfigSetting("EnableCheats", &g_Config.bEnableCheats, true, true, true),
ConfigSetting("CwCheatRefreshRate", &g_Config.iCwCheatRefreshRate, 77, true, true),
@@ -503,7 +547,6 @@ static ConfigSetting generalSettings[] = {
ConfigSetting("GridView1", &g_Config.bGridView1, true),
ConfigSetting("GridView2", &g_Config.bGridView2, true),
ConfigSetting("GridView3", &g_Config.bGridView3, false),
- ConfigSetting("ComboMode", &g_Config.iComboMode, 0),
ConfigSetting("RightAnalogUp", &g_Config.iRightAnalogUp, 0, true, true),
ConfigSetting("RightAnalogDown", &g_Config.iRightAnalogDown, 0, true, true),
ConfigSetting("RightAnalogLeft", &g_Config.iRightAnalogLeft, 0, true, true),
@@ -905,17 +948,16 @@ static ConfigSetting controlSettings[] = {
ConfigSetting("ShowTouchSquare", &g_Config.bShowTouchSquare, true, true, true),
ConfigSetting("ShowTouchTriangle", &g_Config.bShowTouchTriangle, true, true, true),
- ConfigSetting("ComboKey0Mapping", &g_Config.iCombokey0, 0, true, true),
- ConfigSetting("ComboKey1Mapping", &g_Config.iCombokey1, 0, true, true),
- ConfigSetting("ComboKey2Mapping", &g_Config.iCombokey2, 0, true, true),
- ConfigSetting("ComboKey3Mapping", &g_Config.iCombokey3, 0, true, true),
- ConfigSetting("ComboKey4Mapping", &g_Config.iCombokey4, 0, true, true),
-
- ConfigSetting("ComboKey0Toggle", &g_Config.bComboToggle0, false, true, true),
- ConfigSetting("ComboKey1Toggle", &g_Config.bComboToggle1, false, true, true),
- ConfigSetting("ComboKey2Toggle", &g_Config.bComboToggle2, false, true, true),
- ConfigSetting("ComboKey3Toggle", &g_Config.bComboToggle3, false, true, true),
- ConfigSetting("ComboKey4Toggle", &g_Config.bComboToggle4, false, true, true),
+ ConfigSetting("Custom0Mapping", "Custom0Image", "Custom0Shape", "Custom0Toggle", &g_Config.CustomKey0, {0, 0, 0, false}, true, true),
+ ConfigSetting("Custom1Mapping", "Custom1Image", "Custom1Shape", "Custom1Toggle", &g_Config.CustomKey1, {0, 1, 0, false}, true, true),
+ ConfigSetting("Custom2Mapping", "Custom2Image", "Custom2Shape", "Custom2Toggle", &g_Config.CustomKey2, {0, 2, 0, false}, true, true),
+ ConfigSetting("Custom3Mapping", "Custom3Image", "Custom3Shape", "Custom3Toggle", &g_Config.CustomKey3, {0, 3, 0, false}, true, true),
+ ConfigSetting("Custom4Mapping", "Custom4Image", "Custom4Shape", "Custom4Toggle", &g_Config.CustomKey4, {0, 4, 0, false}, true, true),
+ ConfigSetting("Custom5Mapping", "Custom5Image", "Custom5Shape", "Custom5Toggle", &g_Config.CustomKey5, {0, 0, 1, false}, true, true),
+ ConfigSetting("Custom6Mapping", "Custom6Image", "Custom6Shape", "Custom6Toggle", &g_Config.CustomKey6, {0, 1, 1, false}, true, true),
+ ConfigSetting("Custom7Mapping", "Custom7Image", "Custom7Shape", "Custom7Toggle", &g_Config.CustomKey7, {0, 2, 1, false}, true, true),
+ ConfigSetting("Custom8Mapping", "Custom8Image", "Custom8Shape", "Custom8Toggle", &g_Config.CustomKey8, {0, 3, 1, false}, true, true),
+ ConfigSetting("Custom9Mapping", "Custom9Image", "Custom9Shape", "Custom9Toggle", &g_Config.CustomKey9, {0, 4, 1, false}, true, true),
#if defined(_WIN32)
// A win32 user seeing touch controls is likely using PPSSPP on a tablet. There it makes
@@ -948,7 +990,7 @@ static ConfigSetting controlSettings[] = {
ConfigSetting("TouchButtonOpacity", &g_Config.iTouchButtonOpacity, 65, true, true),
ConfigSetting("TouchButtonHideSeconds", &g_Config.iTouchButtonHideSeconds, 20, true, true),
ConfigSetting("AutoCenterTouchAnalog", &g_Config.bAutoCenterTouchAnalog, false, true, true),
- ConfigSetting("AnalogAutoRotSpeed", &g_Config.fAnalogAutoRotSpeed, 15.0f, true, true),
+ ConfigSetting("AnalogAutoRotSpeed", &g_Config.fAnalogAutoRotSpeed, 8.0f, true, true),
// Snap touch control position
ConfigSetting("TouchSnapToGrid", &g_Config.bTouchSnapToGrid, false, true, true),
@@ -974,25 +1016,18 @@ static ConfigSetting controlSettings[] = {
ConfigSetting("fcombo2X", "fcombo2Y", "comboKeyScale2", "ShowComboKey2", &g_Config.touchCombo2, defaultTouchPosHide, true, true),
ConfigSetting("fcombo3X", "fcombo3Y", "comboKeyScale3", "ShowComboKey3", &g_Config.touchCombo3, defaultTouchPosHide, true, true),
ConfigSetting("fcombo4X", "fcombo4Y", "comboKeyScale4", "ShowComboKey4", &g_Config.touchCombo4, defaultTouchPosHide, true, true),
- ConfigSetting("Speed1KeyX", "Speed1KeyY", "Speed1KeyScale", "ShowSpeed1Key", &g_Config.touchSpeed1Key, defaultTouchPosHide, true, true),
- ConfigSetting("Speed2KeyX", "Speed2KeyY", "Speed2KeyScale", "ShowSpeed2Key", &g_Config.touchSpeed2Key, defaultTouchPosHide, true, true),
- ConfigSetting("RapidFireKeyX", "RapidFireKeyY", "RapidFireKeyScale", "ShowRapidFireKey", &g_Config.touchRapidFireKey, defaultTouchPosHide, true, true),
- ConfigSetting("AnalogRotationCWKeyX", "AnalogRotationKeyCWY", "AnalogRotationKeyCWScale", "ShowAnalogRotationCWKey", &g_Config.touchAnalogRotationCWKey, defaultTouchPosHide, true, true),
- ConfigSetting("AnalogRotationCCWKeyX", "AnalogRotationKeyCCWY", "AnalogRotationKeyCCWScale", "ShowAnalogRotationCCWKey", &g_Config.touchAnalogRotationCCWKey, defaultTouchPosHide, true, true),
+ ConfigSetting("fcombo5X", "fcombo5Y", "comboKeyScale5", "ShowComboKey5", &g_Config.touchCombo5, defaultTouchPosHide, true, true),
+ ConfigSetting("fcombo6X", "fcombo6Y", "comboKeyScale6", "ShowComboKey6", &g_Config.touchCombo6, defaultTouchPosHide, true, true),
+ ConfigSetting("fcombo7X", "fcombo7Y", "comboKeyScale7", "ShowComboKey7", &g_Config.touchCombo7, defaultTouchPosHide, true, true),
+ ConfigSetting("fcombo8X", "fcombo8Y", "comboKeyScale8", "ShowComboKey8", &g_Config.touchCombo8, defaultTouchPosHide, true, true),
+ ConfigSetting("fcombo9X", "fcombo9Y", "comboKeyScale9", "ShowComboKey9", &g_Config.touchCombo9, defaultTouchPosHide, true, true),
-#ifdef _WIN32
- ConfigSetting("DInputAnalogDeadzone", &g_Config.fDInputAnalogDeadzone, 0.1f, true, true),
- ConfigSetting("DInputAnalogInverseMode", &g_Config.iDInputAnalogInverseMode, 0, true, true),
- ConfigSetting("DInputAnalogInverseDeadzone", &g_Config.fDInputAnalogInverseDeadzone, 0.0f, true, true),
- ConfigSetting("DInputAnalogSensitivity", &g_Config.fDInputAnalogSensitivity, 1.0f, true, true),
+ ConfigSetting("AnalogDeadzone", &g_Config.fAnalogDeadzone, 0.15f, true, true),
+ ConfigSetting("AnalogInverseDeadzone", &g_Config.fAnalogInverseDeadzone, 0.0f, true, true),
+ ConfigSetting("AnalogSensitivity", &g_Config.fAnalogSensitivity, 1.1f, true, true),
+ ConfigSetting("AnalogIsCircular", &g_Config.bAnalogIsCircular, false , true, true),
ConfigSetting("EnableDInputWithXInput", &g_Config.bEnableDInputWithXInput, false, true, true),
- ConfigSetting("XInputAnalogDeadzone", &g_Config.fXInputAnalogDeadzone, 0.24f, true, true),
- ConfigSetting("XInputAnalogInverseMode", &g_Config.iXInputAnalogInverseMode, 0, true, true),
- ConfigSetting("XInputAnalogInverseDeadzone", &g_Config.fXInputAnalogInverseDeadzone, 0.0f, true, true),
-#endif
- // Also reused as generic analog sensitivity
- ConfigSetting("XInputAnalogSensitivity", &g_Config.fXInputAnalogSensitivity, 1.0f, true, true),
ConfigSetting("AnalogLimiterDeadzone", &g_Config.fAnalogLimiterDeadzone, 0.6f, true, true),
ConfigSetting("AxisBindThreshold", &g_Config.fAxisBindThreshold, 0.25f, true, true),
@@ -1229,17 +1264,22 @@ void Config::Reload() {
reload_ = false;
}
-void Config::Load(const char *iniFileName, const char *controllerIniFilename) {
+// Call this if you change the search path (such as when changing memstick directory. can't
+// really think of any other legit uses).
+void Config::UpdateIniLocation(const char *iniFileName, const char *controllerIniFilename) {
const bool useIniFilename = iniFileName != nullptr && strlen(iniFileName) > 0;
iniFilename_ = FindConfigFile(useIniFilename ? iniFileName : "ppsspp.ini");
+ const bool useControllerIniFilename = controllerIniFilename != nullptr && strlen(controllerIniFilename) > 0;
+ controllerIniFilename_ = FindConfigFile(useControllerIniFilename ? controllerIniFilename : "controls.ini");
+}
+void Config::Load(const char *iniFileName, const char *controllerIniFilename) {
if (!bUpdatedInstanceCounter) {
InitInstanceCounter();
bUpdatedInstanceCounter = true;
}
- const bool useControllerIniFilename = controllerIniFilename != nullptr && strlen(controllerIniFilename) > 0;
- controllerIniFilename_ = FindConfigFile(useControllerIniFilename ? controllerIniFilename : "controls.ini");
+ UpdateIniLocation(iniFileName, controllerIniFilename);
INFO_LOG(LOADER, "Loading config: %s", iniFilename_.c_str());
bSaveSettings = true;
@@ -1255,7 +1295,10 @@ void Config::Load(const char *iniFileName, const char *controllerIniFilename) {
});
iRunCount++;
- if (!File::Exists(Path(currentDirectory)))
+
+ // This check is probably not really necessary here anyway, you can always
+ // press Home or Browse if you're in a bad directory.
+ if (!File::Exists(currentDirectory))
currentDirectory = defaultCurrentDirectory;
Section *log = iniFile.GetOrCreateSection(logSectionName);
@@ -1809,11 +1852,11 @@ void Config::ResetControlLayout() {
reset(g_Config.touchCombo2);
reset(g_Config.touchCombo3);
reset(g_Config.touchCombo4);
- reset(g_Config.touchSpeed1Key);
- reset(g_Config.touchSpeed2Key);
- reset(g_Config.touchRapidFireKey);
- reset(g_Config.touchAnalogRotationCWKey);
- reset(g_Config.touchAnalogRotationCCWKey);
+ reset(g_Config.touchCombo5);
+ reset(g_Config.touchCombo6);
+ reset(g_Config.touchCombo7);
+ reset(g_Config.touchCombo8);
+ reset(g_Config.touchCombo9);
}
void Config::GetReportingInfo(UrlEncoder &data) {
diff --git a/Core/Config.h b/Core/Config.h
index ff9d0309ea..61d4178acd 100644
--- a/Core/Config.h
+++ b/Core/Config.h
@@ -56,6 +56,13 @@ struct ConfigTouchPos {
bool show;
};
+struct ConfigCustomButton {
+ uint64_t key;
+ int image;
+ int shape;
+ bool toggle;
+};
+
struct Config {
public:
Config();
@@ -73,7 +80,6 @@ public:
bool bBrowse; // when opening the emulator, immediately show a file browser
// General
- int iNumWorkerThreads;
bool bScreenshotsAsPNG;
bool bScreenshotsAtRenderRes;
bool bSavestateScreenshotResLimit;
@@ -309,8 +315,6 @@ public:
bool bGridView1;
bool bGridView2;
bool bGridView3;
- //Combo key screen flag
- int iComboMode;
// Right analog binding
int iRightAnalogUp;
@@ -359,11 +363,11 @@ public:
ConfigTouchPos touchCombo2;
ConfigTouchPos touchCombo3;
ConfigTouchPos touchCombo4;
- ConfigTouchPos touchSpeed1Key;
- ConfigTouchPos touchSpeed2Key;
- ConfigTouchPos touchRapidFireKey;
- ConfigTouchPos touchAnalogRotationCWKey;
- ConfigTouchPos touchAnalogRotationCCWKey;
+ ConfigTouchPos touchCombo5;
+ ConfigTouchPos touchCombo6;
+ ConfigTouchPos touchCombo7;
+ ConfigTouchPos touchCombo8;
+ ConfigTouchPos touchCombo9;
// Controls Visibility
bool bShowTouchControls;
@@ -373,35 +377,30 @@ public:
bool bShowTouchTriangle;
bool bShowTouchSquare;
- // Combo_key mapping. These are bitfields.
- int iCombokey0;
- int iCombokey1;
- int iCombokey2;
- int iCombokey3;
- int iCombokey4;
-
- bool bComboToggle0;
- bool bComboToggle1;
- bool bComboToggle2;
- bool bComboToggle3;
- bool bComboToggle4;
+ ConfigCustomButton CustomKey0;
+ ConfigCustomButton CustomKey1;
+ ConfigCustomButton CustomKey2;
+ ConfigCustomButton CustomKey3;
+ ConfigCustomButton CustomKey4;
+ ConfigCustomButton CustomKey5;
+ ConfigCustomButton CustomKey6;
+ ConfigCustomButton CustomKey7;
+ ConfigCustomButton CustomKey8;
+ ConfigCustomButton CustomKey9;
// Ignored on iOS and other platforms that lack pause.
bool bShowTouchPause;
bool bHapticFeedback;
- float fDInputAnalogDeadzone;
- int iDInputAnalogInverseMode;
- float fDInputAnalogInverseDeadzone;
- float fDInputAnalogSensitivity;
bool bEnableDInputWithXInput;
-
// We also use the XInput settings as analog settings on other platforms like Android.
- float fXInputAnalogDeadzone;
- int iXInputAnalogInverseMode;
- float fXInputAnalogInverseDeadzone;
- float fXInputAnalogSensitivity;
+ float fAnalogDeadzone;
+ float fAnalogInverseDeadzone;
+ float fAnalogSensitivity;
+ // convert analog stick circle to square
+ bool bAnalogIsCircular;
+
float fAnalogLimiterDeadzone;
float fAxisBindThreshold;
@@ -524,6 +523,8 @@ public:
void SetSearchPath(const Path &path);
const Path FindConfigFile(const std::string &baseFilename);
+ void UpdateIniLocation(const char *iniFileName = nullptr, const char *controllerIniFilename = nullptr);
+
// Utility functions for "recent" management
void AddRecent(const std::string &file);
void RemoveRecent(const std::string &file);
diff --git a/Core/ControlMapper.cpp b/Core/ControlMapper.cpp
new file mode 100644
index 0000000000..de71298e87
--- /dev/null
+++ b/Core/ControlMapper.cpp
@@ -0,0 +1,374 @@
+#include
+
+#include "Common/Math/math_util.h"
+#include "Common/TimeUtil.h"
+
+#include "Core/KeyMap.h"
+#include "Core/ControlMapper.h"
+#include "Core/Config.h"
+
+static float MapAxisValue(float v) {
+ const float deadzone = g_Config.fAnalogDeadzone;
+ const float invDeadzone = g_Config.fAnalogInverseDeadzone;
+ const float sensitivity = g_Config.fAnalogSensitivity;
+ const float sign = v >= 0.0f ? 1.0f : -1.0f;
+ return sign * Clamp(invDeadzone + (abs(v) - deadzone) / (1.0f - deadzone) * (sensitivity - invDeadzone), 0.0f, 1.0f);
+}
+
+void ConvertAnalogStick(float &x, float &y) {
+ const bool isCircular = g_Config.bAnalogIsCircular;
+
+ float norm = std::max(fabsf(x), fabsf(y));
+
+ if (norm == 0.0f)
+ return;
+
+ if (isCircular) {
+ float newNorm = sqrtf(x * x + y * y);
+ float factor = newNorm / norm;
+ x *= factor;
+ y *= factor;
+ norm = newNorm;
+ }
+
+ float mappedNorm = MapAxisValue(norm);
+ x = Clamp(x / norm * mappedNorm, -1.0f, 1.0f);
+ y = Clamp(y / norm * mappedNorm, -1.0f, 1.0f);
+}
+
+void ControlMapper::SetCallbacks(std::function onVKeyDown, std::function onVKeyUp, std::function setPSPAnalog) {
+ onVKeyDown_ = onVKeyDown;
+ onVKeyUp_ = onVKeyUp;
+ setPSPAnalog_ = setPSPAnalog;
+}
+
+void ControlMapper::SetRawCallback(std::function setRawAnalog) {
+ setRawAnalog_ = setRawAnalog;
+}
+
+
+void ControlMapper::SetPSPAxis(char axis, float value, int stick) {
+ static float history[2][2] = {};
+
+ int axisId = axis == 'X' ? 0 : 1;
+
+ history[stick][axisId] = value;
+
+ float x = history[stick][0];
+ float y = history[stick][1];
+
+ if (setRawAnalog_) {
+ setRawAnalog_(stick, x, y);
+ }
+
+ ConvertAnalogStick(x, y);
+
+ setPSPAnalog_(stick, x, y);
+}
+
+bool ControlMapper::Key(const KeyInput &key, bool *pauseTrigger) {
+ std::vector pspKeys;
+ KeyMap::KeyToPspButton(key.deviceId, key.keyCode, &pspKeys);
+
+ if (pspKeys.size() && (key.flags & KEY_IS_REPEAT)) {
+ // Claim that we handled this. Prevents volume key repeats from popping up the volume control on Android.
+ return true;
+ }
+
+ for (size_t i = 0; i < pspKeys.size(); i++) {
+ pspKey(pspKeys[i], key.flags);
+ }
+
+ if (!pspKeys.size() || key.deviceId == DEVICE_ID_DEFAULT) {
+ if ((key.flags & KEY_DOWN) && key.keyCode == NKCODE_BACK) {
+ *pauseTrigger = true;
+ return true;
+ }
+ }
+
+ return pspKeys.size() > 0;
+}
+
+bool ControlMapper::Axis(const AxisInput &axis) {
+ if (axis.value > 0) {
+ processAxis(axis, 1);
+ return true;
+ } else if (axis.value < 0) {
+ processAxis(axis, -1);
+ return true;
+ } else if (axis.value == 0) {
+ // Both directions! Prevents sticking for digital input devices that are axises (like HAT)
+ processAxis(axis, 1);
+ processAxis(axis, -1);
+ return true;
+ }
+ return false;
+}
+
+void ControlMapper::Update() {
+ if (autoRotatingAnalogCW_) {
+ const double now = time_now_d();
+ // Clamp to a square
+ float x = std::min(1.0f, std::max(-1.0f, 1.42f * (float)cos(now * -g_Config.fAnalogAutoRotSpeed)));
+ float y = std::min(1.0f, std::max(-1.0f, 1.42f * (float)sin(now * -g_Config.fAnalogAutoRotSpeed)));
+
+ setPSPAnalog_(0, x, y);
+ } else if (autoRotatingAnalogCCW_) {
+ const double now = time_now_d();
+ float x = std::min(1.0f, std::max(-1.0f, 1.42f * (float)cos(now * g_Config.fAnalogAutoRotSpeed)));
+ float y = std::min(1.0f, std::max(-1.0f, 1.42f * (float)sin(now * g_Config.fAnalogAutoRotSpeed)));
+
+ setPSPAnalog_(0, x, y);
+ }
+}
+
+
+inline bool IsAnalogStickKey(int key) {
+ switch (key) {
+ case VIRTKEY_AXIS_X_MIN:
+ case VIRTKEY_AXIS_X_MAX:
+ case VIRTKEY_AXIS_Y_MIN:
+ case VIRTKEY_AXIS_Y_MAX:
+ case VIRTKEY_AXIS_RIGHT_X_MIN:
+ case VIRTKEY_AXIS_RIGHT_X_MAX:
+ case VIRTKEY_AXIS_RIGHT_Y_MIN:
+ case VIRTKEY_AXIS_RIGHT_Y_MAX:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static int RotatePSPKeyCode(int x) {
+ switch (x) {
+ case CTRL_UP: return CTRL_RIGHT;
+ case CTRL_RIGHT: return CTRL_DOWN;
+ case CTRL_DOWN: return CTRL_LEFT;
+ case CTRL_LEFT: return CTRL_UP;
+ default:
+ return x;
+ }
+}
+
+void ControlMapper::setVKeyAnalog(char axis, int stick, int virtualKeyMin, int virtualKeyMax, bool setZero) {
+ // The down events can repeat, so just trust the virtKeys array.
+ bool minDown = virtKeys[virtualKeyMin - VIRTKEY_FIRST];
+ bool maxDown = virtKeys[virtualKeyMax - VIRTKEY_FIRST];
+
+ const float scale = virtKeys[VIRTKEY_ANALOG_LIGHTLY - VIRTKEY_FIRST] ? g_Config.fAnalogLimiterDeadzone : 1.0f;
+ float value = 0.0f;
+ if (minDown)
+ value -= scale;
+ if (maxDown)
+ value += scale;
+ if (setZero || minDown || maxDown) {
+ SetPSPAxis(axis, value, stick);
+ }
+}
+
+void ControlMapper::pspKey(int pspKeyCode, int flags) {
+ int rotations = 0;
+ switch (g_Config.iInternalScreenRotation) {
+ case ROTATION_LOCKED_HORIZONTAL180:
+ rotations = 2;
+ break;
+ case ROTATION_LOCKED_VERTICAL:
+ rotations = 1;
+ break;
+ case ROTATION_LOCKED_VERTICAL180:
+ rotations = 3;
+ break;
+ }
+
+ for (int i = 0; i < rotations; i++) {
+ pspKeyCode = RotatePSPKeyCode(pspKeyCode);
+ }
+
+ if (pspKeyCode >= VIRTKEY_FIRST) {
+ int vk = pspKeyCode - VIRTKEY_FIRST;
+ if (flags & KEY_DOWN) {
+ virtKeys[vk] = true;
+ onVKeyDown(pspKeyCode);
+ }
+ if (flags & KEY_UP) {
+ virtKeys[vk] = false;
+ onVKeyUp(pspKeyCode);
+ }
+ } else {
+ // INFO_LOG(SYSTEM, "pspKey %i %i", pspKeyCode, flags);
+ if (flags & KEY_DOWN)
+ __CtrlButtonDown(pspKeyCode);
+ if (flags & KEY_UP)
+ __CtrlButtonUp(pspKeyCode);
+ }
+}
+
+void ControlMapper::onVKeyDown(int vkey) {
+ switch (vkey) {
+
+ case VIRTKEY_AXIS_X_MIN:
+ case VIRTKEY_AXIS_X_MAX:
+ setVKeyAnalog('X', CTRL_STICK_LEFT, VIRTKEY_AXIS_X_MIN, VIRTKEY_AXIS_X_MAX);
+ break;
+ case VIRTKEY_AXIS_Y_MIN:
+ case VIRTKEY_AXIS_Y_MAX:
+ setVKeyAnalog('Y', CTRL_STICK_LEFT, VIRTKEY_AXIS_Y_MIN, VIRTKEY_AXIS_Y_MAX);
+ break;
+
+ case VIRTKEY_AXIS_RIGHT_X_MIN:
+ case VIRTKEY_AXIS_RIGHT_X_MAX:
+ setVKeyAnalog('X', CTRL_STICK_RIGHT, VIRTKEY_AXIS_RIGHT_X_MIN, VIRTKEY_AXIS_RIGHT_X_MAX);
+ break;
+ case VIRTKEY_AXIS_RIGHT_Y_MIN:
+ case VIRTKEY_AXIS_RIGHT_Y_MAX:
+ setVKeyAnalog('Y', CTRL_STICK_RIGHT, VIRTKEY_AXIS_RIGHT_Y_MIN, VIRTKEY_AXIS_RIGHT_Y_MAX);
+ break;
+
+ case VIRTKEY_ANALOG_LIGHTLY:
+ setVKeyAnalog('X', CTRL_STICK_LEFT, VIRTKEY_AXIS_X_MIN, VIRTKEY_AXIS_X_MAX, false);
+ setVKeyAnalog('Y', CTRL_STICK_LEFT, VIRTKEY_AXIS_Y_MIN, VIRTKEY_AXIS_Y_MAX, false);
+ setVKeyAnalog('X', CTRL_STICK_RIGHT, VIRTKEY_AXIS_RIGHT_X_MIN, VIRTKEY_AXIS_RIGHT_X_MAX, false);
+ setVKeyAnalog('Y', CTRL_STICK_RIGHT, VIRTKEY_AXIS_RIGHT_Y_MIN, VIRTKEY_AXIS_RIGHT_Y_MAX, false);
+ break;
+
+ case VIRTKEY_ANALOG_ROTATE_CW:
+ autoRotatingAnalogCW_ = true;
+ autoRotatingAnalogCCW_ = false;
+ break;
+ case VIRTKEY_ANALOG_ROTATE_CCW:
+ autoRotatingAnalogCW_ = false;
+ autoRotatingAnalogCCW_ = true;
+ break;
+
+ default:
+ if (onVKeyDown_)
+ onVKeyDown_(vkey);
+ break;
+ }
+}
+
+void ControlMapper::onVKeyUp(int vkey) {
+ switch (vkey) {
+
+ case VIRTKEY_AXIS_X_MIN:
+ case VIRTKEY_AXIS_X_MAX:
+ setVKeyAnalog('X', CTRL_STICK_LEFT, VIRTKEY_AXIS_X_MIN, VIRTKEY_AXIS_X_MAX);
+ break;
+ case VIRTKEY_AXIS_Y_MIN:
+ case VIRTKEY_AXIS_Y_MAX:
+ setVKeyAnalog('Y', CTRL_STICK_LEFT, VIRTKEY_AXIS_Y_MIN, VIRTKEY_AXIS_Y_MAX);
+ break;
+
+ case VIRTKEY_AXIS_RIGHT_X_MIN:
+ case VIRTKEY_AXIS_RIGHT_X_MAX:
+ setVKeyAnalog('X', CTRL_STICK_RIGHT, VIRTKEY_AXIS_RIGHT_X_MIN, VIRTKEY_AXIS_RIGHT_X_MAX);
+ break;
+ case VIRTKEY_AXIS_RIGHT_Y_MIN:
+ case VIRTKEY_AXIS_RIGHT_Y_MAX:
+ setVKeyAnalog('Y', CTRL_STICK_RIGHT, VIRTKEY_AXIS_RIGHT_Y_MIN, VIRTKEY_AXIS_RIGHT_Y_MAX);
+ break;
+
+ case VIRTKEY_ANALOG_LIGHTLY:
+ setVKeyAnalog('X', CTRL_STICK_LEFT, VIRTKEY_AXIS_X_MIN, VIRTKEY_AXIS_X_MAX, false);
+ setVKeyAnalog('Y', CTRL_STICK_LEFT, VIRTKEY_AXIS_Y_MIN, VIRTKEY_AXIS_Y_MAX, false);
+ setVKeyAnalog('X', CTRL_STICK_RIGHT, VIRTKEY_AXIS_RIGHT_X_MIN, VIRTKEY_AXIS_RIGHT_X_MAX, false);
+ setVKeyAnalog('Y', CTRL_STICK_RIGHT, VIRTKEY_AXIS_RIGHT_Y_MIN, VIRTKEY_AXIS_RIGHT_Y_MAX, false);
+ break;
+
+ case VIRTKEY_ANALOG_ROTATE_CW:
+ autoRotatingAnalogCW_ = false;
+ __CtrlSetAnalogXY(0, 0.0f, 0.0f);
+ break;
+
+ case VIRTKEY_ANALOG_ROTATE_CCW:
+ autoRotatingAnalogCCW_ = false;
+ __CtrlSetAnalogXY(0, 0.0f, 0.0f);
+ break;
+
+ default:
+ if (onVKeyUp_)
+ onVKeyUp_(vkey);
+ break;
+ }
+}
+
+void ControlMapper::processAxis(const AxisInput &axis, int direction) {
+ // Sanity check
+ if (axis.axisId < 0 || axis.axisId >= JOYSTICK_AXIS_MAX) {
+ return;
+ }
+
+ const float scale = virtKeys[VIRTKEY_ANALOG_LIGHTLY - VIRTKEY_FIRST] ? g_Config.fAnalogLimiterDeadzone : 1.0f;
+
+ std::vector results;
+ KeyMap::AxisToPspButton(axis.deviceId, axis.axisId, direction, &results);
+
+ for (int result : results) {
+ float value = fabs(axis.value) * scale;
+ switch (result) {
+ case VIRTKEY_AXIS_X_MIN:
+ SetPSPAxis('X', -value, CTRL_STICK_LEFT);
+ break;
+ case VIRTKEY_AXIS_X_MAX:
+ SetPSPAxis('X', value, CTRL_STICK_LEFT);
+ break;
+ case VIRTKEY_AXIS_Y_MIN:
+ SetPSPAxis('Y', -value, CTRL_STICK_LEFT);
+ break;
+ case VIRTKEY_AXIS_Y_MAX:
+ SetPSPAxis('Y', value, CTRL_STICK_LEFT);
+ break;
+
+ case VIRTKEY_AXIS_RIGHT_X_MIN:
+ SetPSPAxis('X', -value, CTRL_STICK_RIGHT);
+ break;
+ case VIRTKEY_AXIS_RIGHT_X_MAX:
+ SetPSPAxis('X', value, CTRL_STICK_RIGHT);
+ break;
+ case VIRTKEY_AXIS_RIGHT_Y_MIN:
+ SetPSPAxis('Y', -value, CTRL_STICK_RIGHT);
+ break;
+ case VIRTKEY_AXIS_RIGHT_Y_MAX:
+ SetPSPAxis('Y', value, CTRL_STICK_RIGHT);
+ break;
+ }
+ }
+
+ std::vector resultsOpposite;
+ KeyMap::AxisToPspButton(axis.deviceId, axis.axisId, -direction, &resultsOpposite);
+
+ int axisState = 0;
+ float threshold = axis.deviceId == DEVICE_ID_MOUSE ? AXIS_BIND_THRESHOLD_MOUSE : AXIS_BIND_THRESHOLD;
+ if (direction == 1 && axis.value >= threshold) {
+ axisState = 1;
+ } else if (direction == -1 && axis.value <= -threshold) {
+ axisState = -1;
+ } else {
+ axisState = 0;
+ }
+
+ if (axisState != axisState_[axis.axisId]) {
+ axisState_[axis.axisId] = axisState;
+ if (axisState != 0) {
+ for (size_t i = 0; i < results.size(); i++) {
+ if (!IsAnalogStickKey(results[i]))
+ pspKey(results[i], KEY_DOWN);
+ }
+ // Also unpress the other direction (unless both directions press the same key.)
+ for (size_t i = 0; i < resultsOpposite.size(); i++) {
+ if (!IsAnalogStickKey(resultsOpposite[i]) && std::find(results.begin(), results.end(), resultsOpposite[i]) == results.end())
+ pspKey(resultsOpposite[i], KEY_UP);
+ }
+ } else if (axisState == 0) {
+ // Release both directions, trying to deal with some erratic controllers that can cause it to stick.
+ for (size_t i = 0; i < results.size(); i++) {
+ if (!IsAnalogStickKey(results[i]))
+ pspKey(results[i], KEY_UP);
+ }
+ for (size_t i = 0; i < resultsOpposite.size(); i++) {
+ if (!IsAnalogStickKey(resultsOpposite[i]))
+ pspKey(resultsOpposite[i], KEY_UP);
+ }
+ }
+ }
+}
diff --git a/Core/ControlMapper.h b/Core/ControlMapper.h
new file mode 100644
index 0000000000..cb363c7ebc
--- /dev/null
+++ b/Core/ControlMapper.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "Common/Input/InputState.h"
+#include "Core/KeyMap.h"
+
+#include
+
+// Utilities for mapping input events to PSP inputs and virtual keys.
+// Main use is of course from EmuScreen.cpp, but also useful from control settings etc.
+
+// At some point I want to refactor this from using callbacks to simply providing lists of events.
+// Still it won't be able to be completely stateless due to the 2-D processing of analog sticks.
+
+class ControlMapper {
+public:
+ void Update();
+
+ bool Key(const KeyInput &key, bool *pauseTrigger);
+ void pspKey(int pspKeyCode, int flags);
+ bool Axis(const AxisInput &axis);
+
+ // Required callbacks
+ void SetCallbacks(
+ std::function onVKeyDown,
+ std::function onVKeyUp,
+ std::function setPSPAnalog);
+
+ // Optional callback, only used in config
+ void SetRawCallback(std::function setRawAnalog);
+
+private:
+ void processAxis(const AxisInput &axis, int direction);
+ void setVKeyAnalog(char axis, int stick, int virtualKeyMin, int virtualKeyMax, bool setZero = true);
+
+ void SetPSPAxis(char axis, float value, int stick);
+
+ void onVKeyDown(int vkey);
+ void onVKeyUp(int vkey);
+
+ // To track mappable virtual keys. We can have as many as we want.
+ bool virtKeys[VIRTKEY_COUNT]{};
+
+ // De-noise mapped axis updates
+ int axisState_[JOYSTICK_AXIS_MAX]{};
+
+ // Mappable auto-rotation. Useful for keyboard/dpad->analog in a few games.
+ bool autoRotatingAnalogCW_ = false;
+ bool autoRotatingAnalogCCW_ = false;
+
+ // Callbacks
+ std::function onVKeyDown_;
+ std::function onVKeyUp_;
+ std::function setPSPAnalog_;
+ std::function setRawAnalog_;
+};
+
+void ConvertAnalogStick(float &x, float &y);
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index ececc51b5f..d8d96a3f31 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -509,6 +509,7 @@
+
@@ -1064,6 +1065,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index 43a36fa022..486f2a2e10 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -1175,6 +1175,9 @@
Debugger\WebSocket
+
+ Core
+
@@ -1892,6 +1895,9 @@
Debugger\WebSocket
+
+ Core
+
diff --git a/Core/CwCheat.cpp b/Core/CwCheat.cpp
index 5605601ff0..0a348cf089 100644
--- a/Core/CwCheat.cpp
+++ b/Core/CwCheat.cpp
@@ -338,10 +338,14 @@ void hleCheat(u64 userdata, int cyclesLate) {
} else if (gameTitle == "ULES00033" || gameTitle == "ULES00034" || gameTitle == "ULES00035") { // euro, also 34 and 35
cheatEngine->InvalidateICache(0x088655D8, 72);
cheatEngine->InvalidateICache(0x08865668, 4);
- } else if (gameTitle == "ULUS10138") { // MTX MotoTrax. No hack for ULES00581 known.
+ } else if (gameTitle == "ULUS10138") { // MTX MotoTrax US
cheatEngine->InvalidateICache(0x0886DCC0, 72);
cheatEngine->InvalidateICache(0x0886DC20, 4);
cheatEngine->InvalidateICache(0x0886DD40, 4);
+ } else if (gameTitle == "ULES00581") { // MTX MotoTrax EU (ported from US cwcheat codes)
+ cheatEngine->InvalidateICache(0x0886E1D8, 72);
+ cheatEngine->InvalidateICache(0x0886E138, 4);
+ cheatEngine->InvalidateICache(0x0886E258, 4);
}
}
diff --git a/Core/Debugger/SymbolMap.cpp b/Core/Debugger/SymbolMap.cpp
index 809044353d..5b9fb0a818 100644
--- a/Core/Debugger/SymbolMap.cpp
+++ b/Core/Debugger/SymbolMap.cpp
@@ -1013,6 +1013,7 @@ void SymbolMap::GetLabels(std::vector &dest) {
LabelDefinition entry;
entry.value = it->first;
entry.name = ConvertUTF8ToWString(it->second.name);
+ std::transform(entry.name.begin(), entry.name.end(), entry.name.begin(), ::towlower);
dest.push_back(entry);
}
}
diff --git a/Core/Debugger/WebSocket/InputSubscriber.cpp b/Core/Debugger/WebSocket/InputSubscriber.cpp
index b35c8413e7..facedff0fc 100644
--- a/Core/Debugger/WebSocket/InputSubscriber.cpp
+++ b/Core/Debugger/WebSocket/InputSubscriber.cpp
@@ -254,8 +254,7 @@ void WebSocketInputState::AnalogSend(DebuggerRequest &req) {
if (!AnalogValue(req, &x, "x") || !AnalogValue(req, &y, "y"))
return;
- __CtrlSetAnalogX(x, stick == "left" ? CTRL_STICK_LEFT : CTRL_STICK_RIGHT);
- __CtrlSetAnalogY(y, stick == "left" ? CTRL_STICK_LEFT : CTRL_STICK_RIGHT);
+ __CtrlSetAnalogXY(stick == "left" ? CTRL_STICK_LEFT : CTRL_STICK_RIGHT, x, y);
req.Respond();
}
diff --git a/Core/Dialog/PSPOskDialog.cpp b/Core/Dialog/PSPOskDialog.cpp
index fd2cd25252..c8c0670339 100755
--- a/Core/Dialog/PSPOskDialog.cpp
+++ b/Core/Dialog/PSPOskDialog.cpp
@@ -47,7 +47,7 @@ const static int OSK_SHUTDOWN_DELAY_US = 40000;
static std::map> languageMapping;
const int numKeyCols[OSK_KEYBOARD_COUNT] = {12, 12, 13, 13, 12, 12, 12, 12, 12};
-const int numKeyRows[OSK_KEYBOARD_COUNT] = {4, 4, 5, 5, 5, 4, 4, 4, 4};
+const int numKeyRows[OSK_KEYBOARD_COUNT] = {4, 4, 6, 6, 5, 4, 4, 4, 4};
// Japanese (Kana) diacritics
static const wchar_t diacritics[2][103] =
@@ -74,7 +74,7 @@ const int kor_lconsCom[] = {18,0,2,21,3,4,26,3,5,0,7,8,15,7,9,16,7,10,18,7,11,24
// Korean (Hangul) last consonant Separation key
const int kor_lconsSpr[] = {2,1,9,4,4,12,5,4,18,8,8,0,9,8,6,10,8,7,11,8,9,12,8,16,13,8,17,14,8,18,17,17,9};
-static const char16_t oskKeys[OSK_KEYBOARD_COUNT][5][14] =
+static const char16_t oskKeys[OSK_KEYBOARD_COUNT][6][14] =
{
{
// Latin Lowercase
@@ -97,6 +97,7 @@ static const char16_t oskKeys[OSK_KEYBOARD_COUNT][5][14] =
{u"うくすつぬふむゆるをぅゅ゛"},
{u"えけせてねへめ れ ぇ ゜"},
{u"おこそとのほもよろんぉょー"},
+ {u"・。、「」『』〜 "},
},
{
// Katakana
@@ -105,6 +106,7 @@ static const char16_t oskKeys[OSK_KEYBOARD_COUNT][5][14] =
{u"ウクスツヌフムユルヲゥュ゛"},
{u"エケセテネヘメ レ ェ ゜"},
{u"オコソトノホモヨロンォョー"},
+ {u"・。、「」『』〜 "},
},
{
// Korean(Hangul)
diff --git a/Core/ELF/ElfReader.cpp b/Core/ELF/ElfReader.cpp
index 82b0d6116d..a350066f38 100644
--- a/Core/ELF/ElfReader.cpp
+++ b/Core/ELF/ElfReader.cpp
@@ -15,7 +15,11 @@
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+#include
+
#include "Common/StringUtils.h"
+#include "Common/Thread/ParallelLoop.h"
+
#include "Core/MemMap.h"
#include "Core/Reporting.h"
#include "Core/ThreadPools.h"
@@ -60,9 +64,10 @@ bool ElfReader::LoadRelocations(const Elf32_Rel *rels, int numRelocs) {
relocOps.resize(numRelocs);
DEBUG_LOG(LOADER, "Loading %i relocations...", numRelocs);
+ std::atomic numErrors;
+ numErrors.store(0);
- int numErrors = 0;
- GlobalThreadPool::Loop([&](int l, int h) {
+ ParallelRangeLoop(&g_threadManager, [&](int l, int h) {
for (int r = l; r < h; r++) {
u32 info = rels[r].r_info;
u32 addr = rels[r].r_offset;
@@ -96,7 +101,7 @@ bool ElfReader::LoadRelocations(const Elf32_Rel *rels, int numRelocs) {
}
}, 0, numRelocs, 128);
- GlobalThreadPool::Loop([&](int l, int h) {
+ ParallelRangeLoop(&g_threadManager, [&](int l, int h) {
for (int r = l; r < h; r++) {
VERBOSE_LOG(LOADER, "Loading reloc %i (%p)...", r, rels + r);
u32 info = rels[r].r_info;
@@ -210,7 +215,7 @@ bool ElfReader::LoadRelocations(const Elf32_Rel *rels, int numRelocs) {
}, 0, numRelocs, 128);
if (numErrors) {
- WARN_LOG(LOADER, "%i bad relocations found!!!", numErrors);
+ WARN_LOG(LOADER, "%i bad relocations found!!!", numErrors.load());
}
return numErrors == 0;
}
diff --git a/Core/FileLoaders/LocalFileLoader.cpp b/Core/FileLoaders/LocalFileLoader.cpp
index df3593c585..d0c4c6828c 100644
--- a/Core/FileLoaders/LocalFileLoader.cpp
+++ b/Core/FileLoaders/LocalFileLoader.cpp
@@ -25,6 +25,10 @@
#include "Common/File/DirListing.h"
#include "Core/FileLoaders/LocalFileLoader.h"
+#if PPSSPP_PLATFORM(ANDROID)
+#include "android/jni/app-android.h"
+#endif
+
#ifdef _WIN32
#include "Common/CommonWindows.h"
#else
@@ -32,11 +36,6 @@
#endif
#ifndef _WIN32
-LocalFileLoader::LocalFileLoader(int fd, const Path &filename) : fd_(fd), filename_(filename), isOpenedByFd_(fd != -1) {
- if (fd != -1) {
- DetectSizeFd();
- }
-}
void LocalFileLoader::DetectSizeFd() {
#if PPSSPP_PLATFORM(ANDROID) || (defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS < 64)
@@ -52,11 +51,27 @@ void LocalFileLoader::DetectSizeFd() {
#endif
LocalFileLoader::LocalFileLoader(const Path &filename)
- : filesize_(0), filename_(filename), isOpenedByFd_(false) {
+ : filesize_(0), filename_(filename) {
if (filename.empty()) {
ERROR_LOG(FILESYS, "LocalFileLoader can't load empty filenames");
return;
}
+
+#if PPSSPP_PLATFORM(ANDROID)
+ if (filename.Type() == PathType::CONTENT_URI) {
+ int fd = Android_OpenContentUriFd(filename.ToString(), Android_OpenContentUriMode::READ);
+ INFO_LOG(SYSTEM, "Fd %d for content URI: '%s'", fd, filename.c_str());
+ if (fd == -1) {
+ ERROR_LOG(FILESYS, "LoadFileLoader failed to open content URI: '%s'", filename.c_str());
+ return;
+ }
+ fd_ = fd;
+ isOpenedByFd_ = true;
+ DetectSizeFd();
+ return;
+ }
+#endif
+
#ifndef _WIN32
fd_ = open(filename.c_str(), O_RDONLY | O_CLOEXEC);
@@ -106,14 +121,18 @@ bool LocalFileLoader::Exists() {
// If we couldn't open it for reading, we say it does not exist.
#ifndef _WIN32
if (isOpenedByFd_) {
- return true;
+ return fd_ != -1;
}
if (fd_ != -1 || IsDirectory()) {
#else
if (handle_ != INVALID_HANDLE_VALUE || IsDirectory()) {
#endif
File::FileInfo info;
- return File::GetFileInfo(filename_, &info);
+ if (File::GetFileInfo(filename_, &info)) {
+ return info.exists;
+ } else {
+ return false;
+ }
}
return false;
}
@@ -121,7 +140,7 @@ bool LocalFileLoader::Exists() {
bool LocalFileLoader::IsDirectory() {
File::FileInfo info;
if (File::GetFileInfo(filename_, &info)) {
- return info.isDirectory;
+ return info.exists && info.isDirectory;
}
return false;
}
@@ -134,6 +153,11 @@ size_t LocalFileLoader::ReadAt(s64 absolutePos, size_t bytes, size_t count, void
if (bytes == 0)
return 0;
+ if (filesize_ == 0) {
+ ERROR_LOG(FILESYS, "ReadAt from 0-sized file: %s", filename_.c_str());
+ return 0;
+ }
+
#if PPSSPP_PLATFORM(SWITCH)
// Toolchain has no fancy IO API. We must lock.
std::lock_guard guard(readLock_);
diff --git a/Core/FileLoaders/LocalFileLoader.h b/Core/FileLoaders/LocalFileLoader.h
index 9ed9b185fa..9e1f37c10f 100644
--- a/Core/FileLoaders/LocalFileLoader.h
+++ b/Core/FileLoaders/LocalFileLoader.h
@@ -30,7 +30,6 @@ typedef void *HANDLE;
class LocalFileLoader : public FileLoader {
public:
LocalFileLoader(const Path &filename);
- LocalFileLoader(const int fd, const Path &filename);
virtual ~LocalFileLoader();
virtual bool Exists() override;
@@ -44,12 +43,12 @@ public:
private:
#ifndef _WIN32
void DetectSizeFd();
- int fd_;
+ int fd_ = -1;
#else
- HANDLE handle_;
+ HANDLE handle_ = 0;
#endif
- u64 filesize_;
+ u64 filesize_ = 0;
Path filename_;
std::mutex readLock_;
- bool isOpenedByFd_;
+ bool isOpenedByFd_ = false;
};
diff --git a/Core/FileSystems/DirectoryFileSystem.cpp b/Core/FileSystems/DirectoryFileSystem.cpp
index 5e84837b31..f2d106b5b0 100644
--- a/Core/FileSystems/DirectoryFileSystem.cpp
+++ b/Core/FileSystems/DirectoryFileSystem.cpp
@@ -187,7 +187,7 @@ bool DirectoryFileHandle::Open(const Path &basePath, std::string &fileName, File
error = 0;
#if HOST_IS_CASE_SENSITIVE
- if (access & (FILEACCESS_APPEND|FILEACCESS_CREATE|FILEACCESS_WRITE)) {
+ if (access & (FILEACCESS_APPEND | FILEACCESS_CREATE | FILEACCESS_WRITE)) {
DEBUG_LOG(FILESYS, "Checking case for path %s", fileName.c_str());
if (!FixPathCase(basePath.ToString(), fileName, FPC_PATH_MUST_EXIST)) {
error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
@@ -208,7 +208,7 @@ bool DirectoryFileHandle::Open(const Path &basePath, std::string &fileName, File
}
//TODO: tests, should append seek to end of file? seeking in a file opened for append?
-#ifdef _WIN32
+#if PPSSPP_PLATFORM(WINDOWS)
// Convert parameters to Windows permissions and access
DWORD desired = 0;
DWORD sharemode = 0;
@@ -265,6 +265,37 @@ bool DirectoryFileHandle::Open(const Path &basePath, std::string &fileName, File
}
}
#else
+ if (fullName.Type() == PathType::CONTENT_URI) {
+ // Convert flags. Don't want to share this type, bad dependency.
+ u32 flags = File::OPEN_NONE;
+ if (access & FILEACCESS_READ)
+ flags |= File::OPEN_READ;
+ if (access & FILEACCESS_WRITE)
+ flags |= File::OPEN_WRITE;
+ if (access & FILEACCESS_APPEND)
+ flags |= File::OPEN_APPEND;
+ if (access & FILEACCESS_CREATE)
+ flags |= File::OPEN_CREATE;
+ if (access & FILEACCESS_TRUNCATE)
+ flags |= File::OPEN_TRUNCATE;
+
+ int fd = File::OpenFD(fullName, (File::OpenFlag)flags);
+ // Try to detect reads/writes to PSP/GAME to avoid them in replays.
+ if (fullName.FilePathContains("PSP/GAME/")) {
+ inGameDir_ = true;
+ }
+ hFile = fd;
+ if (fd != -1) {
+ // Success
+ return true;
+ } else {
+ // TODO: Need better error codes from OpenFD so we can distinguish
+ // disk full. Just set not found for now.
+ error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
+ return false;
+ }
+ }
+
int flags = 0;
if (access & FILEACCESS_APPEND) {
flags |= O_APPEND;
@@ -623,18 +654,22 @@ int DirectoryFileSystem::OpenFile(std::string filename, FileAccess access, const
err = ReplayApplyDisk(ReplayAction::FILE_OPEN, err, CoreTiming::GetGlobalTimeUs());
if (err != 0) {
+ std::string errorString;
+ int logError;
#ifdef _WIN32
auto win32err = GetLastError();
- ERROR_LOG(FILESYS, "DirectoryFileSystem::OpenFile: FAILED, %i - access = %i, %s", (int)win32err, (int)access, GetStringErrorMsg(win32err).c_str());
+ logError = (int)win32err;
+ errorString = GetStringErrorMsg(win32err);
#else
- ERROR_LOG(FILESYS, "DirectoryFileSystem::OpenFile: FAILED, %i - access = %i", errno, (int)access);
+ logError = (int)errno;
#endif
- //wwwwaaaaahh!!
+ ERROR_LOG(FILESYS, "DirectoryFileSystem::OpenFile(%s): FAILED, %i - access = %d '%s'", filename.c_str(), logError, (int)access, errorString.c_str());
return err;
} else {
#ifdef _WIN32
- if (access & FILEACCESS_APPEND)
- entry.hFile.Seek(0,FILEMOVE_END);
+ if (access & FILEACCESS_APPEND) {
+ entry.hFile.Seek(0, FILEMOVE_END);
+ }
#endif
u32 newHandle = hAlloc->GetNewHandle();
diff --git a/Core/FileSystems/VirtualDiscFileSystem.cpp b/Core/FileSystems/VirtualDiscFileSystem.cpp
index 90bd14b1e7..6cc202e229 100644
--- a/Core/FileSystems/VirtualDiscFileSystem.cpp
+++ b/Core/FileSystems/VirtualDiscFileSystem.cpp
@@ -285,8 +285,7 @@ int VirtualDiscFileSystem::getFileListIndex(std::string &fileName)
#endif
}
- FileType type = File::IsDirectory(fullName) ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;
- if (type == FILETYPE_DIRECTORY)
+ if (File::IsDirectory(fullName))
return -1;
FileListEntry entry = {""};
diff --git a/Core/Font/PGF.cpp b/Core/Font/PGF.cpp
index c1a8802571..20315f4c01 100644
--- a/Core/Font/PGF.cpp
+++ b/Core/Font/PGF.cpp
@@ -230,14 +230,14 @@ bool PGF::ReadPtr(const u8 *ptr, size_t dataSize) {
const u8 *uptr = (const u8 *)wptr;
- if (uptr >= startPtr + dataSize) {
- return false;
- }
-
int shadowCharMapSize = ((header.shadowMapLength * header.shadowMapBpe + 31) & ~31) / 8;
const u8 *shadowCharMap = uptr;
uptr += shadowCharMapSize;
+ if (uptr < startPtr || uptr >= startPtr + dataSize) {
+ return false;
+ }
+
const u16_le *sptr = (const u16_le *)uptr;
if (header.revision == 3) {
charmapCompressionTable1[0].resize(rev3extra.compCharMapLength1);
@@ -257,10 +257,6 @@ bool PGF::ReadPtr(const u8 *ptr, size_t dataSize) {
uptr = (const u8 *)sptr;
- if (uptr >= startPtr + dataSize) {
- return false;
- }
-
int charMapSize = ((header.charMapLength * header.charMapBpe + 31) & ~31) / 8;
const u8 *charMap = uptr;
uptr += charMapSize;
@@ -269,6 +265,10 @@ bool PGF::ReadPtr(const u8 *ptr, size_t dataSize) {
const u8 *charPointerTable = uptr;
uptr += charPointerSize;
+ if (uptr < startPtr || uptr >= startPtr + dataSize) {
+ return false;
+ }
+
// PGF Fontdata.
u32 fontDataOffset = (u32)(uptr - startPtr);
diff --git a/Core/HLE/HLE.h b/Core/HLE/HLE.h
index 7a89618015..4f589aaf23 100644
--- a/Core/HLE/HLE.h
+++ b/Core/HLE/HLE.h
@@ -220,6 +220,7 @@ T hleDoLog(LogTypes::LOG_TYPE t, LogTypes::LOG_LEVELS level, T res, const char *
#define hleLogError(t, res, ...) hleLogHelper(t, LERROR, res, 'x', ##__VA_ARGS__)
#define hleLogWarning(t, res, ...) hleLogHelper(t, LWARNING, res, 'x', ##__VA_ARGS__)
#define hleLogDebug(t, res, ...) hleLogHelper(t, HLE_LOG_LDEBUG, res, 'x', ##__VA_ARGS__)
+#define hleLogVerbose(t, res, ...) hleLogHelper(t, HLE_LOG_LVERBOSE, res, 'x', ##__VA_ARGS__)
#define hleLogSuccessX(t, res, ...) hleLogHelper(t, HLE_LOG_LDEBUG, res, 'x', ##__VA_ARGS__)
#define hleLogSuccessI(t, res, ...) hleLogHelper(t, HLE_LOG_LDEBUG, res, 'i', ##__VA_ARGS__)
#define hleLogSuccessInfoX(t, res, ...) hleLogHelper(t, LINFO, res, 'x', ##__VA_ARGS__)
diff --git a/Core/HLE/sceCtrl.cpp b/Core/HLE/sceCtrl.cpp
index a87cba554f..fe78433792 100644
--- a/Core/HLE/sceCtrl.cpp
+++ b/Core/HLE/sceCtrl.cpp
@@ -188,18 +188,14 @@ void __CtrlButtonUp(u32 buttonBit)
ctrlCurrent.buttons &= ~buttonBit;
}
-void __CtrlSetAnalogX(float x, int stick)
+void __CtrlSetAnalogXY(int stick, float x, float y)
{
- u8 scaled = clamp_u8((int)ceilf(x * 127.5f + 127.5f));
+ u8 scaledX = clamp_u8((int)ceilf(x * 127.5f + 127.5f));
+ // TODO: We might have too many negations of Y...
+ u8 scaledY = clamp_u8((int)ceilf(-y * 127.5f + 127.5f));
std::lock_guard guard(ctrlMutex);
- ctrlCurrent.analog[stick][CTRL_ANALOG_X] = scaled;
-}
-
-void __CtrlSetAnalogY(float y, int stick)
-{
- u8 scaled = clamp_u8((int)ceilf(-y * 127.5f + 127.5f));
- std::lock_guard guard(ctrlMutex);
- ctrlCurrent.analog[stick][CTRL_ANALOG_Y] = scaled;
+ ctrlCurrent.analog[stick][CTRL_ANALOG_X] = scaledX;
+ ctrlCurrent.analog[stick][CTRL_ANALOG_Y] = scaledY;
}
void __CtrlSetRapidFire(bool state)
diff --git a/Core/HLE/sceCtrl.h b/Core/HLE/sceCtrl.h
index 039b4e7404..6a77cd0b9d 100644
--- a/Core/HLE/sceCtrl.h
+++ b/Core/HLE/sceCtrl.h
@@ -72,10 +72,9 @@ void __CtrlButtonDown(u32 buttonBit);
void __CtrlButtonUp(u32 buttonBit);
// Call this to set the position of an analog stick, ideally when it changes.
-// Position value should be from -1 to 1, inclusive, in a square (no need to force to a circle.)
+// X and Y values should be from -1 to 1, inclusive, in a square (no need to force to a circle.)
// No deadzone filtering is done (but note that this applies to the actual PSP as well.)
-void __CtrlSetAnalogX(float value, int stick = CTRL_STICK_LEFT);
-void __CtrlSetAnalogY(float value, int stick = CTRL_STICK_LEFT);
+void __CtrlSetAnalogXY(int stick, float x, float y);
// Call this to enable rapid-fire. This will cause buttons other than arrows to alternate.
void __CtrlSetRapidFire(bool state);
diff --git a/Core/HLE/sceDisplay.cpp b/Core/HLE/sceDisplay.cpp
index 18527fe3f4..053d1e4a7d 100644
--- a/Core/HLE/sceDisplay.cpp
+++ b/Core/HLE/sceDisplay.cpp
@@ -443,7 +443,7 @@ static bool IsRunningSlow() {
best = std::max(fpsHistory[index], best);
}
- return best < System_GetPropertyFloat(SYSPROP_DISPLAY_REFRESH_RATE) * 0.999;
+ return best < System_GetPropertyFloat(SYSPROP_DISPLAY_REFRESH_RATE) * 0.97;
}
return false;
@@ -770,7 +770,10 @@ void __DisplayFlip(int cyclesLate) {
// Let the user know if we're running slow, so they know to adjust settings.
// Sometimes users just think the sound emulation is broken.
static bool hasNotifiedSlow = false;
- if (!g_Config.bHideSlowWarnings && !hasNotifiedSlow && PSP_CoreParameter().fpsLimit == FPSLimit::NORMAL && IsRunningSlow()) {
+ if (!g_Config.bHideSlowWarnings &&
+ !hasNotifiedSlow &&
+ PSP_CoreParameter().fpsLimit == FPSLimit::NORMAL &&
+ IsRunningSlow()) {
#ifndef _DEBUG
auto err = GetI18NCategory("Error");
if (g_Config.bSoftwareRendering) {
diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp
index 2c815c27e5..7ffd58f324 100644
--- a/Core/HLE/sceIo.cpp
+++ b/Core/HLE/sceIo.cpp
@@ -208,9 +208,10 @@ static int asyncDefaultPriority = -1;
class FileNode : public KernelObject {
public:
- FileNode() : callbackID(0), callbackArg(0), asyncResult(0), hasAsyncResult(false), pendingAsyncResult(false), sectorBlockMode(false), closePending(false), npdrm(0), pgdInfo(NULL) {}
+ FileNode() {}
~FileNode() {
- pspFileSystem.CloseFile(handle);
+ if (handle != -1)
+ pspFileSystem.CloseFile(handle);
pgd_close(pgdInfo);
}
const char *GetName() override { return fullpath.c_str(); }
@@ -228,7 +229,7 @@ public:
}
void DoState(PointerWrap &p) override {
- auto s = p.Section("FileNode", 1, 2);
+ auto s = p.Section("FileNode", 1, 3);
if (!s)
return;
@@ -262,35 +263,40 @@ public:
if (s >= 2) {
Do(p, waitingSyncThreads);
}
+ if (s >= 3) {
+ Do(p, isTTY);
+ }
Do(p, pausedWaits);
}
std::string fullpath;
u32 handle;
- u32 callbackID;
- u32 callbackArg;
+ u32 callbackID = 0;
+ u32 callbackArg = 0;
- s64 asyncResult;
- bool hasAsyncResult;
- bool pendingAsyncResult;
+ s64 asyncResult = 0;
+ bool hasAsyncResult = false;
+ bool pendingAsyncResult = false;
- bool sectorBlockMode;
+ bool sectorBlockMode = false;
// TODO: Use an enum instead?
- bool closePending;
+ bool closePending = false;
PSPFileInfo info;
- u32 openMode;
+ u32 openMode = 0;
- u32 npdrm;
- u32 pgd_offset;
- PGD_DESC *pgdInfo;
+ u32 npdrm = 0;
+ u32 pgd_offset = 0;
+ PGD_DESC *pgdInfo = nullptr;
std::vector waitingThreads;
std::vector waitingSyncThreads;
// Key is the callback id it was for, or if no callback, the thread id.
// Value is actually meaningless but kept for consistency with other wait types.
std::map pausedWaits;
+
+ bool isTTY = false;
};
/******************************************************************************/
@@ -1180,6 +1186,14 @@ static bool __IoWrite(int &result, int id, u32 data_addr, int size, int &us) {
const std::string tag = "IoWrite/" + IODetermineFilename(f);
NotifyMemInfo(MemBlockFlags::READ, data_addr, size, tag.c_str(), tag.size());
+ if (f->isTTY) {
+ const char *str = (const char *)data_ptr;
+ const int str_size = size <= 0 ? 0 : (str[validSize - 1] == '\n' ? validSize - 1 : validSize);
+ INFO_LOG(SCEIO, "%s: %.*s", "tty", str_size, str);
+ result = validSize;
+ return true;
+ }
+
bool useThread = __KernelIsDispatchEnabled() && ioManagerThreadEnabled && size > IO_THREAD_MIN_DATA_SIZE;
if (useThread) {
// If there's a pending operation on this file, wait for it to finish and don't overwrite it.
@@ -1284,6 +1298,8 @@ static u32 sceIoGetDevType(int id) {
if (f) {
// TODO: When would this return PSP_DEV_TYPE_ALIAS?
WARN_LOG(SCEIO, "sceIoGetDevType(%d - %s)", id, f->fullpath.c_str());
+ if (f->isTTY)
+ result = (u32)PSPDevType::FILE;
result = (u32)pspFileSystem.DevType(f->handle) & (u32)PSPDevType::EMU_MASK;
} else {
ERROR_LOG(SCEIO, "sceIoGetDevType: unknown id %d", id);
@@ -1468,22 +1484,35 @@ static FileNode *__IoOpen(int &error, const char *filename, int flags, int mode)
if (flags & PSP_O_EXCL)
access |= FILEACCESS_EXCL;
- PSPFileInfo info = pspFileSystem.GetFileInfo(filename);
+ PSPFileInfo info;
+ int h = -1;
+ bool isTTY = false;
+ // TODO: Technically, tty1, etc. too and space doesn't matter.
+ if (startsWithNoCase(filename, "tty0:")) {
+ info.name = filename;
+ info.access = 0777;
+ info.exists = true;
- int h = pspFileSystem.OpenFile(filename, (FileAccess)access);
- if (h < 0) {
- error = h;
- return nullptr;
+ isTTY = true;
+ } else {
+ info = pspFileSystem.GetFileInfo(filename);
+
+ h = pspFileSystem.OpenFile(filename, (FileAccess)access);
+ if (h < 0) {
+ error = h;
+ return nullptr;
+ }
}
error = 0;
FileNode *f = new FileNode();
- SceUID id = kernelObjects.Create(f);
+ kernelObjects.Create(f);
f->handle = h;
f->fullpath = filename;
- f->asyncResult = id;
+ f->asyncResult = h;
f->info = info;
f->openMode = access;
+ f->isTTY = isTTY;
f->npdrm = (flags & PSP_O_NPDRM)? true: false;
f->pgd_offset = 0;
@@ -1527,7 +1556,7 @@ static u32 sceIoOpen(const char *filename, int flags, int mode) {
} else {
asyncParams[id].priority = asyncDefaultPriority;
IFileSystem *sys = pspFileSystem.GetSystemFromFilename(filename);
- if (sys && (sys->DevType(f->handle) & (PSPDevType::BLOCK | PSPDevType::EMU_LBN))) {
+ if (sys && !f->isTTY && (sys->DevType(f->handle) & (PSPDevType::BLOCK | PSPDevType::EMU_LBN))) {
// These are fast to open, no delay or even rescheduling happens.
return hleLogSuccessI(SCEIO, id);
}
@@ -2089,7 +2118,8 @@ static u32 sceIoOpenAsync(const char *filename, int flags, int mode) {
return hleLogError(SCEIO, error, "device not found");
f = new FileNode();
- f->handle = kernelObjects.Create(f);
+ kernelObjects.Create(f);
+ f->handle = -1;
f->fullpath = filename;
f->closePending = true;
}
diff --git a/Core/HLE/sceKernelMemory.cpp b/Core/HLE/sceKernelMemory.cpp
index 634eb43f58..f99dc55978 100644
--- a/Core/HLE/sceKernelMemory.cpp
+++ b/Core/HLE/sceKernelMemory.cpp
@@ -20,6 +20,7 @@
#include
#include
@@ -157,6 +160,9 @@
Screens
+
+ Screens
+
diff --git a/UWP/CommonUWP/CommonUWP.vcxproj b/UWP/CommonUWP/CommonUWP.vcxproj
index c98fee5169..e91ec0d58b 100644
--- a/UWP/CommonUWP/CommonUWP.vcxproj
+++ b/UWP/CommonUWP/CommonUWP.vcxproj
@@ -475,6 +475,7 @@
+
@@ -485,9 +486,11 @@
-
-
+
+
+
+
@@ -583,6 +586,7 @@
+
@@ -594,9 +598,9 @@
-
-
+
+
diff --git a/UWP/CommonUWP/CommonUWP.vcxproj.filters b/UWP/CommonUWP/CommonUWP.vcxproj.filters
index fc0d0a62cb..a72807393e 100644
--- a/UWP/CommonUWP/CommonUWP.vcxproj.filters
+++ b/UWP/CommonUWP/CommonUWP.vcxproj.filters
@@ -100,6 +100,7 @@
+
@@ -179,10 +180,10 @@
Input
-
+
Thread
-
+
Thread
@@ -398,6 +399,7 @@
+
@@ -455,10 +457,16 @@
Input
-
+
Thread
-
+
+ Thread
+
+
+ Thread
+
+
Thread
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj
index f3f026c4d6..dd271d6221 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj
+++ b/UWP/CoreUWP/CoreUWP.vcxproj
@@ -382,6 +382,7 @@
+
@@ -616,6 +617,7 @@
+
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters
index 89b790f3c4..e59d84f7b3 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj.filters
+++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters
@@ -1021,9 +1021,6 @@
Ext\libzip
-
- Ext\libzip
-
Ext\libzip
@@ -1110,6 +1107,7 @@
+
@@ -1748,6 +1746,7 @@
+
diff --git a/UWP/UI_UWP/UI_UWP.vcxproj b/UWP/UI_UWP/UI_UWP.vcxproj
index 23f6ed0ea3..b85cfdce34 100644
--- a/UWP/UI_UWP/UI_UWP.vcxproj
+++ b/UWP/UI_UWP/UI_UWP.vcxproj
@@ -397,6 +397,7 @@
+
@@ -431,6 +432,7 @@
+
diff --git a/UWP/UI_UWP/UI_UWP.vcxproj.filters b/UWP/UI_UWP/UI_UWP.vcxproj.filters
index 8a4208e1ec..bae3799bc9 100644
--- a/UWP/UI_UWP/UI_UWP.vcxproj.filters
+++ b/UWP/UI_UWP/UI_UWP.vcxproj.filters
@@ -18,6 +18,7 @@
+
@@ -53,6 +54,7 @@
+
diff --git a/UWP/XAudioSoundStream.cpp b/UWP/XAudioSoundStream.cpp
index aebc144fb1..028d02c066 100644
--- a/UWP/XAudioSoundStream.cpp
+++ b/UWP/XAudioSoundStream.cpp
@@ -159,7 +159,7 @@ void XAudioBackend::PollLoop() {
// take ownership of the data. It needs to be big enough to fit the max number of buffers we check for
// above, which it is, easily.
- int stereoSamplesRendered = (*callback_)((short*)&realtimeBuffer_[cursor_], readCount / 4, 16, sampleRate_, 2);
+ int stereoSamplesRendered = (*callback_)((short*)&realtimeBuffer_[cursor_], readCount / 4, 16, sampleRate_);
int numBytesRendered = 2 * sizeof(short) * stereoSamplesRendered;
XAUDIO2_BUFFER xaudioBuffer{};
diff --git a/Windows/DSoundStream.cpp b/Windows/DSoundStream.cpp
index ba03f042fb..96265d5eac 100644
--- a/Windows/DSoundStream.cpp
+++ b/Windows/DSoundStream.cpp
@@ -123,7 +123,7 @@ int DSoundAudioBackend::RunThread() {
int numBytesToRender = RoundDown128(ModBufferSize(currentPos_ - lastPos_));
if (numBytesToRender >= 256) {
- int numBytesRendered = 4 * (*callback_)(realtimeBuffer_, numBytesToRender >> 2, 16, 44100, 2);
+ int numBytesRendered = 4 * (*callback_)(realtimeBuffer_, numBytesToRender >> 2, 16, 44100);
//We need to copy the full buffer, regardless of what the mixer claims to have filled
//If we don't do this then the sound will loop if the sound stops and the mixer writes only zeroes
numBytesRendered = numBytesToRender;
diff --git a/Windows/DinputDevice.cpp b/Windows/DinputDevice.cpp
index bc5ab3e6a7..bba15bec56 100644
--- a/Windows/DinputDevice.cpp
+++ b/Windows/DinputDevice.cpp
@@ -176,7 +176,7 @@ DinputDevice::DinputDevice(int devnum) {
dipw.diph.dwHow = DIPH_DEVICE;
dipw.diph.dwObj = 0;
// dwData 10000 is deadzone(0% - 100%), multiply by config scalar
- dipw.dwData = (int)(g_Config.fDInputAnalogDeadzone * 10000);
+ dipw.dwData = 0;
analog |= FAILED(pJoystick->SetProperty(DIPROP_DEADZONE, &dipw.diph)) ? false : true;
}
@@ -209,60 +209,6 @@ void SendNativeAxis(int deviceId, int value, int &lastValue, int axisId) {
lastValue = value;
}
-inline int Signs(int val) {
- return (0 < val) - (val < 0);
-}
-
-inline int LinearMaps(int val, int a0, int a1, int b0, int b1) {
- return b0 + (((val - a0) * (b1 - b0)) / (a1 - a0));
-}
-inline float LinearMaps(float val, float a0, float a1, float b0, float b1) {
- return b0 + (((val - a0) * (b1 - b0)) / (a1 - a0));
-}
-
-static void ApplyNormalization(LONG &jsx, LONG &jsy) {
- // Circle to Square mapping, cribbed from XInputDevice
- float sx = (float)jsx;
- float sy = (float)jsy;
- float scaleFactor = sqrtf((sx * sx + sy * sy) / std::max(sx * sx, sy * sy));
- jsx = (int)(sx * scaleFactor);
- jsy = (int)(sy * scaleFactor);
-
- // Linear range mapping (used to invert deadzones)
- float dz = g_Config.fDInputAnalogDeadzone;
- int idzm = g_Config.iDInputAnalogInverseMode;
- float idz = g_Config.fDInputAnalogInverseDeadzone;
- float md = std::max(dz, idz);
- float st = g_Config.fDInputAnalogSensitivity;
-
- float magnitude = sqrtf((float)(jsx * jsx + jsy * jsy));
- if (magnitude > dz * 10000.0f) {
- if (idzm == 1) {
- int xSign = Signs(jsx);
- if (xSign != 0) {
- jsx = LinearMaps(jsx, xSign * (int)(dz * 10000), xSign * 10000, xSign * (int)(md * 10000), xSign * (int)(st * 10000));
- }
- } else if (idzm == 2) {
- int ySign = Signs(jsy);
- if (ySign != 0) {
- jsy = LinearMaps(jsy, ySign * (int)(dz * 10000.0f), ySign * 10000, ySign * (int)(md * 10000.0f), ySign * (int)(st * 10000));
- }
- } else if (idzm == 3) {
- float xNorm = (float)jsx / magnitude;
- float yNorm = (float)jsy / magnitude;
- float mapMag = LinearMaps(magnitude, dz * 10000.0f, 10000.0f, md * 10000.0f, 10000.0f * st);
- jsx = (short)(xNorm * mapMag);
- jsy = (short)(yNorm * mapMag);
- }
- } else {
- jsx = 0;
- jsy = 0;
- }
-
- jsx = (short)std::min(10000.0f, std::max((float)jsx, -10000.0f));
- jsy = (short)std::min(10000.0f, std::max((float)jsy, -10000.0f));
-}
-
static LONG *ValueForAxisId(DIJOYSTATE2 &js, int axisId) {
switch (axisId) {
case JOYSTICK_AXIS_X: return &js.lX;
@@ -275,14 +221,6 @@ static LONG *ValueForAxisId(DIJOYSTATE2 &js, int axisId) {
}
}
-static void ApplyNormalization(DIJOYSTATE2 &js, int xAxisId, int yAxisId) {
- LONG *nrmX = ValueForAxisId(js, xAxisId);
- LONG *nrmY = ValueForAxisId(js, yAxisId);
- if (nrmX != nullptr && nrmY != nullptr) {
- ApplyNormalization(*nrmX, *nrmY);
- }
-}
-
int DinputDevice::UpdateState() {
if (!pJoystick) return -1;
@@ -303,10 +241,6 @@ int DinputDevice::UpdateState() {
axis.deviceId = DEVICE_ID_PAD_0 + pDevNum;
auto axesToSquare = KeyMap::MappedAxesForDevice(axis.deviceId);
- ApplyNormalization(js, axesToSquare.leftX.axisId, axesToSquare.leftY.axisId);
- // Prevent double normalization.
- if (axesToSquare.leftX.axisId != axesToSquare.rightX.axisId && axesToSquare.leftX.axisId != axesToSquare.rightY.axisId)
- ApplyNormalization(js, axesToSquare.rightX.axisId, axesToSquare.rightY.axisId);
SendNativeAxis(DEVICE_ID_PAD_0 + pDevNum, js.lX, last_lX_, JOYSTICK_AXIS_X);
SendNativeAxis(DEVICE_ID_PAD_0 + pDevNum, js.lY, last_lY_, JOYSTICK_AXIS_Y);
diff --git a/Windows/PPSSPP.vcxproj b/Windows/PPSSPP.vcxproj
index e3b348add4..d9b881f0f5 100644
--- a/Windows/PPSSPP.vcxproj
+++ b/Windows/PPSSPP.vcxproj
@@ -1006,6 +1006,7 @@
true
true
+
true
true
diff --git a/Windows/PPSSPP.vcxproj.filters b/Windows/PPSSPP.vcxproj.filters
index 141d03a3ca..537972bafa 100644
--- a/Windows/PPSSPP.vcxproj.filters
+++ b/Windows/PPSSPP.vcxproj.filters
@@ -538,6 +538,9 @@
Other Platforms\Android
+
+ Other Platforms\Android
+
diff --git a/Windows/WASAPIStream.cpp b/Windows/WASAPIStream.cpp
index e8f7fe79f7..3a7b6565b2 100644
--- a/Windows/WASAPIStream.cpp
+++ b/Windows/WASAPIStream.cpp
@@ -5,6 +5,7 @@
#include "Common/Log.h"
#include "Core/Reporting.h"
#include "Core/Util/AudioFormat.h"
+#include "Common/Data/Encoding/Utf8.h"
#include "Common/Thread/ThreadUtil.h"
@@ -50,6 +51,11 @@ public:
if (!device || FAILED(device->GetId(¤tDevice_))) {
currentDevice_ = nullptr;
}
+
+ if (currentDevice_) {
+ INFO_LOG(SCEAUDIO, "Switching to WASAPI audio device: '%s'", GetDeviceName(currentDevice_).c_str());
+ }
+
deviceChanged_ = false;
}
@@ -89,7 +95,7 @@ public:
HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) override {
std::lock_guard guard(lock_);
- if (flow != eRender || role != eMultimedia) {
+ if (flow != eRender || role != eConsole) {
// Not relevant to us.
return S_OK;
}
@@ -105,7 +111,7 @@ public:
}
deviceChanged_ = true;
- INFO_LOG(SCEAUDIO, "New default WASAPI audio device detected");
+ INFO_LOG(SCEAUDIO, "New default eRender/eConsole WASAPI audio device detected: '%s'", GetDeviceName(pwstrDeviceId).c_str());
return S_OK;
}
@@ -136,6 +142,45 @@ public:
return S_OK;
}
+ std::string GetDeviceName(LPCWSTR pwstrId)
+ {
+ HRESULT hr = S_OK;
+ IMMDevice *pDevice = NULL;
+ IPropertyStore *pProps = NULL;
+ PROPVARIANT varString;
+ PropVariantInit(&varString);
+
+ if (_pEnumerator == NULL)
+ {
+ // Get enumerator for audio endpoint devices.
+ hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
+ NULL, CLSCTX_INPROC_SERVER,
+ __uuidof(IMMDeviceEnumerator),
+ (void**)&_pEnumerator);
+ }
+ if (hr == S_OK)
+ {
+ hr = _pEnumerator->GetDevice(pwstrId, &pDevice);
+ }
+ if (hr == S_OK)
+ {
+ hr = pDevice->OpenPropertyStore(STGM_READ, &pProps);
+ }
+ if (hr == S_OK)
+ {
+ // Get the endpoint device's friendly-name property.
+ hr = pProps->GetValue(PKEY_Device_FriendlyName, &varString);
+ }
+
+ std::string name = ConvertWStringToUTF8((hr == S_OK) ? varString.pwszVal : L"null device");
+
+ PropVariantClear(&varString);
+
+ SAFE_RELEASE(pProps)
+ SAFE_RELEASE(pDevice)
+ return name;
+ }
+
private:
std::mutex lock_;
LONG _cRef = 1;
@@ -236,7 +281,7 @@ WASAPIAudioThread::~WASAPIAudioThread() {
bool WASAPIAudioThread::ActivateDefaultDevice() {
_assert_(device_ == nullptr);
- HRESULT hresult = deviceEnumerator_->GetDefaultAudioEndpoint(eRender, eMultimedia, &device_);
+ HRESULT hresult = deviceEnumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device_);
if (FAILED(hresult) || device_ == nullptr)
return false;
@@ -341,7 +386,7 @@ bool WASAPIAudioThread::ValidateFormat(const WAVEFORMATEXTENSIBLE *fmt) {
if (fmt->Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
if (!memcmp(&fmt->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, sizeof(fmt->SubFormat))) {
- if (fmt->Format.nChannels >= 2)
+ if (fmt->Format.nChannels >= 1)
format_ = Format::IEEE_FLOAT;
} else {
ERROR_LOG_REPORT_ONCE(unexpectedformat, SCEAUDIO, "Got unexpected WASAPI 0xFFFE stream format, expected float!");
@@ -350,7 +395,7 @@ bool WASAPIAudioThread::ValidateFormat(const WAVEFORMATEXTENSIBLE *fmt) {
}
}
} else if (fmt->Format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
- if (fmt->Format.nChannels >= 2)
+ if (fmt->Format.nChannels >= 1)
format_ = Format::IEEE_FLOAT;
} else {
ERROR_LOG_REPORT_ONCE(unexpectedformat2, SCEAUDIO, "Got unexpected non-extensible WASAPI stream format, expected extensible float!");
@@ -374,7 +419,8 @@ bool WASAPIAudioThread::PrepareFormat() {
const int numSamples = numBufferFrames * deviceFormat_->Format.nChannels;
if (format_ == Format::IEEE_FLOAT) {
memset(pData, 0, sizeof(float) * numSamples);
- shortBuf_ = new short[numSamples];
+ // This buffer is always stereo - PPSSPP writes to it.
+ shortBuf_ = new short[numBufferFrames * 2];
} else if (format_ == Format::PCM16) {
memset(pData, 0, sizeof(short) * numSamples);
}
@@ -394,6 +440,7 @@ void WASAPIAudioThread::Run() {
HRESULT hresult = CoCreateInstance(CLSID_MMDeviceEnumerator,
nullptr, /* Object is not created as the part of the aggregate */
CLSCTX_ALL, IID_IMMDeviceEnumerator, (void **)&deviceEnumerator_);
+
if (FAILED(hresult) || deviceEnumerator_ == nullptr)
return;
@@ -446,8 +493,14 @@ void WASAPIAudioThread::Run() {
int chans = deviceFormat_->Format.nChannels;
switch (format_) {
case Format::IEEE_FLOAT:
- callback_(shortBuf_, pNumAvFrames, 16, sampleRate_, chans);
- if (chans == 2) {
+ callback_(shortBuf_, pNumAvFrames, 16, sampleRate_);
+ if (chans == 1) {
+ float *ptr = (float *)pData;
+ memset(ptr, 0, pNumAvFrames * chans * sizeof(float));
+ for (UINT32 i = 0; i < pNumAvFrames; i++) {
+ ptr[i * chans + 0] = 0.5f * ((float)shortBuf_[i * 2] + (float)shortBuf_[i * 2 + 1]) * (1.0f / 32768.0f);
+ }
+ } else if (chans == 2) {
ConvertS16ToF32((float *)pData, shortBuf_, pNumAvFrames * chans);
} else if (chans > 2) {
float *ptr = (float *)pData;
@@ -459,7 +512,7 @@ void WASAPIAudioThread::Run() {
}
break;
case Format::PCM16:
- callback_((short *)pData, pNumAvFrames, 16, sampleRate_, chans);
+ callback_((short *)pData, pNumAvFrames, 16, sampleRate_);
break;
}
}
@@ -482,6 +535,7 @@ void WASAPIAudioThread::Run() {
if (!ActivateDefaultDevice()) {
ERROR_LOG(SCEAUDIO, "WASAPI: Could not activate default device");
+ // TODO: Return to the old device here?
return;
}
notificationClient_->SetCurrentDevice(device_);
diff --git a/Windows/WindowsAudio.h b/Windows/WindowsAudio.h
index 2e9d0f730e..9adc196c01 100644
--- a/Windows/WindowsAudio.h
+++ b/Windows/WindowsAudio.h
@@ -4,7 +4,8 @@
#include "Core/Config.h"
#include "Core/ConfigValues.h"
-typedef int(*StreamCallback)(short *buffer, int numSamples, int bits, int rate, int channels);
+// Always 2 channels.
+typedef int(*StreamCallback)(short *buffer, int numSamples, int bits, int rate);
// Note that the backend may override the passed in sample rate. The actual sample rate
// should be returned by GetSampleRate though.
diff --git a/Windows/XinputDevice.cpp b/Windows/XinputDevice.cpp
index 4edbcf21a1..48b42193ff 100644
--- a/Windows/XinputDevice.cpp
+++ b/Windows/XinputDevice.cpp
@@ -151,88 +151,6 @@ struct Stick {
float y;
};
-inline float Clampf(float val, float min, float max) {
- if (val < min) return min;
- if (val > max) return max;
- return val;
-}
-
-inline float Signf(float val) {
- return (float)((0.0f < val) - (val < 0.0f));
-}
-
-inline float LinearMapf(float val, float a0, float a1, float b0, float b1) {
- return b0 + (((val - a0) * (b1 - b0)) / (a1 - a0));
-}
-
-static Stick NormalizedDeadzoneFilter(short x, short y, float dz, int idzm, float idz, float st) {
- Stick s(x, y, 1.0f / 32767.0f);
-
- float magnitude = sqrtf(s.x * s.x + s.y * s.y);
- if (magnitude > dz) {
-
- // Circle to square mapping (the PSP stick outputs the full -1..1 square of values)
-#if 1
- // Looks way better than the old one, below, in the axis tester.
- float sx = s.x;
- float sy = s.y;
- float scaleFactor = sqrtf((sx * sx + sy * sy) / std::max(sx * sx, sy * sy));
- s.x = sx * scaleFactor;
- s.y = sy * scaleFactor;
-#else
- if (magnitude > 1.0f) {
- s.x *= 1.41421f;
- s.y *= 1.41421f;
- }
-#endif
-
- // Linear range mapping (used to invert deadzones)
- float md = std::max(dz, idz);
-
- if (idzm == 1)
- {
- float xSign = Signf(s.x);
- if (xSign != 0.0f) {
- s.x = LinearMapf(s.x, xSign * dz, xSign, xSign * md, xSign * st);
- }
- }
- else if (idzm == 2)
- {
- float ySign = Signf(s.y);
- if (ySign != 0.0f) {
- s.y = LinearMapf(s.y, ySign * dz, ySign, ySign * md, ySign * st);
- }
- }
- else if (idzm == 3)
- {
- float xNorm = s.x / magnitude;
- float yNorm = s.y / magnitude;
- float mapMag = LinearMapf(magnitude, dz, 1.0f, md, st);
- s.x = xNorm * mapMag;
- s.y = yNorm * mapMag;
- }
-
- s.x = Clampf(s.x, -1.0f, 1.0f);
- s.y = Clampf(s.y, -1.0f, 1.0f);
- } else {
- s.x = 0.0f;
- s.y = 0.0f;
- }
- return s;
-}
-
-bool NormalizedDeadzoneDiffers(short x1, short y1, short x2, short y2, const float dz) {
- Stick s1(x1, y1, 1.0f / 32767.0f);
- Stick s2(x2, y2, 1.0f / 32767.0f);
-
- float magnitude1 = sqrtf(s1.x * s1.x + s1.y * s1.y);
- float magnitude2 = sqrtf(s2.x * s2.x + s2.y * s2.y);
- if (magnitude1 > dz || magnitude2 > dz) {
- return x1 != x2 || y1 != y2;
- }
- return false;
-}
-
bool NormalizedDeadzoneDiffers(u8 x1, u8 x2, const u8 thresh) {
if (x1 > thresh || x2 > thresh) {
return x1 != x2;
@@ -277,11 +195,6 @@ void XinputDevice::UpdatePad(int pad, const XINPUT_STATE &state, XINPUT_VIBRATIO
ApplyButtons(pad, state);
ApplyVibration(pad, vibration);
- const float STICK_DEADZONE = g_Config.fXInputAnalogDeadzone;
- const int STICK_INV_MODE = g_Config.iXInputAnalogInverseMode;
- const float STICK_INV_DEADZONE = g_Config.fXInputAnalogInverseDeadzone;
- const float STICK_SENSITIVITY = g_Config.fXInputAnalogSensitivity;
-
AxisInput axis;
axis.deviceId = DEVICE_ID_X360_0 + pad;
auto sendAxis = [&](AndroidJoystickAxis axisId, float value) {
@@ -290,19 +203,10 @@ void XinputDevice::UpdatePad(int pad, const XINPUT_STATE &state, XINPUT_VIBRATIO
NativeAxis(axis);
};
- if (NormalizedDeadzoneDiffers(prevState[pad].Gamepad.sThumbLX, prevState[pad].Gamepad.sThumbLY, state.Gamepad.sThumbLX, state.Gamepad.sThumbLY, STICK_DEADZONE)) {
- Stick left = NormalizedDeadzoneFilter(state.Gamepad.sThumbLX, state.Gamepad.sThumbLY, STICK_DEADZONE, STICK_INV_MODE, STICK_INV_DEADZONE, STICK_SENSITIVITY);
-
- sendAxis(JOYSTICK_AXIS_X, left.x);
- sendAxis(JOYSTICK_AXIS_Y, left.y);
- }
-
- if (NormalizedDeadzoneDiffers(prevState[pad].Gamepad.sThumbRX, prevState[pad].Gamepad.sThumbRY, state.Gamepad.sThumbRX, state.Gamepad.sThumbRY, STICK_DEADZONE)) {
- Stick right = NormalizedDeadzoneFilter(state.Gamepad.sThumbRX, state.Gamepad.sThumbRY, STICK_DEADZONE, STICK_INV_MODE, STICK_INV_DEADZONE, STICK_SENSITIVITY);
-
- sendAxis(JOYSTICK_AXIS_Z, right.x);
- sendAxis(JOYSTICK_AXIS_RZ, right.y);
- }
+ sendAxis(JOYSTICK_AXIS_X, (float)state.Gamepad.sThumbLX / 32767.0f);
+ sendAxis(JOYSTICK_AXIS_Y, (float)state.Gamepad.sThumbLY / 32767.0f);
+ sendAxis(JOYSTICK_AXIS_Z, (float)state.Gamepad.sThumbRX / 32767.0f);
+ sendAxis(JOYSTICK_AXIS_RZ, (float)state.Gamepad.sThumbRY / 32767.0f);
if (NormalizedDeadzoneDiffers(prevState[pad].Gamepad.bLeftTrigger, state.Gamepad.bLeftTrigger, XINPUT_GAMEPAD_TRIGGER_THRESHOLD)) {
sendAxis(JOYSTICK_AXIS_LTRIGGER, (float)state.Gamepad.bLeftTrigger / 255.0f);
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index e4ce3bb29f..002a779cc4 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -30,7 +30,7 @@
-
+
lock(g_errorMutex);
+ g_error = error;
}
struct AndroidAudioState {
@@ -132,3 +141,11 @@ bool AndroidAudio_Shutdown(AndroidAudioState *state) {
INFO_LOG(AUDIO, "OpenSLWrap completely unloaded.");
return true;
}
+
+const std::string AndroidAudio_GetErrorString(AndroidAudioState *state) {
+ if (!state) {
+ return "No state";
+ }
+ std::unique_lock lock(g_errorMutex);
+ return g_error;
+}
diff --git a/android/jni/AndroidAudio.h b/android/jni/AndroidAudio.h
index 3a8fe5e33c..45fb7f4c8f 100644
--- a/android/jni/AndroidAudio.h
+++ b/android/jni/AndroidAudio.h
@@ -1,6 +1,7 @@
#pragma once
#include
+#include
typedef int (*AndroidAudioCallback)(short *buffer, int num_samples);
@@ -8,19 +9,24 @@ class AudioContext {
public:
AudioContext(AndroidAudioCallback cb, int _FramesPerBuffer, int _SampleRate);
virtual bool Init() { return false; }
- virtual int AudioRecord_Start(int sampleRate) { return false; };
- virtual int AudioRecord_Stop() { return false; };
+ virtual bool AudioRecord_Start(int sampleRate) { return false; };
+ virtual bool AudioRecord_Stop() { return false; };
+
virtual ~AudioContext() {}
protected:
+ void SetErrorString(const std::string &error);
AndroidAudioCallback audioCallback;
int framesPerBuffer;
int sampleRate;
+ std::mutex errorMutex_;
};
struct AndroidAudioState;
+// TODO: Get rid of this unnecessary wrapper layer from the old .so days
+
// It's okay for optimalFramesPerBuffer and optimalSampleRate to be 0. Defaults will be used.
AndroidAudioState *AndroidAudio_Init(AndroidAudioCallback cb, int optimalFramesPerBuffer, int optimalSampleRate);
bool AndroidAudio_Recording_SetSampleRate(AndroidAudioState *state, int sampleRate);
@@ -30,3 +36,4 @@ bool AndroidAudio_Recording_State(AndroidAudioState *state);
bool AndroidAudio_Pause(AndroidAudioState *state);
bool AndroidAudio_Resume(AndroidAudioState *state);
bool AndroidAudio_Shutdown(AndroidAudioState *state);
+const std::string AndroidAudio_GetErrorString(AndroidAudioState *state);
diff --git a/android/jni/AndroidContentURI.h b/android/jni/AndroidContentURI.h
new file mode 100644
index 0000000000..f9837a5ba4
--- /dev/null
+++ b/android/jni/AndroidContentURI.h
@@ -0,0 +1,202 @@
+#pragma once
+
+#include
+
+#include "Common/StringUtils.h"
+#include "Common/Net/URL.h"
+
+// Utility to deal with Android storage URIs of the forms:
+// content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO
+// content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO/document/primary%3APSP%20ISO
+
+// This file compiles on all platforms, to reduce the need for ifdefs.
+
+// I am not 100% sure it's OK to rely on the internal format of file content URIs.
+// On the other hand, I'm sure tons of apps would break if these changed, so I think we can
+// consider them pretty stable. Additionally, the official Document library just manipulates the URIs
+// in similar ways...
+class AndroidContentURI {
+private:
+ std::string provider;
+ std::string root;
+ std::string file;
+public:
+ AndroidContentURI() {}
+ explicit AndroidContentURI(const std::string &path) {
+ Parse(path);
+ }
+
+ bool Parse(const std::string &path) {
+ const char *prefix = "content://";
+ if (!startsWith(path, prefix)) {
+ return false;
+ }
+
+ std::string components = path.substr(strlen(prefix));
+
+ std::vector parts;
+ SplitString(components, '/', parts);
+ if (parts.size() == 3) {
+ provider = parts[0];
+ if (parts[1] != "tree") {
+ return false;
+ }
+ root = UriDecode(parts[2]);
+ return true;
+ } else if (parts.size() == 5) {
+ provider = parts[0];
+ if (parts[1] != "tree") {
+ return false;
+ }
+ root = UriDecode(parts[2]);
+ if (parts[3] != "document") {
+ return false;
+ }
+ file = UriDecode(parts[4]);
+ // Sanity check file path.
+ return startsWith(file, root);
+ } else {
+ // Invalid Content URI
+ return false;
+ }
+ }
+
+ AndroidContentURI WithRootFilePath(const std::string &filePath) {
+ AndroidContentURI uri = *this;
+ uri.file = uri.root;
+ if (!filePath.empty()) {
+ uri.file += "/" + filePath;
+ }
+ return uri;
+ }
+
+ AndroidContentURI WithComponent(const std::string &filePath) {
+ AndroidContentURI uri = *this;
+ uri.file = uri.file + "/" + filePath;
+ return uri;
+ }
+
+ AndroidContentURI WithExtraExtension(const std::string &extension) {
+ AndroidContentURI uri = *this;
+ uri.file = uri.file + extension;
+ return uri;
+ }
+
+ AndroidContentURI WithReplacedExtension(const std::string &oldExtension, const std::string &newExtension) const {
+ _dbg_assert_(!oldExtension.empty() && oldExtension[0] == '.');
+ _dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
+ AndroidContentURI uri = *this;
+ if (endsWithNoCase(file, oldExtension)) {
+ uri.file = file.substr(0, file.size() - oldExtension.size()) + newExtension;
+ }
+ return uri;
+ }
+
+ AndroidContentURI WithReplacedExtension(const std::string &newExtension) const {
+ _dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
+ AndroidContentURI uri = *this;
+ if (file.empty()) {
+ return uri;
+ }
+ std::string extension = GetFileExtension();
+ uri.file = file.substr(0, file.size() - extension.size()) + newExtension;
+ return uri;
+ }
+
+ bool IsTreeURI() const {
+ return file.empty();
+ }
+
+ bool CanNavigateUp() const {
+ return file.size() > root.size();
+ }
+
+ // Only goes downwards in hierarchies. No ".." will ever be generated.
+ std::string PathTo(const AndroidContentURI &other) const {
+ return other.FilePath().substr(FilePath().size() + 1);
+ }
+
+ std::string GetFileExtension() const {
+ size_t pos = file.rfind(".");
+ if (pos == std::string::npos) {
+ return "";
+ }
+ size_t slash_pos = file.rfind("/");
+ if (slash_pos != std::string::npos && slash_pos > pos) {
+ // Don't want to detect "df/file" from "/as.df/file"
+ return "";
+ }
+ std::string ext = file.substr(pos);
+ for (size_t i = 0; i < ext.size(); i++) {
+ ext[i] = tolower(ext[i]);
+ }
+ return ext;
+ }
+
+ std::string GetLastPart() const {
+ if (IsTreeURI()) {
+ // Can't do anything anyway.
+ return std::string();
+ }
+
+ if (!CanNavigateUp()) {
+ size_t colon = file.rfind(':');
+ if (colon == std::string::npos) {
+ return std::string();
+ }
+ return file.substr(colon + 1);
+ }
+
+ size_t slash = file.rfind('/');
+ if (slash == std::string::npos) {
+ return std::string();
+ }
+
+ std::string part = file.substr(slash + 1);
+ return part;
+ }
+
+ bool NavigateUp() {
+ if (!CanNavigateUp()) {
+ return false;
+ }
+
+ size_t slash = file.rfind('/');
+ if (slash == std::string::npos) {
+ return false;
+ }
+
+ file = file.substr(0, slash);
+ return true;
+ }
+
+ bool TreeContains(const AndroidContentURI &fileURI) {
+ if (!IsTreeURI()) {
+ return false;
+ }
+ return startsWith(fileURI.file, root);
+ }
+
+ std::string ToString() const {
+ if (file.empty()) {
+ // Tree URI
+ return StringFromFormat("content://%s/tree/%s", provider.c_str(), UriEncode(root).c_str());
+ } else {
+ // File URI
+ return StringFromFormat("content://%s/tree/%s/document/%s", provider.c_str(), UriEncode(root).c_str(), UriEncode(file).c_str());
+ }
+ }
+
+ // Never store the output of this, only show it to the user.
+ std::string ToVisualString() const {
+ return file;
+ }
+
+ const std::string &FilePath() const {
+ return file;
+ }
+
+ const std::string &RootPath() const {
+ return root;
+ }
+};
diff --git a/android/jni/OpenSLContext.cpp b/android/jni/OpenSLContext.cpp
index 47841567e0..ce568536d7 100644
--- a/android/jni/OpenSLContext.cpp
+++ b/android/jni/OpenSLContext.cpp
@@ -10,6 +10,7 @@
#include
#include "Common/Log.h"
+#include "Common/StringUtils.h"
#include "OpenSLContext.h"
#include "Core/HLE/sceUsbMic.h"
@@ -19,18 +20,14 @@ void OpenSLContext::bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *c
SLuint32 recordsState;
result = (*ctx->recorderRecord)->GetRecordState(ctx->recorderRecord, &recordsState);
- if(result != SL_RESULT_SUCCESS) {
- ERROR_LOG(AUDIO, "GetRecordState error: %d", result);
+ if (!CheckResultStatic(result, "GetRecordState error: %d"))
return;
- }
Microphone::addAudioData((uint8_t*) ctx->recordBuffer[ctx->activeRecordBuffer], ctx->recordBufferSize);
if (recordsState == SL_RECORDSTATE_RECORDING) {
result = (*ctx->recorderBufferQueue)->Enqueue(ctx->recorderBufferQueue, ctx->recordBuffer[ctx->activeRecordBuffer], ctx->recordBufferSize);
- if (result != SL_RESULT_SUCCESS) {
- ERROR_LOG(AUDIO, "Enqueue error: %d", result);
- }
+ CheckResultStatic(result, "Enqueue error");
}
ctx->activeRecordBuffer += 1; // Switch buffer
@@ -61,7 +58,7 @@ void OpenSLContext::BqPlayerCallback(SLAndroidSimpleBufferQueueItf bq) {
memset(buffer[curBuffer] + renderedFrames * 2, 0, byteCount);
}
SLresult result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, buffer[curBuffer], sizeInBytes);
-
+ CheckResult(result, StringFromFormat("Failed to enqueue: %d %d", renderedFrames, sizeInBytes).c_str());
// Comment from sample code:
// the most likely other result is SL_RESULT_BUFFER_INSUFFICIENT,
// which for this code example would indicate a programming error
@@ -82,27 +79,30 @@ bool OpenSLContext::Init() {
SLresult result;
// create engine
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
- if (result != SL_RESULT_SUCCESS) {
- ERROR_LOG(AUDIO, "OpenSL: Failed to create the engine: %d", (int)result);
+ if (!CheckResult(result, "slCreateEngine")) {
engineObject = nullptr;
return false;
}
+
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
- _assert_(SL_RESULT_SUCCESS == result);
+ if (!CheckResult(result, "engine->Realize"))
+ return false;
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
- _assert_(SL_RESULT_SUCCESS == result);
+ if (!CheckResult(result, "engine->GetInterface(ENGINE)"))
+ return false;
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, 0, 0);
- if (result != SL_RESULT_SUCCESS) {
- ERROR_LOG(AUDIO, "OpenSL: Failed to create output mix: %d", (int)result);
+ if (!CheckResult(result, "engine->CreateOutputMix(ENGINE)")) {
(*engineObject)->Destroy(engineObject);
engineEngine = nullptr;
engineObject = nullptr;
return false;
}
+
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
- _assert_(SL_RESULT_SUCCESS == result);
+ if (!CheckResult(result, "outputMix->Realize"))
+ return false;
// The constants, such as SL_SAMPLINGRATE_44_1, are just 44100000.
SLuint32 sr = (SLuint32)sampleRate * 1000;
@@ -142,18 +142,23 @@ bool OpenSLContext::Init() {
}
result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
- _assert_(SL_RESULT_SUCCESS == result);
+ if (!CheckResult(result, "player->Realize"))
+ return false; // TODO: Release stuff!
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
- _assert_(SL_RESULT_SUCCESS == result);
- result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
- &bqPlayerBufferQueue);
- _assert_(SL_RESULT_SUCCESS == result);
+ if (!CheckResult(result, "player->GetInterface(PLAY)"))
+ return false; // TODO: Release stuff!
+ result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue);
+ if (!CheckResult(result, "player->GetInterface(BUFFER_QUEUE)"))
+ return false; // TODO: Release stuff!
result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, &bqPlayerCallbackWrap, this);
- _assert_(SL_RESULT_SUCCESS == result);
+ if (!CheckResult(result, "playerbq->RegisterCallback()"))
+ return false; // TODO: Release stuff!
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);
- _assert_(SL_RESULT_SUCCESS == result);
+ if (!CheckResult(result, "playerbq->GetInterface()"))
+ return false; // TODO: Release stuff!
result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
- _assert_(SL_RESULT_SUCCESS == result);
+ if (!CheckResult(result, "playerbq->SetPlayState(PLAYING)"))
+ return false; // TODO: Release stuff!
// Allocate and enqueue N empty buffers.
for (int i = 0; i < NUM_BUFFERS; i++) {
@@ -172,9 +177,14 @@ bool OpenSLContext::Init() {
return true;
}
-int OpenSLContext::AudioRecord_Start(int sampleRate) {
+bool OpenSLContext::AudioRecord_Start(int sampleRate) {
SLresult result;
+ if (!engineEngine) {
+ SetErrorString("AudioRecord_Start: No engine");
+ return false;
+ }
+
// configure audio source
SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};
SLDataSource audioSrc = {&loc_dev, NULL};
@@ -197,38 +207,29 @@ int OpenSLContext::AudioRecord_Start(int sampleRate) {
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc, &audioSnk,
sizeof(id)/sizeof(id[0]), id, req);
- if (SL_RESULT_SUCCESS != result) {
- ERROR_LOG(AUDIO, "CreateAudioRecorder failed: %d", result);
+ if (!CheckResult(result, "CreateAudioRecorder failed"))
return false;
- }
// realize the audio recorder
result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
- if (SL_RESULT_SUCCESS != result) {
- ERROR_LOG(AUDIO, "Realize failed: %d", result);
+ if (!CheckResult(result, "recorderObject->Realize failed"))
return false;
- }
+
// get the record interface
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
- if (SL_RESULT_SUCCESS != result) {
- ERROR_LOG(AUDIO, "GetInterface(record) failed: %d", result);
+ if (!CheckResult(result, "GetInterface(recorderObject) failed"))
return false;
- }
// get the buffer queue interface
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, (void*) &recorderBufferQueue);
- if (SL_RESULT_SUCCESS != result) {
- ERROR_LOG(AUDIO, "GetInterface(queue interface) failed: %d", result);
+ if (!CheckResult(result, "GetInterface(queue interface) failed"))
return false;
- }
// register callback on the buffer queue
result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, &bqRecorderCallback, this);
- if (SL_RESULT_SUCCESS != result) {
- ERROR_LOG(AUDIO, "RegisterCallback failed: %d", result);
+ if (!CheckResult(result, "RegisterCallback failed"))
return false;
- }
recordBufferSize = (44100 * 20 / 1000 * 2);
for (int i = 0; i < NUM_BUFFERS; i++) {
@@ -236,22 +237,18 @@ int OpenSLContext::AudioRecord_Start(int sampleRate) {
}
for (int i = 0; i < NUM_BUFFERS; i++) {
result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer[i], recordBufferSize);
- if (SL_RESULT_SUCCESS != result) {
- ERROR_LOG(AUDIO, "Enqueue failed: %d", result);
+ if (!CheckResult(result, "Enqueue failed"))
return false;
- }
}
result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
- _assert_(SL_RESULT_SUCCESS == result);
-
- return true;
+ return CheckResult(result, "SetRecordState(recording) failed");
}
-int OpenSLContext::AudioRecord_Stop() {
+bool OpenSLContext::AudioRecord_Stop() {
if (recorderRecord != nullptr) {
SLresult result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
- _assert_(SL_RESULT_SUCCESS == result);
+ CheckResult(result, "SetRecordState(stopped) failed");
}
if (recorderObject != nullptr) {
(*recorderObject)->Destroy(recorderObject);
@@ -274,9 +271,7 @@ OpenSLContext::~OpenSLContext() {
INFO_LOG(AUDIO, "OpenSL: Shutdown - stopping playback");
SLresult result;
result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED);
- if (SL_RESULT_SUCCESS != result) {
- ERROR_LOG(AUDIO, "SetPlayState failed");
- }
+ CheckResult(result, "SetPlayState(stopped) failed");
}
INFO_LOG(AUDIO, "OpenSL: Shutdown - deleting player object");
@@ -311,3 +306,22 @@ OpenSLContext::~OpenSLContext() {
}
INFO_LOG(AUDIO, "OpenSL: Shutdown - finished");
}
+
+bool OpenSLContext::CheckResult(SLresult result, const char *str) {
+ if (result != SL_RESULT_SUCCESS) {
+ ERROR_LOG(AUDIO, "OpenSL failure (%s): %d", str, result);
+ SetErrorString(str);
+ return false;
+ } else {
+ return true;
+ }
+}
+
+bool OpenSLContext::CheckResultStatic(SLresult result, const char *str) {
+ if (result != SL_RESULT_SUCCESS) {
+ ERROR_LOG(AUDIO, "OpenSL failure (%s): %d", str, result);
+ return false;
+ } else {
+ return true;
+ }
+}
diff --git a/android/jni/OpenSLContext.h b/android/jni/OpenSLContext.h
index e95afa8502..81a4c9bc60 100644
--- a/android/jni/OpenSLContext.h
+++ b/android/jni/OpenSLContext.h
@@ -10,11 +10,15 @@ public:
OpenSLContext(AndroidAudioCallback cb, int framesPerBuffer, int sampleRate);
bool Init() override;
- int AudioRecord_Start(int sampleRate) override;
- int AudioRecord_Stop() override;
+ bool AudioRecord_Start(int sampleRate) override;
+ bool AudioRecord_Stop() override;
+
~OpenSLContext();
private:
+ bool CheckResult(SLresult result, const char *str);
+ static bool CheckResultStatic(SLresult result, const char *str);
+
// Should be no reason to need more than two buffers, but make it clear in the code.
enum {
NUM_BUFFERS = 2,
diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp
index 2fad162248..0cfe1f4e19 100644
--- a/android/jni/app-android.cpp
+++ b/android/jni/app-android.cpp
@@ -61,8 +61,10 @@ struct JNIEnv {};
#include "Common/System/System.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/File/Path.h"
+#include "Common/File/DirListing.h"
#include "Common/File/VFS/VFS.h"
#include "Common/File/VFS/AssetReader.h"
+#include "Common/File/AndroidStorage.h"
#include "Common/Input/InputState.h"
#include "Common/Input/KeyCodes.h"
#include "Common/Profiler/Profiler.h"
@@ -111,7 +113,7 @@ static std::atomic emuThreadState((int)EmuThreadState::DISABLED);
void UpdateRunLoopAndroid(JNIEnv *env);
-static AndroidAudioState *g_audioState;
+AndroidAudioState *g_audioState;
struct FrameCommand {
FrameCommand() {}
@@ -171,9 +173,6 @@ static float g_safeInsetBottom = 0.0;
static jmethodID postCommand;
-static jmethodID openContentUri;
-static jmethodID closeContentUri;
-
static jobject nativeActivity;
static volatile bool exitRenderLoop;
static bool renderLoopRunning;
@@ -233,46 +232,6 @@ void AndroidLogger::Log(const LogMessage &message) {
}
}
-bool Android_IsContentUri(const std::string &filename) {
- return startsWith(filename, "content://");
-}
-
-int Android_OpenContentUriFd(const std::string &filename) {
- if (!nativeActivity) {
- return -1;
- }
- auto env = getEnv();
- jstring param = env->NewStringUTF(filename.c_str());
- int fd = env->CallIntMethod(nativeActivity, openContentUri, param);
- return fd;
-}
-
-class ContentURIFileLoader : public ProxiedFileLoader {
-public:
- ContentURIFileLoader(const Path &filename)
- : ProxiedFileLoader(nullptr) { // we overwrite the nullptr below
- int fd = Android_OpenContentUriFd(filename.ToString());
- INFO_LOG(SYSTEM, "Fd %d for content URI: '%s'", fd, filename.c_str());
- backend_ = new LocalFileLoader(fd, filename);
- }
-
- bool ExistsFast() override {
- if (!nativeActivity) {
- // Assume it does if we don't have a NativeActivity right now.
- return true;
- }
- return backend_->ExistsFast();
- }
-};
-
-class AndroidContentLoaderFactory : public FileLoaderFactory {
-public:
- AndroidContentLoaderFactory() {}
- FileLoader *ConstructFileLoader(const Path &filename) override {
- return new ContentURIFileLoader(filename);
- }
-};
-
JNIEnv* getEnv() {
JNIEnv *env;
int status = gJvm->GetEnv((void**)&env, JNI_VERSION_1_6);
@@ -518,10 +477,14 @@ std::string GetJavaString(JNIEnv *env, jstring jstr) {
extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_registerCallbacks(JNIEnv *env, jobject obj) {
nativeActivity = env->NewGlobalRef(obj);
postCommand = env->GetMethodID(env->GetObjectClass(obj), "postCommand", "(Ljava/lang/String;Ljava/lang/String;)V");
- openContentUri = env->GetMethodID(env->GetObjectClass(obj), "openContentUri", "(Ljava/lang/String;)I");
+ _dbg_assert_(postCommand);
+
+ Android_RegisterStorageCallbacks(env, obj);
+ Android_StorageSetNativeActivity(nativeActivity);
}
extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_unregisterCallbacks(JNIEnv *env, jobject obj) {
+ Android_StorageSetNativeActivity(nullptr);
env->DeleteGlobalRef(nativeActivity);
nativeActivity = nullptr;
}
@@ -700,11 +663,6 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init
NativeInit((int)args.size(), &args[0], user_data_path.c_str(), externalStorageDir.c_str(), cacheDir.c_str());
- std::unique_ptr factory(new AndroidContentLoaderFactory());
-
- // Register a content URI file loader.
- RegisterFileLoaderFactory("content://", std::move(factory));
-
// No need to use EARLY_LOG anymore.
retry:
@@ -1124,9 +1082,6 @@ extern "C" jboolean Java_org_ppsspp_ppsspp_NativeApp_joystickAxis(
axis.deviceId = deviceId;
axis.value = value;
- float sensitivity = g_Config.fXInputAnalogSensitivity;
- axis.value *= sensitivity;
-
return NativeAxis(axis);
}
diff --git a/android/jni/app-android.h b/android/jni/app-android.h
index 3a4935d63b..d4cb8d2fee 100644
--- a/android/jni/app-android.h
+++ b/android/jni/app-android.h
@@ -3,20 +3,24 @@
#include "ppsspp_config.h"
#include
-#include "Common/LogManager.h"
+#include
+#include
-#if PPSSPP_PLATFORM(ANDROID)
+#include "Common/LogManager.h"
+#include "Common/File/DirListing.h"
+#include "Common/File/Path.h"
+#include "Common/File/AndroidStorage.h"
+
+#if PPSSPP_PLATFORM(ANDROID) && !defined(__LIBRETRO__)
#include
jclass findClass(const char* name);
JNIEnv* getEnv();
-#endif
-
class AndroidLogger : public LogListener {
public:
void Log(const LogMessage &message) override;
};
-extern std::string g_extFilesDir;
+#endif
diff --git a/android/src/org/ppsspp/ppsspp/NativeActivity.java b/android/src/org/ppsspp/ppsspp/NativeActivity.java
index e0286d6102..6a1b82bf67 100644
--- a/android/src/org/ppsspp/ppsspp/NativeActivity.java
+++ b/android/src/org/ppsspp/ppsspp/NativeActivity.java
@@ -27,6 +27,7 @@ import android.os.PowerManager;
import android.os.Vibrator;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
+import androidx.documentfile.provider.DocumentFile;
import android.text.InputType;
import android.util.Log;
import android.view.Gravity;
@@ -102,7 +103,7 @@ public abstract class NativeActivity extends Activity {
private boolean shuttingDown;
private static final int RESULT_LOAD_IMAGE = 1;
- private static final int RESULT_BROWSE_FILE = 2;
+ private static final int RESULT_OPEN_DOCUMENT = 2;
private static final int RESULT_OPEN_DOCUMENT_TREE = 3;
// Allow for multiple connected gamepads but just consider them the same for now.
@@ -1141,7 +1142,7 @@ public abstract class NativeActivity extends Activity {
cursor.close();
NativeApp.sendMessage("bgImage_updated", picturePath);
}
- } else if (requestCode == RESULT_BROWSE_FILE) {
+ } else if (requestCode == RESULT_OPEN_DOCUMENT) {
Uri selectedFile = data.getData();
if (selectedFile != null) {
// Grab permanent permission so we can show it in recents list etc.
@@ -1152,12 +1153,21 @@ public abstract class NativeActivity extends Activity {
NativeApp.sendMessage("browse_fileSelect", selectedFile.toString());
}
} else if (requestCode == RESULT_OPEN_DOCUMENT_TREE) {
- Uri selectedFile = data.getData();
- if (selectedFile != null) {
- // Convert URI to normal path. (This might not be possible in Android 12+)
- String path = selectedFile.toString();
+ Uri selectedDirectoryUri = data.getData();
+ if (selectedDirectoryUri != null) {
+ String path = selectedDirectoryUri.toString();
Log.i(TAG, "Browse folder finished: " + path);
- NativeApp.sendMessage("browse_folderSelect", path);
+ getContentResolver().takePersistableUriPermission(selectedDirectoryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ DocumentFile documentFile = DocumentFile.fromTreeUri(this, selectedDirectoryUri);
+ Log.i(TAG, "Document name: " + documentFile.getUri());
+ /*
+ // Old debug log
+ DocumentFile[] children = documentFile.listFiles();
+ for (DocumentFile child : children) {
+ Log.i(TAG, "Child: " + child.getUri() + " " + child.getName());
+ }
+ */
+ NativeApp.sendMessage("browse_folderSelect", documentFile.getUri().toString());
}
}
}
@@ -1296,8 +1306,11 @@ public abstract class NativeActivity extends Activity {
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
- //intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);
- startActivityForResult(intent, RESULT_BROWSE_FILE);
+ // Possible alternative approach:
+ // String[] mimeTypes = {"application/octet-stream", "/x-iso9660-image"};
+ // intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
+ startActivityForResult(intent, RESULT_OPEN_DOCUMENT);
+ // intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);
} catch (Exception e) {
Log.e(TAG, e.toString());
return false;
@@ -1305,9 +1318,9 @@ public abstract class NativeActivity extends Activity {
} else if (command.equals("browse_folder")) {
try {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
- intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); // not yet used properly
+ intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); // Only allow local folders.
startActivityForResult(intent, RESULT_OPEN_DOCUMENT_TREE);
return true;
diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java
index d03fcdab47..a09a5e759a 100644
--- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java
+++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java
@@ -8,6 +8,11 @@ import android.os.Bundle;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.util.Log;
+import android.os.storage.StorageManager;
+import androidx.documentfile.provider.DocumentFile;
+import java.util.ArrayList;
+import java.util.UUID;
+import java.io.File;
public class PpssppActivity extends NativeActivity {
private static final String TAG = "PpssppActivity";
@@ -114,17 +119,157 @@ public class PpssppActivity extends NativeActivity {
});
}
- public int openContentUri(String uriString) {
+ public int openContentUri(String uriString, String mode) {
try {
Uri uri = Uri.parse(uriString);
- ParcelFileDescriptor filePfd = getContentResolver().openFileDescriptor(uri, "r");
+ ParcelFileDescriptor filePfd = getContentResolver().openFileDescriptor(uri, mode);
if (filePfd == null) {
Log.e(TAG, "Failed to get file descriptor for " + uriString);
return -1;
}
return filePfd.detachFd(); // Take ownership of the fd.
} catch (Exception e) {
- Log.e(TAG, "Exception opening content uri: " + e.toString());
+ Log.e(TAG, "openContentUri exception: " + e.toString());
+ return -1;
+ }
+ }
+
+ private static String fileInfoToString(DocumentFile file) {
+ String str = "F|";
+ if (file.isVirtual()) {
+ // This we don't want to see.
+ str = "V|";
+ Log.e(TAG, "Got virtual file: " + file.getUri());
+ } else if (file.isDirectory()) {
+ str = "D|";
+ }
+ str += file.length() + "|" + file.getName() + "|" + file.getUri() + "|" + file.lastModified();
+ return str;
+ }
+
+ public String[] listContentUriDir(String uriString) {
+ try {
+ Uri uri = Uri.parse(uriString);
+ DocumentFile documentFile = DocumentFile.fromTreeUri(this, uri);
+ DocumentFile[] children = documentFile.listFiles();
+ ArrayList listing = new ArrayList();
+ // Encode entries into strings for JNI simplicity.
+ for (DocumentFile file : children) {
+ String str = fileInfoToString(file);
+ listing.add(str);
+ }
+ // Is ArrayList weird or what?
+ String[] strings = new String[listing.size()];
+ return listing.toArray(strings);
+ } catch (Exception e) {
+ Log.e(TAG, "listContentUriDir exception: " + e.toString());
+ return new String[]{};
+ }
+ }
+
+ public boolean contentUriCreateDirectory(String rootTreeUri, String dirName) {
+ try {
+ Uri uri = Uri.parse(rootTreeUri);
+ DocumentFile documentFile = DocumentFile.fromTreeUri(this, uri);
+ if (documentFile != null) {
+ DocumentFile createdDir = documentFile.createDirectory(dirName);
+ return createdDir != null;
+ } else {
+ Log.e(TAG, "contentUriCreateDirectory: fromTreeUri returned null");
+ return false;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "contentUriCreateDirectory exception: " + e.toString());
+ return false;
+ }
+ }
+
+ public boolean contentUriCreateFile(String rootTreeUri, String fileName) {
+ try {
+ Uri uri = Uri.parse(rootTreeUri);
+ DocumentFile documentFile = DocumentFile.fromTreeUri(this, uri);
+ if (documentFile != null) {
+ // TODO: Check the file extension and choose MIME type appropriately.
+ DocumentFile createdFile = documentFile.createFile("application/octet-stream", fileName);
+ return createdFile != null;
+ } else {
+ Log.e(TAG, "contentUriCreateFile: fromTreeUri returned null");
+ return false;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "contentUriCreateFile exception: " + e.toString());
+ return false;
+ }
+ }
+
+ public boolean contentUriRemoveFile(String fileName) {
+ try {
+ Uri uri = Uri.parse(fileName);
+ DocumentFile documentFile = DocumentFile.fromSingleUri(this, uri);
+ if (documentFile != null) {
+ return documentFile.delete();
+ } else {
+ return false;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "contentUriRemoveFile exception: " + e.toString());
+ return false;
+ }
+ }
+
+ public String contentUriGetFileInfo(String fileName) {
+ try {
+ Uri uri = Uri.parse(fileName);
+ DocumentFile documentFile = DocumentFile.fromSingleUri(this, uri);
+ if (documentFile != null) {
+ if (documentFile.exists()) {
+ String str = fileInfoToString(documentFile);
+ return str;
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "contentUriGetFileInfo exception: " + e.toString());
+ return null;
+ }
+ }
+
+ // The example in Android documentation uses this.getFilesDir for path.
+ // There's also a way to beg the OS for more space, which might clear caches, but
+ // let's just not bother with that for now.
+ public long contentUriGetFreeStorageSpace(String uriString) {
+ try {
+ StorageManager storageManager = getApplicationContext().getSystemService(StorageManager.class);
+
+ // In 29 and later, we can directly get the UUID for the storage volume
+ // through the URI.
+ UUID volumeUUID;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Uri uri = Uri.parse(uriString);
+ volumeUUID = UUID.fromString(storageManager.getStorageVolume(uri).getUuid());
+ } else {
+ volumeUUID = storageManager.getUuidForPath(this.getFilesDir());
+ }
+ long availableBytes = storageManager.getAllocatableBytes(volumeUUID);
+ return availableBytes;
+ } catch (Exception e) {
+ Log.e(TAG, "contentUriGetFreeStorageSpace exception: " + e.toString());
+ return -1;
+ }
+ }
+
+ public long filePathGetFreeStorageSpace(String filePath) {
+ try {
+ StorageManager storageManager = getApplicationContext().getSystemService(StorageManager.class);
+ File file = new File(filePath);
+ UUID volumeUUID = storageManager.getUuidForPath(file);
+ long availableBytes = storageManager.getAllocatableBytes(volumeUUID);
+ return availableBytes;
+ } catch (Exception e) {
+ Log.e(TAG, "filePathGetFreeStorageSpace exception: " + e.toString());
return -1;
}
}
diff --git a/assets/flash0/font/jpn0.pgf b/assets/flash0/font/jpn0.pgf
index 0dcf86e909..e0fba76219 100644
Binary files a/assets/flash0/font/jpn0.pgf and b/assets/flash0/font/jpn0.pgf differ
diff --git a/assets/lang b/assets/lang
index 3bf6b18eb0..87f58200e4 160000
--- a/assets/lang
+++ b/assets/lang
@@ -1 +1 @@
-Subproject commit 3bf6b18eb0ae68861fbfbb7b43cd764771ca42a9
+Subproject commit 87f58200e4e48a047aa9641270432746c10cc6c4
diff --git a/assets/ui_atlas_luna.meta b/assets/ui_atlas_luna.meta
index e04598283c..9574df7b60 100644
Binary files a/assets/ui_atlas_luna.meta and b/assets/ui_atlas_luna.meta differ
diff --git a/assets/ui_atlas_luna.zim b/assets/ui_atlas_luna.zim
index bcce5f295c..24f9f66b73 100644
Binary files a/assets/ui_atlas_luna.zim and b/assets/ui_atlas_luna.zim differ
diff --git a/atlasscript.txt b/atlasscript.txt
index 6e558dbbc9..495a046f51 100644
--- a/atlasscript.txt
+++ b/atlasscript.txt
@@ -49,4 +49,10 @@ image I_FLAG_KO source_assets/image/flag_ko.png copy
image I_FULLSCREEN source_assets/image/fullscreen.png copy
image I_RESTORE source_assets/image/restore.png copy
image I_SDCARD source_assets/image/sdcard.png copy
-image I_HOME source_assets/image/home.png copy
\ No newline at end of file
+image I_HOME source_assets/image/home.png copy
+image I_A source_assets/image/a.png copy
+image I_B source_assets/image/b.png copy
+image I_C source_assets/image/c.png copy
+image I_D source_assets/image/d.png copy
+image I_E source_assets/image/e.png copy
+image I_F source_assets/image/f.png copy
diff --git a/ext/gason/gason.h b/ext/gason/gason.h
index 7c3eed96f6..1f1cd976a2 100644
--- a/ext/gason/gason.h
+++ b/ext/gason/gason.h
@@ -29,7 +29,7 @@ union JsonValue {
: fval(x) {
}
JsonValue(JsonTag tag = JSON_NULL, void *payload = nullptr) {
- assert((uintptr_t)payload <= JSON_VALUE_PAYLOAD_MASK);
+ assert((uint64_t)payload <= JSON_VALUE_PAYLOAD_MASK);
ival = JSON_VALUE_NAN_MASK | ((uint64_t)tag << JSON_VALUE_TAG_SHIFT) | (uintptr_t)payload;
}
bool isDouble() const {
diff --git a/headless/Headless.cpp b/headless/Headless.cpp
index eb6f1f5436..707ca6ac6a 100644
--- a/headless/Headless.cpp
+++ b/headless/Headless.cpp
@@ -14,11 +14,13 @@
#include "Common/System/NativeApp.h"
#include "Common/System/System.h"
+#include "Common/CPUDetect.h"
#include "Common/File/VFS/VFS.h"
#include "Common/File/VFS/AssetReader.h"
#include "Common/File/FileUtil.h"
#include "Common/GraphicsContext.h"
#include "Common/TimeUtil.h"
+#include "Common/Thread/ThreadManager.h"
#include "Core/Config.h"
#include "Core/ConfigValues.h"
#include "Core/Core.h"
@@ -330,6 +332,8 @@ int main(int argc, const char* argv[])
if (testFilenames.empty())
return printUsage(argv[0], argc <= 1 ? NULL : "No executables specified");
+ g_threadManager.Init(cpu_info.num_cores, cpu_info.logical_cpu_count);
+
LogManager::Init(&g_Config.bEnableLogging);
LogManager *logman = LogManager::GetInstance();
@@ -388,7 +392,6 @@ int main(int argc, const char* argv[])
g_Config.iInternalResolution = 1;
g_Config.iUnthrottleMode = (int)UnthrottleMode::CONTINUOUS;
g_Config.bEnableLogging = fullLog;
- g_Config.iNumWorkerThreads = 1;
g_Config.bSoftwareSkinning = true;
g_Config.iATRACMP3Volume = MAX_CONFIG_VOLUME;
g_Config.iSASVolume = MAX_CONFIG_VOLUME;
diff --git a/ios/ViewController.mm b/ios/ViewController.mm
index 55a13e58d8..235a133733 100644
--- a/ios/ViewController.mm
+++ b/ios/ViewController.mm
@@ -682,31 +682,31 @@ int ToTouchID(UITouch *uiTouch, bool allowAllocate) {
};
#if defined(__IPHONE_12_1) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_1
- if (extendedProfile.leftThumbstickButton != nil) {
+ if ([extendedProfile respondsToSelector:@selector(leftThumbstickButton)] && extendedProfile.leftThumbstickButton != nil) {
extendedProfile.leftThumbstickButton.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
[self controllerButtonPressed:pressed keyCode:NKCODE_BUTTON_11];
};
}
- if (extendedProfile.rightThumbstickButton != nil) {
+ if ([extendedProfile respondsToSelector:@selector(rightThumbstickButton)] && extendedProfile.rightThumbstickButton != nil) {
extendedProfile.rightThumbstickButton.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
[self controllerButtonPressed:pressed keyCode:NKCODE_BUTTON_12];
};
}
#endif
#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
- if (extendedProfile.buttonOptions != nil) {
+ if ([extendedProfile respondsToSelector:@selector(buttonOptions)] && extendedProfile.buttonOptions != nil) {
extendedProfile.buttonOptions.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
[self controllerButtonPressed:pressed keyCode:NKCODE_BUTTON_13];
};
}
- if (extendedProfile.buttonMenu != nil) {
+ if ([extendedProfile respondsToSelector:@selector(buttonMenu)] && extendedProfile.buttonMenu != nil) {
extendedProfile.buttonMenu.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
[self controllerButtonPressed:pressed keyCode:NKCODE_BUTTON_14];
};
}
#endif
#if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0
- if (extendedProfile.buttonHome != nil) {
+ if ([extendedProfile respondsToSelector:@selector(buttonHome)] && extendedProfile.buttonHome != nil) {
extendedProfile.buttonHome.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
[self controllerButtonPressed:pressed keyCode:NKCODE_BUTTON_15];
};
@@ -718,7 +718,7 @@ int ToTouchID(UITouch *uiTouch, bool allowAllocate) {
axisInput.deviceId = DEVICE_ID_PAD_0;
axisInput.flags = 0;
axisInput.axisId = JOYSTICK_AXIS_X;
- axisInput.value = value * g_Config.fXInputAnalogSensitivity;
+ axisInput.value = value;
NativeAxis(axisInput);
};
@@ -727,7 +727,7 @@ int ToTouchID(UITouch *uiTouch, bool allowAllocate) {
axisInput.deviceId = DEVICE_ID_PAD_0;
axisInput.flags = 0;
axisInput.axisId = JOYSTICK_AXIS_Y;
- axisInput.value = -value * g_Config.fXInputAnalogSensitivity;
+ axisInput.value = -value;
NativeAxis(axisInput);
};
@@ -737,7 +737,7 @@ int ToTouchID(UITouch *uiTouch, bool allowAllocate) {
axisInput.deviceId = DEVICE_ID_PAD_0;
axisInput.flags = 0;
axisInput.axisId = JOYSTICK_AXIS_Z;
- axisInput.value = value * g_Config.fXInputAnalogSensitivity;
+ axisInput.value = value;
NativeAxis(axisInput);
};
@@ -746,7 +746,7 @@ int ToTouchID(UITouch *uiTouch, bool allowAllocate) {
axisInput.deviceId = DEVICE_ID_PAD_0;
axisInput.flags = 0;
axisInput.axisId = JOYSTICK_AXIS_RZ;
- axisInput.value = -value * g_Config.fXInputAnalogSensitivity;
+ axisInput.value = -value;
NativeAxis(axisInput);
};
}
diff --git a/korean.txt b/korean.txt
index d225e670a5..e48b5a7c0c 100644
--- a/korean.txt
+++ b/korean.txt
@@ -739,3 +739,1077 @@ ko_KR = "KOREAN"
th_TH = "THAI"
zh_TW = "CHINESE_TRADITIONAL"
zh_CN = "CHINESE_SIMPLIFIED"
+[Audio]
+Alternate speed volume = 대체 속도 볼륨
+Audio backend = 오디오 처리자 (재시작 필요)
+AudioBufferingForBluetooth = 블루투스 친화적 버퍼링 (느림)
+Auto = 자동
+Device = 장치
+DSound (compatible) = DSound (호환됨)
+Enable Sound = 사운드 활성화
+Global volume = 전역 볼륨
+Microphone = Microphone
+Microphone Device = Microphone device
+Mute = 음소거
+Switch on new audio device = 새로운 오디오 장치로 전환
+Use global volume = 전역 볼륨 사용
+WASAPI (fast) = WASAPI (빠름)
+
+[Controls]
+Analog auto-rotation speed = 아날로그 자동 회전 속도
+Analog Axis Sensitivity = 아날로그 축 민감도
+Analog Limiter = 아날로그 제한
+Analog Mapper High End = 아날로그 매퍼 high-end (축 감도)
+Analog Mapper Low End = 아날로그 매퍼 low-end (데드존 반전)
+Analog Mapper Mode = 아날로그 매퍼 모드
+Analog Stick = 아날로그 스틱
+AnalogLimiter Tip = 아날로그 제한 버튼이 눌러졌을 때 아날로그를 제한합니다.
+Auto = 자동
+Auto-centering analog stick = 아날로그 스틱 자동으로 중앙 고정
+Auto-hide buttons after seconds = 버튼 자동 숨김
+Binds = Binds
+Button Opacity = 버튼 투명도
+Button style = 버튼 스타일
+Calibrate D-Pad = 십자 패드 보정
+Calibration = 보정
+Classic = 클래식
+Combo Key Setting = 콤보 키 설정
+Combo Key Setup = 콤보 키 설정하기
+Confine Mouse = 창/화면 안에 커서를 가두기
+Control Mapping = 컨트롤 맵핑
+Custom layout... = 터치 컨트롤 레이아웃 편집...
+Customize tilt = 기울기 사용자화...
+D-PAD = 십자 패드
+Deadzone Radius = 데드존 범위
+DInput Analog Settings = DInput 아날로그 설정 (다이렉트 인풋)
+Disable D-Pad diagonals (4-way touch) = 십자 패드 대각선 비활성 (4방향 터치)
+Glowing borders = 빛나는 테두리
+HapticFeedback = 햅틱 피드백 (진동)
+Ignore gamepads when not focused = 창이 활성화 되지 않았을 때 게임패드 무시
+Ignore Windows Key = 윈도우 키 무시
+Invert Axes = 축 반전
+Invert Tilt along X axis = X축 기울기 반전
+Invert Tilt along Y axis = Y축 기울기 반전
+Keep this button pressed when right analog is pressed = 오른쪽 아날로그가 눌렸을 때 이 버튼을 눌리게끔 유지
+Keyboard = 키보드 조작 설정
+L/R Trigger Buttons = L/R 트리거 버튼
+Landscape = 가로
+Landscape Auto = 가로 자동
+Landscape Reversed = 가로 반전
+Mouse = 마우스 설정
+Mouse sensitivity = 마우스 민감도
+Mouse smoothing = 마우스 가속도(Smoothing)
+MouseControl Tip = 'M' 아이콘을 눌러 조작 매핑 화면에 들어가 마우스를 매핑할 수 있습니다.
+None (Disabled) = 없음 (비활성)
+Off = 끄기
+OnScreen = 화면 터치 컨트롤
+Portrait = 세로
+Portrait Reversed = 세로 반전
+PSP Action Buttons = PSP 액션 버튼
+Screen Rotation = 화면 회전
+seconds, 0 : off = 초 단위, 0 : 끄기
+Sensitivity = 민감도
+Show right analog = 오른쪽 아날로그 보기
+Show Touch Pause Menu Button = 일시 정지 메뉴 버튼 표시
+Thin borders = 얇은 테두리
+Tilt Base Radius = Tilt base radius
+Tilt Input Type = 기울기 입력 방식
+Tilt Sensitivity along X axis = X축 기울기 민감도
+Tilt Sensitivity along Y axis = Y축 기울기 민감도
+To Calibrate = 보정하려면, 기기를 평평한 곳에 놓고 "보정"을 눌러주십시오.
+Touch Control Visibility = 터치 컨트롤 표시
+Use custom right analog = 사용자 정의 오른쪽 아날로그 사용
+Use Mouse Control = 마우스 사용하기
+Visibility = 표시
+X = X
+X + Y = X + Y
+XInput Analog Settings = XInput 아날로그 설정 (Xbox 계열 컨트롤러)
+Y = Y
+
+[CwCheats]
+Cheats = 치트
+Edit Cheat File = 치트 파일 편집
+Enable/Disable All = 모든 치트 활성화/비활성화
+Import Cheats = 치트 가져오기 (cheat.db에서)
+Options = 옵션
+Refresh Rate = 재생률
+
+[DesktopUI]
+# If your language does not show well with the default font, you can use Font to specify a different one.
+# Just add it to your language's ini file and uncomment it (remove the # by Font).
+Font = NanumGothic
+About PPSSPP... = PPSSPP 정보(&A)...
+Auto = 자동(&A)
+Backend = 렌더링 처리자(&B) (PPSSPP 재시작)
+Bicubic = 쌍입방(&B)
+Break = 중단
+Break on Load = 불러올 때 중단
+Buffered Rendering = 버퍼링된 렌더링(&B)
+Buy Gold = 골드 버전 구매(&G)
+Control Mapping... = 컨트롤 맵핑(&O)...
+Debugging = 디버그(&D)
+Deposterize = 색조 융합(&D)
+Direct3D9 = Direct3D 9(&D)
+Direct3D11 = Direct3D &11
+Disassembly = 역어셈블리(&D)...
+Discord = Discord
+Display Layout Editor = 화면 레이아웃 편집기...
+Display Rotation = 화면 회전
+Dump Next Frame to Log = 로그에 다음 프레임 덤프(&U)
+Emulation = 에뮬레이션(&E)
+Enable Chat = 대화 활성화
+Enable Cheats = 치트 활성화(&C)
+Enable Sound = 사운드 활성화(&O)
+Exit = 종료(&X)
+Extract File... = 파일 추출(&X)...
+File = 파일(&F)
+Frame Skipping = 프레임 생략(&F)
+Frame Skipping Type = 프레임 생략 유형
+Fullscreen = 전체 화면(&L)
+Game Settings = 게임 설정(&G)
+GE Debugger... = GE 디버거(&R)...
+GitHub = Git&Hub
+Hardware Transform = 하드웨어 변환(&H)
+Help = 도움말(&H)
+Hybrid = 혼합(&H)
+Hybrid + Bicubic = 혼합 + 쌍입방(&Y)
+Ignore Illegal Reads/Writes = 잘못된 읽기/쓰기 무시(&I)
+Ignore Windows Key = 윈도우 키 무시
+Keep PPSSPP On Top = PPSSPP 항상 위에 표시(&K)
+Landscape = 가로
+Landscape reversed = 가로 반전
+Language... = 언어(&N)...
+Linear = 선형 필터링(&L)
+Load = 불러오기(&L)...
+Load .sym File... = sym 파일 불러오기(&A)...
+Load Map File... = 맵 파일 불러오기(&M)...
+Load State = 상태 불러오기(&O)
+Load State File... = 상태 파일 불러오기(&L)...
+Log Console = 콘솔 로그(&L)
+Memory View... = 메모리 뷰(&V)...
+More Settings... = 기타 설정(&M)...
+Nearest = 근접 필터링(&N)
+Non-Buffered Rendering = 버퍼 없는 렌더링(&S)
+Off = 끄기(&O)
+Open Directory... = 디렉터리 열기(&D)...
+Open from MS:/PSP/GAME... = MS:/PSP/GAME에서 열기(&P)...
+Open Memory Stick = 메모리 스틱 열기(&M)
+Open New Instance = Open new instance
+OpenGL = OpenGL(&O)
+Pause = 일시 정지(&P)
+Pause When Not Focused = 포커싱되지 않았을 때 일시 정지(&P)
+Portrait = 세로
+Portrait reversed = 세로 반전
+Postprocessing Shader = 후처리 셰이더(&G)
+PPSSPP Forums = PPSSPP 포럼(&F)
+Record = 기록(&R)
+Record Audio = 소리 녹음(&A)
+Record Display = 영상 녹화(&D)
+Rendering Mode = 렌더링 방식(&O)
+Rendering Resolution = 렌더링 해상도(&R)
+Reset = 리셋(&E)
+Reset Symbol Table = 심볼 테이블 리셋(&Y)
+Run = 실행(&R)
+Save .sym File... = sym 파일 저장하기(&E)...
+Save Map File... = 맵 파일 저장하기(&S)...
+Save State = 상태 저장하기(&A)
+Save State File... = 상태 파일 저장하기(&S)...
+Savestate Slot = 상태 저장 슬롯(&T)
+Screen Scaling Filter = 화면 크기 조절 필터(&E)
+Show Debug Statistics = 디버그 통계 표시(&G)
+Show FPS Counter = FPS 카운터 표시(&F)
+Skip Number of Frames = 프레임 수 생략
+Skip Percent of FPS = FPS 비율 생략
+Stop = 정지(&S)
+Switch UMD = UMD 교체
+Take Screenshot = 스크린샷 찍기(&T)
+Texture Filtering = 텍스처 필터링(&X)
+Texture Scaling = 텍스처 크기 조절(&T)
+Use Lossless Video Codec (FFV1) = 무손실 비디오 코덱 사용 (FFV1)(&U)
+Use output buffer for video = 비디오에 출력 버퍼 사용
+Vertex Cache = 버텍스 캐시(&V)
+VSync = 수직 동기(&Y)
+Vulkan = 벌칸(Vulkan)
+Window Size = 창 크기(&W)
+www.ppsspp.org = www.ppsspp.org 방문(&P)
+xBRZ = xBRZ(&X)
+
+[Developer]
+Allocator Viewer = 할당기(Allocator) 뷰어 (Vulkan)
+Allow remote debugger = 원격 디버거 허용
+Backspace = 백스페이스
+Block address = 주소를 차단
+By Address = 주소로
+Copy savestates to memstick root = Copy save states to Memory Stick root
+Create/Open textures.ini file for current game = 현재 게임의 textures.ini 파일 생성 및 열기
+Current = 현재
+Dev Tools = 개발자 도구
+DevMenu = 개발 메뉴
+Disabled JIT functionality = 비활성화 된 JIT 기능
+Draw Frametimes Graph = 프레임 레이트 그래프 그리기
+Dump Decrypted Eboot = 게임 부팅 시 복호화된 EBOOT.BIN 덤프 (암호화 되어 있는 경우)
+Dump Frame GPU Commands = 프레임 GPU 명령어를 덤프
+Enable driver bug workarounds = Enable driver bug workarounds
+Enable Logging = 디버그 로깅 활성화
+Enter address = 주소 입력
+FPU = FPU
+Framedump tests = Framedump tests
+Frame Profiler = 프레임 프로파일러
+GPU Driver Test = GPU 드라이버 테스트
+GPU Profile = GPU 프로파일
+Jit Compare = JIT 비교
+JIT debug tools = JIT 디버그 툴
+Language = 언어
+Load language ini = 언어 ini 불러오기
+Log Dropped Frame Statistics = 프레임 저하 통계 기록하기
+Log Level = 로그 깊이
+Log View = 로그 보기
+Logging Channels = 로깅 채널
+Next = 다음
+No block = 블록 없음
+Prev = 이전
+Random = 랜덤
+Replace textures = 텍스처 덮어쓰기
+Reset limited logging = Reset limited logging
+RestoreDefaultSettings = 모든 설정을 다시 기본으로 복원 하시겠습니까?\n컨트롤 맵핑 설정은 변경되지 않습니다.\n\n\n이 작업은 되돌릴 수 없습니다.\n변경 사항을 적용하기 위해 PPSSPP를 재시작해 주십시오.
+RestoreGameDefaultSettings = 게임 지정 설정을 다시\nPPSSPP 기본 설정으로 복원하시겠습니까?
+Resume = Resume
+Run CPU Tests = CPU 테스트 실행
+Save language ini = 언어 ini 저장하기
+Save new textures = 새로운 텍스처 저장
+Shader Viewer = 셰이더 뷰어
+Show Developer Menu = 개발자 메뉴 표시
+Show on-screen messages = Show on-screen messages
+Stats = 상태
+System Information = 시스템 정보
+Texture Replacement = 텍스처 교체하기
+Toggle Audio Debug = 오디오 디버그 토글
+Toggle Freeze = 토글 프리징
+Touchscreen Test = 터치 스크린 테스트
+VFPU = VFPU
+
+[Dialog]
+* PSP res = * PSP 리소스
+Active = 활성화
+Back = 뒤로
+Cancel = 취소
+Center = 중앙
+ChangingGPUBackends = GPU 처리자를 변경하기 위해서는 PPSSPP를 재시작해야 합니다. 재시작할까요?
+ChangingInflightFrames = 그래픽 커멘드 버퍼링을 변경하기 위해서는 PPSSPP를 재시작해야 합니다. 재시작할까요?
+Channel: = Channel:
+Choose PPSSPP save folder = PPSSPP 저장 폴더 선택
+Confirm Overwrite = 데이터를 덮어쓰시겠습니까?
+Confirm Save = 이 데이터를 저장하시겠습니까?
+ConfirmLoad = 이 데이터를 불러오시겠습니까?
+ConnectingAP = Connecting to the access point.\nPlease wait...
+ConnectingPleaseWait = Connecting.\nPlease wait...
+ConnectionName = Connection name
+Corrupted Data = Corrupted data
+Delete = 삭제
+Delete all = 전체 삭제
+Delete completed = 삭제를 완료했습니다.
+DeleteConfirm = 이 데이터는 삭제됩니다.\n계속하시겠습니까?
+DeleteConfirmAll = 이 게임에 대한 모든 저장 데이터를\n삭제하시겠습니까?
+DeleteConfirmGame = 이 게임을 정말로 당신의 장치에서\n삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
+DeleteConfirmGameConfig = 이 게임의 설정을 정말로 삭제하시겠습니까?
+DeleteFailed = 삭제할 수 없습니다.
+Deleting = 삭제 중입니다.\n잠시 기다려 주세요...
+Disable All = 모두 비활성화
+Edit = Edit
+Enable All = 모두 활성화
+Enter = 결정
+Finish = 완료
+Grid = 격자
+Inactive = 비활성화
+InternalError = An internal error has occurred.
+Load = 불러오기
+Load completed = 불러오기를 완료했습니다.
+Loading = 불러오는 중입니다.\n잠시 기다려 주세요...
+LoadingFailed = 불러올 수 없습니다.
+Move = 이동
+Network Connection = 네트워크 연결
+NEW DATA = 새로운 데이터
+No = 아니오
+ObtainingIP = Obtaining IP address.\nPlease wait...
+OK = 확인
+Old savedata detected = 구 버전 저장 데이터 감지됨
+Options = 옵션
+Reset = 리셋
+Resize = 크기 조정
+Retry = 다시 시도
+Save = 저장하기
+Save completed = 저장을 완료했습니다.
+Saving = 저장 중입니다.\n잠시 기다려 주세요...
+SavingFailed = 저장할 수 없습니다.
+Select = 선택
+Shift = 전환
+Snap = Snap
+Space = 스페이스
+SSID = SSID
+Submit = 전송
+Supported = 지원
+There is no data = 데이터가 없습니다.
+Toggle All = 전체 선택
+Toggle List = Toggle list
+Unsupported = 지원되지 않음
+When you save, it will load on a PSP, but not an older PPSSPP = 저장할 경우 PSP에서는 불러올 수 있지만 구 버전의 PPSSPP에서는 불러올 수 없습니다.
+When you save, it will not work on outdated PSP Firmware anymore = 저장할 경우 더 이상 구 버전의 PSP 펌웨어에서는 작동하지 않을 것입니다.
+Yes = 네
+Zoom = 확대
+
+[Error]
+7z file detected (Require 7-Zip) = 파일이 7z 형식으로 압축되어 있습니다.\n먼저 압축을 해제해 주십시오. (7-Zip 또는 WinRAR)
+A PSP game couldn't be found on the disc. = 디스크에서 PSP 게임을 찾을 수 없습니다.
+Cannot boot ELF located outside mountRoot. = 삽입된 Root 이외의 경로에 위치한 ELF는 부팅할 수 없습니다.
+Could not save screenshot file = 스크린샷 파일을 저장할 수 없습니다.
+D3D9or11 = Direct3D 9를 사용할까요? ("아니오"를 선택할 경우 Direct3D 11를 사용합니다.)
+D3D11CompilerMissing = D3DCompiler_47.dll 을 찾을 수 없었습니다. 설치한 후 재시도하거나 대신 예 버튼을 눌러 Direct3D 9 를 사용할 수 있습니다.
+D3D11InitializationError = Direct3D 11 초기화 오류
+D3D11Missing = 현재 사용중인 OS가 D3D11을 포함하지 않고 있습니다. 업데이트를 진행해 보십시오.\n\n예를 눌러 Direct3D 9를 대신 사용합니다.
+D3D11NotSupported = 현재 사용중인 GPU가 Direct3D 11을 지원하지 않습니다.\n\n예를 눌러 Direct3D 9를 대신 사용할 수 있습니다.
+Disk full while writing data = 데이터를 쓰는 도중 디스크가 가득 찼습니다.
+ELF file truncated - can't load = ELF 파일이 불완전합니다 - 불러올 수 없습니다.
+Error loading file = 파일 불러오는 중 오류 :
+Error reading file = 파일을 읽는 동안 오류가 발생했습니다.
+Failed initializing CPU/Memory = Failed initializing CPU or memory
+Failed to identify file = 파일을 식별하는데 실패했습니다.
+Failed to load executable: = 실행 파일을 불러오는데 실패했습니다:
+File corrupt = 파일이 손상되었습니다.
+Game disc read error - ISO corrupt = 게임 디스크 읽기 오류: ISO 파일이 손상되었습니다.
+GenericAllStartupError = PPSSPP failed to start up with any graphics backend. Try upgrading your graphics and other drivers.
+GenericBackendSwitchCrash = PPSSPP crashed while starting.\n\nThis usually means a graphics driver problem. Try upgrading your graphics drivers.\n\nGraphics backend has been switched:
+GenericDirect3D9Error = Direct 3D 9 그래픽 초기화에 실패했습니다. 그래픽 드라이버와 DirectX 9 런타임을 업데이트해 보십시오.\n\nOpenGL으로 전환해 보시겠습니까?\n\오류 메시지:
+GenericGraphicsError = 그래픽 오류
+GenericOpenGLError = OpenGL 그래픽 초기화에 실패했습니다. 그래픽 드라이버를 업데이트해 보십시오.\n\nDirectX 9로 전환해 보시겠습니까?\n\n오류 메시지:
+GenericVulkanError = Vulkan 그래픽 초기화에 실패했습니다. 그래픽 드라이버를 업데이트해 보십시오.\n\nOpenGL으로 전환해 보시겠습니까?\n\n오류 메시지:
+InsufficientOpenGLDriver = OpenGL 드라이버 지원 부족 감지!\n\n현재 PPSSPP를 실행하는데 필요한 OpenGL 2.0을 지원하지 않는 것 같습니다.\n\n당신의 GPU가 OpenGL 2.0과 호환되는지 확인해 주십시오. 호환되는 경우, 당신의 GPU 공급업체의 웹사이트에서 새로운 그래픽 드라이버를 찾아서 설치해야 합니다.\n\n자세한 내용은 https://forums.ppsspp.org 포럼을 방문하십시오.
+Just a directory. = 디렉토리 입니다.
+Missing key = 키가 누락되었습니다.
+MsgErrorCode = Error code:
+MsgErrorSavedataDataBroken = Save data was corrupt.
+MsgErrorSavedataMSFull = Memory Stick full. Check your storage space.
+MsgErrorSavedataNoData = Warning: no save data was found.
+MsgErrorSavedataNoMS = Memory Stick not inserted.
+No EBOOT.PBP, misidentified game = EBOOT.PBP가 없으면, 게임을 식별할 수 없습니다.
+Not a valid disc image. = 유효한 디스크 이미지가 아닙니다.
+OpenGLDriverError = OpenGL 드라이버 오류
+PPSSPP doesn't support UMD Music. = PPSSPP는 UMD 음악을 지원하지 않습니다.
+PPSSPP doesn't support UMD Video. = PPSSPP는 UMD 비디오를 지원하지 않습니다.
+PPSSPP plays PSP games, not PlayStation 1 or 2 games. = PPSSPP plays PSP games, not PlayStation 1 or 2 games.
+PPSSPPDoesNotSupportInternet = PPSSPP는 현재 DLC, PSN, 게임 업데이트에 대한 인터넷 연결을 지원하지 않습니다.
+PS1 EBOOTs are not supported by PPSSPP. = PS1 EBOOTs는 PPSSPP에서 지원되지 않습니다.
+PSX game image detected. = MODE2 이미지 파일입니다. PPSSPP는 PS1 게임을 지원하지 않습니다.
+RAR file detected (Require UnRAR) = 파일이 RAR 형식으로 압축되어 있습니다.\n먼저 압축을 해제해 주십시오. (UnRAR로 시도)
+RAR file detected (Require WINRAR) = 파일이 RAR 형식으로 압축되어 있습니다.\n먼저 압축을 해제해 주십시오. (WinRAR로 시도)
+Running slow: try frameskip, sound is choppy when slow = 느리게 작동 중입니다: 게임 소리가 제대로 재생되지 않을 경우 프레임 스킵을 시도해 보십시오.
+Running slow: Try turning off Software Rendering = 느리게 작동 중입니다: 소프트웨어 렌더링 모드를 해제해 보십시오.
+Save encryption failed. This save won't work on real PSP = 저장 파일 암호화에 실패했습니다. 이 저장 파일은 실제 PSP상에서 동작하지 않을 것입니다.
+textures.ini filenames may not be cross-platform = 다른 운영체제의 "textures.ini" 파일을 사용할 수 없습니다.
+This is a saved state, not a game. = 게임이 아닌 상태 저장 파일입니다. 게임 파일을 선택해 주십시오.
+This is save data, not a game. = 게임이 아닌 저장 파일입니다. 게임 파일을 선택해 주십시오.
+Unable to create cheat file, disk may be full = 치트 파일을 생성할 수 없습니다. 디스크 용량이 꽉 찼을 수도 있습니다.
+Unable to initialize rendering engine. = 렌더링 엔진을 초기화할 수 없습니다.
+Unable to write savedata, disk may be full = 저장 파일을 작성할 수 없습니다. 디스크 용량이 꽉 찼을 수도 있습니다.
+Warning: Video memory FULL, reducing upscaling and switching to slow caching mode = 경고: 비디오 메모리가 꽉 찼습니다. 업스케일링을 줄이고 느린 캐시 모드로 전환합니다.
+Warning: Video memory FULL, switching to slow caching mode = 경고: 비디오 메모리가 꽉 찼습니다. 느린 캐시 모드로 전환합니다.
+ZIP file detected (Require UnRAR) = 파일이 ZIP 형식으로 압축되어 있습니다.\n먼저 압축을 해제해 주십시오. (UnRAR로 시도)
+ZIP file detected (Require WINRAR) = 파일이 ZIP 형식으로 압축되어 있습니다.\n먼저 압축을 해제해 주십시오. (WinRAR로 시도)
+
+[Game]
+Asia = 아시아
+ConfirmDelete = 삭제
+Create Game Config = 게임 설정파일 만들기
+Create Shortcut = 바로가기 만들기
+Delete Game = 게임 삭제
+Delete Game Config = 게임 설정파일 삭제
+Delete Save Data = 저장 데이터 삭제
+Europe = 유럽
+Game = 게임
+Game Settings = 게임 설정
+Homebrew = 홈브류
+Hong Kong = 홍콩
+InstallData = 데이터 설치
+Japan = 일본
+Korea = 대한민국
+MB = MB
+One moment please... = 잠시만 기다려 주세요...
+Play = 재생
+Remove From Recent = 최근 기록 제거
+SaveData = 저장 데이터
+Setting Background = 배경화면 설정
+Show In Folder = 폴더에 표시
+USA = 북미
+Use UI background = UI 배경화면 사용
+
+[Graphics]
+% of the void = 무효 영역의 백분율
+% of viewport = 뷰포트의 백분율
+%, 0:unlimited = %, 0 = 무제한
+(supersampling) = (슈퍼 샘플링)
+(upscaling) = (업스케일링)
+1x PSP = PSP 1배
+2x = 2배
+2x PSP = PSP 2배
+3x = 3배
+3x PSP = PSP 3배
+4x = 4배
+4x PSP = PSP 4배
+5x = 5배
+5x PSP = PSP 5배
+6x PSP = PSP 6배
+7x PSP = PSP 7배
+8x = 8배
+8x PSP = PSP 8배
+9x PSP = PSP 9배
+10x PSP = PSP 10배
+16x = 16배
+Aggressive = 급격한
+Alternative Speed = 대체 속도
+Alternative Speed 2 = 대체 속도 2 (단위 퍼센트, 0 = 무제한)
+Anisotropic Filtering = 비등방성 필터링
+Auto = 자동
+Auto (1:1) = 자동 (1:1)
+Auto (same as Rendering) = 자동 (렌더링 해상도와 같음)
+Auto FrameSkip = 자동 프레임 생략
+Auto Scaling = 자동 스케일링
+Backend = 처리자
+Balanced = 평형
+Bicubic = 쌍입방
+BlockTransfer Tip = 몇몇 게임들의 그래픽 오류를 고치기 위해 이 설정이 필요합니다.
+BlockTransferRequired = 주의 - 이 설정을 키기 위해 이 게임은 "블럭단위 전송 효과 시뮬레이트"를 사용해야 합니다.
+Both = 모두 표시
+Buffer graphics commands (faster, input lag) = 버퍼 그래픽 커맨드 (빠름, 입력 렉 발생)
+Buffered Rendering = 버퍼링된 렌더링
+BufferedRenderingRequired = 주의 - 이 게임은 "렌더링 모드"가 "버퍼됨" 이어야 합니다.
+Camera = 카메라
+Camera Device = 카메라 장치
+Cardboard Screen Size = 화면 크기 (뷰포트의 백분율)
+Cardboard Screen X Shift = X축 이동 (여백의 백분율)
+Cardboard Screen Y Shift = Y축 이동 (여백의 백분율)
+Cardboard VR Settings = 구글 카드보드 설정
+Cheats = 치트
+Clear Speedhack = Clear framebuffers on first use (speed hack)
+ClearSpeedhack Tip = Sometimes faster (mostly on mobile devices), may cause glitches
+CPU Core = CPU 코어
+Debugging = 디버깅
+DefaultCPUClockRequired = 주의 - 이 게임은 CPU 클럭이 기본 설정이어야 합니다.
+Deposterize = 색조 융합
+Deposterize Tip = 업스케일링된 텍스처의 왜곡 현상 고치기
+Device = 장치
+Direct3D 9 = Direct3D 9
+Direct3D 11 = Direct3D 11
+Disable slower effects (speedup) = 느림 효과를 비활성화 (속도 상승)
+Disabled = 비활성화
+Display layout editor = 화면 레이아웃 편집기
+Display Resolution (HW scaler) = 화면 해상도 (하드웨어 스케일러)
+Dump next frame to log = 로그에 다음 프레임 덤프
+Enable Cardboard VR = 구글 카드보드 활성화
+FPS = FPS
+Frame Rate Control = 프레임 비율 제어
+Frame Skipping = 프레임 생략
+Frame Skipping Type = 프레임 생략 유형
+FullScreen = 전체 화면
+Hack Settings = 핵 설정 (이것은 오류의 원인이 됩니다)
+Hardware Tessellation = 하드웨어 테셀레이션
+Hardware Transform = 하드웨어 변환
+hardware transform error - falling back to software = 하드웨어 변환 오류 - 소프트웨어로 전환
+HardwareTessellation Tip = 하드웨어가 곡선을 그리게 하여 언제나 고정된 품질을 사용합니다.
+High = 높음
+Hybrid = 혼합
+Hybrid + Bicubic = 혼합 + 쌍입방
+Ignore camera notch when centering = Ignore camera notch when centering
+Internal Resolution = 내부 해상도
+Lazy texture caching = 텍스처 캐싱을 줄임 (속도 상승)
+Linear = 선형 필터링
+Low = 낮음
+LowCurves = 낮은 품질의 스플라인 및 베지어 곡선 (속도 상승)
+LowCurves Tip = 몇몇 게임에만 사용됩니다. 곡선의 부드러움의 정도를 조정합니다.
+Lower resolution for effects (reduces artifacts) = 효과 해상도 저하 (질감 감소)
+Manual Scaling = 수동 스케일링
+Medium = 중간
+Mode = 방식
+Must Restart = 이 설정을 적용하기 위해서는 PPSSPP를 재시작해 주십시오.
+Native device resolution = 기본 장치 해상도
+Nearest = 근접 필터링
+No buffer = 버퍼 없음
+Non-Buffered Rendering = 버퍼 없는 렌더링 (속도 상승)
+None = 없음
+Number of Frames = 프레임 수
+Off = 끄기
+OpenGL = OpenGL
+Overlay Information = 오버레이 정보
+Partial Stretch = 부분 늘이기
+Percent of FPS = FPS 비율
+Performance = 성능
+Postprocessing effect = Postprocessing effects
+Postprocessing Shader = 후처리 셰이더
+Recreate Activity = Recreate activity
+Render duplicate frames to 60hz = 중복된 프레임을 60Hz로 렌더링
+RenderDuplicateFrames Tip = 낮은 프레임 레이트에서 동작하는 게임을 부드럽게 해 줍니다.
+Rendering Mode = 렌더링 방식
+Rendering Resolution = 렌더링 해상도
+RenderingMode NonBuffered Tip = 빠르지만 몇몇 게임에서는 작동하지 않을 수 있습니다.
+Retain changed textures = 변경된 텍스처를 보유 (속도 감소)
+RetainChangedTextures Tip = 많은 게임들이 이 설정을 적용하면 대부분 느려지지만 몇몇 게임은 더 빨라질 수 있습니다.
+Rotation = 회전
+Safe = 안전
+Screen layout = Screen layout
+Screen Scaling Filter = 화면 크기 조절 필터
+Show Debug Statistics = 디버그 통계 표시
+Show FPS Counter = FPS 카운터 표시
+Simulate Block Transfer = 블록 전송 효과를 시뮬레이션
+SoftGPU Tip = 현재 매우 느림
+Software Rendering = 소프트웨어 렌더링 (실험적)
+Software Skinning = 소프트웨어 스키닝
+SoftwareSkinning Tip = 스키닝 된 모델을 합쳐 CPU에 그립니다. 대부분에 게임이 빨라집니다.
+Speed = 속도
+Stretching = 늘이기
+Texture Filter = 텍스처 필터링
+Texture Filtering = 텍스처 필터링
+Texture Scaling = 텍스처 크기 조절
+Texture Shader = Texture shader
+Turn off Hardware Tessellation - unsupported = "하드웨어 테셀레이션" 끄기: 지원하지 않음
+Unlimited = 무제한
+Up to 1 = 최대 1까지
+Up to 2 = 최대 2까지
+Upscale Level = 확대 수준
+Upscale Type = 확대 종류
+UpscaleLevel Tip = CPU가 할 일이 많아집니다 - 또한 끊김 현상을 줄이기 위해 스케일링이 늦어질 수 있습니다.
+Use all displays = 모든 디스플레이 사용하기
+Vertex Cache = 버텍스 캐시
+VertexCache Tip = 빠르지만 잠시동안 화면이 깜빡거릴 수 있습니다.
+VSync = 수직 동기
+Vulkan = 벌칸(Vulkan)
+Window Size = 창 크기
+xBRZ = xBRZ
+
+[InstallZip]
+Delete ZIP file = ZIP 파일 삭제
+Install = 설치
+Install game from ZIP file? = ZIP 파일로부터 게임을 설치하시겠습니까?
+Install textures from ZIP file? = ZIP 파일로부터 텍스처팩을 설치하시겠습니까?
+Installed! = 설치되었습니다!
+Texture pack doesn't support install = 이 텍스처팩의 설치는 지원하지 않습니다.
+Zip archive corrupt = ZIP 파일이 손상되었습니다.
+Zip file does not contain PSP software = ZIP 파일에 PSP 소프트웨어가 들어있지 않습니다.
+
+[KeyMapping]
+Autoconfigure = 자동 설정
+Autoconfigure for device = 디바이스에 맞게 자동 설정
+Clear All = 모두 지우기
+Default All = 기본값 복원
+Map a new key for = 새로운 키 입력
+Map Key = 키 매핑
+Map Mouse = 마우스 매핑
+Test Analogs = 아날로그 테스트
+You can press ESC to cancel. = 취소하려면 ESC를 눌러주십시오.
+
+[MainMenu]
+Browse = 찾아보기...
+Buy PPSSPP Gold = PPSSPP Gold 구매
+Choose folder = 폴더 선택하기
+Credits = 만든이
+DownloadFromStore = PPSSPP 홈브류 스토어에서 다운로드하십시오.
+Exit = 종료
+Game Settings = 게임 설정
+Games = 게임
+Give PPSSPP permission to access storage = 저장공간에 접근하기 위해 PPSSPP에 권한이 필요합니다.
+Homebrew & Demos = 홈브류 및 데모
+How to get games = 어떻게 게임을 구할 수 있나요?
+How to get homebrew & demos = 어떻게 홈브류 및 데모를 구할 수 있나요?
+Load = 불러오기...
+Loading... = 불러오는 중...
+PinPath = 경로 고정
+PPSSPP can't load games or save right now = 현재 PPSSPP에서 게임을 불러오거나 저장할 수 없습니다
+Recent = 최근 게임
+SavesAreTemporary = 임시 저장장치에 저장하는 중
+SavesAreTemporaryGuidance = 영구적으로 사용하기 위해 PPSSPP를 기억장치에 복사해 주십시오.
+SavesAreTemporaryIgnore = 무시하기
+UnpinPath = 고정 해제
+www.ppsspp.org = www.ppsspp.org
+
+[MainSettings]
+Audio = 오디오
+Controls = 조작
+Graphics = 그래픽
+Networking = 네트워킹
+System = 시스템
+Tools = 도구
+
+[MappableControls]
+Alt speed 1 = 대체 속도 1
+Alt speed 2 = 대체 속도 2
+An.Down = 아날로그.하
+An.Left = 아날로그.좌
+An.Right = 아날로그.우
+An.Up = 아날로그.상
+Analog limiter = 아날로그 제한자
+Analog Stick = 아날로그 스틱
+Audio/Video Recording = 소리/영상 녹화
+Auto Analog Rotation (CCW) = 자동 아날로그 회전 (CCW)
+Auto Analog Rotation (CW) = 자동 아날로그 회전 (CW)
+AxisSwap = 축 스왑
+Circle = 동그라미
+Cross = 십자
+D-pad down = 방향 패드.하
+D-pad left = 방향 패드.좌
+D-pad right = 방향 패드.우
+D-pad up = 방향 패드.상
+DevMenu = 개발 메뉴
+Down = 방향 패드.하
+Dpad = 방향 패드
+Frame Advance = 프레임 건너뛰기
+Hold = 잠금
+Home = 홈
+L = L
+Left = 방향 패드.좌
+Load State = 상태 불러오기
+Mute toggle = 음소거 전환
+Next Slot = 다음 슬롯
+None = 없음
+Note = 참고
+OpenChat = 대화 열기
+Pause = 일시 정지
+R = R
+RapidFire = 연사
+Remote hold = 리모트 잠금
+Rewind = 되감기
+Right = 방향 패드.우
+Right Analog Stick (tap to customize) = 오른쪽 아날로그 스틱 (탭해서 사용자화)
+RightAn.Down = 우측An.하
+RightAn.Left = 우측An.좌
+RightAn.Right = 우측An.우
+RightAn.Up = 우측An.상
+Rotate Analog (CCW) = 아날로그 회전 (CCW)
+Rotate Analog (CW) = 아날로그 회전 (CW)
+Save State = 상태 저장하기
+Screen = 화면
+Screenshot = 스크린샷
+Select = 선택
+SpeedToggle = 속도 전환
+Square = 네모
+Start = 시작
+Texture Dumping = 텍스처 덤프
+Texture Replacement = 텍스처 교체
+Toggle Fullscreen = 전체화면 고정
+Toggle mode = 토글 모드
+Triangle = 세모
+Unthrottle = 터보
+Up = 방향 패드.상
+Vol + = 볼륨 +
+Vol - = 볼륨 -
+Wlan = 무선랜
+
+[Networking]
+Adhoc Multiplayer forum = 애드혹 멀티플레이어 포럼 방문
+AdHoc Server = Ad hoc server
+AdhocServer Failed to Bind Port = Ad hoc server failed to bind port
+Auto = Auto
+Bottom Center = Bottom center
+Bottom Left = Bottom left
+Bottom Right = Bottom right
+Center Left = Center left
+Center Right = Center right
+Change Mac Address = Mac 주소 변경
+Change proAdhocServer Address = PRO 애드혹 서버 IP 주소 변경 (localhost = multiple instances)
+Chat = 대화
+Chat Button Position = 대화 버튼 위치
+Chat Here = 여기에 입력
+Chat Screen Position = 대화 화면 위치
+Disconnected from AdhocServer = Disconnected from ad hoc server
+DNS Error Resolving = DNS error resolving
+Enable built-in PRO Adhoc Server = 내장된 PRO 애드혹 서버 활성화
+Enable network chat = 네트워크 대화 활성화
+Enable networking = 네트워킹/무선 랜 활성화 (베타, 게임 작동을 방해할 수 있음)
+Enable UPnP = Enable UPnP (need a few seconds to detect)
+EnableQuickChat = 빠른 대화 활성화
+Enter a new PSP nickname = 새로운 PSP 별명 입력
+Enter Quick Chat 1 = 빠른 대화 1 참가
+Enter Quick Chat 2 = 빠른 대화 2 참가
+Enter Quick Chat 3 = 빠른 대화 3 참가
+Enter Quick Chat 4 = 빠른 대화 4 참가
+Enter Quick Chat 5 = 빠른 대화 5 참가
+Error = Error
+Failed to Bind Localhost IP = Failed to bind localhost IP
+Failed to Bind Port = Failed to bind port
+Failed to connect to Adhoc Server = Failed to connect to ad hoc server
+Forced First Connect = Forced first connect (faster connect)
+Invalid IP or hostname = 올바르지 않은 IP 또는 호스트 이름
+Minimum Timeout = Minimum timeout (override in ms, 0 = default)
+Misc = Miscellaneous (default = PSP compatibility)
+Network Initialized = 네트워크 초기화됨
+Please change your Port Offset = Please change your port offset
+Port offset = 포트 오프셋(0 = PSP 호환)
+proAdhocServer Address: = Ad hoc server address:
+Quick Chat 1 = 빠른 대화 1
+Quick Chat 2 = 빠른 대화 2
+Quick Chat 3 = 빠른 대화 3
+Quick Chat 4 = 빠른 대화 4
+Quick Chat 5 = 빠른 대화 5
+QuickChat = 빠른 대화
+Send = 보내기
+Send Discord Presence information = Discord 프로필에 PPSSPP 표시
+TCP No Delay = TCP No Delay (faster TCP)
+Top Center = Top center
+Top Left = Top left
+Top Right = Top right
+Unable to find UPnP device = Unable to find UPnP device
+UPnP (port-forwarding) = UPnP (port forwarding)
+UPnP need to be reinitialized = UPnP need to be reinitialized
+UPnP use original port = UPnP use original port (enabled = PSP compatibility)
+Validating address... = 주소 검증하는 중...
+WLAN Channel = WLAN channel
+You're in Offline Mode, go to lobby or online hall = 현재 오프라인 모드입니다. 로비나 온라인 홀로 돌아갑니다.
+
+[Pause]
+Cheats = 치트
+Continue = 계속
+Create Game Config = 게임 설정파일 생성
+Delete Game Config = 게임 설정파일 제거
+Exit to menu = 종료 후 메뉴로
+Game Settings = 게임 설정
+Load State = 상태 불러오기
+Rewind = 되감기
+Save State = 상태 저장하기
+Settings = 설정
+Switch UMD = UMD 교체
+
+[PostShaders]
+(duplicated setting, previous slider will be used) = (duplicated setting, previous slider will be used)
+4xHqGLSL = 4배 HQ GLSL (OpenGL 셰이딩 언어)
+5xBR = 5xBR
+5xBR-lv2 = 5xBR-lv2
+AAColor = AA 색상
+Amount = Amount
+Black border = 검정 테두리
+Bloom = 블룸
+Brightness = 밝기
+Cartoon = 카툰
+ColorCorrection = 색상 교정
+Contrast = 명암
+CRT = CRT 스캔라인
+FXAA = FXAA (Fast Approximate Anti-Aliasing)
+Gamma = 감마
+Grayscale = 그레이스케일
+Intensity = 강도
+InverseColors = 색 반전
+Natural = 자연 색상
+NaturalA = 자연 색상 (블러 없음)
+Off = 끄기
+Power = Power
+PSPColor = PSP color
+Saturation = 채도
+Scanlines = 스캔라인 (CRT)
+Sharpen = 선명하게
+SSAA(Gauss) = 슈퍼샘플링 AA (가우스)
+Tex4xBRZ = 4xBRZ
+UpscaleSpline36 = 스플라인36 확대
+VideoSmoothingAA = VideoSmoothingAA
+Vignette = 비네트
+
+[PSPCredits]
+all the forum mods = 모든 포럼 모드
+build server = 빌드 서버
+Buy Gold = 골드 버전 구매
+check = 최고의 Wii/GC 에뮬 Dolphin 또한 확인하십시오.
+CheckOutPPSSPP = 멋진 PSP 에뮬레이터 PPSSPP를 확인해 보십시오: https://www.ppsspp.org/
+contributors = 도움을 주신 분들:
+created = 만든이:
+Discord = Discord
+info1 = PPSSPP는 교육적인 목적으로만 사용할 수 있습니다.
+info2 = 설치한 게임의 소유 권리가 분명한지를 확인해 주십시오.
+info3 = 해당 게임의 UMD를 소유하거나 디지털로 구매하여 플레이하십시오.
+info4 = 해당 게임을 실제 PSP에 존재하는 PSN Store에서 구매해 주십시오.
+info5 = PSP는 주식회사 소니의 등록 상표입니다.
+iOS builds = iOS 빌드
+license = GPL 2.0+ 라이선스를 따르는 무료 소프트웨어
+list = 호환 목록, 포럼 및 개발 정보
+PPSSPP Forums = PPSSPP 포럼
+Privacy Policy = 개인 정보 정책
+Share PPSSPP = PPSSPP 공유
+specialthanks = 특별히 감사드립니다:
+specialthanksKeithGalocy = - NVIDIA 로부터 (하드웨어, 지도)
+specialthanksMaxim = - 놀라운 Atrac3+ 디코더 지원
+testing = 테스트
+this translation by = 번역:
+title = 빠르고 휴대성이 높은 PSP 에뮬레이터
+tools = 사용된 무료 도구들:
+# Add translators or contributors who translated PPSSPP into your language here.
+# Add translators1-6 for up to 6 lines of translator credits.
+# Leave extra lines blank. 4 contributors per line seems to look best.
+# It is character-set for keyboard(atlas)
+# ㄱㄲㄳㄴㄵㄶㄷㄸㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅃㅄㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎㅏㅐㅑㅒㅓㅔㅕㅖㅗㅛㅜㅠㅡㅣ
+translators1 = mgaver Hisman_Yosika 오원식 MrGiKILL
+translators2 = DDinghoya
+translators3 =
+translators4 =
+translators5 =
+translators6 =
+Twitter @PPSSPP_emu = Twitter @PPSSPP_emu
+website = 웹 사이트에서 확인하세요:
+written = 속도와 이식성을 보장하기 위해 C++로 작성되었습니다.
+
+[RemoteISO]
+Browse Games = 게임 찾아보기
+Local Server Port = 로컬 서버 포트
+Manual Mode Client = 수동 모드 클라이언트
+Remote disc streaming = 원격 디스크 스트리밍
+Remote Port = 원격 포트
+Remote Server = 원격 서버
+Remote Subdirectory = 공유할 하위 폴더
+RemoteISODesc = 최근 게임 목록에 있는 게임들이 공유됩니다.
+RemoteISOLoading = 접속 완료, 게임 목록 불러오는 중...
+RemoteISOScanning = 검색 중... 서버 장치의 "게임 공유하기" 버튼을 눌러주십시오.
+RemoteISOScanningTimeout = 검색 중... 방화벽 설정을 확인하십시오.
+RemoteISOWifi = 팁: 같은 Wi-Fi 네트워크 상에서 서로 접속할 수 있습니다.
+RemoteISOWinFirewall = 경고: Windows 방화벽이 공유를 차단하고 있습니다.
+Settings = 설정
+Share Games (Server) = 게임 공유하기 (서버)
+Share on PPSSPP startup = PPSSPP 시작시 자동으로 공유하기
+Stop Sharing = 공유 끝내기
+Stopping.. = 중지하는 중...
+
+[Reporting]
+Bad = 나쁨
+FeedbackDelayInfo = 피드백 데이터를 전송 중입니다.
+FeedbackDesc = 게임이 어땠나요? 저희에게 알려주십시오!
+FeedbackDisabled = 서버에 호환성 문제 보고를 체크해 주십시오.
+FeedbackIncludeCRC = 참고: 디스크 CRC를 위해 배터리가 사용될 수 있습니다.
+FeedbackIncludeScreen = 스크린샷 포함하기
+FeedbackSubmitDone = 피드백 데이터가 전송되었습니다.
+FeedbackSubmitFail = 서버에 피드백을 전송할 수 없었습니다. PPSSPP를 업데이트해 보십시오.
+FeedbackThanks = 피드백 고맙습니다!
+Gameplay = 게임 플레이
+Graphics = 그래픽
+Great = 좋음
+In-game = 게임 내
+In-game Description = 게임이 실행되었지만 제대로 작동하지 않았습니다
+Menu/Intro = 메뉴/인트로
+Menu/Intro Description = 게임 내에 진입할 수 없었습니다
+Nothing = 전부
+Nothing Description = 전혀 동작하지 않았습니다
+OK = 괜찮음
+Open Browser = 브라우저 열기
+Overall = 평가
+Perfect = 완벽함
+Perfect Description = 게임 전체가 흠 없이 매끄럽게 작동했습니다 - 대박!
+Plays = 플레이 중
+Plays Description = 게임을 즐길 수는 있지만 문제가 있을 수 있습니다
+ReportButton = 피드백 남기기
+Speed = 속도
+Submit Feedback = 피드백 전송
+SuggestionConfig = 적합한 설정을 위해 웹사이트에 올라온 보고서들을 확인해 보십시오.
+SuggestionCPUSpeed0 = 잠긴 CPU 설정을 비활성화해 주십시오.
+SuggestionDowngrade = 구 버전의 PPSSPP로 내려보십시오. (그리고 이 버그를 신고해 주십시오).
+SuggestionsFound = 다른 유저들이 더 나은 결과를 보고했습니다. 피드백 보기를 눌러 확인해 주십시오.
+SuggestionsNone = 다른 유저들도 이 게임이 제대로 작동하지 않았습니다.
+SuggestionsWaiting = 전송 및 다른 유저의 보고서 확인 중...
+SuggestionUpgrade = 새로운 PPSSPP 버전으로 업데이트 하십시오.
+SuggestionVerifyDisc = ISO 파일이 제대로 디스크로부터 복사되었는지 확인해 보십시오.
+Unselected Overall Description = 이 게임이 얼마만큼 잘 작동했나요?
+View Feedback = 모든 피드백 보기
+
+[Savedata]
+Date = 날짜
+Filename = 파일 이름
+No screenshot = 스크린샷 없음
+None yet. Things will appear here after you save. = 아직 없음. 저장하면 나타납니다.
+Save Data = 저장 데이터
+Save States = 상태 저장
+Savedata Manager = 저장 데이터 관리자
+Size = 크기
+
+[Screen]
+Cardboard VR OFF = 구글 카드보드 끄기
+Chainfire3DWarning = 경고: 체인파이어 3D가 검출되었습니다. 문제가 발생할 수 있습니다.
+Failed to load state = 상태 불러오기에 실패했습니다
+Failed to save state = 상태 저장에 실패했습니다
+fixed = 속도: 고정
+GLToolsWarning = 경고: GLTools가 감지되었습니다. 문제가 발생할 수 있습니다.
+In menu = 메뉴
+Load savestate failed = 상태 불러오기 실패
+Loaded State = 상태 불러옴
+Loaded. Save in game, restart, and load for less bugs. = 불러왔습니다. 게임을 저장한 다음 재시작하면 적은 버그로 로드됩니다.
+LoadStateDoesntExist = 상태를 불러오지 못했습니다: 상태 저장이 존재하지 않습니다!
+LoadStateWrongVersion = 상태를 불러오지 못했습니다: 구 버전의 상태 저장입니다!
+norewind = 되감기 저장 상태가 없습니다.
+Playing = 실행
+PressESC = ESC를 누르면 중지 메뉴가 열립니다.
+replaceTextures_false = 텍스처가 더 이상 대체되지 않습니다.
+replaceTextures_true = 텍스처 교체가 활성화되었습니다.
+Save State Failed = 상태를 저장하지 못했습니다!
+Saved State = 상태 저장됨
+saveNewTextures_false = 텍스처 저장이 비활성화되었습니다.
+saveNewTextures_true = 텍스처가 이제 저장 공간에 저장됩니다.
+SpeedCustom2 = 속도: 고정 속도 2
+standard = 속도: 표준
+Untitled PSP game = 이름 없는 PSP 게임
+
+[Store]
+Already Installed = 이미 설치되어 있습니다
+Connection Error = 연결 오류
+Install = 설치
+Launch Game = 게임 실행
+Loading... = 불러오는 중...
+MB = MB
+Size = 크기
+Uninstall = 제거
+
+[SysInfo]
+%d (%d per core, %d cores) = %d (코어 당 %d, 코어 %d개)
+%d bytes = %d 바이트
+(none detected) = (감지되지 않음)
+3D API = 3D API
+ABI = ABI
+API Version = API 버전
+Audio Information = 오디오 정보
+Board = Board
+Build Config = 빌드 설정
+Build Configuration = 빌드 설정
+Built by = 빌드 실행자
+Core Context = 코어 컨텍스트
+Cores = 코어
+CPU Extensions = CPU 확장
+CPU Information = CPU 정보
+CPU Name = 이름
+D3DCompiler Version = D3DCompiler 버전
+Debug = 디버그
+Debugger Present = 디버거 표시
+Device Info = 장치 정보
+Display Information = 디스플레이 정보
+Driver Version = 드라이버 버전
+EGL Extensions = EGL 확장
+Frames per buffer = 버퍼당 프레임
+GPU Information = GPU 정보
+High precision float range = High precision float range
+High precision int range = 정밀한 정수 범위
+Lang/Region = 언어/지역
+Memory Page Size = 메모리 페이지 크기
+Native Resolution = 기본 해상도
+OGL Extensions = OGL 확장
+OpenGL ES 2.0 Extensions = OpenGL ES 2.0 확장
+OpenGL ES 3.0 Extensions = OpenGL ES 3.0 확장
+OpenGL Extensions = OpenGL 확장
+Optimal frames per buffer = 버퍼당 최적 프레임
+Optimal sample rate = 최적 샘플 속도
+OS Information = OS 정보
+PPSSPP build = PPSSPP 빌드
+Refresh rate = 주사율
+Release = 출시
+RW/RX exclusive = RW/RX 독점
+Sample rate = 샘플 속도
+Shading Language = 셰이딩 언어
+Sustained perf mode = 지속 성능 모드
+System Information = 시스템 정보
+System Name = 시스템 이름
+System Version = 시스템 버전
+Threads = 스레드
+Vendor = 공급 업체
+Vendor (detected) = 공급 업체 (감지됨)
+Version Information = 버전 정보
+Vulkan Extensions = 벌칸 확장
+Vulkan Features = 벌칸 기능
+
+[System]
+(broken) = (이용 불가)
+12HR = 12시간
+24HR = 24시간
+Auto = 자동
+Auto Load Savestate = 상태 저장 자동 불러오기
+AVI Dump started. = AVI 덤프 시작됨
+AVI Dump stopped. = AVI 덤프 정지됨
+Cache ISO in RAM = ISO 전체를 램에 캐싱
+Change CPU Clock = CPU 클럭 변경 (불안정)
+Change Memory Stick folder = 메모리 스틱 폴더 변경
+Change Memory Stick Size = Change Memory Stick size (GB)
+Change Nickname = 별명 변경
+ChangingMemstickPath = 게임 저장, 상태 저장, 기타 데이터는 이 폴더에 복사되지 않습니다.\n\n메모리 스틱 폴더를 변경하시겠습니까?
+ChangingMemstickPathInvalid = 이 경로는 메모리 스틱 파일을 저장하는데 사용할 수 없습니다.
+ChangingMemstickPathNotExists = 해당 폴더는 아직 존재하지 않습니다.\n\n게임 저장, 상태 저장, 기타 데이터는 이 폴더에 복사되지 않습니다.\n\n새 메모리 스틱 폴더를 만드시겠습니까?
+Cheats = 치트 (실험적, 포럼 참조)
+Clear Recent = "최근" 지우기
+Clear Recent Games List = 최근 게임 목록 지우기
+Clear UI background = UI 배경화면 초기화
+Confirmation Button = 확인 버튼
+Date Format = 날짜 표시 방식
+Day Light Saving = 서머 타임
+DDMMYYYY = DDMMYYYY
+Decrease size = 크기 줄이기
+Developer Tools = 개발자 도구
+Display Extra Info = 추가 정보 표시
+Display Games on a grid = 그리드에 "게임 탭" 표시
+Display Homebrew on a grid = 그리드에 "홈브류 및 데모 탭" 표시
+Display Recent on a grid = 그리드에 "최근 탭" 표시
+Dynarec (JIT) = 동적 재컴파일 (JIT)
+Emulation = 에뮬레이션
+Enable Cheats = 치트 활성화
+Enable Compatibility Server Reports = 서버에 호환성 문제를 보고
+Failed to load state. Error in the file system. = 상태 저장을 불러올 수 없었습니다. 파일 시스템에 오류가 있습니다.
+Failed to save state. Error in the file system. = 상태 저장을 저장할 수 없었습니다. 파일 시스템에 오류가 있습니다.
+Fast (lag on slow storage) = 빠름 (느린 저장 장치에서 지연 발생)
+Fast Memory = 빠른 메모리 (불안정)
+Force real clock sync (slower, less lag) = 강제 실제 클럭 동기화 (느림, 약간의 지연)
+frames, 0:off = 프레임, 0 = 끄기
+Games list settings = 게임 목록 설정
+General = 일반
+Grid icon size = 그리드 아이콘 크기
+Help the PPSSPP team = PPSSPP 팀 돕기
+Host (bugs, less lag) = 호스트 (버그, 약간의 지연)
+I/O on thread (experimental) = I/O on thread (experimental)
+Ignore bad memory accesses = Ignore bad memory accesses
+Increase size = 크기 늘리기
+Interpreter = 해석기
+IO timing method = I/O 타이밍 방법
+IR Interpreter = IR 인터프리터
+Memory Stick Folder = 메모리 스틱 폴더
+Memory Stick inserted = 메모리 스틱 삽입됨
+MHz, 0:default = MHz, 0 = 기본 값
+MMDDYYYY = MMDDYYYY
+Newest Save = 제일 새로운 저장
+Not a PSP game = PSP 게임이 아님
+Off = 끄기
+Oldest Save = 제일 오래된 저장
+PSP Model = PSP 기종
+PSP Settings = PSP 설정
+PSP-1000 = PSP-1000
+PSP-2000/3000 = PSP-2000/3000
+Record Audio = 소리 녹음하기
+Record Display = 영상 녹화하기
+Reset Recording on Save/Load State = 저장/불러올 때 기록된 것을 재설정하기
+Restore Default Settings = PPSSPP의 설정을 기본으로 복원
+Rewind Snapshot Frequency = 스냅샷 주기 되감기 (mem hog)
+Save path in installed.txt = 경로를 installed.txt에 저장
+Save path in My Documents = 경로를 내 문서에 저장
+Savestate Slot = 상태 저장 슬롯
+Savestate slot backups = 상태 저장 백업
+Screenshots as PNG = PNG 형식으로 스크린샷 저장
+Set UI background... = UI 배경화면 지정하기...
+Show ID = ID 보기
+Show region flag = 지역코드 플래그 보기
+Simulate UMD delays = UMD 지연 시뮬레이션
+Slot 1 = 슬롯 1
+Slot 2 = 슬롯 2
+Slot 3 = 슬롯 3
+Slot 4 = 슬롯 4
+Slot 5 = 슬롯 5
+Storage full = 저장 공간 가득참
+Sustained performance mode = 지속 성능 모드
+Time Format = 시간 표시 방식
+UI = 사용자 인터페이스
+UI Sound = UI sound
+undo %c = %c 백업
+Use Lossless Video Codec (FFV1) = 무손실 비디오 코덱 사용 (FFV1)
+Use O to confirm = 확인 버튼으로 O 사용
+Use output buffer (with overlay) for recording = 기록에 출력 버퍼(오버레이 포함) 사용
+Use system native keyboard = 시스템 기본 키보드 사용
+Use X to confirm = 확인 버튼으로 X 사용
+VersionCheck = PPSSPP의 새로운 버전을 확인
+WARNING: Android battery save mode is on = 주의: 안드로이드 배터리 절약 모드가 켜짐
+WARNING: Battery save mode is on = 주의: 배터리 절약 모드가 켜짐
+YYYYMMDD = YYYYMMDD
+
+[Upgrade]
+Details = Details
+Dismiss = 거절
+Download = 다운로드
+New version of PPSSPP available = 새로운 버전의 PPSSPP를 이용할 수 있습니다.
diff --git a/libretro/Makefile.common b/libretro/Makefile.common
index 704993a645..77fd240274 100644
--- a/libretro/Makefile.common
+++ b/libretro/Makefile.common
@@ -233,6 +233,7 @@ SOURCES_CXX += \
$(COMMONDIR)/Data/Text/WrapText.cpp \
$(COMMONDIR)/File/VFS/VFS.cpp \
$(COMMONDIR)/File/VFS/AssetReader.cpp \
+ $(COMMONDIR)/File/AndroidStorage.cpp \
$(COMMONDIR)/File/DiskFree.cpp \
$(COMMONDIR)/File/Path.cpp \
$(COMMONDIR)/File/PathBrowser.cpp \
@@ -277,8 +278,8 @@ SOURCES_CXX += \
$(COMMONDIR)/Render/TextureAtlas.cpp \
$(COMMONDIR)/Serialize/Serializer.cpp \
$(COMMONDIR)/Thread/ThreadUtil.cpp \
- $(COMMONDIR)/Thread/ThreadPool.cpp \
- $(COMMONDIR)/Thread/PrioritizedWorkQueue.cpp \
+ $(COMMONDIR)/Thread/ParallelLoop.cpp \
+ $(COMMONDIR)/Thread/ThreadManager.cpp \
$(COMMONDIR)/UI/Root.cpp \
$(COMMONDIR)/UI/Screen.cpp \
$(COMMONDIR)/UI/UI.cpp \
@@ -299,6 +300,7 @@ SOURCES_CXX += \
$(COMMONDIR)/OSVersion.cpp \
$(COMMONDIR)/MemoryUtil.cpp \
$(COMMONDIR)/MipsCPUDetect.cpp \
+ $(COMMONDIR)/LogReporting.cpp \
$(COMMONDIR)/SysError.cpp \
$(COMMONDIR)/StringUtils.cpp \
$(COMMONDIR)/TimeUtil.cpp
@@ -425,6 +427,7 @@ SOURCES_CXX += \
$(EXTDIR)/jpge/jpge.cpp \
$(COREDIR)/AVIDump.cpp \
$(COREDIR)/Config.cpp \
+ $(COREDIR)/ControlMapper.cpp \
$(COREDIR)/TextureReplacer.cpp \
$(COREDIR)/Core.cpp \
$(COREDIR)/WaveFile.cpp \
@@ -581,7 +584,7 @@ SOURCES_CXX += \
$(COREDIR)/SaveState.cpp \
$(COREDIR)/Screenshot.cpp \
$(COREDIR)/System.cpp \
- $(COREDIR)/ThreadPools.cpp \
+ $(COREDIR)/ThreadPools.cpp \
$(COREDIR)/Util/BlockAllocator.cpp \
$(COREDIR)/Util/PPGeDraw.cpp \
$(COREDIR)/Util/AudioFormat.cpp \
diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp
index aa2caf6633..35e3f0b86a 100644
--- a/libretro/libretro.cpp
+++ b/libretro/libretro.cpp
@@ -6,6 +6,7 @@
#include
#include
+#include "Common/CPUDetect.h"
#include "Common/Log.h"
#include "Common/LogManager.h"
#include "Common/System/Display.h"
@@ -17,6 +18,7 @@
#include "Common/ConsoleListener.h"
#include "Common/Input/InputState.h"
#include "Common/Thread/ThreadUtil.h"
+#include "Common/Thread/ThreadManager.h"
#include "Common/File/VFS/VFS.h"
#include "Common/File/VFS/AssetReader.h"
@@ -400,6 +402,8 @@ void retro_init(void)
LogManager::Init(&g_Config.bEnableLogging);
+ g_threadManager.Init(cpu_info.num_cores, cpu_info.logical_cpu_count);
+
host = new LibretroHost;
if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &log))
@@ -612,7 +616,7 @@ bool retro_load_game(const struct retro_game_info *game)
if (retro_base_dir.empty())
retro_base_dir = Path(game->path).NavigateUp();
- retro_base_dir /= "PPSSPP";
+ retro_base_dir /= "PPSSPP";
if (retro_save_dir.empty())
retro_save_dir = Path(game->path).NavigateUp();
@@ -671,6 +675,8 @@ void retro_unload_game(void)
delete ctx;
ctx = nullptr;
PSP_CoreParameter().graphicsContext = nullptr;
+
+ g_threadManager.Teardown();
}
void retro_reset(void)
@@ -746,10 +752,12 @@ static void retro_input(void)
}
}
- __CtrlSetAnalogX(input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X) / 32768.0f, CTRL_STICK_LEFT);
- __CtrlSetAnalogY(input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y) / -32768.0f, CTRL_STICK_LEFT);
- __CtrlSetAnalogX(input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X) / 32768.0f, CTRL_STICK_RIGHT);
- __CtrlSetAnalogY(input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y) / -32768.0f, CTRL_STICK_RIGHT);
+ float x_left = input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X) / 32767.0f;
+ float y_left = input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y) / -32767.0f;
+ float x_right = input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X) / 32767.0f;
+ float y_right = input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y) / -32767.0f;
+ __CtrlSetAnalogXY(CTRL_STICK_LEFT, x_left, y_left);
+ __CtrlSetAnalogXY(CTRL_STICK_RIGHT, x_right, y_right);
}
void retro_run(void)
diff --git a/source_assets/image/a.png b/source_assets/image/a.png
new file mode 100644
index 0000000000..524904f292
Binary files /dev/null and b/source_assets/image/a.png differ
diff --git a/source_assets/image/b.png b/source_assets/image/b.png
new file mode 100644
index 0000000000..48b1c2ab39
Binary files /dev/null and b/source_assets/image/b.png differ
diff --git a/source_assets/image/buttons.svg b/source_assets/image/buttons.svg
index 61aa452258..644c2bc86b 100644
--- a/source_assets/image/buttons.svg
+++ b/source_assets/image/buttons.svg
@@ -12,7 +12,7 @@
height="1052.3622047"
id="svg2"
version="1.1"
- inkscape:version="1.0beta2 (5ba348d, 2020-03-28)"
+ inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="buttons.svg">
@@ -1388,6 +1388,198 @@
in2="offset"
id="feComposite5130-3" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A
+ B
+ C
+ D
+ F
+ E
diff --git a/source_assets/image/c.png b/source_assets/image/c.png
new file mode 100644
index 0000000000..bcb7f503d5
Binary files /dev/null and b/source_assets/image/c.png differ
diff --git a/source_assets/image/d.png b/source_assets/image/d.png
new file mode 100644
index 0000000000..9ff0eb4a7d
Binary files /dev/null and b/source_assets/image/d.png differ
diff --git a/source_assets/image/e.png b/source_assets/image/e.png
new file mode 100644
index 0000000000..0d53463ba6
Binary files /dev/null and b/source_assets/image/e.png differ
diff --git a/source_assets/image/f.png b/source_assets/image/f.png
new file mode 100644
index 0000000000..22e16b31e4
Binary files /dev/null and b/source_assets/image/f.png differ
diff --git a/unittest/TestThreadManager.cpp b/unittest/TestThreadManager.cpp
new file mode 100644
index 0000000000..283d6c1947
--- /dev/null
+++ b/unittest/TestThreadManager.cpp
@@ -0,0 +1,82 @@
+#include "Common/Log.h"
+#include "Common/TimeUtil.h"
+#include "Common/Thread/ThreadManager.h"
+#include "Common/Thread/Channel.h"
+#include "Common/Thread/Promise.h"
+#include "Common/Thread/ParallelLoop.h"
+#include "Common/Thread/ThreadUtil.h"
+
+struct ResultObject {
+ bool ok;
+};
+
+ResultObject *ResultProducer() {
+ sleep_ms(250);
+ printf("result produced: thread %d\n", GetCurrentThreadIdForDebug());
+ return new ResultObject{ true };
+}
+
+bool TestMailbox() {
+ Mailbox *mailbox = new Mailbox();
+ mailbox->Send(new ResultObject{ true });
+ ResultObject *data;
+ data = mailbox->Wait();
+ _assert_(data && data->ok);
+ delete data;
+ mailbox->Release();
+ return true;
+}
+
+void rangeFunc(int lower, int upper) {
+ sleep_ms(30);
+ printf(" - range %d-%d (thread %d)\n", lower, upper, GetCurrentThreadIdForDebug());
+}
+
+// This always passes unless something is badly broken, the interesting thing is the
+// logged output.
+bool TestParallelLoop(ThreadManager *threadMan) {
+ printf("tester thread ID: %d\n", GetCurrentThreadIdForDebug());
+
+ printf("waitable test\n");
+ WaitableCounter *waitable = ParallelRangeLoopWaitable(threadMan, rangeFunc, 0, 7, 1);
+ // Can do stuff here if we like.
+ waitable->WaitAndRelease();
+ // Now it's done.
+
+ // Try a loop with stragglers.
+ printf("blocking test #1 [0-65)\n");
+ ParallelRangeLoop(threadMan, rangeFunc, 0, 65, 1);
+ // Try a loop with a relatively large minimum size.
+ printf("blocking test #2 [0-100)\n");
+ ParallelRangeLoop(threadMan, rangeFunc, 0, 100, 40);
+ // Try a loop with minimum size larger than range.
+ printf("waitable test [10-30)\n");
+ WaitableCounter *waitable2 = ParallelRangeLoopWaitable(threadMan, rangeFunc, 10, 30, 40);
+ waitable2->WaitAndRelease();
+ return true;
+}
+
+bool TestThreadManager() {
+ ThreadManager manager;
+ manager.Init(8, 1);
+
+ Promise *object(Promise::Spawn(&manager, &ResultProducer, TaskType::IO_BLOCKING));
+
+ if (!TestParallelLoop(&manager)) {
+ return false;
+ }
+ sleep_ms(1000);
+
+ ResultObject *result = object->BlockUntilReady();
+ if (result) {
+ printf("Got result back!");
+ }
+
+ delete object;
+
+ if (!TestMailbox()) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/unittest/TestX64Emitter.cpp b/unittest/TestX64Emitter.cpp
index 6fe2882859..5ca64f2c1b 100644
--- a/unittest/TestX64Emitter.cpp
+++ b/unittest/TestX64Emitter.cpp
@@ -1,3 +1,7 @@
+#include "ppsspp_config.h"
+
+#if PPSSPP_ARCH(AMD64) || PPSSPP_ARCH(X86)
+
#include "Common/x64Emitter.h"
#include "Core/MIPS/x86/RegCacheFPU.h"
#include "Core/MIPS/x86/Jit.h"
@@ -41,3 +45,11 @@ bool TestX64Emitter() {
PrintLast(emitter);
return true;
}
+
+#else
+
+bool TestX64Emitter() {
+ return true;
+}
+
+#endif
diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp
index 47c48c1eda..7c399f0f9c 100644
--- a/unittest/UnitTest.cpp
+++ b/unittest/UnitTest.cpp
@@ -58,6 +58,8 @@
#include "Core/MIPS/MIPSVFPUUtils.h"
#include "GPU/Common/TextureDecoder.h"
+#include "android/jni/AndroidContentURI.h"
+
#include "unittest/JitHarness.h"
#include "unittest/TestVertexJit.h"
#include "unittest/UnitTest.h"
@@ -75,8 +77,9 @@ bool System_GetPropertyBool(SystemProperty prop) {
switch (prop) {
case SYSPROP_CAN_JIT:
return true;
+ default:
+ return false;
}
- return false;
}
#if PPSSPP_PLATFORM(ANDROID)
@@ -603,6 +606,42 @@ static bool TestPath() {
EXPECT_EQ_STR(Path("C:\\Yo").GetFilename(), std::string("Yo"));
EXPECT_EQ_STR(Path("C:\\Yo\\Lo").GetDirectory(), std::string("C:/Yo"));
EXPECT_EQ_STR(Path("C:\\Yo\\Lo").GetFilename(), std::string("Lo"));
+
+ EXPECT_EQ_STR(Path("/a/b").PathTo(Path("/a/b/c/d/e")), std::string("c/d/e"));
+ EXPECT_EQ_STR(Path("/").PathTo(Path("/home/foo/bar")), std::string("home/foo/bar"));
+
+ return true;
+}
+
+static bool TestAndroidContentURI() {
+ static const char *treeURIString = "content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO";
+ static const char *directoryURIString = "content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO/document/primary%3APSP%20ISO";
+ static const char *fileURIString = "content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO/document/primary%3APSP%20ISO%2FTekken%206.iso";
+
+ AndroidContentURI treeURI;
+ EXPECT_TRUE(treeURI.Parse(std::string(treeURIString)));
+ AndroidContentURI dirURI;
+ EXPECT_TRUE(dirURI.Parse(std::string(directoryURIString)));
+ AndroidContentURI fileURI;
+ EXPECT_TRUE(fileURI.Parse(std::string(fileURIString)));
+
+ std::string lastPart = dirURI.GetLastPart();
+
+ EXPECT_EQ_STR(fileURI.GetLastPart(), std::string("Tekken 6.iso"));
+
+ EXPECT_TRUE(treeURI.TreeContains(fileURI));
+
+ EXPECT_TRUE(fileURI.CanNavigateUp());
+ fileURI.NavigateUp();
+ EXPECT_FALSE(fileURI.CanNavigateUp());
+
+ EXPECT_EQ_STR(fileURI.FilePath(), fileURI.RootPath());
+
+ EXPECT_EQ_STR(fileURI.ToString(), std::string(directoryURIString));
+
+ std::string diff = dirURI.PathTo(fileURI);
+ EXPECT_EQ_STR(diff, std::string("Tekken 6.iso"));
+
return true;
}
@@ -618,6 +657,7 @@ bool TestArmEmitter();
bool TestArm64Emitter();
bool TestX64Emitter();
bool TestShaderGenerators();
+bool TestThreadManager();
TestItem availableTests[] = {
#if PPSSPP_ARCH(ARM64) || PPSSPP_ARCH(AMD64) || PPSSPP_ARCH(X86)
@@ -643,6 +683,8 @@ TestItem availableTests[] = {
TEST_ITEM(MemMap),
TEST_ITEM(ShaderGenerators),
TEST_ITEM(Path),
+ TEST_ITEM(AndroidContentURI),
+ TEST_ITEM(ThreadManager),
};
int main(int argc, const char *argv[]) {
diff --git a/unittest/UnitTests.vcxproj b/unittest/UnitTests.vcxproj
index 0b73c9224a..0a7e620e41 100644
--- a/unittest/UnitTests.vcxproj
+++ b/unittest/UnitTests.vcxproj
@@ -383,6 +383,7 @@
true
+
diff --git a/unittest/UnitTests.vcxproj.filters b/unittest/UnitTests.vcxproj.filters
index 511e700f67..d11a9119ca 100644
--- a/unittest/UnitTests.vcxproj.filters
+++ b/unittest/UnitTests.vcxproj.filters
@@ -12,6 +12,7 @@
Windows
+