| #ifdef WITH_RIVE_SCRIPTING | |
| #include "rive/lua/rive_lua_libs.hpp" | |
| #include "rive/assets/script_asset.hpp" | |
| #include "rive/viewmodel/viewmodel_instance.hpp" | |
| #include "rive/async/work_pool.hpp" | |
| #ifdef RIVE_CANVAS | |
| #include "rive/renderer/render_context.hpp" | |
| #endif | |
| #include "lualib.h" | |
| #include <stdio.h> | |
| #include <unordered_set> | |
| #include <string> | |
| #include <string_view> | |
| #include <queue> | |
| #include <vector> | |
| #include <algorithm> | |
| #include <array> | |
| #include <iterator> | |
| using namespace rive; | |
| int luaopen_rive_base(lua_State* L); | |
| int luaopen_rive_math(lua_State* L); | |
| int luaopen_rive_renderer_library(lua_State* L); | |
| int luaopen_rive_properties(lua_State* L); | |
| int luaopen_rive_artboards(lua_State* L); | |
| int luaopen_rive_data_values(lua_State* L); | |
| int luaopen_rive_data_context(lua_State* L); | |
| int luaopen_rive_input(lua_State* L); | |
| int luaopen_rive_contex(lua_State* L); | |
| int luaopen_rive_audio(lua_State* L); | |
| extern "C" int luaopen_rive_buffer_ext(lua_State* L); | |
| static const luaL_Reg lualibs[] = { | |
| {"", luaopen_base}, | |
| {LUA_TABLIBNAME, luaopen_table}, | |
| {LUA_MATHLIBNAME, luaopen_math}, | |
| {"rive", luaopen_rive_base}, | |
| {LUA_OSLIBNAME, luaopen_os}, | |
| {LUA_STRLIBNAME, luaopen_string}, | |
| {LUA_UTF8LIBNAME, luaopen_utf8}, | |
| {LUA_BUFFERLIBNAME, luaopen_buffer}, | |
| {LUA_BITLIBNAME, luaopen_bit32}, | |
| {"math", luaopen_rive_math}, | |
| {"renderer", luaopen_rive_renderer_library}, | |
| {"properties", luaopen_rive_properties}, | |
| {"artboard", luaopen_rive_artboards}, | |
| {"dataValue", luaopen_rive_data_values}, | |
| {"input", luaopen_rive_input}, | |
| {"context", luaopen_rive_contex}, | |
| {"dataContext", luaopen_rive_data_context}, | |
| {"audio", luaopen_rive_audio}, | |
| {"promise", luaopen_rive_promise}, | |
| {NULL, NULL}, | |
| }; | |
| namespace rive | |
| { | |
| int luaopen_rive(lua_State* L) | |
| { | |
| lua_callbacks(L)->useratom = | |
| [](lua_State*, const char* s, size_t l) -> int16_t { | |
| return rive_lua_findAtom(s, l); | |
| }; | |
| const luaL_Reg* lib = lualibs; | |
| for (; lib->func; lib++) | |
| { | |
| lua_pushcfunction(L, lib->func, NULL); | |
| lua_pushstring(L, lib->name); | |
| lua_call(L, 1, 0); | |
| } | |
| // Extend the buffer library with SIMD-accelerated functions | |
| // (readf16, writef16, stridedcopy, convert). | |
| luaopen_rive_buffer_ext(L); | |
| return 0; | |
| } | |
| int rive_luaErrorHandler(lua_State* L) | |
| { | |
| ScriptingContext* context = | |
| static_cast<ScriptingContext*>(lua_getthreaddata(L)); | |
| context->printError(L); | |
| // Optionally, you can push a new value onto the stack to be returned by | |
| // lua_pcall For example, push a specific error code or a more detailed | |
| // message | |
| const char* error = lua_tostring(L, -1); | |
| lua_pushstring(L, error); | |
| return 1; // Number of return values | |
| // return 0; | |
| } | |
| int rive_lua_pcall(lua_State* state, int nargs, int nresults) | |
| { | |
| ScriptingContext* context = | |
| static_cast<ScriptingContext*>(lua_getthreaddata(state)); | |
| int ret = context->pCall(state, nargs, nresults); | |
| #ifdef RIVE_ORE | |
| rive_lua_closeOrphanRenderPass(state); | |
| rive_lua_closeOrphanCanvasFrames(state); | |
| #endif | |
| return ret; | |
| } | |
| int rive_lua_pcall_with_context(lua_State* state, | |
| ScriptedObject* scriptedObject, | |
| int nargs, | |
| int nresults) | |
| { | |
| ScriptingContext* context = | |
| static_cast<ScriptingContext*>(lua_getthreaddata(state)); | |
| ScopedScriptedObjectContext scope(context, scriptedObject); | |
| int ret = context->pCall(state, nargs, nresults); | |
| #ifdef RIVE_ORE | |
| rive_lua_closeOrphanRenderPass(state); | |
| rive_lua_closeOrphanCanvasFrames(state); | |
| #endif | |
| return ret; | |
| } | |
| int rive_lua_pushRef(lua_State* state, int ref) | |
| { | |
| lua_checkstack(state, 1); | |
| return lua_rawgeti(state, luaRegistryIndex, ref); | |
| } | |
| void rive_lua_pop(lua_State* state, int count) | |
| { | |
| lua_settop(state, -count - 1); | |
| } | |
| static void* l_alloc(void* ud, void* ptr, size_t osize, size_t nsize) | |
| { | |
| (void)ud; | |
| (void)osize; | |
| if (nsize == 0) | |
| { | |
| free(ptr); | |
| // delete[] (uint8_t*)ptr; | |
| return NULL; | |
| } | |
| else | |
| { | |
| // auto nptr = new uint8_t[nsize]; | |
| // memcpy(nptr, ptr, std::min(nsize, osize)); | |
| // delete[] (uint8_t*)ptr; | |
| // return nptr; | |
| return realloc(ptr, nsize); | |
| } | |
| } | |
| static const char* registeredCacheTableKey = "_MODULES"; | |
| static int checkRegisteredModules(lua_State* L, const char* path) | |
| { | |
| luaL_findtable(L, LUA_REGISTRYINDEX, registeredCacheTableKey, 1); | |
| lua_getfield(L, -1, path); | |
| if (lua_isnil(L, -1)) | |
| { | |
| lua_pop(L, 2); | |
| return 0; | |
| } | |
| lua_remove(L, -2); | |
| return 1; | |
| } | |
| static int lua_requireinternal(lua_State* L, const char* requirerChunkname) | |
| { | |
| // Discard extra arguments, we only use path | |
| lua_settop(L, 1); | |
| const char* path = luaL_checkstring(L, 1); | |
| ScriptingContext* context = | |
| static_cast<ScriptingContext*>(lua_getthreaddata(L)); | |
| if (checkRegisteredModules(L, path) == 1) | |
| { | |
| return 1; | |
| } | |
| // Record missing dependency if we're registering a module | |
| if (requirerChunkname != nullptr && context != nullptr) | |
| { | |
| context->recordMissingDependency(requirerChunkname, path); | |
| } | |
| luaL_error(L, "require could not find a script named %s", path); | |
| return 0; | |
| } | |
| static int lua_require(lua_State* L) | |
| { | |
| lua_Debug ar; | |
| int level = 1; | |
| do | |
| { | |
| if (!lua_getinfo(L, level++, "s", &ar)) | |
| { | |
| luaL_error(L, "require is not supported in this context"); | |
| } | |
| } while (ar.what[0] == 'C'); | |
| return lua_requireinternal(L, ar.source); | |
| } | |
| static int luaR_error(lua_State* L) | |
| { | |
| int level = luaL_optinteger(L, 2, 1); | |
| lua_settop(L, 1); | |
| if (lua_isstring(L, 1) && level > 0) | |
| { | |
| luaL_where(L, level); | |
| lua_pushvalue(L, 1); | |
| lua_concat(L, 2); | |
| } | |
| lua_error(L); | |
| } | |
| static int lua_late(lua_State* L) | |
| { | |
| lua_pushnil(L); | |
| return 1; | |
| } | |
| void ScriptingVM::init(lua_State* state, ScriptingContext* context) | |
| { | |
| luaopen_rive(state); | |
| lua_setthreaddata(state, context); | |
| lua_pushcclosurek(state, lua_require, "require", 0, nullptr); | |
| lua_setglobal(state, "require"); | |
| lua_pushcclosurek(state, luaR_error, "error", 0, nullptr); | |
| lua_setglobal(state, "error"); | |
| lua_pushcclosurek(state, lua_late, "late", 0, nullptr); | |
| lua_setglobal(state, "late"); | |
| luaL_sandbox(state); | |
| luaL_sandboxthread(state); | |
| } | |
| ScriptingVM::ScriptingVM(std::unique_ptr<ScriptingContext> context) : | |
| m_ownedContext(std::move(context)) | |
| { | |
| m_state = lua_newstate(l_alloc, nullptr); | |
| init(m_state, m_ownedContext.get()); | |
| } | |
| ScriptingVM::~ScriptingVM() { closeLuaState(); } | |
| void ScriptingVM::closeLuaState() | |
| { | |
| if (m_state == nullptr) | |
| { | |
| return; | |
| } | |
| // Cancel async tasks before closing Lua state to prevent callbacks | |
| // from accessing dead state. | |
| if (m_ownedContext) | |
| m_ownedContext->shutdownAsyncForState(m_state); | |
| // Null every registered ScriptedObject's back-pointer before the lua | |
| // teardown cascade. Once m_vm is gone, scriptDispose() (called from | |
| // cascading destruction inside lua_close) skips its releaseRef / | |
| // disposeScriptedContext calls. Those calls would otherwise dereference | |
| // Lua userdatas (ScriptedContext, ref tables) that lua_close has already | |
| // freed in earlier sweep iterations. | |
| detachScriptedObjects(); | |
| lua_State* state = m_state; | |
| m_state = nullptr; | |
| lua_close(state); | |
| } | |
| void ScriptingVM::replaceContext(std::unique_ptr<ScriptingContext> newContext) | |
| { | |
| #ifdef WITH_RIVE_TOOLS | |
| if (m_ownedContext != nullptr) | |
| { | |
| m_ownedContext->disposeOrphanScriptedProperties(); | |
| } | |
| #endif | |
| m_ownedContext = std::move(newContext); | |
| lua_setthreaddata(m_state, m_ownedContext.get()); | |
| } | |
| void ScriptingVM::addModule(ModuleDetails* moduleDetails) | |
| { | |
| context()->addModule(moduleDetails); | |
| } | |
| void ScriptingVM::performRegistration() | |
| { | |
| context()->performRegistration(m_state); | |
| } | |
| // Loads bytecode into a sandboxed thread without executing it. | |
| // On success, pushes the module thread (with loaded closure) onto L's stack. | |
| // Returns true on success. | |
| bool ScriptingVM::loadModule(lua_State* L, | |
| const char* name, | |
| Span<uint8_t> bytecode) | |
| { | |
| if (bytecode.empty()) | |
| { | |
| return false; | |
| } | |
| // module needs to run in a new thread, isolated from the rest | |
| // note: we create ML on main thread so that it doesn't inherit environment | |
| // of L | |
| lua_State* GL = lua_mainthread(L); | |
| lua_State* ML = lua_newthread(GL); | |
| lua_xmove(GL, L, 1); | |
| // new thread needs to have the globals sandboxed | |
| luaL_sandboxthread(ML); | |
| lua_setthreaddata(ML, lua_getthreaddata(L)); | |
| int status = | |
| luau_load(ML, name, (const char*)bytecode.data(), bytecode.size(), 0); | |
| if (status != 0) | |
| { | |
| // luau_load failed — error string is on ML stack | |
| lua_xmove(ML, L, 1); | |
| ScriptingContext* context = | |
| static_cast<ScriptingContext*>(lua_getthreaddata(L)); | |
| context->printError(L); | |
| lua_pop(L, 2); // pop error + thread | |
| return false; | |
| } | |
| // Thread with loaded closure is on top of L's stack. | |
| return true; | |
| } | |
| // Executes a previously loaded module thread (on top of L's stack from | |
| // loadModule). On success, replaces the thread with the module result. | |
| // If isUtility, also registers the result in the require cache. | |
| // Returns true on success. | |
| bool ScriptingVM::executeModule(lua_State* L, | |
| const char* name, | |
| bool isUtility, | |
| const char* chunkname) | |
| { | |
| const char* display = chunkname != nullptr ? chunkname : name; | |
| // The module thread should be on top of the stack. | |
| lua_State* ML = lua_tothread(L, -1); | |
| if (ML == nullptr) | |
| { | |
| return false; | |
| } | |
| int status = lua_resume(ML, L, 0); | |
| if (status == 0) | |
| { | |
| if (lua_gettop(ML) == 0) | |
| { | |
| lua_pushfstring(ML, "%s:1: module must return a value", display); | |
| } | |
| else if (!lua_istable(ML, -1) && !lua_isfunction(ML, -1)) | |
| { | |
| lua_pushfstring(ML, | |
| "%s:1: module must return a table or function", | |
| display); | |
| } | |
| } | |
| else if (status == LUA_YIELD) | |
| { | |
| lua_pushfstring(ML, "%s:1: module can not yield", display); | |
| } | |
| else if (!lua_isstring(ML, -1)) | |
| { | |
| lua_pushfstring(ML, | |
| "%s:1: unknown error while running module", | |
| display); | |
| } | |
| // add ML result to L stack | |
| lua_xmove(ML, L, 1); | |
| // An error occurred if the top of the stack is a string. | |
| if (lua_isstring(L, -1)) | |
| { | |
| ScriptingContext* context = | |
| static_cast<ScriptingContext*>(lua_getthreaddata(L)); | |
| context->printError(L); | |
| lua_pop(L, 2); // pop error + thread | |
| return false; | |
| } | |
| // remove ML thread from L stack | |
| lua_remove(L, -2); | |
| // added one value to L stack: module result | |
| if (isUtility) | |
| { | |
| // Register into the require cache directly. | |
| luaL_findtable(L, LUA_REGISTRYINDEX, registeredCacheTableKey, 1); | |
| lua_pushstring(L, name); | |
| lua_pushvalue(L, -3); // copy module result (below cache table + name) | |
| lua_settable(L, -3); // cache[name] = result | |
| lua_pop(L, 1); // pop cache table | |
| } | |
| return true; | |
| } | |
| static void dump_stack(lua_State* state) | |
| { | |
| int i; | |
| int top = lua_gettop(state); | |
| for (i = 1; i <= top; i++) | |
| { /* repeat for each level */ | |
| int t = lua_type(state, i); | |
| switch (t) | |
| { | |
| case LUA_TSTRING: /* strings */ | |
| fprintf(stderr, | |
| " (%i)[STRING] %s\n", | |
| i, | |
| lua_tostring(state, i)); | |
| break; | |
| case LUA_TBOOLEAN: /* booleans */ | |
| fprintf(stderr, | |
| " (%i)[BOOLEAN] %s\n", | |
| i, | |
| lua_toboolean(state, i) ? "true" : "false"); | |
| break; | |
| case LUA_TNUMBER: /* numbers */ | |
| fprintf(stderr, | |
| " (%i)[NUMBER] %g\n", | |
| i, | |
| lua_tonumber(state, i)); | |
| break; | |
| default: /* other values */ | |
| fprintf(stderr, " (%i)[%s]\n", i, lua_typename(state, t)); | |
| break; | |
| } | |
| } | |
| fprintf(stderr, "\n"); /* end the listing */ | |
| } | |
| void ScriptingVM::dumpStack(lua_State* state) { dump_stack(state); } | |
| void ScriptingContext::addModule(ModuleDetails* moduleDetails) | |
| { | |
| m_modulesToRegister.push_back(moduleDetails); | |
| m_moduleLookup[moduleDetails->moduleName()] = moduleDetails; | |
| } | |
| bool ScriptingContext::tryRegisterModule(lua_State* state, | |
| ModuleDetails* moduleDetails) | |
| { | |
| #ifndef WITH_RIVE_TOOLS | |
| // In production builds, only allow verified (signed) scripts | |
| if (!moduleDetails->verified()) | |
| { | |
| return false; | |
| } | |
| #endif | |
| std::string name = moduleDetails->moduleName(); | |
| bool registerSuccess = false; | |
| int functionRef = 0; | |
| if (moduleDetails->isProtocolScript()) | |
| { | |
| if (ScriptingVM::registerScript(state, | |
| name.c_str(), | |
| moduleDetails->moduleBytecode())) | |
| { | |
| // registerScript leaves the function on the stack | |
| if (static_cast<lua_Type>(lua_type(state, -1)) == LUA_TFUNCTION) | |
| { | |
| functionRef = lua_ref(state, -1); | |
| } | |
| lua_pop(state, 1); | |
| registerSuccess = true; | |
| } | |
| } | |
| else | |
| { | |
| if (ScriptingVM::registerModule(state, | |
| name.c_str(), | |
| moduleDetails->moduleBytecode())) | |
| { | |
| registerSuccess = true; | |
| } | |
| } | |
| if (registerSuccess) | |
| { | |
| moduleDetails->registrationComplete(functionRef); | |
| onModuleRegistered(moduleDetails); | |
| return true; | |
| } | |
| return false; | |
| } | |
| void ScriptingContext::performRegistration(lua_State* state) | |
| { | |
| // Loop over all of the modules once. We need do a tryRegister | |
| // pass on each module in order to determine if it has any | |
| // required dependencies | |
| for (ModuleDetails* moduleDetails : m_modulesToRegister) | |
| { | |
| if (moduleDetails == nullptr) | |
| { | |
| continue; | |
| } | |
| std::string cacheKey = moduleDetails->moduleName(); | |
| // Skip if already registered | |
| if (checkRegisteredModules(state, cacheKey.c_str()) == 1) | |
| { | |
| lua_pop(state, 1); | |
| continue; | |
| } | |
| tryRegisterModule(state, moduleDetails); | |
| } | |
| // One cleanup pass: producers emit modules in dependency order (the | |
| // editor and rive-cli both sort), so a single sorted retry suffices. | |
| if (!m_pendingModules.empty()) | |
| { | |
| std::vector<ModuleDetails*> pendingModules; | |
| for (auto module : m_pendingModules) | |
| { | |
| pendingModules.push_back(module); | |
| } | |
| std::vector<ModuleDetails*> sortedModules; | |
| std::unordered_set<ModuleDetails*> visitedModules; | |
| ModuleDetails* module = pendingModules.back(); | |
| pendingModules.pop_back(); | |
| sortNextModule(module, | |
| &pendingModules, | |
| &sortedModules, | |
| &visitedModules); | |
| // Register modules in sorted order | |
| for (ModuleDetails* moduleDetails : sortedModules) | |
| { | |
| // Skip utility modules that already made it into the cache. | |
| if (checkRegisteredModules(state, | |
| moduleDetails->moduleName().c_str()) == | |
| 1) | |
| { | |
| lua_pop(state, 1); | |
| continue; | |
| } | |
| // A known-missing require has not registered yet; re-running | |
| // the chunk would repeat its side effects and error. | |
| if (!moduleDetails->missingDependencies().empty()) | |
| { | |
| continue; | |
| } | |
| tryRegisterModule(state, moduleDetails); | |
| } | |
| } | |
| m_modulesToRegister.clear(); | |
| m_pendingModules.clear(); | |
| } | |
| void ScriptingContext::sortNextModule( | |
| ModuleDetails* module, | |
| std::vector<ModuleDetails*>* pendingModules, | |
| std::vector<ModuleDetails*>* sortedModules, | |
| std::unordered_set<ModuleDetails*>* visitedModules) | |
| { | |
| // If already visited, skip | |
| if (visitedModules->find(module) != visitedModules->end()) | |
| { | |
| return; | |
| } | |
| auto dependencies = module->missingDependencies(); | |
| for (const auto& dependencyName : dependencies) | |
| { | |
| auto lookupIt = m_moduleLookup.find(dependencyName); | |
| if (lookupIt != m_moduleLookup.end()) | |
| { | |
| ModuleDetails* dependencyModule = lookupIt->second; | |
| // Recursively process the dependency | |
| sortNextModule(dependencyModule, | |
| pendingModules, | |
| sortedModules, | |
| visitedModules); | |
| } | |
| } | |
| if (std::find(sortedModules->begin(), sortedModules->end(), module) == | |
| sortedModules->end()) | |
| { | |
| sortedModules->push_back(module); | |
| } | |
| visitedModules->insert(module); | |
| if (!pendingModules->empty()) | |
| { | |
| ModuleDetails* nextModule = pendingModules->back(); | |
| pendingModules->pop_back(); | |
| sortNextModule(nextModule, | |
| pendingModules, | |
| sortedModules, | |
| visitedModules); | |
| } | |
| } | |
| void ScriptingContext::recordMissingDependency( | |
| const std::string& requiringModule, | |
| const std::string& missingModule) | |
| { | |
| if (requiringModule.empty()) | |
| { | |
| return; | |
| } | |
| auto it = m_moduleLookup.find(requiringModule); | |
| if (it == m_moduleLookup.end()) | |
| { | |
| return; | |
| } | |
| it->second->addMissingDependency(missingModule); | |
| m_pendingModules.insert(it->second); | |
| } | |
| void ScriptingContext::onModuleRegistered(ModuleDetails* moduleDetails) | |
| { | |
| std::string key = moduleDetails->moduleName(); | |
| for (ModuleDetails* module : m_modulesToRegister) | |
| { | |
| if (!module->missingDependencies().empty()) | |
| { | |
| module->clearMissingDependency(key); | |
| } | |
| } | |
| auto it = m_pendingModules.find(moduleDetails); | |
| if (it != m_pendingModules.end()) | |
| { | |
| m_pendingModules.erase(it); | |
| } | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| void ScriptingContext::registerShaderRstb(std::string name, | |
| std::vector<uint8_t> bytes) | |
| { | |
| m_shaderRstbs[std::move(name)] = std::move(bytes); | |
| } | |
| const std::vector<uint8_t>* ScriptingContext::findShaderRstb( | |
| const std::string& name) const | |
| { | |
| auto it = m_shaderRstbs.find(name); | |
| return it != m_shaderRstbs.end() ? &it->second : nullptr; | |
| } | |
| const std::vector<uint8_t>* ScriptingContext::findShaderRstb( | |
| const ScopedAssetReference& reference) const | |
| { | |
| const std::vector<uint8_t>* found = nullptr; | |
| int bestRank = 0; | |
| for (const auto& entry : m_shaderRstbs) | |
| { | |
| size_t slash = entry.first.rfind('/'); | |
| std::string shortName = slash == std::string::npos | |
| ? entry.first | |
| : entry.first.substr(slash + 1); | |
| int rank = reference.match(entry.first, shortName); | |
| if (rank > bestRank) | |
| { | |
| bestRank = rank; | |
| found = &entry.second; | |
| } | |
| } | |
| return found; | |
| } | |
| void ScriptingContext::setGeneratorRef(uint32_t assetId, int ref) | |
| { | |
| m_assetGeneratorRefs[assetId] = ref; | |
| } | |
| int ScriptingContext::getGeneratorRef(uint32_t assetId) const | |
| { | |
| auto it = m_assetGeneratorRefs.find(assetId); | |
| return it != m_assetGeneratorRefs.end() ? it->second : 0; | |
| } | |
| void ScriptingContext::clearGeneratorRefs() { m_assetGeneratorRefs.clear(); } | |
| bool ScriptingContext::hasGeneratorRef(uint32_t assetId) const | |
| { | |
| return m_assetGeneratorRefs.find(assetId) != m_assetGeneratorRefs.end(); | |
| } | |
| void ScriptingContext::trackOrphanScriptedProperty(ScriptedProperty* property) | |
| { | |
| if (property != nullptr) | |
| { | |
| m_orphanScriptedProperties.push_back(property); | |
| } | |
| } | |
| void ScriptingContext::untrackOrphanScriptedProperty(ScriptedProperty* property) | |
| { | |
| auto it = std::remove(m_orphanScriptedProperties.begin(), | |
| m_orphanScriptedProperties.end(), | |
| property); | |
| m_orphanScriptedProperties.erase(it, m_orphanScriptedProperties.end()); | |
| } | |
| void ScriptingContext::disposeOrphanScriptedProperties() | |
| { | |
| auto orphans = m_orphanScriptedProperties; | |
| for (ScriptedProperty* property : orphans) | |
| { | |
| if (property != nullptr) | |
| { | |
| property->dispose(); | |
| } | |
| } | |
| m_orphanScriptedProperties.clear(); | |
| } | |
| void ScriptingContext::disposeOrphanScriptedProperties(uint32_t tag) | |
| { | |
| if (tag == 0) | |
| { | |
| return; | |
| } | |
| // dispose() untracks each property, so iterate a copy. | |
| auto orphans = m_orphanScriptedProperties; | |
| for (ScriptedProperty* property : orphans) | |
| { | |
| if (property != nullptr && property->orphanOwnerTag() == tag) | |
| { | |
| property->dispose(); | |
| } | |
| } | |
| } | |
| #endif | |
| void ScriptingContext::trackViewModelInstance(rcp<ViewModelInstance> instance) | |
| { | |
| if (instance == nullptr) | |
| { | |
| return; | |
| } | |
| auto& tracked = m_trackedViewModelInstances[instance.get()]; | |
| tracked.instance = instance; | |
| tracked.registrations++; | |
| } | |
| void ScriptingContext::untrackViewModelInstance(ViewModelInstance* instance) | |
| { | |
| if (instance == nullptr) | |
| { | |
| return; | |
| } | |
| auto it = m_trackedViewModelInstances.find(instance); | |
| if (it != m_trackedViewModelInstances.end() && | |
| --it->second.registrations <= 0) | |
| { | |
| m_trackedViewModelInstances.erase(it); | |
| } | |
| } | |
| void ScriptingContext::advanceDetachedViewModels() | |
| { | |
| for (auto& entry : m_trackedViewModelInstances) | |
| { | |
| ViewModelInstance* instance = entry.second.instance.get(); | |
| // Only advance detached roots. Instances with parents are already | |
| // reached through the bound tree or their detached-root ancestor. | |
| if (!instance->hasParents()) | |
| { | |
| instance->advanced(); | |
| } | |
| } | |
| } | |
| // ── WorkPool integration ─────────────────────────────────────────────────── | |
| // getGlobalWorkPool() is defined in work_pool.cpp (shared singleton). | |
| WorkPool* ScriptingContext::workPool() | |
| { | |
| if (m_ownerId == 0) | |
| m_ownerId = WorkPool::nextOwnerId(); | |
| return getGlobalWorkPool().get(); | |
| } | |
| // Forward-declared in lua_image_decode.cpp (WASM only). | |
| #ifdef __EMSCRIPTEN__ | |
| extern void wasm_cancelPendingDecodes(lua_State* mainThread); | |
| #endif | |
| void ScriptingContext::shutdownAsync() | |
| { | |
| if (m_ownerId != 0) | |
| { | |
| auto& pool = getGlobalWorkPoolIfExists(); | |
| if (pool) | |
| pool->cancelAllForOwner(m_ownerId); | |
| m_ownerId = 0; | |
| } | |
| } | |
| // Called from ~ScriptingVM before lua_close. On WASM, also cancel | |
| // browser-native image decodes that bypass WorkPool. | |
| void ScriptingContext::shutdownAsyncForState(lua_State* mainThread) | |
| { | |
| shutdownAsync(); | |
| #ifdef __EMSCRIPTEN__ | |
| wasm_cancelPendingDecodes(mainThread); | |
| #endif | |
| } | |
| bool ScriptingVM::registerScript(lua_State* state, | |
| const char* name, | |
| Span<uint8_t> bytecode, | |
| const char* chunkname) | |
| { | |
| // Check if already registered - leave module on stack for caller to use | |
| if (checkRegisteredModules(state, name) == 1) | |
| { | |
| return true; | |
| } | |
| if (!loadModule(state, chunkname != nullptr ? chunkname : name, bytecode)) | |
| { | |
| return false; | |
| } | |
| if (!executeModule(state, name, false, chunkname)) | |
| { | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool ScriptingVM::registerModule(lua_State* state, | |
| const char* name, | |
| Span<uint8_t> bytecode, | |
| const char* chunkname) | |
| { | |
| // Check if already registered | |
| if (checkRegisteredModules(state, name) == 1) | |
| { | |
| lua_pop(state, 1); | |
| return true; | |
| } | |
| if (!loadModule(state, chunkname != nullptr ? chunkname : name, bytecode)) | |
| { | |
| return false; | |
| } | |
| if (!executeModule(state, name, true, chunkname)) | |
| { | |
| return false; | |
| } | |
| // executeModule with isUtility=true registers into the require cache | |
| // and leaves the module result on the stack. Pop it. | |
| lua_pop(state, 1); | |
| return true; | |
| } | |
| void ScriptingVM::unregisterModule(lua_State* state, const char* name) | |
| { | |
| luaL_findtable(state, LUA_REGISTRYINDEX, registeredCacheTableKey, 1); | |
| lua_pushstring(state, name); | |
| lua_pushnil(state); | |
| lua_settable(state, -3); | |
| lua_pop(state, 1); | |
| } | |
| void ScriptingVM::unregisterModule(const char* name) | |
| { | |
| return unregisterModule(m_state, name); | |
| } | |
| bool ScriptingVM::registerModule(const char* name, Span<uint8_t> bytecode) | |
| { | |
| return registerModule(m_state, name, bytecode); | |
| } | |
| bool ScriptingVM::registerScript(const char* name, Span<uint8_t> bytecode) | |
| { | |
| return registerScript(m_state, name, bytecode); | |
| } | |
| int CPPRuntimeScriptingContext::pCall(lua_State* state, int nargs, int nresults) | |
| { | |
| // calculate stack position for message handler | |
| int hpos = lua_gettop(state) - nargs; | |
| lua_pushcfunction(state, rive_luaErrorHandler, "riveErrorHandler"); | |
| lua_insert(state, hpos); | |
| startTimedExecution(state); | |
| int ret = lua_pcall(state, nargs, nresults, hpos); | |
| endTimedExecution(state); | |
| lua_remove(state, hpos); | |
| return ret; | |
| } | |
| } // namespace rive | |
| #endif |