From b45d783dee17fd9d9475bc539340030ab4a8aae1 Mon Sep 17 00:00:00 2001 From: AverageConsumer <35539970+AverageConsumer@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:09:10 +0200 Subject: [PATCH] feat(android): add asynchronous canvas readback --- .../jni/love/src/modules/graphics/Canvas.cpp | 11 ++- .../jni/love/src/modules/graphics/Canvas.h | 2 + .../src/modules/graphics/opengl/Canvas.cpp | 74 +++++++++++++++++++ .../love/src/modules/graphics/opengl/Canvas.h | 5 ++ .../love/src/modules/graphics/wrap_Canvas.cpp | 27 +++++++ 5 files changed, 118 insertions(+), 1 deletion(-) diff --git a/mobile/android/love/src/jni/love/src/modules/graphics/Canvas.cpp b/mobile/android/love/src/jni/love/src/modules/graphics/Canvas.cpp index 6d62ad63..7e683a48 100644 --- a/mobile/android/love/src/jni/love/src/modules/graphics/Canvas.cpp +++ b/mobile/android/love/src/jni/love/src/modules/graphics/Canvas.cpp @@ -154,6 +154,16 @@ love::image::ImageData *Canvas::newImageData(love::image::Image *module, int sli return module->newImageData(r.w, r.h, dataformat); } +bool Canvas::requestImageData() +{ + return false; +} + +love::image::ImageData *Canvas::pollImageData(love::image::Image *) +{ + return nullptr; +} + void Canvas::draw(Graphics *gfx, Quad *q, const Matrix4 &t) { if (gfx->isCanvasActive(this)) @@ -232,4 +242,3 @@ StringMap Canvas::settingTypes(Ca } // graphics } // love - diff --git a/mobile/android/love/src/jni/love/src/modules/graphics/Canvas.h b/mobile/android/love/src/jni/love/src/modules/graphics/Canvas.h index f1ff4ac3..4a644053 100644 --- a/mobile/android/love/src/jni/love/src/modules/graphics/Canvas.h +++ b/mobile/android/love/src/jni/love/src/modules/graphics/Canvas.h @@ -81,6 +81,8 @@ public: int getRequestedMSAA() const; virtual love::image::ImageData *newImageData(love::image::Image *module, int slice, int mipmap, const Rect &rect); + virtual bool requestImageData(); + virtual love::image::ImageData *pollImageData(love::image::Image *module); virtual void generateMipmaps() = 0; virtual int getMSAA() const = 0; diff --git a/mobile/android/love/src/jni/love/src/modules/graphics/opengl/Canvas.cpp b/mobile/android/love/src/jni/love/src/modules/graphics/opengl/Canvas.cpp index b43d8746..0d7103bf 100644 --- a/mobile/android/love/src/jni/love/src/modules/graphics/opengl/Canvas.cpp +++ b/mobile/android/love/src/jni/love/src/modules/graphics/opengl/Canvas.cpp @@ -23,6 +23,7 @@ #include "Graphics.h" #include // For min/max +#include namespace love { @@ -197,6 +198,9 @@ Canvas::Canvas(const Settings &settings) , texture(0) , renderbuffer(0) , actualSamples(0) + , readbackBuffer(0) + , readbackFence(nullptr) + , readbackSize(0) { format = getSizedFormat(format); @@ -314,6 +318,14 @@ bool Canvas::loadVolatile() void Canvas::unloadVolatile() { + if (readbackFence != nullptr) + glDeleteSync(readbackFence); + if (readbackBuffer != 0) + glDeleteBuffers(1, &readbackBuffer); + readbackFence = nullptr; + readbackBuffer = 0; + readbackSize = 0; + if (fbo != 0 || renderbuffer != 0 || texture != 0) { // This is a bit ugly, but we need some way to destroy the cached FBO @@ -480,6 +492,68 @@ love::image::ImageData *Canvas::newImageData(love::image::Image *module, int sli return data; } +bool Canvas::requestImageData() +{ + if (readbackFence != nullptr || !isReadable() + || !(GLAD_ES_VERSION_3_0 || GLAD_VERSION_3_2) + || texType != TEXTURE_2D + || (format != PIXELFORMAT_RGBA8 && format != PIXELFORMAT_sRGBA8) + || actualSamples > 0) + return false; + + auto gfx = Module::getInstance(Module::M_GRAPHICS); + if (gfx != nullptr && gfx->isCanvasActive(this)) + throw love::Exception("Canvas:requestImageData cannot be called while that Canvas is active."); + + const size_t size = (size_t) pixelWidth * (size_t) pixelHeight * 4; + if (readbackBuffer == 0) + glGenBuffers(1, &readbackBuffer); + + glBindBuffer(GL_PIXEL_PACK_BUFFER, readbackBuffer); + if (readbackSize != size) + { + glBufferData(GL_PIXEL_PACK_BUFFER, size, nullptr, GL_STREAM_READ); + readbackSize = size; + } + + GLuint currentfbo = gl.getFramebuffer(OpenGL::FRAMEBUFFER_ALL); + gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, getFBO()); + glReadPixels(0, 0, pixelWidth, pixelHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + readbackFence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, currentfbo); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + return readbackFence != nullptr; +} + +love::image::ImageData *Canvas::pollImageData(love::image::Image *module) +{ + if (readbackFence == nullptr) + return nullptr; + + GLenum status = glClientWaitSync(readbackFence, 0, 0); + if (status == GL_TIMEOUT_EXPIRED) + return nullptr; + + glDeleteSync(readbackFence); + readbackFence = nullptr; + if (status == GL_WAIT_FAILED) + return nullptr; + + love::image::ImageData *data = module->newImageData(pixelWidth, pixelHeight, PIXELFORMAT_RGBA8); + glBindBuffer(GL_PIXEL_PACK_BUFFER, readbackBuffer); + void *pixels = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, readbackSize, GL_MAP_READ_BIT); + if (pixels == nullptr) + { + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + data->release(); + throw love::Exception("Could not map asynchronous Canvas readback."); + } + memcpy(data->getData(), pixels, readbackSize); + glUnmapBuffer(GL_PIXEL_PACK_BUFFER); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + return data; +} + void Canvas::generateMipmaps() { if (getMipmapCount() == 1 || getMipmapMode() == MIPMAPS_NONE) diff --git a/mobile/android/love/src/jni/love/src/modules/graphics/opengl/Canvas.h b/mobile/android/love/src/jni/love/src/modules/graphics/opengl/Canvas.h index 4576cb5f..be5c785d 100644 --- a/mobile/android/love/src/jni/love/src/modules/graphics/opengl/Canvas.h +++ b/mobile/android/love/src/jni/love/src/modules/graphics/opengl/Canvas.h @@ -54,6 +54,8 @@ public: ptrdiff_t getHandle() const override; love::image::ImageData *newImageData(love::image::Image *module, int slice, int mipmap, const Rect &rect) override; + bool requestImageData() override; + love::image::ImageData *pollImageData(love::image::Image *module) override; void generateMipmaps() override; int getMSAA() const override @@ -107,6 +109,9 @@ private: GLenum status; int actualSamples; + GLuint readbackBuffer; + GLsync readbackFence; + size_t readbackSize; static SupportedFormat supportedFormats[PIXELFORMAT_MAX_ENUM]; static SupportedFormat checkedFormats[PIXELFORMAT_MAX_ENUM]; diff --git a/mobile/android/love/src/jni/love/src/modules/graphics/wrap_Canvas.cpp b/mobile/android/love/src/jni/love/src/modules/graphics/wrap_Canvas.cpp index c4bc7aa6..a686555c 100644 --- a/mobile/android/love/src/jni/love/src/modules/graphics/wrap_Canvas.cpp +++ b/mobile/android/love/src/jni/love/src/modules/graphics/wrap_Canvas.cpp @@ -116,6 +116,31 @@ int w_Canvas_newImageData(lua_State *L) return 1; } +int w_Canvas_requestImageData(lua_State *L) +{ + Canvas *canvas = luax_checkcanvas(L, 1); + bool requested = false; + luax_catchexcept(L, [&](){ requested = canvas->requestImageData(); }); + luax_pushboolean(L, requested); + return 1; +} + +int w_Canvas_pollImageData(lua_State *L) +{ + Canvas *canvas = luax_checkcanvas(L, 1); + love::image::Image *image = luax_getmodule(L, love::image::Image::type); + love::image::ImageData *data = nullptr; + luax_catchexcept(L, [&](){ data = canvas->pollImageData(image); }); + if (data == nullptr) + { + lua_pushnil(L); + return 1; + } + luax_pushtype(L, data); + data->release(); + return 1; +} + int w_Canvas_generateMipmaps(lua_State *L) { Canvas *c = luax_checkcanvas(L, 1); @@ -139,6 +164,8 @@ static const luaL_Reg w_Canvas_functions[] = { "getMSAA", w_Canvas_getMSAA }, { "renderTo", w_Canvas_renderTo }, { "newImageData", w_Canvas_newImageData }, + { "requestImageData", w_Canvas_requestImageData }, + { "pollImageData", w_Canvas_pollImageData }, { "generateMipmaps", w_Canvas_generateMipmaps }, { "getMipmapMode", w_Canvas_getMipmapMode }, { 0, 0 }