Browse Source

Merge remote-tracking branch 'origin/12.0-development' into vulkan

niki 3 years ago
parent
commit
4c415479c0
55 changed files with 2096 additions and 536 deletions
  1. 6 0
      CMakeLists.txt
  2. 1 1
      license.txt
  3. 37 1
      platform/xcode/liblove.xcodeproj/project.pbxproj
  4. 1 1
      platform/xcode/love.xcodeproj/project.pbxproj
  5. 191 131
      src/common/android.cpp
  6. 21 7
      src/common/android.h
  7. 171 77
      src/libraries/dr/dr_flac.h
  8. 5 0
      src/modules/audio/Audio.cpp
  9. 15 0
      src/modules/audio/Audio.h
  10. 9 0
      src/modules/audio/null/Audio.cpp
  11. 3 0
      src/modules/audio/null/Audio.h
  12. 65 5
      src/modules/audio/openal/Audio.cpp
  13. 5 0
      src/modules/audio/openal/Audio.h
  14. 50 2
      src/modules/audio/openal/Pool.cpp
  15. 7 1
      src/modules/audio/openal/Pool.h
  16. 73 21
      src/modules/audio/wrap_Audio.cpp
  17. 20 1
      src/modules/filesystem/NativeFile.cpp
  18. 10 26
      src/modules/filesystem/physfs/Filesystem.cpp
  19. 26 30
      src/modules/filesystem/wrap_Filesystem.cpp
  20. 3 2
      src/modules/graphics/Buffer.cpp
  21. 5 4
      src/modules/graphics/Buffer.h
  22. 166 1
      src/modules/graphics/Graphics.cpp
  23. 37 3
      src/modules/graphics/Graphics.h
  24. 229 0
      src/modules/graphics/GraphicsReadback.cpp
  25. 112 0
      src/modules/graphics/GraphicsReadback.h
  26. 0 41
      src/modules/graphics/Texture.cpp
  27. 0 3
      src/modules/graphics/Texture.h
  28. 1 1
      src/modules/graphics/metal/Buffer.h
  29. 38 24
      src/modules/graphics/metal/Buffer.mm
  30. 4 0
      src/modules/graphics/metal/Graphics.h
  31. 14 16
      src/modules/graphics/metal/Graphics.mm
  32. 54 0
      src/modules/graphics/metal/GraphicsReadback.h
  33. 143 0
      src/modules/graphics/metal/GraphicsReadback.mm
  34. 0 1
      src/modules/graphics/metal/Texture.h
  35. 0 46
      src/modules/graphics/metal/Texture.mm
  36. 35 8
      src/modules/graphics/opengl/Buffer.cpp
  37. 3 1
      src/modules/graphics/opengl/Buffer.h
  38. 16 0
      src/modules/graphics/opengl/FenceSync.cpp
  39. 1 0
      src/modules/graphics/opengl/FenceSync.h
  40. 15 16
      src/modules/graphics/opengl/Graphics.cpp
  41. 4 0
      src/modules/graphics/opengl/Graphics.h
  42. 126 0
      src/modules/graphics/opengl/GraphicsReadback.cpp
  43. 56 0
      src/modules/graphics/opengl/GraphicsReadback.h
  44. 1 1
      src/modules/graphics/opengl/OpenGL.cpp
  45. 33 51
      src/modules/graphics/opengl/Texture.cpp
  46. 2 2
      src/modules/graphics/opengl/Texture.h
  47. 4 4
      src/modules/graphics/vertex.cpp
  48. 1 1
      src/modules/graphics/vertex.h
  49. 126 0
      src/modules/graphics/wrap_Graphics.cpp
  50. 1 0
      src/modules/graphics/wrap_Graphics.h
  51. 95 0
      src/modules/graphics/wrap_GraphicsReadback.cpp
  52. 36 0
      src/modules/graphics/wrap_GraphicsReadback.h
  53. 12 6
      src/modules/graphics/wrap_Texture.cpp
  54. 2 0
      src/modules/keyboard/sdl/Keyboard.cpp
  55. 5 0
      src/modules/love/callbacks.lua

+ 6 - 0
CMakeLists.txt

@@ -515,6 +515,8 @@ set(LOVE_SRC_MODULE_GRAPHICS_ROOT
 	src/modules/graphics/Font.h
 	src/modules/graphics/Graphics.cpp
 	src/modules/graphics/Graphics.h
+	src/modules/graphics/GraphicsReadback.cpp
+	src/modules/graphics/GraphicsReadback.h
 	src/modules/graphics/Mesh.cpp
 	src/modules/graphics/Mesh.h
 	src/modules/graphics/ParticleSystem.cpp
@@ -550,6 +552,8 @@ set(LOVE_SRC_MODULE_GRAPHICS_ROOT
 	src/modules/graphics/wrap_Font.h
 	src/modules/graphics/wrap_Graphics.cpp
 	src/modules/graphics/wrap_Graphics.h
+	src/modules/graphics/wrap_GraphicsReadback.cpp
+	src/modules/graphics/wrap_GraphicsReadback.h
 	src/modules/graphics/wrap_Mesh.cpp
 	src/modules/graphics/wrap_Mesh.h
 	src/modules/graphics/wrap_ParticleSystem.cpp
@@ -575,6 +579,8 @@ set(LOVE_SRC_MODULE_GRAPHICS_OPENGL
 	src/modules/graphics/opengl/FenceSync.h
 	src/modules/graphics/opengl/Graphics.cpp
 	src/modules/graphics/opengl/Graphics.h
+	src/modules/graphics/opengl/GraphicsReadback.cpp
+	src/modules/graphics/opengl/GraphicsReadback.h
 	src/modules/graphics/opengl/OpenGL.cpp
 	src/modules/graphics/opengl/OpenGL.h
 	src/modules/graphics/opengl/Shader.cpp

+ 1 - 1
license.txt

@@ -88,7 +88,7 @@ This distribution contains code from the following projects (full license text b
 
  - dr_flac
 	Website: https://github.com/mackron/dr_libs
-	Source download: https://github.com/mackron/dr_libs/blob/c5e5355/dr_flac.h
+	Source download: https://github.com/mackron/dr_libs/blob/15f37e3/dr_flac.h
 	License: MIT/Expat
 	Copyright 2018 David Reid
 

+ 37 - 1
platform/xcode/liblove.xcodeproj/project.pbxproj

@@ -823,6 +823,8 @@
 		FA6A2B7A1F60B8250074C308 /* wrap_ByteData.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA6A2B781F60B8250074C308 /* wrap_ByteData.cpp */; };
 		FA6A2B7B1F60B8250074C308 /* wrap_ByteData.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA6A2B781F60B8250074C308 /* wrap_ByteData.cpp */; };
 		FA6BDE5C1F31725300786805 /* Color.h in Headers */ = {isa = PBXBuildFile; fileRef = FA6BDE5B1F31725300786805 /* Color.h */; };
+		FA6BDF89280B62A000240F2A /* GraphicsReadback.mm in Sources */ = {isa = PBXBuildFile; fileRef = FA6BDF88280B62A000240F2A /* GraphicsReadback.mm */; };
+		FA6BDF8A280B62A000240F2A /* GraphicsReadback.mm in Sources */ = {isa = PBXBuildFile; fileRef = FA6BDF88280B62A000240F2A /* GraphicsReadback.mm */; };
 		FA6BDF8E281219E900240F2A /* DataStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA6BDF8C281219E900240F2A /* DataStream.cpp */; };
 		FA6BDF8F281219E900240F2A /* DataStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA6BDF8C281219E900240F2A /* DataStream.cpp */; };
 		FA6BDF90281219E900240F2A /* DataStream.h in Headers */ = {isa = PBXBuildFile; fileRef = FA6BDF8D281219E900240F2A /* DataStream.h */; };
@@ -832,6 +834,14 @@
 		FA7E9207277E120900C24CB2 /* theora.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA7E9206277E120900C24CB2 /* theora.xcframework */; };
 		FA84DE612778D7F3002674C6 /* SpirvIntrinsics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA84DE602778D7F3002674C6 /* SpirvIntrinsics.cpp */; };
 		FA84DE622778D7F3002674C6 /* SpirvIntrinsics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA84DE602778D7F3002674C6 /* SpirvIntrinsics.cpp */; };
+		FA84DE6627791C36002674C6 /* GraphicsReadback.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA84DE6427791C36002674C6 /* GraphicsReadback.cpp */; };
+		FA84DE6727791C36002674C6 /* GraphicsReadback.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA84DE6427791C36002674C6 /* GraphicsReadback.cpp */; };
+		FA84DE6827791C36002674C6 /* GraphicsReadback.h in Headers */ = {isa = PBXBuildFile; fileRef = FA84DE6527791C36002674C6 /* GraphicsReadback.h */; };
+		FA84DE6B277943F6002674C6 /* GraphicsReadback.h in Headers */ = {isa = PBXBuildFile; fileRef = FA84DE69277943F6002674C6 /* GraphicsReadback.h */; };
+		FA84DE6C277943F6002674C6 /* GraphicsReadback.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA84DE6A277943F6002674C6 /* GraphicsReadback.cpp */; };
+		FA84DE6D277943F6002674C6 /* GraphicsReadback.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA84DE6A277943F6002674C6 /* GraphicsReadback.cpp */; };
+		FA84DE7127795E22002674C6 /* wrap_GraphicsReadback.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA84DE6F27795E22002674C6 /* wrap_GraphicsReadback.cpp */; };
+		FA84DE7227795E22002674C6 /* wrap_GraphicsReadback.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA84DE6F27795E22002674C6 /* wrap_GraphicsReadback.cpp */; };
 		FA84DE76277CB3D5002674C6 /* SDL2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA84DE75277CB3D4002674C6 /* SDL2.xcframework */; };
 		FA84DE7A277D4C88002674C6 /* modplug.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA84DE79277D4C88002674C6 /* modplug.xcframework */; };
 		FA84DE7C277E045E002674C6 /* ogg.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA84DE7B277E045E002674C6 /* ogg.xcframework */; };
@@ -1907,6 +1917,8 @@
 		FA6A2B771F60B8250074C308 /* wrap_ByteData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = wrap_ByteData.h; sourceTree = "<group>"; };
 		FA6A2B781F60B8250074C308 /* wrap_ByteData.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = wrap_ByteData.cpp; sourceTree = "<group>"; };
 		FA6BDE5B1F31725300786805 /* Color.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Color.h; sourceTree = "<group>"; };
+		FA6BDF88280B62A000240F2A /* GraphicsReadback.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = GraphicsReadback.mm; sourceTree = "<group>"; };
+		FA6BDF8B280B62B600240F2A /* GraphicsReadback.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GraphicsReadback.h; sourceTree = "<group>"; };
 		FA6BDF8C281219E900240F2A /* DataStream.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = DataStream.cpp; sourceTree = "<group>"; };
 		FA6BDF8D281219E900240F2A /* DataStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DataStream.h; sourceTree = "<group>"; };
 		FA7634481E28722A0066EF9E /* StreamBuffer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StreamBuffer.cpp; sourceTree = "<group>"; };
@@ -1917,6 +1929,12 @@
 		FA84DE5E2778D7DC002674C6 /* glslang_c_interface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = glslang_c_interface.h; sourceTree = "<group>"; };
 		FA84DE5F2778D7DC002674C6 /* glslang_c_shader_types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = glslang_c_shader_types.h; sourceTree = "<group>"; };
 		FA84DE602778D7F3002674C6 /* SpirvIntrinsics.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SpirvIntrinsics.cpp; sourceTree = "<group>"; };
+		FA84DE6427791C36002674C6 /* GraphicsReadback.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = GraphicsReadback.cpp; sourceTree = "<group>"; };
+		FA84DE6527791C36002674C6 /* GraphicsReadback.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GraphicsReadback.h; sourceTree = "<group>"; };
+		FA84DE69277943F6002674C6 /* GraphicsReadback.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GraphicsReadback.h; sourceTree = "<group>"; };
+		FA84DE6A277943F6002674C6 /* GraphicsReadback.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GraphicsReadback.cpp; sourceTree = "<group>"; };
+		FA84DE6E27795E22002674C6 /* wrap_GraphicsReadback.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = wrap_GraphicsReadback.h; sourceTree = "<group>"; };
+		FA84DE6F27795E22002674C6 /* wrap_GraphicsReadback.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = wrap_GraphicsReadback.cpp; sourceTree = "<group>"; };
 		FA84DE75277CB3D4002674C6 /* SDL2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SDL2.xcframework; path = ios/libraries/SDL2.xcframework; sourceTree = "<group>"; };
 		FA84DE79277D4C88002674C6 /* modplug.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = modplug.xcframework; path = ios/libraries/modplug.xcframework; sourceTree = "<group>"; };
 		FA84DE7B277E045E002674C6 /* ogg.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ogg.xcframework; path = ios/libraries/ogg.xcframework; sourceTree = "<group>"; };
@@ -2819,6 +2837,8 @@
 				FA1BA09C1E16CFCE00AA2803 /* Font.h */,
 				FA0B7B8A1A95902C000E1D17 /* Graphics.cpp */,
 				FA0B7B8B1A95902C000E1D17 /* Graphics.h */,
+				FA84DE6427791C36002674C6 /* GraphicsReadback.cpp */,
+				FA84DE6527791C36002674C6 /* GraphicsReadback.h */,
 				FADF54231E3DA5BA00012CC0 /* Mesh.cpp */,
 				FADF54241E3DA5BA00012CC0 /* Mesh.h */,
 				FA18CECC23DBC6E000263725 /* metal */,
@@ -2857,6 +2877,8 @@
 				FADF54391E3DAFF700012CC0 /* wrap_Graphics.cpp */,
 				FADF543A1E3DAFF700012CC0 /* wrap_Graphics.h */,
 				FADF54371E3DAFBA00012CC0 /* wrap_Graphics.lua */,
+				FA84DE6F27795E22002674C6 /* wrap_GraphicsReadback.cpp */,
+				FA84DE6E27795E22002674C6 /* wrap_GraphicsReadback.h */,
 				FADF54281E3DAADA00012CC0 /* wrap_Mesh.cpp */,
 				FADF54291E3DAADA00012CC0 /* wrap_Mesh.h */,
 				FADF541E1E3DA52C00012CC0 /* wrap_ParticleSystem.cpp */,
@@ -2887,6 +2909,8 @@
 				FA28EBD41E352DB5003446F4 /* FenceSync.h */,
 				FA0B7B911A95902C000E1D17 /* Graphics.cpp */,
 				FA0B7B921A95902C000E1D17 /* Graphics.h */,
+				FA84DE6A277943F6002674C6 /* GraphicsReadback.cpp */,
+				FA84DE69277943F6002674C6 /* GraphicsReadback.h */,
 				FA0B7B971A95902C000E1D17 /* OpenGL.cpp */,
 				FA0B7B981A95902C000E1D17 /* OpenGL.h */,
 				FA0B7B9D1A95902C000E1D17 /* Shader.cpp */,
@@ -3354,6 +3378,8 @@
 				FA18CEE823DBC8D400263725 /* Buffer.mm */,
 				FA18CED523DBC6E000263725 /* Graphics.h */,
 				FA18CED323DBC6E000263725 /* Graphics.mm */,
+				FA6BDF8B280B62B600240F2A /* GraphicsReadback.h */,
+				FA6BDF88280B62A000240F2A /* GraphicsReadback.mm */,
 				FA18CED423DBC6E000263725 /* Metal.h */,
 				FA18CED023DBC6E000263725 /* Metal.mm */,
 				FA18CECD23DBC6E000263725 /* Shader.h */,
@@ -4338,6 +4364,7 @@
 				FABDA9C92552448300B5C523 /* b2_distance.h in Headers */,
 				FA24348921D401CB00B8918A /* pch.h in Headers */,
 				FAF140991E20934C00F898D2 /* PpTokens.h in Headers */,
+				FA84DE6827791C36002674C6 /* GraphicsReadback.h in Headers */,
 				FAF1405B1E20934C00F898D2 /* InfoSink.h in Headers */,
 				FA18CF2423DCF67900263725 /* spirv_cpp.hpp in Headers */,
 				FA0B7B321A958EA3000E1D17 /* wuff.h in Headers */,
@@ -4394,6 +4421,7 @@
 				FAC7CD961FE755B4006A60C7 /* lz4opt.h in Headers */,
 				FA9D8DD31DEB56C3002CD881 /* pixelformat.h in Headers */,
 				FABDA9DE2552448300B5C523 /* b2_prismatic_joint.h in Headers */,
+				FA84DE6B277943F6002674C6 /* GraphicsReadback.h in Headers */,
 				FAF140A61E20934C00F898D2 /* ScanContext.h in Headers */,
 				FABDA97B2552448200B5C523 /* b2_chain_polygon_contact.h in Headers */,
 				FA0B7E4A1A95902C000E1D17 /* wrap_DistanceJoint.h in Headers */,
@@ -4468,7 +4496,7 @@
 		08FB7793FE84155DC02AAC07 /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastUpgradeCheck = 1310;
+				LastUpgradeCheck = 1340;
 				TargetAttributes = {
 					FA0B78DC1A958B90000E1D17 = {
 						CreatedOnToolsVersion = 6.1.1;
@@ -4548,6 +4576,7 @@
 				FA18CF4723DD1A8100263725 /* ShaderStage.mm in Sources */,
 				FA0B7ECC1A95902C000E1D17 /* wrap_Channel.cpp in Sources */,
 				FA0B7E6D1A95902C000E1D17 /* wrap_RevoluteJoint.cpp in Sources */,
+				FA84DE7227795E22002674C6 /* wrap_GraphicsReadback.cpp in Sources */,
 				FACA02FA1F5E397B0084B28F /* DataModule.cpp in Sources */,
 				FA0B7E641A95902C000E1D17 /* wrap_PolygonShape.cpp in Sources */,
 				FA4F2C031DE936C200CA37D7 /* auxiliar.c in Sources */,
@@ -4619,6 +4648,7 @@
 				FADF53FE1E3D74F200012CC0 /* Text.cpp in Sources */,
 				FA0B7D191A95902C000E1D17 /* TrueTypeRasterizer.cpp in Sources */,
 				FAC271E723B5B5B400C200D3 /* renderstate.cpp in Sources */,
+				FA84DE6727791C36002674C6 /* GraphicsReadback.cpp in Sources */,
 				FA0B7CFB1A95902C000E1D17 /* Filesystem.cpp in Sources */,
 				FABDA9F82552448300B5C523 /* b2_dynamic_tree.cpp in Sources */,
 				FABDA98B2552448300B5C523 /* b2_revolute_joint.cpp in Sources */,
@@ -4651,6 +4681,7 @@
 				FABDA9A32552448300B5C523 /* b2_friction_joint.cpp in Sources */,
 				FAF140AD1E20934C00F898D2 /* Versions.cpp in Sources */,
 				FA0B793C1A958E3B000E1D17 /* runtime.cpp in Sources */,
+				FA6BDF8A280B62A000240F2A /* GraphicsReadback.mm in Sources */,
 				FAF140DC1E20934C00F898D2 /* InitializeDll.cpp in Sources */,
 				FA0B7DBC1A95902C000E1D17 /* Joystick.cpp in Sources */,
 				FA0B7DAF1A95902C000E1D17 /* wrap_CompressedImageData.cpp in Sources */,
@@ -4766,6 +4797,7 @@
 				FA0B7CF81A95902C000E1D17 /* FileData.cpp in Sources */,
 				FA0B7DA61A95902C000E1D17 /* PNGHandler.cpp in Sources */,
 				FAF6C9F523C2DE2900D7B5BC /* Logger.cpp in Sources */,
+				FA84DE6D277943F6002674C6 /* GraphicsReadback.cpp in Sources */,
 				FAE64A932071365100BC7981 /* physfs_platform_haiku.cpp in Sources */,
 				FA0B7E981A95902C000E1D17 /* Sound.cpp in Sources */,
 				FA0B7E371A95902C000E1D17 /* WheelJoint.cpp in Sources */,
@@ -4969,6 +5001,7 @@
 				FAF140A71E20934C00F898D2 /* ShaderLang.cpp in Sources */,
 				FAC271E623B5B5B400C200D3 /* renderstate.cpp in Sources */,
 				FA1BA09D1E16CFCE00AA2803 /* Font.cpp in Sources */,
+				FA84DE7127795E22002674C6 /* wrap_GraphicsReadback.cpp in Sources */,
 				FA0B7E6C1A95902C000E1D17 /* wrap_RevoluteJoint.cpp in Sources */,
 				FA0B7E631A95902C000E1D17 /* wrap_PolygonShape.cpp in Sources */,
 				FAC7CD7B1FE35E95006A60C7 /* physfs_platform_unix.c in Sources */,
@@ -5039,6 +5072,7 @@
 				FADF54341E3DAE6E00012CC0 /* wrap_SpriteBatch.cpp in Sources */,
 				FA0B7E0C1A95902C000E1D17 /* Fixture.cpp in Sources */,
 				FA0B7D181A95902C000E1D17 /* TrueTypeRasterizer.cpp in Sources */,
+				FA84DE6627791C36002674C6 /* GraphicsReadback.cpp in Sources */,
 				FA0B7CFA1A95902C000E1D17 /* Filesystem.cpp in Sources */,
 				FABDA9F72552448300B5C523 /* b2_dynamic_tree.cpp in Sources */,
 				FABDA98A2552448300B5C523 /* b2_revolute_joint.cpp in Sources */,
@@ -5071,6 +5105,7 @@
 				FABDA9A22552448300B5C523 /* b2_friction_joint.cpp in Sources */,
 				217DFC0D1D9F6D490055D849 /* unixudp.c in Sources */,
 				FA0B7CDC1A95902C000E1D17 /* Source.cpp in Sources */,
+				FA6BDF89280B62A000240F2A /* GraphicsReadback.mm in Sources */,
 				FA0B7DC41A95902C000E1D17 /* wrap_JoystickModule.cpp in Sources */,
 				FA0B7E6F1A95902C000E1D17 /* wrap_RopeJoint.cpp in Sources */,
 				FA24348721D401CB00B8918A /* attribute.cpp in Sources */,
@@ -5190,6 +5225,7 @@
 				FA0B7DA51A95902C000E1D17 /* PNGHandler.cpp in Sources */,
 				FA0B7B371A958EA3000E1D17 /* wuff_internal.c in Sources */,
 				FA0B7E971A95902C000E1D17 /* Sound.cpp in Sources */,
+				FA84DE6C277943F6002674C6 /* GraphicsReadback.cpp in Sources */,
 				FA4F2B791DE0125B00CA37D7 /* xxhash.c in Sources */,
 				FA0B7E361A95902C000E1D17 /* WheelJoint.cpp in Sources */,
 				FADF542F1E3DABF600012CC0 /* SpriteBatch.cpp in Sources */,

+ 1 - 1
platform/xcode/love.xcodeproj/project.pbxproj

@@ -328,7 +328,7 @@
 		29B97313FDCFA39411CA2CEA /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastUpgradeCheck = 1310;
+				LastUpgradeCheck = 1340;
 				TargetAttributes = {
 					FA0B7F051A95AAF3000E1D17 = {
 						CreatedOnToolsVersion = 6.1.1;

+ 191 - 131
src/common/android.cpp

@@ -19,6 +19,7 @@
  **/
 
 #include "android.h"
+#include "Object.h"
 
 #ifdef LOVE_ANDROID
 
@@ -45,13 +46,11 @@ namespace android
 void setImmersive(bool immersive_active)
 {
 	JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
-
 	jobject activity = (jobject) SDL_AndroidGetActivity();
+	jclass clazz = env->GetObjectClass(activity);
 
-	jclass clazz(env->GetObjectClass(activity));
-	jmethodID method_id = env->GetMethodID(clazz, "setImmersiveMode", "(Z)V");
-
-	env->CallVoidMethod(activity, method_id, immersive_active);
+	static jmethodID setImmersiveMethod = env->GetMethodID(clazz, "setImmersiveMode", "(Z)V");
+	env->CallVoidMethod(activity, setImmersiveMethod, immersive_active);
 
 	env->DeleteLocalRef(activity);
 	env->DeleteLocalRef(clazz);
@@ -60,18 +59,16 @@ void setImmersive(bool immersive_active)
 bool getImmersive()
 {
 	JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
-
 	jobject activity = (jobject) SDL_AndroidGetActivity();
+	jclass clazz = env->GetObjectClass(activity);
 
-	jclass clazz(env->GetObjectClass(activity));
-	jmethodID method_id = env->GetMethodID(clazz, "getImmersiveMode", "()Z");
-
-	jboolean immersive_active = env->CallBooleanMethod(activity, method_id);
+	static jmethodID getImmersiveMethod = env->GetMethodID(clazz, "getImmersiveMode", "()Z");
+	jboolean immersiveActive = env->CallBooleanMethod(activity, getImmersiveMethod);
 
 	env->DeleteLocalRef(activity);
 	env->DeleteLocalRef(clazz);
 
-	return immersive_active;
+	return immersiveActive;
 }
 
 double getScreenScale()
@@ -81,16 +78,13 @@ double getScreenScale()
 	if (result == -1.)
 	{
 		JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
-		jclass activity = env->FindClass("org/love2d/android/GameActivity");
-
-		jmethodID getMetrics = env->GetStaticMethodID(activity, "getMetrics", "()Landroid/util/DisplayMetrics;");
-		jobject metrics = env->CallStaticObjectMethod(activity, getMetrics);
-		jclass metricsClass = env->GetObjectClass(metrics);
+		jobject activity = (jobject) SDL_AndroidGetActivity();
+		jclass clazz = env->GetObjectClass(activity);
 
-		result = env->GetFloatField(metrics, env->GetFieldID(metricsClass, "density", "F"));
+		jmethodID getDPIMethod = env->GetMethodID(clazz, "getDPIScale", "()F");
+		result = (double) env->CallFloatMethod(activity, getDPIMethod);
 
-		env->DeleteLocalRef(metricsClass);
-		env->DeleteLocalRef(metrics);
+		env->DeleteLocalRef(clazz);
 		env->DeleteLocalRef(activity);
 	}
 
@@ -101,8 +95,10 @@ bool getSafeArea(int &top, int &left, int &bottom, int &right)
 {
 	JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
 	jobject activity = (jobject) SDL_AndroidGetActivity();
-	jclass clazz(env->GetObjectClass(activity));
-	jmethodID methodID = env->GetMethodID(clazz, "initializeSafeArea", "()Z");
+	jclass clazz = env->GetObjectClass(activity);
+
+	static jmethodID methodID = env->GetMethodID(clazz, "getSafeArea", "()Z");
+
 	bool hasSafeArea = false;
 
 	if (methodID == nullptr)
@@ -122,64 +118,33 @@ bool getSafeArea(int &top, int &left, int &bottom, int &right)
 	return hasSafeArea;
 }
 
-const char *getSelectedGameFile()
-{
-	static const char *path = NULL;
-
-	if (path)
-	{
-		delete path;
-		path = NULL;
-	}
-
-	JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
-	jclass activity = env->FindClass("org/love2d/android/GameActivity");
-
-	jmethodID getGamePath = env->GetStaticMethodID(activity, "getGamePath", "()Ljava/lang/String;");
-	jstring gamePath = (jstring) env->CallStaticObjectMethod(activity, getGamePath);
-	const char *utf = env->GetStringUTFChars(gamePath, 0);
-	if (utf)
-	{
-		path = SDL_strdup(utf);
-		env->ReleaseStringUTFChars(gamePath, utf);
-	}
-
-	env->DeleteLocalRef(gamePath);
-	env->DeleteLocalRef(activity);
-
-	return path;
-}
-
 bool openURL(const std::string &url)
 {
 	JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
-	jclass activity = env->FindClass("org/love2d/android/GameActivity");
-
-	jmethodID openURL = env->GetStaticMethodID(activity, "openURLFromLOVE", "(Ljava/lang/String;)Z");
-
-	if (openURL == nullptr)
-	{
-		env->ExceptionClear();
-		openURL = env->GetStaticMethodID(activity, "openURL", "(Ljava/lang/String;)Z");
-	}
+	jobject activity = (jobject) SDL_AndroidGetActivity();
+	jclass clazz = env->GetObjectClass(activity);
 
-	jstring url_jstring = (jstring) env->NewStringUTF(url.c_str());
+	static jmethodID openURL = env->GetMethodID(clazz, "openURLFromLOVE", "(Ljava/lang/String;)Z");
 
-	jboolean result = env->CallStaticBooleanMethod(activity, openURL, url_jstring);
+	jstring jstringURL = env->NewStringUTF(url.c_str());
+	jboolean result = env->CallBooleanMethod(clazz, openURL, jstringURL);
 
-	env->DeleteLocalRef(url_jstring);
+	env->DeleteLocalRef(jstringURL);
+	env->DeleteLocalRef(clazz);
 	env->DeleteLocalRef(activity);
-	return result;
+	return (bool) result;
 }
 
 void vibrate(double seconds)
 {
 	JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
-	jclass activity = env->FindClass("org/love2d/android/GameActivity");
+	jobject activity = (jobject) SDL_AndroidGetActivity();
+	jclass clazz = env->GetObjectClass(activity);
 
-	jmethodID vibrate_method = env->GetStaticMethodID(activity, "vibrate", "(D)V");
-	env->CallStaticVoidMethod(activity, vibrate_method, seconds);
+	static jmethodID vibrateMethod = env->GetMethodID(clazz, "vibrate", "(D)V");
+	env->CallVoidMethod(activity, vibrateMethod, seconds);
 
+	env->DeleteLocalRef(clazz);
 	env->DeleteLocalRef(activity);
 }
 
@@ -192,40 +157,9 @@ void freeGameArchiveMemory(void *ptr)
 	delete[] game_love_data;
 }
 
-bool loadGameArchiveToMemory(const char* filename, char **ptr, size_t *size)
-{
-	SDL_RWops *asset_game_file = SDL_RWFromFile(filename, "rb");
-	if (!asset_game_file) {
-		SDL_Log("Could not find %s", filename);
-		return false;
-	}
-
-	Sint64 file_size = asset_game_file->size(asset_game_file);
-	if (file_size <= 0) {
-		SDL_Log("Could not load game from %s. File has invalid file size: %d.", filename, (int) file_size);
-		return false;
-	}
-
-	*ptr = new char[file_size];
-	if (!*ptr) {
-		SDL_Log("Could not allocate memory for in-memory game archive");
-		return false;
-	}
-
-	size_t bytes_copied = asset_game_file->read(asset_game_file, (void*) *ptr, sizeof(char), (size_t) file_size);
-	if (bytes_copied != file_size) {
-		SDL_Log("Incomplete copy of in-memory game archive!");
-		delete[] *ptr;
-		return false;
-	}
-
-	*size = (size_t) file_size;
-	return true;
-}
-
 bool directoryExists(const char *path)
 {
-	struct stat s;
+	struct stat s {};
 	int err = stat(path, &s);
 	if (err == -1)
 	{
@@ -284,9 +218,9 @@ bool hasRecordingPermission()
 {
 	JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
 	jobject activity = (jobject) SDL_AndroidGetActivity();
+	jclass clazz = env->GetObjectClass(activity);
 
-	jclass clazz(env->GetObjectClass(activity));
-	jmethodID methodID = env->GetMethodID(clazz, "hasRecordAudioPermission", "()Z");
+	static jmethodID methodID = env->GetMethodID(clazz, "hasRecordAudioPermission", "()Z");
 	jboolean result = false;
 
 	if (methodID == nullptr)
@@ -755,25 +689,23 @@ void deinitializeVirtualArchive()
 
 bool checkFusedGame(void **physfsIO_Out)
 {
-	// TODO: Reorder the loading in 12.0
 	PHYSFS_Io *&io = *(PHYSFS_Io **) physfsIO_Out;
 	AAssetManager *assetManager = getAssetManager();
 
-	// Prefer game.love inside assets/ folder
-	AAsset *asset = AAssetManager_open(assetManager, "game.love", AASSET_MODE_RANDOM);
+	// Prefer main.lua inside assets/ folder
+	AAsset *asset = AAssetManager_open(assetManager, "main.lua", AASSET_MODE_STREAMING);
 	if (asset)
 	{
-		io = aasset::io::fromAAsset(assetManager, "game.love", asset);
+		AAsset_close(asset);
+		io = nullptr;
 		return true;
 	}
 
-	// If there's no game.love inside assets/ try main.lua
-	asset = AAssetManager_open(assetManager, "main.lua", AASSET_MODE_STREAMING);
-
+	// If there's no main.lua inside assets/ try game.love
+	asset = AAssetManager_open(assetManager, "game.love", AASSET_MODE_RANDOM);
 	if (asset)
 	{
-		AAsset_close(asset);
-		io = nullptr;
+		io = aasset::io::fromAAsset(assetManager, "game.love", asset);
 		return true;
 	}
 
@@ -784,43 +716,171 @@ bool checkFusedGame(void **physfsIO_Out)
 const char *getCRequirePath()
 {
 	static bool initialized = false;
-	static const char *path = nullptr;
+	static std::string path;
 
 	if (!initialized)
 	{
 		JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
 		jobject activity = (jobject) SDL_AndroidGetActivity();
+		jclass clazz = env->GetObjectClass(activity);
 
-		jclass clazz(env->GetObjectClass(activity));
-		jmethodID method_id = env->GetMethodID(clazz, "getCRequirePath", "()Ljava/lang/String;");
-
-		path = "";
-		initialized = true;
+		static jmethodID getCRequireMethod = env->GetMethodID(clazz, "getCRequirePath", "()Ljava/lang/String;");
 
-		if (method_id)
+		jstring cpath = (jstring) env->CallObjectMethod(activity, getCRequireMethod);
+		const char *utf = env->GetStringUTFChars(cpath, nullptr);
+		if (utf)
 		{
-			jstring cpath = (jstring) env->CallObjectMethod(activity, method_id);
-			const char *utf = env->GetStringUTFChars(cpath, nullptr);
-			if (utf)
-			{
-				path = SDL_strdup(utf);
-				env->ReleaseStringUTFChars(cpath, utf);
-			}
-
-			env->DeleteLocalRef(cpath);
-		}
-		else
-		{
-			// NoSuchMethodException is thrown in case methodID is null
-			env->ExceptionClear();
-			return "";
+			path = utf;
+			env->ReleaseStringUTFChars(cpath, utf);
 		}
 
+		env->DeleteLocalRef(cpath);
 		env->DeleteLocalRef(activity);
 		env->DeleteLocalRef(clazz);
 	}
 
-	return path;
+	return path.c_str();
+}
+
+int getFDFromContentProtocol(const char *path)
+{
+	int fd = -1;
+
+	if (strstr(path, "content://") == path)
+	{
+		JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
+		jobject activity = (jobject) SDL_AndroidGetActivity();
+		jclass clazz = env->GetObjectClass(activity);
+
+		static jmethodID converter = env->GetMethodID(clazz, "convertToFileDescriptor", "(Ljava/lang/String;)I");
+
+		jstring uri = env->NewStringUTF(path);
+		fd = (int) env->CallIntMethod(activity, converter, uri);
+
+		env->DeleteLocalRef(uri);
+		env->DeleteLocalRef(clazz);
+		env->DeleteLocalRef(activity);
+	}
+
+	return fd;
+}
+
+int getFDFromLoveProtocol(const char *path)
+{
+	constexpr const char PROTOCOL[] = "love2d://fd/";
+	constexpr size_t PROTOCOL_LEN = sizeof(PROTOCOL) - 1;
+
+	if (*path == '/')
+		path++;
+
+	if (memcmp(path, PROTOCOL, PROTOCOL_LEN) == 0)
+	{
+		try
+		{
+			return std::stoi(path + PROTOCOL_LEN, nullptr, 10);
+		}
+		catch (std::logic_error &)
+		{ }
+	}
+
+	return -1;
+}
+
+class FileDescriptorTracker: public love::Object
+{
+public:
+	explicit FileDescriptorTracker(int fd): Object(), fd(fd) {}
+	~FileDescriptorTracker() override { close(fd); }
+	int getFd() const { return fd; }
+private:
+	int fd;
+};
+
+struct FileDescriptorIO
+{
+	FileDescriptorTracker *fd;
+	off64_t size;
+	off64_t offset;
+};
+
+void *getIOFromFD(int fd)
+{
+	if (fd == -1)
+		return nullptr;
+
+	// Create file descriptor IO structure
+	FileDescriptorIO *fdio = new FileDescriptorIO();
+	fdio->size = lseek64(fd, 0, SEEK_END);
+	fdio->offset = 0;
+	lseek64(fd, 0, SEEK_SET);
+
+	if (fdio->size == -1)
+	{
+		// Cannot get size
+		delete fdio;
+		return nullptr;
+	}
+
+	fdio->fd = new FileDescriptorTracker(fd);
+
+	PHYSFS_Io *io = new PHYSFS_Io();
+	io->version = 0;
+	io->opaque = fdio;
+	io->read = [](PHYSFS_Io *io, void *buf, PHYSFS_uint64 size)
+	{
+		FileDescriptorIO *fdio = (FileDescriptorIO *) io->opaque;
+		ssize_t ret = pread64(fdio->fd->getFd(), buf, (size_t) size, fdio->offset);
+
+		if (ret == -1)
+			PHYSFS_setErrorCode(PHYSFS_ERR_OTHER_ERROR);
+		else
+			fdio->offset = std::min(fdio->offset + (off64_t) ret, fdio->size);
+
+		return (PHYSFS_sint64) ret;
+	};
+	io->write = nullptr;
+	io->seek = [](PHYSFS_Io *io, PHYSFS_uint64 offset)
+	{
+		FileDescriptorIO *fdio = (FileDescriptorIO *) io->opaque;
+		fdio->offset = std::min(std::max<off64_t>((off64_t) offset, 0), fdio->size);
+		// Always success
+		return 1;
+	};
+	io->tell = [](PHYSFS_Io *io)
+	{
+		FileDescriptorIO *fdio = (FileDescriptorIO *) io->opaque;
+		return (PHYSFS_sint64) fdio->offset;
+	};
+	io->length = [](PHYSFS_Io *io)
+	{
+		FileDescriptorIO *fdio = (FileDescriptorIO *) io->opaque;
+		return (PHYSFS_sint64) fdio->size;
+	};
+	io->duplicate = [](PHYSFS_Io *io)
+	{
+		FileDescriptorIO *fdio = (FileDescriptorIO *) io->opaque;
+		FileDescriptorIO *fdio2 = new FileDescriptorIO();
+		PHYSFS_Io *io2 = new PHYSFS_Io();
+
+		fdio->fd->retain();
+
+		// Copy data
+		*fdio2 = *fdio;
+		*io2 = *io;
+		io2->opaque = fdio2;
+
+		return io2;
+	};
+	io->flush = nullptr;
+	io->destroy = [](PHYSFS_Io *io)
+	{
+		FileDescriptorIO *fdio = (FileDescriptorIO *) io->opaque;
+		fdio->fd->release();
+		delete fdio;
+		delete io;
+	};
+
+	return io;
 }
 
 } // android

+ 21 - 7
src/common/android.h

@@ -50,11 +50,6 @@ double getScreenScale();
  **/
 bool getSafeArea(int &top, int &left, int &bottom, int &right);
 
-/**
- * Gets the selected love file in the device filesystem.
- **/
-const char *getSelectedGameFile();
-
 bool openURL(const std::string &url);
 
 void vibrate(double seconds);
@@ -64,8 +59,6 @@ void vibrate(double seconds);
  */
 void freeGameArchiveMemory(void *ptr);
 
-bool loadGameArchiveToMemory(const char *filename, char **ptr, size_t *size);
-
 bool directoryExists(const char *path);
 
 bool mkdir(const char *path);
@@ -103,6 +96,27 @@ bool checkFusedGame(void **physfsIO_Out);
 
 const char *getCRequirePath();
 
+/**
+ * Convert "content://" to file descriptor.
+ * @param path Path with content:// URI
+ * @return File descriptor if successful, -1 on failure.
+ */
+int getFDFromContentProtocol(const char *path);
+
+/**
+ * Attempt to parse "(/)love2d://fd/<fd>" from path.
+ * @param path Potentially special path.
+ * @return File descriptor passed if successful, -1 if path is not valid.
+ */
+int getFDFromLoveProtocol(const char *path);
+
+/**
+ * Create PHYSFS_Io from file descriptor.
+ * @param fd File descriptor
+ * @return PHYSFS_Io casted to void*.
+ */
+void *getIOFromFD(int fd);
+
 } // android
 } // love
 

+ 171 - 77
src/libraries/dr/dr_flac.h

@@ -1,6 +1,6 @@
 /*
 FLAC audio decoder. Choice of public domain or MIT-0. See license statements at the end of this file.
-dr_flac - v0.12.33 - 2021-12-22
+dr_flac - v0.12.38 - 2022-04-10
 
 David Reid - [email protected]
 
@@ -232,7 +232,7 @@ extern "C" {
 
 #define DRFLAC_VERSION_MAJOR     0
 #define DRFLAC_VERSION_MINOR     12
-#define DRFLAC_VERSION_REVISION  33
+#define DRFLAC_VERSION_REVISION  38
 #define DRFLAC_VERSION_STRING    DRFLAC_XSTRINGIFY(DRFLAC_VERSION_MAJOR) "." DRFLAC_XSTRINGIFY(DRFLAC_VERSION_MINOR) "." DRFLAC_XSTRINGIFY(DRFLAC_VERSION_REVISION)
 
 #include <stddef.h> /* For size_t. */
@@ -1363,9 +1363,15 @@ DRFLAC_API drflac_bool32 drflac_next_cuesheet_track(drflac_cuesheet_track_iterat
     I am using "__inline__" only when we're compiling in strict ANSI mode.
     */
     #if defined(__STRICT_ANSI__)
-        #define DRFLAC_INLINE __inline__ __attribute__((always_inline))
+        #define DRFLAC_GNUC_INLINE_HINT __inline__
     #else
-        #define DRFLAC_INLINE inline __attribute__((always_inline))
+        #define DRFLAC_GNUC_INLINE_HINT inline
+    #endif
+
+    #if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 2)) || defined(__clang__)
+        #define DRFLAC_INLINE DRFLAC_GNUC_INLINE_HINT __attribute__((always_inline))
+    #else
+        #define DRFLAC_INLINE DRFLAC_GNUC_INLINE_HINT
     #endif
 #elif defined(__WATCOMC__)
     #define DRFLAC_INLINE __inline
@@ -1378,7 +1384,7 @@ DRFLAC_API drflac_bool32 drflac_next_cuesheet_track(drflac_cuesheet_track_iterat
     #define DRFLAC_X64
 #elif defined(__i386) || defined(_M_IX86)
     #define DRFLAC_X86
-#elif defined(__arm__) || defined(_M_ARM) || defined(_M_ARM64)
+#elif defined(__arm__) || defined(_M_ARM) || defined(__arm64) || defined(__arm64__) || defined(__aarch64__) || defined(_M_ARM64)
     #define DRFLAC_ARM
 #endif
 
@@ -1431,16 +1437,6 @@ Unfortuantely dr_flac depends on this for a few things so we're just going to di
     #if defined(DRFLAC_ARM)
         #if !defined(DRFLAC_NO_NEON) && (defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64))
             #define DRFLAC_SUPPORT_NEON
-        #endif
-
-        /* Fall back to looking for the #include file. */
-        #if !defined(__GNUC__) && !defined(__clang__) && defined(__has_include)
-            #if !defined(DRFLAC_SUPPORT_NEON) && !defined(DRFLAC_NO_NEON) && __has_include(<arm_neon.h>)
-                #define DRFLAC_SUPPORT_NEON
-            #endif
-        #endif
-
-        #if defined(DRFLAC_SUPPORT_NEON)
             #include <arm_neon.h>
         #endif
     #endif
@@ -1909,6 +1905,12 @@ static DRFLAC_INLINE drflac_uint32 drflac__be2host_32(drflac_uint32 n)
     return n;
 }
 
+static DRFLAC_INLINE drflac_uint32 drflac__be2host_32_ptr_unaligned(const void* pData)
+{
+    const drflac_uint8* pNum = (drflac_uint8*)pData;
+    return *(pNum) << 24 | *(pNum+1) << 16 | *(pNum+2) << 8 | *(pNum+3);
+}
+
 static DRFLAC_INLINE drflac_uint64 drflac__be2host_64(drflac_uint64 n)
 {
     if (drflac__is_little_endian()) {
@@ -1928,6 +1930,12 @@ static DRFLAC_INLINE drflac_uint32 drflac__le2host_32(drflac_uint32 n)
     return n;
 }
 
+static DRFLAC_INLINE drflac_uint32 drflac__le2host_32_ptr_unaligned(const void* pData)
+{
+    const drflac_uint8* pNum = (drflac_uint8*)pData;
+    return *pNum | *(pNum+1) << 8 |  *(pNum+2) << 16 | *(pNum+3) << 24;
+}
+
 
 static DRFLAC_INLINE drflac_uint32 drflac__unsynchsafe_32(drflac_uint32 n)
 {
@@ -2429,6 +2437,10 @@ static DRFLAC_INLINE drflac_bool32 drflac__read_uint32(drflac_bs* bs, unsigned i
         if (!drflac__reload_cache(bs)) {
             return DRFLAC_FALSE;
         }
+        if (bitCountLo > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) {
+            /* This happens when we get to end of stream */
+            return DRFLAC_FALSE;
+        }
 
         *pResultOut = (resultHi << bitCountLo) | (drflac_uint32)DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, bitCountLo);
         bs->consumedBits += bitCountLo;
@@ -2872,9 +2884,24 @@ static DRFLAC_INLINE drflac_bool32 drflac__seek_past_next_set_bit(drflac_bs* bs,
         }
     }
 
+    if (bs->cache == 1) {
+        /* Not catching this would lead to undefined behaviour: a shift of a 32-bit number by 32 or more is undefined */
+        *pOffsetOut = zeroCounter + (drflac_uint32)DRFLAC_CACHE_L1_BITS_REMAINING(bs) - 1;
+        if (!drflac__reload_cache(bs)) {
+            return DRFLAC_FALSE;
+        }
+
+        return DRFLAC_TRUE;
+    }
+
     setBitOffsetPlus1 = drflac__clz(bs->cache);
     setBitOffsetPlus1 += 1;
 
+    if (setBitOffsetPlus1 > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) {
+        /* This happens when we get to end of stream */
+        return DRFLAC_FALSE;
+    }
+
     bs->consumedBits += setBitOffsetPlus1;
     bs->cache <<= setBitOffsetPlus1;
 
@@ -2989,6 +3016,25 @@ static drflac_result drflac__read_utf8_coded_number(drflac_bs* bs, drflac_uint64
 }
 
 
+static DRFLAC_INLINE drflac_uint32 drflac__ilog2_u32(drflac_uint32 x)
+{
+#if 1   /* Needs optimizing. */
+    drflac_uint32 result = 0;
+    while (x > 0) {
+        result += 1;
+        x >>= 1;
+    }
+
+    return result;
+#endif
+}
+
+static DRFLAC_INLINE drflac_bool32 drflac__use_64_bit_prediction(drflac_uint32 bitsPerSample, drflac_uint32 order, drflac_uint32 precision)
+{
+    /* https://web.archive.org/web/20220205005724/https://github.com/ietf-wg-cellar/flac-specification/blob/37a49aa48ba4ba12e8757badfc59c0df35435fec/rfc_backmatter.md */
+    return bitsPerSample + precision + drflac__ilog2_u32(order) > 32;
+}
+
 
 /*
 The next two functions are responsible for calculating the prediction.
@@ -2996,6 +3042,9 @@ The next two functions are responsible for calculating the prediction.
 When the bits per sample is >16 we need to use 64-bit integer arithmetic because otherwise we'll run out of precision. It's
 safe to assume this will be slower on 32-bit platforms so we use a more optimal solution when the bits per sample is <=16.
 */
+#if defined(__clang__)
+__attribute__((no_sanitize("signed-integer-overflow")))
+#endif
 static DRFLAC_INLINE drflac_int32 drflac__calculate_prediction_32(drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pDecodedSamples)
 {
     drflac_int32 prediction = 0;
@@ -3231,7 +3280,7 @@ static DRFLAC_INLINE drflac_int32 drflac__calculate_prediction_64(drflac_uint32
 Reference implementation for reading and decoding samples with residual. This is intentionally left unoptimized for the
 sake of readability and should only be used as a reference.
 */
-static drflac_bool32 drflac__decode_samples_with_residual__rice__reference(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
+static drflac_bool32 drflac__decode_samples_with_residual__rice__reference(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
 {
     drflac_uint32 i;
 
@@ -3270,10 +3319,10 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__reference(drfla
         }
 
 
-        if (bitsPerSample+shift >= 32) {
-            pSamplesOut[i] = decodedRice + drflac__calculate_prediction_64(order, shift, coefficients, pSamplesOut + i);
+        if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) {
+            pSamplesOut[i] = decodedRice + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + i);
         } else {
-            pSamplesOut[i] = decodedRice + drflac__calculate_prediction_32(order, shift, coefficients, pSamplesOut + i);
+            pSamplesOut[i] = decodedRice + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + i);
         }
     }
 
@@ -3370,6 +3419,10 @@ static DRFLAC_INLINE drflac_bool32 drflac__read_rice_parts(drflac_bs* bs, drflac
             if (!drflac__reload_cache(bs)) {
                 return DRFLAC_FALSE;
             }
+            if (bitCountLo > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) {
+                /* This happens when we get to end of stream */
+                return DRFLAC_FALSE;
+            }
         }
 
         riceParamPart = (drflac_uint32)(resultHi | DRFLAC_CACHE_L1_SELECT_AND_SHIFT_SAFE(bs, bitCountLo));
@@ -3450,6 +3503,10 @@ static DRFLAC_INLINE drflac_bool32 drflac__read_rice_parts_x1(drflac_bs* bs, drf
                 if (!drflac__reload_cache(bs)) {
                     return DRFLAC_FALSE;
                 }
+                if (riceParamPartLoBitCount > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) {
+                    /* This happens when we get to end of stream */
+                    return DRFLAC_FALSE;
+                }
 
                 bs_cache = bs->cache;
                 bs_consumedBits = bs->consumedBits + riceParamPartLoBitCount;
@@ -3560,6 +3617,11 @@ static DRFLAC_INLINE drflac_bool32 drflac__seek_rice_parts(drflac_bs* bs, drflac
                     return DRFLAC_FALSE;
                 }
 
+                if (riceParamPartLoBitCount > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) {
+                    /* This happens when we get to end of stream */
+                    return DRFLAC_FALSE;
+                }
+
                 bs_cache = bs->cache;
                 bs_consumedBits = bs->consumedBits + riceParamPartLoBitCount;
             }
@@ -3646,7 +3708,7 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar_zeroorde
     return DRFLAC_TRUE;
 }
 
-static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
+static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
 {
     drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF};
     drflac_uint32 zeroCountPart0 = 0;
@@ -3664,14 +3726,14 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar(drflac_b
     DRFLAC_ASSERT(bs != NULL);
     DRFLAC_ASSERT(pSamplesOut != NULL);
 
-    if (order == 0) {
-        return drflac__decode_samples_with_residual__rice__scalar_zeroorder(bs, bitsPerSample, count, riceParam, order, shift, coefficients, pSamplesOut);
+    if (lpcOrder == 0) {
+        return drflac__decode_samples_with_residual__rice__scalar_zeroorder(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut);
     }
 
     riceParamMask  = (drflac_uint32)~((~0UL) << riceParam);
     pSamplesOutEnd = pSamplesOut + (count & ~3);
 
-    if (bitsPerSample+shift > 32) {
+    if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) {
         while (pSamplesOut < pSamplesOutEnd) {
             /*
             Rice extraction. It's faster to do this one at a time against local variables than it is to use the x4 version
@@ -3699,10 +3761,10 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar(drflac_b
             riceParamPart2  = (riceParamPart2 >> 1) ^ t[riceParamPart2 & 0x01];
             riceParamPart3  = (riceParamPart3 >> 1) ^ t[riceParamPart3 & 0x01];
 
-            pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_64(order, shift, coefficients, pSamplesOut + 0);
-            pSamplesOut[1] = riceParamPart1 + drflac__calculate_prediction_64(order, shift, coefficients, pSamplesOut + 1);
-            pSamplesOut[2] = riceParamPart2 + drflac__calculate_prediction_64(order, shift, coefficients, pSamplesOut + 2);
-            pSamplesOut[3] = riceParamPart3 + drflac__calculate_prediction_64(order, shift, coefficients, pSamplesOut + 3);
+            pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 0);
+            pSamplesOut[1] = riceParamPart1 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 1);
+            pSamplesOut[2] = riceParamPart2 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 2);
+            pSamplesOut[3] = riceParamPart3 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 3);
 
             pSamplesOut += 4;
         }
@@ -3730,10 +3792,10 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar(drflac_b
             riceParamPart2  = (riceParamPart2 >> 1) ^ t[riceParamPart2 & 0x01];
             riceParamPart3  = (riceParamPart3 >> 1) ^ t[riceParamPart3 & 0x01];
 
-            pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_32(order, shift, coefficients, pSamplesOut + 0);
-            pSamplesOut[1] = riceParamPart1 + drflac__calculate_prediction_32(order, shift, coefficients, pSamplesOut + 1);
-            pSamplesOut[2] = riceParamPart2 + drflac__calculate_prediction_32(order, shift, coefficients, pSamplesOut + 2);
-            pSamplesOut[3] = riceParamPart3 + drflac__calculate_prediction_32(order, shift, coefficients, pSamplesOut + 3);
+            pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 0);
+            pSamplesOut[1] = riceParamPart1 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 1);
+            pSamplesOut[2] = riceParamPart2 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 2);
+            pSamplesOut[3] = riceParamPart3 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 3);
 
             pSamplesOut += 4;
         }
@@ -3753,10 +3815,10 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar(drflac_b
         /*riceParamPart0  = (riceParamPart0 >> 1) ^ (~(riceParamPart0 & 0x01) + 1);*/
 
         /* Sample reconstruction. */
-        if (bitsPerSample+shift > 32) {
-            pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_64(order, shift, coefficients, pSamplesOut + 0);
+        if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) {
+            pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 0);
         } else {
-            pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_32(order, shift, coefficients, pSamplesOut + 0);
+            pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 0);
         }
 
         i += 1;
@@ -4212,20 +4274,20 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__sse41_64(drflac
     return DRFLAC_TRUE;
 }
 
-static drflac_bool32 drflac__decode_samples_with_residual__rice__sse41(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
+static drflac_bool32 drflac__decode_samples_with_residual__rice__sse41(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
 {
     DRFLAC_ASSERT(bs != NULL);
     DRFLAC_ASSERT(pSamplesOut != NULL);
 
     /* In my testing the order is rarely > 12, so in this case I'm going to simplify the SSE implementation by only handling order <= 12. */
-    if (order > 0 && order <= 12) {
-        if (bitsPerSample+shift > 32) {
-            return drflac__decode_samples_with_residual__rice__sse41_64(bs, count, riceParam, order, shift, coefficients, pSamplesOut);
+    if (lpcOrder > 0 && lpcOrder <= 12) {
+        if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) {
+            return drflac__decode_samples_with_residual__rice__sse41_64(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut);
         } else {
-            return drflac__decode_samples_with_residual__rice__sse41_32(bs, count, riceParam, order, shift, coefficients, pSamplesOut);
+            return drflac__decode_samples_with_residual__rice__sse41_32(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut);
         }
     } else {
-        return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, order, shift, coefficients, pSamplesOut);
+        return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut);
     }
 }
 #endif
@@ -4562,7 +4624,7 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__neon_64(drflac_
 
     /*
     Pre-loading the coefficients and prior samples is annoying because we need to ensure we don't try reading more than
-    what's available in the input buffers. It would be conenient to use a fall-through switch to do this, but this results
+    what's available in the input buffers. It would be convenient to use a fall-through switch to do this, but this results
     in strict aliasing warnings with GCC. To work around this I'm just doing something hacky. This feels a bit convoluted
     so I think there's opportunity for this to be simplified.
     */
@@ -4710,41 +4772,41 @@ static drflac_bool32 drflac__decode_samples_with_residual__rice__neon_64(drflac_
     return DRFLAC_TRUE;
 }
 
-static drflac_bool32 drflac__decode_samples_with_residual__rice__neon(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
+static drflac_bool32 drflac__decode_samples_with_residual__rice__neon(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
 {
     DRFLAC_ASSERT(bs != NULL);
     DRFLAC_ASSERT(pSamplesOut != NULL);
 
     /* In my testing the order is rarely > 12, so in this case I'm going to simplify the NEON implementation by only handling order <= 12. */
-    if (order > 0 && order <= 12) {
-        if (bitsPerSample+shift > 32) {
-            return drflac__decode_samples_with_residual__rice__neon_64(bs, count, riceParam, order, shift, coefficients, pSamplesOut);
+    if (lpcOrder > 0 && lpcOrder <= 12) {
+        if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) {
+            return drflac__decode_samples_with_residual__rice__neon_64(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut);
         } else {
-            return drflac__decode_samples_with_residual__rice__neon_32(bs, count, riceParam, order, shift, coefficients, pSamplesOut);
+            return drflac__decode_samples_with_residual__rice__neon_32(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut);
         }
     } else {
-        return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, order, shift, coefficients, pSamplesOut);
+        return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut);
     }
 }
 #endif
 
-static drflac_bool32 drflac__decode_samples_with_residual__rice(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
+static drflac_bool32 drflac__decode_samples_with_residual__rice(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
 {
 #if defined(DRFLAC_SUPPORT_SSE41)
     if (drflac__gIsSSE41Supported) {
-        return drflac__decode_samples_with_residual__rice__sse41(bs, bitsPerSample, count, riceParam, order, shift, coefficients, pSamplesOut);
+        return drflac__decode_samples_with_residual__rice__sse41(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut);
     } else
 #elif defined(DRFLAC_SUPPORT_NEON)
     if (drflac__gIsNEONSupported) {
-        return drflac__decode_samples_with_residual__rice__neon(bs, bitsPerSample, count, riceParam, order, shift, coefficients, pSamplesOut);
+        return drflac__decode_samples_with_residual__rice__neon(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut);
     } else
 #endif
     {
         /* Scalar fallback. */
     #if 0
-        return drflac__decode_samples_with_residual__rice__reference(bs, bitsPerSample, count, riceParam, order, shift, coefficients, pSamplesOut);
+        return drflac__decode_samples_with_residual__rice__reference(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut);
     #else
-        return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, order, shift, coefficients, pSamplesOut);
+        return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut);
     #endif
     }
 }
@@ -4765,7 +4827,10 @@ static drflac_bool32 drflac__read_and_seek_residual__rice(drflac_bs* bs, drflac_
     return DRFLAC_TRUE;
 }
 
-static drflac_bool32 drflac__decode_samples_with_residual__unencoded(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 unencodedBitsPerSample, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
+#if defined(__clang__)
+__attribute__((no_sanitize("signed-integer-overflow")))
+#endif
+static drflac_bool32 drflac__decode_samples_with_residual__unencoded(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 unencodedBitsPerSample, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut)
 {
     drflac_uint32 i;
 
@@ -4782,10 +4847,10 @@ static drflac_bool32 drflac__decode_samples_with_residual__unencoded(drflac_bs*
             pSamplesOut[i] = 0;
         }
 
-        if (bitsPerSample >= 24) {
-            pSamplesOut[i] += drflac__calculate_prediction_64(order, shift, coefficients, pSamplesOut + i);
+        if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) {
+            pSamplesOut[i] += drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + i);
         } else {
-            pSamplesOut[i] += drflac__calculate_prediction_32(order, shift, coefficients, pSamplesOut + i);
+            pSamplesOut[i] += drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + i);
         }
     }
 
@@ -4798,7 +4863,7 @@ Reads and decodes the residual for the sub-frame the decoder is currently sittin
 when the decoder is sitting at the very start of the RESIDUAL block. The first <order> residuals will be ignored. The
 <blockSize> and <order> parameters are used to determine how many residual values need to be decoded.
 */
-static drflac_bool32 drflac__decode_samples_with_residual(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 blockSize, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pDecodedSamples)
+static drflac_bool32 drflac__decode_samples_with_residual(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 blockSize, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pDecodedSamples)
 {
     drflac_uint8 residualMethod;
     drflac_uint8 partitionOrder;
@@ -4818,7 +4883,7 @@ static drflac_bool32 drflac__decode_samples_with_residual(drflac_bs* bs, drflac_
     }
 
     /* Ignore the first <order> values. */
-    pDecodedSamples += order;
+    pDecodedSamples += lpcOrder;
 
     if (!drflac__read_uint8(bs, 4, &partitionOrder)) {
         return DRFLAC_FALSE;
@@ -4833,11 +4898,11 @@ static drflac_bool32 drflac__decode_samples_with_residual(drflac_bs* bs, drflac_
     }
 
     /* Validation check. */
-    if ((blockSize / (1 << partitionOrder)) < order) {
+    if ((blockSize / (1 << partitionOrder)) < lpcOrder) {
         return DRFLAC_FALSE;
     }
 
-    samplesInPartition = (blockSize / (1 << partitionOrder)) - order;
+    samplesInPartition = (blockSize / (1 << partitionOrder)) - lpcOrder;
     partitionsRemaining = (1 << partitionOrder);
     for (;;) {
         drflac_uint8 riceParam = 0;
@@ -4858,7 +4923,7 @@ static drflac_bool32 drflac__decode_samples_with_residual(drflac_bs* bs, drflac_
         }
 
         if (riceParam != 0xFF) {
-            if (!drflac__decode_samples_with_residual__rice(bs, bitsPerSample, samplesInPartition, riceParam, order, shift, coefficients, pDecodedSamples)) {
+            if (!drflac__decode_samples_with_residual__rice(bs, bitsPerSample, samplesInPartition, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pDecodedSamples)) {
                 return DRFLAC_FALSE;
             }
         } else {
@@ -4867,7 +4932,7 @@ static drflac_bool32 drflac__decode_samples_with_residual(drflac_bs* bs, drflac_
                 return DRFLAC_FALSE;
             }
 
-            if (!drflac__decode_samples_with_residual__unencoded(bs, bitsPerSample, samplesInPartition, unencodedBitsPerSample, order, shift, coefficients, pDecodedSamples)) {
+            if (!drflac__decode_samples_with_residual__unencoded(bs, bitsPerSample, samplesInPartition, unencodedBitsPerSample, lpcOrder, lpcShift, lpcPrecision, coefficients, pDecodedSamples)) {
                 return DRFLAC_FALSE;
             }
         }
@@ -5036,7 +5101,7 @@ static drflac_bool32 drflac__decode_samples__fixed(drflac_bs* bs, drflac_uint32
         pDecodedSamples[i] = sample;
     }
 
-    if (!drflac__decode_samples_with_residual(bs, subframeBitsPerSample, blockSize, lpcOrder, 0, lpcCoefficientsTable[lpcOrder], pDecodedSamples)) {
+    if (!drflac__decode_samples_with_residual(bs, subframeBitsPerSample, blockSize, lpcOrder, 0, 4, lpcCoefficientsTable[lpcOrder], pDecodedSamples)) {
         return DRFLAC_FALSE;
     }
 
@@ -5091,7 +5156,7 @@ static drflac_bool32 drflac__decode_samples__lpc(drflac_bs* bs, drflac_uint32 bl
         }
     }
 
-    if (!drflac__decode_samples_with_residual(bs, bitsPerSample, blockSize, lpcOrder, lpcShift, coefficients, pDecodedSamples)) {
+    if (!drflac__decode_samples_with_residual(bs, bitsPerSample, blockSize, lpcOrder, lpcShift, lpcPrecision, coefficients, pDecodedSamples)) {
         return DRFLAC_FALSE;
     }
 
@@ -5219,6 +5284,9 @@ static drflac_bool32 drflac__read_next_flac_frame_header(drflac_bs* bs, drflac_u
                 return DRFLAC_FALSE;
             }
             crc8 = drflac_crc8(crc8, header->blockSizeInPCMFrames, 16);
+            if (header->blockSizeInPCMFrames == 0xFFFF) {
+                return DRFLAC_FALSE;    /* Frame is too big. This is the size of the frame minus 1. The STREAMINFO block defines the max block size which is 16-bits. Adding one will make it 17 bits and therefore too big. */
+            }
             header->blockSizeInPCMFrames += 1;
         } else {
             DRFLAC_ASSERT(blockSize >= 8);
@@ -5257,6 +5325,11 @@ static drflac_bool32 drflac__read_next_flac_frame_header(drflac_bs* bs, drflac_u
             header->bitsPerSample = streaminfoBitsPerSample;
         }
 
+        if (header->bitsPerSample != streaminfoBitsPerSample) {
+            /* If this subframe has a different bitsPerSample then streaminfo or the first frame, reject it */
+            return DRFLAC_FALSE;
+        }
+
         if (!drflac__read_uint8(bs, 8, &header->crc8)) {
             return DRFLAC_FALSE;
         }
@@ -5343,6 +5416,11 @@ static drflac_bool32 drflac__decode_subframe(drflac_bs* bs, drflac_frame* frame,
         subframeBitsPerSample += 1;
     }
 
+    if (subframeBitsPerSample > 32) {
+        /* libFLAC and ffmpeg reject 33-bit subframes as well */
+        return DRFLAC_FALSE;
+    }
+
     /* Need to handle wasted bits per sample. */
     if (pSubframe->wastedBitsPerSample >= subframeBitsPerSample) {
         return DRFLAC_FALSE;
@@ -6485,7 +6563,7 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d
                     pRunningData    = (const char*)pRawData;
                     pRunningDataEnd = (const char*)pRawData + blockSize;
 
-                    metadata.data.vorbis_comment.vendorLength = drflac__le2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
+                    metadata.data.vorbis_comment.vendorLength = drflac__le2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
 
                     /* Need space for the rest of the block */
                     if ((pRunningDataEnd - pRunningData) - 4 < (drflac_int64)metadata.data.vorbis_comment.vendorLength) { /* <-- Note the order of operations to avoid overflow to a valid value */
@@ -6493,7 +6571,7 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d
                         return DRFLAC_FALSE;
                     }
                     metadata.data.vorbis_comment.vendor       = pRunningData;                                            pRunningData += metadata.data.vorbis_comment.vendorLength;
-                    metadata.data.vorbis_comment.commentCount = drflac__le2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
+                    metadata.data.vorbis_comment.commentCount = drflac__le2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
 
                     /* Need space for 'commentCount' comments after the block, which at minimum is a drflac_uint32 per comment */
                     if ((pRunningDataEnd - pRunningData) / sizeof(drflac_uint32) < metadata.data.vorbis_comment.commentCount) { /* <-- Note the order of operations to avoid overflow to a valid value */
@@ -6511,7 +6589,7 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d
                             return DRFLAC_FALSE;
                         }
 
-                        commentLength = drflac__le2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
+                        commentLength = drflac__le2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
                         if (pRunningDataEnd - pRunningData < (drflac_int64)commentLength) { /* <-- Note the order of operations to avoid overflow to a valid value */
                             drflac__free_from_callbacks(pRawData, pAllocationCallbacks);
                             return DRFLAC_FALSE;
@@ -6620,8 +6698,8 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d
                     pRunningData    = (const char*)pRawData;
                     pRunningDataEnd = (const char*)pRawData + blockSize;
 
-                    metadata.data.picture.type       = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
-                    metadata.data.picture.mimeLength = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
+                    metadata.data.picture.type       = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
+                    metadata.data.picture.mimeLength = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
 
                     /* Need space for the rest of the block */
                     if ((pRunningDataEnd - pRunningData) - 24 < (drflac_int64)metadata.data.picture.mimeLength) { /* <-- Note the order of operations to avoid overflow to a valid value */
@@ -6629,7 +6707,7 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d
                         return DRFLAC_FALSE;
                     }
                     metadata.data.picture.mime              = pRunningData;                                            pRunningData += metadata.data.picture.mimeLength;
-                    metadata.data.picture.descriptionLength = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
+                    metadata.data.picture.descriptionLength = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
 
                     /* Need space for the rest of the block */
                     if ((pRunningDataEnd - pRunningData) - 20 < (drflac_int64)metadata.data.picture.descriptionLength) { /* <-- Note the order of operations to avoid overflow to a valid value */
@@ -6637,11 +6715,11 @@ static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, d
                         return DRFLAC_FALSE;
                     }
                     metadata.data.picture.description     = pRunningData;                                            pRunningData += metadata.data.picture.descriptionLength;
-                    metadata.data.picture.width           = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
-                    metadata.data.picture.height          = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
-                    metadata.data.picture.colorDepth      = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
-                    metadata.data.picture.indexColorCount = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
-                    metadata.data.picture.pictureDataSize = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4;
+                    metadata.data.picture.width           = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
+                    metadata.data.picture.height          = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
+                    metadata.data.picture.colorDepth      = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
+                    metadata.data.picture.indexColorCount = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
+                    metadata.data.picture.pictureDataSize = drflac__be2host_32_ptr_unaligned(pRunningData); pRunningData += 4;
                     metadata.data.picture.pPictureData    = (const drflac_uint8*)pRunningData;
 
                     /* Need space for the picture after the block */
@@ -7865,7 +7943,7 @@ static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac
 #ifndef DR_FLAC_NO_OGG
     if (init.container == drflac_container_ogg) {
         drflac_oggbs* pInternalOggbs = (drflac_oggbs*)((drflac_uint8*)pFlac->pDecodedSamples + decodedSamplesAllocationSize + seektableSize);
-        *pInternalOggbs = oggbs;
+        DRFLAC_COPY_MEMORY(pInternalOggbs, &oggbs, sizeof(oggbs));
 
         /* The Ogg bistream needs to be layered on top of the original bitstream. */
         pFlac->bs.onRead = drflac__on_read_ogg;
@@ -11786,7 +11864,7 @@ DRFLAC_API const char* drflac_next_vorbis_comment(drflac_vorbis_comment_iterator
         return NULL;
     }
 
-    length = drflac__le2host_32(*(const drflac_uint32*)pIter->pRunningData);
+    length = drflac__le2host_32_ptr_unaligned(pIter->pRunningData);
     pIter->pRunningData += 4;
 
     pComment = pIter->pRunningData;
@@ -11856,6 +11934,22 @@ DRFLAC_API drflac_bool32 drflac_next_cuesheet_track(drflac_cuesheet_track_iterat
 /*
 REVISION HISTORY
 ================
+v0.12.38 - 2022-04-10
+  - Fix compilation error on older versions of GCC.
+
+v0.12.37 - 2022-02-12
+  - Improve ARM detection.
+
+v0.12.36 - 2022-02-07
+  - Fix a compilation error with the ARM build.
+
+v0.12.35 - 2022-02-06
+  - Fix a bug due to underestimating the amount of precision required for the prediction stage.
+  - Fix some bugs found from fuzz testing.
+
+v0.12.34 - 2022-01-07
+  - Fix some misalignment bugs when reading metadata.
+
 v0.12.33 - 2021-12-22
   - Fix a bug with seeking when the seek table does not start at PCM frame 0.
 

+ 5 - 0
src/modules/audio/Audio.cpp

@@ -78,6 +78,11 @@ bool Audio::setMixWithSystem(bool mix)
 #endif
 }
 
+void Audio::setPlaybackDevice(const char */*name*/)
+{
+	throw love::Exception("Re-setting output device is not supported.");
+}
+
 StringMap<Audio::DistanceModel, Audio::DISTANCE_MAX_ENUM>::Entry Audio::distanceModelEntries[] =
 {
 	{"none", Audio::DISTANCE_NONE},

+ 15 - 0
src/modules/audio/Audio.h

@@ -297,6 +297,21 @@ public:
 	virtual void pauseContext() = 0;
 	virtual void resumeContext() = 0;
 
+	/**
+	 * Get current playback device name.
+	 */
+	virtual std::string getPlaybackDevice() = 0;
+
+	/**
+	 * Retrieve list of available playback devices.
+	 */
+	virtual void getPlaybackDevices(std::vector<std::string> &list) = 0;
+
+	/**
+	 * Set the current playback device to specified device name.
+	 */
+	virtual void setPlaybackDevice(const char *name);
+
 private:
 
 	static StringMap<DistanceModel, DISTANCE_MAX_ENUM>::Entry distanceModelEntries[];

+ 9 - 0
src/modules/audio/null/Audio.cpp

@@ -211,6 +211,15 @@ void Audio::resumeContext()
 {
 }
 
+std::string Audio::getPlaybackDevice()
+{
+	return "";
+}
+
+void Audio::getPlaybackDevices(std::vector<std::string> &/*list*/)
+{
+}
+
 
 } // null
 } // audio

+ 3 - 0
src/modules/audio/null/Audio.h

@@ -89,6 +89,9 @@ public:
 	void pauseContext();
 	void resumeContext();
 
+	std::string getPlaybackDevice();
+	void getPlaybackDevices(std::vector<std::string> &list);
+
 private:
 	float volume;
 	DistanceModel distanceModel;

+ 65 - 5
src/modules/audio/openal/Audio.cpp

@@ -93,6 +93,18 @@ ALenum Audio::getFormat(int bitDepth, int channels)
 	return AL_NONE;
 }
 
+static const char *getDeviceSpecifier(ALCdevice *device)
+{
+#ifndef ALC_ALL_DEVICES_SPECIFIER
+	constexpr ALCenum ALC_ALL_DEVICES_SPECIFIER = 0x1013;
+#endif
+	static ALCenum deviceEnum = alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT") == ALC_TRUE
+		? ALC_ALL_DEVICES_SPECIFIER
+		: ALC_DEVICE_SPECIFIER;
+
+	return alcGetString(device, deviceEnum);
+}
+
 Audio::Audio()
 	: device(nullptr)
 	, context(nullptr)
@@ -100,6 +112,9 @@ Audio::Audio()
 	, poolThread(nullptr)
 	, distanceModel(DISTANCE_INVERSE_CLAMPED)
 {
+	attribs.push_back(0);
+	attribs.push_back(0);
+
 	// Before opening new device, check if recording
 	// is requested.
 	if (getRequestRecordingPermission())
@@ -122,12 +137,11 @@ Audio::Audio()
 			throw love::Exception("Could not open device.");
 
 #ifdef ALC_EXT_EFX
-		ALint attribs[4] = { ALC_MAX_AUXILIARY_SENDS, MAX_SOURCE_EFFECTS, 0, 0 };
-#else
-		ALint *attribs = nullptr;
+		attribs.insert(attribs.begin(), ALC_MAX_AUXILIARY_SENDS);
+		attribs.insert(attribs.begin() + 1, MAX_SOURCE_EFFECTS);
 #endif
 
-		context = alcCreateContext(device, attribs);
+		context = alcCreateContext(device, attribs.data());
 
 		if (context == nullptr)
 			throw love::Exception("Could not create context.");
@@ -165,7 +179,7 @@ Audio::Audio()
 
 	try
 	{
-		pool = new Pool();
+		pool = new Pool(device);
 	}
 	catch (love::Exception &)
 	{
@@ -314,6 +328,52 @@ void Audio::resumeContext()
 		alcMakeContextCurrent(context);
 }
 
+std::string Audio::getPlaybackDevice()
+{
+	const char *dev = getDeviceSpecifier(device);
+
+	if (dev == nullptr)
+		throw Exception("Failed to get current device: %s", alcGetString(device, alcGetError(device)));
+
+	return dev;
+}
+
+void Audio::getPlaybackDevices(std::vector<std::string> &list)
+{
+	const char *devices = getDeviceSpecifier(nullptr);
+
+	if (devices == nullptr)
+		throw Exception("Failed to enumerate devices: %s", alcGetString(nullptr, alcGetError(nullptr)));
+
+	for (const char *device = devices; *device; device++)
+	{
+		list.emplace_back(device);
+		device += list.back().length();
+	}
+}
+
+void Audio::setPlaybackDevice(const char* name)
+{
+#ifndef ALC_SOFT_reopen_device
+	typedef ALCboolean (ALC_APIENTRY*LPALCREOPENDEVICESOFT)(ALCdevice *device,
+		const ALCchar *deviceName, const ALCint *attribs);
+#endif
+	static LPALCREOPENDEVICESOFT alcReopenDeviceSOFT = alcIsExtensionPresent(device, "ALC_SOFT_reopen_device") == ALC_TRUE
+		? (LPALCREOPENDEVICESOFT) alcGetProcAddress(device, "alcReopenDeviceSOFT")
+		: nullptr;
+
+	if (alcReopenDeviceSOFT == nullptr)
+	{
+		// Default implementation throws exception. To make
+		// error message consistent, call the base class.
+		love::audio::Audio::setPlaybackDevice(name);
+		return;
+	}
+
+	if (alcReopenDeviceSOFT(device, (const ALCchar *) name, attribs.data()) == ALC_FALSE)
+		throw love::Exception("Cannot set output device: %s", alcGetString(device, alcGetError(device)));
+}
+
 void Audio::setVolume(float volume)
 {
 	alListenerf(AL_GAIN, volume);

+ 5 - 0
src/modules/audio/openal/Audio.h

@@ -127,6 +127,10 @@ public:
 
 	bool getEffectID(const char *name, ALuint &id);
 
+	std::string getPlaybackDevice();
+	void getPlaybackDevices(std::vector<std::string> &list);
+	void setPlaybackDevice(const char *name);
+
 private:
 	void initializeEFX();
 	// The OpenAL device.
@@ -137,6 +141,7 @@ private:
 
 	// The OpenAL context.
 	ALCcontext *context;
+	std::vector<ALCint> attribs;
 
 	// The OpenAL effects
 	struct EffectMapStorage

+ 50 - 2
src/modules/audio/openal/Pool.cpp

@@ -20,6 +20,7 @@
 
 #include "Pool.h"
 
+#include "event/Event.h"
 #include "Source.h"
 
 namespace love
@@ -29,8 +30,20 @@ namespace audio
 namespace openal
 {
 
-Pool::Pool()
-	: sources()
+static Variant::SharedTable *putSourcesAsSharedTable(std::vector<audio::Source *> &sources)
+{
+	Variant::SharedTable *table = new Variant::SharedTable();
+
+	for (int i = 0; i < sources.size(); i++)
+		table->pairs.emplace_back((double) (i + 1), Variant(&Source::type, sources[i]));
+
+	return table;
+}
+
+Pool::Pool(ALCdevice *device)
+	: device(device)
+	, sources()
+	, disconnectNotified(false)
 	, totalSources(0)
 {
 	// Clear errors.
@@ -101,8 +114,43 @@ bool Pool::isPlaying(Source *s)
 
 void Pool::update()
 {
+#ifndef ALC_CONNECTED
+	constexpr ALCenum ALC_CONNECTED = 0x313;
+#endif
+
 	thread::Lock lock(mutex);
 
+	static bool disconnectExtSupported = alcIsExtensionPresent(device, "ALC_EXT_Disconnect") == ALC_TRUE;
+
+	// Device disconnection event
+	if (disconnectExtSupported)
+	{
+		auto eventModule = Module::getInstance<event::Event>(Module::M_EVENT);
+		if (eventModule)
+		{
+			ALCint connected;
+			alcGetIntegerv(device, ALC_CONNECTED, 1, &connected);
+
+			if (connected)
+				disconnectNotified = false;
+			else if (!disconnectNotified)
+			{
+				// Get all sources in this Pool then stop it
+				// since they're all internally stopped.
+				std::vector<audio::Source *> sources = getPlayingSources();
+				Source::stop(sources);
+
+				std::vector<Variant> vargs;
+				vargs.emplace_back(putSourcesAsSharedTable(sources));
+
+				StrongRef<event::Message> msg(new event::Message("audiodisconnected", vargs), Acquire::NORETAIN);
+				eventModule->push(msg);
+
+				disconnectNotified = true;
+			}
+		}
+	}
+
 	std::vector<Source *> torelease;
 
 	for (const auto &i : playing)

+ 7 - 1
src/modules/audio/openal/Pool.h

@@ -64,7 +64,7 @@ class Pool
 {
 public:
 
-	Pool();
+	Pool(ALCdevice *device);
 	~Pool();
 
 	/**
@@ -101,9 +101,15 @@ private:
 	// Maximum possible number of OpenAL sources the pool attempts to generate.
 	static const int MAX_SOURCES = 64;
 
+	// Current OpenAL device
+	ALCdevice *device;
+
 	// OpenAL sources
 	ALuint sources[MAX_SOURCES];
 
+	// Is device disconnection has been notified?
+	bool disconnectNotified;
+
 	// Total number of created sources in the pool.
 	int totalSources;
 

+ 73 - 21
src/modules/audio/wrap_Audio.cpp

@@ -48,32 +48,35 @@ int w_newSource(lua_State *L)
 {
 	Source::Type stype = Source::TYPE_STREAM;
 
-	if (!luax_istype(L, 1, love::sound::SoundData::type) && !luax_istype(L, 1, love::sound::Decoder::type))
+	if (!luax_istype(L, 1, love::sound::SoundData::type))
 	{
-		const char *stypestr = luaL_checkstring(L, 2);
-		if (stypestr && !Source::getConstant(stypestr, stype))
-			return luax_enumerror(L, "source type", Source::getConstants(stype), stypestr);
+		if (!luax_istype(L, 1, love::sound::Decoder::type))
+		{
+			const char *stypestr = luaL_checkstring(L, 2);
+			if (stypestr && !Source::getConstant(stypestr, stype))
+				return luax_enumerror(L, "source type", Source::getConstants(stype), stypestr);
 
-		if (stype == Source::TYPE_QUEUE)
-			return luaL_error(L, "Cannot create queueable sources using newSource. Use newQueueableSource instead.");
-	}
+			if (stype == Source::TYPE_QUEUE)
+				return luaL_error(L, "Cannot create queueable sources using newSource. Use newQueueableSource instead.");
+		}
 
-	if (love::filesystem::luax_cangetdata(L, 1))
-	{
-		// stream type
-		if (stype == Source::TYPE_STATIC)
-			lua_pushstring(L, "memory");
-		else if (!lua_isnone(L, 3))
-			lua_pushvalue(L, 3);
-		else
+		if (love::filesystem::luax_cangetdata(L, 1))
+		{
+			// stream type
+			if (stype == Source::TYPE_STATIC)
+				lua_pushstring(L, "memory");
+			else if (!lua_isnone(L, 3))
+				lua_pushvalue(L, 3);
+			else
+				lua_pushnil(L);
+
+			// buffer size
 			lua_pushnil(L);
 
-		// buffer size
-		lua_pushnil(L);
-
-		// (file, buffer size, stream type)
-		int idxs[] = { 1, lua_gettop(L), lua_gettop(L) - 1 };
-		luax_convobj(L, idxs, 3, "sound", "newDecoder");
+			// (file, buffer size, stream type)
+			int idxs[] = { 1, lua_gettop(L), lua_gettop(L) - 1 };
+			luax_convobj(L, idxs, 3, "sound", "newDecoder");
+		}
 	}
 
 	if (stype == Source::TYPE_STATIC && luax_istype(L, 1, love::sound::Decoder::type))
@@ -543,6 +546,52 @@ int w_setMixWithSystem(lua_State *L)
 	return 1;
 }
 
+int w_getPlaybackDevice(lua_State* L)
+{
+	std::string device;
+
+	luax_catchexcept(L, [&]() { device = instance()->getPlaybackDevice(); });
+	luax_pushstring(L, device);
+	return 1;
+}
+
+int w_getPlaybackDevices(lua_State* L)
+{
+	std::vector<std::string> list;
+
+	luax_catchexcept(L, [&]() { instance()->getPlaybackDevices(list); });
+	lua_createtable(L, 0, (int) list.size());
+	for (int i = 0; i < (int) list.size(); i++)
+	{
+		lua_pushnumber(L, i + 1);
+		lua_pushstring(L, list[i].c_str());
+		lua_rawset(L, -3);
+	}
+
+	return 1;
+}
+
+int w_setPlaybackDevice(lua_State* L)
+{
+	const char *device = luaL_optstring(L, 1, nullptr);
+
+	try
+	{
+		instance()->setPlaybackDevice(device);
+		luax_pushboolean(L, true);
+		return 1;
+	}
+	catch (love::Exception& e)
+	{
+		luax_pushboolean(L, false);
+		lua_pushstring(L, e.what());
+		return 2;
+	}
+
+	// To avoid compiler warning
+	return 0;
+}
+
 // List of functions to wrap.
 static const luaL_Reg functions[] =
 {
@@ -574,6 +623,9 @@ static const luaL_Reg functions[] =
 	{ "getMaxSourceEffects", w_getMaxSourceEffects },
 	{ "isEffectsSupported", w_isEffectsSupported },
 	{ "setMixWithSystem", w_setMixWithSystem },
+	{ "getPlaybackDevice", w_getPlaybackDevice },
+	{ "getPlaybackDevices", w_getPlaybackDevices },
+	{ "setPlaybackDevice", w_setPlaybackDevice },
 
 	{ 0, 0 }
 };

+ 20 - 1
src/modules/filesystem/NativeFile.cpp

@@ -22,6 +22,10 @@
 #include "NativeFile.h"
 #include "common/utf8.h"
 
+#ifdef LOVE_ANDROID
+#include "common/android.h"
+#endif
+
 // Assume POSIX or Visual Studio.
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -84,7 +88,22 @@ bool NativeFile::open(Mode newmode)
 	if (file != nullptr)
 		return false;
 
-#ifdef LOVE_WINDOWS
+#if defined(LOVE_ANDROID)
+	// Try to handle content:// URI
+	int fd = love::android::getFDFromContentProtocol(filename.c_str());
+	if (fd != -1)
+	{
+		if (newmode != MODE_READ)
+		{
+			::close(fd);
+			throw love::Exception("%s is read-only.", filename.c_str());
+		}
+
+		file = fdopen(fd, "rb");
+	}
+	else
+		file = fopen(filename.c_str(), getModeString(newmode));
+#elif defined(LOVE_WINDOWS)
 	// make sure non-ASCII filenames work.
 	std::wstring modestr = to_widestr(getModeString(newmode));
 	std::wstring wfilename = to_widestr(filename);

+ 10 - 26
src/modules/filesystem/physfs/Filesystem.cpp

@@ -236,8 +236,6 @@ bool Filesystem::setSource(const char *source)
 	if (!love::android::createStorageDirectories())
 		SDL_Log("Error creating storage directories!");
 
-	new_search_path = "";
-
 	PHYSFS_Io *gameLoveIO;
 	bool hasFusedGame = love::android::checkFusedGame((void **) &gameLoveIO);
 	bool isAAssetMounted = false;
@@ -263,38 +261,24 @@ bool Filesystem::setSource(const char *source)
 
 	if (!isAAssetMounted)
 	{
-		new_search_path = love::android::getSelectedGameFile();
-
-		// try mounting first, if that fails, load to memory and mount
-		if (!PHYSFS_mount(new_search_path.c_str(), nullptr, 1))
+		// Is this love2d://fd/ URIs?
+		int fd = love::android::getFDFromLoveProtocol(new_search_path.c_str());
+		if (fd != -1)
 		{
-			// PHYSFS cannot yet mount a zip file inside an .apk
-			SDL_Log("Mounting %s did not work. Loading to memory.",
-					new_search_path.c_str());
-			char* game_archive_ptr = NULL;
-			size_t game_archive_size = 0;
-			if (!love::android::loadGameArchiveToMemory(
-						new_search_path.c_str(), &game_archive_ptr,
-						&game_archive_size))
+			PHYSFS_Io *io = (PHYSFS_Io *) love::android::getIOFromFD(fd);
+
+			if (PHYSFS_mountIo(io, "LOVE.FD", nullptr, 0))
 			{
-				SDL_Log("Failure memory loading archive %s", new_search_path.c_str());
-				return false;
-			}
-			if (!PHYSFS_mountMemory(
-					game_archive_ptr, game_archive_size,
-					love::android::freeGameArchiveMemory, "archive.zip", "/", 0))
-			{
-				SDL_Log("Failure mounting in-memory archive.");
-				love::android::freeGameArchiveMemory(game_archive_ptr);
-				return false;
+				gameSource = new_search_path;
+				return true;
 			}
 		}
 	}
-#else
+#endif
+
 	// Add the directory.
 	if (!PHYSFS_mount(new_search_path.c_str(), nullptr, 1))
 		return false;
-#endif
 
 	// Save the game source.
 	gameSource = new_search_path;

+ 26 - 30
src/modules/filesystem/wrap_Filesystem.cpp

@@ -305,7 +305,7 @@ File *luax_getfile(lua_State *L, int idx)
 	return file;
 }
 
-FileData *luax_getfiledata(lua_State *L, int idx)
+FileData *luax_getfiledata(lua_State *L, int idx, bool ioerror)
 {
 	FileData *data = nullptr;
 	File *file = nullptr;
@@ -325,18 +325,33 @@ FileData *luax_getfiledata(lua_State *L, int idx)
 		luaL_argerror(L, idx, "filename, File, or FileData expected");
 		return nullptr; // Never reached.
 	}
-
-	if (file)
+	else if (file && !data)
 	{
-		luax_catchexcept(L,
-			[&]() { data = file->read(); },
-			[&](bool) { file->release(); }
-		);
+		try
+		{
+			data = file->read();
+		}
+		catch (love::Exception &e)
+		{
+			file->release();
+			if (ioerror)
+				luax_ioError(L, "%s", e.what());
+			else
+				luaL_error(L, "%s", e.what());
+			return nullptr; // Never reached.
+		}
+
+		file->release();
 	}
 
 	return data;
 }
 
+FileData *luax_getfiledata(lua_State *L, int idx)
+{
+	return luax_getfiledata(L, idx, false);
+}
+
 Data *luax_getdata(lua_State *L, int idx)
 {
 	Data *data = nullptr;
@@ -389,29 +404,10 @@ int w_newFileData(lua_State *L)
 	// Single argument: treat as filepath or File.
 	if (lua_gettop(L) == 1)
 	{
-		// We don't use luax_getfiledata because we want to use an ioError.
-		if (lua_isstring(L, 1))
-			luax_convobj(L, 1, "filesystem", "newFile");
-
-		// Get FileData from the File.
-		if (luax_istype(L, 1, File::type))
-		{
-			File *file = luax_checkfile(L, 1);
-
-			StrongRef<FileData> data;
-			try
-			{
-				data.set(file->read(), Acquire::NORETAIN);
-			}
-			catch (love::Exception &e)
-			{
-				return luax_ioError(L, "%s", e.what());
-			}
-			luax_pushtype(L, data);
-			return 1;
-		}
-		else
-			return luaL_argerror(L, 1, "filename or File expected");
+		FileData *data = luax_getfiledata(L, 1, true);
+		luax_pushtype(L, data);
+		data->release();
+		return 1;
 	}
 
 	size_t length = 0;

+ 3 - 2
src/modules/graphics/Buffer.cpp

@@ -36,6 +36,7 @@ Buffer::Buffer(Graphics *gfx, const Settings &settings, const std::vector<DataDe
 	, usageFlags(settings.usageFlags)
 	, dataUsage(settings.dataUsage)
 	, mapped(false)
+	, mappedType(MAP_WRITE_INVALIDATE)
 	, immutable(false)
 {
 	if (size == 0 && arraylength == 0)
@@ -61,8 +62,8 @@ Buffer::Buffer(Graphics *gfx, const Settings &settings, const std::vector<DataDe
 	if (storagebuffer && dataUsage == BUFFERDATAUSAGE_STREAM)
 		throw love::Exception("Buffers created with 'stream' data usage cannot be used as a shader storage buffer.");
 
-	if (dataUsage == BUFFERDATAUSAGE_STAGING && (indexbuffer || vertexbuffer || texelbuffer || storagebuffer))
-		throw love::Exception("Buffers created with 'staging' data usage cannot be index, vertex, texel, or shaderstorage buffer types.");
+	if (dataUsage == BUFFERDATAUSAGE_READBACK && (indexbuffer || vertexbuffer || texelbuffer || storagebuffer))
+		throw love::Exception("Buffers created with 'readback' data usage cannot be index, vertex, texel, or shaderstorage buffer types.");
 
 	size_t offset = 0;
 	size_t stride = 0;

+ 5 - 4
src/modules/graphics/Buffer.h

@@ -53,6 +53,7 @@ public:
 	enum MapType
 	{
 		MAP_WRITE_INVALIDATE,
+		MAP_READ_ONLY,
 	};
 
 	struct DataDeclaration
@@ -128,7 +129,7 @@ public:
 	/**
 	 * Fill a portion of the buffer with data.
 	 */
-	virtual void fill(size_t offset, size_t size, const void *data) = 0;
+	virtual bool fill(size_t offset, size_t size, const void *data) = 0;
 
 	/**
 	 * Copy a portion of this Buffer's data to another buffer, using the GPU.
@@ -147,10 +148,10 @@ public:
 	{
 	public:
 
-		Mapper(Buffer &buffer)
+		Mapper(Buffer &buffer, MapType maptype = MAP_WRITE_INVALIDATE)
 			: buffer(buffer)
 		{
-			data = buffer.map(MAP_WRITE_INVALIDATE, 0, buffer.getSize());
+			data = buffer.map(maptype, 0, buffer.getSize());
 		}
 
 		~Mapper()
@@ -179,7 +180,7 @@ protected:
 	BufferDataUsage dataUsage;
 
 	bool mapped;
-
+	MapType mappedType;
 	bool immutable;
 	
 }; // Buffer

+ 166 - 1
src/modules/graphics/Graphics.cpp

@@ -240,6 +240,9 @@ Graphics::~Graphics()
 	for (int i = 0; i < (int) SHADERSTAGE_MAX_ENUM; i++)
 		cachedShaderStages[i].clear();
 
+	pendingReadbacks.clear();
+	clearTemporaryResources();
+
 	Shader::deinitialize();
 }
 
@@ -438,6 +441,46 @@ love::graphics::Text *Graphics::newText(graphics::Font *font, const std::vector<
 	return new Text(font, text);
 }
 
+love::data::ByteData *Graphics::readbackBuffer(Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset)
+{
+	StrongRef<GraphicsReadback> readback;
+	readback.set(newReadbackInternal(READBACK_IMMEDIATE, buffer, offset, size, dest, destoffset), Acquire::NORETAIN);
+
+	auto data = readback->getBufferData();
+	if (data == nullptr)
+		throw love::Exception("love.graphics.readbackBuffer failed.");
+
+	data->retain();
+	return data;
+}
+
+GraphicsReadback *Graphics::readbackBufferAsync(Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset)
+{
+	auto readback = newReadbackInternal(READBACK_ASYNC, buffer, offset, size, dest, destoffset);
+	pendingReadbacks.push_back(readback);
+	return readback;
+}
+
+image::ImageData *Graphics::readbackTexture(Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty)
+{
+	StrongRef<GraphicsReadback> readback;
+	readback.set(newReadbackInternal(READBACK_IMMEDIATE, texture, slice, mipmap, rect, dest, destx, desty), Acquire::NORETAIN);
+
+	auto imagedata = readback->getImageData();
+	if (imagedata == nullptr)
+		throw love::Exception("love.graphics.readbackTexture failed.");
+
+	imagedata->retain();
+	return imagedata;
+}
+
+GraphicsReadback *Graphics::readbackTextureAsync(Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty)
+{
+	auto readback = newReadbackInternal(READBACK_ASYNC, texture, slice, mipmap, rect, dest, destx, desty);
+	pendingReadbacks.push_back(readback);
+	return readback;
+}
+
 void Graphics::cleanupCachedShaderStage(ShaderStageType type, const std::string &hashkey)
 {
 	cachedShaderStages[type].erase(hashkey);
@@ -893,6 +936,10 @@ void Graphics::setRenderTargets(const RenderTargets &rts)
 		realRTs.depthStencil.texture = getTemporaryTexture(dsformat, pixelw, pixelh, reqmsaa);
 		realRTs.depthStencil.slice = 0;
 
+		// TODO: fix this to call release at the right time.
+		// This only works here because nothing else calls getTemporaryTexture.
+		releaseTemporaryTexture(realRTs.depthStencil.texture);
+
 		setRenderTargetsInternal(realRTs, pixelw, pixelh, hasSRGBtexture);
 	}
 	else
@@ -995,12 +1042,15 @@ Texture *Graphics::getTemporaryTexture(PixelFormat format, int w, int h, int sam
 
 	for (TemporaryTexture &temp : temporaryTextures)
 	{
+		if (temp.framesSinceUse < 0)
+			continue;
+
 		Texture *c = temp.texture;
 		if (c->getPixelFormat() == format && c->getPixelWidth() == w
 			&& c->getPixelHeight() == h && c->getRequestedMSAA() == samples)
 		{
 			texture = c;
-			temp.framesSinceUse = 0;
+			temp.framesSinceUse = -1;
 			break;
 		}
 	}
@@ -1022,6 +1072,115 @@ Texture *Graphics::getTemporaryTexture(PixelFormat format, int w, int h, int sam
 	return texture;
 }
 
+void Graphics::releaseTemporaryTexture(Texture *texture)
+{
+	for (TemporaryTexture &temp : temporaryTextures)
+	{
+		if (temp.texture == texture)
+		{
+			temp.framesSinceUse = 0;
+			break;
+		}
+	}
+}
+
+Buffer *Graphics::getTemporaryBuffer(size_t size, DataFormat format, uint32 usageflags, BufferDataUsage datausage)
+{
+	Buffer *buffer = nullptr;
+
+	for (TemporaryBuffer &temp : temporaryBuffers)
+	{
+		if (temp.framesSinceUse < 0)
+			continue;
+
+		Buffer *b = temp.buffer;
+
+		if (temp.size == size && b->getDataMember(0).decl.format == format
+			&& b->getUsageFlags() == usageflags && b->getDataUsage() == datausage)
+		{
+			buffer = b;
+			temp.framesSinceUse = -1;
+			break;
+		}
+	}
+
+	if (buffer == nullptr)
+	{
+		Buffer::Settings settings(usageflags, datausage);
+		buffer = newBuffer(settings, format, nullptr, size, 0);
+
+		temporaryBuffers.emplace_back(buffer, size);
+	}
+
+	return buffer;
+}
+
+void Graphics::releaseTemporaryBuffer(Buffer *buffer)
+{
+	for (TemporaryBuffer &temp : temporaryBuffers)
+	{
+		if (temp.buffer == buffer)
+		{
+			temp.framesSinceUse = 0;
+			break;
+		}
+	}
+}
+
+void Graphics::updateTemporaryResources()
+{
+	for (int i = (int) temporaryTextures.size() - 1; i >= 0; i--)
+	{
+		auto &t = temporaryTextures[i];
+		if (t.framesSinceUse >= MAX_TEMPORARY_RESOURCE_UNUSED_FRAMES)
+		{
+			t.texture->release();
+			t = temporaryTextures.back();
+			temporaryTextures.pop_back();
+		}
+		else if (t.framesSinceUse >= 0)
+			t.framesSinceUse++;
+	}
+
+	for (int i = (int) temporaryBuffers.size() - 1; i >= 0; i--)
+	{
+		auto &t = temporaryBuffers[i];
+		if (t.framesSinceUse >= MAX_TEMPORARY_RESOURCE_UNUSED_FRAMES)
+		{
+			t.buffer->release();
+			t = temporaryBuffers.back();
+			temporaryBuffers.pop_back();
+		}
+		else if (t.framesSinceUse >= 0)
+			t.framesSinceUse++;
+	}
+}
+
+void Graphics::clearTemporaryResources()
+{
+	for (auto temp :temporaryBuffers)
+		temp.buffer->release();
+
+	for (auto temp : temporaryTextures)
+		temp.texture->release();
+
+	temporaryBuffers.clear();
+	temporaryTextures.clear();
+}
+
+void Graphics::updatePendingReadbacks()
+{
+	for (int i = (int)pendingReadbacks.size() - 1; i >= 0; i--)
+	{
+		pendingReadbacks[i]->update();
+		if (pendingReadbacks[i]->isComplete())
+		{
+			pendingReadbacks[i] = pendingReadbacks.back();
+			pendingReadbacks.pop_back();
+		}
+	}
+}
+
 void Graphics::intersectScissor(const Rect &rect)
 {
 	Rect currect = states.back().scissorRect;
@@ -1187,6 +1346,9 @@ void Graphics::copyBuffer(Buffer *source, Buffer *dest, size_t sourceoffset, siz
 	if (dest->getDataUsage() == BUFFERDATAUSAGE_STREAM)
 		throw love::Exception("Buffers created with 'stream' data usage cannot be used as a copy destination.");
 
+	if (source->getDataUsage() == BUFFERDATAUSAGE_READBACK)
+		throw love::Exception("Buffers created with 'readback' data usage cannot be used as a copy source.");
+
 	if (sourcerange.getMax() >= source->getSize())
 		throw love::Exception("Buffer copy source offset and size doesn't fit within the source Buffer's size.");
 
@@ -1296,6 +1458,9 @@ void Graphics::copyBufferToTexture(Buffer *source, Texture *dest, size_t sourceo
 	if (!capabilities.features[FEATURE_COPY_BUFFER_TO_TEXTURE])
 		throw love::Exception("Copying a Buffer to a Texture is not supported on this system.");
 
+	if (source->getDataUsage() == BUFFERDATAUSAGE_READBACK)
+		throw love::Exception("Buffers created with 'readback' data usage cannot be used as a copy source.");
+
 	PixelFormat format = dest->getPixelFormat();
 
 	if (isPixelFormatDepthStencil(format))

+ 37 - 3
src/modules/graphics/Graphics.h

@@ -37,6 +37,7 @@
 #include "Shader.h"
 #include "Quad.h"
 #include "Mesh.h"
+#include "GraphicsReadback.h"
 #include "Deprecations.h"
 #include "renderstate.h"
 #include "math/Transform.h"
@@ -461,6 +462,12 @@ public:
 
 	Text *newText(Font *font, const std::vector<Font::ColoredString> &text = {});
 
+	data::ByteData *readbackBuffer(Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset);
+	GraphicsReadback *readbackBufferAsync(Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset);
+
+	image::ImageData *readbackTexture(Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty);
+	GraphicsReadback *readbackTextureAsync(Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty);
+
 	bool validateShader(bool gles, const std::vector<std::string> &stages, const Shader::CompileOptions &options, std::string &err);
 
 	/**
@@ -857,6 +864,12 @@ public:
 
 	static void flushBatchedDrawsGlobal();
 
+	Texture *getTemporaryTexture(PixelFormat format, int w, int h, int samples);
+	void releaseTemporaryTexture(Texture *texture);
+
+	Buffer *getTemporaryBuffer(size_t size, DataFormat format, uint32 usageflags, BufferDataUsage datausage);
+	void releaseTemporaryBuffer(Buffer *buffer);
+
 	void cleanupCachedShaderStage(ShaderStageType type, const std::string &cachekey);
 
 	template <typename T>
@@ -955,6 +968,19 @@ protected:
 		}
 	};
 
+	struct TemporaryBuffer
+	{
+		Buffer *buffer;
+		size_t size;
+		int framesSinceUse;
+
+		TemporaryBuffer(Buffer *buf, size_t size)
+			: buffer(buf)
+			, size(size)
+			, framesSinceUse(-1)
+		{}
+	};
+
 	struct TemporaryTexture
 	{
 		Texture *texture;
@@ -962,7 +988,7 @@ protected:
 
 		TemporaryTexture(Texture *tex)
 			: texture(tex)
-			, framesSinceUse(0)
+			, framesSinceUse(-1)
 		{}
 	};
 
@@ -971,6 +997,9 @@ protected:
 	virtual Shader *newShaderInternal(StrongRef<ShaderStage> stages[SHADERSTAGE_MAX_ENUM]) = 0;
 	virtual StreamBuffer *newStreamBuffer(BufferUsage type, size_t size) = 0;
 
+	virtual GraphicsReadback *newReadbackInternal(ReadbackMethod method, Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset) = 0;
+	virtual GraphicsReadback *newReadbackInternal(ReadbackMethod method, Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty) = 0;
+
 	virtual bool dispatch(int x, int y, int z) = 0;
 
 	virtual void setRenderTargetsInternal(const RenderTargets &rts, int pixelw, int pixelh, bool hasSRGBtexture) = 0;
@@ -981,7 +1010,10 @@ protected:
 	void createQuadIndexBuffer();
 	void createFanIndexBuffer();
 
-	Texture *getTemporaryTexture(PixelFormat format, int w, int h, int samples);
+	void updateTemporaryResources();
+	void clearTemporaryResources();
+
+	void updatePendingReadbacks();
 
 	void restoreState(const DisplayState &s);
 	void restoreStateChecked(const DisplayState &s);
@@ -1004,6 +1036,7 @@ protected:
 	StrongRef<love::graphics::Font> defaultFont;
 
 	std::vector<ScreenshotInfo> pendingScreenshotCallbacks;
+	std::vector<StrongRef<GraphicsReadback>> pendingReadbacks;
 
 	BatchedDrawState batchedDrawState;
 
@@ -1015,6 +1048,7 @@ protected:
 	std::vector<DisplayState> states;
 	std::vector<StackType> stackTypeStack;
 
+	std::vector<TemporaryBuffer> temporaryBuffers;
 	std::vector<TemporaryTexture> temporaryTextures;
 
 	int renderTargetSwitchCount;
@@ -1029,7 +1063,7 @@ protected:
 	Deprecations deprecations;
 
 	static const size_t MAX_USER_STACK_DEPTH = 128;
-	static const int MAX_TEMPORARY_TEXTURE_UNUSED_FRAMES = 16;
+	static const int MAX_TEMPORARY_RESOURCE_UNUSED_FRAMES = 16;
 
 private:
 

+ 229 - 0
src/modules/graphics/GraphicsReadback.cpp

@@ -0,0 +1,229 @@
+/**
+ * Copyright (c) 2006-2022 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 "GraphicsReadback.h"
+#include "Buffer.h"
+#include "Texture.h"
+#include "Graphics.h"
+#include "data/ByteData.h"
+#include "image/ImageData.h"
+#include "image/Image.h"
+
+namespace love
+{
+namespace graphics
+{
+
+love::Type GraphicsReadback::type("GraphicsReadback", &Object::type);
+
+GraphicsReadback::GraphicsReadback(Graphics *gfx, ReadbackMethod method, Buffer *buffer, size_t offset, size_t size, love::data::ByteData *dest, size_t destoffset)
+	: dataType(DATA_BUFFER)
+	, method(method)
+	, bufferData(dest)
+{
+	const auto &caps = gfx->getCapabilities();
+
+	if (!caps.features[Graphics::FEATURE_COPY_BUFFER])
+		throw love::Exception("readbackBuffer is not supported on this system (buffer copy support is required).");
+
+	if (offset + size > buffer->getSize())
+		throw love::Exception("Invalid offset or size for the given Buffer.");
+
+	if (dest != nullptr && destoffset + size > dest->getSize())
+		throw love::Exception("Invalid destination offset or size for the given ByteData.");
+
+	bufferDataOffset = dest != nullptr ? destoffset : 0;
+}
+
+GraphicsReadback::GraphicsReadback(Graphics *gfx, ReadbackMethod method, Texture *texture, int slice, int mipmap, const Rect &rect, love::image::ImageData *dest, int destx, int desty)
+	: dataType(DATA_TEXTURE)
+	, method(method)
+	, imageData(dest)
+	, rect(rect)
+{
+	const auto &caps = gfx->getCapabilities();
+
+	if (gfx->isRenderTargetActive(texture))
+		throw love::Exception("readbackTexture cannot be called while that Texture is an active render target.");
+
+	if (!texture->isReadable())
+		throw love::Exception("readbackTexture requires a readable Texture.");
+
+	int tw = texture->getPixelWidth(mipmap);
+	int th = texture->getPixelHeight(mipmap);
+	auto texType = texture->getTextureType();
+
+	if (rect.x < 0 || rect.y < 0 || rect.w <= 0 || rect.h <= 0 || (rect.x + rect.w) > tw || (rect.y + rect.h) > th)
+		throw love::Exception("Invalid rectangle dimensions.");
+
+	if (slice < 0 || (texType == TEXTURE_VOLUME && slice >= texture->getDepth(mipmap))
+		|| (texType == TEXTURE_2D_ARRAY && slice >= texture->getLayerCount())
+		|| (texType == TEXTURE_CUBE && slice >= 6))
+	{
+		throw love::Exception("Invalid slice index.");
+	}
+
+	textureFormat = getLinearPixelFormat(texture->getPixelFormat());
+
+	if (!image::ImageData::validPixelFormat(textureFormat))
+	{
+		const char *formatname = "unknown";
+		love::getConstant(textureFormat, formatname);
+		throw love::Exception("ImageData with the '%s' pixel format is not supported.", formatname);
+	}
+
+	bool isRT = texture->isRenderTarget();
+
+	if (method == READBACK_ASYNC)
+	{
+		if (isRT && !caps.features[Graphics::FEATURE_COPY_RENDER_TARGET_TO_BUFFER])
+			throw love::Exception("readbackTextureAsync is not supported on this system.");
+		else if (!isRT && !caps.features[Graphics::FEATURE_COPY_TEXTURE_TO_BUFFER])
+			throw love::Exception("readbackTextureAsync a with non-render-target textures is not supported on this system.");
+	}
+	else
+	{
+		if (!isRT && !caps.features[Graphics::FEATURE_COPY_TEXTURE_TO_BUFFER])
+			throw love::Exception("readbackTexture with a non-render-target texture is not supported on this system.");
+	}
+
+	if (dest != nullptr)
+	{
+		if (dest->getFormat() != textureFormat)
+			throw love::Exception("Destination ImageData pixel format must match the source Texture's format.");
+
+		if (destx < 0 || desty < 0)
+			throw love::Exception("Invalid destination ImageData x/y coordinates.");
+
+		if (destx + rect.w > dest->getWidth() || desty + rect.h > dest->getHeight())
+			throw love::Exception("The specified rectangle does not fit within the destination ImageData's dimensions.");
+	}
+
+	imageDataX = dest != nullptr ? destx : 0;
+	imageDataY = dest != nullptr ? desty : 0;
+}
+
+GraphicsReadback::~GraphicsReadback()
+{
+}
+
+love::data::ByteData *GraphicsReadback::getBufferData() const
+{
+	if (!isComplete())
+		return nullptr;
+	return bufferData;
+}
+
+love::image::ImageData *GraphicsReadback::getImageData() const
+{
+	if (!isComplete())
+		return nullptr;
+	return imageData;
+}
+
+void *GraphicsReadback::prepareReadbackDest(size_t size)
+{
+	if (dataType == DATA_TEXTURE)
+	{
+		if (imageData.get())
+		{
+			// Not the cleanest, but should work since uncompressed formats always
+			// have 1x1 blocks.
+			int pixels = imageDataY * imageData->getWidth() + imageDataX;
+			size_t offset = getPixelFormatUncompressedRowSize(textureFormat, pixels);
+
+			return (uint8 *) imageData->getData() + offset;
+		}
+		else
+		{
+			auto module = Module::getInstance<image::Image>(Module::M_IMAGE);
+			if (module == nullptr)
+				throw love::Exception("The love.image module must be loaded for readbackTexture.");
+
+			imageData.set(module->newImageData(rect.w, rect.h, textureFormat, nullptr), Acquire::NORETAIN);
+			return imageData->getData();
+		}
+	}
+	else
+	{
+		if (!bufferData.get())
+			bufferData.set(new love::data::ByteData(size, false), Acquire::NORETAIN);
+
+		return (uint8 *) bufferData->getData() + bufferDataOffset;
+	}
+}
+
+GraphicsReadback::Status GraphicsReadback::readbackBuffer(Buffer *buffer, size_t offset, size_t size)
+{
+	if (buffer == nullptr)
+		return STATUS_ERROR;
+
+	const void *data = buffer->map(Buffer::MAP_READ_ONLY, offset, size);
+
+	if (data == nullptr)
+		return STATUS_ERROR;
+
+	bool success = true;
+
+	try
+	{
+		void *dest = prepareReadbackDest(size);
+		if (dest == nullptr)
+			return STATUS_ERROR;
+
+		if (imageData.get())
+		{
+			love::thread::Lock lock(imageData->getMutex());
+
+			if (imageData->getWidth() != rect.w)
+			{
+				// Readback of compressed textures into ImageData isn't supported,
+				// so this is fine.
+				size_t stride = getPixelFormatUncompressedRowSize(textureFormat, imageData->getWidth());
+				size_t rowsize = getPixelFormatUncompressedRowSize(textureFormat, rect.w);
+
+				for (int i = 0; i < rect.h; i++)
+				{
+					memcpy(dest, data, rowsize);
+					dest = (uint8 *) dest + stride;
+					data = (uint8 *) data + rowsize;
+				}
+			}
+			else
+			{
+				memcpy(dest, data, std::min(size, imageData->getSize()));
+			}
+		}
+		else
+		{
+			memcpy(dest, data, std::min(size, bufferData->getSize()));
+		}
+	}
+	catch (love::Exception &)
+	{
+		success = false;
+	}
+
+	buffer->unmap(offset, size);
+	return success ? STATUS_COMPLETE : STATUS_ERROR;
+}
+
+} // graphics
+} // love

+ 112 - 0
src/modules/graphics/GraphicsReadback.h

@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2006-2022 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
+
+// LOVE
+#include "common/config.h"
+#include "common/int.h"
+#include "common/math.h"
+#include "common/Object.h"
+#include "common/StringMap.h"
+#include "common/pixelformat.h"
+
+namespace love::image
+{
+class ImageData;
+class CompressedImageData;
+}
+
+namespace love::data
+{
+class ByteData;
+}
+
+namespace love
+{
+namespace graphics
+{
+
+class Buffer;
+class Texture;
+class Graphics;
+
+enum ReadbackMethod
+{
+	READBACK_IMMEDIATE,
+	READBACK_ASYNC,
+};
+
+class GraphicsReadback : public love::Object
+{
+public:
+
+	enum Status
+	{
+		STATUS_WAITING,
+		STATUS_COMPLETE,
+		STATUS_ERROR,
+		STATUS_MAX_ENUM
+	};
+
+	static love::Type type;
+
+	GraphicsReadback(Graphics *gfx, ReadbackMethod method, Buffer *buffer, size_t offset, size_t size, love::data::ByteData *dest, size_t destoffset);
+	GraphicsReadback(Graphics *gfx, ReadbackMethod method, Texture *texture, int slice, int mipmap, const Rect &rect, love::image::ImageData *dest, int destx, int desty);
+	virtual ~GraphicsReadback();
+
+	virtual void wait() = 0;
+	virtual void update() = 0;
+
+	bool isComplete() const { return status != STATUS_WAITING; }
+	ReadbackMethod getMethod() const { return method; }
+	bool hasError() const { return status == STATUS_ERROR; }
+
+	love::data::ByteData *getBufferData() const;
+	love::image::ImageData *getImageData() const;
+
+protected:
+
+	enum DataType
+	{
+		DATA_BUFFER,
+		DATA_TEXTURE,
+	};
+
+	void *prepareReadbackDest(size_t size);
+	Status readbackBuffer(Buffer *buffer, size_t offset, size_t size);
+
+	DataType dataType;
+	ReadbackMethod method;
+	Status status = STATUS_WAITING;
+
+	StrongRef<love::data::ByteData> bufferData;
+	size_t bufferDataOffset = 0;
+
+	StrongRef<love::image::ImageData> imageData;
+	Rect rect = {};
+	PixelFormat textureFormat = PIXELFORMAT_UNKNOWN;
+	int imageDataX = 0;
+	int imageDataY = 0;
+
+}; // GraphicsReadback
+
+} // graphics
+} // love

+ 0 - 41
src/modules/graphics/Texture.cpp

@@ -563,47 +563,6 @@ void Texture::generateMipmaps()
 	generateMipmapsInternal();
 }
 
-love::image::ImageData *Texture::newImageData(love::image::Image *module, int slice, int mipmap, const Rect &r)
-{
-	if (!isReadable())
-		throw love::Exception("Texture:newImageData cannot be called on non-readable Textures.");
-
-	if (!isRenderTarget())
-		throw love::Exception("Texture:newImageData can only be called on render target Textures.");
-
-	if (isPixelFormatDepthStencil(getPixelFormat()))
-		throw love::Exception("Texture:newImageData cannot be called on Textures with depth/stencil pixel formats.");
-
-	if (r.x < 0 || r.y < 0 || r.w <= 0 || r.h <= 0 || (r.x + r.w) > getPixelWidth(mipmap) || (r.y + r.h) > getPixelHeight(mipmap))
-		throw love::Exception("Invalid rectangle dimensions.");
-
-	if (slice < 0 || (texType == TEXTURE_VOLUME && slice >= getDepth(mipmap))
-		|| (texType == TEXTURE_2D_ARRAY && slice >= layers)
-		|| (texType == TEXTURE_CUBE && slice >= 6))
-	{
-		throw love::Exception("Invalid slice index.");
-	}
-
-	Graphics *gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);
-	if (gfx != nullptr && gfx->isRenderTargetActive(this))
-		throw love::Exception("Texture:newImageData cannot be called while that Texture is an active render target.");
-
-	PixelFormat dataformat = getLinearPixelFormat(getPixelFormat());
-
-	if (!image::ImageData::validPixelFormat(dataformat))
-	{
-		const char *formatname = "unknown";
-		love::getConstant(dataformat, formatname);
-		throw love::Exception("ImageData with the '%s' pixel format is not supported.", formatname);
-	}
-
-	auto imagedata = module->newImageData(r.w, r.h, dataformat);
-
-	readbackImageData(imagedata, slice, mipmap, r);
-
-	return imagedata;
-}
-
 TextureType Texture::getTextureType() const
 {
 	return texType;

+ 0 - 3
src/modules/graphics/Texture.h

@@ -245,8 +245,6 @@ public:
 
 	void generateMipmaps();
 
-	love::image::ImageData *newImageData(love::image::Image *module, int slice, int mipmap, const Rect &rect);
-
 	virtual void copyFromBuffer(Buffer *source, size_t sourceoffset, int sourcewidth, size_t size, int slice, int mipmap, const Rect &rect) = 0;
 	virtual void copyToBuffer(Buffer *dest, int slice, int mipmap, const Rect &rect, size_t destoffset, int destwidth, size_t size) = 0;
 
@@ -313,7 +311,6 @@ protected:
 
 	bool supportsGenerateMipmaps(const char *&outReason) const;
 	virtual void generateMipmapsInternal() = 0;
-	virtual void readbackImageData(love::image::ImageData *imagedata, int slice, int mipmap, const Rect &rect) = 0;
 
 	bool validateDimensions(bool throwException) const;
 

+ 1 - 1
src/modules/graphics/metal/Buffer.h

@@ -40,7 +40,7 @@ public:
 
 	void *map(MapType map, size_t offset, size_t size) override;
 	void unmap(size_t usedoffset, size_t usedsize) override;
-	void fill(size_t offset, size_t size, const void *data) override;
+	bool fill(size_t offset, size_t size, const void *data) override;
 	void copyTo(love::graphics::Buffer *dest, size_t sourceoffset, size_t destoffset, size_t size) override;
 
 	ptrdiff_t getHandle() const override { return (ptrdiff_t) buffer; }

+ 38 - 24
src/modules/graphics/metal/Buffer.mm

@@ -18,7 +18,7 @@
 * 3. This notice may not be removed or altered from any source distribution.
 **/
 
-#import "Buffer.h"
+#include "Buffer.h"
 #include "Graphics.h"
 
 namespace love
@@ -65,11 +65,16 @@ Buffer::Buffer(love::graphics::Graphics *gfx, id<MTLDevice> device, const Settin
 	size = getSize();
 	arraylength = getArrayLength();
 
-	MTLResourceOptions opts = MTLResourceStorageModePrivate;
+	MTLResourceOptions opts = 0;
+	if (settings.dataUsage == BUFFERDATAUSAGE_READBACK)
+		opts |= MTLResourceStorageModeShared;
+	else
+		opts |= MTLResourceStorageModePrivate;
+
 	buffer = [device newBufferWithLength:size options:opts];
 
 	if (buffer == nil)
-		throw love::Exception("Could not create buffer (out of VRAM?)");
+		throw love::Exception("Could not create buffer with %d bytes (out of VRAM?)", size);
 
 	if (usageFlags & BUFFERUSAGEFLAG_TEXEL)
 	{
@@ -109,16 +114,30 @@ Buffer::~Buffer()
 	texture = nil;
 }}
 
-void *Buffer::map(MapType /*map*/, size_t offset, size_t size)
+void *Buffer::map(MapType map, size_t offset, size_t size)
 { @autoreleasepool {
-	if (size == 0 || isImmutable())
+	if (size == 0)
+		return nullptr;
+
+	if (map == MAP_WRITE_INVALIDATE && (isImmutable() || dataUsage == BUFFERDATAUSAGE_READBACK))
 		return nullptr;
 
+	if (map == MAP_READ_ONLY && dataUsage != BUFFERDATAUSAGE_READBACK)
+		return  nullptr;
+
 	Range r(offset, size);
 
 	if (!Range(0, getSize()).contains(r))
 		return nullptr;
 
+	if (map == MAP_READ_ONLY)
+	{
+		mappedRange = r;
+		mapped = true;
+		mappedType = map;
+		return (char *) buffer.contents + offset;
+	}
+
 	auto gfx = Graphics::getInstance();
 
 	// TODO: Don't create a new buffer every time, also do something for stream
@@ -129,6 +148,7 @@ void *Buffer::map(MapType /*map*/, size_t offset, size_t size)
 	{
 		mappedRange = r;
 		mapped = true;
+		mappedType = map;
 		return mapBuffer.contents;
 	}
 
@@ -137,6 +157,12 @@ void *Buffer::map(MapType /*map*/, size_t offset, size_t size)
 
 void Buffer::unmap(size_t usedoffset, size_t usedsize)
 { @autoreleasepool {
+	if (mappedType == MAP_READ_ONLY)
+	{
+		mapped = false;
+		return;
+	}
+
 	if (mapBuffer == nil)
 		return;
 
@@ -158,29 +184,17 @@ void Buffer::unmap(size_t usedoffset, size_t usedsize)
 	mapped = false;
 }}
 
-void Buffer::fill(size_t offset, size_t size, const void *data)
+bool Buffer::fill(size_t offset, size_t size, const void *data)
 { @autoreleasepool {
-	if (size == 0 || isImmutable())
-		return;
+	void *dest = map(MAP_WRITE_INVALIDATE, offset, size);
 
-	size_t buffersize = getSize();
+	if (dest == nullptr)
+		return false;
 
-	if (!Range(0, buffersize).contains(Range(offset, size)))
-		return;
+	memcpy(dest, data, size);
 
-	// TODO: Don't create a new buffer every time, also do something for stream
-	// buffers.
-	auto gfx = Graphics::getInstance();
-	auto encoder = gfx->useBlitEncoder();
-
-	auto tempbuffer = [gfx->device newBufferWithLength:size options:MTLResourceStorageModeShared];
-	memcpy(tempbuffer.contents, data, size);
-
-	[encoder copyFromBuffer:tempbuffer
-			   sourceOffset:0
-				   toBuffer:buffer
-		  destinationOffset:offset
-					   size:size];
+	unmap(offset, size);
+	return true;
 }}
 
 void Buffer::copyTo(love::graphics::Buffer *dest, size_t sourceoffset, size_t destoffset, size_t size)

+ 4 - 0
src/modules/graphics/metal/Graphics.h

@@ -199,6 +199,10 @@ private:
 	love::graphics::ShaderStage *newShaderStageInternal(ShaderStageType stage, const std::string &cachekey, const std::string &source, bool gles) override;
 	love::graphics::Shader *newShaderInternal(StrongRef<love::graphics::ShaderStage> stages[SHADERSTAGE_MAX_ENUM]) override;
 	love::graphics::StreamBuffer *newStreamBuffer(BufferUsage usage, size_t size) override;
+
+	love::graphics::GraphicsReadback *newReadbackInternal(ReadbackMethod method, love::graphics::Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset) override;
+	love::graphics::GraphicsReadback *newReadbackInternal(ReadbackMethod method, love::graphics::Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty) override;
+
 	void setRenderTargetsInternal(const RenderTargets &rts, int pixelw, int pixelh, bool hasSRGBcanvas) override;
 	void initCapabilities() override;
 	void getAPIStats(int &shaderswitches) const override;

+ 14 - 16
src/modules/graphics/metal/Graphics.mm

@@ -22,6 +22,7 @@
 #include "StreamBuffer.h"
 #include "Buffer.h"
 #include "Texture.h"
+#include "GraphicsReadback.h"
 #include "Shader.h"
 #include "ShaderStage.h"
 #include "window/Window.h"
@@ -418,6 +419,16 @@ love::graphics::Buffer *Graphics::newBuffer(const Buffer::Settings &settings, co
 	return new Buffer(this, device, settings, format, data, size, arraylength);
 }
 
+love::graphics::GraphicsReadback *Graphics::newReadbackInternal(ReadbackMethod method, love::graphics::Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset)
+{
+	return new GraphicsReadback(this, method, buffer, offset, size, dest, destoffset);
+}
+
+love::graphics::GraphicsReadback *Graphics::newReadbackInternal(ReadbackMethod method, love::graphics::Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty)
+{
+	return new GraphicsReadback(this, method, texture, slice, mipmap, rect, dest, destx, desty);
+}
+
 Matrix4 Graphics::computeDeviceProjection(const Matrix4 &projection, bool /*rendertotexture*/) const
 {
 	uint32 flags = DEVICE_PROJECTION_FLIP_Y;
@@ -501,10 +512,7 @@ void Graphics::unSetMode()
 
 	submitCommandBuffer(SUBMIT_DONE);
 
-	for (auto temp : temporaryTextures)
-		temp.texture->release();
-
-	temporaryTextures.clear();
+	clearTemporaryResources();
 
 	created = false;
 	metalLayer = nil;
@@ -1620,18 +1628,8 @@ void Graphics::present(void *screenshotCallbackData)
 	renderTargetSwitchCount = 0;
 	drawCallsBatched = 0;
 
-	// This assumes temporary canvases will only be used within a render pass.
-	for (int i = (int) temporaryTextures.size() - 1; i >= 0; i--)
-	{
-		if (temporaryTextures[i].framesSinceUse >= MAX_TEMPORARY_TEXTURE_UNUSED_FRAMES)
-		{
-			temporaryTextures[i].texture->release();
-			temporaryTextures[i] = temporaryTextures.back();
-			temporaryTextures.pop_back();
-		}
-		else
-			temporaryTextures[i].framesSinceUse++;
-	}
+	updatePendingReadbacks();
+	updateTemporaryResources();
 }}
 
 int Graphics::getRequestedBackbufferMSAA() const

+ 54 - 0
src/modules/graphics/metal/GraphicsReadback.h

@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2006-2022 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
+
+// LOVE
+#include "graphics/GraphicsReadback.h"
+#include "common/math.h"
+
+#include <atomic>
+
+#import <Metal/MTLCommandBuffer.h>
+
+namespace love::graphics::metal
+{
+
+class GraphicsReadback final : public love::graphics::GraphicsReadback
+{
+public:
+
+	GraphicsReadback(love::graphics::Graphics *gfx, ReadbackMethod method, love::graphics::Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset);
+	GraphicsReadback(love::graphics::Graphics *gfx, ReadbackMethod method, love::graphics::Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty);
+	virtual ~GraphicsReadback();
+
+	void wait() override;
+	void update() override;
+
+private:
+
+	id<MTLCommandBuffer> cmd;
+	std::atomic_bool done;
+
+	StrongRef<love::graphics::Buffer> stagingBuffer;
+
+}; // GraphicsReadback
+
+} // love::graphics::metal

+ 143 - 0
src/modules/graphics/metal/GraphicsReadback.mm

@@ -0,0 +1,143 @@
+/**
+ * Copyright (c) 2006-2022 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 "GraphicsReadback.h"
+#include "Buffer.h"
+#include "Texture.h"
+#include "Graphics.h"
+#include "data/ByteData.h"
+
+namespace love::graphics::metal
+{
+
+GraphicsReadback::GraphicsReadback(love::graphics::Graphics *gfx, ReadbackMethod method, love::graphics::Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset)
+	: love::graphics::GraphicsReadback(gfx, method, buffer, offset, size, dest, destoffset)
+	, done(false)
+{ @autoreleasepool {
+	auto mgfx = (Graphics *) gfx;
+
+	// Immediate readback of readback-type buffers doesn't need a staging buffer.
+	if (method != READBACK_IMMEDIATE || buffer->getDataUsage() != BUFFERDATAUSAGE_READBACK)
+	{
+		stagingBuffer = gfx->getTemporaryBuffer(size, DATAFORMAT_FLOAT, 0, BUFFERDATAUSAGE_READBACK);
+		gfx->copyBuffer(buffer, stagingBuffer, offset, 0, size);
+	}
+
+	// use instead of get, in case this was the first command in the frame.
+	cmd = mgfx->useCommandBuffer();
+
+	auto pthis = this;
+	pthis->retain();
+	[cmd addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull)
+	{
+		pthis->done = true;
+		pthis->release();
+	}];
+
+	if (method == READBACK_IMMEDIATE)
+	{
+		wait();
+
+		if (stagingBuffer.get())
+		{
+			status = readbackBuffer(stagingBuffer, 0, size);
+			gfx->releaseTemporaryBuffer(stagingBuffer);
+		}
+		else
+		{
+			status = readbackBuffer(buffer, offset, size);
+		}
+	}
+}}
+
+GraphicsReadback::GraphicsReadback(love::graphics::Graphics *gfx, ReadbackMethod method, love::graphics::Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty)
+	: love::graphics::GraphicsReadback(gfx, method, texture, slice, mipmap, rect, dest, destx, desty)
+	, done(false)
+{ @autoreleasepool {
+	auto mgfx = (Graphics *) gfx;
+	size_t size = getPixelFormatSliceSize(textureFormat, rect.w, rect.h);
+
+	stagingBuffer = gfx->getTemporaryBuffer(size, DATAFORMAT_FLOAT, 0, BUFFERDATAUSAGE_READBACK);
+
+	gfx->copyTextureToBuffer(texture, stagingBuffer, slice, mipmap, rect, 0, 0);
+
+	cmd = mgfx->getCommandBuffer();
+
+	auto pthis = this;
+	pthis->retain();
+	[cmd addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull)
+	{
+		pthis->done = true;
+		pthis->release();
+	}];
+
+	if (method == READBACK_IMMEDIATE)
+	{
+		wait();
+		status = readbackBuffer(stagingBuffer, 0, size);
+		gfx->releaseTemporaryBuffer(stagingBuffer);
+	}
+}}
+
+GraphicsReadback::~GraphicsReadback()
+{ @autoreleasepool {
+	cmd = nil;
+}}
+
+void GraphicsReadback::wait()
+{ @autoreleasepool {
+	if (status != STATUS_WAITING || cmd == nil)
+		return;
+
+	if (cmd.status == MTLCommandBufferStatusNotEnqueued)
+	{
+		auto gfx = Graphics::getInstance();
+		gfx->submitCommandBuffer(Graphics::SUBMIT_STORE);
+	}
+
+	[cmd waitUntilCompleted];
+	cmd = nil;
+
+	update();
+}}
+
+void GraphicsReadback::update()
+{
+	if (status != STATUS_WAITING)
+		return;
+
+	if (done)
+	{
+		if (stagingBuffer.get())
+			status = readbackBuffer(stagingBuffer, 0, stagingBuffer->getSize());
+		else
+			status = STATUS_ERROR;
+
+		if (stagingBuffer.get())
+		{
+			auto gfx = Module::getInstance<love::graphics::Graphics>(Module::M_GRAPHICS);
+			if (gfx != nullptr)
+				gfx->releaseTemporaryBuffer(stagingBuffer);
+			stagingBuffer.set(nullptr);
+		}
+	}
+}
+
+} // love::graphics::metal

+ 0 - 1
src/modules/graphics/metal/Texture.h

@@ -57,7 +57,6 @@ private:
 
 	void uploadByteData(PixelFormat pixelformat, const void *data, size_t size, int level, int slice, const Rect &r) override;
 	void generateMipmapsInternal() override;
-	void readbackImageData(love::image::ImageData *imagedata, int slice, int mipmap, const Rect &rect) override;
 
 	id<MTLTexture> texture;
 	id<MTLTexture> msaaTexture;

+ 0 - 46
src/modules/graphics/metal/Texture.mm

@@ -273,52 +273,6 @@ void Texture::generateMipmapsInternal()
 	[encoder generateMipmapsForTexture:texture];
 }}
 
-void Texture::readbackImageData(love::image::ImageData *imagedata, int slice, int mipmap, const Rect &rect)
-{ @autoreleasepool {
-	auto gfx = Graphics::getInstance();
-
-	id<MTLBlitCommandEncoder> encoder = gfx->useBlitEncoder();
-
-	size_t rowSize = 0;
-	if (isCompressed())
-		rowSize = getPixelFormatCompressedBlockRowSize(format, rect.w);
-	else
-		rowSize = getPixelFormatUncompressedRowSize(format, rect.w);
-
-	// TODO: Verify this is correct for compressed formats at small sizes.
-	// TODO: make sure this is consistent with the imagedata byte size?
-	size_t sliceSize = getPixelFormatSliceSize(format, rect.w, rect.h);
-
-	int z = texType == TEXTURE_VOLUME ? slice : 0;
-
-	id<MTLBuffer> buffer = [gfx->device newBufferWithLength:sliceSize
-													options:MTLResourceStorageModeShared];
-
-	MTLBlitOption options = MTLBlitOptionNone;
-	if (isPixelFormatDepthStencil(format))
-		options = MTLBlitOptionDepthFromDepthStencil;
-
-	[encoder copyFromTexture:texture
-				 sourceSlice:texType == TEXTURE_VOLUME ? 0 : slice
-				 sourceLevel:mipmap
-				sourceOrigin:MTLOriginMake(rect.x, rect.y, z)
-				  sourceSize:MTLSizeMake(rect.w, rect.h, 1)
-					toBuffer:buffer
-		   destinationOffset:0
-	  destinationBytesPerRow:rowSize
-	destinationBytesPerImage:sliceSize
-					 options:options];
-
-	id<MTLCommandBuffer> cmd = gfx->getCommandBuffer();
-
-	gfx->submitBlitEncoder();
-	gfx->submitCommandBuffer(Graphics::SUBMIT_STORE);
-
-	[cmd waitUntilCompleted];
-
-	memcpy(imagedata->getData(), buffer.contents, imagedata->getSize());
-}}
-
 void Texture::copyFromBuffer(love::graphics::Buffer *source, size_t sourceoffset, int sourcewidth, size_t size, int slice, int mipmap, const Rect &rect)
 { @autoreleasepool {
 	id<MTLBlitCommandEncoder> encoder = Graphics::getInstance()->useBlitEncoder();

+ 35 - 8
src/modules/graphics/opengl/Buffer.cpp

@@ -103,7 +103,7 @@ Buffer::Buffer(love::graphics::Graphics *gfx, const Settings &settings, const st
 	if (!load(data))
 	{
 		unloadVolatile();
-		throw love::Exception("Could not create buffer (out of VRAM?)");
+		throw love::Exception("Could not create buffer with %d bytes (out of VRAM?)", size);
 	}
 }
 
@@ -164,11 +164,17 @@ bool Buffer::supportsOrphan() const
 	return dataUsage == BUFFERDATAUSAGE_STREAM || dataUsage == BUFFERDATAUSAGE_DYNAMIC;
 }
 
-void *Buffer::map(MapType /*map*/, size_t offset, size_t size)
+void *Buffer::map(MapType map, size_t offset, size_t size)
 {
-	if (size == 0 || isImmutable())
+	if (size == 0)
 		return nullptr;
 
+	if (map == MAP_WRITE_INVALIDATE && (isImmutable() || dataUsage == BUFFERDATAUSAGE_READBACK))
+		return nullptr;
+
+	if (map == MAP_READ_ONLY && dataUsage != BUFFERDATAUSAGE_READBACK)
+		return  nullptr;
+
 	Range r(offset, size);
 
 	if (!Range(0, getSize()).contains(r))
@@ -176,7 +182,16 @@ void *Buffer::map(MapType /*map*/, size_t offset, size_t size)
 
 	char *data = nullptr;
 
-	if (ownsMemoryMap)
+	if (map == MAP_READ_ONLY)
+	{
+		gl.bindBuffer(mapUsage, buffer);
+
+		if (GLAD_VERSION_3_0 || GLAD_ES_VERSION_3_0)
+			data = (char *) glMapBufferRange(target, offset, size, GL_MAP_READ_BIT);
+		else if (GLAD_VERSION_1_1)
+			data = (char *) glMapBuffer(target, GL_READ_ONLY) + offset;
+	}
+	else if (ownsMemoryMap)
 	{
 		if (memoryMap == nullptr)
 			memoryMap = (char *) malloc(getSize());
@@ -191,6 +206,7 @@ void *Buffer::map(MapType /*map*/, size_t offset, size_t size)
 	if (data != nullptr)
 	{
 		mapped = true;
+		mappedType = map;
 		mappedRange = r;
 		if (!ownsMemoryMap)
 			memoryMap = data;
@@ -208,6 +224,15 @@ void Buffer::unmap(size_t usedoffset, size_t usedsize)
 
 	mapped = false;
 
+	if (mappedType == MAP_READ_ONLY)
+	{
+		gl.bindBuffer(mapUsage, buffer);
+		glUnmapBuffer(target);
+		if (!ownsMemoryMap)
+			memoryMap = nullptr;
+		return;
+	}
+
 	// Orphan optimization - see fill().
 	if (supportsOrphan() && mappedRange.first == 0 && mappedRange.getSize() == getSize())
 	{
@@ -227,15 +252,15 @@ void Buffer::unmap(size_t usedoffset, size_t usedsize)
 	}
 }
 
-void Buffer::fill(size_t offset, size_t size, const void *data)
+bool Buffer::fill(size_t offset, size_t size, const void *data)
 {
-	if (size == 0 || isImmutable())
-		return;
+	if (size == 0 || isImmutable() || dataUsage == BUFFERDATAUSAGE_READBACK)
+		return false;
 
 	size_t buffersize = getSize();
 
 	if (!Range(0, buffersize).contains(Range(offset, size)))
-		return;
+		return false;
 
 	GLenum gldatausage = OpenGL::getGLBufferDataUsage(dataUsage);
 
@@ -259,6 +284,8 @@ void Buffer::fill(size_t offset, size_t size, const void *data)
 	{
 		glBufferSubData(target, (GLintptr) offset, (GLsizeiptr) size, data);
 	}
+
+	return true;
 }
 
 void Buffer::copyTo(love::graphics::Buffer *dest, size_t sourceoffset, size_t destoffset, size_t size)

+ 3 - 1
src/modules/graphics/opengl/Buffer.h

@@ -52,12 +52,14 @@ public:
 
 	void *map(MapType map, size_t offset, size_t size) override;
 	void unmap(size_t usedoffset, size_t usedsize) override;
-	void fill(size_t offset, size_t size, const void *data) override;
+	bool fill(size_t offset, size_t size, const void *data) override;
 	void copyTo(love::graphics::Buffer *dest, size_t sourceoffset, size_t destoffset, size_t size) override;
 
 	ptrdiff_t getHandle() const override { return buffer; };
 	ptrdiff_t getTexelBufferHandle() const override { return texture; };
 
+	BufferUsage getMapUsage() const { return mapUsage; }
+
 private:
 
 	bool load(const void *initialdata);

+ 16 - 0
src/modules/graphics/opengl/FenceSync.cpp

@@ -45,6 +45,22 @@ bool FenceSync::fence()
 	return !wasActive;
 }
 
+bool FenceSync::isComplete() const
+{
+	if (sync == 0)
+		return true;
+
+	GLenum status = glClientWaitSync(sync, 0, 0);
+
+	if (status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED)
+		return true;
+
+	if (status == GL_WAIT_FAILED)
+		return true;
+
+	return false;
+}
+
 bool FenceSync::cpuWait()
 {
 	if (sync == 0)

+ 1 - 0
src/modules/graphics/opengl/FenceSync.h

@@ -42,6 +42,7 @@ public:
 	~FenceSync();
 
 	bool fence();
+	bool isComplete() const;
 	bool cpuWait();
 	void cleanup();
 

+ 15 - 16
src/modules/graphics/opengl/Graphics.cpp

@@ -26,6 +26,7 @@
 #include "Graphics.h"
 #include "font/Font.h"
 #include "StreamBuffer.h"
+#include "GraphicsReadback.h"
 #include "math/MathModule.h"
 #include "window/Window.h"
 #include "Buffer.h"
@@ -177,6 +178,16 @@ love::graphics::Buffer *Graphics::newBuffer(const Buffer::Settings &settings, co
 	return new Buffer(this, settings, format, data, size, arraylength);
 }
 
+love::graphics::GraphicsReadback *Graphics::newReadbackInternal(ReadbackMethod method, love::graphics::Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset)
+{
+	return new GraphicsReadback(this, method, buffer, offset, size, dest, destoffset);
+}
+
+love::graphics::GraphicsReadback *Graphics::newReadbackInternal(ReadbackMethod method, love::graphics::Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty)
+{
+	return new GraphicsReadback(this, method, texture, slice, mipmap, rect, dest, destx, desty);
+}
+
 Matrix4 Graphics::computeDeviceProjection(const Matrix4 &projection, bool rendertotexture) const
 {
 	uint32 flags = DEVICE_PROJECTION_DEFAULT;
@@ -462,14 +473,12 @@ void Graphics::unSetMode()
 	// mode change.
 	Volatile::unloadAll();
 
+	clearTemporaryResources();
+
 	for (const auto &pair : framebufferObjects)
 		gl.deleteFramebuffer(pair.second);
 
-	for (auto temp : temporaryTextures)
-		temp.texture->release();
-
 	framebufferObjects.clear();
-	temporaryTextures.clear();
 
 	if (mainVAO != 0)
 	{
@@ -1335,18 +1344,8 @@ void Graphics::present(void *screenshotCallbackData)
 	renderTargetSwitchCount = 0;
 	drawCallsBatched = 0;
 
-	// This assumes temporary textures will only be used within a render pass.
-	for (int i = (int) temporaryTextures.size() - 1; i >= 0; i--)
-	{
-		if (temporaryTextures[i].framesSinceUse >= MAX_TEMPORARY_TEXTURE_UNUSED_FRAMES)
-		{
-			temporaryTextures[i].texture->release();
-			temporaryTextures[i] = temporaryTextures.back();
-			temporaryTextures.pop_back();
-		}
-		else
-			temporaryTextures[i].framesSinceUse++;
-	}
+	updatePendingReadbacks();
+	updateTemporaryResources();
 }
 
 int Graphics::getRequestedBackbufferMSAA() const

+ 4 - 0
src/modules/graphics/opengl/Graphics.h

@@ -141,6 +141,10 @@ private:
 	love::graphics::ShaderStage *newShaderStageInternal(ShaderStageType stage, const std::string &cachekey, const std::string &source, bool gles) override;
 	love::graphics::Shader *newShaderInternal(StrongRef<love::graphics::ShaderStage> stages[SHADERSTAGE_MAX_ENUM]) override;
 	love::graphics::StreamBuffer *newStreamBuffer(BufferUsage type, size_t size) override;
+
+	love::graphics::GraphicsReadback *newReadbackInternal(ReadbackMethod method, love::graphics::Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset) override;
+	love::graphics::GraphicsReadback *newReadbackInternal(ReadbackMethod method, love::graphics::Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty) override;
+
 	void setRenderTargetsInternal(const RenderTargets &rts, int pixelw, int pixelh, bool hasSRGBtexture) override;
 	void initCapabilities() override;
 	void getAPIStats(int &shaderswitches) const override;

+ 126 - 0
src/modules/graphics/opengl/GraphicsReadback.cpp

@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2006-2022 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 "GraphicsReadback.h"
+#include "Buffer.h"
+#include "Texture.h"
+#include "graphics/Graphics.h"
+#include "data/ByteData.h"
+
+namespace love
+{
+namespace graphics
+{
+namespace opengl
+{
+
+GraphicsReadback::GraphicsReadback(love::graphics::Graphics *gfx, ReadbackMethod method, love::graphics::Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset)
+	: love::graphics::GraphicsReadback(gfx, method, buffer, offset, size, dest, destoffset)
+{
+	// Immediate readback of readback-type buffers doesn't need a staging buffer.
+	if (method != READBACK_IMMEDIATE || buffer->getDataUsage() != BUFFERDATAUSAGE_READBACK)
+	{
+		stagingBuffer = gfx->getTemporaryBuffer(size, DATAFORMAT_FLOAT, 0, BUFFERDATAUSAGE_READBACK);
+		gfx->copyBuffer(buffer, stagingBuffer, offset, 0, size);
+	}
+
+	if (method == READBACK_IMMEDIATE)
+	{
+		if (stagingBuffer.get())
+		{
+			status = readbackBuffer(stagingBuffer, 0, size);
+			gfx->releaseTemporaryBuffer(stagingBuffer);
+		}
+		else
+		{
+			status = readbackBuffer(buffer, offset, size);
+		}
+	}
+	else
+	{
+		sync.fence();
+	}
+}
+
+GraphicsReadback::GraphicsReadback(love::graphics::Graphics *gfx, ReadbackMethod method, love::graphics::Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty)
+	: love::graphics::GraphicsReadback(gfx, method, texture, slice, mipmap, rect, dest, destx, desty)
+{
+	size_t size = getPixelFormatSliceSize(textureFormat, rect.w, rect.h);
+
+	if (method == READBACK_IMMEDIATE)
+	{
+		void *dest = prepareReadbackDest(size);
+
+		love::thread::Lock lock(imageData->getMutex());
+
+		// Direct readback without copying avoids the need for a staging buffer,
+		// and lowers the system requirements of immediate RT readback.
+		Texture *t = (Texture *) texture;
+		t->readbackInternal(slice, mipmap, rect, imageData->getWidth(), size, dest);
+
+		status = STATUS_COMPLETE;
+	}
+	else
+	{
+		stagingBuffer = gfx->getTemporaryBuffer(size, DATAFORMAT_FLOAT, 0, BUFFERDATAUSAGE_READBACK);
+
+		gfx->copyTextureToBuffer(texture, stagingBuffer, slice, mipmap, rect, 0, 0);
+		sync.fence();
+	}
+}
+
+GraphicsReadback::~GraphicsReadback()
+{
+}
+
+void GraphicsReadback::wait()
+{
+	if (status != STATUS_WAITING)
+		return;
+
+	sync.cpuWait();
+	update();
+}
+
+void GraphicsReadback::update()
+{
+	if (status != STATUS_WAITING)
+		return;
+
+	if (sync.isComplete())
+	{
+		if (stagingBuffer.get())
+			status = readbackBuffer(stagingBuffer, 0, stagingBuffer->getSize());
+		else
+			status = STATUS_ERROR;
+
+		if (stagingBuffer.get())
+		{
+			auto gfx = Module::getInstance<love::graphics::Graphics>(Module::M_GRAPHICS);
+			if (gfx != nullptr)
+				gfx->releaseTemporaryBuffer(stagingBuffer);
+			stagingBuffer.set(nullptr);
+		}
+	}
+}
+
+} // opengl
+} // graphics
+} // love

+ 56 - 0
src/modules/graphics/opengl/GraphicsReadback.h

@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2006-2022 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
+
+// LOVE
+#include "graphics/GraphicsReadback.h"
+#include "FenceSync.h"
+#include "common/math.h"
+
+namespace love
+{
+namespace graphics
+{
+namespace opengl
+{
+
+class GraphicsReadback final : public love::graphics::GraphicsReadback
+{
+public:
+
+	GraphicsReadback(love::graphics::Graphics *gfx, ReadbackMethod method, love::graphics::Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset);
+	GraphicsReadback(love::graphics::Graphics *gfx, ReadbackMethod method, love::graphics::Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty);
+	virtual ~GraphicsReadback();
+
+	void wait() override;
+	void update() override;
+
+private:
+
+	FenceSync sync;
+
+	StrongRef<love::graphics::Buffer> stagingBuffer;
+
+}; // GraphicsReadback
+
+} // opengl
+} // graphics
+} // love

+ 1 - 1
src/modules/graphics/opengl/OpenGL.cpp

@@ -843,7 +843,7 @@ GLenum OpenGL::getGLBufferDataUsage(BufferDataUsage usage)
 		case BUFFERDATAUSAGE_STREAM: return GL_STREAM_DRAW;
 		case BUFFERDATAUSAGE_DYNAMIC: return GL_DYNAMIC_DRAW;
 		case BUFFERDATAUSAGE_STATIC: return GL_STATIC_DRAW;
-		case BUFFERDATAUSAGE_STAGING:
+		case BUFFERDATAUSAGE_READBACK:
 			return (GLAD_VERSION_1_1 || GLAD_ES_VERSION_3_0) ? GL_STREAM_READ : GL_STREAM_DRAW;
 		default: return 0;
 	}

+ 33 - 51
src/modules/graphics/opengl/Texture.cpp

@@ -506,30 +506,46 @@ void Texture::generateMipmapsInternal()
 	glGenerateMipmap(gltextype);
 }
 
-void Texture::readbackImageData(love::image::ImageData *data, int slice, int mipmap, const Rect &r)
+void Texture::readbackInternal(int slice, int mipmap, const Rect &rect, int destwidth, size_t size, void *dest)
 {
-	if (fbo == 0) // Should never be reached.
-		return;
+	// Not supported in GL with compressed textures...
+	if ((GLAD_VERSION_1_1 || GLAD_ES_VERSION_3_0) && !isCompressed())
+		glPixelStorei(GL_PACK_ROW_LENGTH, destwidth);
 
-	bool isSRGB = false;
-	OpenGL::TextureFormat fmt = gl.convertPixelFormat(data->getFormat(), false, isSRGB);
+	gl.bindTextureToUnit(this, 0, false);
 
-	GLuint current_fbo = gl.getFramebuffer(OpenGL::FRAMEBUFFER_ALL);
-	gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, getFBO());
+	bool isSRGB = false;
+	OpenGL::TextureFormat fmt = gl.convertPixelFormat(format, false, isSRGB);
 
-	if (slice > 0 || mipmap > 0)
+	if (gl.isCopyTextureToBufferSupported())
 	{
-		int layer = texType == TEXTURE_CUBE ? 0 : slice;
-		int face = texType == TEXTURE_CUBE ? slice : 0;
-		gl.framebufferTexture(GL_COLOR_ATTACHMENT0, texType, texture, mipmap, layer, face);
+		if (isCompressed())
+			glGetCompressedTextureSubImage(texture, mipmap, rect.x, rect.y, slice, rect.w, rect.h, 1, size, dest);
+		else
+			glGetTextureSubImage(texture, mipmap, rect.x, rect.y, slice, rect.w, rect.h, 1, fmt.externalformat, fmt.type, size, dest);
 	}
+	else if (fbo)
+	{
+		GLuint current_fbo = gl.getFramebuffer(OpenGL::FRAMEBUFFER_ALL);
+		gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, getFBO());
+
+		if (slice > 0 || mipmap > 0)
+		{
+			int layer = texType == TEXTURE_CUBE ? 0 : slice;
+			int face = texType == TEXTURE_CUBE ? slice : 0;
+			gl.framebufferTexture(GL_COLOR_ATTACHMENT0, texType, texture, mipmap, layer, face);
+		}
 
-	glReadPixels(r.x, r.y, r.w, r.h, fmt.externalformat, fmt.type, data->getData());
+		glReadPixels(rect.x, rect.y, rect.w, rect.h, fmt.externalformat, fmt.type, dest);
 
-	if (slice > 0 || mipmap > 0)
-		gl.framebufferTexture(GL_COLOR_ATTACHMENT0, texType, texture, 0, 0, 0);
+		if (slice > 0 || mipmap > 0)
+			gl.framebufferTexture(GL_COLOR_ATTACHMENT0, texType, texture, 0, 0, 0);
 
-	gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, current_fbo);
+		gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, current_fbo);
+	}
+
+	if ((GLAD_VERSION_1_1 || GLAD_ES_VERSION_3_0) && !isCompressed())
+		glPixelStorei(GL_PACK_ROW_LENGTH, 0);
 }
 
 void Texture::copyFromBuffer(love::graphics::Buffer *source, size_t sourceoffset, int sourcewidth, size_t size, int slice, int mipmap, const Rect &rect)
@@ -558,46 +574,12 @@ void Texture::copyToBuffer(love::graphics::Buffer *dest, int slice, int mipmap,
 	GLuint glbuffer = (GLuint) dest->getHandle();
 	glBindBuffer(GL_PIXEL_PACK_BUFFER, glbuffer);
 
-	if (!isCompressed()) // Not supported in GL with compressed textures...
-		glPixelStorei(GL_PACK_ROW_LENGTH, destwidth);
-
-	gl.bindTextureToUnit(this, 0, false);
-
-	bool isSRGB = false;
-	OpenGL::TextureFormat fmt = gl.convertPixelFormat(format, false, isSRGB);
-
-	// glTexSubImage and friends copy from the active pixel_unpack_buffer by
+	// glTexSubImage and friends copy to the active PIXEL_PACK_BUFFER by
 	// treating the pointer as a byte offset.
 	uint8 *byteoffset = (uint8 *)(ptrdiff_t)destoffset;
 
-	if (gl.isCopyTextureToBufferSupported())
-	{
-		if (isCompressed())
-			glGetCompressedTextureSubImage(texture, mipmap, rect.x, rect.y, slice, rect.w, rect.h, 1, size, byteoffset);
-		else
-			glGetTextureSubImage(texture, mipmap, rect.x, rect.y, slice, rect.w, rect.h, 1, fmt.externalformat, fmt.type, size, byteoffset);
-	}
-	else if (fbo)
-	{
-		GLuint current_fbo = gl.getFramebuffer(OpenGL::FRAMEBUFFER_ALL);
-		gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, getFBO());
-
-		if (slice > 0 || mipmap > 0)
-		{
-			int layer = texType == TEXTURE_CUBE ? 0 : slice;
-			int face = texType == TEXTURE_CUBE ? slice : 0;
-			gl.framebufferTexture(GL_COLOR_ATTACHMENT0, texType, texture, mipmap, layer, face);
-		}
-
-		glReadPixels(rect.x, rect.y, rect.w, rect.h, fmt.externalformat, fmt.type, byteoffset);
-
-		if (slice > 0 || mipmap > 0)
-			gl.framebufferTexture(GL_COLOR_ATTACHMENT0, texType, texture, 0, 0, 0);
-
-		gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, current_fbo);
-	}
+	readbackInternal(slice, mipmap, rect, destwidth, size, byteoffset);
 
-	glPixelStorei(GL_PACK_ROW_LENGTH, 0);
 	glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
 }
 

+ 2 - 2
src/modules/graphics/opengl/Texture.h

@@ -58,6 +58,8 @@ public:
 
 	inline GLuint getFBO() const { return fbo; }
 
+	void readbackInternal(int slice, int mipmap, const Rect &rect, int destwidth, size_t size, void *dest);
+
 private:
 	void createTexture();
 
@@ -65,8 +67,6 @@ private:
 
 	void generateMipmapsInternal() override;
 
-	void readbackImageData(love::image::ImageData *imagedata, int slice, int mipmap, const Rect &rect) override;
-
 	Slices slices;
 
 	GLuint fbo;

+ 4 - 4
src/modules/graphics/vertex.cpp

@@ -383,10 +383,10 @@ STRINGMAP_END(IndexDataType, INDEX_MAX_ENUM, indexType)
 
 STRINGMAP_BEGIN(BufferDataUsage, BUFFERDATAUSAGE_MAX_ENUM, bufferDataUsage)
 {
-	{ "stream",  BUFFERDATAUSAGE_STREAM  },
-	{ "dynamic", BUFFERDATAUSAGE_DYNAMIC },
-	{ "static",  BUFFERDATAUSAGE_STATIC  },
-	{ "staging", BUFFERDATAUSAGE_STAGING },
+	{ "stream",   BUFFERDATAUSAGE_STREAM   },
+	{ "dynamic",  BUFFERDATAUSAGE_DYNAMIC  },
+	{ "static",   BUFFERDATAUSAGE_STATIC   },
+	{ "readback", BUFFERDATAUSAGE_READBACK },
 }
 STRINGMAP_END(BufferDataUsage, BUFFERDATAUSAGE_MAX_ENUM, bufferDataUsage)
 

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

@@ -111,7 +111,7 @@ enum BufferDataUsage
 	BUFFERDATAUSAGE_STREAM,
 	BUFFERDATAUSAGE_DYNAMIC,
 	BUFFERDATAUSAGE_STATIC,
-	BUFFERDATAUSAGE_STAGING,
+	BUFFERDATAUSAGE_READBACK,
 	BUFFERDATAUSAGE_MAX_ENUM
 };
 

+ 126 - 0
src/modules/graphics/wrap_Graphics.cpp

@@ -2141,6 +2141,126 @@ int w_newVideo(lua_State *L)
 	return 1;
 }
 
+int w_readbackBuffer(lua_State *L)
+{
+	Buffer *b = luax_checkbuffer(L, 1);
+	lua_Integer offset = luaL_optinteger(L, 2, 0);
+	lua_Integer size = luaL_optinteger(L, 3, b->getSize() - offset);
+
+	data::ByteData *dest = nullptr;
+	size_t destoffset = 0;
+	if (!lua_isnoneornil(L, 4))
+	{
+		dest = luax_checktype<data::ByteData>(L, 4);
+		destoffset = (size_t) luaL_optinteger(L, 5, 0);
+	}
+
+	love::data::ByteData *data = nullptr;
+	luax_catchexcept(L, [&]() { data = instance()->readbackBuffer(b, offset, size, dest, destoffset); });
+
+	luax_pushtype(L, data);
+	data->release();
+	return 1;
+}
+
+int w_readbackBufferAsync(lua_State *L)
+{
+	Buffer *b = luax_checkbuffer(L, 1);
+	lua_Integer offset = luaL_optinteger(L, 2, 0);
+	lua_Integer size = luaL_optinteger(L, 3, b->getSize() - offset);
+
+	data::ByteData *dest = nullptr;
+	size_t destoffset = 0;
+	if (!lua_isnoneornil(L, 4))
+	{
+		dest = luax_checktype<data::ByteData>(L, 4);
+		destoffset = (size_t) luaL_optinteger(L, 5, 0);
+	}
+
+	GraphicsReadback *r = nullptr;
+	luax_catchexcept(L, [&]() { r = instance()->readbackBufferAsync(b, offset, size, dest, destoffset); });
+
+	luax_pushtype(L, r);
+	r->release();
+	return 1;
+}
+
+int w_readbackTexture(lua_State *L)
+{
+	Texture *t = luax_checktexture(L, 1);
+
+	int slice = 0;
+	if (t->getTextureType() != TEXTURE_2D)
+		slice = (int) luaL_checkinteger(L, 2) - 1;
+
+	int mipmap = (int) luaL_optinteger(L, 3, 1) - 1;
+
+	Rect rect = {0, 0, t->getPixelWidth(mipmap), t->getPixelHeight(mipmap)};
+	if (!lua_isnoneornil(L, 4))
+	{
+		rect.x = (int) luaL_checkinteger(L, 4);
+		rect.y = (int) luaL_checkinteger(L, 5);
+		rect.w = (int) luaL_checkinteger(L, 6);
+		rect.h = (int) luaL_checkinteger(L, 7);
+	}
+
+	image::ImageData *dest = nullptr;
+	int destx = 0;
+	int desty = 0;
+
+	if (!lua_isnoneornil(L, 8))
+	{
+		dest = luax_checktype<image::ImageData>(L, 8);
+		destx = (int) luaL_optinteger(L, 9, 0);
+		desty = (int) luaL_optinteger(L, 10, 0);
+	}
+
+	image::ImageData *imagedata = nullptr;
+	luax_catchexcept(L, [&]() { imagedata = instance()->readbackTexture(t, slice, mipmap, rect, dest, destx, desty); });
+
+	luax_pushtype(L, imagedata);
+	imagedata->release();
+	return 1;
+}
+
+int w_readbackTextureAsync(lua_State *L)
+{
+	Texture *t = luax_checktexture(L, 1);
+
+	int slice = 0;
+	if (t->getTextureType() != TEXTURE_2D)
+		slice = (int) luaL_checkinteger(L, 2) - 1;
+
+	int mipmap = (int) luaL_optinteger(L, 3, 1) - 1;
+
+	Rect rect = {0, 0, t->getPixelWidth(mipmap), t->getPixelHeight(mipmap)};
+	if (!lua_isnoneornil(L, 4))
+	{
+		rect.x = (int) luaL_checkinteger(L, 4);
+		rect.y = (int) luaL_checkinteger(L, 5);
+		rect.w = (int) luaL_checkinteger(L, 6);
+		rect.h = (int) luaL_checkinteger(L, 7);
+	}
+
+	image::ImageData *dest = nullptr;
+	int destx = 0;
+	int desty = 0;
+
+	if (!lua_isnoneornil(L, 8))
+	{
+		dest = luax_checktype<image::ImageData>(L, 8);
+		destx = (int) luaL_optinteger(L, 9, 0);
+		desty = (int) luaL_optinteger(L, 10, 0);
+	}
+
+	GraphicsReadback *r = nullptr;
+	luax_catchexcept(L, [&]() { r = instance()->readbackTextureAsync(t, slice, mipmap, rect, dest, destx, desty); });
+
+	luax_pushtype(L, r);
+	r->release();
+	return 1;
+}
+
 int w_setColor(lua_State *L)
 {
 	Colorf c;
@@ -3650,6 +3770,11 @@ static const luaL_Reg functions[] =
 	{ "newText", w_newText },
 	{ "_newVideo", w_newVideo },
 
+	{ "readbackBuffer", w_readbackBuffer },
+	{ "readbackBufferAsync", w_readbackBufferAsync },
+	{ "readbackTexture", w_readbackTexture },
+	{ "readbackTextureAsync", w_readbackTextureAsync },
+
 	{ "validateShader", w_validateShader },
 
 	{ "setCanvas", w_setCanvas },
@@ -3787,6 +3912,7 @@ static const lua_CFunction types[] =
 	luaopen_font,
 	luaopen_quad,
 	luaopen_graphicsbuffer,
+	luaopen_graphicsreadback,
 	luaopen_spritebatch,
 	luaopen_particlesystem,
 	luaopen_shader,

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

@@ -32,6 +32,7 @@
 #include "wrap_Text.h"
 #include "wrap_Video.h"
 #include "wrap_Buffer.h"
+#include "wrap_GraphicsReadback.h"
 #include "Graphics.h"
 
 namespace love

+ 95 - 0
src/modules/graphics/wrap_GraphicsReadback.cpp

@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2006-2021 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.
+ **/
+
+// LOVE
+#include "wrap_GraphicsReadback.h"
+#include "data/ByteData.h"
+#include "image/ImageData.h"
+
+namespace love
+{
+namespace graphics
+{
+
+GraphicsReadback *luax_checkgraphicsreadback(lua_State *L, int idx)
+{
+	return luax_checktype<GraphicsReadback>(L, idx);
+}
+
+int w_GraphicsReadback_isComplete(lua_State *L)
+{
+	GraphicsReadback *t = luax_checkgraphicsreadback(L, 1);
+	luax_pushboolean(L, t->isComplete());
+	return 1;
+}
+
+int w_GraphicsReadback_hasError(lua_State *L)
+{
+	GraphicsReadback *t = luax_checkgraphicsreadback(L, 1);
+	luax_pushboolean(L, t->hasError());
+	return 1;
+}
+
+int w_GraphicsReadback_wait(lua_State *L)
+{
+	GraphicsReadback *t = luax_checkgraphicsreadback(L, 1);
+	t->wait();
+	return 0;
+}
+
+int w_GraphicsReadback_update(lua_State *L)
+{
+	GraphicsReadback *t = luax_checkgraphicsreadback(L, 1);
+	luax_catchexcept(L, [&]() { t->update(); });
+	return 0;
+}
+
+int w_GraphicsReadback_getBufferData(lua_State *L)
+{
+	GraphicsReadback *t = luax_checkgraphicsreadback(L, 1);
+	luax_pushtype(L, t->getBufferData());
+	return 1;
+}
+
+int w_GraphicsReadback_getImageData(lua_State *L)
+{
+	GraphicsReadback *t = luax_checkgraphicsreadback(L, 1);
+	luax_pushtype(L, t->getImageData());
+	return 1;
+}
+
+static const luaL_Reg w_GraphicsReadback_functions[] =
+{
+	{ "isComplete", w_GraphicsReadback_isComplete },
+	{ "hasError", w_GraphicsReadback_hasError },
+	{ "wait", w_GraphicsReadback_wait },
+	{ "update", w_GraphicsReadback_update },
+	{ "getBufferData", w_GraphicsReadback_getBufferData },
+	{ "getImageData", w_GraphicsReadback_getImageData },
+	{ 0, 0 }
+};
+
+extern "C" int luaopen_graphicsreadback(lua_State *L)
+{
+	return luax_register_type(L, &GraphicsReadback::type, w_GraphicsReadback_functions, nullptr);
+}
+
+} // graphics
+} // love

+ 36 - 0
src/modules/graphics/wrap_GraphicsReadback.h

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2006-2021 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
+
+// LOVE
+#include "common/runtime.h"
+#include "GraphicsReadback.h"
+
+namespace love
+{
+namespace graphics
+{
+
+GraphicsReadback *luax_checkgraphicsreadback(lua_State *L, int idx);
+extern "C" int luaopen_graphicsreadback(lua_State *L);
+
+} // graphics
+} // love

+ 12 - 6
src/modules/graphics/wrap_Texture.cpp

@@ -385,16 +385,15 @@ int w_Texture_replacePixels(lua_State *L)
 
 int w_Texture_newImageData(lua_State *L)
 {
+	luax_markdeprecated(L, 1, "Texture:newImageData", API_METHOD, DEPRECATED_RENAMED, "love.graphics.readbackTexture");
+
 	Texture *t = luax_checktexture(L, 1);
-	love::image::Image *image = luax_getmodule<love::image::Image>(L, love::image::Image::type);
 
 	int slice = 0;
-	int mipmap = 0;
-
 	if (t->getTextureType() != TEXTURE_2D)
 		slice = (int) luaL_checkinteger(L, 2) - 1;
 
-	mipmap = (int) luaL_optinteger(L, 3, 1) - 1;
+	int mipmap = (int) luaL_optinteger(L, 3, 1) - 1;
 
 	Rect rect = {0, 0, t->getPixelWidth(mipmap), t->getPixelHeight(mipmap)};
 	if (!lua_isnoneornil(L, 4))
@@ -405,8 +404,12 @@ int w_Texture_newImageData(lua_State *L)
 		rect.h = (int) luaL_checkinteger(L, 7);
 	}
 
+	auto gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);
+	if (gfx == nullptr)
+		return luaL_error(L, "Cannot find Graphics module.");
+
 	love::image::ImageData *img = nullptr;
-	luax_catchexcept(L, [&](){ img = t->newImageData(image, slice, mipmap, rect); });
+	luax_catchexcept(L, [&](){ img = gfx->readbackTexture(t, slice, mipmap, rect, nullptr, 0, 0); });
 
 	luax_pushtype(L, img);
 	img->release();
@@ -501,8 +504,11 @@ const luaL_Reg w_Texture_functions[] =
 	{ "setDepthSampleMode", w_Texture_setDepthSampleMode },
 	{ "generateMipmaps", w_Texture_generateMipmaps },
 	{ "replacePixels", w_Texture_replacePixels },
-	{ "newImageData", w_Texture_newImageData },
 	{ "renderTo", w_Texture_renderTo },
+
+	// Deprecated
+	{ "newImageData", w_Texture_newImageData },
+
 	{ 0, 0 }
 };
 

+ 2 - 0
src/modules/keyboard/sdl/Keyboard.cpp

@@ -99,6 +99,8 @@ bool Keyboard::isModifierActive(ModifierKey key) const
 		return (modstate & KMOD_SCROLL) != 0;
 	case MODKEY_MODE:
 		return (modstate & KMOD_MODE) != 0;
+	default:
+		break;
 	}
 
 	return false;

+ 5 - 0
src/modules/love/callbacks.lua

@@ -129,6 +129,11 @@ function love.createhandlers()
 		localechanged = function ()
 			if love.localechanged then return love.localechanged() end
 		end,
+		audiodisconnected = function (sources)
+			if not love.audiodisconnected or not love.audiodisconnected(sources) then
+				love.audio.setPlaybackDevice()
+			end
+		end,
 	}, {
 		__index = function(self, name)
 			error("Unknown event: " .. name)