blob: e82cb4d9ace0673117e16b426b9fa1c1dd44619a [file]
#ifdef WITH_RIVE_SCRIPTING
#if defined(RIVE_CANVAS) && defined(RIVE_ORE)
#include "lualib.h"
#include "rive/lua/rive_lua_libs.hpp"
#include "rive/renderer/ore/ore_binding_map.hpp"
#include "rive/renderer/ore/ore_context.hpp"
#include "rive/renderer/ore/ore_rstb_entry_container.hpp"
#include "rive/renderer/ore/ore_render_pass.hpp"
#include "rive/renderer/ore/ore_shader_module.hpp"
#include "rive/renderer/render_canvas.hpp"
#include "rive/renderer/render_context.hpp"
#include "rive/renderer/render_context_impl.hpp"
#include "rive/renderer/rive_renderer.hpp"
#include "rive/assets/shader_asset.hpp"
#include "rive/assets/script_asset.hpp"
#include "rive/file.hpp"
#include "rive/scripted/scripted_object.hpp"
#include "rive/shapes/paint/color.hpp"
#include <algorithm>
#include <cstring>
#include <stdio.h>
#include <string>
#include <unordered_map>
#include <vector>
using namespace rive;
using namespace rive::ore;
// ============================================================================
// String-to-enum helpers
// ============================================================================
static BufferUsage bufferUsageFromString(lua_State* L, const char* s)
{
if (strcmp(s, "vertex") == 0)
return BufferUsage::vertex;
if (strcmp(s, "index") == 0)
return BufferUsage::index;
if (strcmp(s, "uniform") == 0)
return BufferUsage::uniform;
luaL_error(L,
"invalid BufferUsage '%s' (expected 'vertex', 'index', or "
"'uniform')",
s);
return BufferUsage::vertex;
}
// Read a `usage` that is either a single string or an array of strings. The
// type is the dual shape BufferUsage | {BufferUsage} so the array form is
// forward-compatible with future multi-usage flags. Today a buffer has one
// usage, so a multi-element array is rejected rather than silently collapsed.
static BufferUsage lua_tobufferusagefield(lua_State* L, int idx)
{
lua_getfield(L, idx, "usage");
BufferUsage usage = BufferUsage::vertex;
if (lua_isstring(L, -1))
{
usage = bufferUsageFromString(L, lua_tostring(L, -1));
}
else if (lua_istable(L, -1))
{
int n = lua_objlen(L, -1);
if (n != 1)
{
luaL_error(L,
"GPUBuffer.new: usage array must hold exactly one "
"value; multiple usages are not yet supported");
}
lua_rawgeti(L, -1, 1);
if (!lua_isstring(L, -1))
luaL_error(L, "GPUBuffer.new: usage array must hold strings");
usage = bufferUsageFromString(L, lua_tostring(L, -1));
lua_pop(L, 1);
}
else
{
luaL_error(L,
"GPUBuffer.new: 'usage' is required (a string or array of "
"strings)");
}
lua_pop(L, 1);
return usage;
}
static TextureFormat lua_totextureformat(lua_State* L, const char* s)
{
if (strcmp(s, "r8unorm") == 0)
return TextureFormat::r8unorm;
if (strcmp(s, "rg8unorm") == 0)
return TextureFormat::rg8unorm;
if (strcmp(s, "rgba8unorm") == 0)
return TextureFormat::rgba8unorm;
if (strcmp(s, "bgra8unorm") == 0)
return TextureFormat::bgra8unorm;
if (strcmp(s, "rgba16float") == 0)
return TextureFormat::rgba16float;
if (strcmp(s, "rg16float") == 0)
return TextureFormat::rg16float;
if (strcmp(s, "r16float") == 0)
return TextureFormat::r16float;
if (strcmp(s, "rgba32float") == 0)
return TextureFormat::rgba32float;
if (strcmp(s, "rgb10a2unorm") == 0)
return TextureFormat::rgb10a2unorm;
if (strcmp(s, "rg11b10ufloat") == 0)
return TextureFormat::r11g11b10float;
if (strcmp(s, "depth16unorm") == 0)
return TextureFormat::depth16unorm;
if (strcmp(s, "depth24plus-stencil8") == 0)
return TextureFormat::depth24plusStencil8;
if (strcmp(s, "depth32float") == 0)
return TextureFormat::depth32float;
if (strcmp(s, "depth32float-stencil8") == 0)
return TextureFormat::depth32floatStencil8;
if (strcmp(s, "bc1-rgba-unorm") == 0)
return TextureFormat::bc1unorm;
if (strcmp(s, "bc3-rgba-unorm") == 0)
return TextureFormat::bc3unorm;
if (strcmp(s, "bc7-rgba-unorm") == 0)
return TextureFormat::bc7unorm;
if (strcmp(s, "etc2-rgb8unorm") == 0)
return TextureFormat::etc2rgb8;
if (strcmp(s, "etc2-rgba8unorm") == 0)
return TextureFormat::etc2rgba8;
if (strcmp(s, "astc-4x4-unorm") == 0)
return TextureFormat::astc4x4;
if (strcmp(s, "astc-6x6-unorm") == 0)
return TextureFormat::astc6x6;
if (strcmp(s, "astc-8x8-unorm") == 0)
return TextureFormat::astc8x8;
luaL_error(L, "invalid TextureFormat: %s", s);
return TextureFormat::rgba8unorm;
}
static const char* lua_totextureformatstring(TextureFormat fmt)
{
switch (fmt)
{
case TextureFormat::r8unorm:
return "r8unorm";
case TextureFormat::rg8unorm:
return "rg8unorm";
case TextureFormat::rgba8unorm:
return "rgba8unorm";
case TextureFormat::bgra8unorm:
return "bgra8unorm";
case TextureFormat::rgba16float:
return "rgba16float";
case TextureFormat::rg16float:
return "rg16float";
case TextureFormat::r16float:
return "r16float";
case TextureFormat::rgba32float:
return "rgba32float";
case TextureFormat::rg32float:
return "rg32float";
case TextureFormat::r32float:
return "r32float";
case TextureFormat::rgb10a2unorm:
return "rgb10a2unorm";
case TextureFormat::r11g11b10float:
return "rg11b10ufloat";
case TextureFormat::depth16unorm:
return "depth16unorm";
case TextureFormat::depth24plusStencil8:
return "depth24plus-stencil8";
case TextureFormat::depth32float:
return "depth32float";
case TextureFormat::depth32floatStencil8:
return "depth32float-stencil8";
default:
return "rgba8unorm";
}
}
// Float color render-target capability a format needs, or none if it is not a
// float format. 16-bit floats need colorBufferHalfFloat; 32-bit and packed
// floats need the full colorBufferFloat.
enum class FloatColorClass
{
none,
half,
full,
};
static FloatColorClass floatColorClass(TextureFormat fmt)
{
switch (fmt)
{
case TextureFormat::rgba16float:
case TextureFormat::rg16float:
case TextureFormat::r16float:
return FloatColorClass::half;
case TextureFormat::rgba32float:
case TextureFormat::rg32float:
case TextureFormat::r32float:
case TextureFormat::r11g11b10float:
return FloatColorClass::full;
default:
return FloatColorClass::none;
}
}
static TextureType lua_totexturetype(lua_State* L, const char* s)
{
if (strcmp(s, "2d") == 0)
return TextureType::texture2D;
if (strcmp(s, "cube") == 0)
return TextureType::cube;
if (strcmp(s, "3d") == 0)
return TextureType::texture3D;
if (strcmp(s, "2d-array") == 0)
return TextureType::array2D;
luaL_error(L, "invalid TextureType: %s", s);
return TextureType::texture2D;
}
static CompareFunction lua_tocompare(lua_State* L, const char* s)
{
if (strcmp(s, "never") == 0)
return CompareFunction::never;
if (strcmp(s, "less") == 0)
return CompareFunction::less;
if (strcmp(s, "equal") == 0)
return CompareFunction::equal;
if (strcmp(s, "less-equal") == 0)
return CompareFunction::lessEqual;
if (strcmp(s, "greater") == 0)
return CompareFunction::greater;
if (strcmp(s, "not-equal") == 0)
return CompareFunction::notEqual;
if (strcmp(s, "greater-equal") == 0)
return CompareFunction::greaterEqual;
if (strcmp(s, "always") == 0)
return CompareFunction::always;
luaL_error(L, "invalid CompareFunction: %s", s);
return CompareFunction::none;
}
static Filter lua_tofilter(lua_State* L, const char* s)
{
if (strcmp(s, "nearest") == 0)
return Filter::nearest;
if (strcmp(s, "linear") == 0)
return Filter::linear;
luaL_error(L, "invalid Filter: %s", s);
return Filter::nearest;
}
static WrapMode lua_towrapmode(lua_State* L, const char* s)
{
if (strcmp(s, "repeat") == 0)
return WrapMode::repeat;
if (strcmp(s, "mirror-repeat") == 0)
return WrapMode::mirrorRepeat;
if (strcmp(s, "clamp-to-edge") == 0)
return WrapMode::clampToEdge;
luaL_error(L, "invalid WrapMode: %s", s);
return WrapMode::clampToEdge;
}
static VertexFormat lua_tovertexformat(lua_State* L, const char* s)
{
if (strcmp(s, "float32") == 0)
return VertexFormat::float1;
if (strcmp(s, "float32x2") == 0)
return VertexFormat::float2;
if (strcmp(s, "float32x3") == 0)
return VertexFormat::float3;
if (strcmp(s, "float32x4") == 0)
return VertexFormat::float4;
if (strcmp(s, "uint8x4") == 0)
return VertexFormat::uint8x4;
if (strcmp(s, "unorm8x4") == 0)
return VertexFormat::unorm8x4;
if (strcmp(s, "snorm8x4") == 0)
return VertexFormat::snorm8x4;
if (strcmp(s, "float16x2") == 0)
return VertexFormat::float16x2;
if (strcmp(s, "float16x4") == 0)
return VertexFormat::float16x4;
luaL_error(L, "invalid VertexFormat: %s", s);
return VertexFormat::float4;
}
static CullMode lua_tocullmode(lua_State* L, const char* s)
{
if (strcmp(s, "none") == 0)
return CullMode::none;
if (strcmp(s, "front") == 0)
return CullMode::front;
if (strcmp(s, "back") == 0)
return CullMode::back;
luaL_error(L, "invalid CullMode: %s", s);
return CullMode::none;
}
static PrimitiveTopology lua_totopology(lua_State* L, const char* s)
{
if (strcmp(s, "triangle-list") == 0)
return PrimitiveTopology::triangleList;
if (strcmp(s, "triangle-strip") == 0)
return PrimitiveTopology::triangleStrip;
if (strcmp(s, "line-list") == 0)
return PrimitiveTopology::lineList;
if (strcmp(s, "line-strip") == 0)
return PrimitiveTopology::lineStrip;
if (strcmp(s, "point-list") == 0)
return PrimitiveTopology::pointList;
luaL_error(L, "invalid PrimitiveTopology: %s", s);
return PrimitiveTopology::triangleList;
}
static BlendFactor lua_toblendfactor(lua_State* L, const char* s)
{
if (strcmp(s, "zero") == 0)
return BlendFactor::zero;
if (strcmp(s, "one") == 0)
return BlendFactor::one;
if (strcmp(s, "src") == 0)
return BlendFactor::srcColor;
if (strcmp(s, "one-minus-src") == 0)
return BlendFactor::oneMinusSrcColor;
if (strcmp(s, "src-alpha") == 0)
return BlendFactor::srcAlpha;
if (strcmp(s, "one-minus-src-alpha") == 0)
return BlendFactor::oneMinusSrcAlpha;
if (strcmp(s, "dst") == 0)
return BlendFactor::dstColor;
if (strcmp(s, "one-minus-dst") == 0)
return BlendFactor::oneMinusDstColor;
if (strcmp(s, "dst-alpha") == 0)
return BlendFactor::dstAlpha;
if (strcmp(s, "one-minus-dst-alpha") == 0)
return BlendFactor::oneMinusDstAlpha;
if (strcmp(s, "src-alpha-saturated") == 0)
return BlendFactor::srcAlphaSaturated;
if (strcmp(s, "constant") == 0)
return BlendFactor::blendColor;
if (strcmp(s, "one-minus-constant") == 0)
return BlendFactor::oneMinusBlendColor;
luaL_error(L, "invalid BlendFactor: %s", s);
return BlendFactor::one;
}
static BlendOp lua_toblendop(lua_State* L, const char* s)
{
if (strcmp(s, "add") == 0)
return BlendOp::add;
if (strcmp(s, "subtract") == 0)
return BlendOp::subtract;
if (strcmp(s, "reverse-subtract") == 0)
return BlendOp::reverseSubtract;
if (strcmp(s, "min") == 0)
return BlendOp::min;
if (strcmp(s, "max") == 0)
return BlendOp::max;
luaL_error(L, "invalid BlendOp: %s", s);
return BlendOp::add;
}
static FaceWinding lua_towinding(lua_State* L, const char* s)
{
if (strcmp(s, "cw") == 0)
return FaceWinding::clockwise;
if (strcmp(s, "ccw") == 0)
return FaceWinding::counterClockwise;
luaL_error(L, "invalid FaceWinding: %s", s);
return FaceWinding::counterClockwise;
}
static StencilOp lua_tostencilop(lua_State* L, const char* s)
{
if (strcmp(s, "keep") == 0)
return StencilOp::keep;
if (strcmp(s, "zero") == 0)
return StencilOp::zero;
if (strcmp(s, "replace") == 0)
return StencilOp::replace;
if (strcmp(s, "increment-clamp") == 0)
return StencilOp::incrementClamp;
if (strcmp(s, "decrement-clamp") == 0)
return StencilOp::decrementClamp;
if (strcmp(s, "invert") == 0)
return StencilOp::invert;
if (strcmp(s, "increment-wrap") == 0)
return StencilOp::incrementWrap;
if (strcmp(s, "decrement-wrap") == 0)
return StencilOp::decrementWrap;
luaL_error(L, "invalid StencilOp: %s", s);
return StencilOp::keep;
}
// Parse a color writemask string. Each of "r"/"g"/"b"/"a" present in the
// string enables that channel. Recognized special values: "" or "none"
// disables all; "all" / "rgba" enables all (also the default if the
// field is absent). Order doesn't matter.
static ColorWriteMask lua_towritemask(lua_State* L, const char* s)
{
if (s == nullptr || strcmp(s, "all") == 0 || strcmp(s, "rgba") == 0)
return ColorWriteMask::all;
if (s[0] == '\0' || strcmp(s, "none") == 0)
return ColorWriteMask::none;
ColorWriteMask out = ColorWriteMask::none;
for (const char* p = s; *p; ++p)
{
switch (*p)
{
case 'r':
case 'R':
out = out | ColorWriteMask::red;
break;
case 'g':
case 'G':
out = out | ColorWriteMask::green;
break;
case 'b':
case 'B':
out = out | ColorWriteMask::blue;
break;
case 'a':
case 'A':
out = out | ColorWriteMask::alpha;
break;
default:
luaL_error(L,
"invalid ColorWriteMask: '%s' "
"(expected r/g/b/a chars or 'all'/'none')",
s);
}
}
return out;
}
// Helper to get a string field from a table at idx, returns nullptr if
// the field doesn't exist.
static const char* lua_getoptionalstringfield(lua_State* L,
int idx,
const char* field)
{
lua_getfield(L, idx, field);
const char* s = lua_isstring(L, -1) ? lua_tostring(L, -1) : nullptr;
lua_pop(L, 1);
return s;
}
// Parse a stencil-face state table:
// { compare="...", failOp="...", depthFailOp="...", passOp="..." }
// Defined after `lua_getoptionalstringfield` because it uses that helper —
// Android NDK clang's `-Werror` rejects forward use of a `static` function
// that hasn't been declared yet.
static void lua_tostencilface(lua_State* L, int idx, StencilFaceState* face)
{
if (!lua_istable(L, idx))
return;
const char* s = lua_getoptionalstringfield(L, idx, "compare");
if (s)
face->compare = lua_tocompare(L, s);
s = lua_getoptionalstringfield(L, idx, "failOp");
if (s)
face->failOp = lua_tostencilop(L, s);
s = lua_getoptionalstringfield(L, idx, "depthFailOp");
if (s)
face->depthFailOp = lua_tostencilop(L, s);
s = lua_getoptionalstringfield(L, idx, "passOp");
if (s)
face->passOp = lua_tostencilop(L, s);
}
static double lua_getoptionalnumberfield(lua_State* L,
int idx,
const char* field,
double def)
{
lua_getfield(L, idx, field);
double v = lua_isnumber(L, -1) ? lua_tonumber(L, -1) : def;
lua_pop(L, 1);
return v;
}
static bool lua_getoptionalboolfield(lua_State* L,
int idx,
const char* field,
bool def)
{
lua_getfield(L, idx, field);
bool v = lua_isboolean(L, -1) ? lua_toboolean(L, -1) : def;
lua_pop(L, 1);
return v;
}
// ============================================================================
// Debug tracing
// ============================================================================
// ============================================================================
// Shader (ore::ShaderModule)
// ============================================================================
// Retrieves the ore GPU context for the current VM from its ScriptingContext.
static Context* getOreContext(lua_State* L)
{
return static_cast<Context*>(
static_cast<ScriptingContext*>(lua_getthreaddata(L))->oreContext());
}
/// RSTB ShaderTarget the active ore backend consumes.
static ShaderTarget currentShaderTarget(Context* oreCtx)
{
return oreCtx != nullptr ? oreCtx->shaderTarget() : ShaderTarget::glsl;
}
/// Build a ScriptedShader's entry list from a decoded ShaderAsset for the
/// active backend target. Parses the RSTB v4 entry-point container
/// (ore_rstb_entry_container.hpp): whole-module targets (WGSL/MSL/SPIR-V) share
/// one ShaderModule across all entries; per-entry targets (GLSL/HLSL) build one
/// module per entry. Returns true if at least one entry/module was created.
static bool buildShaderEntries(Context* oreCtx,
const ShaderAsset& asset,
ScriptedShader* out)
{
if (oreCtx == nullptr || out == nullptr)
return false;
// Start from a clean slate: a ScriptedShader may be reloaded (editor live
// preview) and stale entry records would corrupt entry-point resolution.
out->entries.clear();
ShaderTarget target = currentShaderTarget(oreCtx);
auto blob = asset.findShader(static_cast<uint8_t>(target));
if (blob.empty())
return false;
const uint8_t* blobData = blob.data();
uint32_t blobSize = static_cast<uint32_t>(blob.size());
const uint32_t assetId = asset.assetId();
// Binding-map sidecar (mandatory for module creation) + GL fixup sidecars
// (GLSL only). Every shipped shader carries a sidecar per source variant.
auto bindingMapTargetFor = [](ShaderTarget t) -> uint8_t {
switch (t)
{
case ShaderTarget::wgsl:
return 16;
case ShaderTarget::glsl:
return 11;
case ShaderTarget::msl:
return 10;
case ShaderTarget::hlsl:
return 12;
case ShaderTarget::spirv:
return 13;
}
return 255;
};
uint8_t bmTarget = bindingMapTargetFor(target);
auto bindingMapBlob =
(bmTarget == 255) ? Span<const uint8_t>{} : asset.findShader(bmTarget);
const uint8_t* bindingMapBytes =
bindingMapBlob.empty() ? nullptr : bindingMapBlob.data();
uint32_t bindingMapSize = static_cast<uint32_t>(bindingMapBlob.size());
auto vsGLFixupBlob = (target == ShaderTarget::glsl) ? asset.findShader(14)
: Span<const uint8_t>{};
auto fsGLFixupBlob = (target == ShaderTarget::glsl) ? asset.findShader(15)
: Span<const uint8_t>{};
// Texture-sampler pairs, applied to every module created below.
std::vector<ShaderModule::TextureSamplerPair> pairVec;
{
auto pairs = asset.textureSamplerPairs();
pairVec.reserve(pairs.size());
for (size_t i = 0; i < pairs.size(); i++)
{
pairVec.push_back({pairs[i].texGroup,
pairs[i].texBinding,
pairs[i].sampGroup,
pairs[i].sampBinding});
}
}
// Per-entry targets: one ShaderModule per entry (GL compiles a `main`,
// HLSL D3DCompiles against the cleansed physical name).
if (target == ShaderTarget::glsl || target == ShaderTarget::hlsl)
{
std::vector<rive::ore::RstbEntryView> views;
if (!rive::ore::parsePerEntryContainer(blobData, blobSize, views))
return false;
for (const auto& v : views)
{
// GL/HLSL containers only carry vertex/fragment entries; ignore any
// other stage rather than misclassifying it as a fragment module.
if (v.stage != 0 && v.stage != 1)
continue;
ShaderModuleDesc desc;
desc.stage =
(v.stage == 0) ? ShaderStage::vertex : ShaderStage::fragment;
desc.bindingMapBytes = bindingMapBytes;
desc.bindingMapSize = bindingMapSize;
desc.shaderAssetId = assetId;
if (target == ShaderTarget::hlsl)
{
desc.hlslSource = reinterpret_cast<const char*>(v.source);
desc.hlslSourceSize = v.sourceSize;
desc.hlslEntryPoint = v.physical.c_str();
}
else // glsl
{
desc.code = v.source;
desc.codeSize = v.sourceSize;
auto fx = (v.stage == 0) ? vsGLFixupBlob : fsGLFixupBlob;
desc.glFixupBytes = fx.empty() ? nullptr : fx.data();
desc.glFixupSize = static_cast<uint32_t>(fx.size());
}
auto mod = oreCtx->makeShaderModule(desc);
if (!mod)
return false;
if (!pairVec.empty())
mod->m_textureSamplerPairs = pairVec;
ScriptedShaderEntry e;
e.stage = v.stage;
e.logical = v.logical;
e.physical = v.physical;
e.module = std::move(mod);
out->entries.push_back(std::move(e));
}
return !out->entries.empty();
}
// Whole-module targets: one shared module, one entry record per entry, all
// referencing it. The driver selects the entry by its physical name.
std::vector<rive::ore::RstbEntryView> views;
const uint8_t* src = nullptr;
uint32_t srcLen = 0;
if (!rive::ore::parseWholeModuleContainer(blobData,
blobSize,
views,
&src,
&srcLen))
return false;
ShaderModuleDesc desc;
desc.code = src;
desc.codeSize = srcLen;
desc.bindingMapBytes = bindingMapBytes;
desc.bindingMapSize = bindingMapSize;
desc.shaderAssetId = assetId;
auto mod = oreCtx->makeShaderModule(desc);
if (!mod)
return false;
if (!pairVec.empty())
mod->m_textureSamplerPairs = pairVec;
for (const auto& v : views)
{
ScriptedShaderEntry e;
e.stage = v.stage;
e.logical = v.logical;
e.physical = v.physical;
e.module = mod;
out->entries.push_back(std::move(e));
}
return !out->entries.empty();
}
#ifdef WITH_RIVE_TOOLS
/// Editor live-preview: decode raw RSTB bytes then build entries. The workspace
/// hands us unsigned bytes, so prepend a zero SignedContentHeader envelope byte
/// (`[flags:1][inner]`) to produce ShaderAsset::decode's expected input.
static bool makeShaderFromRstb(Context* oreCtx,
const uint8_t* data,
uint32_t len,
ScriptedShader* out)
{
if (oreCtx == nullptr || data == nullptr || len == 0 || out == nullptr)
return false;
ShaderAsset asset;
SimpleArray<uint8_t> bytes(static_cast<size_t>(len) + 1);
bytes[0] = 0x00; // flags: unsigned, version 0
memcpy(bytes.data() + 1, data, len);
if (!asset.decode(bytes, nullptr))
return false;
return buildShaderEntries(oreCtx, asset, out);
}
#endif // WITH_RIVE_TOOLS
/// Look up a shader by name: first check the per-VM RSTB blobs on the
/// ScriptingContext (editor path, compiled during requestVM), then try the
/// ShaderAsset from file->assets() (runtime .riv path).
/// Populates both vertex and fragment modules on the ScriptedShader.
/// Returns true on success.
bool lua_gpu_load_shader_by_name(ScriptedShader* out,
ScriptingContext* context,
const char* name,
ShaderAsset* fileAsset)
{
Context* oreCtx = static_cast<Context*>(
context != nullptr ? context->oreContext() : nullptr);
#ifdef WITH_RIVE_TOOLS
// Editor path: RSTB blobs compiled from WGSL during requestVM, stored
// per-VM on the ScriptingContext.
if (context != nullptr)
{
const auto* rstb = context->findShaderRstb(name);
if (rstb != nullptr)
{
return makeShaderFromRstb(oreCtx,
rstb->data(),
static_cast<uint32_t>(rstb->size()),
out);
}
}
#endif
// Runtime path: ShaderAsset from the .riv file's asset list. Same
// container parsing as the editor path (buildShaderEntries) so entry-point
// resolution is identical on all backends.
if (fileAsset != nullptr && oreCtx != nullptr)
{
return buildShaderEntries(oreCtx, *fileAsset, out);
}
return false;
}
int lua_gpu_push_shader_by_name(lua_State* L, const char* name)
{
auto* context = static_cast<ScriptingContext*>(lua_getthreaddata(L));
// 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)
{
if (auto* scriptedObject = context->currentScriptedObject())
{
if (auto* scriptAsset = scriptedObject->scriptAsset())
{
if (File* file = scriptAsset->file())
{
for (const auto& asset : file->assets())
{
if (asset->is<ShaderAsset>())
{
auto* sa = asset->as<ShaderAsset>();
// match folderPath/name or bare name
const std::string& fp = sa->folderPath();
if (sa->name() == name ||
(!fp.empty() && fp + "/" + sa->name() == name))
{
fileAsset = sa;
break;
}
}
}
}
}
}
}
auto* scripted = lua_newrive<ScriptedShader>(L);
if (!lua_gpu_load_shader_by_name(scripted, context, name, fileAsset))
{
lua_pop(L, 1);
return 0;
}
return 1;
}
// ============================================================================
// GPUBuffer
// ============================================================================
// `GPUBuffer.new({ size, usage, data?, immutable?, label? })`:
// - `size` : size in bytes.
// - `usage` : 'vertex' | 'index' | 'uniform', or a one-element array of
// the same (the array form is reserved for future
// multi-usage flags).
// - `data` : Lua buffer with initial contents. Required when
// `immutable=true`. Length must match `size` exactly so
// backends can populate the GPU resource at create time.
// - `immutable` : when true, the buffer is GPU-only (no `:write` after
// creation). Required for `BufferDesc::immutable`, which
// backends with a USAGE_IMMUTABLE concept (D3D11) honor
// for static vertex/index buffers.
// - `label` : optional debug name.
static int gpubuffer_construct(lua_State* L)
{
if (getOreContext(L) == nullptr)
{
luaL_error(L, "GPU context not initialized");
}
luaL_checktype(L, 1, LUA_TTABLE);
BufferDesc desc;
// Required positive size that fits in uint32. The raw number path would
// otherwise truncate floats or wrap negatives.
lua_getfield(L, 1, "size");
if (!lua_isnumber(L, -1))
luaL_error(L, "GPUBuffer.new: 'size' is required (bytes)");
double sizeNum = lua_tonumber(L, -1);
lua_pop(L, 1);
// Prove the value is finite and in range before any integer cast: casting
// NaN or out-of-range doubles to uint32 is undefined. The positive range
// test is false for NaN, so the && short-circuits before the cast.
bool validInt =
sizeNum >= 1.0 && sizeNum <= 4294967295.0 &&
sizeNum == static_cast<double>(static_cast<uint32_t>(sizeNum));
if (!validInt)
{
luaL_error(L,
"GPUBuffer.new: 'size' must be a positive integer number "
"of bytes");
}
desc.size = static_cast<uint32_t>(sizeNum);
desc.usage = lua_tobufferusagefield(L, 1);
desc.immutable = lua_getoptionalboolfield(L, 1, "immutable", false);
desc.label = lua_getoptionalstringfield(L, 1, "label");
lua_getfield(L, 1, "data");
if (!lua_isnil(L, -1))
{
size_t dataLen = 0;
const void* dataPtr = lua_tobuffer(L, -1, &dataLen);
if (dataPtr == nullptr)
{
luaL_error(L, "GPUBuffer.new: 'data' must be a Luau buffer");
}
if (dataLen != desc.size)
{
luaL_error(L,
"GPUBuffer.new: data length (%zu) must equal size (%u)",
dataLen,
desc.size);
}
desc.data = dataPtr;
}
lua_pop(L, 1);
if (desc.immutable && desc.data == nullptr)
{
luaL_error(L,
"GPUBuffer.new: immutable=true requires 'data' (the GPU "
"buffer is GPU-only after creation)");
}
auto* ctx = getOreContext(L);
ctx->clearLastError();
auto buffer = ctx->makeBuffer(desc);
if (!buffer)
{
if (!ctx->lastError().empty())
luaL_error(L, "GPUBuffer.new: %s", ctx->lastError().c_str());
luaL_error(L, "GPUBuffer.new: failed to create buffer");
}
auto* scripted = lua_newrive<ScriptedGPUBuffer>(L);
scripted->buffer = std::move(buffer);
scripted->immutable = desc.immutable;
return 1;
}
static int gpubuffer_write(lua_State* L)
{
auto* self = lua_torive<ScriptedGPUBuffer>(L, 1);
if (self->immutable)
{
luaL_error(L,
"GPUBuffer:write: buffer was created with "
"immutable=true; its contents are fixed at construction");
}
// Luau buffer type
size_t len;
const void* data = lua_tobuffer(L, 2, &len);
if (data == nullptr)
{
luaL_typeerror(L, 2, "buffer");
}
uint32_t offset =
lua_isnumber(L, 3) ? static_cast<uint32_t>(lua_tonumber(L, 3)) : 0;
if (offset + len > self->buffer->size())
{
luaL_error(L,
"GPUBuffer:write: offset(%u) + size(%u) = %u exceeds "
"buffer size(%u)",
offset,
(uint32_t)len,
(uint32_t)(offset + len),
self->buffer->size());
}
self->buffer->update(data, static_cast<uint32_t>(len), offset);
return 0;
}
static int gpubuffer_namecall(lua_State* L)
{
int atom;
const char* str = lua_namecallatom(L, &atom);
if (str != nullptr)
{
switch (atom)
{
case (int)LuaAtoms::write:
return gpubuffer_write(L);
}
}
luaL_error(L,
"%s is not a valid method of %s",
str,
ScriptedGPUBuffer::luaName);
return 0;
}
static void gpubuffer_direct_size(void* udata, void* result)
{
lua_userdatadirectfield_setnumber(
result,
(double)((ScriptedGPUBuffer*)udata)->buffer->size());
}
static int gpubuffer_index(lua_State* L)
{
int atom;
const char* key = lua_tostringatom(L, 2, &atom);
if (!key)
{
luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
}
auto* self = lua_torive<ScriptedGPUBuffer>(L, 1);
switch (atom)
{
case (int)LuaAtoms::size:
lua_pushnumber(L, self->buffer->size());
return 1;
}
luaL_error(L, "'%s' is not a valid index of GPUBuffer", key);
return 0;
}
// ============================================================================
// GPUTexture
// ============================================================================
// Validate a user-supplied sampleCount against the device's actual limit.
// Errors with a clear message rather than letting Metal/Vulkan assert-fail.
static void lua_checksamplecount(lua_State* L, uint32_t sampleCount)
{
if (sampleCount <= 1)
return;
// Must be a power of two.
if ((sampleCount & (sampleCount - 1)) != 0)
luaL_error(L,
"sampleCount must be a power of two (got %u)",
sampleCount);
auto* ctx = getOreContext(L);
if (ctx)
{
uint32_t maxSamples = ctx->features().maxSamples;
if (sampleCount > maxSamples)
luaL_error(L,
"sampleCount %u exceeds device maximum of %u — "
"query context:features().maxSamples before creating "
"MSAA textures",
sampleCount,
maxSamples);
}
}
static int gputexture_construct(lua_State* L)
{
if (getOreContext(L) == nullptr)
{
luaL_error(L, "GPU context not initialized");
}
luaL_checktype(L, 1, LUA_TTABLE);
int descIdx = 1;
TextureDesc desc;
desc.width = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "width", 0));
desc.height = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "height", 0));
if (desc.width == 0 || desc.height == 0)
{
luaL_error(L, "GPUTexture requires width and height");
}
const char* fmt = lua_getoptionalstringfield(L, descIdx, "format");
if (fmt)
desc.format = lua_totextureformat(L, fmt);
const char* typ = lua_getoptionalstringfield(L, descIdx, "type");
if (typ)
desc.type = lua_totexturetype(L, typ);
desc.renderTarget =
lua_getoptionalboolfield(L, descIdx, "renderTarget", false);
desc.sampleCount = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "sampleCount", 1));
lua_checksamplecount(L, desc.sampleCount);
desc.numMipmaps = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "mipmaps", 1));
desc.depthOrArrayLayers = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "layers", 1));
auto* ctx = getOreContext(L);
// Gate float render targets: without the matching capability they make an
// incomplete FBO that renders black. Sampled-only float textures are fine.
// 16-bit floats need half-float, 32-bit and packed need full float.
if (desc.renderTarget)
{
FloatColorClass fc = floatColorClass(desc.format);
const Features& feat = ctx->features();
bool ok = fc == FloatColorClass::none ||
(fc == FloatColorClass::half && feat.colorBufferHalfFloat) ||
(fc == FloatColorClass::full && feat.colorBufferFloat);
if (!ok)
{
const char* cap = fc == FloatColorClass::half
? "colorBufferHalfFloat"
: "colorBufferFloat";
luaL_error(L,
"GPUTexture.new: float format %s as a renderTarget "
"requires the %s feature, which the active backend does "
"not support",
lua_totextureformatstring(desc.format),
cap);
}
}
ctx->clearLastError();
auto texture = ctx->makeTexture(desc);
if (!texture)
{
if (!ctx->lastError().empty())
luaL_error(L, "GPUTexture.new: %s", ctx->lastError().c_str());
luaL_error(L, "GPUTexture.new: failed to create texture");
}
auto* scripted = lua_newrive<ScriptedGPUTexture>(L);
scripted->texture = std::move(texture);
return 1;
}
static int gputexture_view(lua_State* L)
{
auto* self = lua_torive<ScriptedGPUTexture>(L, 1);
TextureViewDesc viewDesc;
viewDesc.texture = self->texture.get();
viewDesc.mipCount = self->texture->numMipmaps();
viewDesc.layerCount = self->texture->depthOrArrayLayers();
// Map TextureType -> TextureViewDimension
switch (self->texture->type())
{
case TextureType::texture2D:
viewDesc.dimension = TextureViewDimension::texture2D;
break;
case TextureType::cube:
viewDesc.dimension = TextureViewDimension::cube;
break;
case TextureType::texture3D:
viewDesc.dimension = TextureViewDimension::texture3D;
break;
case TextureType::array2D:
viewDesc.dimension = TextureViewDimension::array2D;
break;
}
if (lua_istable(L, 2))
{
const char* dim = lua_getoptionalstringfield(L, 2, "dimension");
if (dim)
{
if (strcmp(dim, "2d") == 0)
viewDesc.dimension = TextureViewDimension::texture2D;
else if (strcmp(dim, "cube") == 0)
viewDesc.dimension = TextureViewDimension::cube;
else if (strcmp(dim, "3d") == 0)
viewDesc.dimension = TextureViewDimension::texture3D;
else if (strcmp(dim, "2d-array") == 0)
viewDesc.dimension = TextureViewDimension::array2D;
}
viewDesc.baseMipLevel = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, 2, "baseMipLevel", 0));
viewDesc.mipCount = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, 2, "mipCount", viewDesc.mipCount));
viewDesc.baseLayer = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, 2, "baseLayer", 0));
viewDesc.layerCount = static_cast<uint32_t>(
lua_getoptionalnumberfield(L,
2,
"layerCount",
viewDesc.layerCount));
}
auto* ctx = getOreContext(L);
ctx->clearLastError();
auto tv = ctx->makeTextureView(viewDesc);
if (!tv)
{
if (!ctx->lastError().empty())
luaL_error(L, "GPUTexture:view: %s", ctx->lastError().c_str());
luaL_error(L, "GPUTexture:view: failed to create texture view");
}
auto* scripted = lua_newrive<ScriptedGPUTextureView>(L);
scripted->view = std::move(tv);
return 1;
}
static int gputexture_upload(lua_State* L)
{
auto* self = lua_torive<ScriptedGPUTexture>(L, 1);
luaL_checktype(L, 2, LUA_TTABLE);
int descIdx = 2;
lua_getfield(L, descIdx, "data");
size_t len;
const void* data = lua_tobuffer(L, -1, &len);
if (!data)
{
luaL_error(L, "upload requires 'data' field of type buffer");
}
lua_pop(L, 1);
TextureDataDesc uploadDesc;
uploadDesc.data = data;
uploadDesc.width = static_cast<uint32_t>(
lua_getoptionalnumberfield(L,
descIdx,
"width",
self->texture->width()));
uploadDesc.height = static_cast<uint32_t>(
lua_getoptionalnumberfield(L,
descIdx,
"height",
self->texture->height()));
uploadDesc.depth = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "depth", 1));
uploadDesc.x =
static_cast<uint32_t>(lua_getoptionalnumberfield(L, descIdx, "x", 0));
uploadDesc.y =
static_cast<uint32_t>(lua_getoptionalnumberfield(L, descIdx, "y", 0));
uploadDesc.z =
static_cast<uint32_t>(lua_getoptionalnumberfield(L, descIdx, "z", 0));
uploadDesc.mipLevel = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "mipLevel", 0));
uploadDesc.layer = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "layer", 0));
uploadDesc.bytesPerRow = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "bytesPerRow", 0));
uploadDesc.rowsPerImage = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "rowsPerImage", 0));
// Validate region/level/layer against the texture's actual dimensions —
// out-of-range values trip backend asserts (Metal API Validation, D3D12
// GPU hangs, Vulkan validation). Per the
// `feedback_lua_gpu_misuse_validation` rule, surface as luaL_error.
if (uploadDesc.mipLevel >= self->texture->numMipmaps())
{
luaL_error(L,
"upload: mipLevel %u out of range [0, %u)",
uploadDesc.mipLevel,
self->texture->numMipmaps());
}
if (uploadDesc.layer >= self->texture->depthOrArrayLayers())
{
luaL_error(L,
"upload: layer %u out of range [0, %u)",
uploadDesc.layer,
self->texture->depthOrArrayLayers());
}
// Mip-level dimensions: floor-div-by-2 per level, min 1.
uint32_t mipW = std::max(1u, self->texture->width() >> uploadDesc.mipLevel);
uint32_t mipH =
std::max(1u, self->texture->height() >> uploadDesc.mipLevel);
if (uploadDesc.x > mipW || uploadDesc.width > mipW - uploadDesc.x)
{
luaL_error(L,
"upload: x+width (%u+%u) exceeds mip %u width %u",
uploadDesc.x,
uploadDesc.width,
uploadDesc.mipLevel,
mipW);
}
if (uploadDesc.y > mipH || uploadDesc.height > mipH - uploadDesc.y)
{
luaL_error(L,
"upload: y+height (%u+%u) exceeds mip %u height %u",
uploadDesc.y,
uploadDesc.height,
uploadDesc.mipLevel,
mipH);
}
// Compute a tightly-packed bytesPerRow when the caller omits it.
if (uploadDesc.bytesPerRow == 0)
{
uint32_t bpt = textureFormatBytesPerTexel(self->texture->format());
if (bpt == 0)
{
luaL_error(L,
"upload: bytesPerRow must be provided for "
"block-compressed formats");
}
uploadDesc.bytesPerRow = uploadDesc.width * bpt;
}
// Default rowsPerImage to height so Metal's bytesPerImage is correct.
if (uploadDesc.rowsPerImage == 0)
{
uploadDesc.rowsPerImage = uploadDesc.height;
}
// Validate the data buffer is large enough to cover the region.
// GPUs read past the supplied bytes if we don't catch it here; on
// Metal the validation layer aborts, on others it's a silent OOB.
const uint64_t requiredBytes =
static_cast<uint64_t>(uploadDesc.bytesPerRow) *
uploadDesc.rowsPerImage * std::max(1u, uploadDesc.depth);
if (len < requiredBytes)
{
luaL_error(L,
"upload: data buffer is %zu bytes but region requires "
"%llu (bytesPerRow=%u * rowsPerImage=%u * depth=%u)",
len,
static_cast<unsigned long long>(requiredBytes),
uploadDesc.bytesPerRow,
uploadDesc.rowsPerImage,
std::max(1u, uploadDesc.depth));
}
self->texture->upload(uploadDesc);
return 0;
}
static int gputexture_namecall(lua_State* L)
{
int atom;
const char* str = lua_namecallatom(L, &atom);
if (str != nullptr)
{
switch (atom)
{
case (int)LuaAtoms::view:
return gputexture_view(L);
case (int)LuaAtoms::upload:
return gputexture_upload(L);
}
}
luaL_error(L,
"%s is not a valid method of %s",
str,
ScriptedGPUTexture::luaName);
return 0;
}
static void gputexture_direct_width(void* udata, void* result)
{
lua_userdatadirectfield_setnumber(
result,
(double)((ScriptedGPUTexture*)udata)->texture->width());
}
static void gputexture_direct_height(void* udata, void* result)
{
lua_userdatadirectfield_setnumber(
result,
(double)((ScriptedGPUTexture*)udata)->texture->height());
}
static int gputexture_index(lua_State* L)
{
int atom;
const char* key = lua_tostringatom(L, 2, &atom);
if (!key)
{
luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
}
auto* self = lua_torive<ScriptedGPUTexture>(L, 1);
switch (atom)
{
case (int)LuaAtoms::width:
lua_pushnumber(L, self->texture->width());
return 1;
case (int)LuaAtoms::height:
lua_pushnumber(L, self->texture->height());
return 1;
case (int)LuaAtoms::format:
lua_pushstring(L,
lua_totextureformatstring(self->texture->format()));
return 1;
}
luaL_error(L, "'%s' is not a valid index of GPUTexture", key);
return 0;
}
static int gputextureview_index(lua_State* L)
{
int atom;
const char* key = lua_tostringatom(L, 2, &atom);
if (!key)
{
luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
}
auto* self = lua_torive<ScriptedGPUTextureView>(L, 1);
switch (atom)
{
case (int)LuaAtoms::format:
if (self->view && self->view->texture())
lua_pushstring(
L,
lua_totextureformatstring(self->view->texture()->format()));
else
lua_pushnil(L);
return 1;
}
luaL_error(L, "'%s' is not a valid index of GPUTextureView", key);
return 0;
}
// ============================================================================
// GPUSampler
// ============================================================================
static int gpusampler_construct(lua_State* L)
{
if (getOreContext(L) == nullptr)
{
luaL_error(L, "GPU context not initialized");
}
SamplerDesc desc;
if (lua_istable(L, 1))
{
int descIdx = 1;
const char* s;
s = lua_getoptionalstringfield(L, descIdx, "min");
if (s)
desc.minFilter = lua_tofilter(L, s);
s = lua_getoptionalstringfield(L, descIdx, "mag");
if (s)
desc.magFilter = lua_tofilter(L, s);
s = lua_getoptionalstringfield(L, descIdx, "mipmap");
if (s)
desc.mipmapFilter = lua_tofilter(L, s);
s = lua_getoptionalstringfield(L, descIdx, "wrapU");
if (s)
desc.wrapU = lua_towrapmode(L, s);
s = lua_getoptionalstringfield(L, descIdx, "wrapV");
if (s)
desc.wrapV = lua_towrapmode(L, s);
s = lua_getoptionalstringfield(L, descIdx, "wrapW");
if (s)
desc.wrapW = lua_towrapmode(L, s);
s = lua_getoptionalstringfield(L, descIdx, "compare");
if (s)
desc.compare = lua_tocompare(L, s);
desc.minLod = static_cast<float>(
lua_getoptionalnumberfield(L, descIdx, "minLod", 0.0));
desc.maxLod = static_cast<float>(
lua_getoptionalnumberfield(L, descIdx, "maxLod", 32.0));
if (desc.minLod > desc.maxLod)
{
luaL_error(L,
"GPUSampler.new: minLod (%g) > maxLod (%g)",
desc.minLod,
desc.maxLod);
}
desc.maxAnisotropy = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "maxAnisotropy", 1));
// WebGPU + every backend caps anisotropy at 16x and accepts only
// power-of-two values. Reject out-of-range early so a bad sampler
// doesn't cause a backend-specific create failure later.
const uint32_t a = desc.maxAnisotropy;
if (a < 1 || a > 16 || (a & (a - 1)) != 0)
{
luaL_error(L,
"GPUSampler.new: maxAnisotropy must be a power of "
"two in [1, 16] (got %u)",
a);
}
if (a > 1 && !getOreContext(L)->features().anisotropicFiltering)
{
luaL_error(L,
"GPUSampler.new: maxAnisotropy=%u requires "
"anisotropicFiltering feature, which the active "
"backend does not support",
a);
}
}
auto* ctx = getOreContext(L);
ctx->clearLastError();
auto sampler = ctx->makeSampler(desc);
if (!sampler)
{
if (!ctx->lastError().empty())
luaL_error(L, "GPUSampler.new: %s", ctx->lastError().c_str());
luaL_error(L, "GPUSampler.new: failed to create sampler");
}
auto* scripted = lua_newrive<ScriptedGPUSampler>(L);
scripted->sampler = std::move(sampler);
return 1;
}
// ============================================================================
// GPUBindGroup
// ============================================================================
ScriptedGPUBindGroup::~ScriptedGPUBindGroup() {}
// ============================================================================
// GPUBindGroupLayout.new — explicit BindGroupLayout (Phase E).
// ============================================================================
// Map binding-map types to layout-entry types. These mirror the GM helper
// `makeLayoutFromShader` so Lua-built layouts validate the same way as
// C++-built ones.
static BindingKind bindingKindFromResource(ResourceKind k)
{
switch (k)
{
case ResourceKind::UniformBuffer:
return BindingKind::uniformBuffer;
case ResourceKind::StorageBufferRO:
return BindingKind::storageBufferRO;
case ResourceKind::StorageBufferRW:
return BindingKind::storageBufferRW;
case ResourceKind::SampledTexture:
return BindingKind::sampledTexture;
case ResourceKind::StorageTexture:
return BindingKind::storageTexture;
case ResourceKind::Sampler:
return BindingKind::sampler;
case ResourceKind::ComparisonSampler:
return BindingKind::comparisonSampler;
}
return BindingKind::uniformBuffer;
}
static TextureViewDimension viewDimFromBindingMap(TextureViewDim d)
{
switch (d)
{
case TextureViewDim::Cube:
return TextureViewDimension::cube;
case TextureViewDim::CubeArray:
return TextureViewDimension::cubeArray;
case TextureViewDim::D3:
return TextureViewDimension::texture3D;
case TextureViewDim::D2Array:
return TextureViewDimension::array2D;
case TextureViewDim::D1:
case TextureViewDim::D2:
case TextureViewDim::Undefined:
return TextureViewDimension::texture2D;
}
return TextureViewDimension::texture2D;
}
static BindGroupLayoutEntry::SampleType sampleTypeFromBindingMap(
TextureSampleType s)
{
switch (s)
{
case TextureSampleType::UnfilterableFloat:
return BindGroupLayoutEntry::SampleType::floatUnfilterable;
case TextureSampleType::Depth:
return BindGroupLayoutEntry::SampleType::depth;
case TextureSampleType::Sint:
return BindGroupLayoutEntry::SampleType::sint;
case TextureSampleType::Uint:
return BindGroupLayoutEntry::SampleType::uint;
case TextureSampleType::Float:
case TextureSampleType::Undefined:
return BindGroupLayoutEntry::SampleType::floatFilterable;
}
return BindGroupLayoutEntry::SampleType::floatFilterable;
}
// Walk a shader's BindingMap for the given group, populating layout entries
// with kind / visibility / texture metadata / native slots — the same path the
// GM helper takes. Returns the entry count actually filled.
static uint32_t populateEntriesFromShader(BindGroupLayoutEntry* entries,
uint32_t maxEntries,
const ore::ShaderModule* shader,
uint32_t group,
const uint32_t* dynamicUBOBindings,
uint32_t dynamicUBOCount)
{
if (shader == nullptr)
return 0;
auto isDynamic = [&](uint32_t binding) -> bool {
for (uint32_t i = 0; i < dynamicUBOCount; ++i)
if (dynamicUBOBindings[i] == binding)
return true;
return false;
};
const BindingMap& bm = shader->m_bindingMap;
uint32_t n = 0;
for (size_t i = 0; i < bm.size() && n < maxEntries; ++i)
{
const BindingMap::Entry& e = bm.at(i);
if (e.group != group)
continue;
BindGroupLayoutEntry& out = entries[n++];
out.binding = e.binding;
out.kind = bindingKindFromResource(e.kind);
uint8_t vis = 0;
if (e.stageMask & BindingMap::kStageVertex)
vis |= StageVisibility::kVertex;
if (e.stageMask & BindingMap::kStageFragment)
vis |= StageVisibility::kFragment;
if (e.stageMask & BindingMap::kStageCompute)
vis |= StageVisibility::kCompute;
out.visibility.mask = vis;
out.hasDynamicOffset =
(out.kind == BindingKind::uniformBuffer && isDynamic(e.binding));
out.textureViewDim = viewDimFromBindingMap(e.textureViewDim);
out.textureSampleType = sampleTypeFromBindingMap(e.textureSampleType);
out.textureMultisampled = e.textureMultisampled;
const uint16_t vs =
e.backendSlot[static_cast<size_t>(BindingMap::Stage::VS)];
const uint16_t fs =
e.backendSlot[static_cast<size_t>(BindingMap::Stage::FS)];
out.nativeSlotVS = (vs == BindingMap::kAbsent)
? BindGroupLayoutEntry::kNativeSlotAbsent
: static_cast<uint32_t>(vs);
out.nativeSlotFS = (fs == BindingMap::kAbsent)
? BindGroupLayoutEntry::kNativeSlotAbsent
: static_cast<uint32_t>(fs);
}
return n;
}
static int gpubindgrouplayout_construct(lua_State* L)
{
Context* oreCtx = getOreContext(L);
if (oreCtx == nullptr)
luaL_error(L, "GPU context not initialized");
luaL_checktype(L, 1, LUA_TTABLE);
int descIdx = 1;
BindGroupLayoutDesc desc;
desc.groupIndex = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "groupIndex", 0));
if (desc.groupIndex >= ore::kMaxBindGroups)
luaL_error(L,
"GPUBindGroupLayout.new: groupIndex must be in [0, %u)",
ore::kMaxBindGroups);
// Resolve the source shader. Layouts must always derive from a shader
// module — Lua callers cannot meaningfully fill in `nativeSlotVS/FS`
// without the binding map, and getting kind/visibility wrong would
// fail validation anyway.
lua_getfield(L, descIdx, "shader");
auto* scripted = lua_torive<ScriptedShader>(L, -1);
lua_pop(L, 1);
if (scripted == nullptr || !scripted->hasModule())
luaL_error(L,
"GPUBindGroupLayout.new: 'shader' must be a Shader with "
"a loaded module");
static constexpr int kMaxEntries = 16;
BindGroupLayoutEntry entries[kMaxEntries]{};
// Optional `dynamicUBOs`: array of WGSL @binding values whose UBO
// entries should set hasDynamicOffset.
uint32_t dynUBOs[kMaxEntries] = {};
uint32_t dynUBOCount = 0;
lua_getfield(L, descIdx, "dynamicUBOs");
if (lua_istable(L, -1))
{
int dynTbl = lua_gettop(L);
int dynN = (int)lua_objlen(L, dynTbl);
for (int i = 0; i < dynN && dynUBOCount < kMaxEntries; ++i)
{
lua_rawgeti(L, dynTbl, i + 1);
if (lua_isnumber(L, -1))
dynUBOs[dynUBOCount++] =
static_cast<uint32_t>(lua_tonumber(L, -1));
lua_pop(L, 1);
}
}
lua_pop(L, 1); // dynamicUBOs
// Vertex module carries the merged binding map for both stages —
// the same module the GM helper walks.
uint32_t entryCount = populateEntriesFromShader(entries,
kMaxEntries,
scripted->vertexMod(),
desc.groupIndex,
dynUBOs,
dynUBOCount);
desc.entries = entries;
desc.entryCount = entryCount;
rcp<BindGroupLayout> layout = oreCtx->makeBindGroupLayout(desc);
if (!layout)
{
const std::string& err = oreCtx->lastError();
oreCtx->clearLastError();
luaL_error(L,
"GPUBindGroupLayout.new: %s",
err.empty() ? "failed" : err.c_str());
}
auto* w = lua_newrive<ScriptedGPUBindGroupLayout>(L);
w->layout = std::move(layout);
return 1;
}
static int gpubindgroup_construct(lua_State* L)
{
Context* oreCtx = getOreContext(L);
if (oreCtx == nullptr)
{
luaL_error(L, "GPU context not initialized");
}
luaL_checktype(L, 1, LUA_TTABLE);
int descIdx = 1;
BindGroupDesc desc;
// layout (required) — replaces the legacy `pipeline` field. The
// BindGroup is built against a BindGroupLayout (Phase E); the
// layout's groupIndex determines which @group(N) the BindGroup
// binds to.
lua_getfield(L, descIdx, "layout");
auto* layoutScripted = lua_torive<ScriptedGPUBindGroupLayout>(L, -1);
if (layoutScripted == nullptr)
{
luaL_error(L,
"GPUBindGroup.new: 'layout' field is required and must be "
"a GPUBindGroupLayout");
}
desc.layout = layoutScripted->layout.get();
lua_pop(L, 1);
// Parse UBO entries
static constexpr int MAX_ENTRIES = 8;
BindGroupDesc::UBOEntry uboEntries[MAX_ENTRIES];
uint32_t uboCount = 0;
lua_getfield(L, descIdx, "ubos");
if (lua_istable(L, -1))
{
int tbl = lua_gettop(L);
int n = (int)lua_objlen(L, tbl);
for (int i = 0; i < n && i < MAX_ENTRIES; i++)
{
lua_rawgeti(L, tbl, i + 1);
int entry = lua_gettop(L);
uboEntries[uboCount].slot = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, entry, "slot", 0));
if (uboEntries[uboCount].slot > 7)
{
luaL_error(L,
"GPUBindGroup.new: UBO slot must be 0-7 (got %u)",
uboEntries[uboCount].slot);
}
lua_getfield(L, entry, "buffer");
auto* bufScripted = lua_torive<ScriptedGPUBuffer>(L, -1);
uboEntries[uboCount].buffer = bufScripted->buffer.get();
lua_pop(L, 1);
uboEntries[uboCount].offset = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, entry, "offset", 0));
uboEntries[uboCount].size = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, entry, "size", 0));
lua_pop(L, 1); // entry table
uboCount++;
}
}
lua_pop(L, 1);
desc.ubos = uboEntries;
desc.uboCount = uboCount;
// Parse texture entries
BindGroupDesc::TexEntry texEntries[MAX_ENTRIES];
uint32_t texCount = 0;
lua_getfield(L, descIdx, "textures");
if (lua_istable(L, -1))
{
int tbl = lua_gettop(L);
int n = (int)lua_objlen(L, tbl);
for (int i = 0; i < n && i < MAX_ENTRIES; i++)
{
lua_rawgeti(L, tbl, i + 1);
int entry = lua_gettop(L);
texEntries[texCount].slot = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, entry, "slot", 0));
if (texEntries[texCount].slot > 7)
{
luaL_error(
L,
"GPUBindGroup.new: texture slot must be 0-7 (got %u)",
texEntries[texCount].slot);
}
lua_getfield(L, entry, "view");
auto* tv = lua_torive<ScriptedGPUTextureView>(L, -1);
texEntries[texCount].view = tv->view.get();
lua_pop(L, 1);
lua_pop(L, 1); // entry table
texCount++;
}
}
lua_pop(L, 1);
desc.textures = texEntries;
desc.textureCount = texCount;
// Parse sampler entries
BindGroupDesc::SampEntry sampEntries[MAX_ENTRIES];
uint32_t sampCount = 0;
lua_getfield(L, descIdx, "samplers");
if (lua_istable(L, -1))
{
int tbl = lua_gettop(L);
int n = (int)lua_objlen(L, tbl);
for (int i = 0; i < n && i < MAX_ENTRIES; i++)
{
lua_rawgeti(L, tbl, i + 1);
int entry = lua_gettop(L);
sampEntries[sampCount].slot = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, entry, "slot", 0));
if (sampEntries[sampCount].slot > 7)
{
luaL_error(
L,
"GPUBindGroup.new: sampler slot must be 0-7 (got %u)",
sampEntries[sampCount].slot);
}
lua_getfield(L, entry, "sampler");
auto* ss = lua_torive<ScriptedGPUSampler>(L, -1);
sampEntries[sampCount].sampler = ss->sampler.get();
lua_pop(L, 1);
lua_pop(L, 1); // entry table
sampCount++;
}
}
lua_pop(L, 1);
desc.samplers = sampEntries;
desc.samplerCount = sampCount;
oreCtx->clearLastError();
auto bindGroup = oreCtx->makeBindGroup(desc);
if (!bindGroup)
{
if (!oreCtx->lastError().empty())
luaL_error(L, "GPUBindGroup.new: %s", oreCtx->lastError().c_str());
luaL_error(L, "GPUBindGroup.new: failed to create bind group");
}
auto* scripted = lua_newrive<ScriptedGPUBindGroup>(L);
scripted->bindGroup = std::move(bindGroup);
return 1;
}
// ============================================================================
// GPUPipeline
// ============================================================================
// Resolve a stage's entry on a shader by optional WGSL name. Errors (listing
// available names) if a named entry is missing.
static void resolveShaderEntry(lua_State* L,
ScriptedShader* shader,
uint8_t stage,
const char* entryReq,
const char* stageName,
ore::ShaderModule** outModule,
std::string* outEntryName)
{
const ScriptedShaderEntry* e = shader->resolveEntry(stage, entryReq);
if (e == nullptr)
{
std::string avail;
for (const auto& rec : shader->entries)
{
if (rec.stage != stage)
continue;
if (!avail.empty())
avail += ", ";
avail += rec.logical;
}
if (entryReq != nullptr && entryReq[0] != '\0')
luaL_error(L,
"GPUPipeline.new: %s entry point '%s' not found "
"(available: %s)",
stageName,
entryReq,
avail.empty() ? "<none>" : avail.c_str());
luaL_error(L,
"GPUPipeline.new: %s shader has no %s entry point",
stageName,
stageName);
}
*outModule = e->module.get();
*outEntryName = e->physical;
}
// Parse a pipeline stage descriptor at stack index `valueIdx`: a bare Shader or
// WebGPU-style { module = Shader, entryPoint = string? }. Omitting entryPoint
// selects the first entry of that stage. Returns the ScriptedShader (for the
// combined-file fragment fallback) plus the resolved module + physical entry
// name. Returns false only when the value is nil.
static bool resolveStageEntry(lua_State* L,
int valueIdx,
uint8_t stage,
const char* stageName,
ScriptedShader** outShader,
ore::ShaderModule** outModule,
std::string* outEntryName)
{
if (lua_isnil(L, valueIdx))
return false;
ScriptedShader* shader = nullptr;
const char* entryReq = nullptr;
if (lua_istable(L, valueIdx))
{
lua_getfield(L, valueIdx, "module");
shader = lua_torive<ScriptedShader>(L, -1);
lua_pop(L, 1);
entryReq = lua_getoptionalstringfield(L, valueIdx, "entryPoint");
}
else
{
shader = lua_torive<ScriptedShader>(L, valueIdx);
}
if (shader == nullptr || !shader->hasModule())
luaL_error(L,
"GPUPipeline.new: '%s' must be a Shader or "
"{ module = Shader, entryPoint = string? }",
stageName);
resolveShaderEntry(L,
shader,
stage,
entryReq,
stageName,
outModule,
outEntryName);
if (outShader)
*outShader = shader;
return true;
}
static int gpupipeline_construct(lua_State* L)
{
if (getOreContext(L) == nullptr)
{
luaL_error(L, "GPU context not initialized");
}
luaL_checktype(L, 1, LUA_TTABLE);
int descIdx = 1;
PipelineDesc desc;
// vertex / fragment accept a bare Shader or WebGPU-style
// { module = Shader, entryPoint = string? }. Omitting entryPoint selects
// the first @vertex/@fragment of the module (WebGPU default). The resolved
// physical entry names are held in these std::strings, which must outlive
// the makePipeline call below (PipelineDesc keeps const char* into them).
std::string vsEntryName, fsEntryName;
ScriptedShader* vsShader = nullptr;
// vertex shader (required)
lua_getfield(L, descIdx, "vertex");
{
ore::ShaderModule* mod = nullptr;
if (!resolveStageEntry(L,
lua_gettop(L),
0,
"vertex",
&vsShader,
&mod,
&vsEntryName))
luaL_error(L, "GPUPipeline.new: 'vertex' is required");
desc.vertexModule = mod;
desc.vertexEntryPoint = vsEntryName.c_str();
}
lua_pop(L, 1);
// fragment shader. Three cases:
// * explicit `fragment` → use it.
// * absent + at least one colorTarget → fall back to the vertex shader's
// fragment entry (combined file). Deferred until colorTargets parsed.
// * absent + no colorTargets → depth-only pipeline, no fragment.
bool explicitFragment = false;
lua_getfield(L, descIdx, "fragment");
if (!lua_isnil(L, -1))
{
ore::ShaderModule* mod = nullptr;
ScriptedShader* fsShader = nullptr;
resolveStageEntry(L,
lua_gettop(L),
1,
"fragment",
&fsShader,
&mod,
&fsEntryName);
desc.fragmentModule = mod;
desc.fragmentEntryPoint = fsEntryName.c_str();
explicitFragment = true;
}
lua_pop(L, 1);
// vertexLayout (required)
lua_getfield(L, descIdx, "vertexLayout");
luaL_checktype(L, -1, LUA_TTABLE);
int layoutTableIdx = lua_gettop(L);
int vbCount = (int)lua_objlen(L, layoutTableIdx);
// Stack-allocate vertex buffer layouts and attributes
static constexpr int MAX_VB = 8;
static constexpr int MAX_ATTRS = 32;
VertexBufferLayout vbLayouts[MAX_VB];
VertexAttribute attrs[MAX_ATTRS];
int totalAttrs = 0;
for (int vb = 0; vb < vbCount && vb < MAX_VB; vb++)
{
lua_rawgeti(L, layoutTableIdx, vb + 1);
int vbIdx = lua_gettop(L);
vbLayouts[vb].stride = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, vbIdx, "stride", 0));
const char* stepMode = lua_getoptionalstringfield(L, vbIdx, "stepMode");
if (stepMode && strcmp(stepMode, "instance") == 0)
vbLayouts[vb].stepMode = VertexStepMode::instance;
else
vbLayouts[vb].stepMode = VertexStepMode::vertex;
lua_getfield(L, vbIdx, "attributes");
int attrTableIdx = lua_gettop(L);
int attrCount = (int)lua_objlen(L, attrTableIdx);
vbLayouts[vb].attributes = &attrs[totalAttrs];
vbLayouts[vb].attributeCount = attrCount;
for (int a = 0; a < attrCount && totalAttrs < MAX_ATTRS; a++)
{
lua_rawgeti(L, attrTableIdx, a + 1);
int attrIdx = lua_gettop(L);
const char* fmt = lua_getoptionalstringfield(L, attrIdx, "format");
if (fmt)
attrs[totalAttrs].format = lua_tovertexformat(L, fmt);
attrs[totalAttrs].shaderSlot = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, attrIdx, "slot", 0));
attrs[totalAttrs].offset = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, attrIdx, "offset", 0));
totalAttrs++;
lua_pop(L, 1); // pop attr entry
}
lua_pop(L, 1); // pop attributes table
lua_pop(L, 1); // pop vb entry
}
lua_pop(L, 1); // pop vertexLayout table
// colorTargets (optional). WebGPU-shaped: omitting it means "no color
// outputs" (depth-only pipeline, e.g. shadow map). We override the C++
// PipelineDesc default of colorCount=1 so that scripts don't get a
// phantom color target that mismatches a depth-only render pass.
desc.colorCount = 0;
lua_getfield(L, descIdx, "colorTargets");
if (lua_istable(L, -1))
{
int ctTableIdx = lua_gettop(L);
int ctCount = (int)lua_objlen(L, ctTableIdx);
constexpr int kMaxColorTargets =
sizeof(desc.colorTargets) / sizeof(desc.colorTargets[0]);
if (ctCount > kMaxColorTargets)
{
luaL_error(L,
"GPUPipeline.new: colorTargets count %d exceeds "
"maximum of %d",
ctCount,
kMaxColorTargets);
}
desc.colorCount = ctCount;
for (int ct = 0; ct < ctCount; ct++)
{
lua_rawgeti(L, ctTableIdx, ct + 1);
int ctIdx = lua_gettop(L);
const char* fmt = lua_getoptionalstringfield(L, ctIdx, "format");
if (fmt)
desc.colorTargets[ct].format = lua_totextureformat(L, fmt);
// writeMask: optional. String like "rgba" / "rg" / "" / "all".
// Default (when absent) is `ColorWriteMask::all` from PipelineDesc.
const char* wm = lua_getoptionalstringfield(L, ctIdx, "writeMask");
if (wm)
desc.colorTargets[ct].writeMask = lua_towritemask(L, wm);
lua_getfield(L, ctIdx, "blend");
if (lua_istable(L, -1))
{
int blendIdx = lua_gettop(L);
desc.colorTargets[ct].blendEnabled = true;
const char* s;
s = lua_getoptionalstringfield(L, blendIdx, "srcColor");
if (s)
desc.colorTargets[ct].blend.srcColor =
lua_toblendfactor(L, s);
s = lua_getoptionalstringfield(L, blendIdx, "dstColor");
if (s)
desc.colorTargets[ct].blend.dstColor =
lua_toblendfactor(L, s);
s = lua_getoptionalstringfield(L, blendIdx, "colorOp");
if (s)
desc.colorTargets[ct].blend.colorOp = lua_toblendop(L, s);
s = lua_getoptionalstringfield(L, blendIdx, "srcAlpha");
if (s)
desc.colorTargets[ct].blend.srcAlpha =
lua_toblendfactor(L, s);
s = lua_getoptionalstringfield(L, blendIdx, "dstAlpha");
if (s)
desc.colorTargets[ct].blend.dstAlpha =
lua_toblendfactor(L, s);
s = lua_getoptionalstringfield(L, blendIdx, "alphaOp");
if (s)
desc.colorTargets[ct].blend.alphaOp = lua_toblendop(L, s);
}
lua_pop(L, 1); // pop blend
lua_pop(L, 1); // pop color target entry
}
}
lua_pop(L, 1); // pop colorTargets
// Resolve the deferred fragment-module decision (see "fragment shader"
// block above). With color outputs but no explicit fragment shader, fall
// back to the vertex shader's first fragment entry (combined file).
if (!explicitFragment && desc.colorCount > 0)
{
ore::ShaderModule* mod = nullptr;
resolveShaderEntry(L,
vsShader,
1,
nullptr,
"fragment",
&mod,
&fsEntryName);
desc.fragmentModule = mod;
desc.fragmentEntryPoint = fsEntryName.c_str();
}
// depthStencil (optional)
lua_getfield(L, descIdx, "depthStencil");
if (lua_istable(L, -1))
{
int dsIdx = lua_gettop(L);
const char* fmt = lua_getoptionalstringfield(L, dsIdx, "format");
if (fmt)
desc.depthStencil.format = lua_totextureformat(L, fmt);
const char* cmp = lua_getoptionalstringfield(L, dsIdx, "compare");
if (cmp)
desc.depthStencil.depthCompare = lua_tocompare(L, cmp);
desc.depthStencil.depthWriteEnabled =
lua_getoptionalboolfield(L, dsIdx, "write", false);
desc.depthStencil.depthBias = static_cast<int32_t>(
lua_getoptionalnumberfield(L, dsIdx, "depthBias", 0));
desc.depthStencil.depthBiasSlopeScale = static_cast<float>(
lua_getoptionalnumberfield(L, dsIdx, "depthBiasSlopeScale", 0));
desc.depthStencil.depthBiasClamp = static_cast<float>(
lua_getoptionalnumberfield(L, dsIdx, "depthBiasClamp", 0));
}
lua_pop(L, 1);
// stencilFront / stencilBack (optional, both default to "always pass,
// keep on every op"). Each is a table of compare + failOp + depthFailOp
// + passOp strings.
lua_getfield(L, descIdx, "stencilFront");
lua_tostencilface(L, lua_gettop(L), &desc.stencilFront);
lua_pop(L, 1);
lua_getfield(L, descIdx, "stencilBack");
lua_tostencilface(L, lua_gettop(L), &desc.stencilBack);
lua_pop(L, 1);
// stencilReadMask / stencilWriteMask (optional, default 0xFF).
desc.stencilReadMask = static_cast<uint8_t>(
lua_getoptionalnumberfield(L, descIdx, "stencilReadMask", 0xFF));
desc.stencilWriteMask = static_cast<uint8_t>(
lua_getoptionalnumberfield(L, descIdx, "stencilWriteMask", 0xFF));
// bindGroupLayouts (optional). When absent, derive layouts per
// @group(N) from the shader's binding map (WebGPU's `layout: 'auto'`).
// Supply explicitly to share one layout across multiple pipelines.
BindGroupLayout* layoutPtrs[ore::kMaxBindGroups] = {};
uint32_t layoutCount = 0;
std::vector<rcp<BindGroupLayout>> autoLayouts;
lua_getfield(L, descIdx, "bindGroupLayouts");
bool explicitLayouts = lua_istable(L, -1);
if (explicitLayouts)
{
int blglIdx = lua_gettop(L);
int n = (int)lua_objlen(L, blglIdx);
if (n > static_cast<int>(ore::kMaxBindGroups))
{
luaL_error(L,
"GPUPipeline.new: bindGroupLayouts count %d exceeds "
"maximum of %u",
n,
ore::kMaxBindGroups);
}
for (int i = 0; i < n; ++i)
{
lua_rawgeti(L, blglIdx, i + 1);
auto* l = lua_torive<ScriptedGPUBindGroupLayout>(L, -1);
layoutPtrs[i] = l ? l->layout.get() : nullptr;
lua_pop(L, 1);
}
layoutCount = static_cast<uint32_t>(n);
}
lua_pop(L, 1);
if (!explicitLayouts)
{
// Auto path: scan the binding map for unique groups, build a
// layout per group. Sparse-group shaders are supported (e.g.
// group 0 + group 2 → autoLayouts[1] is null/empty).
const BindingMap& bm = vsShader->vertexMod()->m_bindingMap;
uint32_t maxGroup = 0;
bool seen[ore::kMaxBindGroups] = {};
for (size_t i = 0; i < bm.size(); ++i)
{
uint32_t g = bm.at(i).group;
if (g >= ore::kMaxBindGroups)
continue;
seen[g] = true;
if (g + 1 > maxGroup)
maxGroup = g + 1;
}
autoLayouts.resize(maxGroup);
static constexpr int kMaxEntries = 16;
for (uint32_t g = 0; g < maxGroup; ++g)
{
if (!seen[g])
continue;
BindGroupLayoutEntry entries[kMaxEntries]{};
uint32_t n = populateEntriesFromShader(entries,
kMaxEntries,
vsShader->vertexMod(),
g,
nullptr,
0);
BindGroupLayoutDesc lDesc;
lDesc.groupIndex = g;
lDesc.entries = entries;
lDesc.entryCount = n;
autoLayouts[g] = getOreContext(L)->makeBindGroupLayout(lDesc);
layoutPtrs[g] = autoLayouts[g].get();
}
layoutCount = maxGroup;
}
desc.bindGroupLayouts = layoutPtrs;
desc.bindGroupLayoutCount = layoutCount;
// cullMode (optional)
const char* cull = lua_getoptionalstringfield(L, descIdx, "cullMode");
if (cull)
desc.cullMode = lua_tocullmode(L, cull);
// winding (optional, default counterClockwise)
const char* wind = lua_getoptionalstringfield(L, descIdx, "winding");
if (wind)
desc.winding = lua_towinding(L, wind);
// topology (optional)
const char* topo = lua_getoptionalstringfield(L, descIdx, "topology");
if (topo)
desc.topology = lua_totopology(L, topo);
// sampleCount (optional, default 1)
uint32_t pipelineSampleCount = static_cast<uint32_t>(
lua_getoptionalnumberfield(L, descIdx, "sampleCount", 1));
desc.sampleCount = pipelineSampleCount;
// Allocate the ScriptedGPUPipeline first so we can deep-copy the vertex
// layout data into it. Pipeline shallow-copies PipelineDesc, so the
// vertex buffer layout pointers must outlive the Pipeline.
auto* scripted = lua_newrive<ScriptedGPUPipeline>(L);
// Deep-copy layouts and attributes into a single byte buffer:
// [ VertexBufferLayout[vbCount] ][ VertexAttribute[totalAttrs] ]
size_t layoutBytes = sizeof(VertexBufferLayout) * vbCount;
size_t attrBytes = sizeof(VertexAttribute) * totalAttrs;
scripted->ownedVertexLayoutData.resize(layoutBytes + attrBytes);
auto* ownedLayouts = reinterpret_cast<VertexBufferLayout*>(
scripted->ownedVertexLayoutData.data());
auto* ownedAttrs = reinterpret_cast<VertexAttribute*>(
scripted->ownedVertexLayoutData.data() + layoutBytes);
memcpy(ownedLayouts, vbLayouts, layoutBytes);
memcpy(ownedAttrs, attrs, attrBytes);
// Patch each layout's attributes pointer to point into the owned copy.
int attrOffset = 0;
for (int vb = 0; vb < vbCount; vb++)
{
ownedLayouts[vb].attributes = ownedAttrs + attrOffset;
attrOffset += ownedLayouts[vb].attributeCount;
}
desc.vertexBuffers = ownedLayouts;
desc.vertexBufferCount = vbCount;
std::string pipelineError;
auto pipeline = getOreContext(L)->makePipeline(desc, &pipelineError);
if (!pipeline)
{
if (pipelineError.empty())
luaL_error(L, "GPUPipeline.new: failed to create pipeline");
else
luaL_error(L, "GPUPipeline.new: %s", pipelineError.c_str());
}
scripted->pipeline = std::move(pipeline);
scripted->sampleCount = pipelineSampleCount;
scripted->autoBindGroupLayouts = std::move(autoLayouts);
return 1;
}
// pipeline:getBindGroupLayout(groupIndex) — returns an auto-derived layout
// (WebGPU's pipeline.getBindGroupLayout). Errors when explicit layouts were
// supplied — share the explicit one directly instead.
static int gpupipeline_getbindgrouplayout(lua_State* L)
{
auto* self = lua_torive<ScriptedGPUPipeline>(L, 1);
uint32_t group = static_cast<uint32_t>(luaL_checkunsigned(L, 2));
if (self->autoBindGroupLayouts.empty())
luaL_error(L,
"getBindGroupLayout: pipeline was built with explicit "
"bindGroupLayouts; reuse the layout you supplied");
if (group >= self->autoBindGroupLayouts.size() ||
!self->autoBindGroupLayouts[group])
luaL_error(L,
"getBindGroupLayout: group %u not present in shader",
group);
auto* w = lua_newrive<ScriptedGPUBindGroupLayout>(L);
w->layout = self->autoBindGroupLayouts[group];
return 1;
}
static int gpupipeline_namecall(lua_State* L)
{
int atom;
const char* method = lua_namecallatom(L, &atom);
if (method == nullptr)
luaL_error(L, "GPUPipeline: no method specified");
if (strcmp(method, "getBindGroupLayout") == 0)
return gpupipeline_getbindgrouplayout(L);
luaL_error(L, "GPUPipeline: unknown method '%s'", method);
return 0;
}
// ============================================================================
// GPURenderPass
// ============================================================================
static void validate_render_pass(lua_State* L, ScriptedGPURenderPass* self)
{
// pass->isFinished() catches the case where a *previous*
// beginRenderPass auto-finished this pass (because the script forgot
// to :finish() before opening the next one). The wrapper's own
// m_finished is still false there.
if (self->m_finished || !self->pass || self->pass->isFinished())
{
luaL_error(L,
"render pass expired — already finished, or auto-"
"finished by a subsequent beginRenderPass");
}
}
static void validate_pipeline_set(lua_State* L, ScriptedGPURenderPass* self)
{
if (!self->m_pipelineSet)
{
luaL_error(L,
"setPipeline must be called before draw/setVertexBuffer/"
"setBindGroup");
}
}
static int gpurenderpass_setpipeline(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
auto* pipeline = lua_torive<ScriptedGPUPipeline>(L, 2);
if (pipeline->sampleCount != self->sampleCount)
{
luaL_error(L,
"pipeline sampleCount (%u) does not match render pass "
"sampleCount (%u) — recreate the pipeline with matching "
"sampleCount",
pipeline->sampleCount,
self->sampleCount);
}
// Capture any attachment-compat failure from ore::RenderPass::setPipeline.
// Without this, checkPipelineCompat failures silently no-op the Metal
// encoder state and subsequent draws crash inside the driver.
Context* oreCtx = getOreContext(L);
if (oreCtx)
oreCtx->clearLastError();
self->pass->setPipeline(pipeline->pipeline.get());
if (oreCtx && !oreCtx->lastError().empty())
{
luaL_error(L, "setPipeline: %s", oreCtx->lastError().c_str());
}
self->m_pipelineSet = true;
return 0;
}
static int gpurenderpass_setvertexbuffer(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
// Note: WebGPU / Metal / Vulkan / D3D11 all permit `setVertexBuffer`
// before `setPipeline` — vertex-buffer state is layered onto whatever
// pipeline is current at draw time. Don't gate on `m_pipelineSet`.
uint32_t slot = static_cast<uint32_t>(luaL_checkunsigned(L, 2));
if (slot > 7)
{
luaL_error(L, "setVertexBuffer: slot must be 0-7 (got %u)", slot);
}
auto* buffer = lua_torive<ScriptedGPUBuffer>(L, 3);
self->pass->setVertexBuffer(slot, buffer->buffer.get());
return 0;
}
static int gpurenderpass_setindexbuffer(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
auto* buffer = lua_torive<ScriptedGPUBuffer>(L, 2);
IndexFormat fmt = IndexFormat::uint16;
if (lua_isstring(L, 3))
{
const char* s = lua_tostring(L, 3);
if (strcmp(s, "uint32") == 0)
fmt = IndexFormat::uint32;
}
self->pass->setIndexBuffer(buffer->buffer.get(), fmt);
return 0;
}
static int gpurenderpass_setbindgroup(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
uint32_t groupIndex = static_cast<uint32_t>(luaL_checkunsigned(L, 2));
if (groupIndex >= ore::kMaxBindGroups)
{
luaL_error(L,
"setBindGroup: groupIndex must be in [0, %u) (got %u)",
ore::kMaxBindGroups,
groupIndex);
}
auto* bg = lua_torive<ScriptedGPUBindGroup>(L, 3);
// Parse optional dynamic offsets array.
//
// WebGPU contract: `dynamicOffsets[i]` corresponds to the i-th dynamic
// entry in the BindGroupLayout, ordered by ascending `@binding`.
// The count must equal the BindGroup's dynamic-offset count exactly,
// and each value must be aligned to `minUniformBufferOffsetAlignment`
// (256 bytes — D3D11.1's `firstConstant` requirement, D3D12's CBV
// alignment, Vulkan's adapter-default minimum). Validate here so a
// misuse error surfaces on the Lua call site instead of a backend
// assert / silent misbind.
constexpr uint32_t kDynamicOffsetAlign = 256;
uint32_t dynamicOffsets[8] = {};
uint32_t dynamicOffsetCount = 0;
if (lua_istable(L, 4))
{
int tbl = 4;
int n = (int)lua_objlen(L, tbl);
if (n > 8)
{
luaL_error(L,
"setBindGroup: dynamicOffsets count %d exceeds "
"maximum of 8",
n);
}
for (int i = 0; i < n; i++)
{
lua_rawgeti(L, tbl, i + 1);
uint32_t off = static_cast<uint32_t>(lua_tonumber(L, -1));
lua_pop(L, 1);
if ((off % kDynamicOffsetAlign) != 0)
{
luaL_error(L,
"setBindGroup: dynamicOffsets[%d] = %u is not a "
"multiple of %u (alignment requirement)",
i,
off,
kDynamicOffsetAlign);
}
dynamicOffsets[dynamicOffsetCount++] = off;
}
}
if (bg->bindGroup &&
dynamicOffsetCount != bg->bindGroup->dynamicOffsetCount())
{
luaL_error(L,
"setBindGroup: dynamicOffsets count %u does not match the "
"BindGroup's declared dynamic UBO count %u",
dynamicOffsetCount,
bg->bindGroup->dynamicOffsetCount());
}
self->pass->setBindGroup(groupIndex,
bg->bindGroup.get(),
dynamicOffsetCount > 0 ? dynamicOffsets : nullptr,
dynamicOffsetCount);
return 0;
}
static int gpurenderpass_setviewport(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
float x = static_cast<float>(luaL_checknumber(L, 2));
float y = static_cast<float>(luaL_checknumber(L, 3));
float w = static_cast<float>(luaL_checknumber(L, 4));
float h = static_cast<float>(luaL_checknumber(L, 5));
self->pass->setViewport(x, y, w, h);
return 0;
}
static int gpurenderpass_setscissorrect(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
uint32_t x = static_cast<uint32_t>(luaL_checkunsigned(L, 2));
uint32_t y = static_cast<uint32_t>(luaL_checkunsigned(L, 3));
uint32_t w = static_cast<uint32_t>(luaL_checkunsigned(L, 4));
uint32_t h = static_cast<uint32_t>(luaL_checkunsigned(L, 5));
self->pass->setScissorRect(x, y, w, h);
return 0;
}
static int gpurenderpass_setstencilreference(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
uint32_t ref = static_cast<uint32_t>(luaL_checkunsigned(L, 2));
self->pass->setStencilReference(ref);
return 0;
}
static int gpurenderpass_setblendcolor(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
float r = static_cast<float>(luaL_checknumber(L, 2));
float g = static_cast<float>(luaL_checknumber(L, 3));
float b = static_cast<float>(luaL_checknumber(L, 4));
float a = static_cast<float>(luaL_checknumber(L, 5));
self->pass->setBlendColor(r, g, b, a);
return 0;
}
// `pass:draw(vertexCount [, instanceCount [, firstVertex
// [, firstInstance]]])`
static int gpurenderpass_draw(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
validate_pipeline_set(L, self);
uint32_t vertexCount = static_cast<uint32_t>(luaL_checkunsigned(L, 2));
uint32_t instanceCount =
lua_isnumber(L, 3) ? static_cast<uint32_t>(lua_tonumber(L, 3)) : 1;
uint32_t firstVertex =
lua_isnumber(L, 4) ? static_cast<uint32_t>(lua_tonumber(L, 4)) : 0;
uint32_t firstInstance =
lua_isnumber(L, 5) ? static_cast<uint32_t>(lua_tonumber(L, 5)) : 0;
if (firstInstance > 0 && !getOreContext(L)->features().drawBaseInstance)
{
luaL_error(L,
"draw: firstInstance=%u requires the drawBaseInstance "
"feature, which the active backend does not support",
firstInstance);
}
self->pass->draw(vertexCount, instanceCount, firstVertex, firstInstance);
self->drawCallCount++;
return 0;
}
// `pass:drawIndexed(indexCount [, instanceCount [, firstIndex
// [, baseVertex [, firstInstance]]]])`
static int gpurenderpass_drawindexed(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
validate_pipeline_set(L, self);
uint32_t indexCount = static_cast<uint32_t>(luaL_checkunsigned(L, 2));
uint32_t instanceCount =
lua_isnumber(L, 3) ? static_cast<uint32_t>(lua_tonumber(L, 3)) : 1;
uint32_t firstIndex =
lua_isnumber(L, 4) ? static_cast<uint32_t>(lua_tonumber(L, 4)) : 0;
int32_t baseVertex =
lua_isnumber(L, 5) ? static_cast<int32_t>(lua_tointeger(L, 5)) : 0;
uint32_t firstInstance =
lua_isnumber(L, 6) ? static_cast<uint32_t>(lua_tonumber(L, 6)) : 0;
if (baseVertex != 0 && !getOreContext(L)->features().drawBaseInstance)
{
luaL_error(L,
"drawIndexed: baseVertex=%d requires the "
"drawBaseInstance feature, which the active backend "
"does not support",
baseVertex);
}
if (firstInstance > 0 && !getOreContext(L)->features().drawBaseInstance)
{
luaL_error(L,
"drawIndexed: firstInstance=%u requires the "
"drawBaseInstance feature, which the active backend "
"does not support",
firstInstance);
}
self->pass->drawIndexed(indexCount,
instanceCount,
firstIndex,
baseVertex,
firstInstance);
self->drawCallCount++;
return 0;
}
static int gpurenderpass_finish(lua_State* L)
{
auto* self = lua_torive<ScriptedGPURenderPass>(L, 1);
validate_render_pass(L, self);
self->pass->finish();
self->m_finished = true;
// Clear the context's active pass pointer so the next beginRenderPass
// doesn't see a stale (already-finished) pass.
Context* oreCtx = getOreContext(L);
if (oreCtx && oreCtx->activeRenderPass() == self->pass.get())
oreCtx->setActiveRenderPass(nullptr);
return 0;
}
static int gpurenderpass_namecall(lua_State* L)
{
int atom;
const char* str = lua_namecallatom(L, &atom);
if (str != nullptr)
{
switch (atom)
{
case (int)LuaAtoms::setPipeline:
return gpurenderpass_setpipeline(L);
case (int)LuaAtoms::setVertexBuffer:
return gpurenderpass_setvertexbuffer(L);
case (int)LuaAtoms::setIndexBuffer:
return gpurenderpass_setindexbuffer(L);
case (int)LuaAtoms::setBindGroup:
return gpurenderpass_setbindgroup(L);
case (int)LuaAtoms::setViewport:
return gpurenderpass_setviewport(L);
case (int)LuaAtoms::setScissorRect:
return gpurenderpass_setscissorrect(L);
case (int)LuaAtoms::setStencilReference:
return gpurenderpass_setstencilreference(L);
case (int)LuaAtoms::setBlendColor:
return gpurenderpass_setblendcolor(L);
case (int)LuaAtoms::draw:
return gpurenderpass_draw(L);
case (int)LuaAtoms::drawIndexed:
return gpurenderpass_drawindexed(L);
case (int)LuaAtoms::finish:
return gpurenderpass_finish(L);
}
}
luaL_error(L,
"%s is not a valid method of %s",
str,
ScriptedGPURenderPass::luaName);
return 0;
}
// ============================================================================
// ScriptedGPUCanvas / ScriptedCanvas — destructors
// ============================================================================
ScriptedGPUCanvas::~ScriptedGPUCanvas()
{
if (m_L != nullptr && m_imageRef != LUA_NOREF)
{
lua_unref(m_L, m_imageRef);
}
}
ScriptedGPURenderPass::~ScriptedGPURenderPass()
{
// If the script GC'd the wrapper without :finish(), drop the active-
// pass slot before unique_ptr destroys the backend RenderPass — else
// ore::Context::activeRenderPass() would dangle into the next
// beginRenderPass.
if (m_context && pass && m_context->activeRenderPass() == pass.get())
{
m_context->setActiveRenderPass(nullptr);
}
}
ScriptedCanvas::~ScriptedCanvas()
{
// Clean up any active frame renderer that was never ended
delete m_riveRenderer;
m_riveRenderer = nullptr;
if (m_L != nullptr)
{
if (m_rendererRef != LUA_NOREF)
lua_unref(m_L, m_rendererRef);
if (m_imageRef != LUA_NOREF)
lua_unref(m_L, m_imageRef);
}
}
// ============================================================================
// GPUCanvas
// ============================================================================
static LoadOp lua_toloadop_str(const char* s)
{
if (strcmp(s, "load") == 0)
return LoadOp::load;
return LoadOp::clear;
}
static StoreOp lua_tostoreop_str(const char* s)
{
if (strcmp(s, "discard") == 0)
return StoreOp::discard;
return StoreOp::store;
}
// canvas:beginRenderPass(desc) — open a render pass against this canvas.
//
// `desc` is required: at least one color attachment or a depthStencil
// attachment must be supplied. Every attachment carries its own view, and
// sampleCount is derived from those views (all attachments must share one
// sampleCount, matching WebGPU validation).
//
// Color attachment `view` is optional: when omitted it defaults to the
// receiving canvas's own colorView. Provide an explicit view for cases
// where the target differs (e.g. MSAA: view = msaa:view(),
// resolveTarget = canvas:colorView()).
int gpucanvas_beginrenderpass(lua_State* L)
{
auto* self = lua_torive<ScriptedGPUCanvas>(L, 1);
Context* oreCtx = getOreContext(L);
if (oreCtx == nullptr)
{
luaL_error(L, "GPUCanvas:beginRenderPass() requires a GPU context");
}
auto* scriptingContext =
static_cast<ScriptingContext*>(lua_getthreaddata(L));
if (scriptingContext == nullptr || !scriptingContext->canvasDrawingPhase())
{
luaL_error(L,
"GPUCanvas:beginRenderPass() called outside drawing phase");
}
luaL_checktype(L, 2, LUA_TTABLE);
RenderPassDesc passDesc{};
passDesc.colorCount = 0;
// Tracks the first attachment we see (any color slot or depth) and
// becomes the pass's authoritative sampleCount. Subsequent attachments
// must match. -1 means "not yet seen any attachment". The label owns
// its storage so per-iteration stack buffers don't dangle into the
// error path.
int32_t passSampleCount = -1;
std::string sampleCountSourceLabel;
auto recordSampleCount = [&](uint32_t sc, const char* label) {
if (passSampleCount == -1)
{
passSampleCount = static_cast<int32_t>(sc);
sampleCountSourceLabel = label;
}
else if (static_cast<uint32_t>(passSampleCount) != sc)
{
luaL_error(
L,
"beginRenderPass: %s sampleCount (%u) does not match %s "
"sampleCount (%u). All render-pass attachments must share "
"one sampleCount.",
label,
sc,
sampleCountSourceLabel.c_str(),
static_cast<uint32_t>(passSampleCount));
}
};
lua_getfield(L, 2, "color");
if (lua_istable(L, -1))
{
for (int ci = 1; ci <= 4; ++ci)
{
lua_rawgeti(L, -1, ci);
if (!lua_istable(L, -1))
{
lua_pop(L, 1);
break;
}
int slot = passDesc.colorCount++;
lua_getfield(L, -1, "view");
ore::TextureView* viewPtr = nullptr;
if (lua_isnil(L, -1))
{
// Sugar: omitting `view` defaults to the receiving canvas's
// own colorView. Errors only if the canvas is deferred
// (zero-sized — no backing texture).
if (!self->oreColorView)
{
luaL_error(L,
"beginRenderPass: color[%d].view omitted but "
"the receiving canvas has no backing texture "
"(zero-sized). Call canvas:resize(w, h) "
"before drawing, or pass an explicit view.",
slot + 1);
}
viewPtr = self->oreColorView.get();
}
else
{
auto* tv = lua_torive<ScriptedGPUTextureView>(L, -1);
if (!tv || !tv->view)
{
luaL_error(L,
"beginRenderPass: color[%d].view is not a "
"valid GPUTextureView",
slot + 1);
}
viewPtr = tv->view.get();
}
passDesc.colorAttachments[slot].view = viewPtr;
char colorLabel[32];
snprintf(colorLabel, sizeof(colorLabel), "color[%d]", slot + 1);
recordSampleCount(viewPtr->texture()->sampleCount(), colorLabel);
lua_pop(L, 1); // view
// resolveTarget (optional — for MSAA). Source view must be
// multisampled; resolve target must be sampleCount=1 and
// format-matched to the source.
lua_getfield(L, -1, "resolveTarget");
if (!lua_isnil(L, -1))
{
auto* rtv = lua_torive<ScriptedGPUTextureView>(L, -1);
if (rtv && rtv->view)
{
auto* msaaTex =
passDesc.colorAttachments[slot].view
? passDesc.colorAttachments[slot].view->texture()
: nullptr;
if (msaaTex && msaaTex->sampleCount() == 1)
{
luaL_error(
L,
"beginRenderPass: color[%d].resolveTarget is "
"meaningless when the source `view` is single-"
"sampled — drop it, or use an MSAA texture as "
"`view`",
slot + 1);
}
if (msaaTex &&
rtv->view->texture()->format() != msaaTex->format())
{
luaL_error(
L,
"beginRenderPass: resolveTarget format '%s' does "
"not match MSAA attachment format '%s' — resolve "
"requires identical formats. Use canvas.format to "
"match your pipeline and textures.",
lua_totextureformatstring(
rtv->view->texture()->format()),
lua_totextureformatstring(msaaTex->format()));
}
if (rtv->view->texture()->sampleCount() != 1)
{
luaL_error(L,
"beginRenderPass: color[%d].resolveTarget "
"must have sampleCount=1 (got %u)",
slot + 1,
rtv->view->texture()->sampleCount());
}
passDesc.colorAttachments[slot].resolveTarget =
rtv->view.get();
}
}
lua_pop(L, 1); // resolveTarget
lua_getfield(L, -1, "loadOp");
if (!lua_isnil(L, -1))
{
passDesc.colorAttachments[slot].loadOp =
lua_toloadop_str(luaL_checkstring(L, -1));
}
else
{
passDesc.colorAttachments[slot].loadOp = LoadOp::clear;
}
lua_pop(L, 1); // loadOp
lua_getfield(L, -1, "storeOp");
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
luaL_error(L,
"beginRenderPass: color[%d].storeOp is required "
"— use 'discard' for MSAA color (after resolve) "
"or 'store' to keep the rendered output",
slot + 1);
}
passDesc.colorAttachments[slot].storeOp =
lua_tostoreop_str(luaL_checkstring(L, -1));
lua_pop(L, 1); // storeOp
lua_getfield(L, -1, "clearColor");
if (lua_istable(L, -1))
{
lua_rawgeti(L, -1, 1);
passDesc.colorAttachments[slot].clearColor.r =
(float)lua_tonumber(L, -1);
lua_pop(L, 1);
lua_rawgeti(L, -1, 2);
passDesc.colorAttachments[slot].clearColor.g =
(float)lua_tonumber(L, -1);
lua_pop(L, 1);
lua_rawgeti(L, -1, 3);
passDesc.colorAttachments[slot].clearColor.b =
(float)lua_tonumber(L, -1);
lua_pop(L, 1);
lua_rawgeti(L, -1, 4);
passDesc.colorAttachments[slot].clearColor.a =
(float)lua_tonumber(L, -1);
lua_pop(L, 1);
}
lua_pop(L, 1); // clearColor
lua_pop(L, 1); // entry table
}
}
lua_pop(L, 1); // color
lua_getfield(L, 2, "depthStencil");
if (lua_istable(L, -1))
{
lua_getfield(L, -1, "view");
if (lua_isnil(L, -1))
{
luaL_error(L,
"beginRenderPass: depthStencil.view is required — "
"pass GPUTexture:view()");
}
auto* tv = lua_torive<ScriptedGPUTextureView>(L, -1);
if (!tv || !tv->view)
{
luaL_error(L,
"beginRenderPass: depthStencil.view is not a valid "
"GPUTextureView");
}
passDesc.depthStencil.view = tv->view.get();
recordSampleCount(tv->view->texture()->sampleCount(), "depthStencil");
lua_pop(L, 1); // view
passDesc.depthStencil.depthLoadOp = LoadOp::clear;
lua_getfield(L, -1, "depthLoadOp");
if (!lua_isnil(L, -1))
{
passDesc.depthStencil.depthLoadOp =
lua_toloadop_str(luaL_checkstring(L, -1));
}
lua_pop(L, 1);
lua_getfield(L, -1, "depthStoreOp");
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
luaL_error(L,
"beginRenderPass: depthStencil.depthStoreOp is "
"required — use 'discard' for transient/MSAA depth or "
"'store' if you need to read it later");
}
passDesc.depthStencil.depthStoreOp =
lua_tostoreop_str(luaL_checkstring(L, -1));
lua_pop(L, 1);
lua_getfield(L, -1, "depthClearValue");
if (!lua_isnil(L, -1))
{
passDesc.depthStencil.depthClearValue =
static_cast<float>(lua_tonumber(L, -1));
}
lua_pop(L, 1);
}
lua_pop(L, 1); // depthStencil
if (passDesc.colorCount == 0 && !passDesc.depthStencil.view)
{
luaL_error(L,
"beginRenderPass: descriptor must include at least one "
"color attachment or a depthStencil attachment");
}
// Metal (and other backends) only allow one active encoder per command
// buffer. If a previous pass was left open, finish it before opening
// a new encoder.
if (oreCtx->activeRenderPass() && !oreCtx->activeRenderPass()->isFinished())
{
oreCtx->activeRenderPass()->finish();
oreCtx->setActiveRenderPass(nullptr);
}
auto* rp = lua_newrive<ScriptedGPURenderPass>(L);
rp->pass = oreCtx->beginRenderPass(passDesc);
rp->m_context = oreCtx;
rp->m_finished = false;
rp->sampleCount =
passSampleCount < 1 ? 1u : static_cast<uint32_t>(passSampleCount);
rp->label = passDesc.label ? passDesc.label : "";
rp->drawCallCount = 0;
oreCtx->setActiveRenderPass(rp->pass.get());
return 1;
}
// Recreate the underlying RenderCanvas at a new size, then re-wrap its backing
// texture for use in ORE render passes. The handle's `.image` ref continues to
// point to the updated canvas image. Resizing to zero in either dimension
// drops the backing texture and leaves the canvas in a deferred state.
static int gpucanvashandle_resize(lua_State* L)
{
auto* self = lua_torive<ScriptedGPUCanvas>(L, 1);
uint32_t w = static_cast<uint32_t>(luaL_checkunsigned(L, 2));
uint32_t h = static_cast<uint32_t>(luaL_checkunsigned(L, 3));
if (self->renderCtx == nullptr)
{
luaL_error(L, "GPUCanvas: renderCtx not initialized");
}
auto* oreCtx = getOreContext(L);
if (oreCtx == nullptr)
{
luaL_error(L, "GPUCanvas: GPU context not initialized");
}
if (w == 0 || h == 0)
{
if (self->m_L != nullptr && self->m_imageRef != LUA_NOREF)
{
lua_unref(self->m_L, self->m_imageRef);
self->m_imageRef = LUA_NOREF;
}
self->canvas = nullptr;
self->oreColorView = nullptr;
return 0;
}
// Allocate and wrap the new backing BEFORE touching the existing
// canvas/view/imageRef. If either step throws (via luaL_error), the
// canvas keeps its previous, still-valid backing.
auto newCanvas = self->renderCtx->makeRenderCanvas(w, h);
if (!newCanvas)
{
luaL_error(L, "GPUCanvas:resize() failed to create RenderCanvas");
}
auto newColorView = oreCtx->wrapCanvasTexture(newCanvas.get());
if (!newColorView)
{
luaL_error(L, "GPUCanvas:resize() failed to wrap canvas texture");
}
if (self->m_L != nullptr && self->m_imageRef != LUA_NOREF)
{
lua_unref(self->m_L, self->m_imageRef);
self->m_imageRef = LUA_NOREF;
}
self->canvas = std::move(newCanvas);
self->oreColorView = std::move(newColorView);
auto* img = lua_newrive<ScriptedImage>(L);
img->image =
ref_rcp(static_cast<RenderImage*>(self->canvas->renderImage()));
self->m_imageRef = lua_ref(L, -1);
lua_pop(L, 1); // pop image
return 0;
}
static int gpucanvashandle_colorview(lua_State* L)
{
auto* self = lua_torive<ScriptedGPUCanvas>(L, 1);
if (!self->oreColorView)
{
luaL_error(L,
"GPUCanvas:colorView() called on a zero-sized canvas; "
"call canvas:resize(w, h) first");
}
auto* tv = lua_newrive<ScriptedGPUTextureView>(L);
tv->view = self->oreColorView;
return 1;
}
static void gpucanvashandle_direct_width(void* udata, void* result)
{
auto* self = (ScriptedGPUCanvas*)udata;
lua_userdatadirectfield_setnumber(result,
self->canvas ? self->canvas->width() : 0);
}
static void gpucanvashandle_direct_height(void* udata, void* result)
{
auto* self = (ScriptedGPUCanvas*)udata;
lua_userdatadirectfield_setnumber(result,
self->canvas ? self->canvas->height()
: 0);
}
static int gpucanvashandle_index(lua_State* L)
{
int atom;
const char* key = lua_tostringatom(L, 2, &atom);
if (!key)
{
luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
}
auto* self = lua_torive<ScriptedGPUCanvas>(L, 1);
switch (atom)
{
case (int)LuaAtoms::image:
if (self->m_imageRef != LUA_NOREF)
{
rive_lua_pushRef(L, self->m_imageRef);
return 1;
}
lua_pushnil(L);
return 1;
case (int)LuaAtoms::width:
lua_pushnumber(L, self->canvas ? self->canvas->width() : 0);
return 1;
case (int)LuaAtoms::height:
lua_pushnumber(L, self->canvas ? self->canvas->height() : 0);
return 1;
case (int)LuaAtoms::format:
// Realized canvas reports its texture format. Deferred canvas
// reports the format makeRenderCanvas always allocates.
if (self->oreColorView && self->oreColorView->texture())
lua_pushstring(L,
lua_totextureformatstring(
self->oreColorView->texture()->format()));
else
lua_pushstring(
L,
lua_totextureformatstring(TextureFormat::rgba8unorm));
return 1;
}
luaL_error(L, "'%s' is not a valid index of GPUCanvas", key);
return 0;
}
static int gpucanvashandle_namecall(lua_State* L)
{
int atom;
const char* str = lua_namecallatom(L, &atom);
if (str != nullptr)
{
switch (atom)
{
case (int)LuaAtoms::colorView:
return gpucanvashandle_colorview(L);
case (int)LuaAtoms::resize:
return gpucanvashandle_resize(L);
case (int)LuaAtoms::beginRenderPass:
return gpucanvas_beginrenderpass(L);
default:
break;
}
}
luaL_error(L,
"%s is not a valid method of %s",
str,
ScriptedGPUCanvas::luaName);
return 0;
}
// ============================================================================
// Canvas (2D Rive renderer canvas)
// ============================================================================
// Recreate the underlying RenderCanvas at a new size. Must not be called
// between beginFrame() and endFrame(). Resizing to zero in either dimension
// drops the backing texture and leaves the canvas in a deferred state.
static int canvashandle_resize(lua_State* L)
{
auto* self = lua_torive<ScriptedCanvas>(L, 1);
uint32_t w = static_cast<uint32_t>(luaL_checkunsigned(L, 2));
uint32_t h = static_cast<uint32_t>(luaL_checkunsigned(L, 3));
if (self->renderCtx == nullptr)
{
luaL_error(L, "Canvas: renderCtx not initialized");
}
if (self->m_state != CanvasState::Idle)
{
luaL_error(L, "Canvas:resize() called during an active frame");
}
if (w == 0 || h == 0)
{
if (self->m_L != nullptr && self->m_imageRef != LUA_NOREF)
{
lua_unref(self->m_L, self->m_imageRef);
self->m_imageRef = LUA_NOREF;
}
self->canvas = nullptr;
return 0;
}
// Allocate the new backing BEFORE touching the existing canvas/imageRef.
// If makeRenderCanvas throws (via luaL_error), the canvas keeps its
// previous, still-valid backing.
auto newCanvas = self->renderCtx->makeRenderCanvas(w, h);
if (!newCanvas)
{
luaL_error(L, "Canvas:resize() failed to create RenderCanvas");
}
if (self->m_L != nullptr && self->m_imageRef != LUA_NOREF)
{
lua_unref(self->m_L, self->m_imageRef);
self->m_imageRef = LUA_NOREF;
}
self->canvas = std::move(newCanvas);
auto* img = lua_newrive<ScriptedImage>(L);
img->image =
ref_rcp(static_cast<RenderImage*>(self->canvas->renderImage()));
self->m_imageRef = lua_ref(L, -1);
lua_pop(L, 1);
return 0;
}
// Begin a Rive rendering frame on this canvas. Returns a Renderer that the
// caller can use to issue Rive draw calls. Must be paired with endFrame().
//
// Optional `desc` table fields:
// clearColor: Color? ARGB integer (e.g. 0xFF000000 for opaque black).
// Defaults to transparent black (0).
static int canvashandle_beginframe(lua_State* L)
{
auto* self = lua_torive<ScriptedCanvas>(L, 1);
if (self->renderCtx == nullptr)
{
luaL_error(L, "Canvas: renderCtx not initialized");
}
auto* scriptingContext =
static_cast<ScriptingContext*>(lua_getthreaddata(L));
if (scriptingContext == nullptr || !scriptingContext->canvasDrawingPhase())
{
luaL_error(L, "Canvas:beginFrame() called outside drawing phase");
}
if (self->m_state != CanvasState::Idle)
{
luaL_error(L, "Canvas:beginFrame() called during an active frame");
}
if (!self->canvas)
{
luaL_error(L,
"Canvas:beginFrame() called on a zero-sized canvas; "
"call canvas:resize(w, h) first");
}
gpu::RenderContext::FrameDescriptor desc{};
desc.renderTargetWidth = self->canvas->width();
desc.renderTargetHeight = self->canvas->height();
desc.loadAction = gpu::LoadAction::clear;
desc.clearColor = 0; // transparent black
if (lua_gettop(L) >= 2 && lua_istable(L, 2))
{
lua_getfield(L, 2, "clearColor");
if (!lua_isnil(L, -1))
{
// clearColor is a ColorInt (ARGB uint32)
desc.clearColor = static_cast<ColorInt>(lua_tounsigned(L, -1));
}
lua_pop(L, 1);
}
self->renderCtx->beginFrame(desc);
// Allocate a RiveRenderer that issues into this render context.
// Deleted in endFrame() (or in the destructor if endFrame is never called).
self->m_riveRenderer = new RiveRenderer(self->renderCtx);
self->m_state = CanvasState::Rendering;
// Push a non-owning ScriptedRenderer wrapping our RiveRenderer and keep a
// registry ref so the Lua object stays alive until endFrame().
lua_newrive<ScriptedRenderer>(L, self->m_riveRenderer);
lua_pushvalue(L, -1);
self->m_rendererRef = lua_ref(L, -1);
lua_pop(L, 1); // pop the extra copy used for ref; original stays on stack
return 1; // returns the ScriptedRenderer
}
// Flush all pending Rive draw calls for this frame to the canvas render target,
// then release the renderer. Must be called after beginFrame().
static int canvashandle_endframe(lua_State* L)
{
auto* self = lua_torive<ScriptedCanvas>(L, 1);
if (self->m_state != CanvasState::Rendering)
{
luaL_error(L, "Canvas:endFrame() called without beginFrame()");
}
// Null out the ScriptedRenderer's pointer so it can no longer issue draws.
if (self->m_L != nullptr && self->m_rendererRef != LUA_NOREF)
{
rive_lua_pushRef(L, self->m_rendererRef);
if (!lua_isnil(L, -1))
{
auto* sr = lua_torive<ScriptedRenderer>(L, -1);
if (sr != nullptr)
sr->end();
}
lua_pop(L, 1);
lua_unref(self->m_L, self->m_rendererRef);
self->m_rendererRef = LUA_NOREF;
}
// Create a command buffer, flush the render context into the canvas
// render target, then commit. Without a proper command buffer the
// buffer ring mutex would never be unlocked (the completion handler
// that unlocks it is registered on the command buffer).
void* commandBuffer = self->renderCtx->impl()->makeCommandBuffer();
gpu::RenderContext::FlushResources flush{};
flush.renderTarget = self->canvas->renderTarget();
flush.externalCommandBuffer = commandBuffer;
self->renderCtx->flush(flush);
self->renderCtx->impl()->commitCommandBuffer(commandBuffer);
// Destroy the RiveRenderer — it is no longer valid after flush().
delete self->m_riveRenderer;
self->m_riveRenderer = nullptr;
self->m_state = CanvasState::Idle;
return 0;
}
static void canvashandle_direct_width(void* udata, void* result)
{
auto* self = (ScriptedCanvas*)udata;
lua_userdatadirectfield_setnumber(result,
self->canvas ? self->canvas->width() : 0);
}
static void canvashandle_direct_height(void* udata, void* result)
{
auto* self = (ScriptedCanvas*)udata;
lua_userdatadirectfield_setnumber(result,
self->canvas ? self->canvas->height()
: 0);
}
static int canvashandle_index(lua_State* L)
{
int atom;
const char* key = lua_tostringatom(L, 2, &atom);
if (!key)
{
luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
}
auto* self = lua_torive<ScriptedCanvas>(L, 1);
switch (atom)
{
case (int)LuaAtoms::image:
if (self->m_imageRef != LUA_NOREF)
{
rive_lua_pushRef(L, self->m_imageRef);
return 1;
}
lua_pushnil(L);
return 1;
case (int)LuaAtoms::width:
lua_pushnumber(L, self->canvas ? self->canvas->width() : 0);
return 1;
case (int)LuaAtoms::height:
lua_pushnumber(L, self->canvas ? self->canvas->height() : 0);
return 1;
}
luaL_error(L, "'%s' is not a valid index of Canvas", key);
return 0;
}
static int canvashandle_namecall(lua_State* L)
{
int atom;
const char* str = lua_namecallatom(L, &atom);
if (str != nullptr)
{
switch (atom)
{
case (int)LuaAtoms::beginFrame:
return canvashandle_beginframe(L);
case (int)LuaAtoms::endFrame:
return canvashandle_endframe(L);
case (int)LuaAtoms::resize:
return canvashandle_resize(L);
default:
break;
}
}
luaL_error(L,
"%s is not a valid method of %s",
str,
ScriptedCanvas::luaName);
return 0;
}
// ============================================================================
// Registration
// ============================================================================
static const luaL_Reg empty[] = {
{NULL, NULL},
};
template <typename T>
static void register_type_with_constructor(lua_State* L,
lua_CFunction constructor,
lua_CFunction namecall = nullptr,
lua_CFunction indexfn = nullptr)
{
luaL_register(L, T::luaName, empty);
lua_register_rive<T>(L);
if (namecall)
{
lua_pushcfunction(L, namecall, nullptr);
lua_setfield(L, -2, "__namecall");
}
if (indexfn)
{
lua_pushcfunction(L, indexfn, nullptr);
lua_setfield(L, -2, "__index");
}
// Create metatable for the metatable (so we can call T.new())
lua_createtable(L, 0, 1);
lua_pushcfunction(L, constructor, nullptr);
lua_setfield(L, -2, "__index");
// Also set __call so T.new(...) works
// Actually the pattern is T.new(), so set new on the __index table
// Let's do it properly: make __index return the new function
lua_pop(L, 1); // pop the meta-meta
// Simpler: just put "new" on the library table directly
lua_pushcfunction(L, constructor, nullptr);
lua_setfield(L, -2, "new");
lua_setreadonly(L, -1, true);
lua_pop(L, 1); // pop the metatable
}
int luaopen_rive_gpu(lua_State* L)
{
// 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},
{nullptr, nullptr}};
static const luaL_Reg gpuSamplerStatics[] = {{"new", gpusampler_construct},
{nullptr, nullptr}};
static const luaL_Reg gpuPipelineStatics[] = {
{"new", gpupipeline_construct},
{nullptr, nullptr}};
static const luaL_Reg gpuBindGroupStatics[] = {
{"new", gpubindgroup_construct},
{nullptr, nullptr}};
static const luaL_Reg gpuBindGroupLayoutStatics[] = {
{"new", gpubindgrouplayout_construct},
{nullptr, nullptr}};
// Shader
{
luaL_register(L, ScriptedShader::luaName, shaderStatics);
lua_register_rive<ScriptedShader>(L);
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
}
// GPUBuffer
{
luaL_register(L, ScriptedGPUBuffer::luaName, gpuBufferStatics);
lua_register_rive<ScriptedGPUBuffer>(L);
lua_pushcfunction(L, gpubuffer_namecall, nullptr);
lua_setfield(L, -2, "__namecall");
lua_pushcfunction(L, gpubuffer_index, nullptr);
lua_setfield(L, -2, "__index");
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
lua_registeruserdatadirectfieldget(L,
ScriptedGPUBuffer::luaTag,
"size",
gpubuffer_direct_size);
}
// GPUTexture
{
luaL_register(L, ScriptedGPUTexture::luaName, gpuTextureStatics);
lua_register_rive<ScriptedGPUTexture>(L);
lua_pushcfunction(L, gputexture_namecall, nullptr);
lua_setfield(L, -2, "__namecall");
lua_pushcfunction(L, gputexture_index, nullptr);
lua_setfield(L, -2, "__index");
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
lua_registeruserdatadirectfieldget(L,
ScriptedGPUTexture::luaTag,
"width",
gputexture_direct_width);
lua_registeruserdatadirectfieldget(L,
ScriptedGPUTexture::luaTag,
"height",
gputexture_direct_height);
}
// GPUSampler
{
luaL_register(L, ScriptedGPUSampler::luaName, gpuSamplerStatics);
lua_register_rive<ScriptedGPUSampler>(L);
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
}
// GPUPipeline
{
luaL_register(L, ScriptedGPUPipeline::luaName, gpuPipelineStatics);
lua_register_rive<ScriptedGPUPipeline>(L);
lua_pushcfunction(L, gpupipeline_namecall, nullptr);
lua_setfield(L, -2, "__namecall");
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
}
// GPUBindGroup
{
luaL_register(L, ScriptedGPUBindGroup::luaName, gpuBindGroupStatics);
lua_register_rive<ScriptedGPUBindGroup>(L);
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
}
// GPUBindGroupLayout
{
luaL_register(L,
ScriptedGPUBindGroupLayout::luaName,
gpuBindGroupLayoutStatics);
lua_register_rive<ScriptedGPUBindGroupLayout>(L);
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
}
// GPURenderPass
{
luaL_register(L, ScriptedGPURenderPass::luaName, empty);
lua_register_rive<ScriptedGPURenderPass>(L);
lua_pushcfunction(L, gpurenderpass_namecall, nullptr);
lua_setfield(L, -2, "__namecall");
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
}
// GPUTextureView (no constructor — created via GPUTexture:view())
{
luaL_register(L, ScriptedGPUTextureView::luaName, empty);
lua_register_rive<ScriptedGPUTextureView>(L);
lua_pushcfunction(L, gputextureview_index, nullptr);
lua_setfield(L, -2, "__index");
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
}
// GPUCanvas (no public constructor — created via context:gpuCanvas())
{
luaL_register(L, ScriptedGPUCanvas::luaName, empty);
lua_register_rive<ScriptedGPUCanvas>(L);
lua_pushcfunction(L, gpucanvashandle_namecall, nullptr);
lua_setfield(L, -2, "__namecall");
lua_pushcfunction(L, gpucanvashandle_index, nullptr);
lua_setfield(L, -2, "__index");
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
lua_registeruserdatadirectfieldget(L,
ScriptedGPUCanvas::luaTag,
"width",
gpucanvashandle_direct_width);
lua_registeruserdatadirectfieldget(L,
ScriptedGPUCanvas::luaTag,
"height",
gpucanvashandle_direct_height);
}
// Canvas (no public constructor — created via context:canvas())
{
luaL_register(L, ScriptedCanvas::luaName, empty);
lua_register_rive<ScriptedCanvas>(L);
lua_pushcfunction(L, canvashandle_namecall, nullptr);
lua_setfield(L, -2, "__namecall");
lua_pushcfunction(L, canvashandle_index, nullptr);
lua_setfield(L, -2, "__index");
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
lua_registeruserdatadirectfieldget(L,
ScriptedCanvas::luaTag,
"width",
canvashandle_direct_width);
lua_registeruserdatadirectfieldget(L,
ScriptedCanvas::luaTag,
"height",
canvashandle_direct_height);
}
return 0;
}
// ============================================================================
// Image:view() implementation — called from lua_image.cpp
// Lives here because ore headers require ObjC++ on Apple.
// ============================================================================
#include "rive/renderer/rive_render_image.hpp"
// ore::TextureView is complete here — define the destructor and factory.
ScriptedImage::~ScriptedImage() = default;
ScriptedImage* ScriptedImage::luaNew(lua_State* L)
{
return lua_newrive<ScriptedImage>(L);
}
int riveImageViewImpl(lua_State* L)
{
auto* self = lua_torive<ScriptedImage>(L, 1);
if (!self->image)
{
luaL_error(L, "Image has no backing texture");
return 0;
}
// Safe cast — returns nullptr if the image isn't GPU-backed.
auto* riveImage = lite_rtti_cast<RiveRenderImage*>(self->image.get());
if (!riveImage)
{
luaL_error(L, "Image is not a GPU-backed RiveRenderImage");
return 0;
}
gpu::Texture* sourceGpuTex = riveImage->getTexture();
if (!sourceGpuTex)
{
luaL_error(L, "Image GPU texture not available");
return 0;
}
// Get ore::Context from scripting context.
auto* ctx = static_cast<ScriptingContext*>(lua_getthreaddata(L));
auto* oreCtx = static_cast<ore::Context*>(ctx->oreContext());
if (!oreCtx)
{
luaL_error(L, "GPU context not available for Image:view()");
return 0;
}
if (!self->cachedOreView)
{
// GL canvas-import boundary: on GL/WebGL, sampling a Rive 2D
// RenderCanvas as a WGSL texture requires a Y-flipped companion
// because PLS renders the canvas bottom-up while WGSL expects
// V=0 at the visual top of the image. The render context's
// getCanvasImportMirror returns nullptr on every backend except
// GL — on GL it lazily allocates a companion texture, registers
// a per-flush blit hook, and returns the companion image. We
// cache the companion's RiveRenderImage so the companion stays
// alive as long as this ScriptedImage does.
//
// See dev/ore_canvas_import_invariant.md.
gpu::Texture* texToWrap = sourceGpuTex;
#if defined(ORE_BACKEND_GL)
{
auto* renderCtx =
static_cast<gpu::RenderContext*>(ctx->renderContext());
self->cachedMirrorImage =
getCanvasImportMirrorGL(renderCtx,
sourceGpuTex,
self->image->width(),
self->image->height());
if (self->cachedMirrorImage != nullptr)
{
auto* mirrorRive = lite_rtti_cast<RiveRenderImage*>(
self->cachedMirrorImage.get());
if (mirrorRive != nullptr &&
mirrorRive->getTexture() != nullptr)
{
texToWrap = mirrorRive->getTexture();
}
}
}
#endif // ORE_BACKEND_GL
self->cachedOreView = oreCtx->wrapRiveTexture(texToWrap,
self->image->width(),
self->image->height());
if (!self->cachedOreView)
{
luaL_error(L, "Image:view() not supported on this backend");
return 0;
}
}
// Create the GPUTextureView and have it retain the RenderImage so the
// underlying gpu::Texture stays alive even if the Image is GC'd.
auto* tv = lua_newrive<ScriptedGPUTextureView>(L);
tv->view = self->cachedOreView;
tv->retainedImage = self->image;
return 1;
}
namespace rive
{
void rive_lua_closeOrphanRenderPass(lua_State* L)
{
auto* context = static_cast<ScriptingContext*>(lua_getthreaddata(L));
if (context == nullptr)
return;
auto* oreCtx = static_cast<ore::Context*>(context->oreContext());
if (oreCtx == nullptr)
return;
auto* pass = oreCtx->activeRenderPass();
if (pass == nullptr || pass->isFinished())
return;
pass->finish();
oreCtx->setActiveRenderPass(nullptr);
lua_pushstring(L,
"GPU render pass left open at script return. "
"Call :finish() on render passes before returning.");
context->printError(L);
lua_pop(L, 1);
}
} // namespace rive
#endif // RIVE_CANVAS && RIVE_ORE
#endif // WITH_RIVE_SCRIPTING