diff --git a/platform/xcode/liblove.xcodeproj/project.pbxproj b/platform/xcode/liblove.xcodeproj/project.pbxproj index 03faf5e94..59d3ff255 100644 --- a/platform/xcode/liblove.xcodeproj/project.pbxproj +++ b/platform/xcode/liblove.xcodeproj/project.pbxproj @@ -884,6 +884,8 @@ FAAA3FDA1F64B3AD00F89E99 /* lstrlib.h in Headers */ = {isa = PBXBuildFile; fileRef = FAAA3FD51F64B3AD00F89E99 /* lstrlib.h */; }; FAAA3FDB1F64B3AD00F89E99 /* lutf8lib.c in Sources */ = {isa = PBXBuildFile; fileRef = FAAA3FD61F64B3AD00F89E99 /* lutf8lib.c */; }; FAAA3FDC1F64B3AD00F89E99 /* lutf8lib.h in Headers */ = {isa = PBXBuildFile; fileRef = FAAA3FD71F64B3AD00F89E99 /* lutf8lib.h */; }; + FAAC2F79251A9D2200BCB81B /* apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = FAAC2F78251A9D2200BCB81B /* apple.mm */; }; + FAAC2F7A251A9D2200BCB81B /* apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = FAAC2F78251A9D2200BCB81B /* apple.mm */; }; FAAFF04416CB11C700CCDE45 /* OpenAL-Soft.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAAFF04316CB11C700CCDE45 /* OpenAL-Soft.framework */; }; FAB17BE61ABFAA9000F9BA27 /* lz4.c in Sources */ = {isa = PBXBuildFile; fileRef = FAB17BE41ABFAA9000F9BA27 /* lz4.c */; }; FAB17BE71ABFAA9000F9BA27 /* lz4.c in Sources */ = {isa = PBXBuildFile; fileRef = FAB17BE41ABFAA9000F9BA27 /* lz4.c */; }; @@ -1936,6 +1938,8 @@ FAAA3FD51F64B3AD00F89E99 /* lstrlib.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lstrlib.h; sourceTree = ""; }; FAAA3FD61F64B3AD00F89E99 /* lutf8lib.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lutf8lib.c; sourceTree = ""; }; FAAA3FD71F64B3AD00F89E99 /* lutf8lib.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lutf8lib.h; sourceTree = ""; }; + FAAC2F78251A9D2200BCB81B /* apple.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = apple.mm; sourceTree = ""; }; + FAAC2F7F251A9D3E00BCB81B /* apple.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = apple.h; sourceTree = ""; }; FAAFF04316CB11C700CCDE45 /* OpenAL-Soft.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = "OpenAL-Soft.framework"; path = "/Library/Frameworks/OpenAL-Soft.framework"; sourceTree = ""; }; FAB17BE41ABFAA9000F9BA27 /* lz4.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lz4.c; sourceTree = ""; }; FAB17BE51ABFAA9000F9BA27 /* lz4.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lz4.h; sourceTree = ""; }; @@ -2308,6 +2312,8 @@ children = ( FAA3A9AC1B7D465A00CED060 /* android.cpp */, FAA3A9AD1B7D465A00CED060 /* android.h */, + FAAC2F7F251A9D3E00BCB81B /* apple.h */, + FAAC2F78251A9D2200BCB81B /* apple.mm */, FA0B78F71A958E3B000E1D17 /* b64.cpp */, FA0B78F81A958E3B000E1D17 /* b64.h */, FA6BDE5B1F31725300786805 /* Color.h */, @@ -4435,7 +4441,7 @@ 08FB7793FE84155DC02AAC07 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1230; TargetAttributes = { FA0B78DC1A958B90000E1D17 = { CreatedOnToolsVersion = 6.1.1; @@ -4873,6 +4879,7 @@ FA0B7E1F1A95902C000E1D17 /* Physics.cpp in Sources */, FA6A2B7B1F60B8250074C308 /* wrap_ByteData.cpp in Sources */, FA0B7E821A95902C000E1D17 /* Shape.cpp in Sources */, + FAAC2F7A251A9D2200BCB81B /* apple.mm in Sources */, FA0B7ACE1A958EA3000E1D17 /* packet.c in Sources */, FABDA9872552448200B5C523 /* b2_body.cpp in Sources */, FAF140891E20934C00F898D2 /* PoolAlloc.cpp in Sources */, @@ -5283,6 +5290,7 @@ 217DFC0B1D9F6D490055D849 /* unixtcp.c in Sources */, FAF140881E20934C00F898D2 /* PoolAlloc.cpp in Sources */, FA0B7DEE1A95902C000E1D17 /* Mouse.cpp in Sources */, + FAAC2F79251A9D2200BCB81B /* apple.mm in Sources */, FAA54ACC1F91660400A8FA7B /* TheoraVideoStream.cpp in Sources */, FA1E887E1DF363CD00E808AA /* Filter.cpp in Sources */, FABDA9862552448200B5C523 /* b2_body.cpp in Sources */, @@ -5314,7 +5322,7 @@ 10D5479E63C26BB35EB5482E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -5362,10 +5370,10 @@ "\"$(SRCROOT)/../../src/modules\"", "\"$(SRCROOT)/../../src/libraries/enet/libenet/include\"", ); - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "@rpath"; LIBRARY_SEARCH_PATHS = ""; - MACOSX_DEPLOYMENT_TARGET = 10.7; + MACOSX_DEPLOYMENT_TARGET = 10.9; ONLY_ACTIVE_ARCH = NO; SDKROOT = macosx; USE_HEADERMAP = NO; @@ -5376,7 +5384,7 @@ 64274E785071353E1A1D0D4B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -5428,10 +5436,10 @@ "\"$(SRCROOT)/../../src/modules\"", "\"$(SRCROOT)/../../src/libraries/enet/libenet/include\"", ); - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "@rpath"; LIBRARY_SEARCH_PATHS = ""; - MACOSX_DEPLOYMENT_TARGET = 10.7; + MACOSX_DEPLOYMENT_TARGET = 10.9; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; USE_HEADERMAP = NO; @@ -5563,7 +5571,7 @@ FA5326C4189719C700F7BBF4 /* Distribution */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -5611,11 +5619,11 @@ "\"$(SRCROOT)/../../src/modules\"", "\"$(SRCROOT)/../../src/libraries/enet/libenet/include\"", ); - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "@rpath"; LIBRARY_SEARCH_PATHS = ""; LLVM_LTO = YES; - MACOSX_DEPLOYMENT_TARGET = 10.7; + MACOSX_DEPLOYMENT_TARGET = 10.9; ONLY_ACTIVE_ARCH = NO; SDKROOT = macosx; USE_HEADERMAP = NO; @@ -5656,6 +5664,7 @@ "$(inherited)", "$(PROJECT_DIR)/ios/libraries/freetype", ); + MARKETING_VERSION = 11.4; OTHER_LDFLAGS = ( "-undefined", dynamic_lookup, @@ -5699,6 +5708,7 @@ "$(inherited)", "$(PROJECT_DIR)/ios/libraries/freetype", ); + MARKETING_VERSION = 11.4; OTHER_LDFLAGS = ( "-undefined", dynamic_lookup, @@ -5743,6 +5753,7 @@ "$(inherited)", "$(PROJECT_DIR)/ios/libraries/freetype", ); + MARKETING_VERSION = 11.4; OTHER_LDFLAGS = ( "-undefined", dynamic_lookup, diff --git a/platform/xcode/love.xcodeproj/project.pbxproj b/platform/xcode/love.xcodeproj/project.pbxproj index 7b78fdd03..326654779 100644 --- a/platform/xcode/love.xcodeproj/project.pbxproj +++ b/platform/xcode/love.xcodeproj/project.pbxproj @@ -503,7 +503,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -557,9 +557,9 @@ "\"$(SRCROOT)/../../src/modules\"", ); INFOPLIST_FILE = "love-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.7; + MACOSX_DEPLOYMENT_TARGET = 10.9; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ""; "OTHER_LDFLAGS[arch=x86_64]" = ( @@ -582,7 +582,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -638,10 +638,10 @@ "\"$(SRCROOT)/../../src/modules\"", ); INFOPLIST_FILE = "love-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; LLVM_LTO = YES; - MACOSX_DEPLOYMENT_TARGET = 10.7; + MACOSX_DEPLOYMENT_TARGET = 10.9; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = ""; "OTHER_LDFLAGS[arch=x86_64]" = ( @@ -813,7 +813,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -869,10 +869,10 @@ "\"$(SRCROOT)/../../src/modules\"", ); INFOPLIST_FILE = "love-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; LLVM_LTO = YES; - MACOSX_DEPLOYMENT_TARGET = 10.7; + MACOSX_DEPLOYMENT_TARGET = 10.9; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = ""; "OTHER_LDFLAGS[arch=x86_64]" = ( diff --git a/src/common/StringMap.h b/src/common/StringMap.h index 4e6dc25de..b373e8f32 100644 --- a/src/common/StringMap.h +++ b/src/common/StringMap.h @@ -197,6 +197,21 @@ bool getConstant(const char *in, type &out) { return name##s.find(in, out); } \ bool getConstant(type in, const char *&out) { return name##s.find(in, out); } \ std::vector getConstants(type) { return name##s.getNames(); } +#define STRINGMAP_CLASS_DECLARE(type) \ +static bool getConstant(const char *in, type &out); \ +static bool getConstant(type in, const char *&out); \ +static std::vector getConstants(type); \ + +#define STRINGMAP_CLASS_BEGIN(classname, type, count, name) \ +static StringMap::Entry name##Entries[] = + +#define STRINGMAP_CLASS_END(classname, type, count, name) \ +; \ +static StringMap name##s(name##Entries, sizeof(name##Entries)); \ +bool classname::getConstant(const char *in, type &out) { return name##s.find(in, out); } \ +bool classname::getConstant(type in, const char *&out) { return name##s.find(in, out); } \ +std::vector classname::getConstants(type) { return name##s.getNames(); } + } // love #endif // LOVE_STRING_MAP_H diff --git a/src/common/android.cpp b/src/common/android.cpp index 0b7724ef0..22a6ccefa 100644 --- a/src/common/android.cpp +++ b/src/common/android.cpp @@ -148,7 +148,14 @@ bool openURL(const std::string &url) JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv(); jclass activity = env->FindClass("org/love2d/android/GameActivity"); - jmethodID openURL = env->GetStaticMethodID(activity, "openURL", "(Ljava/lang/String;)Z"); + jmethodID openURL = env->GetStaticMethodID(activity, "openURLFromLOVE", "(Ljava/lang/String;)Z"); + + if (openURL == nullptr) + { + env->ExceptionClear(); + openURL = env->GetStaticMethodID(activity, "openURL", "(Ljava/lang/String;)Z"); + } + jstring url_jstring = (jstring) env->NewStringUTF(url.c_str()); jboolean result = env->CallStaticBooleanMethod(activity, openURL, url_jstring); diff --git a/src/common/apple.h b/src/common/apple.h new file mode 100644 index 000000000..207f75663 --- /dev/null +++ b/src/common/apple.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2006-2020 LOVE Development Team + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + **/ + +#pragma once + +#include "config.h" + +#if defined(LOVE_IOS) || defined(LOVE_MACOS) + +#include + +namespace love +{ +namespace apple +{ + +enum UserDirectory +{ + USER_DIRECTORY_HOME, + USER_DIRECTORY_APPSUPPORT, + USER_DIRECTORY_DOCUMENTS, + USER_DIRECTORY_DESKTOP, + USER_DIRECTORY_CACHES, + USER_DIRECTORY_TEMP, +}; + +std::string getUserDirectory(UserDirectory dir); +std::string getExecutablePath(); + +} // apple +} // love + +#endif // defined(LOVE_IOS) || defined(LOVE_MACOS) diff --git a/src/common/apple.mm b/src/common/apple.mm new file mode 100644 index 000000000..dcffa4d76 --- /dev/null +++ b/src/common/apple.mm @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2006-2020 LOVE Development Team + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + **/ + +#include "apple.h" + +#if defined(LOVE_IOS) || defined(LOVE_MACOS) + +#import + +namespace love +{ +namespace apple +{ + +std::string getUserDirectory(UserDirectory dir) +{ + std::string path; + NSSearchPathDirectory nsdir = NSTrashDirectory; + + @autoreleasepool + { + switch (dir) + { + case USER_DIRECTORY_HOME: + return NSHomeDirectory().UTF8String; + case USER_DIRECTORY_APPSUPPORT: + nsdir = NSApplicationSupportDirectory; + break; + case USER_DIRECTORY_DOCUMENTS: + nsdir = NSDocumentDirectory; + break; + case USER_DIRECTORY_DESKTOP: + nsdir = NSDesktopDirectory; + break; + case USER_DIRECTORY_CACHES: + nsdir = NSCachesDirectory; + break; + case USER_DIRECTORY_TEMP: + nsdir = NSItemReplacementDirectory; + break; + } + + NSArray *dirs = [[NSFileManager defaultManager] URLsForDirectory:nsdir inDomains:NSUserDomainMask]; + if (dirs.count > 0) + path = [dirs[0].path UTF8String]; + } + + return path; +} + +std::string getExecutablePath() +{ + @autoreleasepool + { + return std::string([NSBundle mainBundle].executablePath.UTF8String); + } +} + +} // apple +} // love + +#endif // defined(LOVE_IOS) || defined(LOVE_MACOS) diff --git a/src/common/ios.h b/src/common/ios.h index 7c103f9c2..42599535e 100644 --- a/src/common/ios.h +++ b/src/common/ios.h @@ -42,27 +42,12 @@ namespace ios **/ std::string getLoveInResources(bool &fused); -/** - * Gets the directory path where files should be stored. - **/ -std::string getAppdataDirectory(); - -/** - * Get the home directory (on iOS, this really means the app's sandbox dir.) - **/ -std::string getHomeDirectory(); - /** * Opens the specified URL with the default program associated with the URL's * scheme. **/ bool openURL(const std::string &url); -/** - * Returns the full path to the executable. - **/ -std::string getExecutablePath(); - /** * Causes devices with vibration support to vibrate for about 0.5 seconds. **/ diff --git a/src/common/ios.mm b/src/common/ios.mm index 095f570b1..8b1a87c71 100644 --- a/src/common/ios.mm +++ b/src/common/ios.mm @@ -19,6 +19,7 @@ **/ #include "ios.h" +#include "apple.h" #ifdef LOVE_IOS @@ -127,12 +128,6 @@ static bool deleteFileInDocuments(NSString *filename); @end -static NSString *getDocumentsDirectory() -{ - NSArray *docdirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - return docdirs[0]; -} - static NSArray *getLovesInDocuments() { NSMutableArray *paths = [NSMutableArray new]; @@ -339,38 +334,6 @@ std::string getLoveInResources(bool &fused) return path; } -static std::string getUserDirectory(NSSearchPathDirectory dir) -{ - std::string path; - - @autoreleasepool - { - NSArray *dirs = [[NSFileManager defaultManager] URLsForDirectory:dir inDomains:NSUserDomainMask]; - - if (dirs.count > 0) - path = [dirs[0].path UTF8String]; - } - - return path; -} - -std::string getAppdataDirectory() -{ - return getUserDirectory(NSApplicationSupportDirectory); -} - -std::string getHomeDirectory() -{ - std::string path; - - @autoreleasepool - { - path = [NSHomeDirectory() UTF8String]; - } - - return path; -} - bool openURL(const std::string &url) { bool success = false; @@ -387,14 +350,6 @@ bool openURL(const std::string &url) return success; } -std::string getExecutablePath() -{ - @autoreleasepool - { - return std::string([NSBundle mainBundle].executablePath.UTF8String); - } -} - void vibrate() { @autoreleasepool diff --git a/src/common/macos.h b/src/common/macos.h index a3fcbdd9d..297c8f116 100644 --- a/src/common/macos.h +++ b/src/common/macos.h @@ -31,8 +31,6 @@ namespace love namespace macos { -std::string getAppdataDirectory(); - /** * Returns the filepath of the first detected love file in the Resources folder * in the main bundle (love.app.) @@ -46,11 +44,6 @@ std::string getLoveInResources(); **/ std::string checkDropEvents(); -/** - * Returns the full path to the executable. - **/ -std::string getExecutablePath(); - /** * Bounce the dock icon, if the app isn't in the foreground. **/ diff --git a/src/common/macos.mm b/src/common/macos.mm index 1ee165a5b..06edf8a37 100644 --- a/src/common/macos.mm +++ b/src/common/macos.mm @@ -36,26 +36,6 @@ namespace love namespace macos { -static std::string getUserDirectory(NSSearchPathDirectory dir) -{ - std::string path; - - @autoreleasepool - { - NSArray *dirs = [[NSFileManager defaultManager] URLsForDirectory:dir inDomains:NSUserDomainMask]; - - if (dirs.count > 0) - path = [dirs[0].path UTF8String]; - } - - return path; -} - -std::string getAppdataDirectory() -{ - return getUserDirectory(NSApplicationSupportDirectory); -} - std::string getLoveInResources() { std::string path; @@ -94,14 +74,6 @@ std::string checkDropEvents() return dropstr; } -std::string getExecutablePath() -{ - @autoreleasepool - { - return std::string([NSBundle mainBundle].executablePath.UTF8String); - } -} - void requestAttention(bool continuous) { @autoreleasepool diff --git a/src/libraries/physfs/physfs.c b/src/libraries/physfs/physfs.c index eec40b2bf..3dac3291f 100644 --- a/src/libraries/physfs/physfs.c +++ b/src/libraries/physfs/physfs.c @@ -49,6 +49,7 @@ typedef struct __PHYSFS_DIRHANDLE__ char *root; /* subdirectory of archiver to use as root of archive (NULL for actual root) */ size_t rootlen; /* subdirectory of archiver to use as root of archive (NULL for actual root) */ const PHYSFS_Archiver *funcs; /* Ptr to archiver info for this handle. */ + int forWriting; /* Whether this was opened for writing or not. */ struct __PHYSFS_DIRHANDLE__ *next; /* linked list stuff. */ } DirHandle; @@ -862,6 +863,7 @@ static DirHandle *tryOpenDir(PHYSFS_Io *io, const PHYSFS_Archiver *funcs, retval->mountPoint = NULL; retval->funcs = funcs; retval->opaque = opaque; + retval->forWriting = forWriting; } /* else */ } /* if */ @@ -1102,6 +1104,23 @@ static int freeDirHandle(DirHandle *dh, FileHandle *openList) } /* freeDirHandle */ +static int dirHandleFilesOpen(DirHandle *dh, FileHandle *openList) +{ + FileHandle *i; + + if (dh == NULL) + return 0; + + for (i = openList; i != NULL; i = i->next) + { + if (i->dirHandle == dh) + return 1; + } + + return 0; +} /* dirHandleFilesOpen */ + + static char *calculateBaseDir(const char *argv0) { const char dirsep = __PHYSFS_platformDirSeparator; @@ -1749,7 +1768,7 @@ int PHYSFS_setRoot(const char *archive, const char *subdir) static int doMount(PHYSFS_Io *io, const char *fname, - const char *mountPoint, int appendToPath) + const char *mountPoint, int appendToPath, int forWriting) { DirHandle *dh; DirHandle *prev = NULL; @@ -1770,7 +1789,7 @@ static int doMount(PHYSFS_Io *io, const char *fname, prev = i; } /* for */ - dh = createDirHandle(io, fname, mountPoint, 0); + dh = createDirHandle(io, fname, mountPoint, forWriting); BAIL_IF_MUTEX_ERRPASS(!dh, stateLock, 0); if (appendToPath) @@ -1797,7 +1816,7 @@ int PHYSFS_mountIo(PHYSFS_Io *io, const char *fname, BAIL_IF(!io, PHYSFS_ERR_INVALID_ARGUMENT, 0); BAIL_IF(!fname, PHYSFS_ERR_INVALID_ARGUMENT, 0); BAIL_IF(io->version != 0, PHYSFS_ERR_UNSUPPORTED, 0); - return doMount(io, fname, mountPoint, appendToPath); + return doMount(io, fname, mountPoint, appendToPath, 0); } /* PHYSFS_mountIo */ @@ -1813,7 +1832,7 @@ int PHYSFS_mountMemory(const void *buf, PHYSFS_uint64 len, void (*del)(void *), io = __PHYSFS_createMemoryIo(buf, len, del); BAIL_IF_ERRPASS(!io, 0); - retval = doMount(io, fname, mountPoint, appendToPath); + retval = doMount(io, fname, mountPoint, appendToPath, 0); if (!retval) { /* docs say not to call (del) in case of failure, so cheat. */ @@ -1837,7 +1856,7 @@ int PHYSFS_mountHandle(PHYSFS_File *file, const char *fname, io = __PHYSFS_createHandleIo(file); BAIL_IF_ERRPASS(!io, 0); - retval = doMount(io, fname, mountPoint, appendToPath); + retval = doMount(io, fname, mountPoint, appendToPath, 0); if (!retval) { /* docs say not to destruct in case of failure, so cheat. */ @@ -1852,7 +1871,14 @@ int PHYSFS_mountHandle(PHYSFS_File *file, const char *fname, int PHYSFS_mount(const char *newDir, const char *mountPoint, int appendToPath) { BAIL_IF(!newDir, PHYSFS_ERR_INVALID_ARGUMENT, 0); - return doMount(NULL, newDir, mountPoint, appendToPath); + return doMount(NULL, newDir, mountPoint, appendToPath, 0); +} /* PHYSFS_mount */ + + +int PHYSFS_mountRW(const char *newDir, const char *mountPoint, int appendToPath) +{ + BAIL_IF(!newDir, PHYSFS_ERR_INVALID_ARGUMENT, 0); + return doMount(NULL, newDir, mountPoint, appendToPath, 1); } /* PHYSFS_mount */ @@ -1882,6 +1908,8 @@ int PHYSFS_unmount(const char *oldDir) if (strcmp(i->dirName, oldDir) == 0) { next = i->next; + if (i->forWriting && dirHandleFilesOpen(i, openWriteList)) + BAIL_MUTEX(PHYSFS_ERR_FILES_STILL_OPEN, stateLock, 0); BAIL_IF_MUTEX_ERRPASS(!freeDirHandle(i, openReadList), stateLock, 0); @@ -1899,6 +1927,28 @@ int PHYSFS_unmount(const char *oldDir) } /* PHYSFS_unmount */ +int PHYSFS_canUnmount(const char *oldDir) +{ + DirHandle *i; + + BAIL_IF(oldDir == NULL, PHYSFS_ERR_INVALID_ARGUMENT, 0); + + __PHYSFS_platformGrabMutex(stateLock); + for (i = searchPath; i != NULL; i = i->next) + { + if (strcmp(i->dirName, oldDir) == 0) + { + if (i->forWriting && dirHandleFilesOpen(i, openWriteList)) + BAIL_MUTEX(PHYSFS_ERR_OK, stateLock, 0); + if (dirHandleFilesOpen(i, openReadList)) + BAIL_MUTEX(PHYSFS_ERR_OK, stateLock, 0); + BAIL_MUTEX(PHYSFS_ERR_OK, stateLock, 1); + } + } + + BAIL_MUTEX(PHYSFS_ERR_NOT_MOUNTED, stateLock, 0); +} /* PHYSFS_canUnmount */ + char **PHYSFS_getSearchPath(void) { return doEnumStringList(PHYSFS_getSearchPathCallback); @@ -2159,10 +2209,76 @@ static int verifyPath(DirHandle *h, char **_fname, int allowMissing) } /* verifyPath */ -/* This must hold the stateLock before calling. */ -static int doMkdir(const char *_dname, char *dname) +static int countPathComponents(const char *path) +{ + int components = 0; + const char *start; + const char *end; + + if (path == NULL) + return 0; + + if (*path != '/') + components = 1; + + start = path; + while (1) + { + end = strchr(start, '/'); + + if (end == NULL) + break; + + components++; + start = end + 1; + } + + return components; +} /* countPathComponents */ + + +static DirHandle *findWriteHandle(const char *_fname) +{ + DirHandle *i = NULL; + int deepest_path_components = -1; + DirHandle *deepest_dirhandle = NULL; + char *allocated_fname; + char *fname; + size_t len; + + len = strlen(_fname) + longest_root + 1; + allocated_fname = (char *) __PHYSFS_smallAlloc(len); + BAIL_IF(!allocated_fname, PHYSFS_ERR_OUT_OF_MEMORY, NULL); + fname = allocated_fname + longest_root; + + if (sanitizePlatformIndependentPath(_fname, fname)) + { + for (i = searchPath; i != NULL; i = i->next) + { + char *arcfname = fname; + if (i->forWriting && verifyPath(i, &arcfname, 0)) + { + int path_components = 0; + if (i->mountPoint != NULL) + path_components = countPathComponents(i->mountPoint); + if (path_components > deepest_path_components) + { + deepest_path_components = path_components; + deepest_dirhandle = i; + } /* if */ + } /* if */ + } /* for */ + } /* if */ + + __PHYSFS_smallFree(allocated_fname); + + return deepest_dirhandle != NULL ? deepest_dirhandle : writeDir; +} /* findWriteHandle */ + + +/* This must hold the stateLock before calling. */ +static int doMkdir(const char *_dname, char *dname, DirHandle *h) { - DirHandle *h = writeDir; char *start; char *end; int retval = 0; @@ -2212,15 +2328,17 @@ int PHYSFS_mkdir(const char *_dname) int retval = 0; char *dname; size_t len; + DirHandle *h = NULL; BAIL_IF(!_dname, PHYSFS_ERR_INVALID_ARGUMENT, 0); __PHYSFS_platformGrabMutex(stateLock); - BAIL_IF_MUTEX(!writeDir, PHYSFS_ERR_NO_WRITE_DIR, stateLock, 0); - len = strlen(_dname) + dirHandleRootLen(writeDir) + 1; + h = findWriteHandle(_dname); + BAIL_IF_MUTEX(!h, PHYSFS_ERR_NO_WRITE_DIR, stateLock, 0); + len = strlen(_dname) + dirHandleRootLen(h) + 1; dname = (char *) __PHYSFS_smallAlloc(len); BAIL_IF_MUTEX(!dname, PHYSFS_ERR_OUT_OF_MEMORY, stateLock, 0); - retval = doMkdir(_dname, dname); + retval = doMkdir(_dname, dname, h); __PHYSFS_platformReleaseMutex(stateLock); __PHYSFS_smallFree(dname); return retval; @@ -2228,9 +2346,8 @@ int PHYSFS_mkdir(const char *_dname) /* This must hold the stateLock before calling. */ -static int doDelete(const char *_fname, char *fname) +static int doDelete(const char *_fname, char *fname, DirHandle *h) { - DirHandle *h = writeDir; BAIL_IF_ERRPASS(!sanitizePlatformIndependentPathWithRoot(h, _fname, fname), 0); BAIL_IF_ERRPASS(!verifyPath(h, &fname, 0), 0); return h->funcs->remove(h->opaque, fname); @@ -2242,13 +2359,15 @@ int PHYSFS_delete(const char *_fname) int retval; char *fname; size_t len; + DirHandle *h = NULL; __PHYSFS_platformGrabMutex(stateLock); - BAIL_IF_MUTEX(!writeDir, PHYSFS_ERR_NO_WRITE_DIR, stateLock, 0); - len = strlen(_fname) + dirHandleRootLen(writeDir) + 1; + h = findWriteHandle(_fname); + BAIL_IF_MUTEX(!h, PHYSFS_ERR_NO_WRITE_DIR, stateLock, 0); + len = strlen(_fname) + dirHandleRootLen(h) + 1; fname = (char *) __PHYSFS_smallAlloc(len); BAIL_IF_MUTEX(!fname, PHYSFS_ERR_OUT_OF_MEMORY, stateLock, 0); - retval = doDelete(_fname, fname); + retval = doDelete(_fname, fname, h); __PHYSFS_platformReleaseMutex(stateLock); __PHYSFS_smallFree(fname); return retval; @@ -2631,7 +2750,7 @@ static PHYSFS_File *doOpenWrite(const char *_fname, const int appending) __PHYSFS_platformGrabMutex(stateLock); - h = writeDir; + h = findWriteHandle(_fname); BAIL_IF_MUTEX(!h, PHYSFS_ERR_NO_WRITE_DIR, stateLock, 0); len = strlen(_fname) + dirHandleRootLen(h) + 1; @@ -3115,7 +3234,7 @@ int PHYSFS_stat(const char *_fname, PHYSFS_Stat *stat) if (exists) { stat->filetype = PHYSFS_FILETYPE_DIRECTORY; - stat->readonly = 1; + stat->readonly = !i->forWriting; retval = 1; } /* if */ else if (verifyPath(i, &arcfname, 0)) diff --git a/src/libraries/physfs/physfs.h b/src/libraries/physfs/physfs.h index 49d49dae4..b08c02ce7 100644 --- a/src/libraries/physfs/physfs.h +++ b/src/libraries/physfs/physfs.h @@ -2204,6 +2204,14 @@ PHYSFS_DECL int PHYSFS_mount(const char *newDir, const char *mountPoint, int appendToPath); +/** + * Just like mount() but attempts to open the directory for writing as well as + * reading. + */ +PHYSFS_DECL int PHYSFS_mountRW(const char *newDir, + const char *mountPoint, + int appendToPath); + /** * \fn int PHYSFS_getMountPoint(const char *dir) * \brief Determine a mounted archive's mountpoint. @@ -2779,6 +2787,12 @@ PHYSFS_DECL int PHYSFS_enumerate(const char *dir, PHYSFS_EnumerateCallback c, */ PHYSFS_DECL int PHYSFS_unmount(const char *oldDir); +/** + * \fn int PHYSFS_canUnmount(const char *oldDir) + * \brief Check whether a directory or archive can be unmounted. + */ +PHYSFS_DECL int PHYSFS_canUnmount(const char *oldDir); + /** * \fn const PHYSFS_Allocator *PHYSFS_getAllocator(void) diff --git a/src/modules/filesystem/Filesystem.cpp b/src/modules/filesystem/Filesystem.cpp index 109b3c942..391bf732c 100644 --- a/src/modules/filesystem/Filesystem.cpp +++ b/src/modules/filesystem/Filesystem.cpp @@ -26,12 +26,12 @@ #include #include -#if defined(LOVE_MACOS) -#include "common/macos.h" -#elif defined(LOVE_IOS) -#include "common/ios.h" +#if defined(LOVE_MACOS) || defined(LOVE_IOS) +#include "common/apple.h" +#include #elif defined(LOVE_WINDOWS) #include +#include #include "common/utf8.h" #elif defined(LOVE_LINUX) #include @@ -70,6 +70,14 @@ FileData *Filesystem::newFileData(const void *data, size_t size, const char *fil } bool Filesystem::isRealDirectory(const std::string &path) const +{ + FileType ftype = FILETYPE_MAX_ENUM; + if (!getRealPathType(path, ftype)) + return false; + return ftype == FILETYPE_DIRECTORY; +} + +bool Filesystem::getRealPathType(const std::string &path, FileType &ftype) const { #ifdef LOVE_WINDOWS // make sure non-ASCII paths work. @@ -79,23 +87,89 @@ bool Filesystem::isRealDirectory(const std::string &path) const if (_wstat(wpath.c_str(), &buf) != 0) return false; - return (buf.st_mode & _S_IFDIR) == _S_IFDIR; + if ((buf.st_mode & _S_IFREG) == _S_IFREG) + ftype = FILETYPE_FILE; + else if ((buf.st_mode & _S_IFDIR) == _S_IFDIR) + ftype = FILETYPE_DIRECTORY; + else + ftype = FILETYPE_OTHER; #else // Assume POSIX support... struct stat buf; if (stat(path.c_str(), &buf) != 0) return false; - return S_ISDIR(buf.st_mode) != 0; + if (S_ISREG(buf.st_mode)) + ftype = FILETYPE_FILE; + else if (S_ISDIR(buf.st_mode)) + ftype = FILETYPE_DIRECTORY; + else if (S_ISLNK(buf.st_mode)) + ftype = FILETYPE_SYMLINK; + else + ftype = FILETYPE_OTHER; #endif + + return true; +} + +static bool getContainingDirectory(const std::string &path, std::string &newpath) +{ + size_t index = path.find_last_of("/\\"); + + if (index == std::string::npos) + return false; + + newpath = path.substr(0, index); + + // Bail if the root has been stripped out. + return newpath.find("/\\") != std::string::npos; +} + +static bool createDirectoryRaw(const std::string &path) +{ +#ifdef LOVE_WINDOWS + std::wstring wpath = to_widestr(path); + return CreateDirectoryW(wpath.c_str(), nullptr) != 0; +#else + return mkdir(path.c_str(), S_IRWXU) == 0; +#endif +} + +bool Filesystem::createRealDirectory(const std::string &path) +{ + FileType ftype = FILETYPE_MAX_ENUM; + if (getRealPathType(path, ftype)) + return ftype == FILETYPE_DIRECTORY; + + std::vector createpaths = {path}; + + // Find the deepest subdirectory in the given path that actually exists. + while (true) + { + std::string subpath; + if (!getContainingDirectory(createpaths[0], subpath)) + break; + + if (isRealDirectory(subpath)) + break; + + createpaths.insert(createpaths.begin(), subpath); + } + + // Try to create missing subdirectories starting from that existing one. + for (const std::string &p : createpaths) + { + if (!createDirectoryRaw(p)) + return false; + } + + return true; } std::string Filesystem::getExecutablePath() const { -#if defined(LOVE_MACOS) - return love::macos::getExecutablePath(); -#elif defined(LOVE_IOS) - return love::ios::getExecutablePath(); +#if defined(LOVE_MACOS) || defined(LOVE_IOS) + return love::apple::getExecutablePath(); #elif defined(LOVE_WINDOWS) wchar_t buffer[MAX_PATH + 1] = {0}; @@ -120,30 +194,32 @@ std::string Filesystem::getExecutablePath() const #endif } -bool Filesystem::getConstant(const char *in, FileType &out) +STRINGMAP_CLASS_BEGIN(Filesystem, Filesystem::FileType, Filesystem::FILETYPE_MAX_ENUM, fileType) { - return fileTypes.find(in, out); + { "file", Filesystem::FILETYPE_FILE }, + { "directory", Filesystem::FILETYPE_DIRECTORY }, + { "symlink", Filesystem::FILETYPE_SYMLINK }, + { "other", Filesystem::FILETYPE_OTHER }, } +STRINGMAP_CLASS_END(Filesystem, Filesystem::FileType, Filesystem::FILETYPE_MAX_ENUM, fileType) -bool Filesystem::getConstant(FileType in, const char *&out) +STRINGMAP_CLASS_BEGIN(Filesystem, Filesystem::CommonPath, Filesystem::COMMONPATH_MAX_ENUM, commonPath) { - return fileTypes.find(in, out); + { "appsavedir", Filesystem::COMMONPATH_APP_SAVEDIR }, + { "appdocuments", Filesystem::COMMONPATH_APP_DOCUMENTS }, + { "userhome", Filesystem::COMMONPATH_USER_HOME }, + { "userappdata", Filesystem::COMMONPATH_USER_APPDATA }, + { "userdesktop", Filesystem::COMMONPATH_USER_DESKTOP }, + { "userdocuments", Filesystem::COMMONPATH_USER_DOCUMENTS }, } +STRINGMAP_CLASS_END(Filesystem, Filesystem::CommonPath, Filesystem::COMMONPATH_MAX_ENUM, commonPath) -std::vector Filesystem::getConstants(FileType) +STRINGMAP_CLASS_BEGIN(Filesystem, Filesystem::MountPermissions, Filesystem::MOUNT_PERMISSIONS_MAX_ENUM, mountPermissions) { - return fileTypes.getNames(); + { "read", Filesystem::MOUNT_PERMISSIONS_READ }, + { "readwrite", Filesystem::MOUNT_PERMISSIONS_READWRITE }, } - -StringMap::Entry Filesystem::fileTypeEntries[] = -{ - { "file", FILETYPE_FILE }, - { "directory", FILETYPE_DIRECTORY }, - { "symlink", FILETYPE_SYMLINK }, - { "other", FILETYPE_OTHER }, -}; - -StringMap Filesystem::fileTypes(Filesystem::fileTypeEntries, sizeof(Filesystem::fileTypeEntries)); +STRINGMAP_CLASS_END(Filesystem, Filesystem::MountPermissions, Filesystem::MOUNT_PERMISSIONS_MAX_ENUM, mountPermissions) } // filesystem } // love diff --git a/src/modules/filesystem/Filesystem.h b/src/modules/filesystem/Filesystem.h index 715a35f80..e58c9ad13 100644 --- a/src/modules/filesystem/Filesystem.h +++ b/src/modules/filesystem/Filesystem.h @@ -71,12 +71,31 @@ public: FILETYPE_MAX_ENUM }; + enum CommonPath + { + COMMONPATH_APP_SAVEDIR, + COMMONPATH_APP_DOCUMENTS, + COMMONPATH_USER_HOME, + COMMONPATH_USER_APPDATA, + COMMONPATH_USER_DESKTOP, + COMMONPATH_USER_DOCUMENTS, + COMMONPATH_MAX_ENUM + }; + + enum MountPermissions + { + MOUNT_PERMISSIONS_READ, + MOUNT_PERMISSIONS_READWRITE, + MOUNT_PERMISSIONS_MAX_ENUM + }; + struct Info { // Numbers will be -1 if they cannot be determined. int64 size; int64 modtime; FileType type; + bool readonly; }; static love::Type type; @@ -136,8 +155,14 @@ public: virtual bool mount(const char *archive, const char *mountpoint, bool appendToPath = false) = 0; virtual bool mount(Data *data, const char *archivename, const char *mountpoint, bool appendToPath = false) = 0; + + virtual bool mountFullPath(const char *archive, const char *mountpoint, MountPermissions permissions, bool appendToPath = false) = 0; + virtual bool mountCommonPath(CommonPath path, const char *mountpoint, MountPermissions permissions, bool appendToPath = false) = 0; + virtual bool unmount(const char *archive) = 0; virtual bool unmount(Data *data) = 0; + virtual bool unmount(CommonPath path) = 0; + virtual bool unmountFullPath(const char *fullpath) = 0; /** * Creates a new file. @@ -152,6 +177,11 @@ public: **/ virtual FileData *newFileData(const void *data, size_t size, const char *filename) const; + /** + * Gets the full path for the given common path. + */ + virtual std::string getFullCommonPath(CommonPath path) = 0; + /** * Gets the current working directory. **/ @@ -172,7 +202,7 @@ public: /** * Gets the full path of the save folder. **/ - virtual const char *getSaveDirectory() = 0; + virtual std::string getSaveDirectory() = 0; /** * Gets the full path to the directory containing the game source. @@ -258,23 +288,27 @@ public: **/ virtual bool isRealDirectory(const std::string &path) const; + /** + * Recursively creates a directory at the given full OS-dependent path. + **/ + virtual bool createRealDirectory(const std::string &path); + /** * Gets the full platform-dependent path to the executable. **/ virtual std::string getExecutablePath() const; - static bool getConstant(const char *in, FileType &out); - static bool getConstant(FileType in, const char *&out); - static std::vector getConstants(FileType); + STRINGMAP_CLASS_DECLARE(FileType); + STRINGMAP_CLASS_DECLARE(CommonPath); + STRINGMAP_CLASS_DECLARE(MountPermissions); private: + bool getRealPathType(const std::string &path, FileType &ftype) const; + // Should we save external or internal for Android bool useExternal; - static StringMap::Entry fileTypeEntries[]; - static StringMap fileTypes; - }; // Filesystem } // filesystem diff --git a/src/modules/filesystem/NativeFile.cpp b/src/modules/filesystem/NativeFile.cpp index 8082fa7d7..e3a12166a 100644 --- a/src/modules/filesystem/NativeFile.cpp +++ b/src/modules/filesystem/NativeFile.cpp @@ -106,14 +106,25 @@ bool NativeFile::isOpen() const int64 NativeFile::getSize() { + int fd = file ? fileno(file) : -1; + #ifdef LOVE_WINDOWS + + struct _stat64 buf; - // make sure non-ASCII filenames work. - std::wstring wfilename = to_widestr(filename); + if (fd != -1) + { + if (_fstat64(fd, &buf) != 0) + return -1; + } + else + { + // make sure non-ASCII filenames work. + std::wstring wfilename = to_widestr(filename); - struct _stat buf; - if (_wstat(wfilename.c_str(), &buf) != 0) - return -1; + if (_wstat64(wfilename.c_str(), &buf) != 0) + return -1; + } return (int64) buf.st_size; @@ -121,7 +132,13 @@ int64 NativeFile::getSize() // Assume POSIX support... struct stat buf; - if (stat(filename.c_str(), &buf) != 0) + + if (fd != -1) + { + if (fstat(fd, &buf) != 0) + return -1; + } + else if (stat(filename.c_str(), &buf) != 0) return -1; return (int64) buf.st_size; @@ -165,7 +182,7 @@ bool NativeFile::flush() bool NativeFile::isEOF() { - return file == nullptr || feof(file) != 0; + return file == nullptr || tell() >= getSize(); } int64 NativeFile::tell() @@ -173,12 +190,23 @@ int64 NativeFile::tell() if (file == nullptr) return -1; - return (int64) ftell(file); +#ifdef LOVE_WINDOWS + return (int64) _ftelli64(file); +#else + return (int64) ftello(file); +#endif } bool NativeFile::seek(uint64 pos) { - return file != nullptr && fseek(file, (long) pos, SEEK_SET) == 0; + if (file == nullptr) + return false; + +#ifdef LOVE_WINDOWS + return _fseeki64(file, (int64) pos, SEEK_SET) == 0; +#else + return fseeko(file, (off_t) pos, SEEK_SET) == 0; +#endif } bool NativeFile::setBuffer(BufferMode bufmode, int64 size) diff --git a/src/modules/filesystem/physfs/File.cpp b/src/modules/filesystem/physfs/File.cpp index 451be301f..c20626f21 100644 --- a/src/modules/filesystem/physfs/File.cpp +++ b/src/modules/filesystem/physfs/File.cpp @@ -69,14 +69,13 @@ bool File::open(Mode mode) throw love::Exception("Could not open file %s. Does not exist.", filename.c_str()); // Check whether the write directory is set. - if ((mode == MODE_APPEND || mode == MODE_WRITE) && (PHYSFS_getWriteDir() == nullptr) && !setupWriteDirectory()) + if ((mode == MODE_APPEND || mode == MODE_WRITE) && !setupWriteDirectory()) throw love::Exception("Could not set write directory."); // File already open? if (file != nullptr) return false; - PHYSFS_getLastErrorCode(); PHYSFS_File *handle = nullptr; switch (mode) diff --git a/src/modules/filesystem/physfs/Filesystem.cpp b/src/modules/filesystem/physfs/Filesystem.cpp index 99b0d6ff5..b329344ca 100644 --- a/src/modules/filesystem/physfs/Filesystem.cpp +++ b/src/modules/filesystem/physfs/Filesystem.cpp @@ -45,6 +45,10 @@ # include #endif +#if defined(LOVE_IOS) || defined(LOVE_MACOS) +# include "common/apple.h" +#endif + #ifdef LOVE_IOS # include "common/ios.h" #endif @@ -60,44 +64,6 @@ #include "common/android.h" #endif -namespace -{ - size_t getDriveDelim(const std::string &input) - { - for (size_t i = 0; i < input.size(); ++i) - if (input[i] == '/' || input[i] == '\\') - return i; - // Something's horribly wrong - return 0; - } - - std::string getDriveRoot(const std::string &input) - { - return input.substr(0, getDriveDelim(input)+1); - } - - std::string skipDriveRoot(const std::string &input) - { - return input.substr(getDriveDelim(input)+1); - } - - std::string normalize(const std::string &input) - { - std::stringstream out; - bool seenSep = false, isSep = false; - for (size_t i = 0; i < input.size(); ++i) - { - isSep = (input[i] == LOVE_PATH_SEPARATOR[0]); - if (!isSep || !seenSep) - out << input[i]; - seenSep = isSep; - } - - return out.str(); - } - -} - namespace love { namespace filesystem @@ -105,9 +71,46 @@ namespace filesystem namespace physfs { +static std::string normalize(const std::string &input) +{ + std::stringstream out; + bool seenSep = false, isSep = false; + for (size_t i = 0; i < input.size(); ++i) + { + isSep = (input[i] == LOVE_PATH_SEPARATOR[0]); + if (!isSep || !seenSep) + out << input[i]; + seenSep = isSep; + } + + return out.str(); +} + +static const Filesystem::CommonPath appCommonPaths[] = +{ + Filesystem::COMMONPATH_APP_SAVEDIR, + Filesystem::COMMONPATH_APP_DOCUMENTS +}; + +static bool isAppCommonPath(Filesystem::CommonPath path) +{ + switch (path) + { + case Filesystem::COMMONPATH_APP_SAVEDIR: + case Filesystem::COMMONPATH_APP_DOCUMENTS: + return true; + default: + return false; + } +} + Filesystem::Filesystem() - : fused(false) + : appendIdentityToPath(false) + , fused(false) , fusedSet(false) + , fullPaths() + , commonPathMountInfo() + , saveDirectoryNeedsMounting(false) { requirePath = {"?.lua", "?/init.lua"}; cRequirePath = {"??"}; @@ -153,67 +156,65 @@ bool Filesystem::setIdentity(const char *ident, bool appendToPath) if (!PHYSFS_isInit()) return false; - std::string old_save_path = save_path_full; + if (ident == nullptr || strlen(ident) == 0) + return false; - // Store the save directory. - save_identity = std::string(ident); + // Validate whether re-mounting will work. + for (CommonPath p : appCommonPaths) + { + if (!commonPathMountInfo[p].mounted) + continue; - // Generate the relative path to the game save folder. - save_path_relative = std::string(LOVE_APPDATA_PREFIX LOVE_APPDATA_FOLDER LOVE_PATH_SEPARATOR) + save_identity; + // If a file is still open, unmount will fail. + std::string fullPath = getFullCommonPath(p); + if (!fullPath.empty() && !PHYSFS_canUnmount(fullPath.c_str())) + return false; + } - // Generate the full path to the game save folder. - save_path_full = std::string(getAppdataDirectory()) + std::string(LOVE_PATH_SEPARATOR); - if (fused) - save_path_full += std::string(LOVE_APPDATA_PREFIX) + save_identity; - else - save_path_full += save_path_relative; + bool oldMountedCommonPaths[COMMONPATH_MAX_ENUM] = {false}; - save_path_full = normalize(save_path_full); - -#ifdef LOVE_ANDROID - if (save_identity == "") - save_identity = "unnamed"; + // We don't want old save paths to accumulate when we set a new identity. + for (CommonPath p : appCommonPaths) + { + oldMountedCommonPaths[p] = commonPathMountInfo[p].mounted; + if (commonPathMountInfo[p].mounted) + unmount(p); + } - std::string storage_path; - if (isAndroidSaveExternal()) - storage_path = SDL_AndroidGetExternalStoragePath(); - else - storage_path = SDL_AndroidGetInternalStoragePath(); + // These will be re-populated by getFullCommonPath. + for (CommonPath p : appCommonPaths) + fullPaths[p].clear(); - std::string save_directory = storage_path + "/save"; + // Store the save directory. getFullCommonPath(COMMONPATH_APP_*) uses this. + saveIdentity = std::string(ident); + appendIdentityToPath = appendToPath; - save_path_full = storage_path + std::string("/save/") + save_identity; + // Try to mount as readwrite without creating missing directories in the + // path hierarchy. If this fails, setupWriteDirectory will attempt to create + // them and try again. + // This is done so the save directory is only created on-demand. + if (!mountCommonPathInternal(COMMONPATH_APP_SAVEDIR, nullptr, MOUNT_PERMISSIONS_READWRITE, appendToPath, false)) + saveDirectoryNeedsMounting = true; - if (!love::android::directoryExists(save_path_full.c_str()) && - !love::android::mkdir(save_path_full.c_str())) - SDL_Log("Error: Could not create save directory %s!", save_path_full.c_str()); -#endif - - // We now have something like: - // save_identity: game - // save_path_relative: ./LOVE/game - // save_path_full: C:\Documents and Settings\user\Application Data/LOVE/game - - // We don't want old read-only save paths to accumulate when we set a new - // identity. - if (!old_save_path.empty()) - PHYSFS_unmount(old_save_path.c_str()); - - // Try to add the save directory to the search path. - // (No error on fail, it means that the path doesn't exist). - PHYSFS_mount(save_path_full.c_str(), nullptr, appendToPath); - - // HACK: This forces setupWriteDirectory to be called the next time a file - // is opened for writing - otherwise it won't be called at all if it was - // already called at least once before. - PHYSFS_setWriteDir(nullptr); + // Mount any other app common paths with directory creation immediately + // instead of on-demand, since to get to this point they would have to be + // explicitly mounted already beforehand. + for (CommonPath p : appCommonPaths) + { + if (oldMountedCommonPaths[p] && p != COMMONPATH_APP_SAVEDIR) + { + // TODO: error handling? + auto info = commonPathMountInfo[p]; + mountCommonPathInternal(p, info.mountPoint.c_str(), info.permissions, appendToPath, true); + } + } return true; } const char *Filesystem::getIdentity() const { - return save_identity.c_str(); + return saveIdentity.c_str(); } bool Filesystem::setSource(const char *source) @@ -222,7 +223,7 @@ bool Filesystem::setSource(const char *source) return false; // Check whether directory is already set. - if (!game_source.empty()) + if (!gameSource.empty()) return false; std::string new_search_path = source; @@ -264,14 +265,14 @@ bool Filesystem::setSource(const char *source) #endif // Save the game source. - game_source = new_search_path; + gameSource = new_search_path; return true; } const char *Filesystem::getSource() const { - return game_source.c_str(); + return gameSource.c_str(); } bool Filesystem::setupWriteDirectory() @@ -279,54 +280,19 @@ bool Filesystem::setupWriteDirectory() if (!PHYSFS_isInit()) return false; - // These must all be set. - if (save_identity.empty() || save_path_full.empty() || save_path_relative.empty()) + if (!saveDirectoryNeedsMounting) + return true; + + if (saveIdentity.empty()) return false; - // We need to make sure the write directory is created. To do that, we also - // need to make sure all its parent directories are also created. - std::string temp_writedir = getDriveRoot(save_path_full); - std::string temp_createdir = skipDriveRoot(save_path_full); - - // On some sandboxed platforms, physfs will break when its write directory - // is the root of the drive and it tries to create a folder (even if the - // folder's path is in a writable location.) If the user's home folder is - // in the save path, we'll try starting from there instead. - if (save_path_full.find(getUserDirectory()) == 0) - { - temp_writedir = getUserDirectory(); - temp_createdir = save_path_full.substr(getUserDirectory().length()); - - // Strip leading '/' characters from the path we want to create. - size_t startpos = temp_createdir.find_first_not_of('/'); - if (startpos != std::string::npos) - temp_createdir = temp_createdir.substr(startpos); - } - - // Set either '/' or the user's home as a writable directory. - // (We must create the save folder before mounting it). - if (!PHYSFS_setWriteDir(temp_writedir.c_str())) + // Only the save directory is mounted on-demand if it doesn't exist yet. + // Other app common paths are immediately re-mounted in setIdentity. + bool createdir = true; + if (!mountCommonPathInternal(COMMONPATH_APP_SAVEDIR, nullptr, MOUNT_PERMISSIONS_READWRITE, appendIdentityToPath, createdir)) return false; - // Create the save folder. (We're now "at" either '/' or the user's home). - if (!createDirectory(temp_createdir.c_str())) - { - // Clear the write directory in case of error. - PHYSFS_setWriteDir(nullptr); - return false; - } - - // Set the final write directory. - if (!PHYSFS_setWriteDir(save_path_full.c_str())) - return false; - - // Add the directory. (Will not be readded if already present). - if (!PHYSFS_mount(save_path_full.c_str(), nullptr, 0)) - { - PHYSFS_setWriteDir(nullptr); // Clear the write directory in case of error. - return false; - } - + saveDirectoryNeedsMounting = false; return true; } @@ -363,17 +329,52 @@ bool Filesystem::mount(const char *archive, const char *mountpoint, bool appendT // Always disallow mounting of files inside the game source, since it // won't work anyway if the game source is a zipped .love file. - if (realPath.find(game_source) == 0) + if (realPath.find(gameSource) == 0) return false; realPath += LOVE_PATH_SEPARATOR; realPath += archive; } - if (realPath.length() == 0) + return mountFullPath(realPath.c_str(), mountpoint, MOUNT_PERMISSIONS_READ, appendToPath); +} + +bool Filesystem::mountFullPath(const char *archive, const char *mountpoint, MountPermissions permissions, bool appendToPath) +{ + if (!PHYSFS_isInit() || !archive) return false; - return PHYSFS_mount(realPath.c_str(), mountpoint, appendToPath) != 0; + if (permissions == MOUNT_PERMISSIONS_READWRITE) + return PHYSFS_mountRW(archive, mountpoint, appendToPath) != 0; + + return PHYSFS_mount(archive, mountpoint, appendToPath) != 0; +} + +bool Filesystem::mountCommonPathInternal(CommonPath path, const char *mountpoint, MountPermissions permissions, bool appendToPath, bool createDir) +{ + std::string fullpath = getFullCommonPath(path); + if (fullpath.empty()) + return false; + + if (createDir && isAppCommonPath(path) && !isRealDirectory(fullpath)) + { + if (!createRealDirectory(fullpath)) + return false; + } + + if (mountFullPath(fullpath.c_str(), mountpoint, permissions, appendToPath)) + { + std::string mp = mountpoint != nullptr ? mountpoint : "/"; + commonPathMountInfo[path] = {true, mp, permissions}; + return true; + } + + return false; +} + +bool Filesystem::mountCommonPath(CommonPath path, const char *mountpoint, MountPermissions permissions, bool appendToPath) +{ + return mountCommonPathInternal(path, mountpoint, permissions, appendToPath, true); } bool Filesystem::mount(Data *data, const char *archivename, const char *mountpoint, bool appendToPath) @@ -403,42 +404,52 @@ bool Filesystem::unmount(const char *archive) return true; } - std::string realPath; - std::string sourceBase = getSourceBaseDirectory(); - - // Check whether the given archive path is in the list of allowed full paths. auto it = std::find(allowedMountPaths.begin(), allowedMountPaths.end(), archive); - if (it != allowedMountPaths.end()) - realPath = *it; - else if (isFused() && sourceBase.compare(archive) == 0) - { - // Special case: if the game is fused and the archive is the source's - // base directory, unmount it even though it's outside of the save dir. - realPath = sourceBase; - } - else - { - // Not allowed for safety reasons. - if (strlen(archive) == 0 || strstr(archive, "..") || strcmp(archive, "/") == 0) - return false; + return unmountFullPath(archive); - const char *realDir = PHYSFS_getRealDir(archive); - if (!realDir) - return false; + std::string sourceBase = getSourceBaseDirectory(); + if (isFused() && sourceBase.compare(archive) == 0) + return unmountFullPath(archive); - realPath = realDir; - realPath += LOVE_PATH_SEPARATOR; - realPath += archive; - } + if (strlen(archive) == 0 || strstr(archive, "..") || strcmp(archive, "/") == 0) + return false; - const char *mountPoint = PHYSFS_getMountPoint(realPath.c_str()); - if (!mountPoint) + const char *realDir = PHYSFS_getRealDir(archive); + if (!realDir) + return false; + + std::string realPath = realDir; + realPath += LOVE_PATH_SEPARATOR; + realPath += archive; + + if (PHYSFS_getMountPoint(realPath.c_str()) == nullptr) return false; return PHYSFS_unmount(realPath.c_str()) != 0; } +bool Filesystem::unmountFullPath(const char *fullpath) +{ + if (!PHYSFS_isInit() || !fullpath) + return false; + + return PHYSFS_unmount(fullpath) != 0; +} + +bool Filesystem::unmount(CommonPath path) +{ + std::string fullpath = getFullCommonPath(path); + + if (!fullpath.empty() && unmountFullPath(fullpath.c_str())) + { + commonPathMountInfo[path].mounted = false; + return true; + } + + return false; +} + bool Filesystem::unmount(Data *data) { for (const auto &datapair : mountedData) @@ -458,6 +469,166 @@ love::filesystem::File *Filesystem::newFile(const char *filename) const return new File(filename); } +std::string Filesystem::getFullCommonPath(CommonPath path) +{ + if (!fullPaths[path].empty()) + return fullPaths[path]; + + if (isAppCommonPath(path)) + { + if (saveIdentity.empty()) + return fullPaths[path]; + + std::string rootpath; + switch (path) + { + case COMMONPATH_APP_SAVEDIR: + rootpath = getFullCommonPath(COMMONPATH_USER_APPDATA); + break; + case COMMONPATH_APP_DOCUMENTS: + rootpath = getFullCommonPath(COMMONPATH_USER_DOCUMENTS); + break; + default: + break; + } + + if (rootpath.empty()) + return fullPaths[path]; + + std::string suffix; + if (isFused()) + suffix = std::string(LOVE_PATH_SEPARATOR) + saveIdentity; + else + suffix = std::string(LOVE_PATH_SEPARATOR LOVE_APPDATA_FOLDER LOVE_PATH_SEPARATOR) + saveIdentity; + + fullPaths[path] = normalize(rootpath + suffix); + + return fullPaths[path]; + } + +#if defined(LOVE_MACOS) || defined(LOVE_IOS) + + switch (path) + { + case COMMONPATH_APP_SAVEDIR: + case COMMONPATH_APP_DOCUMENTS: + // Handled above. + break; + case COMMONPATH_USER_HOME: + fullPaths[path] = apple::getUserDirectory(apple::USER_DIRECTORY_HOME); + break; + case COMMONPATH_USER_APPDATA: + fullPaths[path] = apple::getUserDirectory(apple::USER_DIRECTORY_APPSUPPORT); + break; + case COMMONPATH_USER_DESKTOP: + fullPaths[path] = apple::getUserDirectory(apple::USER_DIRECTORY_DESKTOP); + break; + case COMMONPATH_USER_DOCUMENTS: + fullPaths[path] = apple::getUserDirectory(apple::USER_DIRECTORY_DOCUMENTS); + break; + case COMMONPATH_MAX_ENUM: + break; + } + +#elif defined(LOVE_WINDOWS) + + PWSTR winpath = nullptr; + HRESULT hr = E_FAIL; + + switch (path) + { + case COMMONPATH_APP_SAVEDIR: + case COMMONPATH_APP_DOCUMENTS: + // Handled above. + break; + case COMMONPATH_USER_HOME: + hr = SHGetKnownFolderPath(FOLDERID_Profile, 0, nullptr, &winpath); + break; + case COMMONPATH_USER_APPDATA: + hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &winpath); + break; + case COMMONPATH_USER_DESKTOP: + hr = SHGetKnownFolderPath(FOLDERID_Desktop, 0, nullptr, &winpath); + break; + case COMMONPATH_USER_DOCUMENTS: + hr = SHGetKnownFolderPath(FOLDERID_Documents, 0, nullptr, &winpath); + break; + case COMMONPATH_MAX_ENUM: + break; + } + + if (SUCCEEDED(hr)) + { + fullPaths[path] = to_utf8(winpath); + CoTaskMemFree(winpath); + } + +#elif defined(LOVE_ANDROID) + + std::string storagepath; + if (isAndroidSaveExternal()) + storagepath = SDL_AndroidGetExternalStoragePath(); + else + storagepath = SDL_AndroidGetInternalStoragePath(); + + switch (path) + { + case COMMONPATH_APP_SAVEDIR: + case COMMONPATH_APP_DOCUMENTS: + // Handled above. + break; + case COMMONPATH_USER_HOME: + fullPaths[path] = normalize(PHYSFS_getUserDir()); + break; + case COMMONPATH_USER_APPDATA: + fullPaths[path] = normalize(storagepath + "/save/"); + break; + case COMMONPATH_USER_DESKTOP: + // No such thing on Android? + break; + case COMMONPATH_USER_DOCUMENTS: + // TODO: something more idiomatic / useful? + fullPaths[path] = normalize(storagepath + "/Documents/"); + break; + case COMMONPATH_MAX_ENUM: + break; + } + +#elif defined(LOVE_LINUX) + + const char *xdgdir = nullptr; + + switch (path) + { + case COMMONPATH_APP_SAVEDIR: + case COMMONPATH_APP_DOCUMENTS: + // Handled above. + break; + case COMMONPATH_USER_HOME: + fullPaths[path] = normalize(PHYSFS_getUserDir()); + break; + case COMMONPATH_USER_APPDATA: + xdgdir = getenv("XDG_DATA_HOME"); + if (!xdgdir) + fullPaths[path] = normalize(std::string(getUserDirectory()) + "/.local/share/"); + else + fullPaths[path] = xdgdir; + break; + case COMMONPATH_USER_DESKTOP: + fullPaths[path] = normalize(std::string(getUserDirectory()) + "/Desktop/"); + break; + case COMMONPATH_USER_DOCUMENTS: + fullPaths[path] = normalize(std::string(getUserDirectory()) + "/Documents/"); + break; + case COMMONPATH_MAX_ENUM: + break; + } + +#endif + + return fullPaths[path]; +} + const char *Filesystem::getWorkingDirectory() { if (cwd.empty()) @@ -474,7 +645,7 @@ const char *Filesystem::getWorkingDirectory() if (getcwd(cwd_char, LOVE_MAX_PATH)) cwd = cwd_char; // if getcwd fails, cwd_char (and thus cwd) will still be empty - delete [] cwd_char; + delete[] cwd_char; #endif } @@ -483,61 +654,22 @@ const char *Filesystem::getWorkingDirectory() std::string Filesystem::getUserDirectory() { -#ifdef LOVE_IOS - // PHYSFS_getUserDir doesn't give exactly the path we want on iOS. - static std::string userDir = normalize(love::ios::getHomeDirectory()); -#else - static std::string userDir = normalize(PHYSFS_getUserDir()); -#endif - - return userDir; + return getFullCommonPath(COMMONPATH_USER_HOME); } std::string Filesystem::getAppdataDirectory() { - if (appdata.empty()) - { -#ifdef LOVE_WINDOWS_UWP - appdata = getUserDirectory(); -#elif defined(LOVE_WINDOWS) - PWSTR path = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &path))) - { - appdata = to_utf8(path); - CoTaskMemFree(path); - } - else - { - wchar_t *w_appdata = _wgetenv(L"APPDATA"); - appdata = to_utf8(w_appdata); - } - replace_char(appdata, '\\', '/'); -#elif defined(LOVE_MACOS) - appdata = normalize(love::macos::getAppdataDirectory()); -#elif defined(LOVE_IOS) - appdata = normalize(love::ios::getAppdataDirectory()); -#elif defined(LOVE_LINUX) - char *xdgdatahome = getenv("XDG_DATA_HOME"); - if (!xdgdatahome) - appdata = normalize(std::string(getUserDirectory()) + "/.local/share/"); - else - appdata = xdgdatahome; -#else - appdata = getUserDirectory(); -#endif - } - return appdata; + return getFullCommonPath(COMMONPATH_USER_APPDATA); } - -const char *Filesystem::getSaveDirectory() +std::string Filesystem::getSaveDirectory() { - return save_path_full.c_str(); + return getFullCommonPath(COMMONPATH_APP_SAVEDIR); } std::string Filesystem::getSourceBaseDirectory() const { - size_t source_len = game_source.length(); + size_t source_len = gameSource.length(); if (source_len == 0) return ""; @@ -546,9 +678,9 @@ std::string Filesystem::getSourceBaseDirectory() const // symbols (i.e. '..' and '.') #ifdef LOVE_WINDOWS // In windows, delimiters can be either '/' or '\'. - size_t base_end_pos = game_source.find_last_of("/\\", source_len - 2); + size_t base_end_pos = gameSource.find_last_of("/\\", source_len - 2); #else - size_t base_end_pos = game_source.find_last_of('/', source_len - 2); + size_t base_end_pos = gameSource.find_last_of('/', source_len - 2); #endif if (base_end_pos == std::string::npos) @@ -558,7 +690,7 @@ std::string Filesystem::getSourceBaseDirectory() const if (base_end_pos == 0) base_end_pos = 1; - return game_source.substr(0, base_end_pos); + return gameSource.substr(0, base_end_pos); } std::string Filesystem::getRealDirectory(const char *filename) const @@ -585,6 +717,7 @@ bool Filesystem::getInfo(const char *filepath, Info &info) const info.size = (int64) stat.filesize; info.modtime = (int64) stat.modtime; + info.readonly = stat.readonly != 0; if (stat.filetype == PHYSFS_FILETYPE_REGULAR) info.type = FILETYPE_FILE; @@ -603,7 +736,7 @@ bool Filesystem::createDirectory(const char *dir) if (!PHYSFS_isInit()) return false; - if (PHYSFS_getWriteDir() == 0 && !setupWriteDirectory()) + if (!setupWriteDirectory()) return false; if (!PHYSFS_mkdir(dir)) @@ -617,7 +750,7 @@ bool Filesystem::remove(const char *file) if (!PHYSFS_isInit()) return false; - if (PHYSFS_getWriteDir() == 0 && !setupWriteDirectory()) + if (!setupWriteDirectory()) return false; if (!PHYSFS_delete(file)) diff --git a/src/modules/filesystem/physfs/Filesystem.h b/src/modules/filesystem/physfs/Filesystem.h index b66417f02..1ad915981 100644 --- a/src/modules/filesystem/physfs/Filesystem.h +++ b/src/modules/filesystem/physfs/Filesystem.h @@ -63,15 +63,21 @@ public: bool mount(const char *archive, const char *mountpoint, bool appendToPath = false) override; bool mount(Data *data, const char *archivename, const char *mountpoint, bool appendToPath = false) override; + bool mountFullPath(const char *archive, const char *mountpoint, MountPermissions permissions, bool appendToPath = false) override; + bool mountCommonPath(CommonPath path, const char *mountpoint, MountPermissions permissions, bool appendToPath = false) override; + bool unmount(const char *archive) override; bool unmount(Data *data) override; + bool unmount(CommonPath path) override; + bool unmountFullPath(const char *fullpath) override; love::filesystem::File *newFile(const char *filename) const override; + std::string getFullCommonPath(CommonPath path) override; const char *getWorkingDirectory() override; std::string getUserDirectory() override; std::string getAppdataDirectory() override; - const char *getSaveDirectory() override; + std::string getSaveDirectory() override; std::string getSourceBaseDirectory() const override; std::string getRealDirectory(const char *filename) const override; @@ -98,26 +104,26 @@ public: private: + struct CommonPathMountInfo + { + bool mounted; + std::string mountPoint; + MountPermissions permissions; + }; + + bool mountCommonPathInternal(CommonPath path, const char *mountpoint, MountPermissions permissions, bool appendToPath, bool createDir); + // Contains the current working directory (UTF8). std::string cwd; - // %APPDATA% on Windows. - std::string appdata; - - // This name will be used to create the folder - // in the appdata/userdata folder. - std::string save_identity; - - // Full and relative paths of the game save folder. - // (Relative to the %APPDATA% folder, meaning that the - // relative string will look something like: ./LOVE/game) - std::string save_path_relative, save_path_full; + // This name will be used to create the folder in the appdata folder. + std::string saveIdentity; + bool appendIdentityToPath; // The full path to the source of the game. - std::string game_source; + std::string gameSource; - // Allow saving outside of the LOVE_APPDATA_FOLDER - // for release 'builds' + // Allow saving outside of the LOVE_APPDATA_FOLDER for release 'builds' bool fused; bool fusedSet; @@ -129,6 +135,12 @@ private: std::map> mountedData; + std::string fullPaths[COMMONPATH_MAX_ENUM]; + + CommonPathMountInfo commonPathMountInfo[COMMONPATH_MAX_ENUM]; + + bool saveDirectoryNeedsMounting; + }; // Filesystem } // physfs diff --git a/src/modules/filesystem/wrap_Filesystem.cpp b/src/modules/filesystem/wrap_Filesystem.cpp index 4ccb04670..45bf3e889 100644 --- a/src/modules/filesystem/wrap_Filesystem.cpp +++ b/src/modules/filesystem/wrap_Filesystem.cpp @@ -148,6 +148,48 @@ int w_mount(lua_State *L) return 1; } +int w_mountFullPath(lua_State *L) +{ + const char *fullpath = luaL_checkstring(L, 1); + const char *mountpoint = luaL_checkstring(L, 2); + + auto permissions = Filesystem::MOUNT_PERMISSIONS_READ; + if (!lua_isnoneornil(L, 3)) + { + const char *permissionstr = luaL_checkstring(L, 3); + if (!Filesystem::getConstant(permissionstr, permissions)) + return luax_enumerror(L, "mount permissions", Filesystem::getConstants(permissions), permissionstr); + } + + bool append = luax_optboolean(L, 4, false); + + luax_pushboolean(L, instance()->mountFullPath(fullpath, mountpoint, permissions, append)); + return 1; +} + +int w_mountCommonPath(lua_State *L) +{ + const char *commonpathstr = luaL_checkstring(L, 1); + Filesystem::CommonPath commonpath; + if (!Filesystem::getConstant(commonpathstr, commonpath)) + return luax_enumerror(L, "common path", Filesystem::getConstants(commonpath), commonpathstr); + + const char *mountpoint = luaL_checkstring(L, 2); + + auto permissions = Filesystem::MOUNT_PERMISSIONS_READ; + if (!lua_isnoneornil(L, 3)) + { + const char *permissionstr = luaL_checkstring(L, 3); + if (!Filesystem::getConstant(permissionstr, permissions)) + return luax_enumerror(L, "mount permissions", Filesystem::getConstants(permissions), permissionstr); + } + + bool append = luax_optboolean(L, 4, false); + + luax_pushboolean(L, instance()->mountCommonPath(commonpath, mountpoint, permissions, append)); + return 1; +} + int w_unmount(lua_State *L) { if (luax_istype(L, 1, Data::type)) @@ -163,6 +205,24 @@ int w_unmount(lua_State *L) return 1; } +int w_unmountFullPath(lua_State *L) +{ + const char *fullpath = luaL_checkstring(L, 1); + luax_pushboolean(L, instance()->unmountFullPath(fullpath)); + return 1; +} + +int w_unmountCommonPath(lua_State *L) +{ + const char *commonpathstr = luaL_checkstring(L, 1); + Filesystem::CommonPath commonpath; + if (!Filesystem::getConstant(commonpathstr, commonpath)) + return luax_enumerror(L, "common path", Filesystem::getConstants(commonpath), commonpathstr); + + luax_pushboolean(L, instance()->unmount(commonpath)); + return 1; +} + int w_newFile(lua_State *L) { const char *filename = luaL_checkstring(L, 1); @@ -331,6 +391,17 @@ int w_newFileData(lua_State *L) return 1; } +int w_getFullCommonPath(lua_State *L) +{ + const char *commonpathstr = luaL_checkstring(L, 1); + Filesystem::CommonPath commonpath; + if (!Filesystem::getConstant(commonpathstr, commonpath)) + return luax_enumerror(L, "common path", Filesystem::getConstants(commonpath), commonpathstr); + + luax_pushstring(L, instance()->getFullCommonPath(commonpath)); + return 1; +} + int w_getWorkingDirectory(lua_State *L) { lua_pushstring(L, instance()->getWorkingDirectory()); @@ -351,7 +422,7 @@ int w_getAppdataDirectory(lua_State *L) int w_getSaveDirectory(lua_State *L) { - lua_pushstring(L, instance()->getSaveDirectory()); + luax_pushstring(L, instance()->getSaveDirectory()); return 1; } @@ -421,6 +492,9 @@ int w_getInfo(lua_State *L) lua_pushstring(L, typestr); lua_setfield(L, -2, "type"); + luax_pushboolean(L, info.readonly); + lua_setfield(L, -2, "readonly"); + // Lua numbers (doubles) can't fit the full range of 64 bit ints. info.size = std::min(info.size, 0x20000000000000LL); if (info.size >= 0) @@ -839,8 +913,13 @@ static const luaL_Reg functions[] = { "setSource", w_setSource }, { "getSource", w_getSource }, { "mount", w_mount }, + { "mountFullPath", w_mountFullPath }, + { "mountCommonPath", w_mountCommonPath }, { "unmount", w_unmount }, + { "unmountFullPath", w_unmountFullPath }, + { "unmountCommonPath", w_unmountCommonPath }, { "newFile", w_newFile }, + { "getFullCommonPath", w_getFullCommonPath }, { "getWorkingDirectory", w_getWorkingDirectory }, { "getUserDirectory", w_getUserDirectory }, { "getAppdataDirectory", w_getAppdataDirectory }, diff --git a/src/modules/graphics/Buffer.cpp b/src/modules/graphics/Buffer.cpp index c9c3f8a5f..aaf9b2228 100644 --- a/src/modules/graphics/Buffer.cpp +++ b/src/modules/graphics/Buffer.cpp @@ -20,6 +20,7 @@ #include "Buffer.h" #include "Graphics.h" +#include "common/memory.h" namespace love { @@ -48,15 +49,20 @@ Buffer::Buffer(Graphics *gfx, const Settings &settings, const std::vector 0) + throw love::Exception("Arrays are not currently supported in shader storage buffers."); + + if (info.baseType == DATA_BASETYPE_BOOL) + throw love::Exception("Bool types are not supported in shader storage buffers."); + + if (info.baseType == DATA_BASETYPE_UNORM || info.baseType == DATA_BASETYPE_SNORM) + throw love::Exception("Normalized formats are not supported in shader storage buffers."); + + size_t alignment = 1; + + // GLSL's std430 packing rules. We also assume all matrices are + // column-major. + if (info.isMatrix) + alignment = info.matrixRows * info.componentSize; + else + alignment = info.components * info.componentSize; + + structurealignment = std::max(structurealignment, alignment); + + memberoffset = alignUp(memberoffset, alignment); + + if (memberoffset != offset && (indexbuffer || vertexbuffer || texelbuffer)) + throw love::Exception("Cannot create Buffer:\nInternal alignment of member '%s' is preventing Buffer from being created as both a shader storage buffer and other buffer types\nMember byte offset needed for shader storage buffer: %d\nMember byte offset needed for other buffer types: %d", + member.decl.name.c_str(), memberoffset, offset); + } + + member.offset = memberoffset; + member.size = membersize; + + offset = member.offset + member.size; dataMembers.push_back(member); } - stride = offset; + stride = alignUp(offset, structurealignment); + + if (storagebuffer && (indexbuffer || vertexbuffer || texelbuffer)) + { + if (stride != offset) + throw love::Exception("Cannot create Buffer:\nBuffer used as a shader storage buffer would have a different number of bytes per array element (%d) than when used as other buffer types (%d)", + stride, offset); + } + + if (storagebuffer && stride > SHADER_STORAGE_BUFFER_MAX_STRIDE) + throw love::Exception("Shader storage buffers cannot have more than %d bytes within each array element.", SHADER_STORAGE_BUFFER_MAX_STRIDE); if (size != 0) { @@ -144,7 +194,8 @@ Buffer::Buffer(Graphics *gfx, const Settings &settings, const std::vectorsize = size; if (texelbuffer && arraylength * dataMembers.size() > caps.limits[Graphics::LIMIT_TEXEL_BUFFER_SIZE]) - throw love::Exception("Cannot create texel buffer: total number of values in the buffer (%d * %d) is too large for this system (maximum %d).", (int) dataMembers.size(), (int) arraylength, caps.limits[Graphics::LIMIT_TEXEL_BUFFER_SIZE]); + throw love::Exception("Cannot create texel buffer: total number of values in the buffer (%d * %d) is too large for this system (maximum %d).", + (int) dataMembers.size(), (int) arraylength, caps.limits[Graphics::LIMIT_TEXEL_BUFFER_SIZE]); } Buffer::~Buffer() diff --git a/src/modules/graphics/Buffer.h b/src/modules/graphics/Buffer.h index 89280ab4f..428b5ce51 100644 --- a/src/modules/graphics/Buffer.h +++ b/src/modules/graphics/Buffer.h @@ -48,6 +48,8 @@ public: static love::Type type; + static const size_t SHADER_STORAGE_BUFFER_MAX_STRIDE = 2048; + enum MapType { MAP_WRITE_INVALIDATE, @@ -60,6 +62,7 @@ public: TYPEFLAG_INDEX = 1 << BUFFERTYPE_INDEX, TYPEFLAG_UNIFORM = 1 << BUFFERTYPE_UNIFORM, TYPEFLAG_TEXEL = 1 << BUFFERTYPE_TEXEL, + TYPEFLAG_SHADER_STORAGE = 1 << BUFFERTYPE_SHADER_STORAGE, }; struct DataDeclaration diff --git a/src/modules/graphics/Graphics.cpp b/src/modules/graphics/Graphics.cpp index 83b2b13ab..2d605c677 100644 --- a/src/modules/graphics/Graphics.cpp +++ b/src/modules/graphics/Graphics.cpp @@ -298,7 +298,7 @@ Shader *Graphics::newShader(const std::vector &stagessource) if (!validstages[i]) continue; - if (info.isStage[i]) + if (info.stages[i] != Shader::ENTRYPOINT_NONE) { isanystage = true; stages[i].set(newShaderStage((ShaderStage::StageType) i, source, info), Acquire::NORETAIN); @@ -377,7 +377,7 @@ bool Graphics::validateShader(bool gles, const std::vector &stagess if (!validstages[i]) continue; - if (info.isStage[i]) + if (info.stages[i] != Shader::ENTRYPOINT_NONE) { isanystage = true; std::string glsl = Shader::createShaderStageCode(this, stype, source, info); @@ -1987,15 +1987,16 @@ StringMap Graphics::features(Grap StringMap::Entry Graphics::systemLimitEntries[] = { - { "pointsize", LIMIT_POINT_SIZE }, - { "texturesize", LIMIT_TEXTURE_SIZE }, - { "texturelayers", LIMIT_TEXTURE_LAYERS }, - { "volumetexturesize", LIMIT_VOLUME_TEXTURE_SIZE }, - { "cubetexturesize", LIMIT_CUBE_TEXTURE_SIZE }, - { "texelbuffersize", LIMIT_TEXEL_BUFFER_SIZE }, - { "rendertargets", LIMIT_RENDER_TARGETS }, - { "texturemsaa", LIMIT_TEXTURE_MSAA }, - { "anisotropy", LIMIT_ANISOTROPY }, + { "pointsize", LIMIT_POINT_SIZE }, + { "texturesize", LIMIT_TEXTURE_SIZE }, + { "texturelayers", LIMIT_TEXTURE_LAYERS }, + { "volumetexturesize", LIMIT_VOLUME_TEXTURE_SIZE }, + { "cubetexturesize", LIMIT_CUBE_TEXTURE_SIZE }, + { "texelbuffersize", LIMIT_TEXEL_BUFFER_SIZE }, + { "shaderstoragebuffersize", LIMIT_SHADER_STORAGE_BUFFER_SIZE }, + { "rendertargets", LIMIT_RENDER_TARGETS }, + { "texturemsaa", LIMIT_TEXTURE_MSAA }, + { "anisotropy", LIMIT_ANISOTROPY }, }; StringMap Graphics::systemLimits(Graphics::systemLimitEntries, sizeof(Graphics::systemLimitEntries)); diff --git a/src/modules/graphics/Graphics.h b/src/modules/graphics/Graphics.h index 15e52d5b7..837c37820 100644 --- a/src/modules/graphics/Graphics.h +++ b/src/modules/graphics/Graphics.h @@ -163,6 +163,7 @@ public: LIMIT_CUBE_TEXTURE_SIZE, LIMIT_TEXTURE_LAYERS, LIMIT_TEXEL_BUFFER_SIZE, + LIMIT_SHADER_STORAGE_BUFFER_SIZE, LIMIT_RENDER_TARGETS, LIMIT_TEXTURE_MSAA, LIMIT_ANISOTROPY, diff --git a/src/modules/graphics/Shader.cpp b/src/modules/graphics/Shader.cpp index 54975381c..904fbb841 100644 --- a/src/modules/graphics/Shader.cpp +++ b/src/modules/graphics/Shader.cpp @@ -26,6 +26,9 @@ // glslang #include "libraries/glslang/glslang/Public/ShaderLang.h" +// Needed for reflection information. +#include "libraries/glslang/glslang/Include/Types.h" + // C++ #include #include @@ -38,6 +41,7 @@ namespace graphics namespace glsl { + static const char global_syntax[] = R"( #if !defined(GL_ES) && __VERSION__ < 140 #define lowp @@ -65,7 +69,11 @@ static const char global_syntax[] = R"( #define DepthCubeImage samplerCubeShadow #endif #define extern uniform -#ifdef GL_EXT_texture_array +#if defined(GL_EXT_texture_array) && (!defined(GL_ES) || __VERSION__ > 100 || defined(GL_OES_gpu_shader5)) +// Only used when !GLSLES1 to work around Ouya driver bug. But we still want it +// enabled for glslang validation when glsl 1-on-3 is used, so also enable it if +// OES_gpu_shader5 exists. +#define LOVE_EXT_TEXTURE_ARRAY_ENABLED #extension GL_EXT_texture_array : enable #endif #ifdef GL_OES_texture_3D @@ -133,7 +141,7 @@ void love_initializeBuiltinUniforms() { static const char global_functions[] = R"( #ifdef GL_ES - #if __VERSION__ >= 300 || defined(GL_EXT_texture_array) + #if __VERSION__ >= 300 || defined(LOVE_EXT_TEXTURE_ARRAY_ENABLED) precision lowp sampler2DArray; #endif #if __VERSION__ >= 300 || defined(GL_OES_texture_3D) @@ -169,7 +177,7 @@ static const char global_functions[] = R"( #if __VERSION__ > 100 || defined(GL_OES_texture_3D) vec4 Texel(sampler3D s, vec3 c) { return love_texture3D(s, c); } #endif - #if __VERSION__ >= 130 || defined(GL_EXT_texture_array) + #if __VERSION__ >= 130 || defined(LOVE_EXT_TEXTURE_ARRAY_ENABLED) vec4 Texel(sampler2DArray s, vec3 c) { return love_texture2DArray(s, c); } #endif #ifdef PIXEL @@ -178,7 +186,7 @@ static const char global_functions[] = R"( #if __VERSION__ > 100 || defined(GL_OES_texture_3D) vec4 Texel(sampler3D s, vec3 c, float b) { return love_texture3D(s, c, b); } #endif - #if __VERSION__ >= 130 || defined(GL_EXT_texture_array) + #if __VERSION__ >= 130 || defined(LOVE_EXT_TEXTURE_ARRAY_ENABLED) vec4 Texel(sampler2DArray s, vec3 c, float b) { return love_texture2DArray(s, c, b); } #endif #endif @@ -282,6 +290,16 @@ void main() { } )"; +static const char vertex_main_raw[] = R"( +void vertexmain(); + +void main() { + love_initializeBuiltinUniforms(); + setPointSize(); + vertexmain(); +} +)"; + static const char pixel_header[] = R"( #ifdef GL_ES precision mediump float; @@ -291,29 +309,10 @@ static const char pixel_header[] = R"( #if __VERSION__ >= 130 #define varying in - // Some drivers seem to make the pixel shader do more work when multiple - // pixel shader outputs are defined, even when only one is actually used. - // TODO: We should use reflection or something instead of this, to determine - // how many outputs are actually used in the shader code. - #ifdef LOVE_MULTI_RENDER_TARGETS - LOVE_IO_LOCATION(0) out vec4 love_RenderTargets[love_MaxRenderTargets]; - #define love_PixelColor love_RenderTargets[0] - #else - LOVE_IO_LOCATION(0) out vec4 love_PixelColor; - #endif -#else - #ifdef LOVE_MULTI_RENDER_TARGETS - #define love_RenderTargets gl_FragData - #endif - #define love_PixelColor gl_FragColor #endif // Legacy #define love_MaxCanvases love_MaxRenderTargets -#define love_Canvases love_RenderTargets -#ifdef LOVE_MULTI_RENDER_TARGETS -#define LOVE_MULTI_CANVASES 1 -#endif // See Shader::updateScreenParams in Shader.cpp. #define love_PixelCoord (vec2(gl_FragCoord.x, (gl_FragCoord.y * love_ScreenSize.z) + love_ScreenSize.w)) @@ -342,6 +341,12 @@ vec4 VideoTexel(vec2 texcoords) { )"; static const char pixel_main[] = R"( +#if __VERSION__ >= 130 + LOVE_IO_LOCATION(0) out vec4 love_PixelColor; +#else + #define love_PixelColor gl_FragColor +#endif + uniform sampler2D MainTex; varying LOVE_HIGHP_OR_MEDIUMP vec4 VaryingTexCoord; varying mediump vec4 VaryingColor; @@ -355,6 +360,30 @@ void main() { )"; static const char pixel_main_custom[] = R"( +#if __VERSION__ >= 130 + // Some drivers seem to make the pixel shader do more work when multiple + // pixel shader outputs are defined, even when only one is actually used. + // TODO: We should use reflection or something instead of this, to determine + // how many outputs are actually used in the shader code. + #ifdef LOVE_MULTI_RENDER_TARGETS + LOVE_IO_LOCATION(0) out vec4 love_RenderTargets[love_MaxRenderTargets]; + #define love_PixelColor love_RenderTargets[0] + #else + LOVE_IO_LOCATION(0) out vec4 love_PixelColor; + #endif +#else + #ifdef LOVE_MULTI_RENDER_TARGETS + #define love_RenderTargets gl_FragData + #endif + #define love_PixelColor gl_FragColor +#endif + +// Legacy +#define love_Canvases love_RenderTargets +#ifdef LOVE_MULTI_RENDER_TARGETS +#define LOVE_MULTI_CANVASES 1 +#endif + varying LOVE_HIGHP_OR_MEDIUMP vec4 VaryingTexCoord; varying mediump vec4 VaryingColor; @@ -366,6 +395,15 @@ void main() { } )"; +static const char pixel_main_raw[] = R"( +void pixelmain(); + +void main() { + love_initializeBuiltinUniforms(); + pixelmain(); +} +)"; + struct StageInfo { const char *name; @@ -373,12 +411,13 @@ struct StageInfo const char *functions; const char *main; const char *main_custom; + const char *main_raw; }; static const StageInfo stageInfo[] = { - { "VERTEX", vertex_header, vertex_functions, vertex_main, vertex_main }, - { "PIXEL", pixel_header, pixel_functions, pixel_main, pixel_main_custom }, + { "VERTEX", vertex_header, vertex_functions, vertex_main, vertex_main, vertex_main_raw }, + { "PIXEL", pixel_header, pixel_functions, pixel_main, pixel_main_custom, pixel_main_raw }, }; static_assert((sizeof(stageInfo) / sizeof(StageInfo)) == ShaderStage::STAGE_MAX_ENUM, "Stages array size must match ShaderStage enum."); @@ -407,30 +446,38 @@ static Shader::Language getTargetLanguage(const std::string &src) return lang; } -static bool isVertexCode(const std::string &src) +static Shader::EntryPoint getVertexEntryPoint(const std::string &src) { - std::regex r("vec4\\s+position\\s*\\("); std::smatch m; - return std::regex_search(src, m, r); + + if (std::regex_search(src, m, std::regex("void\\s+vertexmain\\s*\\("))) + return Shader::ENTRYPOINT_RAW; + + if (std::regex_search(src, m, std::regex("vec4\\s+position\\s*\\("))) + return Shader::ENTRYPOINT_HIGHLEVEL; + + return Shader::ENTRYPOINT_NONE; } -static bool isPixelCode(const std::string &src, bool &custompixel, bool &mrt) +static Shader::EntryPoint getPixelEntryPoint(const std::string &src, bool &mrt) { - custompixel = false; mrt = false; std::smatch m; + + if (std::regex_search(src, m, std::regex("void\\s+pixelmain\\s*\\("))) + return Shader::ENTRYPOINT_RAW; + if (std::regex_search(src, m, std::regex("vec4\\s+effect\\s*\\("))) - return true; + return Shader::ENTRYPOINT_HIGHLEVEL; if (std::regex_search(src, m, std::regex("void\\s+effect\\s*\\("))) { - custompixel = true; if (src.find("love_RenderTargets") != std::string::npos || src.find("love_Canvases") != std::string::npos) mrt = true; - return true; + return Shader::ENTRYPOINT_CUSTOM; } - return false; + return Shader::ENTRYPOINT_NONE; } } // glsl @@ -446,8 +493,8 @@ Shader::SourceInfo Shader::getSourceInfo(const std::string &src) { SourceInfo info = {}; info.language = glsl::getTargetLanguage(src); - info.isStage[ShaderStage::STAGE_VERTEX] = glsl::isVertexCode(src); - info.isStage[ShaderStage::STAGE_PIXEL] = glsl::isPixelCode(src, info.customPixelFunction, info.usesMRT); + info.stages[ShaderStage::STAGE_VERTEX] = glsl::getVertexEntryPoint(src); + info.stages[ShaderStage::STAGE_PIXEL] = glsl::getPixelEntryPoint(src, info.usesMRT); return info; } @@ -456,6 +503,12 @@ std::string Shader::createShaderStageCode(Graphics *gfx, ShaderStage::StageType if (info.language == Shader::LANGUAGE_MAX_ENUM) throw love::Exception("Invalid shader language"); + if (info.stages[stage] == ENTRYPOINT_NONE) + throw love::Exception("Cannot find entry point for shader stage."); + + if (info.stages[stage] == ENTRYPOINT_RAW && info.language == LANGUAGE_GLSL1) + throw love::Exception("Shaders using a raw entry point (vertexmain or pixelmain) must use GLSL 3 or greater."); + const auto &features = gfx->getCapabilities().features; if (info.language == LANGUAGE_GLSL3 && !features[Graphics::FEATURE_GLSL3]) @@ -490,7 +543,15 @@ std::string Shader::createShaderStageCode(Graphics *gfx, ShaderStage::StageType ss << glsl::global_uniforms; ss << glsl::global_functions; ss << stageinfo.functions; - ss << (info.customPixelFunction ? stageinfo.main_custom : stageinfo.main); + + if (info.stages[stage] == ENTRYPOINT_HIGHLEVEL) + ss << stageinfo.main; + else if (info.stages[stage] == ENTRYPOINT_CUSTOM) + ss << stageinfo.main_custom; + else if (info.stages[stage] == ENTRYPOINT_RAW) + ss << stageinfo.main_raw; + else + throw love::Exception("Unknown shader entry point %d", info.stages[stage]); ss << ((!gles && (lang == Shader::LANGUAGE_GLSL1 || glsl1on3)) ? "#line 0\n" : "#line 1\n"); ss << code; @@ -501,7 +562,7 @@ Shader::Shader(ShaderStage *vertex, ShaderStage *pixel) : stages() { std::string err; - if (!validate(vertex, pixel, err)) + if (!validateInternal(vertex, pixel, err, validationReflection)) throw love::Exception("%s", err.c_str()); stages[ShaderStage::STAGE_VERTEX] = vertex; @@ -584,7 +645,13 @@ void Shader::checkMainTexture(Texture *tex) const checkMainTextureType(tex->getTextureType(), tex->getSamplerState().depthSampleMode.hasValue); } -bool Shader::validate(ShaderStage *vertex, ShaderStage *pixel, std::string &err) +bool Shader::validate(ShaderStage* vertex, ShaderStage* pixel, std::string& err) +{ + ValidationReflection reflection; + return validateInternal(vertex, pixel, err, reflection); +} + +bool Shader::validateInternal(ShaderStage *vertex, ShaderStage *pixel, std::string &err, ValidationReflection &reflection) { glslang::TProgram program; @@ -600,6 +667,59 @@ bool Shader::validate(ShaderStage *vertex, ShaderStage *pixel, std::string &err) return false; } + if (!program.buildReflection(EShReflectionSeparateBuffers)) + { + err = "Cannot get reflection information for shader."; + return false; + } + + for (int i = 0; i < program.getNumBufferBlocks(); i++) + { + const glslang::TObjectReflection &info = program.getBufferBlock(i); + const glslang::TType *type = info.getType(); + if (type != nullptr) + { + const glslang::TQualifier &qualifiers = type->getQualifier(); + + if ((!qualifiers.isReadOnly() || qualifiers.isWriteOnly()) && (info.stages & (EShLangVertexMask | EShLangFragmentMask))) + { + err = "Shader validation error:\nStorage Buffer block '" + info.name + "' must be marked as readonly in vertex and pixel shaders."; + return false; + } + + if (qualifiers.layoutPacking != glslang::ElpStd430) + { + err = "Shader validation error:\nStorage Buffer block '" + info.name + "' must use the std430 packing layout."; + return false; + } + + const glslang::TTypeList *structure = type->getStruct(); + if (structure == nullptr || structure->size() != 1) + { + err = "Shader validation error:\nStorage Buffer block '" + info.name + "' must contain a single unsized array of structs."; + return false; + } + + const glslang::TType* structtype = (*structure)[0].type; + if (structtype == nullptr || structtype->getBasicType() != glslang::EbtStruct || !structtype->isUnsizedArray()) + { + err = "Shader validation error:\nStorage Buffer block '" + info.name + "' must contain a single unsized array of structs."; + return false; + } + + BufferReflection bufferReflection = {}; + bufferReflection.stride = (size_t) info.size; + bufferReflection.memberCount = (size_t) info.numMembers; + + reflection.storageBuffers[info.name] = bufferReflection; + } + else + { + err = "Shader validation error:\nCannot retrieve type information for Storage Buffer Block '" + info.name + "'."; + return false; + } + } + return true; } diff --git a/src/modules/graphics/Shader.h b/src/modules/graphics/Shader.h index 2c4717626..c04c51bb8 100644 --- a/src/modules/graphics/Shader.h +++ b/src/modules/graphics/Shader.h @@ -78,6 +78,7 @@ public: UNIFORM_BOOL, UNIFORM_SAMPLER, UNIFORM_TEXELBUFFER, + UNIFORM_STORAGEBUFFER, UNIFORM_UNKNOWN, UNIFORM_MAX_ENUM }; @@ -90,11 +91,18 @@ public: STANDARD_MAX_ENUM }; + enum EntryPoint + { + ENTRYPOINT_NONE, + ENTRYPOINT_HIGHLEVEL, + ENTRYPOINT_CUSTOM, + ENTRYPOINT_RAW, + }; + struct SourceInfo { Language language; - bool isStage[ShaderStage::STAGE_MAX_ENUM]; - bool customPixelFunction; + EntryPoint stages[ShaderStage::STAGE_MAX_ENUM]; bool usesMRT; }; @@ -119,6 +127,8 @@ public: TextureType textureType; DataBaseType texelBufferType; bool isDepthSampler; + size_t bufferStride; + size_t bufferMemberCount; std::string name; union @@ -220,8 +230,23 @@ public: protected: + struct BufferReflection + { + size_t stride; + size_t memberCount; + }; + + struct ValidationReflection + { + std::map storageBuffers; + }; + + static bool validateInternal(ShaderStage* vertex, ShaderStage* pixel, std::string& err, ValidationReflection &reflection); + StrongRef stages[ShaderStage::STAGE_MAX_ENUM]; + ValidationReflection validationReflection; + }; // Shader } // graphics diff --git a/src/modules/graphics/opengl/Buffer.cpp b/src/modules/graphics/opengl/Buffer.cpp index 43276e59f..a9e33c0f9 100644 --- a/src/modules/graphics/opengl/Buffer.cpp +++ b/src/modules/graphics/opengl/Buffer.cpp @@ -78,6 +78,8 @@ Buffer::Buffer(love::graphics::Graphics *gfx, const Settings &settings, const st mapType = BUFFERTYPE_VERTEX; else if (typeFlags & TYPEFLAG_INDEX) mapType = BUFFERTYPE_INDEX; + else if (typeFlags & TYPEFLAG_SHADER_STORAGE) + mapType = BUFFERTYPE_SHADER_STORAGE; target = OpenGL::getGLBufferType(mapType); diff --git a/src/modules/graphics/opengl/Graphics.cpp b/src/modules/graphics/opengl/Graphics.cpp index f75e92b20..0d329cb24 100644 --- a/src/modules/graphics/opengl/Graphics.cpp +++ b/src/modules/graphics/opengl/Graphics.cpp @@ -333,8 +333,8 @@ bool Graphics::setMode(void */*context*/, int width, int height, int pixelwidth, glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); // Set whether drawing converts input from linear -> sRGB colorspace. - if (GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_sRGB || GLAD_EXT_framebuffer_sRGB - || GLAD_ES_VERSION_3_0) + if (!gl.bugs.brokenSRGB && (GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_sRGB + || GLAD_EXT_framebuffer_sRGB || GLAD_ES_VERSION_3_0)) { if (GLAD_VERSION_1_0 || GLAD_EXT_sRGB_write_control) gl.setEnableState(OpenGL::ENABLE_FRAMEBUFFER_SRGB, isGammaCorrect()); @@ -362,7 +362,19 @@ bool Graphics::setMode(void */*context*/, int width, int height, int pixelwidth, const float texel[] = {0.0f, 0.0f, 0.0f, 1.0f}; - love::graphics::Buffer *buffer = newBuffer(settings, format, texel, sizeof(texel), 1); + auto buffer = newBuffer(settings, format, texel, sizeof(texel), 1); + defaultBuffers[BUFFERTYPE_TEXEL].set(buffer, Acquire::NORETAIN); + } + + if (capabilities.features[FEATURE_GLSL4] && defaultBuffers[BUFFERTYPE_SHADER_STORAGE].get() == nullptr) + { + Buffer::Settings settings(Buffer::TYPEFLAG_SHADER_STORAGE, BUFFERUSAGE_STATIC); + std::vector format = {{"", DATAFORMAT_FLOAT, 0}}; + + std::vector data; + data.resize(Buffer::SHADER_STORAGE_BUFFER_MAX_STRIDE / 4); + + auto buffer = newBuffer(settings, format, data.data(), data.size() * sizeof(float), data.size()); defaultBuffers[BUFFERTYPE_TEXEL].set(buffer, Acquire::NORETAIN); } @@ -376,6 +388,9 @@ bool Graphics::setMode(void */*context*/, int width, int height, int pixelwidth, if (defaultBuffers[BUFFERTYPE_TEXEL].get()) gl.setDefaultTexelBuffer((GLuint) defaultBuffers[BUFFERTYPE_TEXEL]->getTexelBufferHandle()); + if (defaultBuffers[BUFFERTYPE_SHADER_STORAGE].get()) + gl.setDefaultStorageBuffer((GLuint) defaultBuffers[BUFFERTYPE_SHADER_STORAGE]->getHandle()); + // Reload all volatile objects. if (!Volatile::loadAll()) ::printf("Could not reload all volatile objects.\n"); @@ -1538,7 +1553,7 @@ void Graphics::initCapabilities() capabilities.features[FEATURE_GLSL3] = GLAD_ES_VERSION_3_0 || gl.isCoreProfile(); capabilities.features[FEATURE_GLSL4] = GLAD_ES_VERSION_3_1 || (gl.isCoreProfile() && GLAD_VERSION_4_3); capabilities.features[FEATURE_INSTANCING] = gl.isInstancingSupported(); - capabilities.features[FEATURE_TEXEL_BUFFER] = gl.areTexelBuffersSupported(); + capabilities.features[FEATURE_TEXEL_BUFFER] = gl.isBufferTypeSupported(BUFFERTYPE_TEXEL); static_assert(FEATURE_MAX_ENUM == 11, "Graphics::initCapabilities must be updated when adding a new graphics feature!"); capabilities.limits[LIMIT_POINT_SIZE] = gl.getMaxPointSize(); @@ -1547,10 +1562,11 @@ void Graphics::initCapabilities() capabilities.limits[LIMIT_VOLUME_TEXTURE_SIZE] = gl.getMax3DTextureSize(); capabilities.limits[LIMIT_CUBE_TEXTURE_SIZE] = gl.getMaxCubeTextureSize(); capabilities.limits[LIMIT_TEXEL_BUFFER_SIZE] = gl.getMaxTexelBufferSize(); + capabilities.limits[LIMIT_SHADER_STORAGE_BUFFER_SIZE] = gl.getMaxShaderStorageBufferSize(); capabilities.limits[LIMIT_RENDER_TARGETS] = gl.getMaxRenderTargets(); capabilities.limits[LIMIT_TEXTURE_MSAA] = gl.getMaxSamples(); capabilities.limits[LIMIT_ANISOTROPY] = gl.getMaxAnisotropy(); - static_assert(LIMIT_MAX_ENUM == 9, "Graphics::initCapabilities must be updated when adding a new system limit!"); + static_assert(LIMIT_MAX_ENUM == 10, "Graphics::initCapabilities must be updated when adding a new system limit!"); for (int i = 0; i < TEXTURE_MAX_ENUM; i++) capabilities.textureTypes[i] = gl.isTextureTypeSupported((TextureType) i); diff --git a/src/modules/graphics/opengl/OpenGL.cpp b/src/modules/graphics/opengl/OpenGL.cpp index f87e00362..b0b9f3af4 100644 --- a/src/modules/graphics/opengl/OpenGL.cpp +++ b/src/modules/graphics/opengl/OpenGL.cpp @@ -101,9 +101,12 @@ OpenGL::OpenGL() , max3DTextureSize(0) , maxCubeTextureSize(0) , maxTextureArrayLayers(0) + , maxTexelBufferSize(0) + , maxShaderStorageBufferSize(0) , maxRenderTargets(1) , maxSamples(1) , maxTextureUnits(1) + , maxShaderStorageBufferBindings(0) , maxPointSize(1) , coreProfile(false) , vendor(VENDOR_UNKNOWN) @@ -150,6 +153,16 @@ bool OpenGL::initContext() if (strstr(device, "HD Graphics 4000") || strstr(device, "HD Graphics 2500")) bugs.clientWaitSyncStalls = true; } + + if (getVendor() == VENDOR_INTEL) + { + const char *device = (const char *) glGetString(GL_RENDERER); + if (strstr(device, "HD Graphics 3000") || strstr(device, "HD Graphics 2000") + || !strcmp(device, "Intel(R) HD Graphics") || !strcmp(device, "Intel(R) HD Graphics Family")) + { + bugs.brokenSRGB = true; + } + } #endif #ifdef LOVE_WINDOWS @@ -210,8 +223,8 @@ void OpenGL::setupContext() setEnableState(ENABLE_SCISSOR_TEST, state.enableState[ENABLE_SCISSOR_TEST]); setEnableState(ENABLE_FACE_CULL, state.enableState[ENABLE_FACE_CULL]); - if (GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_sRGB || GLAD_EXT_framebuffer_sRGB - || GLAD_EXT_sRGB_write_control) + if (!bugs.brokenSRGB && (GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_sRGB + || GLAD_EXT_framebuffer_sRGB || GLAD_EXT_sRGB_write_control)) { setEnableState(ENABLE_FRAMEBUFFER_SRGB, state.enableState[ENABLE_FRAMEBUFFER_SRGB]); } @@ -225,9 +238,13 @@ void OpenGL::setupContext() for (int i = 0; i < (int) BUFFERTYPE_MAX_ENUM; i++) { state.boundBuffers[i] = 0; - glBindBuffer(getGLBufferType((BufferType) i), 0); + if (isBufferTypeSupported((BufferType) i)) + glBindBuffer(getGLBufferType((BufferType) i), 0); } + if (isBufferTypeSupported(BUFFERTYPE_SHADER_STORAGE)) + state.boundIndexedBuffers[BUFFERTYPE_SHADER_STORAGE].resize(maxShaderStorageBufferBindings, 0); + // Initialize multiple texture unit support for shaders. for (int i = 0; i < TEXTURE_MAX_ENUM + 1; i++) { @@ -280,10 +297,6 @@ void OpenGL::deInitContext() } } - if (state.defaultTexelBuffer != 0) - gl.deleteTexture(state.defaultTexelBuffer); - state.defaultTexelBuffer = 0; - contextInitialized = false; } @@ -391,15 +404,23 @@ void OpenGL::initOpenGLFunctions() } } - if (GLAD_ES_VERSION_2_0 && GLAD_OES_texture_3D && !GLAD_ES_VERSION_3_0) + if (GLAD_ES_VERSION_2_0 && !GLAD_ES_VERSION_3_0) { - // Function signatures don't match, we'll have to conditionally call it - //fp_glTexImage3D = fp_glTexImage3DOES; - fp_glTexSubImage3D = fp_glTexSubImage3DOES; - fp_glCopyTexSubImage3D = fp_glCopyTexSubImage3DOES; - fp_glCompressedTexImage3D = fp_glCompressedTexImage3DOES; - fp_glCompressedTexSubImage3D = fp_glCompressedTexSubImage3DOES; - fp_glFramebufferTexture3D = fp_glFramebufferTexture3DOES; + // The Nvidia Tegra 3 driver (used by Ouya) claims to support GL_EXT_texture_array but + // segfaults if you actually try to use it. OpenGL ES 2.0 devices should use OES_texture_3D. + // GL_EXT_texture_array is for desktops. + GLAD_EXT_texture_array = false; + + if (GLAD_OES_texture_3D) + { + // Function signatures don't match, we'll have to conditionally call it + //fp_glTexImage3D = fp_glTexImage3DOES; + fp_glTexSubImage3D = fp_glTexSubImage3DOES; + fp_glCopyTexSubImage3D = fp_glCopyTexSubImage3DOES; + fp_glCompressedTexImage3D = fp_glCompressedTexImage3DOES; + fp_glCompressedTexSubImage3D = fp_glCompressedTexSubImage3DOES; + fp_glFramebufferTexture3D = fp_glFramebufferTexture3DOES; + } } if (!GLAD_VERSION_3_2 && !GLAD_ES_VERSION_3_2 && !GLAD_ARB_draw_elements_base_vertex) @@ -463,11 +484,22 @@ void OpenGL::initMaxValues() else maxTextureArrayLayers = 0; - if (areTexelBuffersSupported()) + if (isBufferTypeSupported(BUFFERTYPE_TEXEL)) glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTexelBufferSize); else maxTexelBufferSize = 0; + if (isBufferTypeSupported(BUFFERTYPE_SHADER_STORAGE)) + { + glGetIntegerv(GL_MAX_SHADER_STORAGE_BLOCK_SIZE, &maxShaderStorageBufferSize); + glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxShaderStorageBufferBindings); + } + else + { + maxShaderStorageBufferSize = 0; + maxShaderStorageBufferBindings = 0; + } + int maxattachments = 1; int maxdrawbuffers = 1; @@ -589,6 +621,7 @@ GLenum OpenGL::getGLBufferType(BufferType type) case BUFFERTYPE_INDEX: return GL_ELEMENT_ARRAY_BUFFER; case BUFFERTYPE_TEXEL: return GL_TEXTURE_BUFFER; case BUFFERTYPE_UNIFORM: return GL_UNIFORM_BUFFER; + case BUFFERTYPE_SHADER_STORAGE: return GL_SHADER_STORAGE_BUFFER; case BUFFERTYPE_MAX_ENUM: return GL_ZERO; } @@ -786,6 +819,12 @@ void OpenGL::deleteBuffer(GLuint buffer) { if (state.boundBuffers[i] == buffer) state.boundBuffers[i] = 0; + + for (GLuint &bufferid : state.boundIndexedBuffers[i]) + { + if (bufferid == buffer) + bufferid = 0; + } } } @@ -1125,6 +1164,19 @@ void OpenGL::bindTextureToUnit(Texture *texture, int textureunit, bool restorepr bindTextureToUnit(textype, handle, textureunit, restoreprev, bindforedit); } +void OpenGL::bindIndexedBuffer(GLuint buffer, BufferType type, int index) +{ + auto &bindings = state.boundIndexedBuffers[type]; + if (bindings.size() > (size_t) index && buffer != bindings[index]) + { + bindings[index] = buffer; + glBindBufferBase(getGLBufferType(type), index, buffer); + + // glBindBufferBase affects glBindBuffer as well... for some reason. + state.boundBuffers[type] = buffer; + } +} + void OpenGL::deleteTexture(GLuint texture) { // glDeleteTextures binds texture 0 to all texture units the deleted texture @@ -1375,9 +1427,28 @@ bool OpenGL::isTextureTypeSupported(TextureType type) const return GLAD_VERSION_3_0 || GLAD_ES_VERSION_3_0 || GLAD_EXT_texture_array; case TEXTURE_CUBE: return GLAD_VERSION_1_3 || GLAD_ES_VERSION_2_0; - default: + case TEXTURE_MAX_ENUM: return false; } + return false; +} + +bool OpenGL::isBufferTypeSupported(BufferType type) const +{ + switch (type) + { + case BUFFERTYPE_VERTEX: + case BUFFERTYPE_INDEX: + return true; + case BUFFERTYPE_TEXEL: + // Not supported in ES until 3.2, which we don't support shaders for... + return GLAD_VERSION_3_1; + case BUFFERTYPE_SHADER_STORAGE: + return (GLAD_VERSION_4_3 && isCoreProfile()) || GLAD_ES_VERSION_3_1; + case BUFFERTYPE_MAX_ENUM: + return false; + } + return false; } bool OpenGL::isClampZeroOneTextureWrapSupported() const @@ -1418,12 +1489,6 @@ bool OpenGL::isMultiFormatMRTSupported() const return getMaxRenderTargets() > 1 && (GLAD_ES_VERSION_3_0 || GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_object); } -bool OpenGL::areTexelBuffersSupported() const -{ - // Not supported in ES until 3.2, which we don't support shaders for... - return GLAD_VERSION_3_1; -} - int OpenGL::getMax2DTextureSize() const { return std::max(max2DTextureSize, 1); @@ -1449,6 +1514,11 @@ int OpenGL::getMaxTexelBufferSize() const return maxTexelBufferSize; } +int OpenGL::getMaxShaderStorageBufferSize() const +{ + return maxShaderStorageBufferSize; +} + int OpenGL::getMaxRenderTargets() const { return std::min(maxRenderTargets, MAX_COLOR_RENDER_TARGETS); @@ -1464,6 +1534,11 @@ int OpenGL::getMaxTextureUnits() const return maxTextureUnits; } +int OpenGL::getMaxShaderStorageBufferBindings() const +{ + return maxShaderStorageBufferBindings; +} + float OpenGL::getMaxPointSize() const { return maxPointSize; @@ -1882,6 +1957,8 @@ bool OpenGL::isPixelFormatSupported(PixelFormat pixelformat, bool rendertarget, else return true; case PIXELFORMAT_RGBA8_UNORM_sRGB: + if (gl.bugs.brokenSRGB) + return false; if (rendertarget) { if (GLAD_VERSION_1_0) diff --git a/src/modules/graphics/opengl/OpenGL.h b/src/modules/graphics/opengl/OpenGL.h index 2ae6d13f4..d1ac0ae86 100644 --- a/src/modules/graphics/opengl/OpenGL.h +++ b/src/modules/graphics/opengl/OpenGL.h @@ -178,6 +178,13 @@ public: **/ bool brokenR8PixelFormat; + /** + * Intel HD Graphics drivers on Windows prior to the HD 2500/4000 have + * completely broken sRGB support. + * https://github.com/love2d/love/issues/1592 + **/ + bool brokenSRGB; + /** * Other bugs which have workarounds that don't use conditional code at * the moment: @@ -312,6 +319,9 @@ public: GLuint getDefaultTexelBuffer() const { return state.defaultTexelBuffer; } void setDefaultTexelBuffer(GLuint tex) { state.defaultTexelBuffer = tex; } + GLuint getDefaultStorageBuffer() const { return state.defaultStorageBuffer; } + void setDefaultStorageBuffer(GLuint buf) { state.defaultStorageBuffer = buf; } + /** * Helper for setting the active texture unit. * @@ -331,6 +341,8 @@ public: void bindBufferTextureToUnit(GLuint texture, int textureunit, bool restoreprev, bool bindforedit); + void bindIndexedBuffer(GLuint buffer, BufferType type, int index); + /** * Helper for deleting an OpenGL texture. * Cleans up if the texture is currently bound. @@ -350,6 +362,7 @@ public: bool rawTexStorage(TextureType target, int levels, PixelFormat pixelformat, bool &isSRGB, int width, int height, int depth = 1); bool isTextureTypeSupported(TextureType type) const; + bool isBufferTypeSupported(BufferType type) const; bool isClampZeroOneTextureWrapSupported() const; bool isPixelShaderHighpSupported() const; bool isInstancingSupported() const; @@ -357,7 +370,6 @@ public: bool isSamplerLODBiasSupported() const; bool isBaseVertexSupported() const; bool isMultiFormatMRTSupported() const; - bool areTexelBuffersSupported() const; /** * Returns the maximum supported width or height of a texture. @@ -372,6 +384,11 @@ public: **/ int getMaxTexelBufferSize() const; + /** + * Returns the maximum number of bytes in a shader storage buffer. + **/ + int getMaxShaderStorageBufferSize() const; + /** * Returns the maximum supported number of simultaneous render targets. **/ @@ -387,6 +404,11 @@ public: **/ int getMaxTextureUnits() const; + /** + * Returns the maximum number of shader storage buffer bindings. + **/ + int getMaxShaderStorageBufferBindings() const; + /** * Returns the maximum point size. **/ @@ -451,9 +473,11 @@ private: int maxCubeTextureSize; int maxTextureArrayLayers; int maxTexelBufferSize; + int maxShaderStorageBufferSize; int maxRenderTargets; int maxSamples; int maxTextureUnits; + int maxShaderStorageBufferBindings; float maxPointSize; bool coreProfile; @@ -468,6 +492,8 @@ private: // Texture unit state (currently bound texture for each texture unit.) std::vector boundTextures[TEXTURE_MAX_ENUM + 1]; + std::vector boundIndexedBuffers[BUFFERTYPE_MAX_ENUM]; + bool enableState[ENABLE_MAX_ENUM]; GLenum faceCullMode; @@ -488,6 +514,7 @@ private: GLuint defaultTexture[TEXTURE_MAX_ENUM]; GLuint defaultTexelBuffer; + GLuint defaultStorageBuffer; } state; diff --git a/src/modules/graphics/opengl/Shader.cpp b/src/modules/graphics/opengl/Shader.cpp index 0a6c9804c..38c9a74bc 100644 --- a/src/modules/graphics/opengl/Shader.cpp +++ b/src/modules/graphics/opengl/Shader.cpp @@ -38,6 +38,11 @@ namespace graphics namespace opengl { +static bool isBuffer(Shader::UniformType utype) +{ + return utype == Shader::UNIFORM_TEXELBUFFER || utype == Shader::UNIFORM_STORAGEBUFFER; +} + Shader::Shader(love::graphics::ShaderStage *vertex, love::graphics::ShaderStage *pixel) : love::graphics::Shader(vertex, pixel) , program(0) @@ -69,7 +74,7 @@ Shader::~Shader() delete[] p.second.textures; } - else if (p.second.baseType == UNIFORM_TEXELBUFFER) + else if (isBuffer(p.second.baseType)) { for (int i = 0; i < p.second.count; i++) { @@ -195,7 +200,7 @@ void Shader::mapActiveUniforms() u.data = malloc(u.dataSize); break; case UNIFORM_MATRIX: - u.dataSize = sizeof(float) * (u.matrix.rows * u.matrix.columns) * u.count; + u.dataSize = sizeof(float) * ((size_t)u.matrix.rows * u.matrix.columns) * u.count; u.data = malloc(u.dataSize); break; default: @@ -267,7 +272,7 @@ void Shader::mapActiveUniforms() break; case UNIFORM_MATRIX: glGetUniformfv(program, location, &u.floats[offset]); - offset += u.matrix.rows * u.matrix.columns; + offset += (size_t)u.matrix.rows * u.matrix.columns; break; default: break; @@ -310,13 +315,90 @@ void Shader::mapActiveUniforms() } } + if (gl.isBufferTypeSupported(BUFFERTYPE_SHADER_STORAGE)) + { + GLint numstoragebuffers = 0; + glGetProgramInterfaceiv(program, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES, &numstoragebuffers); + + char namebuffer[2048] = { '\0' }; + + for (int sindex = 0; sindex < numstoragebuffers; sindex++) + { + UniformInfo u = {}; + u.baseType = UNIFORM_STORAGEBUFFER; + + GLsizei namelength = 0; + glGetProgramResourceName(program, GL_SHADER_STORAGE_BLOCK, sindex, 2048, &namelength, namebuffer); + + u.name = std::string(namebuffer, namelength); + u.count = 1; + + const auto reflectionit = validationReflection.storageBuffers.find(u.name); + if (reflectionit != validationReflection.storageBuffers.end()) + { + u.bufferStride = reflectionit->second.stride; + u.bufferMemberCount = reflectionit->second.memberCount; + } + + // Make sure previously set uniform data is preserved, and shader- + // initialized values are retrieved. + auto oldu = olduniforms.find(u.name); + if (oldu != olduniforms.end()) + { + u.data = oldu->second.data; + u.dataSize = oldu->second.dataSize; + u.buffers = oldu->second.buffers; + } + else + { + u.dataSize = sizeof(int) * 1; + u.data = malloc(u.dataSize); + + u.ints[0] = -1; + + u.buffers = new love::graphics::Buffer * [u.count]; + memset(u.buffers, 0, sizeof(Buffer*)* u.count); + } + + GLenum props[] = { GL_BUFFER_BINDING }; + glGetProgramResourceiv(program, GL_SHADER_STORAGE_BLOCK, sindex, 1, props, 1, nullptr, u.ints); + + BufferBinding binding; + binding.bindingindex = u.ints[0]; + binding.buffer = gl.getDefaultStorageBuffer(); + + if (binding.bindingindex >= 0) + { + int activeindex = (int)activeStorageBufferBindings.size(); + + storageBufferBindingIndexToActiveBinding[binding.bindingindex] = activeindex; + + activeStorageBufferBindings.push_back(binding); + } + + uniforms[u.name] = u; + + for (int i = 0; i < u.count; i++) + { + if (u.buffers[i] == nullptr) + continue; + Volatile* v = dynamic_cast(u.buffers[i]); + if (v != nullptr) + v->loadVolatile(); + } + + sendBuffers(&u, u.buffers, u.count, true); + } + } + // Make sure uniforms that existed before but don't exist anymore are // cleaned up. This theoretically shouldn't happen, but... for (const auto &p : olduniforms) { if (uniforms.find(p.first) == uniforms.end()) { - free(p.second.data); + if (p.second.data != nullptr) + free(p.second.data); if (p.second.baseType == UNIFORM_SAMPLER) { @@ -328,7 +410,7 @@ void Shader::mapActiveUniforms() delete[] p.second.textures; } - else if (p.second.baseType == UNIFORM_TEXELBUFFER) + else if (isBuffer(p.second.baseType)) { for (int i = 0; i < p.second.count; i++) { @@ -354,6 +436,9 @@ bool Shader::loadVolatile() textureUnits.clear(); textureUnits.push_back(TextureUnit()); + storageBufferBindingIndexToActiveBinding.resize(gl.getMaxShaderStorageBufferBindings(), -1); + activeStorageBufferBindings.clear(); + for (const auto &stage : stages) { if (stage.get() != nullptr) @@ -491,6 +576,9 @@ void Shader::attach() } } + for (auto bufferbinding : activeStorageBufferBindings) + gl.bindIndexedBuffer(bufferbinding.buffer, BUFFERTYPE_SHADER_STORAGE, bufferbinding.bindingindex); + // send any pending uniforms to the shader program. for (const auto &p : pendingUniformUpdates) updateUniform(p.first, p.second, true); @@ -713,10 +801,18 @@ static bool isTexelBufferTypeCompatible(DataBaseType a, DataBaseType b) void Shader::sendBuffers(const UniformInfo *info, love::graphics::Buffer **buffers, int count, bool internalUpdate) { - if (info->baseType != UNIFORM_TEXELBUFFER) - return; + uint32 requiredtypeflags = 0; - uint32 requiredtypeflags = Buffer::TYPEFLAG_TEXEL; + bool texelbinding = info->baseType == UNIFORM_TEXELBUFFER; + bool storagebinding = info->baseType == UNIFORM_STORAGEBUFFER; + + if (texelbinding) + requiredtypeflags = Buffer::TYPEFLAG_TEXEL; + else if (storagebinding) + requiredtypeflags = Buffer::TYPEFLAG_SHADER_STORAGE; + + if (requiredtypeflags == 0) + return; bool shaderactive = current == this; @@ -736,17 +832,43 @@ void Shader::sendBuffers(const UniformInfo *info, love::graphics::Buffer **buffe { if (internalUpdate) continue; - else + else if (texelbinding) throw love::Exception("Shader uniform '%s' is a texel buffer, but the given Buffer was not created with texel buffer capabilities.", info->name.c_str()); + else if (storagebinding) + throw love::Exception("Shader uniform '%s' is a shader storage buffer block, but the given Buffer was not created with shader storage buffer capabilities.", info->name.c_str()); + else + throw love::Exception("Shader uniform '%s' does not match the types supported by the given Buffer.", info->name.c_str()); } - DataBaseType basetype = buffer->getDataMember(0).info.baseType; - if (!isTexelBufferTypeCompatible(basetype, info->texelBufferType)) + if (texelbinding) { - if (internalUpdate) - continue; - else - throw love::Exception("Texel buffer's data format base type must match the variable declared in the shader."); + DataBaseType basetype = buffer->getDataMember(0).info.baseType; + if (!isTexelBufferTypeCompatible(basetype, info->texelBufferType)) + { + if (internalUpdate) + continue; + else + throw love::Exception("Texel buffer's data format base type must match the variable declared in the shader."); + } + } + else if (storagebinding) + { + if (info->bufferStride != buffer->getArrayStride()) + { + if (internalUpdate) + continue; + else + throw love::Exception("Shader storage block '%s' has an array stride of %d bytes, but the given Buffer has an array stride of %d bytes.", + info->name.c_str(), info->bufferStride, buffer->getArrayStride()); + } + else if (info->bufferMemberCount != buffer->getDataMembers().size()) + { + if (internalUpdate) + continue; + else + throw love::Exception("Shader storage block '%s' has a struct with %d fields, but the given Buffer has a format with %d members.", + info->name.c_str(), info->bufferMemberCount, buffer->getDataMembers().size()); + } } buffer->retain(); @@ -757,19 +879,39 @@ void Shader::sendBuffers(const UniformInfo *info, love::graphics::Buffer **buffe info->buffers[i] = buffer; - GLuint gltex = 0; - if (buffers[i] != nullptr) - gltex = (GLuint) buffer->getTexelBufferHandle(); - else - gltex = gl.getDefaultTexelBuffer(); + if (texelbinding) + { + GLuint gltex = 0; + if (buffers[i] != nullptr) + gltex = (GLuint) buffer->getTexelBufferHandle(); + else + gltex = gl.getDefaultTexelBuffer(); - int texunit = info->ints[i]; + int texunit = info->ints[i]; - if (shaderactive) - gl.bindBufferTextureToUnit(gltex, texunit, false, false); + if (shaderactive) + gl.bindBufferTextureToUnit(gltex, texunit, false, false); - // Store texture id so it can be re-bound to the texture unit later. - textureUnits[texunit].texture = gltex; + // Store texture id so it can be re-bound to the texture unit later. + textureUnits[texunit].texture = gltex; + } + else if (storagebinding) + { + int bindingindex = info->ints[i]; + + GLuint glbuffer = 0; + if (buffers[i] != nullptr) + glbuffer = (GLuint) buffer->getHandle(); + else + glbuffer = gl.getDefaultStorageBuffer(); + + if (shaderactive) + gl.bindIndexedBuffer(glbuffer, BUFFERTYPE_SHADER_STORAGE, bindingindex); + + int activeindex = storageBufferBindingIndexToActiveBinding[bindingindex]; + if (activeindex >= 0) + activeStorageBufferBindings[activeindex].buffer = glbuffer; + } } } diff --git a/src/modules/graphics/opengl/Shader.h b/src/modules/graphics/opengl/Shader.h index a396db2e4..ef93122db 100644 --- a/src/modules/graphics/opengl/Shader.h +++ b/src/modules/graphics/opengl/Shader.h @@ -80,6 +80,12 @@ private: bool active = false; }; + struct BufferBinding + { + int bindingindex = 0; + GLuint buffer = 0; + }; + // Map active uniform names to their locations. void mapActiveUniforms(); @@ -114,6 +120,9 @@ private: // Texture unit pool for setting textures std::vector textureUnits; + std::vector storageBufferBindingIndexToActiveBinding; + std::vector activeStorageBufferBindings; + std::vector> pendingUniformUpdates; float lastPointSize; diff --git a/src/modules/graphics/opengl/Texture.cpp b/src/modules/graphics/opengl/Texture.cpp index 362e5a2a5..7a3af84de 100644 --- a/src/modules/graphics/opengl/Texture.cpp +++ b/src/modules/graphics/opengl/Texture.cpp @@ -34,7 +34,7 @@ namespace graphics namespace opengl { -static GLenum createFBO(GLuint &framebuffer, TextureType texType, PixelFormat format, GLuint texture, int layers, bool clear) +static GLenum createFBO(GLuint &framebuffer, TextureType texType, PixelFormat format, GLuint texture, int mips, int layers, bool clear) { // get currently bound fbo to reset to it later GLuint current_fbo = gl.getFramebuffer(OpenGL::FRAMEBUFFER_ALL); @@ -42,11 +42,6 @@ static GLenum createFBO(GLuint &framebuffer, TextureType texType, PixelFormat fo glGenFramebuffers(1, &framebuffer); gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, framebuffer); - // Intel driver bug: https://github.com/love2d/love/issues/1592 - bool current_srgb = gl.isStateEnabled(OpenGL::ENABLE_FRAMEBUFFER_SRGB); - if (current_srgb && isPixelFormatDepthStencil(format)) - gl.setEnableState(OpenGL::ENABLE_FRAMEBUFFER_SRGB, false); - if (texture != 0) { if (isPixelFormatDepthStencil(format) && (GLAD_ES_VERSION_3_0 || !GLAD_ES_VERSION_2_0)) @@ -68,37 +63,40 @@ static GLenum createFBO(GLuint &framebuffer, TextureType texType, PixelFormat fo // Make sure all faces and layers of the texture are initialized to // transparent black. This is unfortunately probably pretty slow for // 2D-array and 3D textures with a lot of layers... - for (int layer = layers - 1; layer >= 0; layer--) + for (int mip = mips - 1; mip >= 0; mip--) { - for (int face = faces - 1; face >= 0; face--) + for (int layer = layers - 1; layer >= 0; layer--) { - for (GLenum attachment : fmt.framebufferAttachments) + for (int face = faces - 1; face >= 0; face--) { - if (attachment == GL_NONE) - continue; - - gl.framebufferTexture(attachment, texType, texture, 0, layer, face); - } - - if (clear) - { - if (isPixelFormatDepthStencil(format)) + for (GLenum attachment : fmt.framebufferAttachments) { - bool hadDepthWrites = gl.hasDepthWrites(); - if (!hadDepthWrites) // glDepthMask also affects glClear. - gl.setDepthWrites(true); + if (attachment == GL_NONE) + continue; - gl.clearDepth(1.0); - glClearStencil(0); - glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - - if (!hadDepthWrites) - gl.setDepthWrites(hadDepthWrites); + gl.framebufferTexture(attachment, texType, texture, mip, layer, face); } - else + + if (clear) { - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glClear(GL_COLOR_BUFFER_BIT); + if (isPixelFormatDepthStencil(format)) + { + bool hadDepthWrites = gl.hasDepthWrites(); + if (!hadDepthWrites) // glDepthMask also affects glClear. + gl.setDepthWrites(true); + + gl.clearDepth(1.0); + glClearStencil(0); + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + + if (!hadDepthWrites) + gl.setDepthWrites(hadDepthWrites); + } + else + { + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + } } } } @@ -109,10 +107,6 @@ static GLenum createFBO(GLuint &framebuffer, TextureType texType, PixelFormat fo gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, current_fbo); - // Restore sRGB state if we turned it off above. - if (current_srgb && isPixelFormatDepthStencil(format)) - gl.setEnableState(OpenGL::ENABLE_FRAMEBUFFER_SRGB, current_srgb); - return status; } @@ -328,28 +322,43 @@ void Texture::createTexture() bool hasdata = slices.get(0, 0) != nullptr; + // All mipmap levels need to be initialized - for color formats we can clear + // the base mip and use glGenerateMipmap after that's done. Depth and + // stencil formats don't always support glGenerateMipmap so we need to + // individually clear each mip level in that case. We avoid doing that for + // color formats because of an Intel driver bug: + // https://github.com/love2d/love/issues/1585 + int clearmips = 1; + if (isPixelFormatDepthStencil(format)) + clearmips = mipmapCount; + // Create a local FBO used for glReadPixels as well as MSAA blitting. if (isRenderTarget()) { bool clear = !hasdata; int slices = texType == TEXTURE_VOLUME ? depth : layers; - framebufferStatus = createFBO(fbo, texType, format, texture, slices, clear); + framebufferStatus = createFBO(fbo, texType, format, texture, clearmips, slices, clear); } else if (!hasdata) { // Initialize all slices to transparent black. - std::vector emptydata(getPixelFormatSliceSize(format, w, h)); + for (int mip = 0; mip < clearmips; mip++) + { + int mipw = getPixelWidth(mip); + int miph = getPixelHeight(mip); + std::vector emptydata(getPixelFormatSliceSize(format, mipw, miph)); - Rect r = {0, 0, w, h}; - int slices = texType == TEXTURE_VOLUME ? depth : layers; - slices = texType == TEXTURE_CUBE ? 6 : slices; - for (int i = 0; i < slices; i++) - uploadByteData(format, emptydata.data(), emptydata.size(), 0, i, r); + Rect r = {0, 0, mipw, miph}; + int slices = texType == TEXTURE_VOLUME ? getDepth(mip) : layers; + slices = texType == TEXTURE_CUBE ? 6 : slices; + for (int i = 0; i < slices; i++) + uploadByteData(format, emptydata.data(), emptydata.size(), mip, i, r); + } } // Non-readable textures can't have mipmaps (enforced in the base class), // so generateMipmaps here is fine - when they aren't already initialized. - if (getMipmapCount() > 1 && slices.getMipmapCount() <= 1) + if (clearmips < mipmapCount && slices.getMipmapCount() <= 1 && getMipmapsMode() != MIPMAPS_NONE) generateMipmaps(); } diff --git a/src/modules/graphics/vertex.cpp b/src/modules/graphics/vertex.cpp index 15d16891e..d448b9895 100644 --- a/src/modules/graphics/vertex.cpp +++ b/src/modules/graphics/vertex.cpp @@ -339,9 +339,10 @@ const char *getConstant(BuiltinVertexAttribute attrib) STRINGMAP_BEGIN(BufferType, BUFFERTYPE_MAX_ENUM, bufferTypeName) { - { "vertex", BUFFERTYPE_VERTEX }, - { "index", BUFFERTYPE_INDEX }, - { "texel", BUFFERTYPE_TEXEL }, + { "vertex", BUFFERTYPE_VERTEX }, + { "index", BUFFERTYPE_INDEX }, + { "texel", BUFFERTYPE_TEXEL }, + { "shaderstorage", BUFFERTYPE_SHADER_STORAGE }, } STRINGMAP_END(BufferType, BUFFERTYPE_MAX_ENUM, bufferTypeName) diff --git a/src/modules/graphics/vertex.h b/src/modules/graphics/vertex.h index 34dce00e5..101a875d8 100644 --- a/src/modules/graphics/vertex.h +++ b/src/modules/graphics/vertex.h @@ -60,6 +60,7 @@ enum BufferType BUFFERTYPE_INDEX, BUFFERTYPE_UNIFORM, BUFFERTYPE_TEXEL, + BUFFERTYPE_SHADER_STORAGE, BUFFERTYPE_MAX_ENUM }; diff --git a/src/modules/graphics/wrap_Shader.cpp b/src/modules/graphics/wrap_Shader.cpp index ad73f5c91..bf7a3a8e1 100644 --- a/src/modules/graphics/wrap_Shader.cpp +++ b/src/modules/graphics/wrap_Shader.cpp @@ -278,8 +278,6 @@ int w_Shader_sendTextures(lua_State *L, int startidx, Shader *shader, const Shad for (int i = 0; i < count; i++) { Texture *tex = luax_checktexture(L, startidx + i); - if (tex->getTextureType() != info->textureType) - return luaL_argerror(L, startidx + i, "invalid texture type for uniform"); textures.push_back(tex); } @@ -321,6 +319,7 @@ static int w_Shader_sendLuaValues(lua_State *L, int startidx, Shader *shader, co case Shader::UNIFORM_SAMPLER: return w_Shader_sendTextures(L, startidx, shader, info); case Shader::UNIFORM_TEXELBUFFER: + case Shader::UNIFORM_STORAGEBUFFER: return w_Shader_sendBuffers(L, startidx, shader, info); default: return luaL_error(L, "Unknown variable type for shader uniform '%s", name); @@ -329,8 +328,8 @@ static int w_Shader_sendLuaValues(lua_State *L, int startidx, Shader *shader, co static int w_Shader_sendData(lua_State *L, int startidx, Shader *shader, const Shader::UniformInfo *info, bool colors) { - if (info->baseType == Shader::UNIFORM_SAMPLER) - return luaL_error(L, "Uniform sampler values (textures) cannot be sent to Shaders via Data objects."); + if (info->baseType == Shader::UNIFORM_SAMPLER || info->baseType == Shader::UNIFORM_TEXELBUFFER || info->baseType == Shader::UNIFORM_STORAGEBUFFER) + return luaL_error(L, "Only value types (floats, ints, vectors, matrices, etc) be sent to Shaders via Data objects."); math::Transform::MatrixLayout layout = math::Transform::MATRIX_ROW_MAJOR; int dataidx = startidx; diff --git a/src/modules/love/love.cpp b/src/modules/love/love.cpp index 8cc02f9bb..c6f1c5d2f 100644 --- a/src/modules/love/love.cpp +++ b/src/modules/love/love.cpp @@ -371,6 +371,23 @@ static int w_deprecation__gc(lua_State *) return 0; } +static void luax_addcompatibilityalias(lua_State *L, const char *module, const char *name, const char *alias) +{ + lua_getglobal(L, module); + if (lua_istable(L, -1)) + { + lua_getfield(L, -1, alias); + bool hasalias = !lua_isnoneornil(L, -1); + lua_pop(L, 1); + if (!hasalias) + { + lua_getfield(L, -1, name); + lua_setfield(L, -2, alias); + } + } + lua_pop(L, 1); +} + int luaopen_love(lua_State *L) { love::luax_insistpinnedthread(L); @@ -480,6 +497,13 @@ int luaopen_love(lua_State *L) love::luax_require(L, "love.data"); lua_pop(L, 1); +#if LUA_VERSION_NUM <= 501 + // These are deprecated in Lua 5.1. LuaJIT 2.1 removes them, but code + // written assuming LuaJIT 2.0 or Lua 5.1 is used might still rely on them. + luax_addcompatibilityalias(L, "math", "fmod", "mod"); + luax_addcompatibilityalias(L, "string", "gmatch", "gfind"); +#endif + #ifdef LOVE_ENABLE_LUASOCKET love::luasocket::__open(L); #endif diff --git a/src/modules/physics/box2d/Body.cpp b/src/modules/physics/box2d/Body.cpp index dfdddf1ec..472c57546 100644 --- a/src/modules/physics/box2d/Body.cpp +++ b/src/modules/physics/box2d/Body.cpp @@ -361,6 +361,30 @@ void Body::getLocalVector(float x, float y, float &x_o, float &y_o) y_o = v.y; } +int Body::getLocalPoints(lua_State *L) +{ + int argc = lua_gettop(L); + int vcount = (int)argc/2; + // at least one point + love::luax_assert_argc(L, 2); + + for (int i = 0; iGetLocalPoint(Physics::scaleDown(b2Vec2(x, y)))); + // And then we push the result + lua_pushnumber(L, point.x); + lua_pushnumber(L, point.y); + } + + return argc; +} + void Body::getLinearVelocityFromWorldPoint(float x, float y, float &x_o, float &y_o) { b2Vec2 v = Physics::scaleUp(body->GetLinearVelocityFromWorldPoint(Physics::scaleDown(b2Vec2(x, y)))); diff --git a/src/modules/physics/box2d/Body.h b/src/modules/physics/box2d/Body.h index a7b2010de..5f83fdcb6 100644 --- a/src/modules/physics/box2d/Body.h +++ b/src/modules/physics/box2d/Body.h @@ -327,6 +327,12 @@ public: **/ void getLocalVector(float x, float y, float &x_o, float &y_o); + /** + * Transforms a series of points (x, y) from world coordinates + * to local coordinates. + **/ + int getLocalPoints(lua_State *L); + /** * Gets the velocity on the Body for the given world point. * @param x The x-coordinate of the world point. diff --git a/src/modules/physics/box2d/wrap_Body.cpp b/src/modules/physics/box2d/wrap_Body.cpp index 03c0336de..efaba3f9d 100644 --- a/src/modules/physics/box2d/wrap_Body.cpp +++ b/src/modules/physics/box2d/wrap_Body.cpp @@ -472,6 +472,13 @@ int w_Body_getLocalVector(lua_State *L) return 2; } +int w_Body_getLocalPoints(lua_State *L) +{ + Body *t = luax_checkbody(L, 1); + lua_remove(L, 1); + return t->getLocalPoints(L); +} + int w_Body_getLinearVelocityFromWorldPoint(lua_State *L) { Body *t = luax_checkbody(L, 1); @@ -691,6 +698,7 @@ static const luaL_Reg w_Body_functions[] = { "getWorldPoints", w_Body_getWorldPoints }, { "getLocalPoint", w_Body_getLocalPoint }, { "getLocalVector", w_Body_getLocalVector }, + { "getLocalPoints", w_Body_getLocalPoints }, { "getLinearVelocityFromWorldPoint", w_Body_getLinearVelocityFromWorldPoint }, { "getLinearVelocityFromLocalPoint", w_Body_getLinearVelocityFromLocalPoint }, { "isBullet", w_Body_isBullet }, diff --git a/src/modules/window/sdl/Window.cpp b/src/modules/window/sdl/Window.cpp index f81a0a1cd..5cedf9ea0 100644 --- a/src/modules/window/sdl/Window.cpp +++ b/src/modules/window/sdl/Window.cpp @@ -489,15 +489,23 @@ bool Window::setWindow(int width, int height, WindowSettings *settings) height = mode.h; } - // On Android we always must have fullscreen type FULLSCREEN_TYPE_DESKTOP + // On Android, disable fullscreen first on window creation so it's + // possible to change the orientation by specifying portait width and + // height, otherwise SDL will pick the current orientation dimensions when + // fullscreen flag is set. Don't worry, we'll set it back later when user + // also requested fullscreen after the window is created. + // See https://github.com/love2d/love-android/issues/196 #ifdef LOVE_ANDROID + bool fullscreen = f.fullscreen; + + f.fullscreen = false; f.fstype = FULLSCREEN_DESKTOP; #endif int x = f.x; int y = f.y; - if (f.useposition && !f.fullscreen) + if (f.useposition) { // The position needs to be in the global coordinate space. SDL_Rect displaybounds = {}; @@ -596,7 +604,7 @@ bool Window::setWindow(int width, int height, WindowSettings *settings) // Enforce minimum window dimensions. SDL_SetWindowMinimumSize(window, f.minwidth, f.minheight); - if (this->settings.display != f.display || ((f.useposition || f.centered) && !f.fullscreen)) + if (this->settings.display != f.display || f.useposition || f.centered) SDL_SetWindowPosition(window, x, y); SDL_RaiseWindow(window); @@ -629,8 +637,11 @@ bool Window::setWindow(int width, int height, WindowSettings *settings) } } + // Set fullscreen when user requested it before. + // See above for explanation. #ifdef LOVE_ANDROID - love::android::setImmersive(f.fullscreen); + setFullscreen(fullscreen); + love::android::setImmersive(fullscreen); #endif return true;