Browse Source

Add love.graphics.copyTextureToBuffer and copyBufferToTexture.

Alex Szpakowski 3 years ago
parent
commit
52101b2563

+ 4 - 2
src/modules/data/ByteData.cpp

@@ -42,7 +42,8 @@ ByteData::ByteData(const void *d, size_t size)
 	: size(size)
 	: size(size)
 {
 {
 	create();
 	create();
-	memcpy(data, d, size);
+	if (d != nullptr)
+		memcpy(data, d, size);
 }
 }
 
 
 ByteData::ByteData(void *d, size_t size, bool own)
 ByteData::ByteData(void *d, size_t size, bool own)
@@ -53,7 +54,8 @@ ByteData::ByteData(void *d, size_t size, bool own)
 	else
 	else
 	{
 	{
 		create();
 		create();
-		memcpy(data, d, size);
+		if (d != nullptr)
+			memcpy(data, d, size);
 	}
 	}
 }
 }
 
 

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

@@ -1076,6 +1076,169 @@ void Graphics::copyBuffer(Buffer *source, Buffer *dest, size_t sourceoffset, siz
 	source->copyTo(dest, sourceoffset, destoffset, size);
 	source->copyTo(dest, sourceoffset, destoffset, size);
 }
 }
 
 
+void Graphics::copyTextureToBuffer(Texture *source, Buffer *dest, int slice, int mipmap, const Rect &rect, size_t destoffset, int destwidth)
+{
+	if (!capabilities.features[FEATURE_COPY_TEXTURE_TO_BUFFER])
+	{
+		if (!source->isRenderTarget())
+			throw love::Exception("Copying a non-render target Texture to a Buffer is not supported on this system.");
+
+		if (!capabilities.features[FEATURE_COPY_RENDER_TARGET_TO_BUFFER])
+			throw love::Exception("Copying a render target Texture to a Buffer is not supported on this system.");
+	}
+
+	PixelFormat format = source->getPixelFormat();
+
+	if (isPixelFormatDepthStencil(format))
+		throw love::Exception("Copying a depth/stencil Texture to a Buffer is not supported.");
+
+	if (!source->isReadable())
+		throw love::Exception("copyTextureToBuffer can only be called on readable Textures.");
+
+	if (dest->getDataUsage() == BUFFERDATAUSAGE_STREAM)
+		throw love::Exception("Buffers created with 'stream' data usage cannot be used as a copy destination.");
+
+	if (isRenderTargetActive(source))
+		throw love::Exception("copyTextureToBuffer cannot be called while the Texture is an active render target.");
+
+	if (mipmap < 0 || mipmap >= source->getMipmapCount())
+		throw love::Exception("Invalid texture mipmap index %d.", mipmap + 1);
+
+	TextureType textype = source->getTextureType();
+	if (slice < 0 || (textype == TEXTURE_CUBE && slice >= 6)
+		|| (textype == TEXTURE_VOLUME && slice >= source->getDepth(mipmap))
+		|| (textype == TEXTURE_2D_ARRAY && slice >= source->getLayerCount()))
+	{
+		throw love::Exception("Invalid texture slice index %d.", slice + 1);
+	}
+
+	int mipw = source->getPixelWidth(mipmap);
+	int miph = source->getPixelHeight(mipmap);
+
+	if (rect.x < 0 || rect.y < 0 || rect.w <= 0 || rect.h <= 0
+		|| (rect.x + rect.w) > mipw || (rect.y + rect.h) > miph)
+	{
+		throw love::Exception("Invalid rectangle dimensions (x=%d, y=%d, w=%d, h=%d) for %dx%d texture.", rect.x, rect.y, rect.w, rect.h, mipw, miph);
+	}
+
+	if (destwidth <= 0)
+		destwidth = rect.w;
+
+	size_t size = 0;
+
+	if (isPixelFormatCompressed(format))
+	{
+		if (destwidth != rect.w) // OpenGL limitation...
+			throw love::Exception("Copying a compressed texture to a buffer cannot use a custom destination width.");
+
+		const PixelFormatInfo &info = getPixelFormatInfo(format);
+		int bw = (int) info.blockWidth;
+		int bh = (int) info.blockHeight;
+		if (rect.x % bw != 0 || rect.y % bh != 0 ||
+			((rect.w % bw != 0 || rect.h % bh != 0) && rect.x + rect.w != source->getPixelWidth(mipmap)))
+		{
+			const char *name = nullptr;
+			love::getConstant(format, name);
+			throw love::Exception("Compressed texture format %s only supports copying a sub-rectangle with offset and dimensions that are a multiple of %d x %d.", name, bw, bh);
+		}
+
+		// Note: this will need to change if destwidth == rect.w restriction
+		// is removed.
+		size = getPixelFormatSliceSize(format, destwidth, rect.h);
+	}
+	else
+	{
+		// Not the cleanest, but should work since uncompressed formats always
+		// have 1x1 blocks.
+		int pixels = (rect.h - 1) * destwidth + rect.w;
+		size = getPixelFormatUncompressedRowSize(format, pixels);
+	}
+
+	Range destrange(destoffset, size);
+
+	if (destrange.getMax() >= dest->getSize())
+		throw love::Exception("Buffer copy destination offset and width/height doesn't fit within the destination Buffer.");
+
+	source->copyToBuffer(dest, slice, mipmap, rect, destoffset, destwidth, size);
+}
+
+void Graphics::copyBufferToTexture(Buffer *source, Texture *dest, size_t sourceoffset, int sourcewidth, int slice, int mipmap, const Rect &rect)
+{
+	if (!capabilities.features[FEATURE_COPY_BUFFER_TO_TEXTURE])
+		throw love::Exception("Copying a Buffer to a Texture is not supported on this system.");
+
+	PixelFormat format = dest->getPixelFormat();
+
+	if (isPixelFormatDepthStencil(format))
+		throw love::Exception("Copying a Buffer to a depth/stencil Texture is not supported.");
+
+	if (!dest->isReadable())
+		throw love::Exception("copyBufferToTexture can only be called on readable Textures.");
+
+	if (isRenderTargetActive(dest))
+		throw love::Exception("copyBufferToTexture cannot be called while the Texture is an active render target.");
+
+	if (mipmap < 0 || mipmap >= dest->getMipmapCount())
+		throw love::Exception("Invalid texture mipmap index %d.", mipmap + 1);
+
+	TextureType textype = dest->getTextureType();
+	if (slice < 0 || (textype == TEXTURE_CUBE && slice >= 6)
+		|| (textype == TEXTURE_VOLUME && slice >= dest->getDepth(mipmap))
+		|| (textype == TEXTURE_2D_ARRAY && slice >= dest->getLayerCount()))
+	{
+		throw love::Exception("Invalid texture slice index %d.", slice + 1);
+	}
+
+	int mipw = dest->getPixelWidth(mipmap);
+	int miph = dest->getPixelHeight(mipmap);
+
+	if (rect.x < 0 || rect.y < 0 || rect.w <= 0 || rect.h <= 0
+		|| (rect.x + rect.w) > mipw || (rect.y + rect.h) > miph)
+	{
+		throw love::Exception("Invalid rectangle dimensions (x=%d, y=%d, w=%d, h=%d) for %dx%d texture.", rect.x, rect.y, rect.w, rect.h, mipw, miph);
+	}
+
+	if (sourcewidth <= 0)
+		sourcewidth = rect.w;
+
+	size_t size = 0;
+
+	if (isPixelFormatCompressed(format))
+	{
+		if (sourcewidth != rect.w) // OpenGL limitation...
+			throw love::Exception("Copying a buffer to a compressed texture cannot use a custom source width.");
+
+		const PixelFormatInfo &info = getPixelFormatInfo(format);
+		int bw = (int) info.blockWidth;
+		int bh = (int) info.blockHeight;
+		if (rect.x % bw != 0 || rect.y % bh != 0 ||
+			((rect.w % bw != 0 || rect.h % bh != 0) && rect.x + rect.w != dest->getPixelWidth(mipmap)))
+		{
+			const char *name = nullptr;
+			love::getConstant(format, name);
+			throw love::Exception("Compressed texture format %s only supports copying a sub-rectangle with offset and dimensions that are a multiple of %d x %d.", name, bw, bh);
+		}
+
+		// Note: this will need to change if sourcewidth == rect.w restriction
+		// is removed.
+		size = getPixelFormatSliceSize(format, sourcewidth, rect.h);
+	}
+	else
+	{
+		// Not the cleanest, but should work since uncompressed formats always
+		// have 1x1 blocks.
+		int pixels = (rect.h - 1) * sourcewidth + rect.w;
+		size = getPixelFormatUncompressedRowSize(format, pixels);
+	}
+
+	Range sourcerange(sourceoffset, size);
+
+	if (sourcerange.getMax() >= source->getSize())
+		throw love::Exception("Buffer copy source offset and width/height doesn't fit within the source Buffer.");
+
+	dest->copyFromBuffer(source, sourceoffset, sourcewidth, size, slice, mipmap, rect);
+}
+
 void Graphics::dispatchThreadgroups(Shader* shader, int x, int y, int z)
 void Graphics::dispatchThreadgroups(Shader* shader, int x, int y, int z)
 {
 {
 	if (!shader->hasStage(SHADERSTAGE_COMPUTE))
 	if (!shader->hasStage(SHADERSTAGE_COMPUTE))
@@ -1907,6 +2070,9 @@ STRINGMAP_CLASS_BEGIN(Graphics, Graphics::Feature, Graphics::FEATURE_MAX_ENUM, f
 	{ "instancing",               Graphics::FEATURE_INSTANCING           },
 	{ "instancing",               Graphics::FEATURE_INSTANCING           },
 	{ "texelbuffer",              Graphics::FEATURE_TEXEL_BUFFER         },
 	{ "texelbuffer",              Graphics::FEATURE_TEXEL_BUFFER         },
 	{ "copybuffer",               Graphics::FEATURE_COPY_BUFFER          },
 	{ "copybuffer",               Graphics::FEATURE_COPY_BUFFER          },
+	{ "copybuffertotexture",      Graphics::FEATURE_COPY_BUFFER_TO_TEXTURE },
+	{ "copytexturetobuffer",      Graphics::FEATURE_COPY_TEXTURE_TO_BUFFER },
+	{ "copyrendertargettobuffer", Graphics::FEATURE_COPY_RENDER_TARGET_TO_BUFFER },
 }
 }
 STRINGMAP_CLASS_END(Graphics, Graphics::Feature, Graphics::FEATURE_MAX_ENUM, feature)
 STRINGMAP_CLASS_END(Graphics, Graphics::Feature, Graphics::FEATURE_MAX_ENUM, feature)
 
 

+ 5 - 0
src/modules/graphics/Graphics.h

@@ -145,6 +145,9 @@ public:
 		FEATURE_INSTANCING,
 		FEATURE_INSTANCING,
 		FEATURE_TEXEL_BUFFER,
 		FEATURE_TEXEL_BUFFER,
 		FEATURE_COPY_BUFFER,
 		FEATURE_COPY_BUFFER,
+		FEATURE_COPY_BUFFER_TO_TEXTURE,
+		FEATURE_COPY_TEXTURE_TO_BUFFER,
+		FEATURE_COPY_RENDER_TARGET_TO_BUFFER,
 		FEATURE_MAX_ENUM
 		FEATURE_MAX_ENUM
 	};
 	};
 
 
@@ -674,6 +677,8 @@ public:
 	void captureScreenshot(const ScreenshotInfo &info);
 	void captureScreenshot(const ScreenshotInfo &info);
 
 
 	void copyBuffer(Buffer *source, Buffer *dest, size_t sourceoffset, size_t destoffset, size_t size);
 	void copyBuffer(Buffer *source, Buffer *dest, size_t sourceoffset, size_t destoffset, size_t size);
+	void copyTextureToBuffer(Texture *source, Buffer *dest, int slice, int mipmap, const Rect &rect, size_t destoffset, int destwidth);
+	void copyBufferToTexture(Buffer *source, Texture *dest, size_t sourceoffset, int sourcewidth, int slice, int mipmap, const Rect &rect);
 
 
 	void dispatchThreadgroups(Shader* shader, int x, int y, int z);
 	void dispatchThreadgroups(Shader* shader, int x, int y, int z);
 
 

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

@@ -46,6 +46,7 @@ namespace graphics
 {
 {
 
 
 class Graphics;
 class Graphics;
+class Buffer;
 
 
 enum TextureType
 enum TextureType
 {
 {
@@ -246,6 +247,9 @@ public:
 
 
 	love::image::ImageData *newImageData(love::image::Image *module, int slice, int mipmap, const Rect &rect);
 	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;
+
 	virtual ptrdiff_t getRenderTargetHandle() const = 0;
 	virtual ptrdiff_t getRenderTargetHandle() const = 0;
 
 
 	TextureType getTextureType() const;
 	TextureType getTextureType() const;

+ 4 - 1
src/modules/graphics/opengl/Graphics.cpp

@@ -1640,7 +1640,10 @@ void Graphics::initCapabilities()
 	capabilities.features[FEATURE_INSTANCING] = gl.isInstancingSupported();
 	capabilities.features[FEATURE_INSTANCING] = gl.isInstancingSupported();
 	capabilities.features[FEATURE_TEXEL_BUFFER] = gl.isBufferUsageSupported(BUFFERUSAGE_TEXEL);
 	capabilities.features[FEATURE_TEXEL_BUFFER] = gl.isBufferUsageSupported(BUFFERUSAGE_TEXEL);
 	capabilities.features[FEATURE_COPY_BUFFER] = gl.isCopyBufferSupported();
 	capabilities.features[FEATURE_COPY_BUFFER] = gl.isCopyBufferSupported();
-	static_assert(FEATURE_MAX_ENUM == 12, "Graphics::initCapabilities must be updated when adding a new graphics feature!");
+	capabilities.features[FEATURE_COPY_BUFFER_TO_TEXTURE] = gl.isCopyBufferToTextureSupported();
+	capabilities.features[FEATURE_COPY_TEXTURE_TO_BUFFER] = gl.isCopyTextureToBufferSupported();
+	capabilities.features[FEATURE_COPY_RENDER_TARGET_TO_BUFFER] = gl.isCopyRenderTargetToBufferSupported();
+	static_assert(FEATURE_MAX_ENUM == 15, "Graphics::initCapabilities must be updated when adding a new graphics feature!");
 
 
 	capabilities.limits[LIMIT_POINT_SIZE] = gl.getMaxPointSize();
 	capabilities.limits[LIMIT_POINT_SIZE] = gl.getMaxPointSize();
 	capabilities.limits[LIMIT_TEXTURE_SIZE] = gl.getMax2DTextureSize();
 	capabilities.limits[LIMIT_TEXTURE_SIZE] = gl.getMax2DTextureSize();

+ 18 - 0
src/modules/graphics/opengl/OpenGL.cpp

@@ -1530,6 +1530,24 @@ bool OpenGL::isCopyBufferSupported() const
 	return GLAD_VERSION_3_1 || GLAD_ES_VERSION_3_0;
 	return GLAD_VERSION_3_1 || GLAD_ES_VERSION_3_0;
 }
 }
 
 
+bool OpenGL::isCopyBufferToTextureSupported() const
+{
+	// Requires pixel unpack buffer binding support.
+	return GLAD_VERSION_2_0 || GLAD_ES_VERSION_3_0;
+}
+
+bool OpenGL::isCopyTextureToBufferSupported() const
+{
+	// Requires glGetTextureSubImage support.
+	return GLAD_VERSION_4_5;
+}
+
+bool OpenGL::isCopyRenderTargetToBufferSupported() const
+{
+	// Requires pixel pack buffer binding support.
+	return GLAD_VERSION_2_0 || GLAD_ES_VERSION_3_0;
+}
+
 int OpenGL::getMax2DTextureSize() const
 int OpenGL::getMax2DTextureSize() const
 {
 {
 	return std::max(max2DTextureSize, 1);
 	return std::max(max2DTextureSize, 1);

+ 3 - 0
src/modules/graphics/opengl/OpenGL.h

@@ -372,6 +372,9 @@ public:
 	bool isBaseVertexSupported() const;
 	bool isBaseVertexSupported() const;
 	bool isMultiFormatMRTSupported() const;
 	bool isMultiFormatMRTSupported() const;
 	bool isCopyBufferSupported() const;
 	bool isCopyBufferSupported() const;
+	bool isCopyBufferToTextureSupported() const;
+	bool isCopyTextureToBufferSupported() const;
+	bool isCopyRenderTargetToBufferSupported() const;
 
 
 	/**
 	/**
 	 * Returns the maximum supported width or height of a texture.
 	 * Returns the maximum supported width or height of a texture.

+ 70 - 0
src/modules/graphics/opengl/Texture.cpp

@@ -22,6 +22,7 @@
 
 
 #include "graphics/Graphics.h"
 #include "graphics/Graphics.h"
 #include "Graphics.h"
 #include "Graphics.h"
+#include "Buffer.h"
 #include "common/int.h"
 #include "common/int.h"
 
 
 // STD
 // STD
@@ -524,6 +525,75 @@ void Texture::readbackImageData(love::image::ImageData *data, int slice, int mip
 	gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, current_fbo);
 	gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, current_fbo);
 }
 }
 
 
+void Texture::copyFromBuffer(love::graphics::Buffer *source, size_t sourceoffset, int sourcewidth, size_t size, int slice, int mipmap, const Rect &rect)
+{
+	// Higher level code does validation.
+
+	GLuint glbuffer = (GLuint) source->getHandle();
+	glBindBuffer(GL_PIXEL_UNPACK_BUFFER, glbuffer);
+
+	if (!isCompressed()) // Not supported in GL with compressed textures...
+		glPixelStorei(GL_UNPACK_ROW_LENGTH, sourcewidth);
+
+	// glTexSubImage and friends copy from the active pixel_unpack_buffer by
+	// treating the pointer as a byte offset.
+	const uint8 *byteoffset = (const uint8 *)(ptrdiff_t)sourceoffset;
+	uploadByteData(format, byteoffset, size, mipmap, slice, rect);
+
+	glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+	glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+}
+
+void Texture::copyToBuffer(love::graphics::Buffer *dest, int slice, int mipmap, const Rect &rect, size_t destoffset, int destwidth, size_t size)
+{
+	// Higher level code does validation.
+
+	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
+	// treating the pointer as a byte offset.
+	uint8 *byteoffset = (uint8 *)(ptrdiff_t)destoffset;
+
+	if (GLAD_VERSION_4_5)
+	{
+		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);
+	}
+
+	glPixelStorei(GL_PACK_ROW_LENGTH, 0);
+	glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+}
+
 void Texture::setSamplerState(const SamplerState &s)
 void Texture::setSamplerState(const SamplerState &s)
 {
 {
 	if (s.depthSampleMode.hasValue && !gl.isDepthCompareSampleSupported())
 	if (s.depthSampleMode.hasValue && !gl.isDepthCompareSampleSupported())

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

@@ -46,6 +46,9 @@ public:
 	bool loadVolatile() override;
 	bool loadVolatile() override;
 	void unloadVolatile() override;
 	void unloadVolatile() override;
 
 
+	void copyFromBuffer(love::graphics::Buffer *source, size_t sourceoffset, int sourcewidth, size_t size, int slice, int mipmap, const Rect &rect) override;
+	void copyToBuffer(love::graphics::Buffer *dest, int slice, int mipmap, const Rect &rect, size_t destoffset, int destwidth, size_t size) override;
+
 	void setSamplerState(const SamplerState &s) override;
 	void setSamplerState(const SamplerState &s) override;
 
 
 	ptrdiff_t getHandle() const override;
 	ptrdiff_t getHandle() const override;

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

@@ -3353,6 +3353,70 @@ int w_copyBuffer(lua_State *L)
 	return 0;
 	return 0;
 }
 }
 
 
+int w_copyBufferToTexture(lua_State *L)
+{
+	Buffer *source = luax_checkbuffer(L, 1);
+	Texture *dest = luax_checktexture(L, 2);
+
+	ptrdiff_t sourceoffset = luaL_optinteger(L, 3, 0);
+	if (sourceoffset < 0)
+		return luaL_error(L, "copyBufferToTexture source offset cannot be negative.");
+
+	int sourcewidth = (int) luaL_optinteger(L, 4, 0);
+
+	int slice = 0;
+	int mipmap = 0;
+
+	if (dest->getTextureType() != TEXTURE_2D)
+		slice = (int) luaL_checkinteger(L, 5) - 1;
+
+	mipmap = (int) luaL_optinteger(L, 6, 1) - 1;
+
+	Rect rect = {0, 0, dest->getPixelWidth(mipmap), dest->getPixelHeight(mipmap)};
+	if (!lua_isnoneornil(L, 7))
+	{
+		rect.x = (int) luaL_checkinteger(L, 7);
+		rect.y = (int) luaL_checkinteger(L, 8);
+		rect.w = (int) luaL_checkinteger(L, 9);
+		rect.h = (int) luaL_checkinteger(L, 10);
+	}
+
+	luax_catchexcept(L, [&](){ instance()->copyBufferToTexture(source, dest, sourceoffset, sourcewidth, slice, mipmap, rect); });
+	return 0;
+}
+
+int w_copyTextureToBuffer(lua_State *L)
+{
+	Texture *source = luax_checktexture(L, 1);
+	Buffer *dest = luax_checkbuffer(L, 2);
+
+	int slice = 0;
+	int mipmap = 0;
+
+	if (source->getTextureType() != TEXTURE_2D)
+		slice = (int) luaL_checkinteger(L, 3) - 1;
+
+	mipmap = (int) luaL_optinteger(L, 4, 1) - 1;
+
+	Rect rect = {0, 0, source->getPixelWidth(mipmap), source->getPixelHeight(mipmap)};
+	if (!lua_isnoneornil(L, 5))
+	{
+		rect.x = (int) luaL_checkinteger(L, 5);
+		rect.y = (int) luaL_checkinteger(L, 6);
+		rect.w = (int) luaL_checkinteger(L, 7);
+		rect.h = (int) luaL_checkinteger(L, 8);
+	}
+
+	ptrdiff_t destoffset = luaL_optinteger(L, 9, 0);
+	if (destoffset < 0)
+		return luaL_error(L, "copyTextureToBuffer dest offset cannot be negative.");
+
+	int destwidth = (int) luaL_optinteger(L, 10, 0);
+
+	luax_catchexcept(L, [&](){ instance()->copyTextureToBuffer(source, dest, slice, mipmap, rect, destoffset, destwidth); });
+	return 0;
+}
+
 int w_flushBatch(lua_State *)
 int w_flushBatch(lua_State *)
 {
 {
 	instance()->flushBatchedDraws();
 	instance()->flushBatchedDraws();
@@ -3553,6 +3617,8 @@ static const luaL_Reg functions[] =
 	{ "dispatchThreadgroups", w_dispatchThreadgroups },
 	{ "dispatchThreadgroups", w_dispatchThreadgroups },
 
 
 	{ "copyBuffer", w_copyBuffer },
 	{ "copyBuffer", w_copyBuffer },
+	{ "copyBufferToTexture", w_copyBufferToTexture },
+	{ "copyTextureToBuffer", w_copyTextureToBuffer },
 
 
 	{ "isCreated", w_isCreated },
 	{ "isCreated", w_isCreated },
 	{ "isActive", w_isActive },
 	{ "isActive", w_isActive },

+ 1 - 1
src/modules/graphics/wrap_Texture.cpp

@@ -390,13 +390,13 @@ int w_Texture_newImageData(lua_State *L)
 
 
 	int slice = 0;
 	int slice = 0;
 	int mipmap = 0;
 	int mipmap = 0;
-	Rect rect = {0, 0, t->getPixelWidth(), t->getPixelHeight()};
 
 
 	if (t->getTextureType() != TEXTURE_2D)
 	if (t->getTextureType() != TEXTURE_2D)
 		slice = (int) luaL_checkinteger(L, 2) - 1;
 		slice = (int) luaL_checkinteger(L, 2) - 1;
 
 
 	mipmap = (int) luaL_optinteger(L, 3, 1) - 1;
 	mipmap = (int) luaL_optinteger(L, 3, 1) - 1;
 
 
+	Rect rect = {0, 0, t->getPixelWidth(mipmap), t->getPixelHeight(mipmap)};
 	if (!lua_isnoneornil(L, 4))
 	if (!lua_isnoneornil(L, 4))
 	{
 	{
 		rect.x = (int) luaL_checkinteger(L, 4);
 		rect.x = (int) luaL_checkinteger(L, 4);