blob: d1e3993b0e75c8dffaafcc3580223c8130d99d4c [file]
#ifdef WITH_RIVE_SCRIPTING
#include <stdio.h>
#include "rive/lua/rive_lua_libs.hpp"
#include "rive/scripted/scripted_object.hpp"
#include "rive/assets/image_asset.hpp"
#include "rive/assets/blob_asset.hpp"
#include "rive/file.hpp"
#ifdef WITH_RIVE_AUDIO
#include "rive/audio/audio_engine.hpp"
#include "rive/assets/audio_asset.hpp"
#endif
#ifdef RIVE_CANVAS
#include "rive/renderer/render_context.hpp"
#include "rive/renderer/render_canvas.hpp"
#ifdef RIVE_ORE
#include "rive/renderer/ore/ore_context.hpp"
#include "rive/assets/script_asset.hpp"
#include "rive/assets/shader_asset.hpp"
#endif
#endif
#include <math.h>
#include <stdio.h>
#include <string.h>
using namespace rive;
// Pushes a GPU features table onto the Lua stack. Queries the ORE context
// when available, otherwise returns conservative defaults. Always returns 1.
int lua_push_gpu_features(lua_State* L)
{
#if defined(RIVE_CANVAS) && defined(RIVE_ORE)
auto* oreCtx = static_cast<ore::Context*>(
static_cast<ScriptingContext*>(lua_getthreaddata(L))->oreContext());
if (oreCtx != nullptr)
{
const auto& f = oreCtx->features();
lua_createtable(L, 0, 18);
lua_pushboolean(L, f.bc);
lua_setfield(L, -2, "bc");
lua_pushboolean(L, f.etc2);
lua_setfield(L, -2, "etc2");
lua_pushboolean(L, f.astc);
lua_setfield(L, -2, "astc");
lua_pushnumber(L, f.maxTextureSize2D);
lua_setfield(L, -2, "maxTextureSize2D");
lua_pushnumber(L, f.maxTextureSizeCube);
lua_setfield(L, -2, "maxTextureSizeCube");
lua_pushnumber(L, f.maxTextureSize3D);
lua_setfield(L, -2, "maxTextureSize3D");
lua_pushboolean(L, f.anisotropicFiltering);
lua_setfield(L, -2, "anisotropicFiltering");
lua_pushboolean(L, f.texture3D);
lua_setfield(L, -2, "texture3D");
lua_pushboolean(L, f.textureArrays);
lua_setfield(L, -2, "textureArrays");
lua_pushboolean(L, f.colorBufferFloat);
lua_setfield(L, -2, "colorBufferFloat");
lua_pushboolean(L, f.perTargetBlend);
lua_setfield(L, -2, "perTargetBlend");
lua_pushboolean(L, f.perTargetWriteMask);
lua_setfield(L, -2, "perTargetWriteMask");
lua_pushboolean(L, f.drawBaseInstance);
lua_setfield(L, -2, "drawBaseInstance");
lua_pushboolean(L, f.depthBiasClamp);
lua_setfield(L, -2, "depthBiasClamp");
lua_pushnumber(L, f.maxColorAttachments);
lua_setfield(L, -2, "maxColorAttachments");
lua_pushnumber(L, f.maxUniformBufferSize);
lua_setfield(L, -2, "maxUniformBufferSize");
lua_pushnumber(L, f.maxSamplers);
lua_setfield(L, -2, "maxSamplers");
lua_pushnumber(L, f.maxSamples);
lua_setfield(L, -2, "maxSamples");
lua_setreadonly(L, -1, true);
return 1;
}
#endif
// Fallback when no ore context is available
lua_createtable(L, 0, 18);
lua_pushboolean(L, false);
lua_setfield(L, -2, "bc");
lua_pushboolean(L, false);
lua_setfield(L, -2, "etc2");
lua_pushboolean(L, false);
lua_setfield(L, -2, "astc");
lua_pushnumber(L, 4096);
lua_setfield(L, -2, "maxTextureSize2D");
lua_pushnumber(L, 4096);
lua_setfield(L, -2, "maxTextureSizeCube");
lua_pushnumber(L, 256);
lua_setfield(L, -2, "maxTextureSize3D");
lua_pushboolean(L, false);
lua_setfield(L, -2, "anisotropicFiltering");
lua_pushboolean(L, false);
lua_setfield(L, -2, "texture3D");
lua_pushboolean(L, false);
lua_setfield(L, -2, "textureArrays");
lua_pushboolean(L, false);
lua_setfield(L, -2, "colorBufferFloat");
lua_pushboolean(L, false);
lua_setfield(L, -2, "perTargetBlend");
lua_pushboolean(L, false);
lua_setfield(L, -2, "perTargetWriteMask");
lua_pushboolean(L, false);
lua_setfield(L, -2, "drawBaseInstance");
lua_pushboolean(L, false);
lua_setfield(L, -2, "depthBiasClamp");
lua_pushnumber(L, 4);
lua_setfield(L, -2, "maxColorAttachments");
lua_pushnumber(L, 16384);
lua_setfield(L, -2, "maxUniformBufferSize");
lua_pushnumber(L, 16);
lua_setfield(L, -2, "maxSamplers");
lua_pushnumber(L, 4);
lua_setfield(L, -2, "maxSamples");
lua_setreadonly(L, -1, true);
return 1;
}
// Push the platform's preferred canvas color format onto the Lua stack as a
// string. makeRenderCanvas() hardcodes RGBA8 on Metal so that Rive's internal
// PLS shaders (which work in RGBA space) can render into the canvas without
// format-dependent swizzling.
// Metal (macOS/iOS) → rgba8unorm (off-screen canvas, not a CAMetalLayer
// surface)
// D3D11/D3D12 → bgra8unorm
// Vulkan/GL/WebGPU → rgba8unorm (safe default; actual format may vary —
// query canvas.format for the authoritative value once
// a canvas has been created)
int lua_push_preferred_canvas_format(lua_State* L)
{
#if defined(_WIN32)
lua_pushstring(L, "bgra8unorm");
#else
lua_pushstring(L, "rgba8unorm");
#endif
return 1;
}
ScriptedContext::ScriptedContext(ScriptedObject* scriptedObject) :
m_scriptedObject(scriptedObject)
{}
int ScriptedContext::pushViewModel(lua_State* state)
{
if (m_scriptedObject)
{
auto dataContext = m_scriptedObject->dataContext();
if (dataContext && dataContext->viewModelInstance())
{
auto viewModelInstance = dataContext->viewModelInstance();
lua_newrive<ScriptedViewModel>(
state,
state,
ref_rcp(viewModelInstance->viewModel()),
viewModelInstance);
return 1;
}
}
m_missingRequestedData = true;
return 0;
}
int ScriptedContext::pushRootViewModel(lua_State* state)
{
if (m_scriptedObject)
{
auto dataContext = m_scriptedObject->dataContext();
if (dataContext)
{
auto viewModelInstance = dataContext->rootViewModelInstance();
if (viewModelInstance)
{
lua_newrive<ScriptedViewModel>(
state,
state,
ref_rcp(viewModelInstance->viewModel()),
viewModelInstance);
return 1;
}
}
}
m_missingRequestedData = true;
return 0;
}
int ScriptedContext::pushDataContext(lua_State* state)
{
if (m_scriptedObject)
{
auto dataContext = m_scriptedObject->dataContext();
if (dataContext)
{
lua_newrive<ScriptedDataContext>(state, state, dataContext);
return 1;
}
}
m_missingRequestedData = true;
return 0;
}
static int context_namecall(lua_State* L)
{
int atom;
const char* str = lua_namecallatom(L, &atom);
if (str != nullptr)
{
auto scriptedContext = lua_torive<ScriptedContext>(L, 1);
if (scriptedContext->scriptedObject() == nullptr)
{
luaL_error(L,
"context:%s() called on a disposed context — the "
"context passed to init() must not be used after "
"init() returns",
str);
return 0;
}
switch (atom)
{
case (int)LuaAtoms::markNeedsUpdate:
{
auto scriptedObject = scriptedContext->scriptedObject();
scriptedObject->markNeedsUpdate();
return 0;
}
case (int)LuaAtoms::viewModel:
{
return scriptedContext->pushViewModel(L);
}
case (int)LuaAtoms::rootViewModel:
{
return scriptedContext->pushRootViewModel(L);
}
case (int)LuaAtoms::image:
{
const char* imageName = luaL_checkstring(L, 2);
// First, try to find the image from the file's assets (runtime)
auto scriptedObject = scriptedContext->scriptedObject();
auto scriptAsset = scriptedObject->scriptAsset();
if (scriptAsset != nullptr)
{
File* file = scriptAsset->file();
if (file != nullptr)
{
// Find ImageAsset by name
auto assets = file->assets();
for (const auto& asset : assets)
{
if (asset->is<ImageAsset>())
{
ImageAsset* imageAsset =
asset->as<ImageAsset>();
if (imageAsset->name() == imageName)
{
RenderImage* renderImage =
imageAsset->renderImage();
if (renderImage != nullptr)
{
auto scriptedImage =
lua_newrive<ScriptedImage>(L);
// ref_rcp properly refs the RenderImage
// for the rcp<>. When ScriptedImage is
// GC'd, rcp<> destructor will deref()
scriptedImage->image =
ref_rcp(renderImage);
return 1;
}
}
}
}
}
}
return 0; // return nil if not found
}
case (int)LuaAtoms::blob:
{
const char* blobName = luaL_checkstring(L, 2);
auto scriptedObject = scriptedContext->scriptedObject();
auto scriptAsset = scriptedObject->scriptAsset();
if (scriptAsset != nullptr)
{
File* file = scriptAsset->file();
if (file != nullptr)
{
auto assets = file->assets();
for (const auto& asset : assets)
{
if (asset->is<BlobAsset>())
{
BlobAsset* blobAsset = asset->as<BlobAsset>();
if (blobAsset->name() == blobName)
{
auto bytes = blobAsset->bytes();
if (!bytes.empty())
{
auto scriptedBlob =
lua_newrive<ScriptedBlob>(L);
scriptedBlob->asset = ref_rcp(
static_cast<FileAsset*>(blobAsset));
return 1;
}
}
}
}
}
}
return 0; // return nil if not found
}
case (int)LuaAtoms::dataContext:
{
return scriptedContext->pushDataContext(L);
}
#ifdef WITH_RIVE_AUDIO
case (int)LuaAtoms::audio:
{
const char* audioName = luaL_checkstring(L, 2);
auto scriptedObject = scriptedContext->scriptedObject();
auto scriptAsset = scriptedObject->scriptAsset();
if (scriptAsset != nullptr)
{
File* file = scriptAsset->file();
if (file != nullptr)
{
auto assets = file->assets();
for (const auto& asset : assets)
{
if (asset->is<AudioAsset>())
{
AudioAsset* audioAsset =
asset->as<AudioAsset>();
if (audioAsset->name() == audioName)
{
auto audioSource =
audioAsset->audioSource();
if (audioSource != nullptr)
{
auto scriptedAudioSource =
lua_newrive<ScriptedAudioSource>(L);
scriptedAudioSource->source(
audioSource);
return 1;
}
}
}
}
}
}
return 0; // return nil if not found
}
#endif
case (int)LuaAtoms::canvas:
{
// context:canvas({ width = w, height = h, clearColor = c })
luaL_checktype(L, 2, LUA_TTABLE);
lua_getfield(L, 2, "width");
uint32_t cw = (uint32_t)luaL_checknumber(L, -1);
lua_pop(L, 1);
lua_getfield(L, 2, "height");
uint32_t ch = (uint32_t)luaL_checknumber(L, -1);
lua_pop(L, 1);
#ifndef RIVE_CANVAS
(void)cw;
(void)ch;
luaL_error(L, "context:canvas() requires a RIVE_CANVAS build");
return 0;
#else
auto* scriptingCtx =
static_cast<ScriptingContext*>(lua_getthreaddata(L));
auto* renderCtx = static_cast<gpu::RenderContext*>(
scriptingCtx->renderContext());
if (renderCtx == nullptr)
{
luaL_error(
L,
"context:canvas() requires a RenderContext — call "
"setRenderContext() first");
return 0;
}
auto canvas = renderCtx->makeRenderCanvas(cw, ch);
if (!canvas)
{
luaL_error(
L,
"context:canvas() failed to create RenderCanvas");
return 0;
}
auto* handle = lua_newrive<ScriptedCanvas>(L);
handle->m_L = L;
handle->canvas = std::move(canvas);
handle->renderCtx = renderCtx;
// Create a ScriptedImage backed by canvas->renderImage() so
// the script can composite it with renderer:drawImage()
auto* img = lua_newrive<ScriptedImage>(L);
img->image = ref_rcp(
static_cast<RenderImage*>(handle->canvas->renderImage()));
handle->m_imageRef = lua_ref(L, -1);
lua_pop(L, 1); // pop image, handle remains on top
return 1;
#endif
}
case (int)LuaAtoms::gpuCanvas:
{
// context:gpuCanvas({ width = w, height = h })
luaL_checktype(L, 2, LUA_TTABLE);
lua_getfield(L, 2, "width");
uint32_t gw = (uint32_t)luaL_checknumber(L, -1);
lua_pop(L, 1);
lua_getfield(L, 2, "height");
uint32_t gh = (uint32_t)luaL_checknumber(L, -1);
lua_pop(L, 1);
#if !defined(RIVE_CANVAS) || !defined(RIVE_ORE)
(void)gw;
(void)gh;
luaL_error(L,
"context:gpuCanvas() requires a RIVE_CANVAS + "
"RIVE_ORE build");
return 0;
#else
auto* gpuScriptingCtx =
static_cast<ScriptingContext*>(lua_getthreaddata(L));
auto* gpuRenderCtx = static_cast<gpu::RenderContext*>(
gpuScriptingCtx->renderContext());
if (gpuRenderCtx == nullptr)
{
luaL_error(
L,
"context:gpuCanvas() requires a RenderContext — call "
"setRenderContext() first");
return 0;
}
auto* oreCtx =
static_cast<ore::Context*>(gpuScriptingCtx->oreContext());
if (oreCtx == nullptr)
{
luaL_error(
L,
"context:gpuCanvas() requires a GPU context — call "
"scriptingWorkspaceSetOreContext() before requestVM()");
return 0;
}
auto canvas = gpuRenderCtx->makeRenderCanvas(gw, gh);
if (!canvas)
{
luaL_error(
L,
"context:gpuCanvas() failed to create RenderCanvas");
return 0;
}
auto colorView = oreCtx->wrapCanvasTexture(canvas.get());
if (!colorView)
{
luaL_error(
L,
"context:gpuCanvas() failed to wrap canvas texture");
return 0;
}
auto* handle = lua_newrive<ScriptedGPUCanvas>(L);
handle->m_L = L;
handle->canvas = std::move(canvas);
handle->oreColorView = std::move(colorView);
handle->renderCtx = gpuRenderCtx;
// Optional MSAA: context:gpuCanvas({ ..., sampleCount = 4 })
lua_getfield(L, 2, "sampleCount");
uint32_t gpuCanvasSamples =
lua_isnumber(L, -1)
? static_cast<uint32_t>(lua_tonumber(L, -1))
: 1;
lua_pop(L, 1);
if (gpuCanvasSamples > 1)
{
if ((gpuCanvasSamples & (gpuCanvasSamples - 1)) != 0)
luaL_error(L,
"gpuCanvas sampleCount must be a power of "
"two (got %u)",
gpuCanvasSamples);
uint32_t maxSamples = oreCtx->features().maxSamples;
if (gpuCanvasSamples > maxSamples)
luaL_error(L,
"gpuCanvas sampleCount %u exceeds device "
"maximum of %u — query "
"context:features().maxSamples first",
gpuCanvasSamples,
maxSamples);
}
if (gpuCanvasSamples > 1)
{
ore::TextureDesc msaaDesc;
msaaDesc.width = gw;
msaaDesc.height = gh;
msaaDesc.format = handle->oreColorView->texture()->format();
msaaDesc.renderTarget = true;
msaaDesc.sampleCount = gpuCanvasSamples;
msaaDesc.label = "GPUCanvasMSAAColor";
handle->oreMSAAColorTexture = oreCtx->makeTexture(msaaDesc);
if (!handle->oreMSAAColorTexture)
{
luaL_error(
L,
"context:gpuCanvas() failed to create MSAA color "
"texture (sampleCount=%u)",
gpuCanvasSamples);
return 0;
}
ore::TextureViewDesc msaaViewDesc;
msaaViewDesc.texture = handle->oreMSAAColorTexture.get();
handle->oreMSAAColorView =
oreCtx->makeTextureView(msaaViewDesc);
// Depth buffer — sampleCount must match the MSAA color
// attachment or Metal/Vulkan will reject the render pass.
ore::TextureDesc depthDesc;
depthDesc.width = gw;
depthDesc.height = gh;
depthDesc.format = ore::TextureFormat::depth32float;
depthDesc.renderTarget = true;
depthDesc.sampleCount = gpuCanvasSamples;
depthDesc.label = "GPUCanvasMSAADepth";
handle->oreDepthTexture = oreCtx->makeTexture(depthDesc);
if (!handle->oreDepthTexture)
{
luaL_error(
L,
"context:gpuCanvas() failed to create MSAA depth "
"texture (sampleCount=%u)",
gpuCanvasSamples);
return 0;
}
ore::TextureViewDesc depthViewDesc;
depthViewDesc.texture = handle->oreDepthTexture.get();
handle->oreDepthView =
oreCtx->makeTextureView(depthViewDesc);
}
// Create a ScriptedImage backed by canvas->renderImage() so
// the script can composite it with renderer:drawImage()
auto* img = lua_newrive<ScriptedImage>(L);
img->image = ref_rcp(
static_cast<RenderImage*>(handle->canvas->renderImage()));
handle->m_imageRef = lua_ref(L, -1);
lua_pop(L, 1); // pop image, handle remains on top
return 1;
#endif
}
case (int)LuaAtoms::features:
return lua_push_gpu_features(L);
case (int)LuaAtoms::preferredCanvasFormat:
return lua_push_preferred_canvas_format(L);
case (int)LuaAtoms::loadShader:
{
#if defined(RIVE_CANVAS) && defined(RIVE_ORE)
const char* shaderName = luaL_checkstring(L, 2);
// Runtime path: search file->assets() for a ShaderAsset
// with the matching name.
ShaderAsset* fileAsset = nullptr;
auto scriptedObject = scriptedContext->scriptedObject();
auto scriptAsset = scriptedObject->scriptAsset();
if (scriptAsset != nullptr)
{
File* file = scriptAsset->file();
if (file != nullptr)
{
for (const auto& asset : file->assets())
{
if (asset->is<ShaderAsset>())
{
auto* sa = asset->as<ShaderAsset>();
if (sa->name() == shaderName)
{
fileAsset = sa;
break;
}
}
}
}
}
auto* scriptingCtx =
static_cast<ScriptingContext*>(lua_getthreaddata(L));
auto* scripted = lua_newrive<ScriptedShader>(L);
if (lua_gpu_load_shader_by_name(scripted,
scriptingCtx,
shaderName,
fileAsset))
{
return 1;
}
lua_pop(L, 1);
// Legacy-compat fallback: pre-ShaderAsset .riv files pack
// WGSL shaders into ScriptAssets. Detect by RSTB magic and
// build the shader from those raw bytes.
if (scriptAsset != nullptr)
{
File* file = scriptAsset->file();
if (file != nullptr)
{
for (const auto& asset : file->assets())
{
if (!asset->is<ScriptAsset>())
continue;
auto* sa = asset->as<ScriptAsset>();
if (sa->name() != shaderName)
continue;
auto bc = sa->moduleBytecode();
if (bc.size() < 4 || bc[0] != 'R' || bc[1] != 'S' ||
bc[2] != 'T' || bc[3] != 'B')
continue;
auto* legacyScripted =
lua_newrive<ScriptedShader>(L);
if (lua_gpu_make_shader_from_rstb(
legacyScripted,
scriptingCtx,
bc.data(),
static_cast<uint32_t>(bc.size())))
{
return 1;
}
lua_pop(L, 1);
break;
}
}
}
return 0; // return nil — shader not found or compile failed
#else
return 0;
#endif
}
case (int)LuaAtoms::decodeImage:
{
// Defined in lua_image_decode.cpp.
extern int context_decodeImage_impl(lua_State * L);
return context_decodeImage_impl(L);
}
default:
break;
}
}
luaL_error(L,
"%s is not a valid method of %s",
str,
ScriptedContext::luaName);
return 0;
}
int luaopen_rive_contex(lua_State* L)
{
{
lua_register_rive<ScriptedContext>(L);
lua_pushcfunction(L, context_namecall, nullptr);
lua_setfield(L, -2, "__namecall");
lua_setreadonly(L, -1, true);
lua_pop(L, 1); // pop the metatable
}
return 0;
}
#endif