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 +#include "Common/Thread/ParallelLoop.h" #include "Core/CoreTiming.h" #include "Core/Debugger/MemBlockInfo.h" #include "Core/HLE/HLE.h" @@ -431,7 +432,7 @@ void __KernelMemoryInit() MemBlockInfoInit(); kernelMemory.Init(PSP_GetKernelMemoryBase(), PSP_GetKernelMemoryEnd() - PSP_GetKernelMemoryBase(), false); userMemory.Init(PSP_GetUserMemoryBase(), PSP_GetUserMemoryEnd() - PSP_GetUserMemoryBase(), false); - GlobalThreadPool::Memset(Memory::GetPointer(PSP_GetKernelMemoryBase()), 0, PSP_GetUserMemoryEnd() - PSP_GetKernelMemoryBase()); + ParallelMemset(&g_threadManager, Memory::GetPointer(PSP_GetKernelMemoryBase()), 0, PSP_GetUserMemoryEnd() - PSP_GetKernelMemoryBase()); NotifyMemInfo(MemBlockFlags::WRITE, PSP_GetKernelMemoryBase(), PSP_GetUserMemoryEnd() - PSP_GetKernelMemoryBase(), "MemInit"); INFO_LOG(SCEKERNEL, "Kernel and user memory pools initialized"); diff --git a/Core/HLE/sceKernelThread.cpp b/Core/HLE/sceKernelThread.cpp index c8732fd74e..5183bec0f6 100644 --- a/Core/HLE/sceKernelThread.cpp +++ b/Core/HLE/sceKernelThread.cpp @@ -1345,7 +1345,7 @@ int __KernelGetThreadExitStatus(SceUID threadID) { if (t->nt.status == THREADSTATUS_DORMANT) { return hleLogSuccessI(SCEKERNEL, t->nt.exitStatus); } - return hleLogDebug(SCEKERNEL, SCE_KERNEL_ERROR_NOT_DORMANT, "not dormant"); + return hleLogVerbose(SCEKERNEL, SCE_KERNEL_ERROR_NOT_DORMANT, "not dormant"); } int sceKernelGetThreadExitStatus(SceUID threadID) { diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 2994bca069..e1f5b69867 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -1303,10 +1303,16 @@ static int sceNetAdhocPdpCreate(const char *mac, int port, int bufferSize, u32 f if (isLocalServer) { getLocalIp(&addr); } - - addr.sin_port = htons(static_cast(port + static_cast(portOffset))); - // The port might be under 1024 (ie. GTA:VCS use port 1, Ford Street Racing use port 0 (UNUSED_PORT), etc) and already used by other application/host OS, should we add 1024 to the port whenever it tried to use an already used port? - + uint16_t requestedport = static_cast(port + static_cast(portOffset)); + // Avoid getting random port due to port offset when original port wasn't 0 (ie. original_port + port_offset = 65536 = 0) + if (requestedport == 0 && port > 0) + requestedport = 65535; // Hopefully it will be safe to default it to 65535 since there can't be more than one port that can bumped into 65536 + // Show a warning about privileged ports + if (requestedport != 0 && requestedport < 1024) { + WARN_LOG(SCENET, "sceNetAdhocPdpCreate - Ports below 1024(ie. %hu) may require Admin Privileges", requestedport); + } + addr.sin_port = htons(requestedport); + // Bound Socket to local Port int iResult = bind(usocket, (struct sockaddr*)&addr, sizeof(addr)); @@ -1318,9 +1324,10 @@ static int sceNetAdhocPdpCreate(const char *mac, int port, int bufferSize, u32 f // Update sport with the port assigned internal->lport = ntohs(local.sin_port) socklen_t len = sizeof(addr); if (getsockname(usocket, (struct sockaddr*)&addr, &len) == 0) { - if (port + static_cast(portOffset) > 65535 || static_cast(ntohs(addr.sin_port)) - static_cast(portOffset) <= 0) - WARN_LOG(SCENET, "sceNetAdhocPdpCreate - Shifting to Negative Port: %d -> %d -> %d", port, port + portOffset, ntohs(addr.sin_port) - portOffset); - port = ntohs(addr.sin_port) - portOffset; + uint16_t boundport = ntohs(addr.sin_port); + if (port + static_cast(portOffset) >= 65536 || static_cast(boundport) - static_cast(portOffset) <= 0) + WARN_LOG(SCENET, "sceNetAdhocPdpCreate - Wrapped Port Detected: Original(%d) -> Requested(%d), Bound(%d) -> BoundOriginal(%d)", port, requestedport, boundport, boundport - portOffset); + port = boundport - portOffset; } // Allocate Memory for Internal Data @@ -1526,7 +1533,7 @@ static int sceNetAdhocPdpSend(int id, const char *mac, u32 port, void *data, int // Free Network Lock //_freeNetworkLock(); - hleEatMicro(1000); // Can be longer than 1ms tho + hleEatMicro(50); // Can be longer than 1ms tho // Sent Data if (sent >= 0) { DEBUG_LOG(SCENET, "sceNetAdhocPdpSend[%i:%u]: Sent %u bytes to %s:%u\n", id, getLocalPort(pdpsocket.id), sent, ip2str(target.sin_addr).c_str(), ntohs(target.sin_port)); @@ -1537,7 +1544,7 @@ static int sceNetAdhocPdpSend(int id, const char *mac, u32 port, void *data, int // Non-Blocking if (flag) - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); // Does PDP can Timeout? There is no concept of Timeout when sending UDP due to no ACK, but might happen if the socket buffer is full, not sure about PDP since some games did use the timeout arg return hleLogDebug(SCENET, ERROR_NET_ADHOC_TIMEOUT, "timeout?"); // ERROR_NET_ADHOC_INVALID_ADDR; @@ -1618,7 +1625,7 @@ static int sceNetAdhocPdpSend(int id, const char *mac, u32 port, void *data, int // Free Network Lock //_freeNetworkLock(); - hleEatMicro(1000); + hleEatMicro(50); // Success, Broadcast never fails! return 0; // len; } @@ -1761,7 +1768,7 @@ static int sceNetAdhocPdpRecv(int id, void *addr, void * port, void *buf, void * // Free Network Lock //_freeNetworkLock(); - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_NOT_ENOUGH_SPACE, "not enough space"); //received; + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_NOT_ENOUGH_SPACE, "not enough space"); //received; } sinlen = sizeof(sin); memset(&sin, 0, sinlen); @@ -1776,10 +1783,10 @@ static int sceNetAdhocPdpRecv(int id, void *addr, void * port, void *buf, void * return WaitBlockingAdhocSocket(threadSocketId, PDP_RECV, id, buf, len, timeout, saddr, sport, "pdp recv"); } - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); } - hleEatMicro(1000); + hleEatMicro(50); // Received Data. UDP can also receives 0 data, while on TCP 0 data = connection gracefully closed, but not sure about PDP tho if (received >= 0) { DEBUG_LOG(SCENET, "sceNetAdhocPdpRecv[%i:%u]: Received %u bytes from %s:%u\n", id, getLocalPort(pdpsocket.id), received, ip2str(sin.sin_addr).c_str(), ntohs(sin.sin_port)); @@ -2024,7 +2031,7 @@ int sceNetAdhocPollSocket(u32 socketStructAddr, int count, int timeout, int nonb } } // Workaround to get 30 FPS instead of the too fast 60 FPS on Fate Unlimited Codes, it's abit absurd for a non-blocking call to have this much delay tho, and hleDelayResult doesn't works as good as hleEatMicro for this workaround. - hleEatMicro(1000); // hleEatMicro(7500); // normally 1ms, but using 7.5ms here seems to show better result for Bleach Heat the Soul 7 and other games with too high FPS, but may have a risk of slowing down games that already runs at normal FPS? (need more games to test this) + hleEatMicro(50); // hleEatMicro(7500); // normally 1ms, but using 7.5ms here seems to show better result for Bleach Heat the Soul 7 and other games with too high FPS, but may have a risk of slowing down games that already runs at normal FPS? (need more games to test this) return hleLogDebug(SCENET, affectedsockets, "success"); } //else if (nonblock && affectedsockets < 0) @@ -2268,7 +2275,7 @@ int sceNetAdhocctlGetScanInfo(u32 sizeAddr, u32 bufAddr) { // Multithreading Unlock peerlock.unlock(); - hleEatMicro(2000); + hleEatMicro(200); // Return Success return 0; } @@ -2715,7 +2722,7 @@ int sceNetAdhocctlCreate(const char *groupName) { adhocctlCurrentMode = ADHOCCTL_MODE_NORMAL; adhocConnectionType = ADHOC_CREATE; - return NetAdhocctl_Create(groupName); + return hleLogDebug(SCENET, NetAdhocctl_Create(groupName), ""); } int sceNetAdhocctlConnect(const char* groupName) { @@ -2729,7 +2736,7 @@ int sceNetAdhocctlConnect(const char* groupName) { adhocctlCurrentMode = ADHOCCTL_MODE_NORMAL; adhocConnectionType = ADHOC_CONNECT; - return NetAdhocctl_Create(groupName); + return hleLogDebug(SCENET, NetAdhocctl_Create(groupName), ""); } int sceNetAdhocctlJoin(u32 scanInfoAddr) { @@ -2753,7 +2760,7 @@ int sceNetAdhocctlJoin(u32 scanInfoAddr) { // TODO: Adhoc Server may need to be changed to differentiate between Host/Create and Join, otherwise it can't support multiple Host using the same Group name, thus causing one of the Host to be confused being treated as Join. adhocctlCurrentMode = ADHOCCTL_MODE_NORMAL; adhocConnectionType = ADHOC_JOIN; - return NetAdhocctl_Create(grpName); + return hleLogDebug(SCENET, NetAdhocctl_Create(grpName), ""); } // Invalid Argument @@ -3090,7 +3097,7 @@ static int sceNetAdhocGetPtpStat(u32 structSize, u32 structAddr) { // Update Buffer Length *buflen = i * sizeof(SceNetAdhocPtpStat); - hleEatMicro(1000); // Not sure how long it takes, since GetPtpStat didn't get logged when using prx files on JPCSP + hleEatMicro(50); // Not sure how long it takes, since GetPtpStat didn't get logged when using prx files on JPCSP // Success return 0; } @@ -3182,16 +3189,25 @@ static int sceNetAdhocPtpOpen(const char *srcmac, int sport, const char *dstmac, if (isLocalServer) { getLocalIp(&addr); } - addr.sin_port = htons(static_cast(sport + static_cast(portOffset))); + uint16_t requestedport = static_cast(sport + static_cast(portOffset)); + // Avoid getting random port due to port offset when original port wasn't 0 (ie. original_port + port_offset = 65536 = 0) + if (requestedport == 0 && sport > 0) + requestedport = 65535; // Hopefully it will be safe to default it to 65535 since there can't be more than one port that can bumped into 65536 + // Show a warning about privileged ports + if (requestedport != 0 && requestedport < 1024) { + WARN_LOG(SCENET, "sceNetAdhocPtpOpen - Ports below 1024(ie. %hu) may require Admin Privileges", requestedport); + } + addr.sin_port = htons(requestedport); // Bound Socket to local Port if (bind(tcpsocket, (struct sockaddr*)&addr, sizeof(addr)) == 0) { // Update sport with the port assigned internal->lport = ntohs(local.sin_port) socklen_t len = sizeof(addr); if (getsockname(tcpsocket, (struct sockaddr*)&addr, &len) == 0) { - if (sport + static_cast(portOffset) > 65535 || static_cast(ntohs(addr.sin_port)) - static_cast(portOffset) <= 0) - WARN_LOG(SCENET, "sceNetAdhocPtpOpen - Shifting to Negative Port: %d -> %d -> %d", sport, sport + portOffset, ntohs(addr.sin_port) - portOffset); - sport = ntohs(addr.sin_port) - portOffset; + uint16_t boundport = ntohs(addr.sin_port); + if (sport + static_cast(portOffset) >= 65536 || static_cast(boundport) - static_cast(portOffset) <= 0) + WARN_LOG(SCENET, "sceNetAdhocPtpOpen - Wrapped Port Detected: Original(%d) -> Requested(%d), Bound(%d) -> BoundOriginal(%d)", sport, requestedport, boundport, boundport - portOffset); + sport = boundport - portOffset; } // Allocate Memory @@ -3431,7 +3447,7 @@ static int sceNetAdhocPtpAccept(int id, u32 peerMacAddrPtr, u32 peerPortPtr, int // Listener Socket if (ptpsocket.state == ADHOC_PTP_STATE_LISTEN) { - hleEatMicro(500); + hleEatMicro(50); // Address Information struct sockaddr_in peeraddr; memset(&peeraddr, 0, sizeof(peeraddr)); @@ -3467,26 +3483,26 @@ static int sceNetAdhocPtpAccept(int id, u32 peerMacAddrPtr, u32 peerPortPtr, int // Action would block if (flag) - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); // Timeout - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_TIMEOUT, "timeout"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_TIMEOUT, "timeout"); } // Client Socket - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_NOT_LISTENED, "not listened"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_NOT_LISTENED, "not listened"); } // Invalid Socket - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_INVALID_SOCKET_ID, "invalid socket id"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_INVALID_SOCKET_ID, "invalid socket id"); } // Invalid Arguments - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_INVALID_ARG, "invalid arg"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_INVALID_ARG, "invalid arg"); } // Library is uninitialized - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_NOT_INITIALIZED, "not initialized"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_NOT_INITIALIZED, "not initialized"); } int NetAdhocPtp_Connect(int id, int timeout, int flag, bool allowForcedConnect) { @@ -3512,7 +3528,7 @@ int NetAdhocPtp_Connect(int id, int timeout, int flag, bool allowForcedConnect) // Valid Client Socket if (ptpsocket.state == ADHOC_PTP_STATE_CLOSED || ptpsocket.state == ADHOC_PTP_STATE_SYN_SENT) { - hleEatMicro(500); + hleEatMicro(50); // Target Address struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); @@ -3744,7 +3760,15 @@ static int sceNetAdhocPtpListen(const char *srcmac, int sport, int bufsize, int if (isLocalServer) { getLocalIp(&addr); } - addr.sin_port = htons(static_cast(sport + static_cast(portOffset))); + uint16_t requestedport = static_cast(sport + static_cast(portOffset)); + // Avoid getting random port due to port offset when original port wasn't 0 (ie. original_port + port_offset = 65536 = 0) + if (requestedport == 0 && sport > 0) + requestedport = 65535; // Hopefully it will be safe to default it to 65535 since there can't be more than one port that can bumped into 65536 + // Show a warning about privileged ports + if (requestedport != 0 && requestedport < 1024) { + WARN_LOG(SCENET, "sceNetAdhocPtpListen - Ports below 1024(ie. %hu) may require Admin Privileges", requestedport); + } + addr.sin_port = htons(requestedport); int iResult = 0; // Bound Socket to local Port @@ -3752,9 +3776,10 @@ static int sceNetAdhocPtpListen(const char *srcmac, int sport, int bufsize, int // Update sport with the port assigned internal->lport = ntohs(local.sin_port) socklen_t len = sizeof(addr); if (getsockname(tcpsocket, (struct sockaddr*)&addr, &len) == 0) { - if (sport + static_cast(portOffset) > 65535 || static_cast(ntohs(addr.sin_port)) - static_cast(portOffset) <= 0) - WARN_LOG(SCENET, "sceNetAdhocPtpListen - Shifting to Negative Port: %d -> %d -> %d", sport, sport + portOffset, ntohs(addr.sin_port) - portOffset); - sport = ntohs(addr.sin_port) - portOffset; + uint16_t boundport = ntohs(addr.sin_port); + if (sport + static_cast(portOffset) >= 65536 || static_cast(boundport) - static_cast(portOffset) <= 0) + WARN_LOG(SCENET, "sceNetAdhocPtpListen - Wrapped Port Detected: Original(%d) -> Requested(%d), Bound(%d) -> BoundOriginal(%d)", sport, requestedport, boundport, boundport - portOffset); + sport = boundport - portOffset; } // Switch into Listening Mode if ((iResult = listen(tcpsocket, backlog)) == 0) { @@ -3894,7 +3919,7 @@ static int sceNetAdhocPtpSend(int id, u32 dataAddr, u32 dataSizeAddr, int timeou // Success if (sent > 0) { - hleEatMicro(1000); // mostly 1ms, sometimes 1~10ms ? doesn't seems to be switching to a different thread during this duration + hleEatMicro(50); // mostly 1ms, sometimes 1~10ms ? doesn't seems to be switching to a different thread during this duration // Save Length *len = sent; @@ -3912,7 +3937,7 @@ static int sceNetAdhocPtpSend(int id, u32 dataAddr, u32 dataSizeAddr, int timeou else if (sent == SOCKET_ERROR && (error == EAGAIN || error == EWOULDBLOCK || (ptpsocket.state == ADHOC_PTP_STATE_SYN_SENT && (error == ENOTCONN || connectInProgress(error))))) { // Non-Blocking if (flag) - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); // Simulate blocking behaviour with non-blocking socket u64 threadSocketId = ((u64)__KernelGetCurThread()) << 32 | ptpsocket.id; @@ -4001,13 +4026,13 @@ static int sceNetAdhocPtpRecv(int id, u32 dataAddr, u32 dataSizeAddr, int timeou return WaitBlockingAdhocSocket(threadSocketId, PTP_RECV, id, buf, len, timeout, nullptr, nullptr, "ptp recv"); } - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); } // Free Network Lock // _freeNetworkLock(); - hleEatMicro(1000); + hleEatMicro(50); // Received Data if (received > 0) { @@ -4103,7 +4128,7 @@ static int sceNetAdhocPtpFlush(int id, int timeout, int nonblock) { // Connected Socket if (ptpsocket.state == ADHOC_PTP_STATE_ESTABLISHED) { - hleEatMicro(1000); + hleEatMicro(50); // There are two ways to flush, you can either set TCP_NODELAY to 1 or TCP_CORK to 0. // Apply Send Timeout Settings to Socket setSockTimeout(ptpsocket.id, SO_SNDTIMEO, timeout); @@ -4113,7 +4138,7 @@ static int sceNetAdhocPtpFlush(int id, int timeout, int nonblock) { if (error == EAGAIN || error == EWOULDBLOCK) { // Non-Blocking if (nonblock) - return hleLogSuccessVerboseI(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); + return hleLogSuccessVerboseX(SCENET, ERROR_NET_ADHOC_WOULD_BLOCK, "would block"); // Simulate blocking behaviour with non-blocking socket u64 threadSocketId = ((u64)__KernelGetCurThread()) << 32 | ptpsocket.id; @@ -4264,7 +4289,7 @@ static int sceNetAdhocGameModeUpdateMaster() { gma.dataSent = 0; } - hleEatMicro(1000); + hleEatMicro(100); return 0; } @@ -4329,7 +4354,7 @@ static int sceNetAdhocGameModeUpdateReplica(int id, u32 infoAddr) { } } - hleEatMicro(1000); + hleEatMicro(100); return 0; } @@ -5297,7 +5322,7 @@ static int sceNetAdhocMatchingGetMembers(int matchingId, u32 sizeAddr, u32 buf) } // Return Success - return hleDelayResult(0, "delay 1 ~ 10ms", 1000); // seems to have different thread running within the delay duration + return hleDelayResult(0, "delay 100 ~ 1000us", 100); // seems to have different thread running within the delay duration } // Gran Turismo may replace the 1st bit of the 1st byte of MAC address's OUI with 0 (unicast bit), or replace the whole 6-bytes of MAC address with all 00 (invalid mac) for unknown reason @@ -5684,7 +5709,7 @@ static int sceNetAdhocctlGetGameModeInfo(u32 infoAddr) { break; } - hleEatMicro(1000); + hleEatMicro(100); return 0; } @@ -5771,7 +5796,7 @@ static int sceNetAdhocctlGetPeerList(u32 sizeAddr, u32 bufAddr) { peerlock.unlock(); // Return Success - return hleDelayResult(0, "delay 1 ~ 10ms", 1000); // seems to have different thread running within the delay duration + return hleDelayResult(0, "delay 100 ~ 1000us", 100); // seems to have different thread running within the delay duration } // Invalid Arguments @@ -5888,7 +5913,7 @@ static int sceNetAdhocctlGetAddrByName(const char *nickName, u32 sizeAddr, u32 b peerlock.unlock(); // Return Success - return hleLogDebug(SCENET, hleDelayResult(0, "delay 1 ~ 10ms", 1000), "success"); // FIXME: Might have similar delay with GetPeerList? need to know which games using this tho + return hleLogDebug(SCENET, hleDelayResult(0, "delay 100 ~ 1000us", 100), "success"); // FIXME: Might have similar delay with GetPeerList? need to know which games using this tho } // Invalid Arguments @@ -5925,7 +5950,7 @@ const HLEFunction sceNetAdhocctl[] = { }; int sceNetAdhocDiscoverInitStart() { - ERROR_LOG(SCENET, "UNIMPL sceNetAdhocDiscoverInitStart()"); + ERROR_LOG_REPORT_ONCE(sceNetAdhocDiscoverInitStart, SCENET, "UNIMPL sceNetAdhocDiscoverInitStart() at %08x", currentMIPS->pc); return 0; } @@ -5940,13 +5965,13 @@ int sceNetAdhocDiscoverTerm() { } int sceNetAdhocDiscoverUpdate() { - ERROR_LOG(SCENET, "UNIMPL sceNetAdhocDiscoverUpdate()"); + ERROR_LOG(SCENET, "UNIMPL sceNetAdhocDiscoverUpdate() at %08x", currentMIPS->pc); return 0; } int sceNetAdhocDiscoverGetStatus() { ERROR_LOG(SCENET, "UNIMPL sceNetAdhocDiscoverGetStatus()"); - return 0; + return 0; // returning 2 will trigger Legend Of The Dragon to call sceNetAdhocctlGetPeerList } int sceNetAdhocDiscoverRequestSuspend() diff --git a/Core/Loaders.cpp b/Core/Loaders.cpp index 4ad17643d4..226cb597c3 100644 --- a/Core/Loaders.cpp +++ b/Core/Loaders.cpp @@ -337,7 +337,7 @@ bool LoadFile(FileLoader **fileLoaderPtr, std::string *error_string) { case IdentifiedFileType::UNKNOWN_ELF: case IdentifiedFileType::UNKNOWN: default: - ERROR_LOG(LOADER, "Failed to identify file"); + ERROR_LOG(LOADER, "Failed to identify file: %s", fileLoader->GetPath().c_str()); *error_string = "Failed to identify file"; break; } diff --git a/Core/MemMap.cpp b/Core/MemMap.cpp index ab674e569c..34ec45e706 100644 --- a/Core/MemMap.cpp +++ b/Core/MemMap.cpp @@ -43,7 +43,7 @@ #include "Core/MIPS/MIPS.h" #include "Core/MIPS/JitCommon/JitBlockCache.h" #include "Core/MIPS/JitCommon/JitCommon.h" -#include "Core/ThreadPools.h" +#include "Common/Thread/ParallelLoop.h" #include "UI/OnScreenDisplay.h" namespace Memory { @@ -213,7 +213,10 @@ bool MemoryMap_Setup(u32 flags) { } // Grab some pagefile backed memory out of the void ... - g_arena.GrabLowMemSpace(total_mem); + if (!g_arena.GrabMemSpace(total_mem)) { + // It'll already have logged. + return false; + } #endif #if !PPSSPP_PLATFORM(ANDROID) @@ -318,19 +321,19 @@ static void DoMemoryVoid(PointerWrap &p, uint32_t start, uint32_t size) { switch (p.mode) { case PointerWrap::MODE_READ: - GlobalThreadPool::Memcpy(d, storage, size); + ParallelMemcpy(&g_threadManager, d, storage, size); break; case PointerWrap::MODE_WRITE: - GlobalThreadPool::Memcpy(storage, d, size); + ParallelMemcpy(&g_threadManager, storage, d, size); break; case PointerWrap::MODE_MEASURE: // Nothing to do here. break; case PointerWrap::MODE_VERIFY: - GlobalThreadPool::Loop([&](int l, int h) { + ParallelRangeLoop(&g_threadManager, [&](int l, int h) { for (int i = l; i < h; i++) _dbg_assert_msg_(d[i] == storage[i], "Savestate verification failure: %d (0x%X) (at %p) != %d (0x%X) (at %p).\n", d[i], d[i], &d[i], storage[i], storage[i], &storage[i]); - }, 0, size); + }, 0, size, 128); break; } storage += size; diff --git a/Core/Reporting.cpp b/Core/Reporting.cpp index 72edac0ed6..4e69f353ee 100644 --- a/Core/Reporting.cpp +++ b/Core/Reporting.cpp @@ -60,9 +60,6 @@ namespace Reporting static u32 spamProtectionCount = 0; // Temporarily stores a reference to the hostname. static std::string lastHostname; - // Keeps track of report-only-once identifiers. Since they're always constants, a pointer is okay. - static std::map logNTimes; - static std::mutex logNTimesLock; // Keeps track of whether a harmful setting was ever used. static bool everUnsupported = false; @@ -331,14 +328,18 @@ namespace Reporting #endif } + bool MessageAllowed(); + void SendReportMessage(const char *message, const char *formatted); + void Init() { // New game, clean slate. spamProtectionCount = 0; - logNTimes.clear(); + ResetCounts(); everUnsupported = false; currentSupported = IsSupported(); pendingMessagesDone = false; + Reporting::SetupCallbacks(&MessageAllowed, &SendReportMessage); } void Shutdown() @@ -377,29 +378,6 @@ namespace Reporting everUnsupported = true; } - 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(); - } - std::string CurrentGameID() { // TODO: Maybe ParamSFOData shouldn't include nulls in std::strings? Don't work to break savedata, though... @@ -639,41 +617,13 @@ namespace Reporting return 0; } - void ReportMessage(const char *message, ...) - { + bool MessageAllowed() { if (!IsEnabled() || CheckSpamLimited()) - return; - int pos = NextFreePos(); - if (pos == -1) - 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); - - Payload &payload = payloadBuffer[pos]; - payload.type = RequestType::MESSAGE; - payload.string1 = message; - payload.string2 = temp; - - std::lock_guard guard(pendingMessageLock); - pendingMessages.push_back(pos); - pendingMessageCond.notify_one(); - - if (!messageThread.joinable()) { - messageThread = std::thread(ProcessPending); - } + return false; + return true; } - void ReportMessageFormatted(const char *message, const char *formatted) - { - if (!IsEnabled() || CheckSpamLimited()) - return; + void SendReportMessage(const char *message, const char *formatted) { int pos = NextFreePos(); if (pos == -1) return; diff --git a/Core/Reporting.h b/Core/Reporting.h index a1b04ab86f..fb16689bf8 100644 --- a/Core/Reporting.h +++ b/Core/Reporting.h @@ -22,29 +22,7 @@ #include "Common/CommonTypes.h" #include "Common/File/Path.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) +#include "Common/LogReporting.h" class PointerWrap; @@ -54,9 +32,6 @@ namespace Reporting void Init(); void Shutdown(); - // Resets counts on any count-limited logs (see ShouldLogNTimes). - void ResetCounts(); - // Check savestate compatibility, mostly needed on load. void DoState(PointerWrap &p); @@ -76,12 +51,6 @@ namespace Reporting // Use the default reporting setting (per compiled settings) of host and enabled state. void EnableDefault(); - // 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); - // Report the compatibility of the current game / configuration. void ReportCompatibility(const char *compat, int graphics, int speed, int gameplay, const std::string &screenshotFilename); @@ -98,9 +67,6 @@ namespace Reporting // To avoid stalling, call HasCRC() in update() or similar and call this if it returns true. uint32_t RetrieveCRC(const Path &gamePath); - // Returns true if that identifier has not been logged yet. - bool ShouldLogNTimes(const char *identifier, int n); - enum class ReportStatus { WORKING, BUSY, diff --git a/Core/System.cpp b/Core/System.cpp index d47407f6d0..808e39ff76 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -581,38 +581,47 @@ CoreParameter &PSP_CoreParameter() { } Path GetSysDirectory(PSPDirectories directoryType) { + Path memStickDirectory = g_Config.memStickDirectory; + Path pspDirectory; + if (memStickDirectory.GetFilename() == "PSP") { + // Let's strip this off, to easily allow choosing a root directory named "PSP" on Android. + pspDirectory = memStickDirectory; + } else { + pspDirectory = memStickDirectory / "PSP"; + } + switch (directoryType) { case DIRECTORY_CHEATS: - return g_Config.memStickDirectory / "PSP/Cheats"; + return pspDirectory / "Cheats"; case DIRECTORY_GAME: - return g_Config.memStickDirectory / "PSP/GAME"; + return pspDirectory / "GAME"; case DIRECTORY_SAVEDATA: - return g_Config.memStickDirectory / "PSP/SAVEDATA"; + return pspDirectory / "SAVEDATA"; case DIRECTORY_SCREENSHOT: - return g_Config.memStickDirectory / "PSP/SCREENSHOT"; + return pspDirectory / "SCREENSHOT"; case DIRECTORY_SYSTEM: - return g_Config.memStickDirectory / "PSP/SYSTEM"; + return pspDirectory / "SYSTEM"; case DIRECTORY_PAUTH: - return g_Config.memStickDirectory / "PAUTH"; + return memStickDirectory / "PAUTH"; // This one's at the root... case DIRECTORY_DUMP: - return g_Config.memStickDirectory / "PSP/SYSTEM/DUMP"; + return pspDirectory / "SYSTEM/DUMP"; case DIRECTORY_SAVESTATE: - return g_Config.memStickDirectory / "PSP/PPSSPP_STATE"; + return pspDirectory / "PPSSPP_STATE"; case DIRECTORY_CACHE: - return g_Config.memStickDirectory / "PSP/SYSTEM/CACHE"; + return pspDirectory / "SYSTEM/CACHE"; case DIRECTORY_TEXTURES: - return g_Config.memStickDirectory / "PSP/TEXTURES"; + return pspDirectory / "TEXTURES"; case DIRECTORY_PLUGINS: - return g_Config.memStickDirectory / "PSP/PLUGINS"; + return pspDirectory / "PLUGINS"; case DIRECTORY_APP_CACHE: if (!g_Config.appCacheDirectory.empty()) { return g_Config.appCacheDirectory; } - return g_Config.memStickDirectory / "PSP/SYSTEM/CACHE"; + return pspDirectory / "SYSTEM/CACHE"; case DIRECTORY_VIDEO: - return g_Config.memStickDirectory / "PSP/VIDEO"; + return pspDirectory / "VIDEO"; case DIRECTORY_AUDIO: - return g_Config.memStickDirectory / "PSP/AUDIO"; + return pspDirectory / "AUDIO"; case DIRECTORY_MEMSTICK_ROOT: return g_Config.memStickDirectory; // Just return the memory stick root if we run into some sort of problem. diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index b692072d4d..3e7f3dcdec 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -30,6 +30,7 @@ #include "Common/Data/Text/Parsers.h" #include "Common/File/FileUtil.h" #include "Common/StringUtils.h" +#include "Common/Thread/ParallelLoop.h" #include "Core/Config.h" #include "Core/Host.h" #include "Core/System.h" @@ -747,12 +748,13 @@ void ReplacedTexture::Load(int level, void *out, int rowPitch) { int w, h, f; uint8_t *image; + const int MIN_LINES_PER_THREAD = 4; if (LoadZIMPtr(&zim[0], zimSize, &w, &h, &f, &image)) { - GlobalThreadPool::Loop([&](int low, int high) { - for (int y = low; y < high; ++y) { + ParallelRangeLoop(&g_threadManager, [&](int l, int h) { + for (int y = l; y < h; ++y) { memcpy((uint8_t *)out + rowPitch * y, image + w * 4 * y, w * 4); } - }, 0, h); + }, 0, h, MIN_LINES_PER_THREAD); free(image); } diff --git a/Core/ThreadPools.cpp b/Core/ThreadPools.cpp index e4eb7b5b9d..103856020a 100644 --- a/Core/ThreadPools.cpp +++ b/Core/ThreadPools.cpp @@ -1,26 +1,3 @@ #include "ThreadPools.h" -#include "../Core/Config.h" -#include "Common/MakeUnique.h" - -std::unique_ptr GlobalThreadPool::pool; -std::once_flag GlobalThreadPool::init_flag; - -void GlobalThreadPool::Loop(const std::function& loop, int lower, int upper, int minSize) { - std::call_once(init_flag, Inititialize); - pool->ParallelLoop(loop, lower, upper, minSize); -} - -void GlobalThreadPool::Memcpy(void *dest, const void *src, int size) { - std::call_once(init_flag, Inititialize); - pool->ParallelMemcpy(dest, src, size); -} - -void GlobalThreadPool::Memset(void *dest, uint8_t val, int size) { - std::call_once(init_flag, Inititialize); - pool->ParallelMemset(dest, val, size); -} - -void GlobalThreadPool::Inititialize() { - pool = make_unique(g_Config.iNumWorkerThreads); -} +ThreadManager g_threadManager; diff --git a/Core/ThreadPools.h b/Core/ThreadPools.h index 86af0afdad..efde2b9f47 100644 --- a/Core/ThreadPools.h +++ b/Core/ThreadPools.h @@ -1,17 +1,5 @@ #pragma once -#include "Common/Thread/ThreadPool.h" +#include "Common/Thread/ThreadManager.h" -class GlobalThreadPool { -public: - // will execute slices of "loop" from "lower" to "upper" - // in parallel on the global thread pool - static void Loop(const std::function& loop, int lower, int upper, int minSize = -1); - static void Memcpy(void *dest, const void *src, int size); - static void Memset(void *dest, uint8_t val, int size); - -private: - static std::unique_ptr pool; - static std::once_flag init_flag; - static void Inititialize(); -}; +extern ThreadManager g_threadManager; diff --git a/Core/Util/GameManager.cpp b/Core/Util/GameManager.cpp index b77d81e6d3..96688e9cdd 100644 --- a/Core/Util/GameManager.cpp +++ b/Core/Util/GameManager.cpp @@ -49,6 +49,26 @@ GameManager g_GameManager; +static struct zip *ZipOpenPath(Path fileName) { + int error = 0; + // Need to special case for content URI here, similar to OpenCFile. + struct zip *z; +#if PPSSPP_PLATFORM(ANDROID) + if (fileName.Type() == PathType::CONTENT_URI) { + int fd = File::OpenFD(fileName, File::OPEN_READ); + z = zip_fdopen(fd, 0, &error); + } else +#endif + { // continuation of above else in the ifdef + z = zip_open(fileName.c_str(), 0, &error); + } + + if (!z) { + ERROR_LOG(HLE, "Failed to open ZIP file '%s', error code=%i", fileName.c_str(), error); + } + return z; +} + GameManager::GameManager() { } @@ -131,7 +151,7 @@ bool GameManager::Uninstall(std::string name) { void GameManager::Update() { if (curDownload_.get() && curDownload_->Done()) { INFO_LOG(HLE, "Download completed! Status = %d", curDownload_->ResultCode()); - Path fileName = Path(curDownload_->outfile()); + Path fileName = curDownload_->outfile(); if (curDownload_->ResultCode() == 200) { if (!File::Exists(fileName)) { ERROR_LOG(HLE, "Downloaded file '%s' does not exist :(", fileName.c_str()); @@ -172,8 +192,7 @@ static void countSlashes(const std::string &fileName, int *slashLocation, int *s } ZipFileContents DetectZipFileContents(const Path &fileName, ZipFileInfo *info) { - int error = 0; - struct zip *z = zip_open(fileName.c_str(), 0, &error); + struct zip *z = ZipOpenPath(fileName); if (!z) { return ZipFileContents::UNKNOWN; } @@ -283,10 +302,9 @@ bool GameManager::InstallGame(Path url, Path fileName, bool deleteAfter) { Path dest = pspGame; int error = 0; - // TODO(scoped): zip_open ain't gonna work. zip_fdopen though.. - struct zip *z = zip_open(fileName.c_str(), 0, &error); + struct zip *z = ZipOpenPath(fileName); if (!z) { - ERROR_LOG(HLE, "Failed to open ZIP file '%s', error code=%i", fileName.c_str(), error); + installInProgress_ = false; return false; } diff --git a/GPU/Common/DepalettizeShaderCommon.cpp b/GPU/Common/DepalettizeShaderCommon.cpp index 11c598a7f0..ba8f520e1b 100644 --- a/GPU/Common/DepalettizeShaderCommon.cpp +++ b/GPU/Common/DepalettizeShaderCommon.cpp @@ -25,6 +25,7 @@ #include "Common/Log.h" #include "Core/Reporting.h" #include "GPU/GPUState.h" +#include "GPU/Common/GPUStateUtils.h" #include "GPU/Common/DepalettizeShaderCommon.h" #define WRITE p+=sprintf @@ -71,6 +72,12 @@ void GenerateDepalShader300(char *buffer, GEBufferFormat pixelFormat, ShaderLang WRITE(p, "out vec4 fragColor0;\n"); WRITE(p, "uniform sampler2D tex;\n"); WRITE(p, "uniform sampler2D pal;\n"); + + if (pixelFormat == GE_FORMAT_DEPTH16) { + DepthScaleFactors factors = GetDepthScaleFactors(); + WRITE(p, "const float z_scale = %f;\n", factors.scale); + WRITE(p, "const float z_offset = %f;\n", factors.offset); + } } if (language == HLSL_D3D11) { diff --git a/GPU/Common/GPUStateUtils.cpp b/GPU/Common/GPUStateUtils.cpp index 331436b02a..47fc87a5e0 100644 --- a/GPU/Common/GPUStateUtils.cpp +++ b/GPU/Common/GPUStateUtils.cpp @@ -575,9 +575,6 @@ void ConvertViewportAndScissor(bool useBufferedRendering, float renderWidth, flo renderHeightFactor = renderHeight / 272.0f; } - _assert_(renderWidthFactor > 0.0); - _assert_(renderHeightFactor > 0.0); - renderX = gstate_c.curRTOffsetX; // Scissor @@ -667,6 +664,12 @@ void ConvertViewportAndScissor(bool useBufferedRendering, float renderWidth, flo left += overageLeft; right -= overageRight; + // Protect against the viewport being entirely outside the scissor. + // Emit a tiny but valid viewport. Really, we should probably emit a flag to ignore draws. + if (right <= left) { + right = left + 1.0f; + } + wScale = vpWidth / (right - left); xOffset = drift / (right - left); } @@ -690,6 +693,12 @@ void ConvertViewportAndScissor(bool useBufferedRendering, float renderWidth, flo top += overageTop; bottom -= overageBottom; + // Protect against the viewport being entirely outside the scissor. + // Emit a tiny but valid viewport. Really, we should probably emit a flag to ignore draws. + if (bottom <= top) { + bottom = top + 1.0f; + } + hScale = vpHeight / (bottom - top); yOffset = drift / (bottom - top); } @@ -697,11 +706,8 @@ void ConvertViewportAndScissor(bool useBufferedRendering, float renderWidth, flo out.viewportX = left * renderWidthFactor + displayOffsetX; out.viewportY = top * renderHeightFactor + displayOffsetY; - - // The calculations should end up with zero or positive values, but let's protect against any - // precision issues. See #13921. - out.viewportW = std::max(0.0f, (right - left) * renderWidthFactor); - out.viewportH = std::max(0.0f, (bottom - top) * renderHeightFactor); + out.viewportW = (right - left) * renderWidthFactor; + out.viewportH = (bottom - top) * renderHeightFactor; // The depth viewport parameters are the same, but we handle it a bit differently. // When clipping is enabled, depth is clamped to [0, 65535]. And minz/maxz discard. diff --git a/GPU/Common/PostShader.cpp b/GPU/Common/PostShader.cpp index c14754ff32..96e4405b36 100644 --- a/GPU/Common/PostShader.cpp +++ b/GPU/Common/PostShader.cpp @@ -127,6 +127,7 @@ void LoadPostShaderInfo(const std::vector &directories) { section.Get("Upscaling", &info.isUpscalingFilter, false); section.Get("SSAA", &info.SSAAFilterLevel, 0); section.Get("60fps", &info.requires60fps, false); + section.Get("UsePreviousFrame", &info.usePreviousFrame, false); if (info.parent == "Off") info.parent = ""; diff --git a/GPU/Common/PostShader.h b/GPU/Common/PostShader.h index 76c2e8ce6f..5dfad5c03f 100644 --- a/GPU/Common/PostShader.h +++ b/GPU/Common/PostShader.h @@ -43,6 +43,8 @@ struct ShaderInfo { int SSAAFilterLevel; // Force constant/max refresh for animated filters bool requires60fps; + // Takes previous frame as input (for blending effects.) + bool usePreviousFrame; struct Setting { std::string name; diff --git a/GPU/Common/PresentationCommon.cpp b/GPU/Common/PresentationCommon.cpp index 59590e8401..b6e9ecbdfc 100644 --- a/GPU/Common/PresentationCommon.cpp +++ b/GPU/Common/PresentationCommon.cpp @@ -185,6 +185,10 @@ void PresentationCommon::CalculatePostShaderUniforms(int bufferWidth, int buffer uniforms->pixelDelta[0] = u_pixel_delta; uniforms->pixelDelta[1] = v_pixel_delta; memcpy(uniforms->time, time, 4 * sizeof(float)); + uniforms->timeDelta[0] = time[0] - previousUniforms_.time[0]; + uniforms->timeDelta[1] = (time[2] - previousUniforms_.time[2]) * (1.0f / 60.0f); + uniforms->timeDelta[2] = time[2] - previousUniforms_.time[2]; + uniforms->timeDelta[3] = time[3] != previousUniforms_.time[3] ? 1.0f : 0.0f; uniforms->video = hasVideo_ ? 1.0f : 0.0f; // The shader translator tacks this onto our shaders, if we don't set it they render garbage. @@ -222,12 +226,35 @@ bool PresentationCommon::UpdatePostShader() { if (shaderInfo.empty()) return false; + bool usePreviousFrame = false; + bool usePreviousAtOutputResolution = false; for (int i = 0; i < shaderInfo.size(); ++i) { const ShaderInfo *next = i + 1 < shaderInfo.size() ? shaderInfo[i + 1] : nullptr; if (!BuildPostShader(shaderInfo[i], next)) { DestroyPostShader(); return false; } + if (shaderInfo[i]->usePreviousFrame) { + usePreviousFrame = true; + usePreviousAtOutputResolution = shaderInfo[i]->outputResolution; + } + } + + if (usePreviousFrame) { + int w = usePreviousAtOutputResolution ? pixelWidth_ : renderWidth_; + int h = usePreviousAtOutputResolution ? pixelHeight_ : renderHeight_; + + static constexpr int FRAMES = 2; + previousFramebuffers_.resize(FRAMES); + previousIndex_ = 0; + + for (int i = 0; i < FRAMES; ++i) { + previousFramebuffers_[i] = draw_->CreateFramebuffer({ w, h, 1, 1, false, "inter_presentation" }); + if (!previousFramebuffers_[i]) { + DestroyPostShader(); + return false; + } + } } usePostShader_ = true; @@ -263,8 +290,9 @@ bool PresentationCommon::BuildPostShader(const ShaderInfo *shaderInfo, const Sha { "u_texelDelta", 1, 1, UniformType::FLOAT2, offsetof(PostShaderUniforms, texelDelta) }, { "u_pixelDelta", 2, 2, UniformType::FLOAT2, offsetof(PostShaderUniforms, pixelDelta) }, { "u_time", 3, 3, UniformType::FLOAT4, offsetof(PostShaderUniforms, time) }, - { "u_setting", 4, 4, UniformType::FLOAT4, offsetof(PostShaderUniforms, setting) }, - { "u_video", 5, 5, UniformType::FLOAT1, offsetof(PostShaderUniforms, video) }, + { "u_timeDelta", 4, 4, UniformType::FLOAT4, offsetof(PostShaderUniforms, timeDelta) }, + { "u_setting", 5, 5, UniformType::FLOAT4, offsetof(PostShaderUniforms, setting) }, + { "u_video", 6, 6, UniformType::FLOAT1, offsetof(PostShaderUniforms, video) }, } }; Draw::Pipeline *pipeline = CreatePipeline({ vs, fs }, true, &postShaderDesc); @@ -467,6 +495,7 @@ void PresentationCommon::DestroyPostShader() { DoReleaseVector(postShaderModules_); DoReleaseVector(postShaderPipelines_); DoReleaseVector(postShaderFramebuffers_); + DoReleaseVector(previousFramebuffers_); postShaderInfo_.clear(); postShaderFBOUsage_.clear(); } @@ -531,7 +560,7 @@ void PresentationCommon::CopyToOutput(OutputFlags flags, int uvRotation, float u bool useNearest = flags & OutputFlags::NEAREST; const bool usePostShader = usePostShader_ && !(flags & OutputFlags::RB_SWIZZLE); const bool isFinalAtOutputResolution = usePostShader && postShaderFramebuffers_.size() < postShaderPipelines_.size(); - bool usePostShaderOutput = false; + Draw::Framebuffer *postShaderOutput = nullptr; int lastWidth = srcWidth_; int lastHeight = srcHeight_; @@ -615,6 +644,47 @@ void PresentationCommon::CopyToOutput(OutputFlags flags, int uvRotation, float u } } + // Grab the previous framebuffer early so we can change previousIndex_ when we want. + Draw::Framebuffer *previousFramebuffer = previousFramebuffers_.empty() ? nullptr : previousFramebuffers_[previousIndex_]; + + PostShaderUniforms uniforms; + const auto performShaderPass = [&](const ShaderInfo *shaderInfo, Draw::Framebuffer *postShaderFramebuffer, Draw::Pipeline *postShaderPipeline) { + if (postShaderOutput) { + draw_->BindFramebufferAsTexture(postShaderOutput, 0, Draw::FB_COLOR_BIT, 0); + } else { + BindSource(0); + } + BindSource(1); + if (shaderInfo->usePreviousFrame) + draw_->BindFramebufferAsTexture(previousFramebuffer, 2, Draw::FB_COLOR_BIT, 0); + + int nextWidth, nextHeight; + draw_->GetFramebufferDimensions(postShaderFramebuffer, &nextWidth, &nextHeight); + Draw::Viewport viewport{ 0, 0, (float)nextWidth, (float)nextHeight, 0.0f, 1.0f }; + draw_->SetViewports(1, &viewport); + draw_->SetScissorRect(0, 0, nextWidth, nextHeight); + + CalculatePostShaderUniforms(lastWidth, lastHeight, nextWidth, nextHeight, shaderInfo, &uniforms); + + draw_->BindPipeline(postShaderPipeline); + draw_->UpdateDynamicUniformBuffer(&uniforms, sizeof(uniforms)); + + Draw::SamplerState *sampler = useNearest || shaderInfo->isUpscalingFilter ? samplerNearest_ : samplerLinear_; + draw_->BindSamplerStates(0, 1, &sampler); + draw_->BindSamplerStates(1, 1, &sampler); + if (shaderInfo->usePreviousFrame) + draw_->BindSamplerStates(2, 1, &sampler); + + draw_->BindVertexBuffers(0, 1, &vdata_, &postVertsOffset); + draw_->BindIndexBuffer(idata_, 0); + draw_->DrawIndexed(6, 0); + draw_->BindIndexBuffer(nullptr, 0); + + postShaderOutput = postShaderFramebuffer; + lastWidth = nextWidth; + lastHeight = nextHeight; + }; + if (usePostShader) { bool flipped = flags & OutputFlags::POSITION_FLIPPED; float post_v0 = !flipped ? 1.0f : 0.0f; @@ -629,40 +699,17 @@ void PresentationCommon::CopyToOutput(OutputFlags flags, int uvRotation, float u Draw::Pipeline *postShaderPipeline = postShaderPipelines_[i]; const ShaderInfo *shaderInfo = &postShaderInfo_[i]; Draw::Framebuffer *postShaderFramebuffer = postShaderFramebuffers_[i]; + if (!isFinalAtOutputResolution && i == postShaderFramebuffers_.size() - 1 && !previousFramebuffers_.empty()) { + // This is the last pass and we're going direct to the backbuffer after this. + // Redirect output to a separate framebuffer to keep the previous frame. + previousIndex_++; + if (previousIndex_ >= previousFramebuffers_.size()) + previousIndex_ = 0; + postShaderFramebuffer = previousFramebuffers_[previousIndex_]; + } draw_->BindFramebufferAsRenderTarget(postShaderFramebuffer, { Draw::RPAction::DONT_CARE, Draw::RPAction::DONT_CARE, Draw::RPAction::DONT_CARE }, "PostShader"); - - if (usePostShaderOutput) { - draw_->BindFramebufferAsTexture(postShaderFramebuffers_[i - 1], 0, Draw::FB_COLOR_BIT, 0); - } else { - BindSource(0); - } - BindSource(1); - - int nextWidth, nextHeight; - draw_->GetFramebufferDimensions(postShaderFramebuffer, &nextWidth, &nextHeight); - Draw::Viewport viewport{ 0, 0, (float)nextWidth, (float)nextHeight, 0.0f, 1.0f }; - draw_->SetViewports(1, &viewport); - draw_->SetScissorRect(0, 0, nextWidth, nextHeight); - - PostShaderUniforms uniforms; - CalculatePostShaderUniforms(lastWidth, lastHeight, nextWidth, nextHeight, shaderInfo, &uniforms); - - draw_->BindPipeline(postShaderPipeline); - draw_->UpdateDynamicUniformBuffer(&uniforms, sizeof(uniforms)); - - Draw::SamplerState *sampler = useNearest || shaderInfo->isUpscalingFilter ? samplerNearest_ : samplerLinear_; - draw_->BindSamplerStates(0, 1, &sampler); - draw_->BindSamplerStates(1, 1, &sampler); - - draw_->BindVertexBuffers(0, 1, &vdata_, &postVertsOffset); - draw_->BindIndexBuffer(idata_, 0); - draw_->DrawIndexed(6, 0); - draw_->BindIndexBuffer(nullptr, 0); - - usePostShaderOutput = true; - lastWidth = nextWidth; - lastHeight = nextHeight; + performShaderPass(shaderInfo, postShaderFramebuffer, postShaderPipeline); } if (isFinalAtOutputResolution && postShaderInfo_.back().isUpscalingFilter) @@ -671,8 +718,23 @@ void PresentationCommon::CopyToOutput(OutputFlags flags, int uvRotation, float u draw_->UpdateBuffer(vdata_, (const uint8_t *)verts, 0, postVertsOffset, Draw::UPDATE_DISCARD); } + // If we need to save the previous frame, we have to save any final pass in a framebuffer. + if (isFinalAtOutputResolution && !previousFramebuffers_.empty()) { + Draw::Pipeline *postShaderPipeline = postShaderPipelines_.back(); + const ShaderInfo *shaderInfo = &postShaderInfo_.back(); + + // Pick the next to render to. + previousIndex_++; + if (previousIndex_ >= previousFramebuffers_.size()) + previousIndex_ = 0; + Draw::Framebuffer *postShaderFramebuffer = previousFramebuffers_[previousIndex_]; + + draw_->BindFramebufferAsRenderTarget(postShaderFramebuffer, { Draw::RPAction::CLEAR, Draw::RPAction::DONT_CARE, Draw::RPAction::DONT_CARE }, "InterFrameBlit"); + performShaderPass(shaderInfo, postShaderFramebuffer, postShaderPipeline); + } + Draw::Pipeline *pipeline = flags & OutputFlags::RB_SWIZZLE ? texColorRBSwizzle_ : texColor_; - if (isFinalAtOutputResolution) { + if (isFinalAtOutputResolution && previousFramebuffers_.empty()) { pipeline = postShaderPipelines_.back(); } @@ -681,15 +743,14 @@ void PresentationCommon::CopyToOutput(OutputFlags flags, int uvRotation, float u draw_->BindPipeline(pipeline); - if (usePostShaderOutput) { - draw_->BindFramebufferAsTexture(postShaderFramebuffers_.back(), 0, Draw::FB_COLOR_BIT, 0); + if (postShaderOutput) { + draw_->BindFramebufferAsTexture(postShaderOutput, 0, Draw::FB_COLOR_BIT, 0); } else { BindSource(0); } BindSource(1); - if (isFinalAtOutputResolution) { - PostShaderUniforms uniforms; + if (isFinalAtOutputResolution && previousFramebuffers_.empty()) { CalculatePostShaderUniforms(lastWidth, lastHeight, (int)rc.w, (int)rc.h, &postShaderInfo_.back(), &uniforms); draw_->UpdateDynamicUniformBuffer(&uniforms, sizeof(uniforms)); } else { @@ -732,6 +793,8 @@ void PresentationCommon::CopyToOutput(OutputFlags flags, int uvRotation, float u // Unbinds all textures and samplers too, needed since sometimes a MakePixelTexture is deleted etc. draw_->BindPipeline(nullptr); + + previousUniforms_ = uniforms; } void PresentationCommon::CalculateRenderResolution(int *width, int *height, int *scaleFactor, bool *upscaling, bool *ssaa) { diff --git a/GPU/Common/PresentationCommon.h b/GPU/Common/PresentationCommon.h index c3f56699f9..63d8e4a2dc 100644 --- a/GPU/Common/PresentationCommon.h +++ b/GPU/Common/PresentationCommon.h @@ -31,6 +31,7 @@ struct CardboardSettings { struct PostShaderUniforms { float texelDelta[2]; float pixelDelta[2]; float time[4]; + float timeDelta[4]; float setting[4]; float video; float pad[3]; // Used on Direct3D9. @@ -131,6 +132,9 @@ protected: std::vector postShaderPipelines_; std::vector postShaderFramebuffers_; std::vector postShaderInfo_; + std::vector previousFramebuffers_; + int previousIndex_ = 0; + PostShaderUniforms previousUniforms_{}; Draw::Texture *srcTexture_ = nullptr; Draw::Framebuffer *srcFramebuffer_ = nullptr; diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index e7f26a6e14..39e55da1e9 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -367,7 +367,7 @@ TexCacheEntry *TextureCacheCommon::SetTexture() { int bufw = GetTextureBufw(0, texaddr, format); u8 maxLevel = gstate.getTextureMaxLevel(); - u32 texhash = MiniHash((const u32 *)Memory::GetPointerUnchecked(texaddr)); + u32 minihash = MiniHash((const u32 *)Memory::GetPointerUnchecked(texaddr)); TexCache::iterator entryIter = cache_.find(cachekey); TexCacheEntry *entry = nullptr; @@ -437,7 +437,7 @@ TexCacheEntry *TextureCacheCommon::SetTexture() { rehash = true; } - if (texhash != entry->hash) { + if (minihash != entry->minihash) { match = false; reason = "minihash"; } else if (entry->GetHashStatus() == TexCacheEntry::STATUS_RELIABLE) { @@ -548,7 +548,7 @@ TexCacheEntry *TextureCacheCommon::SetTexture() { // We have to decode it, let's setup the cache entry first. entry->addr = texaddr; - entry->hash = texhash; + entry->minihash = minihash; entry->dim = dim; entry->format = format; entry->maxLevel = maxLevel; @@ -1306,6 +1306,39 @@ static inline void ConvertFormatToRGBA8888(GEPaletteFormat format, u32 *dst, con ConvertFormatToRGBA8888(GETextureFormat(format), dst, src, numPixels); } +template +static void DecodeDXTBlock(uint8_t *out, int outPitch, uint32_t texaddr, const uint8_t *texptr, int w, int h, int bufw, bool reverseColors, bool useBGRA) { + int minw = std::min(bufw, w); + uint32_t *dst = (uint32_t *)out; + int outPitch32 = outPitch / sizeof(uint32_t); + const DXTBlock *src = (const DXTBlock *)texptr; + + if (!Memory::IsValidRange(texaddr, (h / 4) * (bufw / 4) * sizeof(DXTBlock))) { + ERROR_LOG_REPORT(G3D, "DXT%d texture extends beyond valid RAM: %08x + %d x %d", n, texaddr, bufw, h); + uint32_t limited = Memory::ValidSize(texaddr, (h / 4) * (bufw / 4) * sizeof(DXTBlock)); + // This might possibly be 0, but try to decode what we can (might even be how the PSP behaves.) + h = (((int)limited / sizeof(DXTBlock)) / (bufw / 4)) * 4; + } + + for (int y = 0; y < h; y += 4) { + u32 blockIndex = (y / 4) * (bufw / 4); + int blockHeight = std::min(h - y, 4); + for (int x = 0; x < minw; x += 4) { + if (n == 1) + DecodeDXT1Block(dst + outPitch32 * y + x, (const DXT1Block *)src + blockIndex, outPitch32, blockHeight, false); + if (n == 3) + DecodeDXT3Block(dst + outPitch32 * y + x, (const DXT3Block *)src + blockIndex, outPitch32, blockHeight); + if (n == 5) + DecodeDXT5Block(dst + outPitch32 * y + x, (const DXT5Block *)src + blockIndex, outPitch32, blockHeight); + blockIndex++; + } + } + w = (w + 3) & ~3; + if (reverseColors) { + ReverseColors(out, out, GE_TFMT_8888, outPitch32 * h, useBGRA); + } +} + void TextureCacheCommon::DecodeTextureLevel(u8 *out, int outPitch, GETextureFormat format, GEPaletteFormat clutformat, uint32_t texaddr, int level, int bufw, bool reverseColors, bool useBGRA, bool expandTo32bit) { bool swizzled = gstate.isTextureSwizzled(); if ((texaddr & 0x00600000) != 0 && Memory::IsVRAMAddress(texaddr)) { @@ -1481,70 +1514,16 @@ void TextureCacheCommon::DecodeTextureLevel(u8 *out, int outPitch, GETextureForm break; case GE_TFMT_DXT1: - { - int minw = std::min(bufw, w); - u32 *dst = (u32 *)out; - int outPitch32 = outPitch / sizeof(u32); - DXT1Block *src = (DXT1Block*)texptr; - - for (int y = 0; y < h; y += 4) { - u32 blockIndex = (y / 4) * (bufw / 4); - int blockHeight = std::min(h - y, 4); - for (int x = 0; x < minw; x += 4) { - DecodeDXT1Block(dst + outPitch32 * y + x, src + blockIndex, outPitch32, blockHeight, false); - blockIndex++; - } - } - w = (w + 3) & ~3; - if (reverseColors) { - ReverseColors(out, out, GE_TFMT_8888, outPitch32 * h, useBGRA); - } + DecodeDXTBlock(out, outPitch, texaddr, texptr, w, h, bufw, reverseColors, useBGRA); break; - } case GE_TFMT_DXT3: - { - int minw = std::min(bufw, w); - u32 *dst = (u32 *)out; - int outPitch32 = outPitch / sizeof(u32); - DXT3Block *src = (DXT3Block*)texptr; - - for (int y = 0; y < h; y += 4) { - u32 blockIndex = (y / 4) * (bufw / 4); - int blockHeight = std::min(h - y, 4); - for (int x = 0; x < minw; x += 4) { - DecodeDXT3Block(dst + outPitch32 * y + x, src + blockIndex, outPitch32, blockHeight); - blockIndex++; - } - } - w = (w + 3) & ~3; - if (reverseColors) { - ReverseColors(out, out, GE_TFMT_8888, outPitch32 * h, useBGRA); - } + DecodeDXTBlock(out, outPitch, texaddr, texptr, w, h, bufw, reverseColors, useBGRA); break; - } case GE_TFMT_DXT5: - { - int minw = std::min(bufw, w); - u32 *dst = (u32 *)out; - int outPitch32 = outPitch / sizeof(u32); - DXT5Block *src = (DXT5Block*)texptr; - - for (int y = 0; y < h; y += 4) { - u32 blockIndex = (y / 4) * (bufw / 4); - int blockHeight = std::min(h - y, 4); - for (int x = 0; x < minw; x += 4) { - DecodeDXT5Block(dst + outPitch32 * y + x, src + blockIndex, outPitch32, blockHeight); - blockIndex++; - } - } - w = (w + 3) & ~3; - if (reverseColors) { - ReverseColors(out, out, GE_TFMT_8888, outPitch32 * h, useBGRA); - } + DecodeDXTBlock(out, outPitch, texaddr, texptr, w, h, bufw, reverseColors, useBGRA); break; - } default: ERROR_LOG_REPORT(G3D, "Unknown Texture Format %d!!!", format); @@ -1843,7 +1822,7 @@ void TextureCacheCommon::Invalidate(u32 addr, int size, GPUInvalidationType type if (type == GPU_INVALIDATE_FORCE) { // Just random values to force the hash not to match. entry->fullhash = (entry->fullhash ^ 0x12345678) + 13; - entry->hash = (entry->hash ^ 0x89ABCDEF) + 89; + entry->minihash = (entry->minihash ^ 0x89ABCDEF) + 89; } if (type != GPU_INVALIDATE_ALL) { gpuStats.numTextureInvalidations++; diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index cbafece711..58439e2889 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -139,7 +139,7 @@ struct TexCacheEntry { int status; u32 addr; - u32 hash; + u32 minihash; u32 sizeInRAM; // Could be computed u8 format; // GeTextureFormat u8 maxLevel; diff --git a/GPU/Common/TextureScalerCommon.cpp b/GPU/Common/TextureScalerCommon.cpp index 0bd5d2e958..dd67e20116 100644 --- a/GPU/Common/TextureScalerCommon.cpp +++ b/GPU/Common/TextureScalerCommon.cpp @@ -26,6 +26,7 @@ #include "Common/Common.h" #include "Common/Log.h" #include "Common/CommonFuncs.h" +#include "Common/Thread/ParallelLoop.h" #include "Core/ThreadPools.h" #include "Common/CPUDetect.h" #include "ext/xbrz/xbrz.h" @@ -612,24 +613,26 @@ bool TextureScalerCommon::Scale(u32* &data, u32 &dstFmt, int &width, int &height return false; } +const int MIN_LINES_PER_THREAD = 4; + void TextureScalerCommon::ScaleXBRZ(int factor, u32* source, u32* dest, int width, int height) { xbrz::ScalerCfg cfg; - GlobalThreadPool::Loop(std::bind(&xbrz::scale, factor, source, dest, width, height, xbrz::ColorFormat::ARGB, cfg, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&xbrz::scale, factor, source, dest, width, height, xbrz::ColorFormat::ARGB, cfg, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); } void TextureScalerCommon::ScaleBilinear(int factor, u32* source, u32* dest, int width, int height) { - bufTmp1.resize(width*height*factor); + bufTmp1.resize(width * height * factor); u32 *tmpBuf = bufTmp1.data(); - GlobalThreadPool::Loop(std::bind(&bilinearH, factor, source, tmpBuf, width, std::placeholders::_1, std::placeholders::_2), 0, height); - GlobalThreadPool::Loop(std::bind(&bilinearV, factor, tmpBuf, dest, width, 0, height, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&bilinearH, factor, source, tmpBuf, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); + ParallelRangeLoop(&g_threadManager, std::bind(&bilinearV, factor, tmpBuf, dest, width, 0, height, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); } void TextureScalerCommon::ScaleBicubicBSpline(int factor, u32* source, u32* dest, int width, int height) { - GlobalThreadPool::Loop(std::bind(&scaleBicubicBSpline, factor, source, dest, width, height, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager,std::bind(&scaleBicubicBSpline, factor, source, dest, width, height, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); } void TextureScalerCommon::ScaleBicubicMitchell(int factor, u32* source, u32* dest, int width, int height) { - GlobalThreadPool::Loop(std::bind(&scaleBicubicMitchell, factor, source, dest, width, height, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager,std::bind(&scaleBicubicMitchell, factor, source, dest, width, height, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); } void TextureScalerCommon::ScaleHybrid(int factor, u32* source, u32* dest, int width, int height, bool bicubic) { @@ -645,8 +648,9 @@ void TextureScalerCommon::ScaleHybrid(int factor, u32* source, u32* dest, int wi bufTmp1.resize(width*height); bufTmp2.resize(width*height*factor*factor); bufTmp3.resize(width*height*factor*factor); - GlobalThreadPool::Loop(std::bind(&generateDistanceMask, source, bufTmp1.data(), width, height, std::placeholders::_1, std::placeholders::_2), 0, height); - GlobalThreadPool::Loop(std::bind(&convolve3x3, bufTmp1.data(), bufTmp2.data(), KERNEL_SPLAT, width, height, std::placeholders::_1, std::placeholders::_2), 0, height); + + ParallelRangeLoop(&g_threadManager,std::bind(&generateDistanceMask, source, bufTmp1.data(), width, height, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); + ParallelRangeLoop(&g_threadManager,std::bind(&convolve3x3, bufTmp1.data(), bufTmp2.data(), KERNEL_SPLAT, width, height, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); ScaleBilinear(factor, bufTmp2.data(), bufTmp3.data(), width, height); // mask C is now in bufTmp3 @@ -659,13 +663,13 @@ void TextureScalerCommon::ScaleHybrid(int factor, u32* source, u32* dest, int wi // Now we can mix it all together // The factor 8192 was found through practical testing on a variety of textures - GlobalThreadPool::Loop(std::bind(&mix, dest, bufTmp2.data(), bufTmp3.data(), 8192, width*factor, std::placeholders::_1, std::placeholders::_2), 0, height*factor); + ParallelRangeLoop(&g_threadManager,std::bind(&mix, dest, bufTmp2.data(), bufTmp3.data(), 8192, width*factor, std::placeholders::_1, std::placeholders::_2), 0, height*factor, MIN_LINES_PER_THREAD); } void TextureScalerCommon::DePosterize(u32* source, u32* dest, int width, int height) { bufTmp3.resize(width*height); - GlobalThreadPool::Loop(std::bind(&deposterizeH, source, bufTmp3.data(), width, std::placeholders::_1, std::placeholders::_2), 0, height); - GlobalThreadPool::Loop(std::bind(&deposterizeV, bufTmp3.data(), dest, width, height, std::placeholders::_1, std::placeholders::_2), 0, height); - GlobalThreadPool::Loop(std::bind(&deposterizeH, dest, bufTmp3.data(), width, std::placeholders::_1, std::placeholders::_2), 0, height); - GlobalThreadPool::Loop(std::bind(&deposterizeV, bufTmp3.data(), dest, width, height, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager,std::bind(&deposterizeH, source, bufTmp3.data(), width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); + ParallelRangeLoop(&g_threadManager,std::bind(&deposterizeV, bufTmp3.data(), dest, width, height, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); + ParallelRangeLoop(&g_threadManager,std::bind(&deposterizeH, dest, bufTmp3.data(), width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); + ParallelRangeLoop(&g_threadManager,std::bind(&deposterizeV, bufTmp3.data(), dest, width, height, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_LINES_PER_THREAD); } diff --git a/GPU/Common/TextureScalerCommon.h b/GPU/Common/TextureScalerCommon.h index cf34381305..6b2a6eb488 100644 --- a/GPU/Common/TextureScalerCommon.h +++ b/GPU/Common/TextureScalerCommon.h @@ -22,6 +22,8 @@ #include +static const int MIN_TEXSCALE_LINES_PER_THREAD = 4; + class TextureScalerCommon { public: TextureScalerCommon(); diff --git a/GPU/D3D11/TextureScalerD3D11.cpp b/GPU/D3D11/TextureScalerD3D11.cpp index 45aa67e18d..0a40d617c8 100644 --- a/GPU/D3D11/TextureScalerD3D11.cpp +++ b/GPU/D3D11/TextureScalerD3D11.cpp @@ -20,6 +20,7 @@ #include #include "Common/Data/Convert/ColorConv.h" #include "Core/ThreadPools.h" +#include "Common/Thread/ParallelLoop.h" #include "GPU/Common/TextureScalerCommon.h" #include "GPU/D3D11/TextureScalerD3D11.h" #include "GPU/D3D11/GPU_D3D11.h" @@ -39,15 +40,15 @@ void TextureScalerD3D11::ConvertTo8888(u32 format, u32* source, u32* &dest, int break; case DXGI_FORMAT_B4G4R4A4_UNORM: - GlobalThreadPool::Loop(std::bind(&convert4444_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert4444_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; case DXGI_FORMAT_B5G6R5_UNORM: - GlobalThreadPool::Loop(std::bind(&convert565_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert565_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; case DXGI_FORMAT_B5G5R5A1_UNORM: - GlobalThreadPool::Loop(std::bind(&convert5551_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert5551_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; default: diff --git a/GPU/Debugger/Record.cpp b/GPU/Debugger/Record.cpp index dc9ae32834..400d1fb71e 100644 --- a/GPU/Debugger/Record.cpp +++ b/GPU/Debugger/Record.cpp @@ -21,10 +21,12 @@ #include #include #include +#include #include #include "Common/Common.h" #include "Common/File/FileUtil.h" +#include "Common/Thread/ParallelLoop.h" #include "Common/Log.h" #include "Common/StringUtils.h" @@ -179,7 +181,7 @@ static const u8 *mymemmem(const u8 *haystack, size_t off, size_t hlen, const u8 std::mutex resultLock; int range = (int)(last_possible - first_possible); - GlobalThreadPool::Loop([&](int l, int h) { + ParallelRangeLoop(&g_threadManager, [&](int l, int h) { const u8 *p = haystack + off + l; const u8 *pend = haystack + off + h; diff --git a/GPU/Directx9/TextureScalerDX9.cpp b/GPU/Directx9/TextureScalerDX9.cpp index 574fa3eebc..f1f72ff253 100644 --- a/GPU/Directx9/TextureScalerDX9.cpp +++ b/GPU/Directx9/TextureScalerDX9.cpp @@ -19,6 +19,7 @@ #include "Common/Common.h" #include "Common/Data/Convert/ColorConv.h" +#include "Common/Thread/ParallelLoop.h" #include "Core/ThreadPools.h" #include "GPU/Common/TextureScalerCommon.h" #include "GPU/Directx9/TextureScalerDX9.h" @@ -41,15 +42,15 @@ void TextureScalerDX9::ConvertTo8888(u32 format, u32* source, u32* &dest, int wi break; case D3DFMT_A4R4G4B4: - GlobalThreadPool::Loop(std::bind(&convert4444_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert4444_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; case D3DFMT_R5G6B5: - GlobalThreadPool::Loop(std::bind(&convert565_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert565_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; case D3DFMT_A1R5G5B5: - GlobalThreadPool::Loop(std::bind(&convert5551_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert5551_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; default: diff --git a/GPU/GLES/DepthBufferGLES.cpp b/GPU/GLES/DepthBufferGLES.cpp index 9cd487ae5a..7eae61caa6 100644 --- a/GPU/GLES/DepthBufferGLES.cpp +++ b/GPU/GLES/DepthBufferGLES.cpp @@ -133,7 +133,7 @@ void FramebufferManagerGLES::PackDepthbuffer(VirtualFramebuffer *vfb, int x, int // We must bind the program after starting the render pass, and set the color mask after clearing. render_->SetScissor({ 0, 0, vfb->renderWidth, vfb->renderHeight }); render_->SetDepth(false, false, GL_ALWAYS); - render_->SetRaster(false, GL_CCW, GL_FRONT, GL_FALSE); + render_->SetRaster(false, GL_CCW, GL_FRONT, GL_FALSE, GL_FALSE); render_->BindProgram(depthDownloadProgram_); if (!gstate_c.Supports(GPU_SUPPORTS_ACCURATE_DEPTH)) { diff --git a/GPU/GLES/FramebufferManagerGLES.cpp b/GPU/GLES/FramebufferManagerGLES.cpp index 5870c899ef..f28b4f7016 100644 --- a/GPU/GLES/FramebufferManagerGLES.cpp +++ b/GPU/GLES/FramebufferManagerGLES.cpp @@ -203,7 +203,7 @@ void FramebufferManagerGLES::DrawActiveTexture(float x, float y, float w, float // We always want a plain state here, well, except for when it's used by the stencil stuff... render_->SetDepth(false, false, GL_ALWAYS); - render_->SetRaster(false, GL_CCW, GL_FRONT, GL_FALSE); + render_->SetRaster(false, GL_CCW, GL_FRONT, GL_FALSE, GL_FALSE); if (!(flags & DRAWTEX_KEEP_STENCIL_ALPHA)) { render_->SetNoBlendAndMask(0xF); render_->SetStencilDisabled(); diff --git a/GPU/GLES/GPU_GLES.cpp b/GPU/GLES/GPU_GLES.cpp index d60e1b5592..18b11d57f5 100644 --- a/GPU/GLES/GPU_GLES.cpp +++ b/GPU/GLES/GPU_GLES.cpp @@ -221,6 +221,14 @@ void GPU_GLES::CheckGPUFeatures() { if (gl_extensions.ARB_texture_float || gl_extensions.OES_texture_float) features |= GPU_SUPPORTS_TEXTURE_FLOAT; + if (draw_->GetDeviceCaps().depthClampSupported) { + features |= GPU_SUPPORTS_DEPTH_CLAMP | GPU_SUPPORTS_ACCURATE_DEPTH; + // Our implementation of depth texturing needs simple Z range, so can't + // use the extension hacks (yet). + if (gl_extensions.GLES3) + features |= GPU_SUPPORTS_DEPTH_TEXTURE; + } + // If we already have a 16-bit depth buffer, we don't need to round. bool prefer24 = draw_->GetDeviceCaps().preferredDepthBufferFormat == Draw::DataFormat::D24_S8; if (prefer24) { diff --git a/GPU/GLES/StateMappingGLES.cpp b/GPU/GLES/StateMappingGLES.cpp index 992695e4a5..404dc506ac 100644 --- a/GPU/GLES/StateMappingGLES.cpp +++ b/GPU/GLES/StateMappingGLES.cpp @@ -230,7 +230,23 @@ void DrawEngineGLES::ApplyDrawState(int prim) { GLenum cullMode = cullingMode[gstate.getCullMode() ^ !useBufferedRendering]; cullEnable = !gstate.isModeClear() && prim != GE_PRIM_RECTANGLES && gstate.isCullEnabled(); - renderManager->SetRaster(cullEnable, GL_CCW, cullMode, dither); + + bool depthClampEnable = false; + if (gstate.isModeClear() || gstate.isModeThrough()) { + // TODO: Might happen in clear mode if not through... + depthClampEnable = false; + } else { + if (gstate.getDepthRangeMin() == 0 || gstate.getDepthRangeMax() == 65535) { + // TODO: Still has a bug where we clamp to depth range if one is not the full range. + // But the alternate is not clamping in either direction... + depthClampEnable = gstate.isDepthClampEnabled() && gstate_c.Supports(GPU_SUPPORTS_DEPTH_CLAMP); + } else { + // We just want to clip in this case, the clamp would be clipped anyway. + depthClampEnable = false; + } + } + + renderManager->SetRaster(cullEnable, GL_CCW, cullMode, dither, depthClampEnable); } if (gstate_c.IsDirty(DIRTY_DEPTHSTENCIL_STATE)) { diff --git a/GPU/GLES/StencilBufferGLES.cpp b/GPU/GLES/StencilBufferGLES.cpp index df9cd06193..480690b930 100644 --- a/GPU/GLES/StencilBufferGLES.cpp +++ b/GPU/GLES/StencilBufferGLES.cpp @@ -191,7 +191,7 @@ bool FramebufferManagerGLES::NotifyStencilUpload(u32 addr, int size, StencilUplo render_->SetDepth(false, false, GL_ALWAYS); render_->Clear(0, 0, 0, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, 0x8, 0, 0, 0, 0); render_->SetStencilFunc(GL_TRUE, GL_ALWAYS, 0xFF, 0xFF); - render_->SetRaster(false, GL_CCW, GL_FRONT, GL_FALSE); + render_->SetRaster(false, GL_CCW, GL_FRONT, GL_FALSE, GL_FALSE); render_->BindProgram(stencilUploadProgram_); render_->SetNoBlendAndMask(0x8); diff --git a/GPU/GLES/TextureCacheGLES.cpp b/GPU/GLES/TextureCacheGLES.cpp index f69be7a23f..31049790b8 100644 --- a/GPU/GLES/TextureCacheGLES.cpp +++ b/GPU/GLES/TextureCacheGLES.cpp @@ -341,13 +341,15 @@ protected: }; void TextureCacheGLES::ApplyTextureFramebuffer(VirtualFramebuffer *framebuffer, GETextureFormat texFormat, FramebufferNotificationChannel channel) { - DepalShader *depal = nullptr; + DepalShader *depalShader = nullptr; uint32_t clutMode = gstate.clutformat & 0xFFFFFF; bool need_depalettize = IsClutFormat(texFormat); - bool useShaderDepal = framebufferManager_->GetCurrentRenderVFB() != framebuffer && (gstate_c.Supports(GPU_SUPPORTS_GLSL_ES_300) || gstate_c.Supports(GPU_SUPPORTS_GLSL_330)); + bool depth = channel == NOTIFY_FB_DEPTH; + bool useShaderDepal = framebufferManager_->GetCurrentRenderVFB() != framebuffer && (gstate_c.Supports(GPU_SUPPORTS_GLSL_ES_300) || gstate_c.Supports(GPU_SUPPORTS_GLSL_330)) && !depth; if (!gstate_c.Supports(GPU_SUPPORTS_32BIT_INT_FSHADER)) { useShaderDepal = false; + depth = false; // Can't support this } if (need_depalettize && !g_Config.bDisableSlowFramebufEffects) { @@ -375,10 +377,10 @@ void TextureCacheGLES::ApplyTextureFramebuffer(VirtualFramebuffer *framebuffer, return; } + depalShader = depalShaderCache_->GetDepalettizeShader(clutMode, depth ? GE_FORMAT_DEPTH16 : framebuffer->drawnFormat); gstate_c.SetUseShaderDepal(false); - depal = depalShaderCache_->GetDepalettizeShader(clutMode, framebuffer->drawnFormat); } - if (depal) { + if (depalShader) { shaderManager_->DirtyLastShader(); const GEPaletteFormat clutFormat = gstate.getClutPaletteFormat(); @@ -388,11 +390,12 @@ void TextureCacheGLES::ApplyTextureFramebuffer(VirtualFramebuffer *framebuffer, render_->SetScissor(GLRect2D{ 0, 0, (int)framebuffer->renderWidth, (int)framebuffer->renderHeight }); render_->SetViewport(GLRViewport{ 0.0f, 0.0f, (float)framebuffer->renderWidth, (float)framebuffer->renderHeight, 0.0f, 1.0f }); - TextureShaderApplier shaderApply(depal, framebuffer->bufferWidth, framebuffer->bufferHeight, framebuffer->renderWidth, framebuffer->renderHeight); + TextureShaderApplier shaderApply(depalShader, framebuffer->bufferWidth, framebuffer->bufferHeight, framebuffer->renderWidth, framebuffer->renderHeight); shaderApply.ApplyBounds(gstate_c.vertBounds, gstate_c.curTextureXOffset, gstate_c.curTextureYOffset); shaderApply.Use(render_, drawEngine_, shadeInputLayout_); - framebufferManagerGL_->BindFramebufferAsColorTexture(0, framebuffer, BINDFBCOLOR_SKIP_COPY | BINDFBCOLOR_FORCE_SELF); + draw_->BindFramebufferAsTexture(framebuffer->fbo, 0, depth ? Draw::FB_DEPTH_BIT : Draw::FB_COLOR_BIT, 0); + render_->BindTexture(TEX_SLOT_CLUT, clutTexture); render_->SetTextureSampler(TEX_SLOT_CLUT, GL_REPEAT, GL_CLAMP_TO_EDGE, GL_NEAREST, GL_NEAREST, 0.0f); diff --git a/GPU/GLES/TextureScalerGLES.cpp b/GPU/GLES/TextureScalerGLES.cpp index abe06bb5bd..b535dcd291 100644 --- a/GPU/GLES/TextureScalerGLES.cpp +++ b/GPU/GLES/TextureScalerGLES.cpp @@ -16,15 +16,17 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include -#include "Common/GPU/OpenGL/GLCommon.h" -#include "GPU/Common/TextureScalerCommon.h" -#include "GPU/GLES/TextureScalerGLES.h" #include "Common/Data/Convert/ColorConv.h" #include "Common/Log.h" -#include "Core/ThreadPools.h" +#include "Common/Thread/ParallelLoop.h" +#include "Common/GPU/OpenGL/GLCommon.h" #include "Common/GPU/DataFormat.h" +#include "Core/ThreadPools.h" +#include "GPU/Common/TextureScalerCommon.h" +#include "GPU/GLES/TextureScalerGLES.h" + int TextureScalerGLES::BytesPerPixel(u32 format) { return ((Draw::DataFormat)format == Draw::DataFormat::R8G8B8A8_UNORM) ? 4 : 2; } @@ -41,15 +43,15 @@ void TextureScalerGLES::ConvertTo8888(u32 format, u32* source, u32* &dest, int w break; case Draw::DataFormat::R4G4B4A4_UNORM_PACK16: - GlobalThreadPool::Loop(std::bind(&convert4444_gl, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert4444_gl, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; case Draw::DataFormat::R5G6B5_UNORM_PACK16: - GlobalThreadPool::Loop(std::bind(&convert565_gl, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert565_gl, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; case Draw::DataFormat::R5G5B5A1_UNORM_PACK16: - GlobalThreadPool::Loop(std::bind(&convert5551_gl, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert5551_gl, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; default: diff --git a/GPU/Software/Rasterizer.cpp b/GPU/Software/Rasterizer.cpp index f961175c2c..d4486356b2 100644 --- a/GPU/Software/Rasterizer.cpp +++ b/GPU/Software/Rasterizer.cpp @@ -21,11 +21,12 @@ #include "Common/Data/Convert/ColorConv.h" #include "Common/Profiler/Profiler.h" - +#include "Common/Thread/ParallelLoop.h" #include "Core/ThreadPools.h" #include "Core/Config.h" #include "Core/MemMap.h" #include "Core/Reporting.h" +#include "Core/ThreadPools.h" #include "GPU/GPUState.h" #include "GPU/Common/TextureDecoder.h" @@ -1321,29 +1322,32 @@ void DrawTriangle(const VertexData& v0, const VertexData& v1, const VertexData& // 32 because we do two pixels at once, and we don't want overlap. int rangeY = (maxY - minY) / 32 + 1; int rangeX = (maxX - minX) / 32 + 1; + + const int MIN_LINES_PER_THREAD = 4; + if (rangeY >= 12 && rangeX >= rangeY * 4) { if (gstate.isModeClear()) { auto bound = [&](int a, int b) -> void { DrawTriangleSlice(v0, v1, v2, minX, minY, maxX, maxY, false, a, b); }; - GlobalThreadPool::Loop(bound, 0, rangeX); + ParallelRangeLoop(&g_threadManager, bound, 0, rangeX, MIN_LINES_PER_THREAD); } else { auto bound = [&](int a, int b) -> void { DrawTriangleSlice(v0, v1, v2, minX, minY, maxX, maxY, false, a, b); }; - GlobalThreadPool::Loop(bound, 0, rangeX); + ParallelRangeLoop(&g_threadManager, bound, 0, rangeX, MIN_LINES_PER_THREAD); } } else if (rangeY >= 12 && rangeX >= 12) { if (gstate.isModeClear()) { auto bound = [&](int a, int b) -> void { DrawTriangleSlice(v0, v1, v2, minX, minY, maxX, maxY, true, a, b); }; - GlobalThreadPool::Loop(bound, 0, rangeY); + ParallelRangeLoop(&g_threadManager, bound, 0, rangeY, MIN_LINES_PER_THREAD); } else { auto bound = [&](int a, int b) -> void { DrawTriangleSlice(v0, v1, v2, minX, minY, maxX, maxY, true, a, b); }; - GlobalThreadPool::Loop(bound, 0, rangeY); + ParallelRangeLoop(&g_threadManager, bound, 0, rangeY, MIN_LINES_PER_THREAD); } } else { if (gstate.isModeClear()) { diff --git a/GPU/Vulkan/DepalettizeShaderVulkan.cpp b/GPU/Vulkan/DepalettizeShaderVulkan.cpp index 7768218998..c404d1870a 100644 --- a/GPU/Vulkan/DepalettizeShaderVulkan.cpp +++ b/GPU/Vulkan/DepalettizeShaderVulkan.cpp @@ -39,25 +39,20 @@ void main() { } )"; -static const VkComponentMapping VULKAN_4444_SWIZZLE = { VK_COMPONENT_SWIZZLE_A, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B }; -static const VkComponentMapping VULKAN_1555_SWIZZLE = { VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_A }; -static const VkComponentMapping VULKAN_565_SWIZZLE = { VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY }; -static const VkComponentMapping VULKAN_8888_SWIZZLE = { VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY }; - static VkFormat GetClutDestFormat(GEPaletteFormat format, VkComponentMapping *componentMapping) { switch (format) { case GE_CMODE_16BIT_ABGR4444: *componentMapping = VULKAN_4444_SWIZZLE; - return VK_FORMAT_B4G4R4A4_UNORM_PACK16; + return VULKAN_4444_FORMAT; case GE_CMODE_16BIT_ABGR5551: *componentMapping = VULKAN_1555_SWIZZLE; - return VK_FORMAT_A1R5G5B5_UNORM_PACK16; + return VULKAN_1555_FORMAT; case GE_CMODE_16BIT_BGR5650: *componentMapping = VULKAN_565_SWIZZLE; - return VK_FORMAT_B5G6R5_UNORM_PACK16; + return VULKAN_565_FORMAT; case GE_CMODE_32BIT_ABGR8888: *componentMapping = VULKAN_8888_SWIZZLE; - return VK_FORMAT_R8G8B8A8_UNORM; + return VULKAN_8888_FORMAT; } return VK_FORMAT_UNDEFINED; } diff --git a/GPU/Vulkan/GPU_Vulkan.cpp b/GPU/Vulkan/GPU_Vulkan.cpp index e52df542be..6b4c5e7d03 100644 --- a/GPU/Vulkan/GPU_Vulkan.cpp +++ b/GPU/Vulkan/GPU_Vulkan.cpp @@ -220,7 +220,7 @@ void GPU_Vulkan::CheckGPUFeatures() { } // Might enable this later - in the first round we are mostly looking at depth/stencil/discard. - // if (g_Config.bDisableVendorBugChecks) + // if (!g_Config.bEnableVendorBugChecks) // features |= GPU_SUPPORTS_ACCURATE_DEPTH; // Mandatory features on Vulkan, which may be checked in "centralized" code @@ -256,9 +256,14 @@ void GPU_Vulkan::CheckGPUFeatures() { features |= GPU_SUPPORTS_ANISOTROPY; } + // These are VULKAN_4444_FORMAT and friends. uint32_t fmt4444 = draw_->GetDataFormatSupport(Draw::DataFormat::B4G4R4A4_UNORM_PACK16); uint32_t fmt1555 = draw_->GetDataFormatSupport(Draw::DataFormat::A1R5G5B5_UNORM_PACK16); - uint32_t fmt565 = draw_->GetDataFormatSupport(Draw::DataFormat::R5G6B5_UNORM_PACK16); + + // Note that we are (accidentally) using B5G6R5 instead of the mandatory R5G6B5. + // Support is almost as widespread, but not quite. So let's just not use any 16-bit formats + // if it's not available, for simplicity. + uint32_t fmt565 = draw_->GetDataFormatSupport(Draw::DataFormat::B5G6R5_UNORM_PACK16); if ((fmt4444 & Draw::FMT_TEXTURE) && (fmt565 & Draw::FMT_TEXTURE) && (fmt1555 & Draw::FMT_TEXTURE)) { features |= GPU_SUPPORTS_16BIT_FORMATS; } else { diff --git a/GPU/Vulkan/TextureCacheVulkan.cpp b/GPU/Vulkan/TextureCacheVulkan.cpp index 572cb64c37..5c2e359ff6 100644 --- a/GPU/Vulkan/TextureCacheVulkan.cpp +++ b/GPU/Vulkan/TextureCacheVulkan.cpp @@ -43,6 +43,7 @@ #include "GPU/Common/PostShader.h" #include "GPU/Common/TextureCacheCommon.h" #include "GPU/Common/TextureDecoder.h" +#include "GPU/Vulkan/VulkanContext.h" #include "GPU/Vulkan/TextureCacheVulkan.h" #include "GPU/Vulkan/FramebufferManagerVulkan.h" #include "GPU/Vulkan/DepalettizeShaderVulkan.h" @@ -53,17 +54,6 @@ #define TEXCACHE_MAX_SLAB_SIZE (32 * 1024 * 1024) #define TEXCACHE_SLAB_PRESSURE 4 -// Note: some drivers prefer B4G4R4A4_UNORM_PACK16 over R4G4B4A4_UNORM_PACK16. -#define VULKAN_4444_FORMAT VK_FORMAT_B4G4R4A4_UNORM_PACK16 -#define VULKAN_1555_FORMAT VK_FORMAT_A1R5G5B5_UNORM_PACK16 -#define VULKAN_565_FORMAT VK_FORMAT_B5G6R5_UNORM_PACK16 -#define VULKAN_8888_FORMAT VK_FORMAT_R8G8B8A8_UNORM - -static const VkComponentMapping VULKAN_4444_SWIZZLE = { VK_COMPONENT_SWIZZLE_A, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B }; -static const VkComponentMapping VULKAN_1555_SWIZZLE = { VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_A }; -static const VkComponentMapping VULKAN_565_SWIZZLE = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A }; -static const VkComponentMapping VULKAN_8888_SWIZZLE = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A }; - const char *copyShader = R"( #version 450 #extension GL_ARB_separate_shader_objects : enable diff --git a/GPU/Vulkan/TextureScalerVulkan.cpp b/GPU/Vulkan/TextureScalerVulkan.cpp index e714e3eb0c..cdcb00181b 100644 --- a/GPU/Vulkan/TextureScalerVulkan.cpp +++ b/GPU/Vulkan/TextureScalerVulkan.cpp @@ -21,16 +21,11 @@ #include "Common/Data/Convert/ColorConv.h" #include "Common/GPU/Vulkan/VulkanContext.h" #include "Common/Log.h" +#include "Common/Thread/ParallelLoop.h" #include "Core/ThreadPools.h" #include "GPU/Common/TextureScalerCommon.h" #include "GPU/Vulkan/TextureScalerVulkan.h" - -// TODO: Share in TextureCacheVulkan.h? -// Note: some drivers prefer B4G4R4A4_UNORM_PACK16 over R4G4B4A4_UNORM_PACK16. -#define VULKAN_4444_FORMAT VK_FORMAT_B4G4R4A4_UNORM_PACK16 -#define VULKAN_1555_FORMAT VK_FORMAT_A1R5G5B5_UNORM_PACK16 -#define VULKAN_565_FORMAT VK_FORMAT_B5G6R5_UNORM_PACK16 -#define VULKAN_8888_FORMAT VK_FORMAT_R8G8B8A8_UNORM +#include "GPU/Vulkan/VulkanUtil.h" int TextureScalerVulkan::BytesPerPixel(u32 format) { return (format == VULKAN_8888_FORMAT) ? 4 : 2; @@ -47,15 +42,15 @@ void TextureScalerVulkan::ConvertTo8888(u32 format, u32* source, u32* &dest, int break; case VULKAN_4444_FORMAT: - GlobalThreadPool::Loop(std::bind(&convert4444_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert4444_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; case VULKAN_565_FORMAT: - GlobalThreadPool::Loop(std::bind(&convert565_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert565_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; case VULKAN_1555_FORMAT: - GlobalThreadPool::Loop(std::bind(&convert5551_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height); + ParallelRangeLoop(&g_threadManager, std::bind(&convert5551_dx9, (u16*)source, dest, width, std::placeholders::_1, std::placeholders::_2), 0, height, MIN_TEXSCALE_LINES_PER_THREAD); break; default: diff --git a/GPU/Vulkan/VulkanUtil.cpp b/GPU/Vulkan/VulkanUtil.cpp index 3f4e889e9b..e54c7c4840 100644 --- a/GPU/Vulkan/VulkanUtil.cpp +++ b/GPU/Vulkan/VulkanUtil.cpp @@ -20,6 +20,11 @@ #include "Common/GPU/Vulkan/VulkanContext.h" #include "GPU/Vulkan/VulkanUtil.h" +const VkComponentMapping VULKAN_4444_SWIZZLE = { VK_COMPONENT_SWIZZLE_A, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B }; +const VkComponentMapping VULKAN_1555_SWIZZLE = { VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_A }; +const VkComponentMapping VULKAN_565_SWIZZLE = { VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY }; +const VkComponentMapping VULKAN_8888_SWIZZLE = { VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY }; + Vulkan2D::Vulkan2D(VulkanContext *vulkan) : vulkan_(vulkan) { InitDeviceObjects(); } diff --git a/GPU/Vulkan/VulkanUtil.h b/GPU/Vulkan/VulkanUtil.h index 8e837df544..6f2b2b4b36 100644 --- a/GPU/Vulkan/VulkanUtil.h +++ b/GPU/Vulkan/VulkanUtil.h @@ -25,6 +25,17 @@ #include "Common/GPU/Vulkan/VulkanLoader.h" #include "Common/GPU/Vulkan/VulkanImage.h" +extern const VkComponentMapping VULKAN_4444_SWIZZLE; +extern const VkComponentMapping VULKAN_1555_SWIZZLE; +extern const VkComponentMapping VULKAN_565_SWIZZLE; +extern const VkComponentMapping VULKAN_8888_SWIZZLE; + +// Note: some drivers prefer B4G4R4A4_UNORM_PACK16 over R4G4B4A4_UNORM_PACK16. +#define VULKAN_4444_FORMAT VK_FORMAT_B4G4R4A4_UNORM_PACK16 +#define VULKAN_1555_FORMAT VK_FORMAT_A1R5G5B5_UNORM_PACK16 +#define VULKAN_565_FORMAT VK_FORMAT_B5G6R5_UNORM_PACK16 // TODO: Does not actually have mandatory support, though R5G6B5 does! See #14602 +#define VULKAN_8888_FORMAT VK_FORMAT_R8G8B8A8_UNORM + // Vulkan doesn't really have the concept of an FBO that owns the images, // but it does have the concept of a framebuffer as a set of attachments. // VulkanFBO is an approximation of the FBO concept the other backends use diff --git a/SDL/SDLJoystick.cpp b/SDL/SDLJoystick.cpp index b32d7a471e..c129ac821a 100644 --- a/SDL/SDLJoystick.cpp +++ b/SDL/SDLJoystick.cpp @@ -174,8 +174,7 @@ void SDLJoystick::ProcessInput(SDL_Event &event){ case SDL_CONTROLLERAXISMOTION: AxisInput axis; axis.axisId = event.caxis.axis; - // 1.2 to try to approximate the PSP's clamped rectangular range. - axis.value = 1.2 * event.caxis.value * g_Config.fXInputAnalogSensitivity / 32767.0f; + axis.value = event.caxis.value / 32767.0f; if (axis.value > 1.0f) axis.value = 1.0f; if (axis.value < -1.0f) axis.value = -1.0f; axis.deviceId = DEVICE_ID_PAD_0 + getDeviceIndex(event.caxis.which); diff --git a/UI/ComboKeyMappingScreen.cpp b/UI/ComboKeyMappingScreen.cpp index f1582ef9aa..3fd324e845 100644 --- a/UI/ComboKeyMappingScreen.cpp +++ b/UI/ComboKeyMappingScreen.cpp @@ -30,117 +30,167 @@ #include "Common/StringUtils.h" #include "Core/Config.h" -#include "TouchControlVisibilityScreen.h" #include "UI/ComboKeyMappingScreen.h" +class ButtonPreview : public UI::View { +public: + ButtonPreview(ImageID bgImg, ImageID img, float rotationIcon, bool flipShape, float rotationShape, int x, int y) + : View(new UI::AnchorLayoutParams(x, y, UI::NONE, UI::NONE, true)), bgImg_(bgImg), img_(img), rotI_(rotationIcon), + flipS_(flipShape), rotS_(rotationShape), x_(x), y_(y) {} + + void Draw(UIContext &dc) override { + float opacity = g_Config.iTouchButtonOpacity / 100.0f; + + uint32_t colorBg = colorAlpha(g_Config.iTouchButtonStyle != 0 ? 0xFFFFFF : 0xc0b080, opacity); + uint32_t color = colorAlpha(0xFFFFFF, opacity); + + dc.Draw()->DrawImageRotated(bgImg_, x_, y_, 1.0f, rotS_*PI/180, colorBg, flipS_); + dc.Draw()->DrawImageRotated(img_, x_, y_, 1.0f, rotI_*PI/180, color, false); + } +private: + int x_; + int y_; + float rotI_; + float rotS_; + bool flipS_; + ImageID bgImg_; + ImageID img_; +}; + void ComboKeyScreen::CreateViews() { using namespace UI; + using namespace CustomKey; auto co = GetI18NCategory("Controls"); + auto mc = GetI18NCategory("MappableControls"); root_ = new LinearLayout(ORIENT_VERTICAL); - root_->Add(new ItemHeader(co->T("Combo Key Setting"))); + root_->Add(new ItemHeader(co->T("Custom Key Setting"))); LinearLayout *root__ = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(1.0)); root_->Add(root__); LinearLayout *leftColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(120, FILL_PARENT)); auto di = GetI18NCategory("Dialog"); - static const ImageID comboKeyImages[5] = { - ImageID("I_1"), ImageID("I_2"), ImageID("I_3"), ImageID("I_4"), ImageID("I_5"), - }; - - comboselect = new ChoiceStrip(ORIENT_VERTICAL, new AnchorLayoutParams(10, 10, NONE, NONE)); - comboselect->SetSpacing(10); - for (int i = 0; i < 5; i++) { - comboselect->AddChoice(comboKeyImages[i]); - } - comboselect->SetSelection(*mode, false); - comboselect->OnChoice.Handle(this, &ComboKeyScreen::onCombo); - leftColumn->Add(comboselect); - root__->Add(leftColumn); - rightScroll_ = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f)); - leftColumn->Add(new Spacer(new LinearLayoutParams(1.0f))); - leftColumn->Add(new Choice(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnBack); - root__->Add(rightScroll_); - - const int cellSize = 400; - - UI::GridLayoutSettings gridsettings(cellSize, 64, 5); - gridsettings.fillCells = true; - GridLayout *grid = rightScroll_->Add(new GridLayout(gridsettings, new LayoutParams(FILL_PARENT, WRAP_CONTENT))); - - bool *toggle = nullptr; + ConfigCustomButton* cfg = nullptr; + bool* show = nullptr; memset(array, 0, sizeof(array)); - switch (*mode) { + switch (id_) { case 0: - toggle = &g_Config.bComboToggle0; - for (int i = 0; i < 16; i++) - array[i] = (0x01 == ((g_Config.iCombokey0 >> i) & 0x01)); + cfg = &g_Config.CustomKey0; + show = &g_Config.touchCombo0.show; + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) + array[i] = (0x01 == ((g_Config.CustomKey0.key >> i) & 0x01)); break; case 1: - toggle = &g_Config.bComboToggle1; - for (int i = 0; i < 16; i++) - array[i] = (0x01 == ((g_Config.iCombokey1 >> i) & 0x01)); + cfg = &g_Config.CustomKey1; + show = &g_Config.touchCombo1.show; + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) + array[i] = (0x01 == ((g_Config.CustomKey1.key >> i) & 0x01)); break; case 2: - toggle = &g_Config.bComboToggle2; - for (int i = 0; i < 16; i++) - array[i] = (0x01 == ((g_Config.iCombokey2 >> i) & 0x01)); + cfg = &g_Config.CustomKey2; + show = &g_Config.touchCombo2.show; + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) + array[i] = (0x01 == ((g_Config.CustomKey2.key >> i) & 0x01)); break; case 3: - toggle = &g_Config.bComboToggle3; - for (int i = 0; i < 16; i++) - array[i] = (0x01 == ((g_Config.iCombokey3 >> i) & 0x01)); + cfg = &g_Config.CustomKey3; + show = &g_Config.touchCombo3.show; + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) + array[i] = (0x01 == ((g_Config.CustomKey3.key >> i) & 0x01)); break; case 4: - toggle = &g_Config.bComboToggle4; - for (int i = 0; i < 16; i++) - array[i] = (0x01 == ((g_Config.iCombokey4 >> i) & 0x01)); + cfg = &g_Config.CustomKey4; + show = &g_Config.touchCombo4.show; + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) + array[i] = (0x01 == ((g_Config.CustomKey4.key >> i) & 0x01)); + break; + case 5: + cfg = &g_Config.CustomKey5; + show = &g_Config.touchCombo5.show; + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) + array[i] = (0x01 == ((g_Config.CustomKey5.key >> i) & 0x01)); + break; + case 6: + cfg = &g_Config.CustomKey6; + show = &g_Config.touchCombo6.show; + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) + array[i] = (0x01 == ((g_Config.CustomKey6.key >> i) & 0x01)); + break; + case 7: + cfg = &g_Config.CustomKey7; + show = &g_Config.touchCombo7.show; + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) + array[i] = (0x01 == ((g_Config.CustomKey7.key >> i) & 0x01)); + break; + case 8: + cfg = &g_Config.CustomKey8; + show = &g_Config.touchCombo8.show; + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) + array[i] = (0x01 == ((g_Config.CustomKey8.key >> i) & 0x01)); + break; + case 9: + cfg = &g_Config.CustomKey9; + show = &g_Config.touchCombo9.show; + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) + array[i] = (0x01 == ((g_Config.CustomKey9.key >> i) & 0x01)); break; default: // This shouldn't happen, let's just not crash. - toggle = &g_Config.bComboToggle0; + cfg = &g_Config.CustomKey0; + show = &g_Config.touchCombo0.show; break; } - std::map keyImages; - keyImages["Circle"] = ImageID("I_CIRCLE"); - keyImages["Cross"] = ImageID("I_CROSS"); - keyImages["Square"] = ImageID("I_SQUARE"); - keyImages["Triangle"] = ImageID("I_TRIANGLE"); - keyImages["L"] = ImageID("I_L"); - keyImages["R"] = ImageID("I_R"); - keyImages["Start"] = ImageID("I_START"); - keyImages["Select"] = ImageID("I_SELECT"); - keyToggles["Circle"] = &array[13]; - keyToggles["Cross"] = &array[14]; - keyToggles["Square"] = &array[15]; - keyToggles["Triangle"] = &array[12]; - keyToggles["L"] = &array[8]; - keyToggles["R"] = &array[9]; - keyToggles["Left"] = &array[7]; - keyToggles["Up"] = &array[4]; - keyToggles["Right"] = &array[5]; - keyToggles["Down"] = &array[6]; - keyToggles["Start"] = &array[3]; - keyToggles["Select"] = &array[0]; + leftColumn->Add(new ButtonPreview(g_Config.iTouchButtonStyle == 0 ? comboKeyShapes[cfg->shape].i : comboKeyShapes[cfg->shape].l, + comboKeyImages[cfg->image].i, comboKeyImages[cfg->image].r, comboKeyShapes[cfg->shape].f, comboKeyShapes[cfg->shape].r, 62, 82)); - std::map::iterator imageFinder; + root__->Add(leftColumn); + rightScroll_ = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1.0f)); + leftColumn->Add(new Spacer(new LinearLayoutParams(1.0f))); + leftColumn->Add(new Choice(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnBack); + root__->Add(rightScroll_); - auto mc = GetI18NCategory("MappableControls"); + LinearLayout *vertLayout = new LinearLayout(ORIENT_VERTICAL); + rightScroll_->Add(vertLayout); + + vertLayout->Add(new ItemHeader(co->T("Button Style"))); + vertLayout->Add(new CheckBox(show, co->T("Visible"))); - for (auto i = keyToggles.begin(); i != keyToggles.end(); ++i) { + // All icon and name are defined in GamepadEmu.h + static const char *imageNames[ARRAY_SIZE(comboKeyImages)]; + for (int i = 0; i < ARRAY_SIZE(imageNames); ++i) { + imageNames[i] = comboKeyImages[i].n; + } + PopupMultiChoice *icon = vertLayout->Add(new PopupMultiChoice(&(cfg->image), co->T("Icon"), imageNames, 0, ARRAY_SIZE(imageNames), mc->GetName(), screenManager())); + icon->OnChoice.Handle(this, &ComboKeyScreen::onCombo); + + // All shape and name are defined in GamepadEmu.h + static const char *shapeNames[ARRAY_SIZE(comboKeyShapes)]; + for (int i = 0; i < ARRAY_SIZE(shapeNames); ++i) { + shapeNames[i] = comboKeyShapes[i].n; + } + vertLayout->Add(new PopupMultiChoice(&(cfg->shape), co->T("Shape"), shapeNames, 0, ARRAY_SIZE(shapeNames), mc->GetName(), screenManager()))->OnChoice.Handle(this, &ComboKeyScreen::onCombo); + + vertLayout->Add(new ItemHeader(co->T("Button Binding"))); + vertLayout->Add(new CheckBox(&(cfg->toggle), co->T("Toggle mode"))); + + const int cellSize = 400; + UI::GridLayoutSettings gridsettings(cellSize, 64, 5); + gridsettings.fillCells = true; + GridLayout *grid = vertLayout->Add(new GridLayout(gridsettings, new LayoutParams(FILL_PARENT, WRAP_CONTENT))); + + // Button name, image and action are defined in GamepadEmu.h + for (int i = 0; i < ARRAY_SIZE(comboKeyList); ++i) { LinearLayout *row = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); row->SetSpacing(0); - CheckBox *checkbox = new CheckBox(i->second, "", "", new LinearLayoutParams(50, WRAP_CONTENT)); + CheckBox *checkbox = new CheckBox(&array[i], "", "", new LinearLayoutParams(50, WRAP_CONTENT)); row->Add(checkbox); - imageFinder = keyImages.find(i->first); Choice *choice; - if (imageFinder != keyImages.end()) { - choice = new Choice(keyImages[imageFinder->first], new LinearLayoutParams(1.0f)); - } - else { - choice = new Choice(mc->T(i->first.c_str()), new LinearLayoutParams(1.0f)); + if (comboKeyList[i].i.isValid()) { + choice = new Choice(comboKeyList[i].i, new LinearLayoutParams(1.0f)); + } else { + choice = new Choice(mc->T(comboKeyList[i].n), new LinearLayoutParams(1.0f)); } ChoiceEventHandler *choiceEventHandler = new ChoiceEventHandler(checkbox); @@ -150,76 +200,67 @@ void ComboKeyScreen::CreateViews() { row->Add(choice); grid->Add(row); - } - - LinearLayout *row = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); - row->SetSpacing(0); - - CheckBox *checkbox = new CheckBox(toggle, "", "", new LinearLayoutParams(50, WRAP_CONTENT)); - row->Add(checkbox); - - Choice *choice = new Choice(mc->T("Toggle mode"), new LinearLayoutParams(1.0f)); - ChoiceEventHandler *choiceEventHandler = new ChoiceEventHandler(checkbox); - choice->OnClick.Handle(choiceEventHandler, &ChoiceEventHandler::onChoiceClick); - choice->SetCentered(true); - - row->Add(choice); - grid->Add(row); } -static int arrayToInt(bool ary[16]) { - int value = 0; - for (int i = 15; i >= 0; i--) { +static uint64_t arrayToInt(bool ary[ARRAY_SIZE(CustomKey::comboKeyList)]) { + uint64_t value = 0; + for (int i = ARRAY_SIZE(CustomKey::comboKeyList)-1; i >= 0; i--) { value |= ary[i] ? 1 : 0; - value = value << 1; + if (i > 0) { + value = value << 1; + } + } + return value; +} + +void ComboKeyScreen::saveArray() { + switch (id_) { + case 0: + g_Config.CustomKey0.key = arrayToInt(array); + break; + case 1: + g_Config.CustomKey1.key = arrayToInt(array); + break; + case 2: + g_Config.CustomKey2.key = arrayToInt(array); + break; + case 3: + g_Config.CustomKey3.key = arrayToInt(array); + break; + case 4: + g_Config.CustomKey4.key = arrayToInt(array); + break; + case 5: + g_Config.CustomKey5.key = arrayToInt(array); + break; + case 6: + g_Config.CustomKey6.key = arrayToInt(array); + break; + case 7: + g_Config.CustomKey7.key = arrayToInt(array); + break; + case 8: + g_Config.CustomKey8.key = arrayToInt(array); + break; + case 9: + g_Config.CustomKey9.key = arrayToInt(array); + break; } - return value >> 1; } void ComboKeyScreen::onFinish(DialogResult result) { - switch (*mode) { - case 0: - g_Config.iCombokey0 = arrayToInt(array); - break; - case 1: - g_Config.iCombokey1 = arrayToInt(array); - break; - case 2: - g_Config.iCombokey2 = arrayToInt(array); - break; - case 3: - g_Config.iCombokey3 = arrayToInt(array); - break; - case 4: - g_Config.iCombokey4 = arrayToInt(array); - break; - } - g_Config.Save("ComboKeyScreen::onFInish"); + saveArray(); + g_Config.Save("ComboKeyScreen::onFinish"); } UI::EventReturn ComboKeyScreen::ChoiceEventHandler::onChoiceClick(UI::EventParams &e){ checkbox_->Toggle(); - - return UI::EVENT_DONE; }; UI::EventReturn ComboKeyScreen::onCombo(UI::EventParams &e) { - switch (*mode){ - case 0:g_Config.iCombokey0 = arrayToInt(array); - break; - case 1:g_Config.iCombokey1 = arrayToInt(array); - break; - case 2:g_Config.iCombokey2 = arrayToInt(array); - break; - case 3:g_Config.iCombokey3 = arrayToInt(array); - break; - case 4:g_Config.iCombokey4 = arrayToInt(array); - } - *mode = comboselect->GetSelection(); + saveArray(); CreateViews(); return UI::EVENT_DONE; } - - diff --git a/UI/ComboKeyMappingScreen.h b/UI/ComboKeyMappingScreen.h index bafc3a974b..aa4fd71a06 100644 --- a/UI/ComboKeyMappingScreen.h +++ b/UI/ComboKeyMappingScreen.h @@ -18,6 +18,7 @@ #pragma once #include "MiscScreens.h" +#include "UI/GamepadEmu.h" #include @@ -27,15 +28,16 @@ namespace UI { class ComboKeyScreen : public UIDialogScreenWithBackground { public: - ComboKeyScreen(int *key): mode(key) {} + ComboKeyScreen(int id): id_(id) {} void CreateViews() override; void onFinish(DialogResult result) override; UI::EventReturn onCombo(UI::EventParams &e); private: - std::map keyToggles; - bool array[16]; - int *mode; + void saveArray(); + + bool array[ARRAY_SIZE(CustomKey::comboKeyList)]; + int id_; UI::ChoiceStrip *comboselect; UI::ScrollView *rightScroll_; class ChoiceEventHandler{ diff --git a/UI/ControlMappingScreen.cpp b/UI/ControlMappingScreen.cpp index 2fc9fba4d6..ed1e0a71cc 100644 --- a/UI/ControlMappingScreen.cpp +++ b/UI/ControlMappingScreen.cpp @@ -42,9 +42,9 @@ #include "UI/ControlMappingScreen.h" #include "UI/GameSettingsScreen.h" -class ControlMapper : public UI::LinearLayout { +class SingleControlMapper : public UI::LinearLayout { public: - ControlMapper(ControlMappingScreen *ctrlScreen, int pspKey, std::string keyName, ScreenManager *scrm, UI::LinearLayoutParams *layoutParams = 0); + SingleControlMapper(ControlMappingScreen *ctrlScreen, int pspKey, std::string keyName, ScreenManager *scrm, UI::LinearLayoutParams *layoutParams = 0); void Update() override; int GetPspKey() const { return pspKey_; } @@ -75,12 +75,12 @@ private: bool refresh_; }; -ControlMapper::ControlMapper(ControlMappingScreen *ctrlScreen, int pspKey, std::string keyName, ScreenManager *scrm, UI::LinearLayoutParams *layoutParams) +SingleControlMapper::SingleControlMapper(ControlMappingScreen *ctrlScreen, int pspKey, std::string keyName, ScreenManager *scrm, UI::LinearLayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_VERTICAL, layoutParams), ctrlScreen_(ctrlScreen), action_(NONE), pspKey_(pspKey), keyName_(keyName), scrm_(scrm), refresh_(false) { Refresh(); } -void ControlMapper::Update() { +void SingleControlMapper::Update() { if (refresh_) { refresh_ = false; Refresh(); @@ -88,7 +88,7 @@ void ControlMapper::Update() { } } -void ControlMapper::Refresh() { +void SingleControlMapper::Refresh() { bool hasFocus = UI::GetFocusedView() == this; Clear(); auto mc = GetI18NCategory("MappableControls"); @@ -114,19 +114,19 @@ void ControlMapper::Refresh() { // First, look among images. if (iter != keyImages.end()) { Choice *c = root->Add(new Choice(iter->second, new LinearLayoutParams(200, itemH))); - c->OnClick.Handle(this, &ControlMapper::OnReplaceAll); + c->OnClick.Handle(this, &SingleControlMapper::OnReplaceAll); } else { // No image? Let's translate. Choice *c = new Choice(mc->T(keyName_.c_str()), new LinearLayoutParams(200, itemH)); c->SetCentered(true); - root->Add(c)->OnClick.Handle(this, &ControlMapper::OnReplaceAll); + root->Add(c)->OnClick.Handle(this, &SingleControlMapper::OnReplaceAll); } Choice *p = root->Add(new Choice(" + ", new LayoutParams(WRAP_CONTENT, itemH))); - p->OnClick.Handle(this, &ControlMapper::OnAdd); + p->OnClick.Handle(this, &SingleControlMapper::OnAdd); if (g_Config.bMouseControl) { Choice *p = root->Add(new Choice("M", new LayoutParams(WRAP_CONTENT, itemH))); - p->OnClick.Handle(this, &ControlMapper::OnAddMouse); + p->OnClick.Handle(this, &SingleControlMapper::OnAddMouse); } LinearLayout *rightColumn = root->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f))); @@ -145,24 +145,24 @@ void ControlMapper::Refresh() { char tagbuf[16]; sprintf(tagbuf, "%i", (int)i); c->SetTag(tagbuf); - c->OnClick.Handle(this, &ControlMapper::OnReplace); + c->OnClick.Handle(this, &SingleControlMapper::OnReplace); Choice *d = row->Add(new Choice(" X ", new LayoutParams(WRAP_CONTENT, itemH))); d->SetTag(tagbuf); - d->OnClick.Handle(this, &ControlMapper::OnDelete); + d->OnClick.Handle(this, &SingleControlMapper::OnDelete); } if (mappings.size() == 0) { // look like an empty line Choice *c = rightColumn->Add(new Choice("", new LinearLayoutParams(FILL_PARENT, itemH))); - c->OnClick.Handle(this, &ControlMapper::OnAdd); + c->OnClick.Handle(this, &SingleControlMapper::OnAdd); } if (hasFocus) this->SetFocus(); } -void ControlMapper::MappedCallback(KeyDef kdf) { +void SingleControlMapper::MappedCallback(KeyDef kdf) { switch (action_) { case ADD: KeyMap::SetKeyMapping(pspKey_, kdf, false); @@ -183,36 +183,36 @@ void ControlMapper::MappedCallback(KeyDef kdf) { // After this, we do not exist any more. So the refresh_ = true is probably irrelevant. } -UI::EventReturn ControlMapper::OnReplace(UI::EventParams ¶ms) { +UI::EventReturn SingleControlMapper::OnReplace(UI::EventParams ¶ms) { actionIndex_ = atoi(params.v->Tag().c_str()); action_ = REPLACEONE; auto km = GetI18NCategory("KeyMapping"); - scrm_->push(new KeyMappingNewKeyDialog(pspKey_, true, std::bind(&ControlMapper::MappedCallback, this, std::placeholders::_1), km)); + scrm_->push(new KeyMappingNewKeyDialog(pspKey_, true, std::bind(&SingleControlMapper::MappedCallback, this, std::placeholders::_1), km)); return UI::EVENT_DONE; } -UI::EventReturn ControlMapper::OnReplaceAll(UI::EventParams ¶ms) { +UI::EventReturn SingleControlMapper::OnReplaceAll(UI::EventParams ¶ms) { action_ = REPLACEALL; auto km = GetI18NCategory("KeyMapping"); - scrm_->push(new KeyMappingNewKeyDialog(pspKey_, true, std::bind(&ControlMapper::MappedCallback, this, std::placeholders::_1), km)); + scrm_->push(new KeyMappingNewKeyDialog(pspKey_, true, std::bind(&SingleControlMapper::MappedCallback, this, std::placeholders::_1), km)); return UI::EVENT_DONE; } -UI::EventReturn ControlMapper::OnAdd(UI::EventParams ¶ms) { +UI::EventReturn SingleControlMapper::OnAdd(UI::EventParams ¶ms) { action_ = ADD; auto km = GetI18NCategory("KeyMapping"); - scrm_->push(new KeyMappingNewKeyDialog(pspKey_, true, std::bind(&ControlMapper::MappedCallback, this, std::placeholders::_1), km)); + scrm_->push(new KeyMappingNewKeyDialog(pspKey_, true, std::bind(&SingleControlMapper::MappedCallback, this, std::placeholders::_1), km)); return UI::EVENT_DONE; } -UI::EventReturn ControlMapper::OnAddMouse(UI::EventParams ¶ms) { +UI::EventReturn SingleControlMapper::OnAddMouse(UI::EventParams ¶ms) { action_ = ADD; g_Config.bMapMouse = true; auto km = GetI18NCategory("KeyMapping"); - scrm_->push(new KeyMappingNewMouseKeyDialog(pspKey_, true, std::bind(&ControlMapper::MappedCallback, this, std::placeholders::_1), km)); + scrm_->push(new KeyMappingNewMouseKeyDialog(pspKey_, true, std::bind(&SingleControlMapper::MappedCallback, this, std::placeholders::_1), km)); return UI::EVENT_DONE; } -UI::EventReturn ControlMapper::OnDelete(UI::EventParams ¶ms) { +UI::EventReturn SingleControlMapper::OnDelete(UI::EventParams ¶ms) { int index = atoi(params.v->Tag().c_str()); KeyMap::g_controllerMap[pspKey_].erase(KeyMap::g_controllerMap[pspKey_].begin() + index); KeyMap::g_controllerMapGeneration++; @@ -237,7 +237,7 @@ void ControlMappingScreen::CreateViews() { if (!KeyMap::HasBuiltinController(sysName) && KeyMap::GetSeenPads().size()) { leftColumn->Add(new Choice(km->T("Autoconfigure")))->OnClick.Handle(this, &ControlMappingScreen::OnAutoConfigure); } - leftColumn->Add(new Choice(km->T("Test Analogs")))->OnClick.Handle(this, &ControlMappingScreen::OnTestAnalogs); + leftColumn->Add(new Spacer(new LinearLayoutParams(1.0f))); AddStandardBack(leftColumn); @@ -252,7 +252,7 @@ void ControlMappingScreen::CreateViews() { std::vector mappableKeys = KeyMap::GetMappableKeys(); for (size_t i = 0; i < mappableKeys.size(); i++) { - ControlMapper *mapper = rightColumn->Add(new ControlMapper(this, mappableKeys[i].key, mappableKeys[i].name, screenManager(), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + SingleControlMapper *mapper = rightColumn->Add(new SingleControlMapper(this, mappableKeys[i].key, mappableKeys[i].name, screenManager(), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); mappers_.push_back(mapper); } } @@ -283,11 +283,6 @@ UI::EventReturn ControlMappingScreen::OnAutoConfigure(UI::EventParams ¶ms) { return UI::EVENT_DONE; } -UI::EventReturn ControlMappingScreen::OnTestAnalogs(UI::EventParams ¶ms) { - screenManager()->push(new AnalogTestScreen()); - return UI::EVENT_DONE; -} - void ControlMappingScreen::dialogFinished(const Screen *dialog, DialogResult result) { if (result == DR_OK && dialog->tag() == "listpopup") { ListPopupScreen *popup = (ListPopupScreen *)dialog; @@ -428,22 +423,21 @@ bool KeyMappingNewMouseKeyDialog::axis(const AxisInput &axis) { return true; } +enum class StickHistoryViewType { + INPUT, + OUTPUT +}; + class JoystickHistoryView : public UI::InertView { public: - JoystickHistoryView(int xAxis, int xDevice, int xDir, int yAxis, int yDevice, int yDir, UI::LayoutParams *layoutParams = nullptr) - : UI::InertView(layoutParams), - xAxis_(xAxis), xDir_(xDir), - yAxis_(yAxis), yDir_(yDir) {} + JoystickHistoryView(StickHistoryViewType type, std::string title, UI::LayoutParams *layoutParams = nullptr) + : UI::InertView(layoutParams), title_(title), type_(type) {} void Draw(UIContext &dc) override; - std::string DescribeText() const override { return ""; } + std::string DescribeText() const override { return "Analog Stick View"; } void Update() override; - void Axis(const AxisInput &input) override { - // TODO: Check input.deviceId? - if (input.axisId == xAxis_) { - curX_ = input.value * xDir_; - } else if (input.axisId == yAxis_) { - curY_ = input.value * yDir_; - } + void SetXY(float x, float y) { + curX_ = x; + curY_ = y; } private: @@ -452,128 +446,195 @@ private: float y; }; - int xAxis_; - int xDir_; - int yAxis_; - int yDir_; - float curX_ = 0.0f; float curY_ = 0.0f; std::deque locations_; int maxCount_ = 500; + std::string title_; + StickHistoryViewType type_; }; void JoystickHistoryView::Draw(UIContext &dc) { - if (xAxis_ > -1 && yAxis_ > -1) { - const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(ImageID("I_CROSS")); - if (!image) { - return; - } - float minRadius = std::min(bounds_.w, bounds_.h) * 0.5f - image->w; - dc.BeginNoTex(); - dc.Draw()->RectOutline(bounds_.centerX() - minRadius, bounds_.centerY() - minRadius, minRadius * 2.0f, minRadius * 2.0f, 0x80FFFFFF); - dc.Flush(); - dc.Begin(); - int a = maxCount_ - (int)locations_.size(); - for (auto iter = locations_.begin(); iter != locations_.end(); ++iter) { - float x = bounds_.centerX() + minRadius * iter->x; - float y = bounds_.centerY() - minRadius * iter->y; - float alpha = (float)a / maxCount_; - if (alpha < 0.0f) { - alpha = 0.0f; - } - dc.Draw()->DrawImage(ImageID("I_CROSS"), x, y, 0.8f, colorAlpha(0xFFFFFF, alpha), ALIGN_CENTER); - a++; - } - dc.Flush(); - } else { - dc.Begin(); - dc.DrawText("N/A", bounds_.centerX(), bounds_.centerY(), 0xFFFFFFFF, ALIGN_CENTER); - dc.Flush(); + const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(ImageID("I_CROSS")); + if (!image) { + return; } + float minRadius = std::min(bounds_.w, bounds_.h) * 0.5f - image->w; + dc.Begin(); + dc.DrawTextShadow(title_.c_str(), bounds_.centerX(), bounds_.centerY() + minRadius + 5.0, 0xFFFFFFFF, ALIGN_TOP | ALIGN_HCENTER); + dc.Flush(); + dc.BeginNoTex(); + dc.Draw()->RectOutline(bounds_.centerX() - minRadius, bounds_.centerY() - minRadius, minRadius * 2.0f, minRadius * 2.0f, 0x80FFFFFF); + dc.Flush(); + dc.Begin(); + + // First draw a grid. + float dx = 1.0f / 10.0f; + for (int ix = -10; ix <= 10; ix++) { + // First draw vertical lines. + float fx = ix * dx; + for (int iy = -10; iy < 10; iy++) { + float ax = fx; + float ay = iy * dx; + float bx = fx; + float by = (iy + 1) * dx; + + if (type_ == StickHistoryViewType::OUTPUT) { + ConvertAnalogStick(ax, ay); + ConvertAnalogStick(bx, by); + } + + ax = ax * minRadius + bounds_.centerX(); + ay = ay * minRadius + bounds_.centerY(); + + bx = bx * minRadius + bounds_.centerX(); + by = by * minRadius + bounds_.centerY(); + + dc.Draw()->Line(dc.theme->whiteImage, ax, ay, bx, by, 1.0, 0x70FFFFFF); + } + } + + for (int iy = -10; iy <= 10; iy++) { + // Then horizontal. + float fy = iy * dx; + for (int ix = -10; ix < 10; ix++) { + float ax = ix * dx; + float ay = fy; + float bx = (ix + 1) * dx; + float by = fy; + + if (type_ == StickHistoryViewType::OUTPUT) { + ConvertAnalogStick(ax, ay); + ConvertAnalogStick(bx, by); + } + + ax = ax * minRadius + bounds_.centerX(); + ay = ay * minRadius + bounds_.centerY(); + + bx = bx * minRadius + bounds_.centerX(); + by = by * minRadius + bounds_.centerY(); + + dc.Draw()->Line(dc.theme->whiteImage, ax, ay, bx, by, 1.0, 0x70FFFFFF); + } + } + + + int a = maxCount_ - (int)locations_.size(); + for (auto iter = locations_.begin(); iter != locations_.end(); ++iter) { + float x = bounds_.centerX() + minRadius * iter->x; + float y = bounds_.centerY() - minRadius * iter->y; + float alpha = (float)a / (float)(maxCount_ - 1); + if (alpha < 0.0f) { + alpha = 0.0f; + } + // Emphasize the newest (higher) ones. + alpha = powf(alpha, 3.7f); + // Highlight the output. + if (alpha >= 1.0f && type_ == StickHistoryViewType::OUTPUT) { + dc.Draw()->DrawImage(ImageID("I_CIRCLE"), x, y, 1.0f, colorAlpha(0xFFFFFF, 1.0), ALIGN_CENTER); + } else { + dc.Draw()->DrawImage(ImageID("I_CIRCLE"), x, y, 0.8f, colorAlpha(0xC0C0C0, alpha * 0.5f), ALIGN_CENTER); + } + a++; + } + dc.Flush(); } void JoystickHistoryView::Update() { - if (xAxis_ > -1 && yAxis_ > -1) { - locations_.push_back(Location{ curX_, curY_ }); - if ((int)locations_.size() > maxCount_) { - locations_.pop_front(); - } + locations_.push_back(Location{ curX_, curY_ }); + if ((int)locations_.size() > maxCount_) { + locations_.pop_front(); } } -bool AnalogTestScreen::key(const KeyInput &key) { - bool retval = true; +AnalogSetupScreen::AnalogSetupScreen() { + mapper_.SetCallbacks([](int vkey) {}, [](int vkey) {}, [&](int stick, float x, float y) { + analogX_[stick] = x; + analogY_[stick] = y; + }); + mapper_.SetRawCallback([&](int stick, float x, float y) { + rawX_[stick] = x; + rawY_[stick] = y; + }); +} + +void AnalogSetupScreen::update() { + mapper_.Update(); + // We ignore the secondary stick for now and just use the two views + // for raw and psp input. + if (stickView_[0]) { + stickView_[0]->SetXY(analogX_[0], analogY_[0]); + } + if (stickView_[1]) { + stickView_[1]->SetXY(rawX_[0], rawY_[0]); + } + UIScreen::update(); +} + +bool AnalogSetupScreen::key(const KeyInput &key) { + // Allow testing auto-rotation + bool pauseTrigger = false; + mapper_.Key(key, &pauseTrigger); + if (UI::IsEscapeKey(key)) { TriggerFinish(DR_BACK); return true; } - - char buf[512]; - snprintf(buf, sizeof(buf), "Keycode: %d Device ID: %d [%s%s%s%s]", key.keyCode, key.deviceId, - (key.flags & KEY_IS_REPEAT) ? "REP" : "", - (key.flags & KEY_UP) ? "UP" : "", - (key.flags & KEY_DOWN) ? "DOWN" : "", - (key.flags & KEY_CHAR) ? "CHAR" : ""); - if (lastLastKeyEvent_ && lastKeyEvent_) { - lastLastKeyEvent_->SetText(lastKeyEvent_->GetText()); - lastKeyEvent_->SetText(buf); - } - return retval; + return true; } -bool AnalogTestScreen::axis(const AxisInput &axis) { - UIScreen::axis(axis); - // This is mainly to catch axis events that would otherwise get translated - // into arrow keys, since seeing keyboard arrow key events appear when using - // a controller would be confusing for the user. - if (IgnoreAxisForMapping(axis.axisId)) - return false; +bool AnalogSetupScreen::axis(const AxisInput &axis) { + // We DON'T call UIScreen::Axis here! Otherwise it'll try to move the UI focus around. + // UIScreen::axis(axis); - if (axis.value > g_Config.fAxisBindThreshold || axis.value < -g_Config.fAxisBindThreshold) { - char buf[512]; - snprintf(buf, sizeof(buf), "Axis: %d (value %1.3f) Device ID: %d", - axis.axisId, axis.value, axis.deviceId); - // Null-check just in case they weren't created yet. - if (lastLastKeyEvent_ && lastKeyEvent_) { - lastLastKeyEvent_->SetText(lastKeyEvent_->GetText()); - lastKeyEvent_->SetText(buf); - } - return true; - } - return false; + // Instead we just send the input directly to the mapper, that we'll visualize. + return mapper_.Axis(axis); } -void AnalogTestScreen::CreateViews() { +void AnalogSetupScreen::CreateViews() { using namespace UI; auto di = GetI18NCategory("Dialog"); - root_ = new LinearLayout(ORIENT_VERTICAL); + root_ = new LinearLayout(ORIENT_HORIZONTAL); + + LinearLayout *leftColumn = root_->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(300.0f, FILL_PARENT))); + LinearLayout *rightColumn = root_->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f))); + + auto co = GetI18NCategory("Controls"); + ScrollView *scroll = leftColumn->Add(new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0))); + + LinearLayout *scrollContents = scroll->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(300.0f, WRAP_CONTENT))); + + scrollContents->Add(new ItemHeader(co->T("Analog Settings", "Analog Settings"))); + + // TODO: Would be nicer if these didn't pop up... + scrollContents->Add(new PopupSliderChoiceFloat(&g_Config.fAnalogDeadzone, 0.0f, 0.5f, co->T("Deadzone radius"), 0.01f, screenManager(), "/ 1.0")); + scrollContents->Add(new PopupSliderChoiceFloat(&g_Config.fAnalogInverseDeadzone, 0.0f, 1.0f, co->T("Low end radius"), 0.01f, screenManager(), "/ 1.0")); + scrollContents->Add(new PopupSliderChoiceFloat(&g_Config.fAnalogSensitivity, 0.0f, 2.0f, co->T("Sensitivity (scale)", "Sensitivity"), 0.01f, screenManager(), "x")); + // TODO: This should probably be a slider. + scrollContents->Add(new CheckBox(&g_Config.bAnalogIsCircular, co->T("Circular stick input"))); + scrollContents->Add(new PopupSliderChoiceFloat(&g_Config.fAnalogAutoRotSpeed, 0.0f, 20.0f, co->T("Auto-rotation speed"), 1.0f, screenManager())); + scrollContents->Add(new Choice(co->T("Reset to defaults")))->OnClick.Handle(this, &AnalogSetupScreen::OnResetToDefaults); LinearLayout *theTwo = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(1.0f)); - int axis1, device1, dir1; - int axis2, device2, dir2; + stickView_[0] = theTwo->Add(new JoystickHistoryView(StickHistoryViewType::OUTPUT, co->T("Calibrated"), new LinearLayoutParams(1.0f))); + stickView_[1] = theTwo->Add(new JoystickHistoryView(StickHistoryViewType::INPUT, co->T("Raw input"), new LinearLayoutParams(1.0f))); - if (!KeyMap::AxisFromPspButton(VIRTKEY_AXIS_X_MAX, &device1, &axis1, &dir1)) axis1 = -1; - if (!KeyMap::AxisFromPspButton(VIRTKEY_AXIS_Y_MAX, &device2, &axis2, &dir2)) axis2 = -1; + rightColumn->Add(theTwo); - theTwo->Add(new JoystickHistoryView(axis1, device1, dir1, axis2, device2, dir2, new LinearLayoutParams(1.0f))); + leftColumn->Add(new Button(di->T("Back"), new LayoutParams(FILL_PARENT, WRAP_CONTENT)))->OnClick.Handle(this, &UIScreen::OnBack); +} - if (!KeyMap::AxisFromPspButton(VIRTKEY_AXIS_RIGHT_X_MAX, &device1, &axis1, &dir1)) axis1 = -1; - if (!KeyMap::AxisFromPspButton(VIRTKEY_AXIS_RIGHT_Y_MAX, &device2, &axis2, &dir2)) axis2 = -1; - - theTwo->Add(new JoystickHistoryView(axis1, device1, dir1, axis2, device2, dir2, new LinearLayoutParams(1.0f))); - - root_->Add(theTwo); - - lastLastKeyEvent_ = root_->Add(new TextView("-", new LayoutParams(FILL_PARENT, WRAP_CONTENT))); - lastLastKeyEvent_->SetTextColor(0x80FFFFFF); // semi-transparent - lastKeyEvent_ = root_->Add(new TextView("-", new LayoutParams(FILL_PARENT, WRAP_CONTENT))); - - root_->Add(new Button(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnBack); +UI::EventReturn AnalogSetupScreen::OnResetToDefaults(UI::EventParams &e) { + g_Config.fAnalogDeadzone = 0.15f; + g_Config.fAnalogInverseDeadzone = 0.0f; + g_Config.fAnalogSensitivity = 1.1f; + g_Config.bAnalogIsCircular = false; + g_Config.fAnalogAutoRotSpeed = 8.0f; + return UI::EVENT_DONE; } bool TouchTestScreen::touch(const TouchInput &touch) { @@ -634,7 +695,12 @@ void TouchTestScreen::CreateViews() { auto di = GetI18NCategory("Dialog"); auto gr = GetI18NCategory("Graphics"); root_ = new LinearLayout(ORIENT_VERTICAL); - LinearLayout *theTwo = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(1.0f)); + LinearLayout *theTwo = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)); + + lastLastKeyEvent_ = theTwo->Add(new TextView("-", new LayoutParams(FILL_PARENT, WRAP_CONTENT))); + lastLastKeyEvent_->SetTextColor(0x80FFFFFF); // semi-transparent + lastKeyEvent_ = theTwo->Add(new TextView("-", new LayoutParams(FILL_PARENT, WRAP_CONTENT))); + root_->Add(theTwo); #if !PPSSPP_PLATFORM(UWP) @@ -664,6 +730,42 @@ extern int display_xres; extern int display_yres; #endif +bool TouchTestScreen::key(const KeyInput &key) { + char buf[512]; + snprintf(buf, sizeof(buf), "Keycode: %d Device ID: %d [%s%s%s%s]", key.keyCode, key.deviceId, + (key.flags & KEY_IS_REPEAT) ? "REP" : "", + (key.flags & KEY_UP) ? "UP" : "", + (key.flags & KEY_DOWN) ? "DOWN" : "", + (key.flags & KEY_CHAR) ? "CHAR" : ""); + if (lastLastKeyEvent_ && lastKeyEvent_) { + lastLastKeyEvent_->SetText(lastKeyEvent_->GetText()); + lastKeyEvent_->SetText(buf); + } + return true; +} + +bool TouchTestScreen::axis(const AxisInput &axis) { + + // This is mainly to catch axis events that would otherwise get translated + // into arrow keys, since seeing keyboard arrow key events appear when using + // a controller would be confusing for the user. + if (IgnoreAxisForMapping(axis.axisId)) + return false; + + + if (axis.value > g_Config.fAxisBindThreshold || axis.value < -g_Config.fAxisBindThreshold) { + char buf[512]; + snprintf(buf, sizeof(buf), "Axis: %d (value %1.3f) Device ID: %d", + axis.axisId, axis.value, axis.deviceId); + // Null-check just in case they weren't created yet. + if (lastLastKeyEvent_ && lastKeyEvent_) { + lastLastKeyEvent_->SetText(lastKeyEvent_->GetText()); + lastKeyEvent_->SetText(buf); + } + } + return true; +} + void TouchTestScreen::render() { UIDialogScreenWithBackground::render(); UIContext *ui_context = screenManager()->getUIContext(); @@ -706,7 +808,7 @@ void TouchTestScreen::render() { g_dpi_scale_x, g_dpi_scale_y, g_dpi_scale_real_x, g_dpi_scale_real_y); - ui_context->DrawTextShadow(buffer, bounds.x + 20.0f, bounds.y + 20.0f, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII); + ui_context->DrawTextShadow(buffer, bounds.centerX(), bounds.y + 20.0f, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII); ui_context->Flush(); } diff --git a/UI/ControlMappingScreen.h b/UI/ControlMappingScreen.h index d9f7776fd4..c8dc1adbe5 100644 --- a/UI/ControlMappingScreen.h +++ b/UI/ControlMappingScreen.h @@ -24,11 +24,13 @@ #include "Common/UI/View.h" #include "Common/UI/UIScreen.h" - #include "Common/Data/Text/I18n.h" + +#include "Core/ControlMapper.h" + #include "UI/MiscScreens.h" -class ControlMapper; +class SingleControlMapper; class ControlMappingScreen : public UIDialogScreenWithBackground { public: @@ -47,7 +49,7 @@ private: virtual void dialogFinished(const Screen *dialog, DialogResult result) override; UI::ScrollView *rightScroll_; - std::vector mappers_; + std::vector mappers_; }; class KeyMappingNewKeyDialog : public PopupScreen { @@ -80,15 +82,15 @@ public: pspBtn_ = btn; } - virtual bool key(const KeyInput &key) override; - virtual bool axis(const AxisInput &axis) override; + bool key(const KeyInput &key) override; + bool axis(const AxisInput &axis) override; protected: void CreatePopupContents(UI::ViewGroup *parent) override; - virtual bool FillVertical() const override { return false; } - virtual bool ShowButtons() const override { return true; } - virtual void OnCompleted(DialogResult result) override {} + bool FillVertical() const override { return false; } + bool ShowButtons() const override { return true; } + void OnCompleted(DialogResult result) override {} private: int pspBtn_; @@ -96,18 +98,31 @@ private: bool mapped_; // Prevent double registrations }; -class AnalogTestScreen : public UIDialogScreenWithBackground { +class JoystickHistoryView; + +class AnalogSetupScreen : public UIDialogScreenWithBackground { public: - AnalogTestScreen() {} + AnalogSetupScreen(); bool key(const KeyInput &key) override; bool axis(const AxisInput &axis) override; -protected: - virtual void CreateViews() override; + void update() override; - UI::TextView *lastKeyEvent_ = nullptr; - UI::TextView *lastLastKeyEvent_ = nullptr; +protected: + void CreateViews() override; + +private: + UI::EventReturn OnResetToDefaults(UI::EventParams &e); + + ControlMapper mapper_; + + float analogX_[2]{}; + float analogY_[2]{}; + float rawX_[2]{}; + float rawY_[2]{}; + + JoystickHistoryView *stickView_[2]{}; }; class TouchTestScreen : public UIDialogScreenWithBackground { @@ -121,6 +136,9 @@ public: bool touch(const TouchInput &touch) override; void render() override; + bool key(const KeyInput &key) override; + bool axis(const AxisInput &axis) override; + protected: struct TrackedTouch { int id; @@ -132,6 +150,9 @@ protected: }; TrackedTouch touches_[MAX_TOUCH_POINTS]{}; + UI::TextView *lastKeyEvent_ = nullptr; + UI::TextView *lastLastKeyEvent_ = nullptr; + void CreateViews() override; UI::EventReturn OnImmersiveModeChange(UI::EventParams &e); diff --git a/UI/DevScreens.cpp b/UI/DevScreens.cpp index 4827a2500c..b91723e6f0 100644 --- a/UI/DevScreens.cpp +++ b/UI/DevScreens.cpp @@ -472,13 +472,9 @@ void SystemInfoScreen::CreateViews() { deviceSpecs->Add(new ItemHeader(si->T("CPU Information"))); deviceSpecs->Add(new InfoItem(si->T("CPU Name", "Name"), cpu_info.brand_string)); -#if PPSSPP_ARCH(ARM) || PPSSPP_ARCH(ARM64) || PPSSPP_ARCH(MIPS) || PPSSPP_ARCH(MIPS64) - deviceSpecs->Add(new InfoItem(si->T("Cores"), StringFromInt(cpu_info.num_cores))); -#else int totalThreads = cpu_info.num_cores * cpu_info.logical_cpu_count; std::string cores = StringFromFormat(si->T("%d (%d per core, %d cores)"), totalThreads, cpu_info.logical_cpu_count, cpu_info.num_cores); deviceSpecs->Add(new InfoItem(si->T("Threads"), cores)); -#endif #if PPSSPP_PLATFORM(IOS) deviceSpecs->Add(new InfoItem(si->T("JIT available"), System_GetPropertyBool(SYSPROP_CAN_JIT) ? di->T("Yes") : di->T("No"))); #endif diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 2aef0ca6f7..1f66cbc159 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -18,6 +18,9 @@ #include "ppsspp_config.h" #include +#include + +using namespace std::placeholders; #include "Common/Render/TextureAtlas.h" #include "Common/GPU/OpenGL/GLFeatures.h" @@ -93,6 +96,7 @@ static AVIDump avi; #endif +// TODO: Ugly! static bool frameStep_; static int lastNumFlips; static bool startDumping; @@ -129,14 +133,50 @@ static void __EmuScreenVblank() #endif } +// Handles control rotation due to internal screen rotation. +// TODO: This should be a callback too, so we don't actually call the __Ctrl functions +// from settings screens, etc. +static void SetPSPAnalog(int stick, float x, float y) { + switch (g_Config.iInternalScreenRotation) { + case ROTATION_LOCKED_HORIZONTAL: + // Standard rotation. No change. + break; + case ROTATION_LOCKED_HORIZONTAL180: + x = -x; + y = -y; + break; + case ROTATION_LOCKED_VERTICAL: + { + float new_y = -x; + x = y; + y = new_y; + break; + } + case ROTATION_LOCKED_VERTICAL180: + { + float new_y = y = x; + x = -y; + y = new_y; + break; + } + default: + break; + } + + __CtrlSetAnalogXY(stick, x, y); +} + EmuScreen::EmuScreen(const Path &filename) - : bootPending_(true), gamePath_(filename), invalid_(true), quit_(false), pauseTrigger_(false), saveStatePreviewShownTime_(0.0), saveStatePreview_(nullptr) { - memset(axisState_, 0, sizeof(axisState_)); + : gamePath_(filename) { saveStateSlot_ = SaveState::GetCurrentSlot(); __DisplayListenVblank(__EmuScreenVblank); frameStep_ = false; lastNumFlips = gpuStats.numFlips; startDumping = false; + controlMapper_.SetCallbacks( + std::bind(&EmuScreen::onVKeyDown, this, _1), + std::bind(&EmuScreen::onVKeyUp, this, _1), + &SetPSPAnalog); // Make sure we don't leave it at powerdown after the last game. // TODO: This really should be handled elsewhere if it isn't. @@ -308,7 +348,6 @@ void EmuScreen::bootComplete() { osm.Show(sc->T("PressESC", "Press ESC to open the pause menu"), 3.0f); } #endif - memset(virtKeys, 0, sizeof(virtKeys)); #if !PPSSPP_PLATFORM(UWP) if (GetGPUBackend() == GPUBackend::OPENGL) { @@ -596,31 +635,6 @@ void EmuScreen::onVKeyDown(int virtualKeyCode) { } #endif - 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_REWIND: if (SaveState::CanRewind()) { SaveState::Rewind(&AfterSaveStateAction); @@ -669,14 +683,6 @@ void EmuScreen::onVKeyDown(int virtualKeyCode) { case VIRTKEY_MUTE_TOGGLE: g_Config.bEnableSound = !g_Config.bEnableSound; break; - case VIRTKEY_ANALOG_ROTATE_CW: - autoRotatingAnalogCW_ = true; - autoRotatingAnalogCCW_ = false; - break; - case VIRTKEY_ANALOG_ROTATE_CCW: - autoRotatingAnalogCW_ = false; - autoRotatingAnalogCCW_ = true; - break; } } @@ -701,302 +707,25 @@ void EmuScreen::onVKeyUp(int virtualKeyCode) { } break; - 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_RAPID_FIRE: __CtrlSetRapidFire(false); break; - case VIRTKEY_ANALOG_ROTATE_CW: - autoRotatingAnalogCW_ = false; - __CtrlSetAnalogX(0.0f, 0); - __CtrlSetAnalogY(0.0f, 0); - break; - - case VIRTKEY_ANALOG_ROTATE_CCW: - autoRotatingAnalogCCW_ = false; - __CtrlSetAnalogX(0.0f, 0); - __CtrlSetAnalogY(0.0f, 0); - break; - default: break; } } -// Handles control rotation due to internal screen rotation. -static void SetPSPAxis(char axis, float value, int stick) { - switch (g_Config.iInternalScreenRotation) { - case ROTATION_LOCKED_HORIZONTAL: - // Standard rotation. - break; - case ROTATION_LOCKED_HORIZONTAL180: - value = -value; - break; - case ROTATION_LOCKED_VERTICAL: - value = axis == 'Y' ? value : -value; - axis = (axis == 'X') ? 'Y' : 'X'; - break; - case ROTATION_LOCKED_VERTICAL180: - value = axis == 'Y' ? -value : value; - axis = (axis == 'X') ? 'Y' : 'X'; - break; - default: - break; - } - if (axis == 'X') - __CtrlSetAnalogX(value, stick); - else if (axis == 'Y') - __CtrlSetAnalogY(value, stick); -} - -inline void EmuScreen::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); -} - bool EmuScreen::key(const KeyInput &key) { Core_NotifyActivity(); - 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; -} - -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 EmuScreen::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; - } - - switch (pspKeyCode) { - case CTRL_NOTE: - if (flags & KEY_DOWN) - g_Config.bEnableSound = !g_Config.bEnableSound; - break; - case CTRL_VOL_UP: - if (flags & KEY_DOWN && g_Config.iGlobalVolume < VOLUME_MAX) - g_Config.iGlobalVolume = g_Config.iGlobalVolume + 1; - break; - case CTRL_VOL_DOWN: - if (flags & KEY_DOWN && g_Config.iGlobalVolume > 0) - g_Config.iGlobalVolume = g_Config.iGlobalVolume - 1; - break; - case CTRL_WLAN: - if (flags & KEY_DOWN) - g_Config.bEnableWlan = !g_Config.bEnableWlan; - break; - case CTRL_SCREEN: - if (flags & KEY_DOWN) - NativeMessageReceived("display layout editor", ""); - 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); - } + return controlMapper_.Key(key, &pauseTrigger_); } bool EmuScreen::axis(const AxisInput &axis) { Core_NotifyActivity(); - 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; -} - -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; - } -} - -void EmuScreen::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 : g_Config.fAxisBindThreshold; - 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); - } - } - } + return controlMapper_.Axis(axis); } class GameInfoBGView : public UI::InertView { @@ -1046,7 +775,7 @@ void EmuScreen::CreateViews() { const Bounds &bounds = screenManager()->getUIContext()->GetLayoutBounds(); InitPadLayout(bounds.w, bounds.h); - root_ = CreatePadLayout(bounds.w, bounds.h, &pauseTrigger_); + root_ = CreatePadLayout(bounds.w, bounds.h, &pauseTrigger_, &controlMapper_); if (g_Config.bShowDeveloperMenu) { root_->Add(new Button(dev->T("DevMenu")))->OnClick.Handle(this, &EmuScreen::OnDevTools); } @@ -1226,16 +955,7 @@ void EmuScreen::update() { if (invalid_) return; - if (autoRotatingAnalogCW_) { - const float now = time_now_d(); - // Clamp to a square - __CtrlSetAnalogX(std::min(1.0f, std::max(-1.0f, 1.42f*cosf(now*-g_Config.fAnalogAutoRotSpeed))), 0); - __CtrlSetAnalogY(std::min(1.0f, std::max(-1.0f, 1.42f*sinf(now*-g_Config.fAnalogAutoRotSpeed))), 0); - } else if (autoRotatingAnalogCCW_) { - const float now = time_now_d(); - __CtrlSetAnalogX(std::min(1.0f, std::max(-1.0f, 1.42f*cosf(now*g_Config.fAnalogAutoRotSpeed))), 0); - __CtrlSetAnalogY(std::min(1.0f, std::max(-1.0f, 1.42f*sinf(now*g_Config.fAnalogAutoRotSpeed))), 0); - } + controlMapper_.Update(); // This is here to support the iOS on screen back button. if (pauseTrigger_) { diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index df2adf9d48..acc8be960b 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -27,6 +27,7 @@ #include "Common/UI/UIScreen.h" #include "Common/UI/Tween.h" #include "Core/KeyMap.h" +#include "Core/ControlMapper.h" struct AxisInput; @@ -62,43 +63,34 @@ private: void bootComplete(); bool hasVisibleUI(); void renderUI(); - void processAxis(const AxisInput &axis, int direction); - void pspKey(int pspKeyCode, int flags); void onVKeyDown(int virtualKeyCode); void onVKeyUp(int virtualKeyCode); - void setVKeyAnalog(char axis, int stick, int virtualKeyMin, int virtualKeyMax, bool setZero = true); void autoLoad(); void checkPowerDown(); UI::Event OnDevMenu; UI::Event OnChatMenu; - bool bootPending_; + bool bootPending_ = true; Path gamePath_; // Something invalid was loaded, don't try to emulate - bool invalid_; - bool quit_; + bool invalid_ = true; + bool quit_ = false; bool stopRender_ = false; std::string errorMessage_; // If set, pauses at the end of the frame. - bool pauseTrigger_; - - // To track mappable virtual keys. We can have as many as we want. - bool virtKeys[VIRTKEY_COUNT]; + bool pauseTrigger_ = false; // In-memory save state used for freezeFrame, which is useful for debugging. std::vector freezeState_; std::string tag_; - // De-noise mapped axis updates - int axisState_[JOYSTICK_AXIS_MAX]; - - double saveStatePreviewShownTime_; - AsyncImageFileView *saveStatePreview_; + double saveStatePreviewShownTime_ = 0.0; + AsyncImageFileView *saveStatePreview_ = nullptr; int saveStateSlot_; UI::CallbackColorTween *loadingViewColor_ = nullptr; @@ -111,6 +103,5 @@ private: UI::Button *cardboardDisableButton_ = nullptr; OnScreenMessagesView *onScreenMessagesView_ = nullptr; - bool autoRotatingAnalogCW_ = false; - bool autoRotatingAnalogCCW_ = false; + ControlMapper controlMapper_; }; diff --git a/UI/GameInfoCache.cpp b/UI/GameInfoCache.cpp index 64e95df69e..6fac432f99 100644 --- a/UI/GameInfoCache.cpp +++ b/UI/GameInfoCache.cpp @@ -23,7 +23,7 @@ #include #include "Common/GPU/thin3d.h" -#include "Common/Thread/PrioritizedWorkQueue.h" +#include "Common/Thread/ThreadManager.h" #include "Common/File/VFS/VFS.h" #include "Common/File/FileUtil.h" #include "Common/File/Path.h" @@ -214,8 +214,8 @@ bool GameInfo::LoadFromPath(const Path &gamePath) { std::shared_ptr GameInfo::GetFileLoader() { if (filePath_.empty()) { - // Happens when workqueue tries to figure out priorities in PrioritizedWorkQueue::Pop(), - // because priority() calls GetFileLoader()... gnarly. + // Happens when workqueue tries to figure out priorities, + // because Priority() calls GetFileLoader()... gnarly. return fileLoader; } if (!fileLoader) { @@ -331,24 +331,28 @@ static bool ReadVFSToString(const char *filename, std::string *contents, std::mu } -class GameInfoWorkItem : public PrioritizedWorkQueueItem { +class GameInfoWorkItem : public Task { public: GameInfoWorkItem(const Path &gamePath, std::shared_ptr &info) : gamePath_(gamePath), info_(info) { } ~GameInfoWorkItem() override { + info_->pending.store(false); + info_->working.store(false); info_->DisposeFileLoader(); + info_->readyEvent.Notify(); } - void run() override { + void Run() override { + // An early-return will result in the destructor running, where we can set + // flags like working and pending. + if (!info_->LoadFromPath(gamePath_)) { - info_->pending = false; return; } // In case of a remote file, check if it actually exists before locking. if (!info_->GetFileLoader()->Exists()) { - info_->pending = false; return; } @@ -371,9 +375,7 @@ public: if (pbp.IsELF()) { goto handleELF; } - ERROR_LOG(LOADER, "invalid pbp '%s'\n", pbpLoader->GetPath().ToVisualString().c_str()); - info_->pending = false; - info_->working = false; + ERROR_LOG(LOADER, "invalid pbp '%s'\n", pbpLoader->GetPath().c_str()); return; } @@ -541,15 +543,11 @@ handleELF: // few files. auto fl = info_->GetFileLoader(); if (!fl) { - info_->pending = false; - info_->working = false; - return; // Happens with UWP currently, TODO... + return; } BlockDevice *bd = constructBlockDevice(info_->GetFileLoader().get()); if (!bd) { - info_->pending = false; - info_->working = false; - return; // nothing to do here.. + return; } ISOFileSystem umd(&handles, bd); @@ -629,27 +627,16 @@ handleELF: info_->installDataSize = info_->GetInstallDataSizeInBytes(); } - info_->pending = false; - info_->working = false; // INFO_LOG(SYSTEM, "Completed writing info for %s", info_->GetTitle().c_str()); } - float priority() override { - auto fl = info_->GetFileLoader(); - if (fl && fl->IsRemote()) { - // Increase the value so remote info loads after non-remote. - return info_->lastAccessedTime + 1000.0f; - } - return info_->lastAccessedTime; - } - private: Path gamePath_; std::shared_ptr info_; DISALLOW_COPY_AND_ASSIGN(GameInfoWorkItem); }; -GameInfoCache::GameInfoCache() : gameInfoWQ_(nullptr) { +GameInfoCache::GameInfoCache() { Init(); } @@ -658,28 +645,15 @@ GameInfoCache::~GameInfoCache() { Shutdown(); } -void GameInfoCache::Init() { - gameInfoWQ_ = new PrioritizedWorkQueue(); - ProcessWorkQueueOnThreadWhile(gameInfoWQ_); -} +void GameInfoCache::Init() {} void GameInfoCache::Shutdown() { CancelAll(); - - if (gameInfoWQ_) { - StopProcessingWorkQueue(gameInfoWQ_); - delete gameInfoWQ_; - gameInfoWQ_ = nullptr; - } } void GameInfoCache::Clear() { CancelAll(); - if (gameInfoWQ_) { - gameInfoWQ_->Flush(); - gameInfoWQ_->WaitUntilDone(); - } info_.clear(); } @@ -706,31 +680,21 @@ void GameInfoCache::FlushBGs() { } void GameInfoCache::PurgeType(IdentifiedFileType fileType) { - if (gameInfoWQ_) - gameInfoWQ_->Flush(); - restart: - for (auto iter = info_.begin(); iter != info_.end(); iter++) { + for (auto iter = info_.begin(); iter != info_.end();) { auto &info = iter->second; - if (!info->working && info->fileType == fileType) { - info_.erase(iter); - goto restart; + info->readyEvent.Wait(); + if (info->fileType == fileType) { + iter = info_.erase(iter); + } else { + iter++; } } } void GameInfoCache::WaitUntilDone(std::shared_ptr &info) { - while (info->pending) { - if (gameInfoWQ_->WaitUntilDone(false)) { - // A true return means everything finished, so bail out. - // This way even if something gets stuck, we won't hang. - break; - } - - // Otherwise, wait again if it's not done... - } + info->readyEvent.Wait(); } - // Runs on the main thread. Only call from render() and similar, not update()! // Can also be called from the audio thread for menu background music. std::shared_ptr GameInfoCache::GetInfo(Draw::DrawContext *draw, const Path &gamePath, int wantFlags) { @@ -776,11 +740,11 @@ std::shared_ptr GameInfoCache::GetInfo(Draw::DrawContext *draw, const } GameInfoWorkItem *item = new GameInfoWorkItem(gamePath, info); - gameInfoWQ_->Add(item); + g_threadManager.EnqueueTask(item, TaskType::IO_BLOCKING); // Don't re-insert if we already have it. if (info_.find(pathStr) == info_.end()) - info_[pathStr] = std::shared_ptr(info); + info_[pathStr] = info; return info; } diff --git a/UI/GameInfoCache.h b/UI/GameInfoCache.h index f2abd58c7d..c39408b231 100644 --- a/UI/GameInfoCache.h +++ b/UI/GameInfoCache.h @@ -23,6 +23,7 @@ #include #include +#include "Common/Thread/Event.h" #include "Core/ELF/ParamSFO.h" #include "Common/File/Path.h" #include "UI/TextureUtil.h" @@ -31,7 +32,6 @@ namespace Draw { class DrawContext; class Texture; } -class PrioritizedWorkQueue; // A GameInfo holds information about a game, and also lets you do things that the VSH // does on the PSP, namely checking for and deleting savedata, and similar things. @@ -140,9 +140,12 @@ public: u64 gameSize = 0; u64 saveDataSize = 0; u64 installDataSize = 0; + std::atomic pending{}; std::atomic working{}; + Event readyEvent; + protected: // Note: this can change while loading, use GetTitle(). std::string title; @@ -182,9 +185,6 @@ private: // Maps ISO path to info. Need to use shared_ptr as we can return these pointers - // and if they get destructed while being in use, that's bad. std::map > info_; - - // Work queue and management - PrioritizedWorkQueue *gameInfoWQ_; }; // This one can be global, no good reason not to. diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 3f1514d838..75b24423f6 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -49,8 +49,8 @@ #include "UI/TouchControlVisibilityScreen.h" #include "UI/TiltAnalogSettingsScreen.h" #include "UI/TiltEventProcessor.h" -#include "UI/ComboKeyMappingScreen.h" #include "UI/GPUDriverTestScreen.h" +#include "UI/MemStickScreen.h" #include "Common/File/FileUtil.h" #include "Common/OSVersion.h" @@ -79,6 +79,14 @@ #include "Windows/W32Util/ShellUtil.h" #endif +#if PPSSPP_PLATFORM(ANDROID) + +#include "android/jni/AndroidAudio.h" + +extern AndroidAudioState *g_audioState; + +#endif + GameSettingsScreen::GameSettingsScreen(const Path &gamePath, std::string gameID, bool editThenRestore) : UIDialogScreenWithGameBackground(gamePath), gameID_(gameID), enableReports_(false), editThenRestore_(editThenRestore) { lastVertical_ = UseVerticalLayout(); @@ -653,7 +661,13 @@ void GameSettingsScreen::CreateViews() { //#if defined(__ANDROID__) CheckBox *extraAudio = audioSettings->Add(new CheckBox(&g_Config.bExtraAudioBuffering, a->T("AudioBufferingForBluetooth", "Bluetooth-friendly buffer (slower)"))); extraAudio->SetEnabledPtr(&g_Config.bEnableSound); -//#endif +//#endif#if defined(__ANDROID__) + // Show OpenSL debug info + const std::string audioErrorStr = AndroidAudio_GetErrorString(g_audioState); + if (!audioErrorStr.empty()) { + audioSettings->Add(new InfoItem(a->T("Audio Error"), audioErrorStr)); + } +#endif PopupSliderChoice *sasVol = audioSettings->Add(new PopupSliderChoice(&g_Config.iSASVolume, 0, MAX_CONFIG_VOLUME, a->T("SAS volume"), screenManager())); sasVol->SetEnabledPtr(&g_Config.bEnableSound); @@ -670,6 +684,7 @@ void GameSettingsScreen::CreateViews() { tabHolder->AddTab(ms->T("Controls"), controlsSettingsScroll); controlsSettings->Add(new ItemHeader(ms->T("Controls"))); controlsSettings->Add(new Choice(co->T("Control Mapping")))->OnClick.Handle(this, &GameSettingsScreen::OnControlMapping); + controlsSettings->Add(new Choice(co->T("Calibrate Analog Stick")))->OnClick.Handle(this, &GameSettingsScreen::OnCalibrateAnalogs); if (!g_Config.bSimpleUI) { #if defined(USING_WIN_UI) @@ -701,7 +716,7 @@ void GameSettingsScreen::CreateViews() { if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) != DEVICE_TYPE_TV) { controlsSettings->Add(new ItemHeader(co->T("OnScreen", "On-Screen Touch Controls"))); controlsSettings->Add(new CheckBox(&g_Config.bShowTouchControls, co->T("OnScreen", "On-Screen Touch Controls"))); - layoutEditorChoice_ = controlsSettings->Add(new Choice(co->T("Custom layout..."))); + layoutEditorChoice_ = controlsSettings->Add(new Choice(co->T("Customize Touch Controls"))); layoutEditorChoice_->OnClick.Handle(this, &GameSettingsScreen::OnTouchControlLayout); layoutEditorChoice_->SetEnabledPtr(&g_Config.bShowTouchControls); @@ -709,11 +724,6 @@ void GameSettingsScreen::CreateViews() { CheckBox *floatingAnalog = controlsSettings->Add(new CheckBox(&g_Config.bAutoCenterTouchAnalog, co->T("Auto-centering analog stick"))); floatingAnalog->SetEnabledPtr(&g_Config.bShowTouchControls); - // Combo key setup - Choice *comboKey = controlsSettings->Add(new Choice(co->T("Combo Key Setup"))); - comboKey->OnClick.Handle(this, &GameSettingsScreen::OnComboKey); - comboKey->SetEnabledPtr(&g_Config.bShowTouchControls); - // On non iOS systems, offer to let the user see this button. // Some Windows touch devices don't have a back button or other button to call up the menu. if (System_GetPropertyBool(SYSPROP_HAS_BACK_BUTTON)) { @@ -741,25 +751,6 @@ void GameSettingsScreen::CreateViews() { style->SetEnabledPtr(&g_Config.bShowTouchControls); } -#ifdef _WIN32 - static const char *inverseDeadzoneModes[] = { "Off", "X", "Y", "X + Y" }; - - controlsSettings->Add(new ItemHeader(co->T("DInput Analog Settings", "DInput Analog Settings"))); - controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fDInputAnalogDeadzone, 0.0f, 1.0f, co->T("Deadzone Radius"), 0.01f, screenManager(), "/ 1.0")); - controlsSettings->Add(new PopupMultiChoice(&g_Config.iDInputAnalogInverseMode, co->T("Analog Mapper Mode"), inverseDeadzoneModes, 0, ARRAY_SIZE(inverseDeadzoneModes), co->GetName(), screenManager())); - controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fDInputAnalogInverseDeadzone, 0.0f, 1.0f, co->T("Analog Mapper Low End", "Analog Mapper Low End (Inverse Deadzone)"), 0.01f, screenManager(), "/ 1.0")); - controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fDInputAnalogSensitivity, 0.0f, 10.0f, co->T("Analog Mapper High End", "Analog Mapper High End (Axis Sensitivity)"), 0.01f, screenManager(), "x")); - - controlsSettings->Add(new ItemHeader(co->T("XInput Analog Settings", "XInput Analog Settings"))); - controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fXInputAnalogDeadzone, 0.0f, 1.0f, co->T("Deadzone Radius"), 0.01f, screenManager(), "/ 1.0")); - controlsSettings->Add(new PopupMultiChoice(&g_Config.iXInputAnalogInverseMode, co->T("Analog Mapper Mode"), inverseDeadzoneModes, 0, ARRAY_SIZE(inverseDeadzoneModes), co->GetName(), screenManager())); - controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fXInputAnalogInverseDeadzone, 0.0f, 1.0f, co->T("Analog Mapper Low End", "Analog Mapper Low End (Inverse Deadzone)"), 0.01f, screenManager(), "/ 1.0")); - controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fXInputAnalogSensitivity, 0.0f, 10.0f, co->T("Analog Mapper High End", "Analog Mapper High End (Axis Sensitivity)"), 0.01f, screenManager(), "x")); -#else - controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fXInputAnalogSensitivity, 0.0f, 10.0f, co->T("Analog Axis Sensitivity", "Analog Axis Sensitivity"), 0.01f, screenManager(), "x")); -#endif - controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fAnalogAutoRotSpeed, 0.0f, 25.0f, co->T("Analog auto-rotation speed"), 1.0f, screenManager())); - controlsSettings->Add(new ItemHeader(co->T("Keyboard", "Keyboard Control Settings"))); #if defined(USING_WIN_UI) controlsSettings->Add(new CheckBox(&g_Config.bIgnoreWindowsKey, co->T("Ignore Windows Key"))); @@ -793,7 +784,7 @@ void GameSettingsScreen::CreateViews() { networkingSettings->Add(new ItemHeader(ms->T("Networking"))); - networkingSettings->Add(new Choice(n->T("Adhoc Multiplayer forum")))->OnClick.Handle(this, &GameSettingsScreen::OnAdhocGuides); + networkingSettings->Add(new Choice(n->T("PPSSPP Ad-Hoc Wiki Page")))->OnClick.Handle(this, &GameSettingsScreen::OnAdhocGuides); networkingSettings->Add(new CheckBox(&g_Config.bEnableWlan, n->T("Enable networking", "Enable networking/wlan (beta)"))); networkingSettings->Add(new ChoiceWithValueDisplay(&g_Config.sMACAddress, n->T("Change Mac Address"), (const char*)nullptr))->OnClick.Handle(this, &GameSettingsScreen::OnChangeMacAddress); @@ -922,7 +913,7 @@ void GameSettingsScreen::CreateViews() { systemSettings->Add(new ItemHeader(sy->T("Emulation"))); - systemSettings->Add(new CheckBox(&g_Config.bFastMemory, sy->T("Fast Memory", "Fast Memory (Unstable)")))->OnClick.Handle(this, &GameSettingsScreen::OnJitAffectingSetting); + systemSettings->Add(new CheckBox(&g_Config.bFastMemory, sy->T("Fast Memory", "Fast Memory")))->OnClick.Handle(this, &GameSettingsScreen::OnJitAffectingSetting); systemSettings->Add(new CheckBox(&g_Config.bIgnoreBadMemAccess, sy->T("Ignore bad memory accesses"))); systemSettings->Add(new CheckBox(&g_Config.bSeparateIOThread, sy->T("I/O on thread (experimental)")))->SetEnabled(!PSP_IsInited()); @@ -939,38 +930,14 @@ void GameSettingsScreen::CreateViews() { PopupSliderChoice *rewindFreq = systemSettings->Add(new PopupSliderChoice(&g_Config.iRewindFlipFrequency, 0, 1800, sy->T("Rewind Snapshot Frequency", "Rewind Snapshot Frequency (mem hog)"), screenManager(), sy->T("frames, 0:off"))); rewindFreq->SetZeroLabel(sy->T("Off")); + systemSettings->Add(new ItemHeader(sy->T("PSP Memory Stick"))); + systemSettings->Add(new CheckBox(&g_Config.bMemStickInserted, sy->T("Memory Stick inserted"))); - systemSettings->Add(new PopupSliderChoice(&g_Config.iMemStickSizeGB, 1, 32, sy->T("Change Memory Stick Size", "Change Memory Stick Size(GB)"), screenManager(), "GB")); + systemSettings->Add(new PopupSliderChoice(&g_Config.iMemStickSizeGB, 1, 32, sy->T("Change Memory Stick Size", "Memory Stick Size (GB)"), screenManager(), "GB")); - } - - systemSettings->Add(new ItemHeader(sy->T("General"))); - -#if PPSSPP_PLATFORM(ANDROID) - if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_MOBILE) { - static const char *screenRotation[] = {"Auto", "Landscape", "Portrait", "Landscape Reversed", "Portrait Reversed", "Landscape Auto"}; - PopupMultiChoice *rot = systemSettings->Add(new PopupMultiChoice(&g_Config.iScreenRotation, co->T("Screen Rotation"), screenRotation, 0, ARRAY_SIZE(screenRotation), co->GetName(), screenManager())); - rot->OnChoice.Handle(this, &GameSettingsScreen::OnScreenRotation); - - if (System_GetPropertyBool(SYSPROP_SUPPORTS_SUSTAINED_PERF_MODE)) { - systemSettings->Add(new CheckBox(&g_Config.bSustainedPerformanceMode, sy->T("Sustained performance mode")))->OnClick.Handle(this, &GameSettingsScreen::OnSustainedPerformanceModeChange); - } - } -#endif - if (!g_Config.bSimpleUI) { - systemSettings->Add(new CheckBox(&g_Config.bCheckForNewVersion, sy->T("VersionCheck", "Check for new versions of PPSSPP"))); - } - systemSettings->Add(new Choice(sy->T("Restore Default Settings")))->OnClick.Handle(this, &GameSettingsScreen::OnRestoreDefaultSettings); - if (!g_Config.bSimpleUI) { - systemSettings->Add(new CheckBox(&g_Config.bEnableStateUndo, sy->T("Savestate slot backups"))); - static const char *autoLoadSaveStateChoices[] = { "Off", "Oldest Save", "Newest Save", "Slot 1", "Slot 2", "Slot 3", "Slot 4", "Slot 5" }; - systemSettings->Add(new PopupMultiChoice(&g_Config.iAutoLoadSaveState, sy->T("Auto Load Savestate"), autoLoadSaveStateChoices, 0, ARRAY_SIZE(autoLoadSaveStateChoices), sy->GetName(), screenManager())); -#if defined(USING_WIN_UI) || defined(USING_QT_UI) || PPSSPP_PLATFORM(ANDROID) - systemSettings->Add(new CheckBox(&g_Config.bBypassOSKWithKeyboard, sy->T("Use system native keyboard"))); -#endif -#if PPSSPP_PLATFORM(ANDROID) + }#if PPSSPP_PLATFORM(ANDROID) memstickDisplay_ = g_Config.memStickDirectory.ToVisualString(); - auto memstickPath = systemSettings->Add(new ChoiceWithValueDisplay(&memstickDisplay_, sy->T("Change Memory Stick folder"), (const char *)nullptr)); + auto memstickPath = systemSettings->Add(new ChoiceWithValueDisplay(&memstickDisplay_, sy->T("Change Memory Stick folder", "Memory Stick folder"), (const char *)nullptr)); memstickPath->SetEnabled(!PSP_IsInited()); memstickPath->OnClick.Handle(this, &GameSettingsScreen::OnChangeMemStickDir); #elif defined(_WIN32) && !PPSSPP_PLATFORM(UWP) @@ -1017,6 +984,29 @@ void GameSettingsScreen::CreateViews() { } } #endif + systemSettings->Add(new ItemHeader(sy->T("General"))); + +#if PPSSPP_PLATFORM(ANDROID) + if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_MOBILE) { + static const char *screenRotation[] = { "Auto", "Landscape", "Portrait", "Landscape Reversed", "Portrait Reversed", "Landscape Auto" }; + PopupMultiChoice *rot = systemSettings->Add(new PopupMultiChoice(&g_Config.iScreenRotation, co->T("Screen Rotation"), screenRotation, 0, ARRAY_SIZE(screenRotation), co->GetName(), screenManager())); + rot->OnChoice.Handle(this, &GameSettingsScreen::OnScreenRotation); + + if (System_GetPropertyBool(SYSPROP_SUPPORTS_SUSTAINED_PERF_MODE)) { + systemSettings->Add(new CheckBox(&g_Config.bSustainedPerformanceMode, sy->T("Sustained performance mode")))->OnClick.Handle(this, &GameSettingsScreen::OnSustainedPerformanceModeChange); + } + } +#endif + if (!g_Config.bSimpleUI) { + systemSettings->Add(new CheckBox(&g_Config.bCheckForNewVersion, sy->T("VersionCheck", "Check for new versions of PPSSPP"))); + } + systemSettings->Add(new Choice(sy->T("Restore Default Settings")))->OnClick.Handle(this, &GameSettingsScreen::OnRestoreDefaultSettings); + systemSettings->Add(new CheckBox(&g_Config.bEnableStateUndo, sy->T("Savestate slot backups"))); + static const char *autoLoadSaveStateChoices[] = { "Off", "Oldest Save", "Newest Save", "Slot 1", "Slot 2", "Slot 3", "Slot 4", "Slot 5" }; + systemSettings->Add(new PopupMultiChoice(&g_Config.iAutoLoadSaveState, sy->T("Auto Load Savestate"), autoLoadSaveStateChoices, 0, ARRAY_SIZE(autoLoadSaveStateChoices), sy->GetName(), screenManager())); +#if defined(USING_WIN_UI) || defined(USING_QT_UI) || PPSSPP_PLATFORM(ANDROID) + systemSettings->Add(new CheckBox(&g_Config.bBypassOSKWithKeyboard, sy->T("Use system native keyboard"))); +#endif #if PPSSPP_ARCH(AMD64) systemSettings->Add(new CheckBox(&g_Config.bCacheFullIsoInRam, sy->T("Cache ISO in RAM", "Cache full ISO in RAM")))->SetEnabled(!PSP_IsInited()); @@ -1109,7 +1099,8 @@ void RecreateActivity() { } UI::EventReturn GameSettingsScreen::OnAdhocGuides(UI::EventParams &e) { - LaunchBrowser("https://forums.ppsspp.org/forumdisplay.php?fid=34"); + auto n = GetI18NCategory("Networking"); + LaunchBrowser(n->T("MultiplayerHowToURL", "https://github.com/hrydgard/ppsspp/wiki/How-to-play-multiplayer-games-with-PPSSPP")); return UI::EVENT_DONE; } @@ -1144,40 +1135,12 @@ UI::EventReturn GameSettingsScreen::OnJitAffectingSetting(UI::EventParams &e) { return UI::EVENT_DONE; } -#if PPSSPP_PLATFORM(ANDROID) - UI::EventReturn GameSettingsScreen::OnChangeMemStickDir(UI::EventParams &e) { - auto sy = GetI18NCategory("System"); - System_InputBoxGetString(sy->T("Memory Stick Folder"), g_Config.memStickDirectory.ToString(), [&](bool result, const std::string &value) { - auto sy = GetI18NCategory("System"); - auto di = GetI18NCategory("Dialog"); - - if (result) { - std::string newPath = value; - size_t pos = newPath.find_last_not_of("/"); - // Gotta have at least something but a /, and also needs to start with a /. - if (newPath.empty() || pos == newPath.npos || newPath[0] != '/') { - settingInfo_->Show(sy->T("ChangingMemstickPathInvalid", "That path couldn't be used to save Memory Stick files."), nullptr); - return; - } - if (pos != newPath.size() - 1) { - newPath = newPath.substr(0, pos + 1); - } - - pendingMemstickFolder_ = newPath; - std::string promptMessage = sy->T("ChangingMemstickPath", "Save games, save states, and other data will not be copied to this folder.\n\nChange the Memory Stick folder?"); - if (!File::Exists(Path(newPath))) { - promptMessage = sy->T("ChangingMemstickPathNotExists", "That folder doesn't exist yet.\n\nSave games, save states, and other data will not be copied to this folder.\n\nCreate a new Memory Stick folder?"); - } - // Add the path for clarity and proper confirmation. - promptMessage += "\n\n" + newPath + "/"; - screenManager()->push(new PromptScreen(promptMessage, di->T("Yes"), di->T("No"), std::bind(&GameSettingsScreen::CallbackMemstickFolder, this, std::placeholders::_1))); - } - }); + screenManager()->push(new MemStickScreen()); return UI::EVENT_DONE; } -#elif defined(_WIN32) && !PPSSPP_PLATFORM(UWP) +#if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) UI::EventReturn GameSettingsScreen::OnSavePathMydoc(UI::EventParams &e) { const Path &PPSSPPpath = File::GetExeDirectory(); @@ -1242,12 +1205,8 @@ UI::EventReturn GameSettingsScreen::OnChangeBackground(UI::EventParams &e) { const Path bgJpg = GetSysDirectory(DIRECTORY_SYSTEM) / "background.jpg"; if (File::Exists(bgPng) || File::Exists(bgJpg)) { - if (File::Exists(bgPng)) { - File::Delete(bgPng); - } - if (File::Exists(bgJpg)) { - File::Delete(bgJpg); - } + File::Delete(bgPng); + File::Delete(bgJpg); NativeMessageReceived("bgImage_updated", ""); } else { @@ -1354,10 +1313,13 @@ void GameSettingsScreen::CallbackMemstickFolder(bool yes) { } File::Delete(Path(testWriteFile)); - File::WriteDataToFile(true, pendingMemstickFolder_.c_str(), (unsigned int)pendingMemstickFolder_.size(), memstickDirFile); - // Save so the settings, at least, are transferred. - g_Config.memStickDirectory = Path(pendingMemstickFolder_); - g_Config.Save("MemstickPathChanged"); + if (!File::WriteDataToFile(true, pendingMemstickFolder_.c_str(), (unsigned int)pendingMemstickFolder_.size(), memstickDirFile)) { + WARN_LOG(SYSTEM, "Failed to write memstick folder to '%s'", memstickDirFile.c_str()); + } else { + // Save so the settings, at least, are transferred. + g_Config.memStickDirectory = Path(pendingMemstickFolder_); + g_Config.Save("MemstickPathChanged"); + } screenManager()->RecreateAllViews(); } } @@ -1547,11 +1509,6 @@ UI::EventReturn GameSettingsScreen::OnChangeMacAddress(UI::EventParams &e) { return UI::EVENT_DONE; } -UI::EventReturn GameSettingsScreen::OnComboKey(UI::EventParams &e) { - screenManager()->push(new ComboKeyScreen(&g_Config.iComboMode)); - return UI::EVENT_DONE; -} - UI::EventReturn GameSettingsScreen::OnLanguage(UI::EventParams &e) { auto dev = GetI18NCategory("Developer"); auto langScreen = new NewLanguageScreen(dev->T("Language")); @@ -1616,6 +1573,11 @@ UI::EventReturn GameSettingsScreen::OnControlMapping(UI::EventParams &e) { return UI::EVENT_DONE; } +UI::EventReturn GameSettingsScreen::OnCalibrateAnalogs(UI::EventParams &e) { + screenManager()->push(new AnalogSetupScreen()); + return UI::EVENT_DONE; +} + UI::EventReturn GameSettingsScreen::OnTouchControlLayout(UI::EventParams &e) { screenManager()->push(new TouchControlLayoutScreen()); return UI::EVENT_DONE; @@ -1955,7 +1917,7 @@ void HostnameSelectScreen::CreatePopupContents(UI::ViewGroup *parent) { buttonsRow2->Add(new Button(di->T("Toggle List")))->OnClick.Handle(this, &HostnameSelectScreen::OnShowIPListClick); buttonsRow2->Add(new Spacer(new LinearLayoutParams(1.0, G_RIGHT))); - std::vector listIP = {"myneighborsushicat.com", "socom.cc", "localhost"}; // TODO: Add some saved recent history too? + std::vector listIP = {"socom.cc", "myneighborsushicat.com", "localhost"}; // TODO: Add some saved recent history too? net::GetIPList(listIP); ipRows_ = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0)); ScrollView* scrollView = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); @@ -2126,52 +2088,3 @@ void HostnameSelectScreen::OnCompleted(DialogResult result) { if (result == DR_OK) *value_ = StripSpaces(addrView_->GetText()); } - -SettingInfoMessage::SettingInfoMessage(int align, UI::AnchorLayoutParams *lp) - : UI::LinearLayout(UI::ORIENT_HORIZONTAL, lp) { - using namespace UI; - SetSpacing(0.0f); - Add(new UI::Spacer(10.0f)); - text_ = Add(new UI::TextView("", align, false, new LinearLayoutParams(1.0, Margins(0, 10)))); - Add(new UI::Spacer(10.0f)); -} - -void SettingInfoMessage::Show(const std::string &text, UI::View *refView) { - if (refView) { - Bounds b = refView->GetBounds(); - const UI::AnchorLayoutParams *lp = GetLayoutParams()->As(); - if (b.y >= cutOffY_) { - ReplaceLayoutParams(new UI::AnchorLayoutParams(lp->width, lp->height, lp->left, 80.0f, lp->right, lp->bottom, lp->center)); - } else { - ReplaceLayoutParams(new UI::AnchorLayoutParams(lp->width, lp->height, lp->left, dp_yres - 80.0f - 40.0f, lp->right, lp->bottom, lp->center)); - } - } - text_->SetText(text); - timeShown_ = time_now_d(); -} - -void SettingInfoMessage::Draw(UIContext &dc) { - static const double FADE_TIME = 1.0; - static const float MAX_ALPHA = 0.9f; - - // Let's show longer messages for more time (guesstimate at reading speed.) - // Note: this will give multibyte characters more time, but they often have shorter words anyway. - double timeToShow = std::max(1.5, text_->GetText().size() * 0.05); - - double sinceShow = time_now_d() - timeShown_; - float alpha = MAX_ALPHA; - if (timeShown_ == 0.0 || sinceShow > timeToShow + FADE_TIME) { - alpha = 0.0f; - } else if (sinceShow > timeToShow) { - alpha = MAX_ALPHA - MAX_ALPHA * (float)((sinceShow - timeToShow) / FADE_TIME); - } - - if (alpha >= 0.1f) { - UI::Style style = dc.theme->popupTitle; - style.background.color = colorAlpha(style.background.color, alpha - 0.1f); - dc.FillRect(style.background, bounds_); - } - - text_->SetTextColor(whiteAlpha(alpha)); - ViewGroup::Draw(dc); -} diff --git a/UI/GameSettingsScreen.h b/UI/GameSettingsScreen.h index a3761603c9..27bf96d562 100644 --- a/UI/GameSettingsScreen.h +++ b/UI/GameSettingsScreen.h @@ -24,8 +24,6 @@ #include "Common/UI/UIScreen.h" #include "UI/MiscScreens.h" -class SettingInfoMessage; - // Per-game settings screen - enables you to configure graphic options, control options, etc // per game. class GameSettingsScreen : public UIDialogScreenWithGameBackground { @@ -71,11 +69,11 @@ private: // Event handlers UI::EventReturn OnControlMapping(UI::EventParams &e); + UI::EventReturn OnCalibrateAnalogs(UI::EventParams &e); UI::EventReturn OnTouchControlLayout(UI::EventParams &e); UI::EventReturn OnDumpNextFrameToLog(UI::EventParams &e); UI::EventReturn OnTiltTypeChange(UI::EventParams &e); UI::EventReturn OnTiltCustomize(UI::EventParams &e); - UI::EventReturn OnComboKey(UI::EventParams &e); // Global settings handlers UI::EventReturn OnLanguage(UI::EventParams &e); @@ -109,9 +107,8 @@ private: UI::EventReturn OnMicDeviceChange(UI::EventParams& e); UI::EventReturn OnAudioDevice(UI::EventParams &e); UI::EventReturn OnJitAffectingSetting(UI::EventParams &e); -#if PPSSPP_PLATFORM(ANDROID) UI::EventReturn OnChangeMemStickDir(UI::EventParams &e); -#elif defined(_WIN32) && !PPSSPP_PLATFORM(UWP) +#if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) UI::EventReturn OnSavePathMydoc(UI::EventParams &e); UI::EventReturn OnSavePathOther(UI::EventParams &e); #endif @@ -142,23 +139,6 @@ private: std::string pendingMemstickFolder_; }; -class SettingInfoMessage : public UI::LinearLayout { -public: - SettingInfoMessage(int align, UI::AnchorLayoutParams *lp); - - void SetBottomCutoff(float y) { - cutOffY_ = y; - } - void Show(const std::string &text, UI::View *refView = nullptr); - - void Draw(UIContext &dc); - -private: - UI::TextView *text_ = nullptr; - double timeShown_ = 0.0; - float cutOffY_; -}; - class DeveloperToolsScreen : public UIDialogScreenWithBackground { public: DeveloperToolsScreen() {} diff --git a/UI/GamepadEmu.cpp b/UI/GamepadEmu.cpp index d38bac4b92..8394a3faa2 100644 --- a/UI/GamepadEmu.cpp +++ b/UI/GamepadEmu.cpp @@ -31,6 +31,7 @@ #include "Core/Core.h" #include "Core/System.h" #include "Core/HLE/sceCtrl.h" +#include "Core/ControlMapper.h" #include "UI/GamepadEmu.h" static u32 GetButtonColor() { @@ -158,68 +159,6 @@ void BoolButton::Touch(const TouchInput &input) { } } -void FPSLimitButton::Touch(const TouchInput &input) { - bool lastDown = pointerDownMask_ != 0; - MultiTouchButton::Touch(input); - bool down = pointerDownMask_ != 0; - - if (!down && lastDown && IsDown()) { - PSP_CoreParameter().fpsLimit = FPSLimit::NORMAL; - } else if (down && !lastDown && PSP_CoreParameter().fpsLimit == FPSLimit::NORMAL) { - int limit = limit_ == FPSLimit::CUSTOM1 ? g_Config.iFpsLimit1 : g_Config.iFpsLimit2; - // Validate it actually has a setting (may this should override visible?) - if (limit >= 0) { - PSP_CoreParameter().fpsLimit = limit_; - } - } -} - -bool FPSLimitButton::IsDown() { - return PSP_CoreParameter().fpsLimit == limit_; -} - -void RapidFireButton::Touch(const TouchInput &input) { - bool lastDown = pointerDownMask_ != 0; - MultiTouchButton::Touch(input); - bool down = pointerDownMask_ != 0; - if (down && !lastDown) { - __CtrlSetRapidFire(!__CtrlGetRapidFire()); - } -} - -bool RapidFireButton::IsDown() { - return __CtrlGetRapidFire(); -} - -void AnalogRotationButton::Touch(const TouchInput &input) { - bool lastDown = pointerDownMask_ != 0; - MultiTouchButton::Touch(input); - bool down = pointerDownMask_ != 0; - if (down && !lastDown) { - autoRotating_ = true; - } else if (lastDown && !down) { - autoRotating_ = false; - __CtrlSetAnalogX(0.0f, 0); - __CtrlSetAnalogY(0.0f, 0); - } -} - -void AnalogRotationButton::Update() { - const float now = time_now_d(); - float delta = now - lastFrameTime_; - if (delta > 0) { - secondsWithoutTouch_ += delta; - } - lastFrameTime_ = now; - - if (autoRotating_) { - float speed = clockWise_ ? -g_Config.fAnalogAutoRotSpeed : g_Config.fAnalogAutoRotSpeed; - // Clamp to a square - __CtrlSetAnalogX(std::min(1.0f, std::max(-1.0f, 1.42f*cosf(now*speed))), 0); - __CtrlSetAnalogY(std::min(1.0f, std::max(-1.0f, 1.42f*sinf(now*speed))), 0); - } -} - void PSPButton::Touch(const TouchInput &input) { bool lastDown = pointerDownMask_ != 0; MultiTouchButton::Touch(input); @@ -234,31 +173,30 @@ void PSPButton::Touch(const TouchInput &input) { } } +bool ComboKey::IsDown() { + return (toggle_ && on_) || (!toggle_ && pointerDownMask_ != 0); +} + void ComboKey::Touch(const TouchInput &input) { + using namespace CustomKey; bool lastDown = pointerDownMask_ != 0; MultiTouchButton::Touch(input); bool down = pointerDownMask_ != 0; - static const int combo[16] = {CTRL_SQUARE ,CTRL_TRIANGLE ,CTRL_CIRCLE ,CTRL_CROSS ,CTRL_UP ,CTRL_DOWN ,CTRL_LEFT ,CTRL_RIGHT ,CTRL_START ,CTRL_SELECT ,CTRL_LTRIGGER ,CTRL_RTRIGGER }; - if (down || lastDown) { - for (int i = 0; i < 16; i++) { - if (pspButtonBit_ & combo[i]) - { - if (down && !lastDown) { - if (g_Config.bHapticFeedback) { - Vibrate(HAPTIC_VIRTUAL_KEY); - } - if (!toggle_) { - __CtrlButtonDown(combo[i]); - } else { - if (__CtrlPeekButtons() & combo[i]) - __CtrlButtonUp(combo[i]); - else - __CtrlButtonDown(combo[i]); - } - } - else if (lastDown && !down && !toggle_) { - __CtrlButtonUp(combo[i]); - } + + if (down && !lastDown) { + if (g_Config.bHapticFeedback) + Vibrate(HAPTIC_VIRTUAL_KEY); + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) { + if (pspButtonBit_ & (1UL << i)) { + controllMapper_->pspKey(comboKeyList[i].c, (on_ && toggle_) ? KEY_UP : KEY_DOWN); + } + } + if (toggle_) + on_ = !on_; + } else if (!toggle_ && lastDown && !down) { + for (int i = 0; i < ARRAY_SIZE(comboKeyList); i++) { + if (pspButtonBit_ & (1UL << i)) { + controllMapper_->pspKey(comboKeyList[i].c, KEY_UP); } } } @@ -441,8 +379,7 @@ void PSPStick::Touch(const TouchInput &input) { dragPointerId_ = -1; centerX_ = bounds_.centerX(); centerY_ = bounds_.centerY(); - __CtrlSetAnalogX(0.0f, stick_); - __CtrlSetAnalogY(0.0f, stick_); + __CtrlSetAnalogXY(stick_, 0.0f, 0.0f); return; } if (input.flags & TOUCH_DOWN) { @@ -492,11 +429,9 @@ void PSPStick::ProcessTouch(float x, float y, bool down) { dx = std::min(1.0f, std::max(-1.0f, dx)); dy = std::min(1.0f, std::max(-1.0f, dy)); - __CtrlSetAnalogX(dx, stick_); - __CtrlSetAnalogY(-dy, stick_); + __CtrlSetAnalogXY(stick_, dx, -dy); } else { - __CtrlSetAnalogX(0.0f, stick_); - __CtrlSetAnalogY(0.0f, stick_); + __CtrlSetAnalogXY(stick_, 0.0f, 0.0f); } } @@ -700,12 +635,6 @@ void InitPadLayout(float xres, float yres, float globalScale) { int unthrottle_key_Y = yres - 60 * scale; initTouchPos(g_Config.touchUnthrottleKey, unthrottle_key_X, unthrottle_key_Y); - initTouchPos(g_Config.touchSpeed1Key, unthrottle_key_X, unthrottle_key_Y - 60 * scale); - initTouchPos(g_Config.touchSpeed2Key, unthrottle_key_X + bottom_key_spacing * scale, unthrottle_key_Y - 60 * scale); - initTouchPos(g_Config.touchRapidFireKey, unthrottle_key_X + 2*bottom_key_spacing * scale, unthrottle_key_Y - 60 * scale); - initTouchPos(g_Config.touchAnalogRotationCCWKey, unthrottle_key_X, unthrottle_key_Y - 120 * scale); - initTouchPos(g_Config.touchAnalogRotationCWKey, unthrottle_key_X + bottom_key_spacing * scale, unthrottle_key_Y - 120 * scale); - // L and R------------------------------------------------------------ // Put them above the analog stick / above the buttons to the right. // The corners were very hard to reach.. @@ -738,9 +667,29 @@ void InitPadLayout(float xres, float yres, float globalScale) { int combo4_key_X = halfW + bottom_key_spacing * scale * 2.2f; int combo4_key_Y = yres / 3; initTouchPos(g_Config.touchCombo4, combo4_key_X, combo4_key_Y); + + int combo5_key_X = halfW - bottom_key_spacing * scale * 1.2f; + int combo5_key_Y = yres / 2; + initTouchPos(g_Config.touchCombo5, combo5_key_X, combo5_key_Y); + + int combo6_key_X = halfW - bottom_key_spacing * scale * 2.2f; + int combo6_key_Y = yres / 2; + initTouchPos(g_Config.touchCombo6, combo6_key_X, combo6_key_Y); + + int combo7_key_X = halfW - bottom_key_spacing * scale * 3.2f; + int combo7_key_Y = yres / 2; + initTouchPos(g_Config.touchCombo7, combo7_key_X, combo7_key_Y); + + int combo8_key_X = halfW - bottom_key_spacing * scale * 1.2f; + int combo8_key_Y = yres / 3; + initTouchPos(g_Config.touchCombo8, combo8_key_X, combo8_key_Y); + + int combo9_key_X = halfW - bottom_key_spacing * scale * 2.2f; + int combo9_key_Y = yres / 3; + initTouchPos(g_Config.touchCombo9, combo9_key_X, combo9_key_Y); } -UI::ViewGroup *CreatePadLayout(float xres, float yres, bool *pause) { +UI::ViewGroup *CreatePadLayout(float xres, float yres, bool *pause, ControlMapper* controllMapper) { using namespace UI; AnchorLayout *root = new AnchorLayout(new LayoutParams(FILL_PARENT, FILL_PARENT)); @@ -767,13 +716,11 @@ UI::ViewGroup *CreatePadLayout(float xres, float yres, bool *pause) { const int halfW = xres / 2; const ImageID roundImage = g_Config.iTouchButtonStyle ? ImageID("I_ROUND_LINE") : ImageID("I_ROUND"); - const ImageID rectImage = g_Config.iTouchButtonStyle ? ImageID("I_RECT_LINE") : ImageID("I_RECT"); const ImageID shoulderImage = g_Config.iTouchButtonStyle ? ImageID("I_SHOULDER_LINE") : ImageID("I_SHOULDER"); const ImageID dirImage = g_Config.iTouchButtonStyle ? ImageID("I_DIR_LINE") : ImageID("I_DIR"); const ImageID stickImage = g_Config.iTouchButtonStyle ? ImageID("I_STICK_LINE") : ImageID("I_STICK"); const ImageID stickBg = g_Config.iTouchButtonStyle ? ImageID("I_STICK_BG_LINE") : ImageID("I_STICK_BG"); - static const ImageID comboKeyImages[5] = { ImageID("I_1"), ImageID("I_2"), ImageID("I_3"), ImageID("I_4"), ImageID("I_5") }; auto addPSPButton = [=](int buttonBit, const char *key, ImageID bgImg, ImageID bgDownImg, ImageID img, const ConfigTouchPos &touch, ButtonOffset off = { 0, 0 }) -> PSPButton * { if (touch.show) { @@ -781,21 +728,21 @@ UI::ViewGroup *CreatePadLayout(float xres, float yres, bool *pause) { } return nullptr; }; - auto addComboKey = [=](int buttonBit, const char *key, bool toggle, ImageID bgImg, ImageID bgDownImg, ImageID img, const ConfigTouchPos &touch) -> ComboKey * { - if (touch.show) { - return root->Add(new ComboKey(buttonBit, key, toggle, bgImg, bgDownImg, img, touch.scale, buttonLayoutParams(touch))); - } - return nullptr; - }; auto addBoolButton = [=](bool *value, const char *key, ImageID bgImg, ImageID bgDownImg, ImageID img, const ConfigTouchPos &touch) -> BoolButton * { if (touch.show) { return root->Add(new BoolButton(value, key, bgImg, bgDownImg, img, touch.scale, buttonLayoutParams(touch))); } return nullptr; }; - auto addFPSLimitButton = [=](FPSLimit value, const char *key, ImageID bgImg, ImageID bgDownImg, ImageID img, const ConfigTouchPos &touch) -> FPSLimitButton * { + auto addComboKey = [=](const ConfigCustomButton& cfg, const char *key, const ConfigTouchPos &touch) -> ComboKey * { + using namespace CustomKey; if (touch.show) { - return root->Add(new FPSLimitButton(value, key, bgImg, bgDownImg, img, touch.scale, buttonLayoutParams(touch))); + auto aux = root->Add(new ComboKey(cfg.key, key, cfg.toggle, controllMapper, + g_Config.iTouchButtonStyle == 0 ? comboKeyShapes[cfg.shape].i : comboKeyShapes[cfg.shape].l, comboKeyShapes[cfg.shape].i, + comboKeyImages[cfg.image].i, touch.scale, buttonLayoutParams(touch))); + aux->SetAngle(comboKeyImages[cfg.image].r, comboKeyShapes[cfg.shape].r); + aux->FlipImageH(comboKeyShapes[cfg.shape].f); + return aux; } return nullptr; }; @@ -828,28 +775,6 @@ UI::ViewGroup *CreatePadLayout(float xres, float yres, bool *pause) { }); } - if (g_Config.touchRapidFireKey.show) { - auto rapidFire = root->Add(new RapidFireButton("Rapid fire button", rectImage, ImageID("I_RECT"), ImageID("I_ARROW"), g_Config.touchRapidFireKey.scale, buttonLayoutParams(g_Config.touchRapidFireKey))); - rapidFire->SetAngle(90.0f, 180.0f); - } - - if (g_Config.touchAnalogRotationCWKey.show) { - auto analogRotationCC = root->Add(new AnalogRotationButton(true, "Analog clockwise rotation button", rectImage, ImageID("I_RECT"), ImageID("I_ARROW"), g_Config.touchAnalogRotationCWKey.scale, buttonLayoutParams(g_Config.touchAnalogRotationCWKey))); - analogRotationCC->SetAngle(190.0f, 180.0f); - } - - if (g_Config.touchAnalogRotationCCWKey.show) { - auto analogRotationCCW = root->Add(new AnalogRotationButton(false, "Analog counter clockwise rotation button", rectImage, ImageID("I_RECT"), ImageID("I_ARROW"), g_Config.touchAnalogRotationCCWKey.scale, buttonLayoutParams(g_Config.touchAnalogRotationCCWKey))); - analogRotationCCW->SetAngle(350.0f, 180.0f); - } - - FPSLimitButton *speed1 = addFPSLimitButton(FPSLimit::CUSTOM1, "Alternate speed 1 button", rectImage, ImageID("I_RECT"), ImageID("I_ARROW"), g_Config.touchSpeed1Key); - if (speed1) - speed1->SetAngle(170.0f, 180.0f); - FPSLimitButton *speed2 = addFPSLimitButton(FPSLimit::CUSTOM2, "Alternate speed 2 button", rectImage, ImageID("I_RECT"), ImageID("I_ARROW"), g_Config.touchSpeed2Key); - if (speed2) - speed2->SetAngle(190.0f, 180.0f); - addPSPButton(CTRL_LTRIGGER, "Left shoulder button", shoulderImage, ImageID("I_SHOULDER"), ImageID("I_L"), g_Config.touchLKey); PSPButton *rTrigger = addPSPButton(CTRL_RTRIGGER, "Right shoulder button", shoulderImage, ImageID("I_SHOULDER"), ImageID("I_R"), g_Config.touchRKey); if (rTrigger) @@ -868,11 +793,16 @@ UI::ViewGroup *CreatePadLayout(float xres, float yres, bool *pause) { root->Add(new PSPStick(stickBg, "Right analog stick", stickImage, ImageID("I_STICK"), 1, g_Config.touchRightAnalogStick.scale, buttonLayoutParams(g_Config.touchRightAnalogStick))); } - addComboKey(g_Config.iCombokey0, "Combo 1 button", g_Config.bComboToggle0, roundImage, ImageID("I_ROUND"), comboKeyImages[0], g_Config.touchCombo0); - addComboKey(g_Config.iCombokey1, "Combo 2 button", g_Config.bComboToggle1, roundImage, ImageID("I_ROUND"), comboKeyImages[1], g_Config.touchCombo1); - addComboKey(g_Config.iCombokey2, "Combo 3 button", g_Config.bComboToggle2, roundImage, ImageID("I_ROUND"), comboKeyImages[2], g_Config.touchCombo2); - addComboKey(g_Config.iCombokey3, "Combo 4 button", g_Config.bComboToggle3, roundImage, ImageID("I_ROUND"), comboKeyImages[3], g_Config.touchCombo3); - addComboKey(g_Config.iCombokey4, "Combo 5 button", g_Config.bComboToggle4, roundImage, ImageID("I_ROUND"), comboKeyImages[4], g_Config.touchCombo4); + addComboKey(g_Config.CustomKey0, "Custom 1 button", g_Config.touchCombo0); + addComboKey(g_Config.CustomKey1, "Custom 2 button", g_Config.touchCombo1); + addComboKey(g_Config.CustomKey2, "Custom 3 button", g_Config.touchCombo2); + addComboKey(g_Config.CustomKey3, "Custom 4 button", g_Config.touchCombo3); + addComboKey(g_Config.CustomKey4, "Custom 5 button", g_Config.touchCombo4); + addComboKey(g_Config.CustomKey5, "Custom 6 button", g_Config.touchCombo5); + addComboKey(g_Config.CustomKey6, "Custom 7 button", g_Config.touchCombo6); + addComboKey(g_Config.CustomKey7, "Custom 8 button", g_Config.touchCombo7); + addComboKey(g_Config.CustomKey8, "Custom 9 button", g_Config.touchCombo8); + addComboKey(g_Config.CustomKey9, "Custom 10 button", g_Config.touchCombo9); return root; } diff --git a/UI/GamepadEmu.h b/UI/GamepadEmu.h index d6fc612a8e..20c1a72a2d 100644 --- a/UI/GamepadEmu.h +++ b/UI/GamepadEmu.h @@ -23,6 +23,7 @@ #include "Common/UI/View.h" #include "Common/UI/ViewGroup.h" #include "Core/CoreParameter.h" +#include "UI/EmuScreen.h" class GamepadView : public UI::View { public: @@ -86,41 +87,6 @@ private: bool *value_; }; -class FPSLimitButton : public MultiTouchButton { -public: - FPSLimitButton(FPSLimit limit, const char *key, ImageID bgImg, ImageID bgDownImg, ImageID img, float scale, UI::LayoutParams *layoutParams) - : MultiTouchButton(key, bgImg, bgDownImg, img, scale, layoutParams), limit_(limit) { - - } - void Touch(const TouchInput &input) override; - bool IsDown() override; - -private: - FPSLimit limit_; -}; - -class RapidFireButton : public MultiTouchButton { -public: - RapidFireButton(const char *key, ImageID bgImg, ImageID bgDownImg, ImageID img, float scale, UI::LayoutParams *layoutParams) - : MultiTouchButton(key, bgImg, bgDownImg, img, scale, layoutParams) { - } - void Touch(const TouchInput &input) override; - bool IsDown() override; -}; - -class AnalogRotationButton : public MultiTouchButton { -public: - AnalogRotationButton(bool clockWise, const char *key, ImageID bgImg, ImageID bgDownImg, ImageID img, float scale, UI::LayoutParams *layoutParams) - : MultiTouchButton(key, bgImg, bgDownImg, img, scale, layoutParams), clockWise_(clockWise) { - } - void Touch(const TouchInput &input) override; - void Update() override; - -private: - bool autoRotating_ = false; - bool clockWise_; -}; - class PSPButton : public MultiTouchButton { public: PSPButton(int pspButtonBit, const char *key, ImageID bgImg, ImageID bgDownImg, ImageID img, float scale, UI::LayoutParams *layoutParams) @@ -196,18 +162,125 @@ private: //initializes the layout from Config. if a default layout does not exist, //it sets up default values void InitPadLayout(float xres, float yres, float globalScale = 1.15f); -UI::ViewGroup *CreatePadLayout(float xres, float yres, bool *pause); +UI::ViewGroup *CreatePadLayout(float xres, float yres, bool *pause, ControlMapper* controllMapper); const int D_pad_Radius = 50; const int baseActionButtonSpacing = 60; class ComboKey : public MultiTouchButton { public: - ComboKey(int pspButtonBit, const char *key, bool toggle, ImageID bgImg, ImageID bgDownImg, ImageID img, float scale, UI::LayoutParams *layoutParams) - : MultiTouchButton(key, bgImg, bgDownImg, img, scale, layoutParams), pspButtonBit_(pspButtonBit), toggle_(toggle) { + ComboKey(uint64_t pspButtonBit, const char *key, bool toggle, ControlMapper* controllMapper, ImageID bgImg, ImageID bgDownImg, ImageID img, float scale, UI::LayoutParams *layoutParams) + : MultiTouchButton(key, bgImg, bgDownImg, img, scale, layoutParams), pspButtonBit_(pspButtonBit), toggle_(toggle), controllMapper_(controllMapper), on_(false) { } void Touch(const TouchInput &input) override; + bool IsDown() override; private: - int pspButtonBit_; + uint64_t pspButtonBit_; bool toggle_; + ControlMapper* controllMapper_; + bool on_; +}; + +// Just edit this to add new image, shape or button function +namespace CustomKey { + // Image list + struct keyImage { + const char* n; // UI name + ImageID i; // ImageID + float r; // Rotation angle in degree + }; + static const keyImage comboKeyImages[] = { + { "1", ImageID("I_1"), 0.0f }, + { "2", ImageID("I_2"), 0.0f }, + { "3", ImageID("I_3"), 0.0f }, + { "4", ImageID("I_4"), 0.0f }, + { "5", ImageID("I_5"), 0.0f }, + { "6", ImageID("I_6"), 0.0f }, + { "A", ImageID("I_A"), 0.0f }, + { "B", ImageID("I_B"), 0.0f }, + { "C", ImageID("I_C"), 0.0f }, + { "D", ImageID("I_D"), 0.0f }, + { "E", ImageID("I_E"), 0.0f }, + { "F", ImageID("I_F"), 0.0f }, + { "Circle", ImageID("I_CIRCLE"), 0.0f }, + { "Cross", ImageID("I_CROSS"), 0.0f }, + { "Square", ImageID("I_SQUARE"), 0.0f }, + { "Triangle", ImageID("I_TRIANGLE"), 0.0f }, + { "L", ImageID("I_L"), 0.0f }, + { "R", ImageID("I_R"), 0.0f }, + { "Start", ImageID("I_START"), 0.0f }, + { "Select", ImageID("I_SELECT"), 0.0f }, + { "Plus", ImageID("I_CROSS"), 45.0f }, + { "Rhombus", ImageID("I_SQUARE"), 45.0f }, + { "Down Triangle", ImageID("I_TRIANGLE"), 180.0f }, + { "Arrow up", ImageID("I_ARROW"), 90.0f}, + { "Arrow down", ImageID("I_ARROW"), 270.0f}, + { "Arrow left", ImageID("I_ARROW"), 0.0f}, + { "Arrow right", ImageID("I_ARROW"), 180.0f}, + { "Gear", ImageID("I_GEAR"), 0.0f}, + }; + + // Shape list + struct keyShape { + const char* n; // UI name + ImageID i; // ImageID + ImageID l; // ImageID line version + float r; // Rotation angle in dregree + bool f; // Flip Horizontally + }; + static const keyShape comboKeyShapes[] = { + { "Circle", ImageID("I_ROUND"), ImageID("I_ROUND_LINE"), 0.0f, false }, + { "Rectangle", ImageID("I_RECT"), ImageID("I_RECT_LINE"), 0.0f, false }, + { "Vertical Rectangle", ImageID("I_RECT"), ImageID("I_RECT_LINE"), 90.0f, false }, + { "L button", ImageID("I_SHOULDER"), ImageID("I_SHOULDER_LINE"), 0.0f, false }, + { "R button", ImageID("I_SHOULDER"), ImageID("I_SHOULDER_LINE"), 0.0f, true }, + { "Arrow up", ImageID("I_DIR"), ImageID("I_DIR_LINE"), 270.0f, false }, + { "Arrow down", ImageID("I_DIR"), ImageID("I_DIR_LINE"), 90.0f, false }, + { "Arrow left", ImageID("I_DIR"), ImageID("I_DIR_LINE"), 180.0f, false }, + { "Arrow right", ImageID("I_DIR"), ImageID("I_DIR_LINE"), 0.0f, false }, + }; + + // Button list + struct keyList { + const char* n; // UI name + ImageID i; // UI ImageID + uint32_t c; // Key code + }; + static const keyList comboKeyList[] = { + { "Square", ImageID("I_SQUARE"), CTRL_SQUARE }, + { "Triangle", ImageID("I_TRIANGLE"), CTRL_TRIANGLE }, + { "Circle", ImageID("I_CIRCLE"), CTRL_CIRCLE }, + { "Cross", ImageID("I_CROSS"), CTRL_CROSS }, + { "Up", ImageID::invalid(), CTRL_UP }, + { "Down", ImageID::invalid(), CTRL_DOWN }, + { "Left", ImageID::invalid(), CTRL_LEFT }, + { "Right", ImageID::invalid(), CTRL_RIGHT }, + { "Start", ImageID("I_START"), CTRL_START }, + { "Select", ImageID("I_SELECT"), CTRL_SELECT }, + { "L", ImageID("I_L"), CTRL_LTRIGGER }, + { "R", ImageID("I_R"), CTRL_RTRIGGER }, + { "RapidFire", ImageID::invalid(), VIRTKEY_RAPID_FIRE }, + { "Unthrottle", ImageID::invalid(), VIRTKEY_UNTHROTTLE }, + { "SpeedToggle", ImageID::invalid(), VIRTKEY_SPEED_TOGGLE }, + { "Rewind", ImageID::invalid(), VIRTKEY_REWIND }, + { "Save State", ImageID::invalid(), VIRTKEY_SAVE_STATE }, + { "Load State", ImageID::invalid(), VIRTKEY_LOAD_STATE }, + { "Next Slot", ImageID::invalid(), VIRTKEY_NEXT_SLOT }, + { "Toggle Fullscreen", ImageID::invalid(), VIRTKEY_TOGGLE_FULLSCREEN }, + { "Alt speed 1", ImageID::invalid(), VIRTKEY_SPEED_CUSTOM1 }, + { "Alt speed 2", ImageID::invalid(), VIRTKEY_SPEED_CUSTOM2 }, + { "Texture Dumping", ImageID::invalid(), VIRTKEY_TEXTURE_DUMP }, + { "Texture Replacement", ImageID::invalid(), VIRTKEY_TEXTURE_REPLACE }, + { "Screenshot", ImageID::invalid(), VIRTKEY_SCREENSHOT }, + { "Mute toggle", ImageID::invalid(), VIRTKEY_MUTE_TOGGLE }, + { "OpenChat", ImageID::invalid(), VIRTKEY_OPENCHAT }, + { "Auto Analog Rotation (CW)", ImageID::invalid(), VIRTKEY_ANALOG_ROTATE_CW }, + { "Auto Analog Rotation (CCW)", ImageID::invalid(), VIRTKEY_ANALOG_ROTATE_CCW }, + { "Pause", ImageID::invalid(), VIRTKEY_PAUSE }, + { "DevMenu", ImageID::invalid(), VIRTKEY_DEVMENU }, +#ifndef MOBILE_DEVICE + { "Record", ImageID::invalid(), VIRTKEY_RECORD }, +#endif + }; + static_assert(ARRAY_SIZE(comboKeyList) <= 64, "Too many key for a uint64_t bit mask"); }; diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index f31f20e5dd..c357900798 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -669,6 +669,11 @@ void GameBrowser::Refresh() { } else { topBar->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 64.0f, 1.0f))); } + + if (browseFlags_ & BrowseFlags::HOMEBREW_STORE) { + topBar->Add(new Choice(mm->T("PPSSPP Homebrew Store"), new UI::LinearLayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::OnHomebrewStore); + } + ChoiceStrip *layoutChoice = topBar->Add(new ChoiceStrip(ORIENT_HORIZONTAL)); layoutChoice->AddChoice(ImageID("I_GRID")); layoutChoice->AddChoice(ImageID("I_LINES")); @@ -806,11 +811,6 @@ void GameBrowser::Refresh() { OnClick.Handle(this, &GameBrowser::PinToggleClick); } - if (browseFlags_ & BrowseFlags::HOMEBREW_STORE) { - Add(new Spacer()); - Add(new Choice(mm->T("DownloadFromStore", "Download from the PPSSPP Homebrew Store"), new UI::LinearLayoutParams(UI::WRAP_CONTENT, UI::WRAP_CONTENT)))->OnClick.Handle(this, &GameBrowser::OnHomebrewStore); - } - if (!lastText_.empty() && gameButtons.empty()) { Add(new Spacer()); Add(new Choice(lastText_, new UI::LinearLayoutParams(UI::WRAP_CONTENT, UI::WRAP_CONTENT)))->OnClick.Handle(this, &GameBrowser::LastClick); @@ -1203,34 +1203,7 @@ void MainScreen::sendMessage(const char *message, const char *value) { LaunchFile(screenManager(), Path(std::string(value))); } if (!strcmp(message, "browse_folderSelect")) { - std::string filename; -#if PPSSPP_PLATFORM(ANDROID) - // Hacky way to get a normal path from a Android Storage Framework path. - // Is not gonna work forever, but ship-hack for 1.11. - std::string url = value; - const char *prefix = "content://com.android.externalstorage.documents/tree/"; - const char *primaryPrefix = "/storage/primary/"; - if (startsWith(url, prefix)) { - url = UriDecode(url.substr(strlen(prefix))); - size_t colonPos = url.find(":"); - if (colonPos != std::string::npos) { - url[colonPos] = '/'; - } - url = "/storage/" + url; - if (startsWith(url, primaryPrefix)) { - url = g_Config.memStickDirectory.ToString() + url.substr(strlen(primaryPrefix)); - } - INFO_LOG(SYSTEM, "Translated '%s' into '%s'", value, url.c_str()); - } else { - // It's not gonna work. - // TODO: Show an error message? - INFO_LOG(SYSTEM, "Failed to parse content string: '%s'", value); - return; - } - filename = url; -#else - filename = value; -#endif + std::string filename = value; INFO_LOG(SYSTEM, "Got folder: '%s'", filename.c_str()); int tab = tabHolder_->GetCurrentTab(); // Don't allow browsing in the other tabs (I don't think it's possible to reach the option though) diff --git a/UI/MemStickScreen.cpp b/UI/MemStickScreen.cpp new file mode 100644 index 0000000000..6759457f39 --- /dev/null +++ b/UI/MemStickScreen.cpp @@ -0,0 +1,155 @@ +// Copyright (c) 2013- 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 "ppsspp_config.h" + +#include "android/jni/app-android.h" + +#include "Common/Log.h" +#include "Common/UI/UI.h" +#include "Common/UI/View.h" +#include "Common/UI/ViewGroup.h" + +#include "Common/StringUtils.h" +#include "Common/File/FileUtil.h" +#include "Common/File/Path.h" +#include "Common/System/System.h" +#include "Common/System/NativeApp.h" +#include "Common/Data/Text/I18n.h" +#include "Common/System/Display.h" + +#include "Core/Util/GameManager.h" +#include "Core/System.h" + +#include "UI/MemStickScreen.h" +#include "UI/MainScreen.h" +#include "UI/MiscScreens.h" + +#include "Core/Config.h" + +MemStickScreen::~MemStickScreen() { } + +void MemStickScreen::CreateViews() { + using namespace UI; + + auto di = GetI18NCategory("Dialog"); + auto iz = GetI18NCategory("MemStick"); + + Margins actionMenuMargins(15, 15, 15, 0); + + root_ = new AnchorLayout(); + + ViewGroup *columns = new LinearLayout(ORIENT_HORIZONTAL, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT)); + root_->Add(columns); + + ViewGroup *leftColumn = new AnchorLayout(new LinearLayoutParams(1.0)); + ViewGroup *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(300, FILL_PARENT, actionMenuMargins)); + columns->Add(leftColumn); + columns->Add(rightColumnItems); + + Path path = g_Config.memStickDirectory; + int64_t freeSpaceAtMemStick = -1; +#if PPSSPP_PLATFORM(ANDROID) + if (Android_IsContentUri(path.ToString())) { + freeSpaceAtMemStick = Android_GetFreeSpaceByContentUri(path.ToString()); + } else { + freeSpaceAtMemStick = Android_GetFreeSpaceByFilePath(path.ToString()); + } +#endif + + int leftSide = 100; + settingInfo_ = new SettingInfoMessage(ALIGN_CENTER | FLAG_WRAP_TEXT, new AnchorLayoutParams(dp_xres - leftSide - 40.0f, WRAP_CONTENT, leftSide, dp_yres - 80.0f - 40.0f, NONE, NONE)); + settingInfo_->SetBottomCutoff(dp_yres - 200.0f); + + root_->Add(settingInfo_); + + leftColumn->Add(new TextView(iz->T("Memory Stick Storage"), ALIGN_LEFT, false, new AnchorLayoutParams(10, 10, NONE, NONE))); + leftColumn->Add(new TextView(iz->T("MemoryStickDescription", "Choose where your PSP memory stick data (savegames, etc) is stored"), ALIGN_LEFT, false, new AnchorLayoutParams(10, 50, NONE, NONE))); + leftColumn->Add(new TextView(g_Config.memStickDirectory.ToVisualString(), ALIGN_LEFT, false, new AnchorLayoutParams(10, 140, NONE, NONE))); + + std::string freeSpaceText = "Free space: N/A"; + if (freeSpaceAtMemStick >= 0) { + freeSpaceText = StringFromFormat("free space: %lld MB", freeSpaceAtMemStick / (1024 * 1024)); + } + + leftColumn->Add(new TextView(freeSpaceText, ALIGN_LEFT, false, new AnchorLayoutParams(10, 240, NONE, NONE))); + + rightColumnItems->Add(new Choice(iz->T("Browse")))->OnClick.Handle(this, &MemStickScreen::OnBrowse); + rightColumnItems->Add(new Choice(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnOK); +} + +UI::EventReturn MemStickScreen::OnBrowse(UI::EventParams ¶ms) { + auto sy = GetI18NCategory("System"); + System_SendMessage("browse_folder", ""); + return UI::EVENT_DONE; +} + +void MemStickScreen::sendMessage(const char *message, const char *value) { + // Always call the base class method first to handle the most common messages. + UIDialogScreenWithBackground::sendMessage(message, value); + + if (screenManager()->topScreen() == this) { + if (!strcmp(message, "browse_folderSelect")) { + std::string filename; + filename = value; + INFO_LOG(SYSTEM, "Got folder: '%s'", filename.c_str()); + pendingMemStickFolder_ = Path(filename); + CallbackMemStickFolder(true); + } + } +} + +void MemStickScreen::CallbackMemStickFolder(bool yes) { + auto sy = GetI18NCategory("System"); + + if (yes) { + Path memStickDirFile = g_Config.internalDataDirectory / "memstick_dir.txt"; + Path testWriteFile = pendingMemStickFolder_ / ".write_verify_file"; + + // Doesn't already exist, create. + // Should this ever happen? + if (pendingMemStickFolder_.Type() == PathType::NATIVE) { + if (!File::Exists(pendingMemStickFolder_)) { + File::CreateFullPath(pendingMemStickFolder_); + } + if (!File::WriteDataToFile(true, "1", 1, testWriteFile)) { + settingInfo_->Show(sy->T("ChangingMemstickPathInvalid", "That path couldn't be used to save Memory Stick files."), nullptr); + return; + } + File::Delete(testWriteFile); + } else { + // TODO: Do the same but with scoped storage? Not really necessary, right? If it came from a browse + // for folder, we can assume it exists, barring wacky race conditions like the user being connected + // by USB and deleting it. + } + + // This doesn't need the storage API - this path is accessible the normal way. + std::string str = pendingMemStickFolder_.ToString(); + if (!File::WriteDataToFile(true, str.c_str(), (unsigned int)str.size(), memStickDirFile)) { + ERROR_LOG(SYSTEM, "Failed to write memstick path '%s' to '%s'", pendingMemStickFolder_.c_str(), memStickDirFile.c_str()); + // Not sure what to do if this file. + } + + // Save so the settings, at least, are transferred. + g_Config.memStickDirectory = pendingMemStickFolder_; + g_Config.SetSearchPath(GetSysDirectory(DIRECTORY_SYSTEM)); + g_Config.UpdateIniLocation(); + + g_Config.Save("MemstickPathChanged"); + screenManager()->RecreateAllViews(); + } +} diff --git a/UI/MemStickScreen.h b/UI/MemStickScreen.h new file mode 100644 index 0000000000..b0c4f55dee --- /dev/null +++ b/UI/MemStickScreen.h @@ -0,0 +1,48 @@ +// Copyright (c) 2013- 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 +#include + +#include "ppsspp_config.h" + +#include "Common/File/Path.h" +#include "UI/MiscScreens.h" +#include "Common/UI/UIScreen.h" + +// MemStickScreen - let's you configure your memory stick directory. +// Currently only useful for Android. +class MemStickScreen : public UIDialogScreenWithBackground { +public: + MemStickScreen() {} + virtual ~MemStickScreen(); + + std::string tag() const override { return "game"; } + +protected: + void CreateViews() override; + void CallbackMemStickFolder(bool yes); + void sendMessage(const char *message, const char *value) override; + +private: + // Event handlers + UI::EventReturn OnBrowse(UI::EventParams &e); + Path pendingMemStickFolder_; + SettingInfoMessage *settingInfo_; +}; diff --git a/UI/MiscScreens.cpp b/UI/MiscScreens.cpp index e67bbe64f0..d32c74402e 100644 --- a/UI/MiscScreens.cpp +++ b/UI/MiscScreens.cpp @@ -928,3 +928,52 @@ void CreditsScreen::render() { dc.Flush(); } + +SettingInfoMessage::SettingInfoMessage(int align, UI::AnchorLayoutParams *lp) + : UI::LinearLayout(UI::ORIENT_HORIZONTAL, lp) { + using namespace UI; + SetSpacing(0.0f); + Add(new UI::Spacer(10.0f)); + text_ = Add(new UI::TextView("", align, false, new LinearLayoutParams(1.0, Margins(0, 10)))); + Add(new UI::Spacer(10.0f)); +} + +void SettingInfoMessage::Show(const std::string &text, UI::View *refView) { + if (refView) { + Bounds b = refView->GetBounds(); + const UI::AnchorLayoutParams *lp = GetLayoutParams()->As(); + if (b.y >= cutOffY_) { + ReplaceLayoutParams(new UI::AnchorLayoutParams(lp->width, lp->height, lp->left, 80.0f, lp->right, lp->bottom, lp->center)); + } else { + ReplaceLayoutParams(new UI::AnchorLayoutParams(lp->width, lp->height, lp->left, dp_yres - 80.0f - 40.0f, lp->right, lp->bottom, lp->center)); + } + } + text_->SetText(text); + timeShown_ = time_now_d(); +} + +void SettingInfoMessage::Draw(UIContext &dc) { + static const double FADE_TIME = 1.0; + static const float MAX_ALPHA = 0.9f; + + // Let's show longer messages for more time (guesstimate at reading speed.) + // Note: this will give multibyte characters more time, but they often have shorter words anyway. + double timeToShow = std::max(1.5, text_->GetText().size() * 0.05); + + double sinceShow = time_now_d() - timeShown_; + float alpha = MAX_ALPHA; + if (timeShown_ == 0.0 || sinceShow > timeToShow + FADE_TIME) { + alpha = 0.0f; + } else if (sinceShow > timeToShow) { + alpha = MAX_ALPHA - MAX_ALPHA * (float)((sinceShow - timeToShow) / FADE_TIME); + } + + if (alpha >= 0.1f) { + UI::Style style = dc.theme->popupTitle; + style.background.color = colorAlpha(style.background.color, alpha - 0.1f); + dc.FillRect(style.background, bounds_); + } + + text_->SetTextColor(whiteAlpha(alpha)); + ViewGroup::Draw(dc); +} diff --git a/UI/MiscScreens.h b/UI/MiscScreens.h index 08a142e1fc..5539d40fdc 100644 --- a/UI/MiscScreens.h +++ b/UI/MiscScreens.h @@ -163,3 +163,20 @@ private: double startTime_ = 0.0; }; + +class SettingInfoMessage : public UI::LinearLayout { +public: + SettingInfoMessage(int align, UI::AnchorLayoutParams *lp); + + void SetBottomCutoff(float y) { + cutOffY_ = y; + } + void Show(const std::string &text, UI::View *refView = nullptr); + + void Draw(UIContext &dc); + +private: + UI::TextView *text_ = nullptr; + double timeShown_ = 0.0; + float cutOffY_; +}; diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 175e159c90..afa1205f0a 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -78,6 +78,7 @@ #include "Common/OSVersion.h" #include "Common/GPU/ShaderTranslation.h" +#include "Core/ControlMapper.h" #include "Core/Config.h" #include "Core/ConfigValues.h" #include "Core/Core.h" @@ -96,6 +97,7 @@ #include "Core/Util/GameManager.h" #include "Core/Util/AudioFormat.h" #include "Core/WebServer.h" +#include "Core/ThreadPools.h" #include "GPU/GPUInterface.h" #include "UI/BackgroundAudio.h" @@ -120,6 +122,9 @@ #if PPSSPP_PLATFORM(UWP) #include #endif +#if PPSSPP_PLATFORM(ANDROID) +#include "android/jni/app-android.h" +#endif // The new UI framework, for initialization @@ -200,7 +205,7 @@ public: }; #ifdef _WIN32 -int Win32Mix(short *buffer, int numSamples, int bits, int rate, int channels) { +int Win32Mix(short *buffer, int numSamples, int bits, int rate) { return NativeMix(buffer, numSamples); } #endif @@ -333,13 +338,26 @@ static void PostLoadConfig() { i18nrepo.LoadIni(g_Config.sLanguageIni); else i18nrepo.LoadIni(g_Config.sLanguageIni, langOverridePath); + + g_threadManager.Init(cpu_info.num_cores, cpu_info.logical_cpu_count); } -void CreateDirectoriesAndroid() { +static bool CreateDirectoriesAndroid() { // On Android, create a PSP directory tree in the external_dir, // to hopefully reduce confusion a bit. - INFO_LOG(IO, "Creating %s", (g_Config.memStickDirectory / "PSP").c_str()); - File::CreateFullPath(g_Config.memStickDirectory / "PSP"); + + Path pspDir = g_Config.memStickDirectory; + if (pspDir.GetFilename() != "PSP") { + pspDir /= "PSP"; + } + + INFO_LOG(IO, "Creating '%s' and subdirs:", pspDir.c_str()); + File::CreateFullPath(pspDir); + if (!File::Exists(pspDir)) { + INFO_LOG(IO, "Not a workable memstick directory. Giving up"); + return false; + } + File::CreateFullPath(GetSysDirectory(DIRECTORY_SAVEDATA)); File::CreateFullPath(GetSysDirectory(DIRECTORY_SAVESTATE)); File::CreateFullPath(GetSysDirectory(DIRECTORY_GAME)); @@ -354,6 +372,7 @@ void CreateDirectoriesAndroid() { File::CreateEmptyFile(GetSysDirectory(DIRECTORY_SYSTEM) / ".nomedia"); File::CreateEmptyFile(GetSysDirectory(DIRECTORY_TEXTURES) / ".nomedia"); File::CreateEmptyFile(GetSysDirectory(DIRECTORY_PLUGINS) / ".nomedia"); + return true; } static void CheckFailedGPUBackends() { @@ -442,9 +461,6 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch std::string user_data_path = savegame_dir; pendingMessages.clear(); pendingInputBoxes.clear(); -#if PPSSPP_PLATFORM(IOS) - user_data_path += "/"; -#endif // external_dir has all kinds of meanings depending on platform. // on iOS it's even the path to bundled app assets. It's a mess. @@ -465,6 +481,7 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch VFSRegister("", new DirectoryAssetReader(Path("/usr/share/ppsspp/assets"))); VFSRegister("", new DirectoryAssetReader(Path("/usr/share/games/ppsspp/assets"))); #endif + #if PPSSPP_PLATFORM(SWITCH) Path assetPath = Path(user_data_path) / "assets"; VFSRegister("", new DirectoryAssetReader(assetPath)); @@ -482,27 +499,47 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch g_Config.defaultCurrentDirectory = Path("/"); g_Config.internalDataDirectory = Path(savegame_dir); -#if defined(__ANDROID__) - // TODO: This needs to change in Android 12. - // - // Maybe there should be an option to use internal memory instead, but I think - // that for most people, using external memory (SDCard/USB Storage) makes the - // most sense. - g_Config.defaultCurrentDirectory = Path(external_dir); +#if PPSSPP_PLATFORM(ANDROID) + // In Android 12 with scoped storage, due to the above, the external directory + // is no longer the plain root of external storage, but it's an app specific directory + // on external storage (g_extFilesDir). + if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) { + g_Config.defaultCurrentDirectory = Path(g_extFilesDir); + } else { + // Maybe there should be an option to use internal memory instead, but I think + // that for most people, using external memory (SDCard/USB Storage) makes the + // most sense. + g_Config.defaultCurrentDirectory = Path(external_dir); + } + + // Might also add an option to move it to internal / non-visible storage, but there's + // little point, really. + g_Config.memStickDirectory = Path(external_dir); g_Config.flash0Directory = Path(external_dir) / "flash0"; Path memstickDirFile = g_Config.internalDataDirectory / "memstick_dir.txt"; if (File::Exists(memstickDirFile)) { std::string memstickDir; - File::ReadFileToString(true, memstickDirFile, memstickDir); - Path memstickPath(memstickDir); - if (!memstickPath.empty() && File::Exists(memstickPath)) { - g_Config.memStickDirectory = memstickPath; - } else { - ERROR_LOG(SYSTEM, "Couldn't read directory '%s' specified by memstick_dir.txt.", memstickDir.c_str()); + if (File::ReadFileToString(true, memstickDirFile, memstickDir)) { + Path memstickPath(memstickDir); + if (!memstickPath.empty() && File::Exists(memstickPath)) { + g_Config.memStickDirectory = memstickPath; + INFO_LOG(SYSTEM, "Memstick Directory from memstick_dir.txt: '%s'", g_Config.memStickDirectory.c_str()); + } else { + ERROR_LOG(SYSTEM, "Couldn't read directory '%s' specified by memstick_dir.txt.", memstickDir.c_str()); + if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) { + // TODO: Gotta resolve this somehow... + // I think we wanna pop up the memstick dir chooser before any other screen in this case. + // For now we just choose a default. + g_Config.memStickDirectory = g_Config.defaultCurrentDirectory; + } + } } + } else { + INFO_LOG(SYSTEM, "No memstick directory file found. Using '%s'", memstickDirFile.c_str()); } + #elif PPSSPP_PLATFORM(IOS) g_Config.defaultCurrentDirectory = g_Config.internalDataDirectory; g_Config.memStickDirectory = Path(user_data_path); @@ -1291,6 +1328,11 @@ bool NativeKey(const KeyInput &key) { } bool NativeAxis(const AxisInput &axis) { + if (!screenManager) { + // Too early. + return false; + } + using namespace TiltEventProcessor; // only handle tilt events if tilt is enabled. @@ -1304,8 +1346,7 @@ bool NativeAxis(const AxisInput &axis) { } // create the base coordinate tilt system from the calibration data. - // This is static for no particular reason, can be un-static'ed - static Tilt baseTilt; + Tilt baseTilt; baseTilt.x_ = g_Config.fTiltBaseX; baseTilt.y_ = g_Config.fTiltBaseY; diff --git a/UI/Store.cpp b/UI/Store.cpp index 3461db5a1f..089709f1e2 100644 --- a/UI/Store.cpp +++ b/UI/Store.cpp @@ -304,11 +304,8 @@ void ProductView::Update() { std::string ProductView::DownloadURL() { if (entry_.downloadURL.empty()) { - // Construct the URL, easy to predict from our server - std::string shortName = entry_.file; - if (shortName.find('.') == std::string::npos) - shortName += ".zip"; - return storeBaseUrl + "files/" + shortName; + // Construct the URL. + return storeBaseUrl + "files/" + entry_.file + ".zip"; } else { // Use the provided URL, for external hosting. return entry_.downloadURL; @@ -559,14 +556,6 @@ UI::EventReturn StoreScreen::OnRetry(UI::EventParams &e) { return UI::EVENT_DONE; } -std::string StoreScreen::GetStoreJsonURL(std::string storePath) const { - std::string path = storeBaseUrl + storePath; - if (*path.rbegin() != '/') - path += '/'; - path += "index.json"; - return path; -} - std::string StoreScreen::GetTranslatedString(const json::JsonGet json, std::string key, const char *fallback) const { json::JsonGet dict = json.getDict("en_US"); if (dict && json.hasChild(lang_.c_str(), JSON_OBJECT)) { diff --git a/UI/Store.h b/UI/Store.h index b8cc7aefd0..d2d15a809f 100644 --- a/UI/Store.h +++ b/UI/Store.h @@ -81,7 +81,6 @@ private: ProductItemView *GetSelectedItem(); std::vector FilterEntries(); - std::string GetStoreJsonURL(std::string storePath) const; std::string GetTranslatedString(const json::JsonGet json, std::string key, const char *fallback = nullptr) const; std::shared_ptr listing_; diff --git a/UI/TiltEventProcessor.cpp b/UI/TiltEventProcessor.cpp index 2f66906239..8beb768508 100644 --- a/UI/TiltEventProcessor.cpp +++ b/UI/TiltEventProcessor.cpp @@ -96,8 +96,7 @@ void TiltEventProcessor::TranslateTiltToInput(const Tilt &tilt) { } void TiltEventProcessor::GenerateAnalogStickEvent(const Tilt &tilt) { - __CtrlSetAnalogX(clamp(tilt.x_), CTRL_STICK_LEFT); - __CtrlSetAnalogY(clamp(tilt.y_), CTRL_STICK_LEFT); + __CtrlSetAnalogXY(CTRL_STICK_LEFT, clamp(tilt.x_), clamp(tilt.y_)); tiltAnalogSet = true; } @@ -182,8 +181,7 @@ void TiltEventProcessor::ResetTiltEvents() { tiltButtonsDown = 0; if (tiltAnalogSet) { - __CtrlSetAnalogX(0.0f, CTRL_STICK_LEFT); - __CtrlSetAnalogY(0.0f, CTRL_STICK_LEFT); + __CtrlSetAnalogXY(CTRL_STICK_LEFT, 0.0f, 0.0f); tiltAnalogSet = false; } } diff --git a/UI/TouchControlLayoutScreen.cpp b/UI/TouchControlLayoutScreen.cpp index 25a5a07df0..3f040f5ae3 100644 --- a/UI/TouchControlLayoutScreen.cpp +++ b/UI/TouchControlLayoutScreen.cpp @@ -333,6 +333,7 @@ void ControlLayoutView::Touch(const TouchInput &touch) { } void ControlLayoutView::CreateViews() { + using namespace CustomKey; const Bounds &bounds = GetBounds(); if (bounds.w == 0.0f || bounds.h == 0.0f) { // Layout hasn't happened yet, return. @@ -356,8 +357,6 @@ void ControlLayoutView::CreateViews() { ImageID stickBg = g_Config.iTouchButtonStyle ? ImageID("I_STICK_BG_LINE") : ImageID("I_STICK_BG"); ImageID roundImage = g_Config.iTouchButtonStyle ? ImageID("I_ROUND_LINE") : ImageID("I_ROUND"); - const ImageID comboKeyImages[5] = { ImageID("I_1"), ImageID("I_2"), ImageID("I_3"), ImageID("I_4"), ImageID("I_5") }; - auto addDragDropButton = [&](ConfigTouchPos &pos, const char *key, ImageID bgImg, ImageID img) { DragDropButton *b = nullptr; if (pos.show) { @@ -377,22 +376,6 @@ void ControlLayoutView::CreateViews() { if (auto *unthrottle = addDragDropButton(g_Config.touchUnthrottleKey, "Unthrottle button", rectImage, ImageID("I_ARROW"))) { unthrottle->SetAngle(180.0f); } - if (auto *speed1 = addDragDropButton(g_Config.touchSpeed1Key, "Alternate speed 1 button", rectImage, ImageID("I_ARROW"))) { - speed1->SetAngle(170.0f, 180.0f); - } - if (auto *speed2 = addDragDropButton(g_Config.touchSpeed2Key, "Alternate speed 2 button", rectImage, ImageID("I_ARROW"))) { - speed2->SetAngle(190.0f, 180.0f); - } - if (auto *rapidFire = addDragDropButton(g_Config.touchRapidFireKey, "Rapid fire button", rectImage, ImageID("I_ARROW"))) { - rapidFire->SetAngle(90.0f, 180.0f); - } - if (auto *analogRotationCW = addDragDropButton(g_Config.touchAnalogRotationCWKey, "Analog clockwise rotation button", rectImage, ImageID("I_ARROW"))) { - analogRotationCW->SetAngle(190.0f, 180.0f); - } - if (auto *analogRotationCCW = addDragDropButton(g_Config.touchAnalogRotationCCWKey, "Analog counter clockwise rotation button", rectImage, ImageID("I_ARROW"))) { - analogRotationCCW->SetAngle(350.0f, 180.0f); - } - addDragDropButton(g_Config.touchLKey, "Left shoulder button", shoulderImage, ImageID("I_L")); if (auto *rbutton = addDragDropButton(g_Config.touchRKey, "Right shoulder button", shoulderImage, ImageID("I_R"))) { rbutton->FlipImageH(true); @@ -400,11 +383,27 @@ void ControlLayoutView::CreateViews() { addDragDropButton(g_Config.touchAnalogStick, "Left analog stick", stickBg, stickImage); addDragDropButton(g_Config.touchRightAnalogStick, "Right analog stick", stickBg, stickImage); - addDragDropButton(g_Config.touchCombo0, "Combo 1 button", roundImage, comboKeyImages[0]); - addDragDropButton(g_Config.touchCombo1, "Combo 2 button", roundImage, comboKeyImages[1]); - addDragDropButton(g_Config.touchCombo2, "Combo 3 button", roundImage, comboKeyImages[2]); - addDragDropButton(g_Config.touchCombo3, "Combo 4 button", roundImage, comboKeyImages[3]); - addDragDropButton(g_Config.touchCombo4, "Combo 5 button", roundImage, comboKeyImages[4]); + + auto addDragComboKey = [&](ConfigTouchPos &pos, const char *key, const ConfigCustomButton& cfg) { + DragDropButton *b = nullptr; + if (pos.show) { + b = new DragDropButton(pos, key, g_Config.iTouchButtonStyle == 0 ? comboKeyShapes[cfg.shape].i : comboKeyShapes[cfg.shape].l, comboKeyImages[cfg.image].i, bounds); + b->FlipImageH(comboKeyShapes[cfg.shape].f); + b->SetAngle(comboKeyImages[cfg.image].r, comboKeyShapes[cfg.shape].r); + controls_.push_back(b); + } + return b; + }; + addDragComboKey(g_Config.touchCombo0, "Custom 1 button", g_Config.CustomKey0); + addDragComboKey(g_Config.touchCombo1, "Custom 2 button", g_Config.CustomKey1); + addDragComboKey(g_Config.touchCombo2, "Custom 3 button", g_Config.CustomKey2); + addDragComboKey(g_Config.touchCombo3, "Custom 4 button", g_Config.CustomKey3); + addDragComboKey(g_Config.touchCombo4, "Custom 5 button", g_Config.CustomKey4); + addDragComboKey(g_Config.touchCombo5, "Custom 6 button", g_Config.CustomKey5); + addDragComboKey(g_Config.touchCombo6, "Custom 7 button", g_Config.CustomKey6); + addDragComboKey(g_Config.touchCombo7, "Custom 8 button", g_Config.CustomKey7); + addDragComboKey(g_Config.touchCombo8, "Custom 9 button", g_Config.CustomKey8); + addDragComboKey(g_Config.touchCombo9, "Custom 10 button", g_Config.CustomKey9); for (size_t i = 0; i < controls_.size(); i++) { Add(controls_[i]); @@ -500,7 +499,7 @@ void TouchControlLayoutScreen::CreateViews() { Choice *reset = new Choice(di->T("Reset"), "", false, new AnchorLayoutParams(leftColumnWidth, WRAP_CONTENT, 10, NONE, NONE, 84)); Choice *back = new Choice(di->T("Back"), "", false, new AnchorLayoutParams(leftColumnWidth, WRAP_CONTENT, 10, NONE, NONE, 10)); - Choice *visibility = new Choice(co->T("Visibility"), "", false, new AnchorLayoutParams(leftColumnWidth, WRAP_CONTENT, 10, NONE, NONE, 298)); + Choice *visibility = new Choice(co->T("Customize"), "", false, new AnchorLayoutParams(leftColumnWidth, WRAP_CONTENT, 10, NONE, NONE, 298)); // controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fButtonScale, 0.80, 2.0, co->T("Button Scaling"), screenManager())) // ->OnChange.Handle(this, &GameSettingsScreen::OnChangeControlScaling); diff --git a/UI/TouchControlVisibilityScreen.cpp b/UI/TouchControlVisibilityScreen.cpp index 9a6bf19a49..af88eb93e3 100644 --- a/UI/TouchControlVisibilityScreen.cpp +++ b/UI/TouchControlVisibilityScreen.cpp @@ -20,6 +20,7 @@ #include "TouchControlVisibilityScreen.h" #include "Core/Config.h" #include "Common/Data/Text/I18n.h" +#include "ComboKeyMappingScreen.h" static const int leftColumnWidth = 140; @@ -42,6 +43,7 @@ private: void TouchControlVisibilityScreen::CreateViews() { using namespace UI; + using namespace CustomKey; auto di = GetI18NCategory("Dialog"); auto co = GetI18NCategory("Controls"); @@ -70,30 +72,62 @@ void TouchControlVisibilityScreen::CreateViews() { gridsettings.fillCells = true; GridLayout *grid = vert->Add(new GridLayoutList(gridsettings, new LayoutParams(FILL_PARENT, WRAP_CONTENT))); - static const char* rightAnalogKey = "Right Analog Stick (tap to customize)"; toggles_.clear(); - toggles_.push_back({ "Circle", &g_Config.bShowTouchCircle, ImageID("I_CIRCLE") }); - toggles_.push_back({ "Cross", &g_Config.bShowTouchCross, ImageID("I_CROSS") }); - toggles_.push_back({ "Square", &g_Config.bShowTouchSquare, ImageID("I_SQUARE") }); - toggles_.push_back({ "Triangle", &g_Config.bShowTouchTriangle, ImageID("I_TRIANGLE") }); - toggles_.push_back({ "L", &g_Config.touchLKey.show, ImageID("I_L") }); - toggles_.push_back({ "R", &g_Config.touchRKey.show, ImageID("I_R") }); - toggles_.push_back({ "Start", &g_Config.touchStartKey.show, ImageID("I_START") }); - toggles_.push_back({ "Select", &g_Config.touchSelectKey.show, ImageID("I_SELECT") }); - toggles_.push_back({ "Dpad", &g_Config.touchDpad.show, ImageID::invalid() }); - toggles_.push_back({ "Analog Stick", &g_Config.touchAnalogStick.show, ImageID::invalid() }); - toggles_.push_back({ rightAnalogKey, &g_Config.touchRightAnalogStick.show, ImageID::invalid() }); - toggles_.push_back({ "Unthrottle", &g_Config.touchUnthrottleKey.show, ImageID::invalid() }); - toggles_.push_back({ "Combo0", &g_Config.touchCombo0.show, ImageID("I_1") }); - toggles_.push_back({ "Combo1", &g_Config.touchCombo1.show, ImageID("I_2") }); - toggles_.push_back({ "Combo2", &g_Config.touchCombo2.show, ImageID("I_3") }); - toggles_.push_back({ "Combo3", &g_Config.touchCombo3.show, ImageID("I_4") }); - toggles_.push_back({ "Combo4", &g_Config.touchCombo4.show, ImageID("I_5") }); - toggles_.push_back({ "Alt speed 1", &g_Config.touchSpeed1Key.show, ImageID::invalid() }); - toggles_.push_back({ "Alt speed 2", &g_Config.touchSpeed2Key.show, ImageID::invalid() }); - toggles_.push_back({ "RapidFire", &g_Config.touchRapidFireKey.show, ImageID::invalid() }); - toggles_.push_back({ "Auto Analog Rotation (CW)", &g_Config.touchAnalogRotationCWKey.show, ImageID::invalid() }); - toggles_.push_back({ "Auto Analog Rotation (CCW)", &g_Config.touchAnalogRotationCCWKey.show, ImageID::invalid() }); + toggles_.push_back({ "Circle", &g_Config.bShowTouchCircle, ImageID("I_CIRCLE"), nullptr }); + toggles_.push_back({ "Cross", &g_Config.bShowTouchCross, ImageID("I_CROSS"), nullptr }); + toggles_.push_back({ "Square", &g_Config.bShowTouchSquare, ImageID("I_SQUARE"), nullptr }); + toggles_.push_back({ "Triangle", &g_Config.bShowTouchTriangle, ImageID("I_TRIANGLE"), nullptr }); + toggles_.push_back({ "L", &g_Config.touchLKey.show, ImageID("I_L"), nullptr }); + toggles_.push_back({ "R", &g_Config.touchRKey.show, ImageID("I_R"), nullptr }); + toggles_.push_back({ "Start", &g_Config.touchStartKey.show, ImageID("I_START"), nullptr }); + toggles_.push_back({ "Select", &g_Config.touchSelectKey.show, ImageID("I_SELECT"), nullptr }); + toggles_.push_back({ "Dpad", &g_Config.touchDpad.show, ImageID::invalid(), nullptr }); + toggles_.push_back({ "Analog Stick", &g_Config.touchAnalogStick.show, ImageID::invalid(), nullptr }); + toggles_.push_back({ "Right Analog Stick", &g_Config.touchRightAnalogStick.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new RightAnalogMappingScreen()); + return UI::EVENT_DONE; + }}); + toggles_.push_back({ "Unthrottle", &g_Config.touchUnthrottleKey.show, ImageID::invalid(), nullptr }); + toggles_.push_back({ "Custom 1", &g_Config.touchCombo0.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new ComboKeyScreen(0)); + return UI::EVENT_DONE; + }}); + toggles_.push_back({ "Custom 2", &g_Config.touchCombo1.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new ComboKeyScreen(1)); + return UI::EVENT_DONE; + }}); + toggles_.push_back({ "Custom 3", &g_Config.touchCombo2.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new ComboKeyScreen(2)); + return UI::EVENT_DONE; + }}); + toggles_.push_back({ "Custom 4", &g_Config.touchCombo3.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new ComboKeyScreen(3)); + return UI::EVENT_DONE; + }}); + toggles_.push_back({ "Custom 5", &g_Config.touchCombo4.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new ComboKeyScreen(4)); + return UI::EVENT_DONE; + }}); + toggles_.push_back({ "Custom 6", &g_Config.touchCombo5.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new ComboKeyScreen(5)); + return UI::EVENT_DONE; + }}); + toggles_.push_back({ "Custom 7", &g_Config.touchCombo6.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new ComboKeyScreen(6)); + return UI::EVENT_DONE; + }}); + toggles_.push_back({ "Custom 8", &g_Config.touchCombo7.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new ComboKeyScreen(7)); + return UI::EVENT_DONE; + }}); + toggles_.push_back({ "Custom 9", &g_Config.touchCombo8.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new ComboKeyScreen(8)); + return UI::EVENT_DONE; + }}); + toggles_.push_back({ "Custom 10", &g_Config.touchCombo9.show, ImageID::invalid(), [=](EventParams &e) { + screenManager()->push(new ComboKeyScreen(9)); + return UI::EVENT_DONE; + }}); auto mc = GetI18NCategory("MappableControls"); for (auto toggle : toggles_) { @@ -102,21 +136,17 @@ void TouchControlVisibilityScreen::CreateViews() { CheckBox *checkbox = new CheckBox(toggle.show, "", "", new LinearLayoutParams(50, WRAP_CONTENT)); row->Add(checkbox); - - if (toggle.key == rightAnalogKey) { - Choice *rightAnalog = new Choice(mc->T(rightAnalogKey), "", false, new LinearLayoutParams(1.0f)); - rightAnalog->SetCentered(true); - row->Add(rightAnalog)->OnClick.Handle(this, &TouchControlVisibilityScreen::RightAnalogBindScreen); + Choice *choice; + if (toggle.handle) { + choice = new Choice(std::string(mc->T(toggle.key))+mc->T(" (tap to customize)"), "", false, new LinearLayoutParams(1.0f)); + choice->OnClick.Add(toggle.handle); + } else if (toggle.img.isValid()) { + choice = new CheckBoxChoice(toggle.img, checkbox, new LinearLayoutParams(1.0f)); } else { - Choice *choice; - if (toggle.img.isValid()) { - choice = new CheckBoxChoice(toggle.img, checkbox, new LinearLayoutParams(1.0f)); - } else { - choice = new CheckBoxChoice(mc->T(toggle.key), checkbox, new LinearLayoutParams(1.0f)); - } - choice->SetCentered(true); - row->Add(choice); + choice = new CheckBoxChoice(mc->T(toggle.key), checkbox, new LinearLayoutParams(1.0f)); } + choice->SetCentered(true); + row->Add(choice); grid->Add(row); } } @@ -143,14 +173,14 @@ void RightAnalogMappingScreen::CreateViews() { vert->SetSpacing(0); static const char *rightAnalogButton[] = {"None", "L", "R", "Square", "Triangle", "Circle", "Cross", "D-pad up", "D-pad down", "D-pad left", "D-pad right", "Start", "Select"}; - + + vert->Add(new CheckBox(&g_Config.touchRightAnalogStick.show, co->T("Visible"))); vert->Add(new CheckBox(&g_Config.bRightAnalogCustom, co->T("Use custom right analog"))); PopupMultiChoice *rightAnalogUp = vert->Add(new PopupMultiChoice(&g_Config.iRightAnalogUp, mc->T("RightAn.Up"), rightAnalogButton, 0, ARRAY_SIZE(rightAnalogButton), mc->GetName(), screenManager())); PopupMultiChoice *rightAnalogDown = vert->Add(new PopupMultiChoice(&g_Config.iRightAnalogDown, mc->T("RightAn.Down"), rightAnalogButton, 0, ARRAY_SIZE(rightAnalogButton), mc->GetName(), screenManager())); PopupMultiChoice *rightAnalogLeft = vert->Add(new PopupMultiChoice(&g_Config.iRightAnalogLeft, mc->T("RightAn.Left"), rightAnalogButton, 0, ARRAY_SIZE(rightAnalogButton), mc->GetName(), screenManager())); PopupMultiChoice *rightAnalogRight = vert->Add(new PopupMultiChoice(&g_Config.iRightAnalogRight, mc->T("RightAn.Right"), rightAnalogButton, 0, ARRAY_SIZE(rightAnalogButton), mc->GetName(), screenManager())); PopupMultiChoice *rightAnalogPress = vert->Add(new PopupMultiChoice(&g_Config.iRightAnalogPress, co->T("Keep this button pressed when right analog is pressed"), rightAnalogButton, 0, ARRAY_SIZE(rightAnalogButton), mc->GetName(), screenManager())); - vert->Add(new CheckBox(&g_Config.touchRightAnalogStick.show, co->T("Show right analog"))); rightAnalogUp->SetEnabledPtr(&g_Config.bRightAnalogCustom); rightAnalogDown->SetEnabledPtr(&g_Config.bRightAnalogCustom); rightAnalogLeft->SetEnabledPtr(&g_Config.bRightAnalogCustom); diff --git a/UI/TouchControlVisibilityScreen.h b/UI/TouchControlVisibilityScreen.h index 04be4a1872..c3a7df1613 100644 --- a/UI/TouchControlVisibilityScreen.h +++ b/UI/TouchControlVisibilityScreen.h @@ -28,6 +28,7 @@ struct TouchButtonToggle { const char *key; bool *show; ImageID img; + std::function handle; }; class TouchControlVisibilityScreen : public UIDialogScreenWithBackground { diff --git a/UI/UI.vcxproj b/UI/UI.vcxproj index 8c621c2f4e..b56a983acd 100644 --- a/UI/UI.vcxproj +++ b/UI/UI.vcxproj @@ -51,6 +51,7 @@ + @@ -85,6 +86,7 @@ + @@ -394,4 +396,4 @@ - + \ No newline at end of file diff --git a/UI/UI.vcxproj.filters b/UI/UI.vcxproj.filters index eb282757cf..2c63641535 100644 --- a/UI/UI.vcxproj.filters +++ b/UI/UI.vcxproj.filters @@ -78,6 +78,9 @@ Screens + + Screens + @@ -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 +