feature(scripting): serialize implemented methods (#12670) 8957983a44
feature(scripting): serialize implemented methods, detected by executing the generator in the editor

Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index 8cad039..c7a2a83 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-867dce9f736ed8ec73f271d9a034a30b65e385a4
+8957983a44b178d3746bf37ebfd27391bf7a1d3d
diff --git a/dev/defs/assets/script_asset.json b/dev/defs/assets/script_asset.json
index 75b6e1a..3423237 100644
--- a/dev/defs/assets/script_asset.json
+++ b/dev/defs/assets/script_asset.json
@@ -48,6 +48,18 @@
       "description": "Topological order from compilation — scripts with lower values are depended on by scripts with higher values. Not exported to runtime; the order is inherent in the position the ScriptAsset is written into the .riv.",
       "runtime": false,
       "journal": false
+    },
+    "serializedImplementedMethods": {
+      "type": "uint",
+      "initialValue": "2097151",
+      "key": {
+        "int": 1022,
+        "string": "serializedimplementedmethods"
+      },
+      "description": "Bitfield of the optional script methods (init/draw/drawCanvas/advance/etc.) the editor detected by executing the generator. Bit layout matches OptionalScriptedMethods (bits 0-20). The runtime reads these directly instead of detecting at load. Defaults to all bits set ((1<<21)-1 = 2097151) so files exported before this property existed behave as 'implements everything' and rely on graceful dispatch (each callback no-ops when the method isn't actually present).",
+      "coop": false,
+      "exportsToRuntimeConditionally": true,
+      "journal": false
     }
   }
-}
+}
\ No newline at end of file
diff --git a/include/rive/assets/script_asset.hpp b/include/rive/assets/script_asset.hpp
index cff7db4..cb0a963 100644
--- a/include/rive/assets/script_asset.hpp
+++ b/include/rive/assets/script_asset.hpp
@@ -94,12 +94,11 @@
 
     int m_implementedMethods = 0;
 
-protected:
-#ifdef WITH_RIVE_SCRIPTING
-    bool verifyImplementation(ScriptedObject* object, lua_State* state);
-#endif
-
 public:
+    // Bits 0-20 hold the method flags (see ScriptAsset::serializedImplemented
+    // Methods, which the editor serializes and the runtime reads directly).
+    static const uint32_t methodMask = (1u << 21) - 1;
+
     int implementedMethods() { return m_implementedMethods; }
     void implementedMethods(int implemented)
     {
diff --git a/include/rive/generated/assets/script_asset_base.hpp b/include/rive/generated/assets/script_asset_base.hpp
index 31be61f..bfc334b 100644
--- a/include/rive/generated/assets/script_asset_base.hpp
+++ b/include/rive/generated/assets/script_asset_base.hpp
@@ -33,10 +33,12 @@
 
     static const uint16_t generatorFunctionRefPropertyKey = 893;
     static const uint16_t isModulePropertyKey = 914;
+    static const uint16_t serializedImplementedMethodsPropertyKey = 1022;
 
 protected:
     uint32_t m_GeneratorFunctionRef = 0;
     bool m_IsModule = false;
+    uint32_t m_SerializedImplementedMethods = 2097151;
 
 public:
     inline uint32_t generatorFunctionRef() const
@@ -64,11 +66,26 @@
         isModuleChanged();
     }
 
+    inline uint32_t serializedImplementedMethods() const
+    {
+        return m_SerializedImplementedMethods;
+    }
+    void serializedImplementedMethods(uint32_t value)
+    {
+        if (m_SerializedImplementedMethods == value)
+        {
+            return;
+        }
+        m_SerializedImplementedMethods = value;
+        serializedImplementedMethodsChanged();
+    }
+
     Core* clone() const override;
     void copy(const ScriptAssetBase& object)
     {
         m_GeneratorFunctionRef = object.m_GeneratorFunctionRef;
         m_IsModule = object.m_IsModule;
+        m_SerializedImplementedMethods = object.m_SerializedImplementedMethods;
         TextAsset::copy(object);
     }
 
@@ -82,6 +99,10 @@
             case isModulePropertyKey:
                 m_IsModule = CoreBoolType::deserialize(reader);
                 return true;
+            case serializedImplementedMethodsPropertyKey:
+                m_SerializedImplementedMethods =
+                    CoreUintType::deserialize(reader);
+                return true;
         }
         return TextAsset::deserialize(propertyKey, reader);
     }
@@ -89,6 +110,7 @@
 protected:
     virtual void generatorFunctionRefChanged() {}
     virtual void isModuleChanged() {}
+    virtual void serializedImplementedMethodsChanged() {}
 };
 } // namespace rive
 
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index a51ebaf..920eb85 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -387,12 +387,18 @@
                 return new DataEnumSystem();
             case ViewModelPropertyViewModelBase::typeKey:
                 return new ViewModelPropertyViewModel();
-            case ViewModelInstanceBase::typeKey:
-                return new ViewModelInstance();
-            case ViewModelPropertyBooleanBase::typeKey:
-                return new ViewModelPropertyBoolean();
+            case DataEnumValueBase::typeKey:
+                return new DataEnumValue();
+            case ViewModelPropertyTriggerBase::typeKey:
+                return new ViewModelPropertyTrigger();
+            case ViewModelPropertyStringBase::typeKey:
+                return new ViewModelPropertyString();
             case ViewModelPropertyColorBase::typeKey:
                 return new ViewModelPropertyColor();
+            case ViewModelPropertyBooleanBase::typeKey:
+                return new ViewModelPropertyBoolean();
+            case ViewModelInstanceBase::typeKey:
+                return new ViewModelInstance();
             case ViewModelPropertyAssetImageBase::typeKey:
                 return new ViewModelPropertyAssetImage();
             case ViewModelInstanceBooleanBase::typeKey:
@@ -405,18 +411,12 @@
                 return new ViewModelInstanceTrigger();
             case ViewModelInstanceSymbolListIndexBase::typeKey:
                 return new ViewModelInstanceSymbolListIndex();
-            case ViewModelPropertyStringBase::typeKey:
-                return new ViewModelPropertyString();
             case ViewModelInstanceViewModelBase::typeKey:
                 return new ViewModelInstanceViewModel();
-            case ViewModelPropertyTriggerBase::typeKey:
-                return new ViewModelPropertyTrigger();
             case ViewModelInstanceAssetBase::typeKey:
                 return new ViewModelInstanceAsset();
             case ViewModelInstanceAssetImageBase::typeKey:
                 return new ViewModelInstanceAssetImage();
-            case DataEnumValueBase::typeKey:
-                return new DataEnumValue();
             case CustomPropertyTriggerBase::typeKey:
                 return new CustomPropertyTrigger();
             case ScriptInputTriggerBase::typeKey:
@@ -1669,6 +1669,10 @@
             case ScriptAssetBase::generatorFunctionRefPropertyKey:
                 object->as<ScriptAssetBase>()->generatorFunctionRef(value);
                 break;
+            case ScriptAssetBase::serializedImplementedMethodsPropertyKey:
+                object->as<ScriptAssetBase>()->serializedImplementedMethods(
+                    value);
+                break;
             case AudioEventBase::assetIdPropertyKey:
                 object->as<AudioEventBase>()->assetId(value);
                 break;
@@ -3570,6 +3574,9 @@
                 return object->as<FileAssetBase>()->assetId();
             case ScriptAssetBase::generatorFunctionRefPropertyKey:
                 return object->as<ScriptAssetBase>()->generatorFunctionRef();
+            case ScriptAssetBase::serializedImplementedMethodsPropertyKey:
+                return object->as<ScriptAssetBase>()
+                    ->serializedImplementedMethods();
             case AudioEventBase::assetIdPropertyKey:
                 return object->as<AudioEventBase>()->assetId();
             case GamepadInputBase::kindPropertyKey:
@@ -4496,6 +4503,7 @@
             case CustomPropertyEnumBase::enumIdPropertyKey:
             case FileAssetBase::assetIdPropertyKey:
             case ScriptAssetBase::generatorFunctionRefPropertyKey:
+            case ScriptAssetBase::serializedImplementedMethodsPropertyKey:
             case AudioEventBase::assetIdPropertyKey:
             case GamepadInputBase::kindPropertyKey:
             case GamepadInputBase::mappingPropertyKey:
@@ -5319,6 +5327,8 @@
                 return object->is<FileAssetBase>();
             case ScriptAssetBase::generatorFunctionRefPropertyKey:
                 return object->is<ScriptAssetBase>();
+            case ScriptAssetBase::serializedImplementedMethodsPropertyKey:
+                return object->is<ScriptAssetBase>();
             case AudioEventBase::assetIdPropertyKey:
                 return object->is<AudioEventBase>();
             case GamepadInputBase::kindPropertyKey:
diff --git a/include/rive/lua/rive_lua_libs.hpp b/include/rive/lua/rive_lua_libs.hpp
index d134944..bea58b4 100644
--- a/include/rive/lua/rive_lua_libs.hpp
+++ b/include/rive/lua/rive_lua_libs.hpp
@@ -325,7 +325,7 @@
     gpuCanvas,
     drawCanvas,
     features,
-    loadShader,
+    shader,
     format,
     preferredCanvasFormat,
 
@@ -1470,6 +1470,13 @@
     void setCanvasDrawingPhase(bool value) { m_canvasDrawingPhase = value; }
     bool canvasDrawingPhase() const { return m_canvasDrawingPhase; }
 
+    // When set, context:gpuCanvas() always returns a deferred (texture-less)
+    // canvas regardless of requested size, never calling makeRenderCanvas.
+    // Used by the editor's headless method-detection VM, which has no GPU
+    // device / RenderContext. Default false: normal runtimes allocate.
+    void setGpuCanvasDeferOnly(bool value) { m_gpuCanvasDeferOnly = value; }
+    bool gpuCanvasDeferOnly() const { return m_gpuCanvasDeferOnly; }
+
     // WebGL/WASM only: GL context handle saved at riveGPUBeginFrame so
     // riveGPUEndFrame can restore the caller's context afterwards.
     void setPrevGLContext(intptr_t h) { m_prevGLContext = h; }
@@ -1495,6 +1502,7 @@
     uint64_t m_ownerId = 0;
     bool m_oreFrameOpen = false;
     bool m_canvasDrawingPhase = false;
+    bool m_gpuCanvasDeferOnly = false;
     intptr_t m_prevGLContext = 0;
 #ifdef __EMSCRIPTEN__
     int m_glHandle = 0;
diff --git a/src/animation/scripted_listener_action.cpp b/src/animation/scripted_listener_action.cpp
index 81e1b1c..a6a1a6c 100644
--- a/src/animation/scripted_listener_action.cpp
+++ b/src/animation/scripted_listener_action.cpp
@@ -47,10 +47,13 @@
         return;
     }
     rive_lua_pushRef(L, m_self);
-    if (performsAction())
+    // Stack: [self]
+    // Probe the fields directly (rather than gating on performs()/
+    // performsAction(), which are assumed-present for legacy files):
+    // performAction takes precedence over perform, matching prior behavior.
+    if (static_cast<lua_Type>(lua_getfield(L, -1, "performAction")) ==
+        LUA_TFUNCTION)
     {
-        // Stack: [self]
-        lua_getfield(L, -1, "performAction");
         // Stack: [self, performAction]
         lua_pushvalue(L, -2);
         rive_lua_push_scripted_invocation(L, invocation);
@@ -64,23 +67,33 @@
             // Stack: [self, status]
             lua_pop(L, 1);
         }
+        // Stack: [self]
     }
-    else if (performs())
+    else
     {
-        lua_getfield(L, -1, "perform");
-        // Stack: [self, perform]
-        lua_pushvalue(L, -2);
-        // Stack: [self, perform, self]
-        rive_lua_push_pointer_arg_for_perform(L, invocation);
-        // Stack: [self, perform, self, pointerEvent]
-        if (static_cast<lua_Status>(rive_lua_pcall_with_context(
-                L,
-                const_cast<ScriptedListenerAction*>(this),
-                2,
-                0)) != LUA_OK)
+        lua_pop(L, 1); // non-function performAction field -> [self]
+        if (static_cast<lua_Type>(lua_getfield(L, -1, "perform")) ==
+            LUA_TFUNCTION)
         {
-            // Stack: [self, status]
-            lua_pop(L, 1);
+            // Stack: [self, perform]
+            lua_pushvalue(L, -2);
+            // Stack: [self, perform, self]
+            rive_lua_push_pointer_arg_for_perform(L, invocation);
+            // Stack: [self, perform, self, pointerEvent]
+            if (static_cast<lua_Status>(rive_lua_pcall_with_context(
+                    L,
+                    const_cast<ScriptedListenerAction*>(this),
+                    2,
+                    0)) != LUA_OK)
+            {
+                // Stack: [self, status]
+                lua_pop(L, 1);
+            }
+            // Stack: [self]
+        }
+        else
+        {
+            lua_pop(L, 1); // non-function perform field -> [self]
         }
     }
     // Stack: [self]
diff --git a/src/assets/script_asset.cpp b/src/assets/script_asset.cpp
index 07733de..b1df3dc 100644
--- a/src/assets/script_asset.cpp
+++ b/src/assets/script_asset.cpp
@@ -66,214 +66,6 @@
 bool ScriptInput::validateHydrationPrerequisites() { return true; }
 
 #ifdef WITH_RIVE_SCRIPTING
-bool OptionalScriptedMethods::verifyImplementation(ScriptedObject* object,
-                                                   lua_State* state)
-{
-    // Log the stack-top type before pcall so we can see whether it's nil
-    // (meaning generator-ref resolved to nothing) vs a function that then
-    // errored internally.
-    int topType = static_cast<int>(lua_type(state, -1));
-
-    lua_pushvalue(state, -1);
-    if (static_cast<lua_Status>(rive_lua_pcall(state, 0, 1)) != LUA_OK)
-    {
-        const char* err = lua_tostring(state, -1);
-        fprintf(stderr,
-                "Verifying implementation pcall failed (protocol=%d, "
-                "top-type-before=%d): %s\n",
-                (int)object->scriptProtocol(),
-                topType,
-                err ? err : "(no error message)");
-        rive_lua_pop(state, 1);
-        return false;
-    }
-    if (static_cast<lua_Type>(lua_type(state, -1)) != LUA_TTABLE)
-    {
-        fprintf(stderr,
-                "Verifying implementation not a table (protocol=%d)?\n",
-                (int)object->scriptProtocol());
-        rive_lua_pop(state, 1);
-        return false;
-    }
-    m_implementedMethods = 0;
-
-    auto scriptProtocol = object->scriptProtocol();
-    if (scriptProtocol == ScriptProtocol::node ||
-        scriptProtocol == ScriptProtocol::layout ||
-        scriptProtocol == ScriptProtocol::converter ||
-        scriptProtocol == ScriptProtocol::pathEffect ||
-        scriptProtocol == ScriptProtocol::listenerAction ||
-        scriptProtocol == ScriptProtocol::transitionCondition ||
-        scriptProtocol == ScriptProtocol::interpolator)
-    {
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "update")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_updatesBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "advance")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_advancesBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "pointerDown")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsPointerDownBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "pointerUp")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsPointerUpBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "pointerMove")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsPointerMoveBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "pointerCanceled")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsPointerCancelBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "pointerExit")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsPointerExitBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(
-                lua_getfield(state, -1, "gamepadConnected")) == LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsGamepadConnect;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "gamepadEvent")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsGamepadEvent;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(
-                lua_getfield(state, -1, "gamepadDisconnected")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsGamepadDisconnect;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "init")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_initsBit;
-        }
-        rive_lua_pop(state, 1);
-    }
-    if (scriptProtocol == ScriptProtocol::layout)
-    {
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "measure")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_measuresBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "resize")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_resizesBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "draw")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_drawsBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "drawCanvas")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_drawsCanvasBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "keyboardEvent")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsKeyboardInputBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "textEvent")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsTextInputBit;
-        }
-        rive_lua_pop(state, 1);
-    }
-    else if (scriptProtocol == ScriptProtocol::node)
-    {
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "draw")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_drawsBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "drawCanvas")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_drawsCanvasBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "keyboardEvent")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsKeyboardInputBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "textEvent")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_wantsTextInputBit;
-        }
-        rive_lua_pop(state, 1);
-    }
-    else if (scriptProtocol == ScriptProtocol::converter)
-    {
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "convert")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_dataConvertsBit;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "reverseConvert")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_dataReverseConvertsBit;
-        }
-        rive_lua_pop(state, 1);
-    }
-    else if (scriptProtocol == ScriptProtocol::listenerAction)
-    {
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "perform")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_listenerPerforms;
-        }
-        rive_lua_pop(state, 1);
-        if (static_cast<lua_Type>(lua_getfield(state, -1, "performAction")) ==
-            LUA_TFUNCTION)
-        {
-            m_implementedMethods |= m_listenerPerformsAction;
-        }
-        rive_lua_pop(state, 1);
-    }
-    rive_lua_pop(state, 1);
-    return true;
-}
-
 ScriptingVM* ScriptAsset::scriptingVM()
 {
     if (m_file == nullptr)
@@ -355,11 +147,14 @@
 
     if (!m_initted)
     {
-        if (!verifyImplementation(object, state))
-        {
-            rive_lua_pop(state, 1);
-            return false;
-        }
+        // The editor detected the implemented methods (by executing the
+        // generator in its workspace) and serialized the bitfield; the runtime
+        // never detects. Files predating this property decode to the all-bits
+        // default (2097151), so they behave as "implements everything" and rely
+        // on graceful dispatch (each callback no-ops when the method isn't
+        // actually a function on the returned table).
+        OptionalScriptedMethods::implementedMethods(
+            serializedImplementedMethods() & methodMask);
         m_initted = true;
     }
     object->implementedMethods(implementedMethods());
diff --git a/src/lua/lua_scripted_context.cpp b/src/lua/lua_scripted_context.cpp
index 212c9a7..1cd4d11 100644
--- a/src/lua/lua_scripted_context.cpp
+++ b/src/lua/lua_scripted_context.cpp
@@ -453,6 +453,18 @@
 #else
                 auto* gpuScriptingCtx =
                     static_cast<ScriptingContext*>(lua_getthreaddata(L));
+                if (gpuScriptingCtx->gpuCanvasDeferOnly())
+                {
+                    // Headless detection (editor method-detection VM): there is
+                    // no real RenderContext/GPU device. Hand back a deferred
+                    // canvas with no backing texture regardless of requested
+                    // size, so a generator that creates a sized canvas at
+                    // construction runs without reaching makeRenderCanvas.
+                    auto* handle = lua_newrive<ScriptedGPUCanvas>(L);
+                    handle->m_L = L;
+                    handle->renderCtx = nullptr;
+                    return 1;
+                }
                 auto* gpuRenderCtx = static_cast<gpu::RenderContext*>(
                     gpuScriptingCtx->renderContext());
                 if (gpuRenderCtx == nullptr)
@@ -515,7 +527,7 @@
             case (int)LuaAtoms::preferredCanvasFormat:
                 return lua_push_preferred_canvas_format(L);
 
-            case (int)LuaAtoms::loadShader:
+            case (int)LuaAtoms::shader:
             {
 #if defined(RIVE_CANVAS) && defined(RIVE_ORE)
                 const char* shaderName = luaL_checkstring(L, 2);
diff --git a/src/lua/renderer/lua_gpu.cpp b/src/lua/renderer/lua_gpu.cpp
index 87270d1..c028942 100644
--- a/src/lua/renderer/lua_gpu.cpp
+++ b/src/lua/renderer/lua_gpu.cpp
@@ -863,7 +863,7 @@
 {
     auto* context = static_cast<ScriptingContext*>(lua_getthreaddata(L));
 
-    // Resolve the file-side ShaderAsset by name. Without this, Shader.new()
+    // Resolve the file-side ShaderAsset by name. Without this, the lookup
     // falls back to the editor-only RSTB cache which is empty at runtime.
     ShaderAsset* fileAsset = nullptr;
     if (context != nullptr)
@@ -900,20 +900,6 @@
     return 1;
 }
 
-static int shader_construct(lua_State* L)
-{
-    // Argument 1 is the name of a pre-compiled WGSL shader asset that was
-    // bundled with the Rive file (set via the "wgslAssetName" field in the
-    // editor).  Raw WGSL source strings are NOT accepted at runtime; shaders
-    // must be pre-compiled and embedded as assets.
-    const char* name = luaL_checkstring(L, 1);
-    if (lua_gpu_push_shader_by_name(L, name) == 0)
-    {
-        luaL_error(L, "Shader.new: no shader asset named '%s' found", name);
-    }
-    return 1;
-}
-
 // ============================================================================
 // GPUBuffer
 // ============================================================================
@@ -3396,8 +3382,8 @@
 
 int luaopen_rive_gpu(lua_State* L)
 {
-    static const luaL_Reg shaderStatics[] = {{"new", shader_construct},
-                                             {nullptr, nullptr}};
+    // Shader has no constructor; shaders are obtained via context:shader(name).
+    static const luaL_Reg shaderStatics[] = {{nullptr, nullptr}};
     static const luaL_Reg gpuBufferStatics[] = {{"new", gpubuffer_construct},
                                                 {nullptr, nullptr}};
     static const luaL_Reg gpuTextureStatics[] = {{"new", gputexture_construct},
diff --git a/src/lua/rive_lua_libs.cpp b/src/lua/rive_lua_libs.cpp
index 64aa082..678451b 100644
--- a/src/lua/rive_lua_libs.cpp
+++ b/src/lua/rive_lua_libs.cpp
@@ -277,7 +277,7 @@
     {"gpuCanvas", (int16_t)LuaAtoms::gpuCanvas},
     {"features", (int16_t)LuaAtoms::features},
     {"drawCanvas", (int16_t)LuaAtoms::drawCanvas},
-    {"loadShader", (int16_t)LuaAtoms::loadShader},
+    {"shader", (int16_t)LuaAtoms::shader},
     {"format", (int16_t)LuaAtoms::format},
     {"preferredCanvasFormat", (int16_t)LuaAtoms::preferredCanvasFormat},
     {"andThen", (int16_t)LuaAtoms::andThen},
diff --git a/src/scripted/scripted_data_converter.cpp b/src/scripted/scripted_data_converter.cpp
index 3cc0e90..381fd35 100644
--- a/src/scripted/scripted_data_converter.cpp
+++ b/src/scripted/scripted_data_converter.cpp
@@ -97,8 +97,14 @@
     // Stack: []
     rive_lua_pushRef(L, m_self);
     // Stack: [self]
-    lua_getfield(L, -1, method.c_str());
-
+    if (static_cast<lua_Type>(lua_getfield(L, -1, method.c_str())) !=
+        LUA_TFUNCTION)
+    {
+        // Assumed for legacy files but not implemented; pass the value through
+        // unchanged (same as !dataConverts()).
+        rive_lua_pop(L, 2); // non-function field + self
+        return value;
+    }
     // Stack: [self, field]
     lua_pushvalue(L, -2);
     // Stack: [self, field, self]
diff --git a/src/scripted/scripted_drawable.cpp b/src/scripted/scripted_drawable.cpp
index 01923a4..61d6fdc 100644
--- a/src/scripted/scripted_drawable.cpp
+++ b/src/scripted/scripted_drawable.cpp
@@ -42,17 +42,25 @@
     // Stack: [scriptedRenderer]
     rive_lua_pushRef(L, m_self);
     // Stack: [scriptedRenderer, self]
-    lua_getfield(L, -1, "draw");
-    // Stack: [scriptedRenderer, self, "draw"]
-    lua_pushvalue(L, -2);
-    // Stack: [scriptedRenderer, self, "draw", self]
-    lua_pushvalue(L, -4);
-    // Stack: [scriptedRenderer, self, "draw", self, scriptedRenderer]
-    if (static_cast<lua_Status>(rive_lua_pcall_with_context(L, this, 2, 0)) !=
-        LUA_OK)
+    if (static_cast<lua_Type>(lua_getfield(L, -1, "draw")) == LUA_TFUNCTION)
     {
-        // Stack: [scriptedRenderer, self, status]
-        rive_lua_pop(L, 1);
+        // Stack: [scriptedRenderer, self, "draw"]
+        lua_pushvalue(L, -2);
+        // Stack: [scriptedRenderer, self, "draw", self]
+        lua_pushvalue(L, -4);
+        // Stack: [scriptedRenderer, self, "draw", self, scriptedRenderer]
+        if (static_cast<lua_Status>(
+                rive_lua_pcall_with_context(L, this, 2, 0)) != LUA_OK)
+        {
+            // Stack: [scriptedRenderer, self, status]
+            rive_lua_pop(L, 1);
+        }
+    }
+    else
+    {
+        // draw is assumed for legacy files but not implemented; no-op (the
+        // save/transform above stay balanced with the restore below).
+        rive_lua_pop(L, 1); // non-function field
     }
     scriptedRenderer->end();
     // Stack: [scriptedRenderer, self]
@@ -157,7 +165,9 @@
     if (static_cast<lua_Type>(lua_getfield(state, -1, mName.c_str())) !=
         LUA_TFUNCTION)
     {
-        fprintf(stderr, "expected %s to be a function\n", mName.c_str());
+        // The pointer handler is assumed present for legacy files (all-bits
+        // default) but isn't actually implemented: report "not hit" so the
+        // state machine keeps walking other hit targets.
         rive_lua_pop(state, 1);
     }
     else
@@ -208,7 +218,13 @@
     // Stack: []
     rive_lua_pushRef(L, self());
     // Stack: [self]
-    lua_getfield(L, -1, "keyboardEvent");
+    if (static_cast<lua_Type>(lua_getfield(L, -1, "keyboardEvent")) !=
+        LUA_TFUNCTION)
+    {
+        // Assumed for legacy files but not implemented; no-op.
+        rive_lua_pop(L, 2); // non-function field + self
+        return shouldStopPropagation;
+    }
     // Stack: [self, field]
     lua_pushvalue(L, -2);
     // Stack: [self, field, self]
@@ -255,7 +271,13 @@
     // Stack: []
     rive_lua_pushRef(L, self());
     // Stack: [self]
-    lua_getfield(L, -1, "textEvent");
+    if (static_cast<lua_Type>(lua_getfield(L, -1, "textEvent")) !=
+        LUA_TFUNCTION)
+    {
+        // Assumed for legacy files but not implemented; no-op.
+        rive_lua_pop(L, 2); // non-function field + self
+        return shouldStopPropagation;
+    }
     // Stack: [self, field]
     lua_pushvalue(L, -2);
     // Stack: [self, field, self]
diff --git a/src/scripted/scripted_layout.cpp b/src/scripted/scripted_layout.cpp
index 05e8ef0..20bb6b7 100644
--- a/src/scripted/scripted_layout.cpp
+++ b/src/scripted/scripted_layout.cpp
@@ -28,7 +28,12 @@
     // Stack: []
     rive_lua_pushRef(L, m_self);
     // Stack: [self]
-    lua_getfield(L, -1, "resize");
+    if (static_cast<lua_Type>(lua_getfield(L, -1, "resize")) != LUA_TFUNCTION)
+    {
+        // Assumed for legacy files but not implemented; no-op.
+        rive_lua_pop(L, 2); // non-function field + self
+        return;
+    }
     // Stack: [self, function]
     lua_pushvalue(L, -2);
     // Stack: [self, function, self]
@@ -60,7 +65,13 @@
     // Stack: []
     rive_lua_pushRef(L, m_self);
     // Stack: [self]
-    lua_getfield(L, -1, "measure");
+    if (static_cast<lua_Type>(lua_getfield(L, -1, "measure")) != LUA_TFUNCTION)
+    {
+        // Assumed for legacy files but not implemented; report no measurement
+        // (same as !measures()).
+        rive_lua_pop(L, 2); // non-function field + self
+        return Vec2D(0, 0);
+    }
     // Stack: [self, field]
     lua_pushvalue(L, -2);
     // Stack: [self, field, self]
diff --git a/src/scripted/scripted_object.cpp b/src/scripted/scripted_object.cpp
index e2c6362..2eff633 100644
--- a/src/scripted/scripted_object.cpp
+++ b/src/scripted/scripted_object.cpp
@@ -183,7 +183,13 @@
         return false;
     }
     rive_lua_pushRef(L, m_self);
-    lua_getfield(L, -1, "advance");
+    // implementedMethods may be assumed for legacy files (all-bits default); if
+    // the field isn't actually a function, treat it as not implemented.
+    if (static_cast<lua_Type>(lua_getfield(L, -1, "advance")) != LUA_TFUNCTION)
+    {
+        rive_lua_pop(L, 2); // non-function field + self
+        return false;
+    }
     lua_pushvalue(L, -2);
     lua_pushnumber(L, elapsedSeconds);
     if (static_cast<lua_Status>(rive_lua_pcall_with_context(L, this, 2, 1)) !=
@@ -205,7 +211,12 @@
         return;
     }
     rive_lua_pushRef(L, m_self);
-    lua_getfield(L, -1, "drawCanvas");
+    if (static_cast<lua_Type>(lua_getfield(L, -1, "drawCanvas")) !=
+        LUA_TFUNCTION)
+    {
+        rive_lua_pop(L, 2); // non-function field + self
+        return;
+    }
     lua_pushvalue(L, -2);
     if (static_cast<lua_Status>(rive_lua_pcall(L, 1, 0)) != LUA_OK)
     {
@@ -222,11 +233,18 @@
     {
         return;
     }
-    m_inUpdatePhase = true;
     // Stack: []
     rive_lua_pushRef(L, m_self);
     // Stack: [self]
-    lua_getfield(L, -1, "update");
+    if (static_cast<lua_Type>(lua_getfield(L, -1, "update")) != LUA_TFUNCTION)
+    {
+        // Not actually implemented (assumed for legacy files); no-op. The
+        // update phase never started, so there's no flag to reset.
+        rive_lua_pop(L, 2); // non-function field + self
+        return;
+    }
+    // Only inside the update phase while the callback actually runs.
+    m_inUpdatePhase = true;
     // Stack: [self, field] Swap self and field
     lua_insert(L, -2);
     // Stack: [field, self]
@@ -242,7 +260,13 @@
 {
     rive_lua_pushRef(L, m_self);
     // Stack: [self]
-    lua_getfield(L, -1, "init");
+    if (static_cast<lua_Type>(lua_getfield(L, -1, "init")) != LUA_TFUNCTION)
+    {
+        // init is optional and not implemented (assumed for legacy files);
+        // nothing to run — the object is considered initialized.
+        rive_lua_pop(L, 2); // non-function field + self
+        return true;
+    }
     // Stack: [self, field]
     lua_pushvalue(L, -2);
     // Stack: [self, field, self]
diff --git a/tests/unit_tests/silvers/script_input_color_trigger.sriv b/tests/unit_tests/silvers/script_input_color_trigger.sriv
index 1864827..aed4bab 100644
--- a/tests/unit_tests/silvers/script_input_color_trigger.sriv
+++ b/tests/unit_tests/silvers/script_input_color_trigger.sriv
Binary files differ