| #ifdef WITH_RIVE_SCRIPTING |
| |
| #include "rive/lua/rive_lua_libs.hpp" |
| #include "lua.h" |
| #include "lualib.h" |
| |
| #include <cassert> |
| #include <cstring> |
| #include <vector> |
| #include <unordered_map> |
| |
| // ============================================================================ |
| // Native path — uses Bitmap::decode via WorkPool |
| // ============================================================================ |
| |
| #ifndef __EMSCRIPTEN__ |
| |
| #include "rive/async/work_pool.hpp" |
| #ifdef RIVE_DECODERS |
| #include "rive/decoders/bitmap_decoder.hpp" |
| #endif |
| |
| namespace rive |
| { |
| |
| #ifdef RIVE_DECODERS |
| class ImageDecodeTask : public WorkTask |
| { |
| public: |
| std::vector<uint8_t> m_encodedData; |
| uint32_t m_width = 0; |
| uint32_t m_height = 0; |
| lua_State* m_state = nullptr; |
| int m_promiseRef = LUA_NOREF; |
| std::unique_ptr<Bitmap> m_bitmap; |
| |
| bool execute() override |
| { |
| m_bitmap = Bitmap::decode(m_encodedData.data(), m_encodedData.size()); |
| if (!m_bitmap) |
| { |
| m_errorMessage = "failed to decode image data"; |
| return false; |
| } |
| if (m_bitmap->pixelFormat() != Bitmap::PixelFormat::RGBAPremul) |
| { |
| m_bitmap->pixelFormat(Bitmap::PixelFormat::RGBAPremul); |
| } |
| m_width = m_bitmap->width(); |
| m_height = m_bitmap->height(); |
| return true; |
| } |
| |
| void onComplete() override |
| { |
| if (!m_state || m_promiseRef == LUA_NOREF) |
| return; |
| lua_State* L = m_state; |
| lua_rawgeti(L, LUA_REGISTRYINDEX, m_promiseRef); |
| auto* promise = lua_torive<ScriptedPromise>(L, -1, true); |
| lua_pop(L, 1); |
| if (promise && promise->isPending()) |
| { |
| if (m_bitmap->pixelFormat() != Bitmap::PixelFormat::RGBAPremul) |
| { |
| lua_pushstring(L, |
| "internal error: decoded image is not " |
| "RGBAPremul"); |
| promise->reject(L, lua_gettop(L)); |
| lua_pop(L, 1); |
| m_bitmap.reset(); |
| lua_unref(L, m_promiseRef); |
| m_promiseRef = LUA_NOREF; |
| return; |
| } |
| lua_newtable(L); |
| size_t pixelSize = (size_t)m_width * m_height * 4; |
| void* buf = lua_newbuffer(L, pixelSize); |
| memcpy(buf, m_bitmap->bytes(), pixelSize); |
| lua_setfield(L, -2, "data"); |
| lua_pushnumber(L, m_width); |
| lua_setfield(L, -2, "width"); |
| lua_pushnumber(L, m_height); |
| lua_setfield(L, -2, "height"); |
| promise->resolve(L, lua_gettop(L)); |
| lua_pop(L, 1); |
| } |
| // Release decoded bitmap immediately — the pixels have been |
| // copied to the Lua buffer. The TaskRef userdata in the onCancel |
| // closure may keep this task alive until GC, so eagerly free the |
| // large allocation. |
| m_bitmap.reset(); |
| m_encodedData.clear(); |
| m_encodedData.shrink_to_fit(); |
| lua_unref(L, m_promiseRef); |
| m_promiseRef = LUA_NOREF; |
| } |
| |
| void onError(const std::string& error) override |
| { |
| if (!m_state || m_promiseRef == LUA_NOREF) |
| return; |
| lua_State* L = m_state; |
| lua_rawgeti(L, LUA_REGISTRYINDEX, m_promiseRef); |
| auto* promise = lua_torive<ScriptedPromise>(L, -1, true); |
| lua_pop(L, 1); |
| if (promise && promise->isPending()) |
| { |
| lua_pushstring(L, error.c_str()); |
| promise->reject(L, lua_gettop(L)); |
| lua_pop(L, 1); |
| } |
| lua_unref(L, m_promiseRef); |
| m_promiseRef = LUA_NOREF; |
| } |
| |
| void onCancel() override |
| { |
| m_promiseRef = LUA_NOREF; |
| m_state = nullptr; |
| } |
| }; |
| #endif // RIVE_DECODERS |
| |
| } // namespace rive |
| |
| int context_decodeImage_impl(lua_State* L) |
| { |
| using namespace rive; |
| |
| size_t len = 0; |
| const void* data = nullptr; |
| if (lua_isbuffer(L, 2)) |
| data = lua_tobuffer(L, 2, &len); |
| else |
| { |
| luaL_typeerror(L, 2, "buffer"); |
| return 0; |
| } |
| if (!data || len == 0) |
| { |
| luaL_error(L, "decodeImage: empty buffer"); |
| return 0; |
| } |
| |
| #ifndef RIVE_DECODERS |
| luaL_error(L, "decodeImage: not supported on this platform"); |
| return 0; |
| #else |
| auto* ctx = static_cast<ScriptingContext*>(lua_getthreaddata(L)); |
| WorkPool* wp = ctx->workPool(); |
| |
| lua_State* mainThread = lua_mainthread(L); |
| lua_newrive<ScriptedPromise>(L, mainThread); |
| int promiseIdx = lua_gettop(L); |
| lua_pushvalue(L, promiseIdx); |
| int promiseRef = lua_ref(L, -1); |
| lua_pop(L, 1); |
| |
| auto task = make_rcp<ImageDecodeTask>(); |
| task->m_encodedData.assign(static_cast<const uint8_t*>(data), |
| static_cast<const uint8_t*>(data) + len); |
| task->m_state = mainThread; |
| task->m_promiseRef = promiseRef; |
| task->setOwnerId(ctx->ownerId()); |
| |
| // Wrap the rcp<WorkTask> in a Lua userdata with a destructor so the |
| // task stays alive as long as the onCancel closure exists. This |
| // prevents a use-after-free if the WorkPool is destroyed (dropping |
| // its rcp) before the promise is cancelled. |
| struct TaskRef |
| { |
| rcp<WorkTask> ref; |
| }; |
| auto* taskUD = static_cast<TaskRef*>( |
| lua_newuserdatadtor(L, sizeof(TaskRef), [](void* p) { |
| static_cast<TaskRef*>(p)->~TaskRef(); |
| })); |
| new (taskUD) TaskRef{task}; // copy rcp before submit moves it |
| int taskUDIdx = lua_gettop(L); |
| |
| wp->submit(std::move(task)); |
| |
| // Register onCancel hook: cancel the WorkPool task so the decode |
| // thread skips execution (or the result is discarded), and release |
| // the promise registry reference so the Promise can be collected. |
| auto* promise = lua_torive<ScriptedPromise>(L, promiseIdx); |
| lua_pushvalue(L, taskUDIdx); |
| lua_remove(L, taskUDIdx); // remove original, closure upvalue has it |
| lua_pushinteger(L, promiseRef); |
| lua_pushcclosurek( |
| L, |
| [](lua_State* L) -> int { |
| auto* tr = |
| static_cast<TaskRef*>(lua_touserdata(L, lua_upvalueindex(1))); |
| if (tr && tr->ref) |
| tr->ref->cancel(); |
| int pRef = (int)lua_tointeger(L, lua_upvalueindex(2)); |
| lua_unref(L, pRef); |
| return 0; |
| }, |
| nullptr, |
| 2, |
| nullptr); |
| promise->m_onCancelRef = lua_ref(L, -1); |
| lua_pop(L, 1); |
| |
| lua_pushvalue(L, promiseIdx); |
| return 1; |
| #endif // RIVE_DECODERS |
| } |
| |
| // ============================================================================ |
| // WASM path — uses browser's createImageBitmap, no WorkPool |
| // ============================================================================ |
| |
| #else // __EMSCRIPTEN__ |
| |
| #include <emscripten.h> |
| |
| namespace |
| { |
| |
| // Track in-flight decodes so the JS callback can find the Lua state + promise. |
| struct PendingDecode |
| { |
| lua_State* state; |
| int promiseRef; |
| }; |
| std::unordered_map<uint32_t, PendingDecode> s_pendingDecodes; |
| uint32_t s_nextDecodeId = 1; |
| |
| // Generate a unique decode ID, skipping any that are still in-flight. |
| uint32_t nextDecodeId() |
| { |
| uint32_t id = s_nextDecodeId++; |
| // On wraparound, skip IDs that collide with in-flight decodes. |
| while (id == 0 || s_pendingDecodes.count(id)) |
| id = s_nextDecodeId++; |
| return id; |
| } |
| |
| } // namespace |
| |
| // Start a browser-native image decode. createImageBitmap is async; the JS |
| // callback fires between frames and calls back into C++ to resolve the promise. |
| EM_JS(void, |
| wasm_start_image_decode, |
| (uint32_t requestId, const uint8_t* data, int dataLen), |
| { |
| // Copy from WASM heap (SharedArrayBuffer can't be used for Blob). |
| var sourceView = Module["HEAP8"].subarray(data, data + dataLen); |
| var buffer = new Uint8Array(dataLen); |
| buffer.set(sourceView); |
| |
| var blob = new Blob([buffer]); |
| createImageBitmap(blob) |
| .then(function(bmp) { |
| // Draw to OffscreenCanvas to extract raw RGBA pixels. |
| var canvas = new OffscreenCanvas(bmp.width, bmp.height); |
| var ctx2d = canvas.getContext("2d"); |
| ctx2d.drawImage(bmp, 0, 0); |
| var imageData = |
| ctx2d.getImageData(0, 0, bmp.width, bmp.height); |
| |
| // Allocate WASM memory and copy pixels. |
| var numBytes = imageData.data.length; |
| var ptr = Module._malloc(numBytes); |
| Module.HEAPU8.set(imageData.data, ptr); |
| |
| Module._wasm_image_decode_complete(requestId, |
| bmp.width, |
| bmp.height, |
| ptr, |
| numBytes); |
| }) |
| .catch(function(err) { |
| var msg = err.message || "decode failed"; |
| var msgLen = Module.lengthBytesUTF8(msg) + 1; |
| var msgPtr = Module._malloc(msgLen); |
| Module.stringToUTF8(msg, msgPtr, msgLen); |
| Module._wasm_image_decode_error(requestId, msgPtr); |
| Module._free(msgPtr); |
| }); |
| }); |
| |
| // C callbacks invoked by JS when createImageBitmap resolves/rejects. |
| extern "C" |
| { |
| using namespace rive; |
| |
| EMSCRIPTEN_KEEPALIVE |
| void wasm_image_decode_complete(uint32_t requestId, |
| int width, |
| int height, |
| uint8_t* pixels, |
| int numBytes) |
| { |
| auto it = s_pendingDecodes.find(requestId); |
| if (it == s_pendingDecodes.end()) |
| { |
| free(pixels); |
| return; |
| } |
| |
| auto [state, promiseRef] = it->second; |
| s_pendingDecodes.erase(it); |
| |
| if (!state) |
| { |
| free(pixels); |
| return; |
| } |
| |
| lua_State* L = state; |
| lua_rawgeti(L, LUA_REGISTRYINDEX, promiseRef); |
| auto* promise = lua_torive<ScriptedPromise>(L, -1, true); |
| lua_pop(L, 1); |
| |
| if (promise && promise->isPending()) |
| { |
| // getImageData() returns straight RGBA; premultiply in-place so |
| // the scripting API always delivers premultiplied RGBA8. |
| for (int i = 0; i < numBytes; i += 4) |
| { |
| uint8_t a = pixels[i + 3]; |
| if (a < 255) |
| { |
| pixels[i + 0] = (uint16_t(pixels[i + 0]) * a + 127) / 255; |
| pixels[i + 1] = (uint16_t(pixels[i + 1]) * a + 127) / 255; |
| pixels[i + 2] = (uint16_t(pixels[i + 2]) * a + 127) / 255; |
| } |
| } |
| |
| lua_newtable(L); |
| |
| void* buf = lua_newbuffer(L, numBytes); |
| memcpy(buf, pixels, numBytes); |
| lua_setfield(L, -2, "data"); |
| |
| lua_pushnumber(L, width); |
| lua_setfield(L, -2, "width"); |
| |
| lua_pushnumber(L, height); |
| lua_setfield(L, -2, "height"); |
| |
| promise->resolve(L, lua_gettop(L)); |
| lua_pop(L, 1); |
| } |
| |
| lua_unref(L, promiseRef); |
| free(pixels); |
| } |
| |
| EMSCRIPTEN_KEEPALIVE |
| void wasm_image_decode_error(uint32_t requestId, const char* msg) |
| { |
| auto it = s_pendingDecodes.find(requestId); |
| if (it == s_pendingDecodes.end()) |
| return; |
| |
| auto [state, promiseRef] = it->second; |
| s_pendingDecodes.erase(it); |
| |
| if (state) |
| { |
| lua_State* L = state; |
| lua_rawgeti(L, LUA_REGISTRYINDEX, promiseRef); |
| auto* promise = lua_torive<ScriptedPromise>(L, -1, true); |
| lua_pop(L, 1); |
| |
| if (promise && promise->isPending()) |
| { |
| lua_pushstring(L, msg); |
| promise->reject(L, lua_gettop(L)); |
| lua_pop(L, 1); |
| } |
| lua_unref(L, promiseRef); |
| } |
| } |
| |
| } // extern "C" |
| |
| // Cancel all pending decodes for a Lua state (called on shutdown). |
| // Must be called while mainThread is still valid so we can unref promises. |
| namespace rive |
| { |
| void wasm_cancelPendingDecodes(lua_State* mainThread) |
| { |
| for (auto it = s_pendingDecodes.begin(); it != s_pendingDecodes.end();) |
| { |
| if (it->second.state == mainThread) |
| { |
| lua_unref(mainThread, it->second.promiseRef); |
| it = s_pendingDecodes.erase(it); |
| } |
| else |
| { |
| ++it; |
| } |
| } |
| } |
| } // namespace rive |
| |
| int context_decodeImage_impl(lua_State* L) |
| { |
| using namespace rive; |
| |
| size_t len = 0; |
| const void* data = nullptr; |
| if (lua_isbuffer(L, 2)) |
| data = lua_tobuffer(L, 2, &len); |
| else |
| { |
| luaL_typeerror(L, 2, "buffer"); |
| return 0; |
| } |
| if (!data || len == 0) |
| { |
| luaL_error(L, "decodeImage: empty buffer"); |
| return 0; |
| } |
| |
| lua_State* mainThread = lua_mainthread(L); |
| lua_newrive<ScriptedPromise>(L, mainThread); |
| int promiseIdx = lua_gettop(L); |
| lua_pushvalue(L, promiseIdx); |
| int promiseRef = lua_ref(L, -1); |
| lua_pop(L, 1); |
| |
| // Register the pending decode so the JS callback can find the promise. |
| uint32_t id = nextDecodeId(); |
| s_pendingDecodes[id] = {mainThread, promiseRef}; |
| |
| // Start the browser-native decode. This returns immediately. |
| wasm_start_image_decode(id, |
| static_cast<const uint8_t*>(data), |
| static_cast<int>(len)); |
| |
| // Register onCancel hook: erase from pending decodes so the JS |
| // callback becomes a no-op (the browser will still finish decoding, |
| // but the result is discarded). |
| auto* promise = lua_torive<ScriptedPromise>(L, promiseIdx); |
| lua_pushinteger(L, id); |
| lua_pushinteger(L, promiseRef); |
| lua_pushcclosurek( |
| L, |
| [](lua_State* L) -> int { |
| uint32_t decodeId = (uint32_t)lua_tointeger(L, lua_upvalueindex(1)); |
| int pRef = (int)lua_tointeger(L, lua_upvalueindex(2)); |
| s_pendingDecodes.erase(decodeId); |
| lua_unref(L, pRef); |
| return 0; |
| }, |
| nullptr, |
| 2, |
| nullptr); |
| promise->m_onCancelRef = lua_ref(L, -1); |
| lua_pop(L, 1); |
| |
| lua_pushvalue(L, promiseIdx); |
| return 1; |
| } |
| |
| #endif // __EMSCRIPTEN__ |
| |
| #endif // WITH_RIVE_SCRIPTING |