| #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 = new Uint8Array(wasmMemory.buffer, 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 |