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.
This commit is contained in:
Alex Szpakowski
2022-05-30 21:53:55 -03:00
parent fb450b1df4
commit 3a5c3df02f
27 changed files with 1186 additions and 152 deletions
+16
View File
@@ -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
View File
@@ -42,6 +42,7 @@ public:
~FenceSync();
bool fence();
bool isComplete() const;
bool cpuWait();
void cleanup();
+12
View File
@@ -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
View File
@@ -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;
@@ -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
@@ -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
+35 -53
View File
@@ -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);
gl.bindTextureToUnit(this, 0, false);
bool isSRGB = false;
OpenGL::TextureFormat fmt = gl.convertPixelFormat(data->getFormat(), false, isSRGB);
OpenGL::TextureFormat fmt = gl.convertPixelFormat(format, false, isSRGB);
GLuint current_fbo = gl.getFramebuffer(OpenGL::FRAMEBUFFER_ALL);
gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, getFBO());
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(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);
gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, current_fbo);
}
glReadPixels(r.x, r.y, r.w, r.h, fmt.externalformat, fmt.type, data->getData());
if (slice > 0 || mipmap > 0)
gl.framebufferTexture(GL_COLOR_ATTACHMENT0, texType, texture, 0, 0, 0);
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());
readbackInternal(slice, mipmap, rect, destwidth, size, byteoffset);
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);
}
+2 -2
View File
@@ -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;