Browse Source

Add sync and async texture and buffer readback APIs.

- Add imagedata = love.graphics.readbackTexture(texture, [slice, mipmap, x, y, w, h, [dest, destx, desty]]).
- Add readback = love.graphics.readbackTextureAsync(texture, [slice, mipmap, x, y, w, h, [dest, destx, desty]]).
- Add bytedata = love.graphics.readbackBuffer(buffer, [offset, size, [dest, destoffset]]).
- Add readback = ove.graphics.readbackBufferAsync(buffer, [offset, size, [dest, destoffset]]).
- Deprecate Texture:newImageData.

The async variants return a new GraphicsReadback object. It has the following methods:
- isComplete()
- hasError()
- wait()
- getBufferData()
- getImageData()
- update() (called automatically every frame by love, not normally needed).

Support notes:
- readbackBuffer and readbackBufferAsync require buffer copying support.
- readbackTexture with a render target (canvas) is always supported. readbackTexture with a non-render-target requires copy-texture-to-buffer support.
- readbackTextureAsync with a render target requires copy-render-target-to-buffer support. readbackTextureAsync with a non-render-target requires copy-texture-to-buffer support.
Alex Szpakowski 3 years ago
parent
commit
3a5c3df02f

+ 6 - 0
CMakeLists.txt

@@ -512,6 +512,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
@@ -547,6 +549,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
@@ -572,6 +576,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

+ 36 - 0
platform/xcode/liblove.xcodeproj/project.pbxproj

@@ -826,12 +826,22 @@
 		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 */; };
+		FA6BDF89280B62A000240F2A /* GraphicsReadback.mm in Sources */ = {isa = PBXBuildFile; fileRef = FA6BDF88280B62A000240F2A /* GraphicsReadback.mm */; };
+		FA6BDF8A280B62A000240F2A /* GraphicsReadback.mm in Sources */ = {isa = PBXBuildFile; fileRef = FA6BDF88280B62A000240F2A /* GraphicsReadback.mm */; };
 		FA76344A1E28722A0066EF9E /* StreamBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA7634481E28722A0066EF9E /* StreamBuffer.cpp */; };
 		FA76344B1E28722A0066EF9E /* StreamBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA7634481E28722A0066EF9E /* StreamBuffer.cpp */; };
 		FA76344C1E28722A0066EF9E /* StreamBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = FA7634491E28722A0066EF9E /* StreamBuffer.h */; };
 		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 */; };
@@ -1909,6 +1919,8 @@
 		FA6BDE5B1F31725300786805 /* Color.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Color.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>"; };
+		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>"; };
 		FA7634481E28722A0066EF9E /* StreamBuffer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StreamBuffer.cpp; sourceTree = "<group>"; };
 		FA7634491E28722A0066EF9E /* StreamBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StreamBuffer.h; sourceTree = "<group>"; };
 		FA7DA04C1C16874A0056B200 /* wrap_Math.lua */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = wrap_Math.lua; 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 */,
@@ -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 */,

+ 55 - 0
src/modules/graphics/Graphics.cpp

@@ -235,6 +235,7 @@ Graphics::~Graphics()
 	for (int i = 0; i < (int) SHADERSTAGE_MAX_ENUM; i++)
 		cachedShaderStages[i].clear();
 
+	pendingReadbacks.clear();
 	clearTemporaryResources();
 
 	Shader::deinitialize();
@@ -434,6 +435,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);
@@ -1120,6 +1161,20 @@ void Graphics::clearTemporaryResources()
 	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;

+ 13 - 0
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"
@@ -460,6 +461,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);
 
 	/**
@@ -989,6 +996,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;
@@ -1002,6 +1012,8 @@ protected:
 	void updateTemporaryResources();
 	void clearTemporaryResources();
 
+	void updatePendingReadbacks();
+
 	void restoreState(const DisplayState &s);
 	void restoreStateChecked(const DisplayState &s);
 
@@ -1023,6 +1035,7 @@ protected:
 	StrongRef<love::graphics::Font> defaultFont;
 
 	std::vector<ScreenshotInfo> pendingScreenshotCallbacks;
+	std::vector<StrongRef<GraphicsReadback>> pendingReadbacks;
 
 	BatchedDrawState batchedDrawState;
 

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

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

+ 12 - 0
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;
@@ -1617,6 +1628,7 @@ void Graphics::present(void *screenshotCallbackData)
 	renderTargetSwitchCount = 0;
 	drawCallsBatched = 0;
 
+	updatePendingReadbacks();
 	updateTemporaryResources();
 }}
 

+ 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();

+ 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();
 

+ 12 - 0
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;
@@ -1333,6 +1344,7 @@ void Graphics::present(void *screenshotCallbackData)
 	renderTargetSwitchCount = 0;
 	drawCallsBatched = 0;
 
+	updatePendingReadbacks();
 	updateTemporaryResources();
 }
 

+ 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

+ 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();
@@ -66,8 +68,6 @@ private:
 
 	void generateMipmapsInternal() override;
 
-	void readbackImageData(love::image::ImageData *imagedata, int slice, int mipmap, const Rect &rect) override;
-
 	Slices slices;
 
 	GLuint fbo;

+ 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 }
 };