Browse Source

Merge branch '12.0-development' into metal

Alex Szpakowski 4 years ago
parent
commit
98f8fe02a1
40 changed files with 1691 additions and 573 deletions
  1. 21 10
      platform/xcode/liblove.xcodeproj/project.pbxproj
  2. 9 9
      platform/xcode/love.xcodeproj/project.pbxproj
  3. 15 0
      src/common/StringMap.h
  4. 8 1
      src/common/android.cpp
  5. 50 0
      src/common/apple.h
  6. 79 0
      src/common/apple.mm
  7. 0 15
      src/common/ios.h
  8. 1 46
      src/common/ios.mm
  9. 0 7
      src/common/macos.h
  10. 0 28
      src/common/macos.mm
  11. 137 18
      src/libraries/physfs/physfs.c
  12. 14 0
      src/libraries/physfs/physfs.h
  13. 102 26
      src/modules/filesystem/Filesystem.cpp
  14. 41 7
      src/modules/filesystem/Filesystem.h
  15. 37 9
      src/modules/filesystem/NativeFile.cpp
  16. 1 2
      src/modules/filesystem/physfs/File.cpp
  17. 344 211
      src/modules/filesystem/physfs/Filesystem.cpp
  18. 27 15
      src/modules/filesystem/physfs/Filesystem.h
  19. 80 1
      src/modules/filesystem/wrap_Filesystem.cpp
  20. 59 8
      src/modules/graphics/Buffer.cpp
  21. 3 0
      src/modules/graphics/Buffer.h
  22. 12 11
      src/modules/graphics/Graphics.cpp
  23. 1 0
      src/modules/graphics/Graphics.h
  24. 159 39
      src/modules/graphics/Shader.cpp
  25. 27 2
      src/modules/graphics/Shader.h
  26. 2 0
      src/modules/graphics/opengl/Buffer.cpp
  27. 21 5
      src/modules/graphics/opengl/Graphics.cpp
  28. 100 23
      src/modules/graphics/opengl/OpenGL.cpp
  29. 28 1
      src/modules/graphics/opengl/OpenGL.h
  30. 167 25
      src/modules/graphics/opengl/Shader.cpp
  31. 9 0
      src/modules/graphics/opengl/Shader.h
  32. 52 43
      src/modules/graphics/opengl/Texture.cpp
  33. 4 3
      src/modules/graphics/vertex.cpp
  34. 1 0
      src/modules/graphics/vertex.h
  35. 3 4
      src/modules/graphics/wrap_Shader.cpp
  36. 24 0
      src/modules/love/love.cpp
  37. 24 0
      src/modules/physics/box2d/Body.cpp
  38. 6 0
      src/modules/physics/box2d/Body.h
  39. 8 0
      src/modules/physics/box2d/wrap_Body.cpp
  40. 15 4
      src/modules/window/sdl/Window.cpp

+ 21 - 10
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 = "<group>"; };
 		FAAA3FD61F64B3AD00F89E99 /* lutf8lib.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lutf8lib.c; sourceTree = "<group>"; };
 		FAAA3FD71F64B3AD00F89E99 /* lutf8lib.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lutf8lib.h; sourceTree = "<group>"; };
+		FAAC2F78251A9D2200BCB81B /* apple.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = apple.mm; sourceTree = "<group>"; };
+		FAAC2F7F251A9D3E00BCB81B /* apple.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = apple.h; sourceTree = "<group>"; };
 		FAAFF04316CB11C700CCDE45 /* OpenAL-Soft.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = "OpenAL-Soft.framework"; path = "/Library/Frameworks/OpenAL-Soft.framework"; sourceTree = "<absolute>"; };
 		FAB17BE41ABFAA9000F9BA27 /* lz4.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lz4.c; sourceTree = "<group>"; };
 		FAB17BE51ABFAA9000F9BA27 /* lz4.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lz4.h; sourceTree = "<group>"; };
@@ -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,

+ 9 - 9
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]" = (

+ 15 - 0
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<std::string> 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<std::string> getConstants(type); \
+
+#define STRINGMAP_CLASS_BEGIN(classname, type, count, name) \
+static StringMap<type, count>::Entry name##Entries[] =
+
+#define STRINGMAP_CLASS_END(classname, type, count, name) \
+; \
+static StringMap<type, count> 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<std::string> classname::getConstants(type) { return name##s.getNames(); }
+
 } // love
 
 #endif // LOVE_STRING_MAP_H

+ 8 - 1
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);

+ 50 - 0
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 <string>
+
+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)

+ 79 - 0
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 <Foundation/Foundation.h>
+
+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<NSURL *> *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)

+ 0 - 15
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.
  **/

+ 1 - 46
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<NSURL *> *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

+ 0 - 7
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.
  **/

+ 0 - 28
src/common/macos.mm

@@ -36,26 +36,6 @@ namespace love
 namespace macos
 {
 
-static std::string getUserDirectory(NSSearchPathDirectory dir)
-{
-	std::string path;
-
-	@autoreleasepool
-	{
-		NSArray<NSURL *> *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

+ 137 - 18
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 */
 
 
+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)
+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))

+ 14 - 0
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)

+ 102 - 26
src/modules/filesystem/Filesystem.cpp

@@ -26,12 +26,12 @@
 #include <sys/types.h>
 #include <sys/stat.h>
 
-#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 <unistd.h>
 #elif defined(LOVE_WINDOWS)
 #include <windows.h>
+#include <fileapi.h>
 #include "common/utf8.h"
 #elif defined(LOVE_LINUX)
 #include <unistd.h>
@@ -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<std::string> 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<std::string> 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<Filesystem::FileType, Filesystem::FILETYPE_MAX_ENUM>::Entry Filesystem::fileTypeEntries[] =
-{
-	{ "file",      FILETYPE_FILE      },
-	{ "directory", FILETYPE_DIRECTORY },
-	{ "symlink",   FILETYPE_SYMLINK   },
-	{ "other",     FILETYPE_OTHER     },
-};
-
-StringMap<Filesystem::FileType, Filesystem::FILETYPE_MAX_ENUM> Filesystem::fileTypes(Filesystem::fileTypeEntries, sizeof(Filesystem::fileTypeEntries));
+STRINGMAP_CLASS_END(Filesystem, Filesystem::MountPermissions, Filesystem::MOUNT_PERMISSIONS_MAX_ENUM, mountPermissions)
 
 } // filesystem
 } // love

+ 41 - 7
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<std::string> 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<FileType, FILETYPE_MAX_ENUM>::Entry fileTypeEntries[];
-	static StringMap<FileType, FILETYPE_MAX_ENUM> fileTypes;
-
 }; // Filesystem
 
 } // filesystem

+ 37 - 9
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)

+ 1 - 2
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)

+ 344 - 211
src/modules/filesystem/physfs/Filesystem.cpp

@@ -45,6 +45,10 @@
 #	include <unistd.h>
 #endif
 
+#if defined(LOVE_IOS) || defined(LOVE_MACOS)
+#	include "common/apple.h"
+#endif
+
 #ifdef LOVE_IOS
 #	include "common/ios.h"
 #endif
@@ -60,54 +64,53 @@
 #include "common/android.h"
 #endif
 
-namespace
+namespace love
+{
+namespace filesystem
+{
+namespace physfs
 {
-	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)
+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)
 	{
-		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();
+		isSep = (input[i] == LOVE_PATH_SEPARATOR[0]);
+		if (!isSep || !seenSep)
+			out << input[i];
+		seenSep = isSep;
 	}
 
+	return out.str();
 }
 
-namespace love
-{
-namespace filesystem
+static const Filesystem::CommonPath appCommonPaths[] =
 {
-namespace physfs
+	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;
-
-	// Store the save directory.
-	save_identity = std::string(ident);
-
-	// 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;
-
-	// 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;
-
-	save_path_full = normalize(save_path_full);	
-	
-#ifdef LOVE_ANDROID
-	if (save_identity == "")
-		save_identity = "unnamed";
-
-	std::string storage_path;
-	if (isAndroidSaveExternal())
-		storage_path = SDL_AndroidGetExternalStoragePath();
-	else
-		storage_path = SDL_AndroidGetInternalStoragePath();
-
-	std::string save_directory = storage_path + "/save";
-
-	save_path_full = storage_path + std::string("/save/") + save_identity;
+	if (ident == nullptr || strlen(ident) == 0)
+		return false;
 
-	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
+	// Validate whether re-mounting will work.
+	for (CommonPath p : appCommonPaths)
+	{
+		if (!commonPathMountInfo[p].mounted)
+			continue;
 
-	// 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
+		// If a file is still open, unmount will fail.
+		std::string fullPath = getFullCommonPath(p);
+		if (!fullPath.empty() && !PHYSFS_canUnmount(fullPath.c_str()))
+			return false;
+	}
 
-	// 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());
+	bool oldMountedCommonPaths[COMMONPATH_MAX_ENUM] = {false};
 
-	// 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);
+	// 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);
+	}
 
-	// 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);
+	// These will be re-populated by getFullCommonPath.
+	for (CommonPath p : appCommonPaths)
+		fullPaths[p].clear();
+
+	// Store the save directory. getFullCommonPath(COMMONPATH_APP_*) uses this.
+	saveIdentity = std::string(ident);
+	appendIdentityToPath = appendToPath;
+
+	// 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;
+
+	// 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())
-		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()))
-		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;
-	}
+	if (!saveDirectoryNeedsMounting)
+		return true;
 
-	// Set the final write directory.
-	if (!PHYSFS_setWriteDir(save_path_full.c_str()))
+	if (saveIdentity.empty())
 		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.
+	// 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;
-	}
 
+	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;
+
+	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;
 
-	return PHYSFS_mount(realPath.c_str(), mountpoint, appendToPath) != 0;
+	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 *realDir = PHYSFS_getRealDir(archive);
+	if (!realDir)
+		return false;
+
+	std::string realPath = realDir;
+	realPath += LOVE_PATH_SEPARATOR;
+	realPath += archive;
 
-	const char *mountPoint = PHYSFS_getMountPoint(realPath.c_str());
-	if (!mountPoint)
+	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))

+ 27 - 15
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:
 
-	// Contains the current working directory (UTF8).
-	std::string cwd;
+	struct CommonPathMountInfo
+	{
+		bool mounted;
+		std::string mountPoint;
+		MountPermissions permissions;
+	};
 
-	// %APPDATA% on Windows.
-	std::string appdata;
+	bool mountCommonPathInternal(CommonPath path, const char *mountpoint, MountPermissions permissions, bool appendToPath, bool createDir);
 
-	// This name will be used to create the folder
-	// in the appdata/userdata folder.
-	std::string save_identity;
+	// Contains the current working directory (UTF8).
+	std::string cwd;
 
-	// 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<std::string, StrongRef<Data>> mountedData;
 
+	std::string fullPaths[COMMONPATH_MAX_ENUM];
+
+	CommonPathMountInfo commonPathMountInfo[COMMONPATH_MAX_ENUM];
+
+	bool saveDirectoryNeedsMounting;
+
 }; // Filesystem
 
 } // physfs

+ 80 - 1
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<int64>(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 },

+ 59 - 8
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<DataDe
 	bool indexbuffer = settings.typeFlags & TYPEFLAG_INDEX;
 	bool vertexbuffer = settings.typeFlags & TYPEFLAG_VERTEX;
 	bool texelbuffer = settings.typeFlags & TYPEFLAG_TEXEL;
+	bool storagebuffer = settings.typeFlags & TYPEFLAG_SHADER_STORAGE;
 
-	if (!indexbuffer && !vertexbuffer && !texelbuffer)
-		throw love::Exception("Buffer must be created with at least one buffer type (index, vertex, or texel).");
+	if (!indexbuffer && !vertexbuffer && !texelbuffer && !storagebuffer)
+		throw love::Exception("Buffer must be created with at least one buffer type (index, vertex, texel, or shaderstorage).");
 
 	if (texelbuffer && !caps.features[Graphics::FEATURE_TEXEL_BUFFER])
 		throw love::Exception("Texel buffers are not supported on this system.");
 
+	if (storagebuffer && !caps.features[Graphics::FEATURE_GLSL4])
+		throw love::Exception("Shader Storage buffers are not supported on this system (GLSL 4 support is necessary.)");
+
 	size_t offset = 0;
 	size_t stride = 0;
+	size_t structurealignment = 1;
 
 	for (const DataDeclaration &decl : bufferformat)
 	{
@@ -116,16 +122,60 @@ Buffer::Buffer(Graphics *gfx, const Settings &settings, const std::vector<DataDe
 				throw love::Exception("Signed normalized formats are not supported in texel buffers.");
 		}
 
-		// TODO: alignment
-		member.offset = offset;
-		member.size = member.info.size;
+		size_t memberoffset = offset;
+		size_t membersize = member.info.size;
+
+		// Storage buffers are always treated as being an array of a structure.
+		// The structure's contents are the buffer format declaration.
+		if (storagebuffer)
+		{
+			// TODO: We can support these.
+			if (decl.arrayLength > 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;
 
-		offset += member.size;
+			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::vector<DataDe
 	this->size = 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()

+ 3 - 0
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

+ 12 - 11
src/modules/graphics/Graphics.cpp

@@ -298,7 +298,7 @@ Shader *Graphics::newShader(const std::vector<std::string> &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<std::string> &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::Feature, Graphics::FEATURE_MAX_ENUM> Graphics::features(Grap
 
 StringMap<Graphics::SystemLimit, Graphics::LIMIT_MAX_ENUM>::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::SystemLimit, Graphics::LIMIT_MAX_ENUM> Graphics::systemLimits(Graphics::systemLimitEntries, sizeof(Graphics::systemLimitEntries));

+ 1 - 0
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,

+ 159 - 39
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 <string>
 #include <regex>
@@ -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;
 }
 

+ 27 - 2
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<std::string, BufferReflection> storageBuffers;
+	};
+
+	static bool validateInternal(ShaderStage* vertex, ShaderStage* pixel, std::string& err, ValidationReflection &reflection);
+
 	StrongRef<ShaderStage> stages[ShaderStage::STAGE_MAX_ENUM];
 
+	ValidationReflection validationReflection;
+
 }; // Shader
 
 } // graphics

+ 2 - 0
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);
 

+ 21 - 5
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<Buffer::DataDeclaration> format = {{"", DATAFORMAT_FLOAT, 0}};
+
+		std::vector<float> 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);

+ 100 - 23
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)

+ 28 - 1
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<GLuint> boundTextures[TEXTURE_MAX_ENUM + 1];
 
+		std::vector<GLuint> 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;
 

+ 167 - 25
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<Volatile*>(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;
+
+	bool texelbinding = info->baseType == UNIFORM_TEXELBUFFER;
+	bool storagebinding = info->baseType == UNIFORM_STORAGEBUFFER;
 
-	uint32 requiredtypeflags = Buffer::TYPEFLAG_TEXEL;
+	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;
+		}
 	}
 }
 

+ 9 - 0
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<TextureUnit> textureUnits;
 
+	std::vector<int> storageBufferBindingIndexToActiveBinding;
+	std::vector<BufferBinding> activeStorageBufferBindings;
+
 	std::vector<std::pair<const UniformInfo *, int>> pendingUniformUpdates;
 
 	float lastPointSize;

+ 52 - 43
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);
-
-						gl.clearDepth(1.0);
-						glClearStencil(0);
-						glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+						if (attachment == GL_NONE)
+							continue;
 
-						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<uint8> emptydata(getPixelFormatSliceSize(format, w, h));
-
-		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);
+		for (int mip = 0; mip < clearmips; mip++)
+		{
+			int mipw = getPixelWidth(mip);
+			int miph = getPixelHeight(mip);
+			std::vector<uint8> emptydata(getPixelFormatSliceSize(format, mipw, miph));
+
+			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();
 }
 

+ 4 - 3
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)
 

+ 1 - 0
src/modules/graphics/vertex.h

@@ -60,6 +60,7 @@ enum BufferType
 	BUFFERTYPE_INDEX,
 	BUFFERTYPE_UNIFORM,
 	BUFFERTYPE_TEXEL,
+	BUFFERTYPE_SHADER_STORAGE,
 	BUFFERTYPE_MAX_ENUM
 };
 

+ 3 - 4
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;

+ 24 - 0
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

+ 24 - 0
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; i<vcount; i++)
+	{
+		float x = (float)lua_tonumber(L, 1);
+		float y = (float)lua_tonumber(L, 2);
+		// Remove them, so we don't run out of stack space
+		lua_remove(L, 1);
+		lua_remove(L, 1);
+		// Time for scaling
+		b2Vec2 point = Physics::scaleUp(body->GetLocalPoint(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))));

+ 6 - 0
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.

+ 8 - 0
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 },

+ 15 - 4
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;