| #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_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/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 lua_tobufferusage(lua_State* L, int idx) |
| { |
| const char* s = luaL_checkstring(L, idx); |
| 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", s); |
| return BufferUsage::vertex; |
| } |
| |
| 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::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"; |
| } |
| } |
| |
| 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()); |
| } |
| |
| /// Returns the RSTB `ShaderTarget` byte that matches the ore backend |
| /// compiled into this build. Dispatches on `ORE_BACKEND_*` rather than |
| /// host-OS macros (`__APPLE__` / `_WIN32`) so hosts whose backend doesn't |
| /// match the host's "default" runtime API — e.g. the Linux GPU recorder |
| /// (Vulkan, not GL) or Mac MoltenVK — pick the correct pre-compiled |
| /// variant out of the RSTB table. |
| /// |
| /// Target enum (must match `ShaderAsset` RSTB format): |
| /// 0 = WGSL passthrough, 1 = GLSL ES3, 2 = MSL, 3 = HLSL SM5, |
| /// 5 = SPIR-V |
| static uint8_t currentShaderTarget() |
| { |
| #if defined(ORE_BACKEND_METAL) |
| return 2; // MSL |
| #elif defined(ORE_BACKEND_VK) |
| return 5; // SPIR-V |
| #elif defined(ORE_BACKEND_D3D11) || defined(ORE_BACKEND_D3D12) |
| return 3; // HLSL SM5, compiled to DXBC at runtime via D3DCompile |
| #elif defined(ORE_BACKEND_WGPU) |
| return 0; // WGSL passthrough (Dawn + wagyu) |
| #elif defined(ORE_BACKEND_GL) |
| return 1; // GLSL ES3 |
| #else |
| return 1; |
| #endif |
| } |
| |
| /// Try to create ShaderModules from a raw RSTB blob. |
| /// For GLSL targets, the blob may contain two sources separated by a null |
| /// byte (vertex then fragment). Populates both module and fragmentModule on |
| /// the ScriptedShader. Returns true on success. |
| /// Copy texture-sampler pairs from a ShaderAsset into both |
| /// vertex and fragment ShaderModules of a ScriptedShader. |
| static void propagatePairs(const ShaderAsset& asset, ScriptedShader* out) |
| { |
| auto pairs = asset.textureSamplerPairs(); |
| if (pairs.empty()) |
| return; |
| std::vector<ShaderModule::TextureSamplerPair> vec; |
| vec.reserve(pairs.size()); |
| for (size_t i = 0; i < pairs.size(); i++) |
| { |
| vec.push_back({pairs[i].texGroup, |
| pairs[i].texBinding, |
| pairs[i].sampGroup, |
| pairs[i].sampBinding}); |
| } |
| if (out->module) |
| out->module->m_textureSamplerPairs = vec; |
| if (out->fragmentModule) |
| out->fragmentModule->m_textureSamplerPairs = vec; |
| } |
| |
| 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::decode expects a SignedContentHeader envelope |
| // (shared with ScriptAsset: `[flags:1][sig:64?][inner]`). The workspace |
| // hands us raw RSTB bytes, so prepend an unsigned envelope (flags=0, |
| // no signature) to produce the canonical input format. |
| 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; |
| |
| uint8_t target = currentShaderTarget(); |
| auto blob = asset.findShader(0, target); |
| if (blob.empty()) |
| return false; |
| |
| const char* blobData = reinterpret_cast<const char*>(blob.data()); |
| uint32_t blobSize = static_cast<uint32_t>(blob.size()); |
| |
| // Binding-map sidecar (mandatory) + GL fixup sidecars (only present |
| // for the GLSL source target). RFC §14.4: every shipped shader carries |
| // a sidecar paired with its source variant. |
| auto bindingMapTargetFor = [](uint8_t t) -> uint8_t { |
| switch (t) |
| { |
| case 0: |
| return 16; |
| case 1: |
| return 11; |
| case 2: |
| return 10; |
| case 3: |
| return 12; |
| case 5: |
| return 13; |
| default: |
| return 255; |
| } |
| }; |
| uint8_t bmTarget = bindingMapTargetFor(target); |
| auto bindingMapBlob = (bmTarget == 255) ? Span<const uint8_t>{} |
| : asset.findShader(0, bmTarget); |
| const uint8_t* bindingMapBytes = |
| bindingMapBlob.empty() ? nullptr : bindingMapBlob.data(); |
| uint32_t bindingMapSize = static_cast<uint32_t>(bindingMapBlob.size()); |
| auto vsGLFixupBlob = |
| (target == 1) ? asset.findShader(0, 14) : Span<const uint8_t>{}; |
| auto fsGLFixupBlob = |
| (target == 1) ? asset.findShader(0, 15) : Span<const uint8_t>{}; |
| const uint8_t* vsGLFixupBytes = |
| vsGLFixupBlob.empty() ? nullptr : vsGLFixupBlob.data(); |
| uint32_t vsGLFixupSize = static_cast<uint32_t>(vsGLFixupBlob.size()); |
| const uint8_t* fsGLFixupBytes = |
| fsGLFixupBlob.empty() ? nullptr : fsGLFixupBlob.data(); |
| uint32_t fsGLFixupSize = static_cast<uint32_t>(fsGLFixupBlob.size()); |
| |
| // HLSL SM5 via SPIRV-Cross (D3D11/D3D12 on Windows). |
| // Blob format: "vsEntry\0fsEntry\0vsHLSL\0fsHLSL" |
| // Compiled to DXBC at runtime via D3DCompile in ensureD3DShaders(). |
| if (target == 3) |
| { |
| // Parse: vsEntry\0 |
| const char* vsEntry = blobData; |
| const char* vsEnd = |
| static_cast<const char*>(memchr(vsEntry, '\0', blobSize)); |
| if (!vsEnd) |
| return false; |
| // Parse: fsEntry\0 |
| const char* fsEntry = vsEnd + 1; |
| uint32_t remaining = |
| blobSize - static_cast<uint32_t>(fsEntry - blobData); |
| const char* fsEnd = |
| static_cast<const char*>(memchr(fsEntry, '\0', remaining)); |
| if (!fsEnd) |
| return false; |
| // Parse: vsHLSL\0 |
| const char* vsHLSL = fsEnd + 1; |
| remaining = blobSize - static_cast<uint32_t>(vsHLSL - blobData); |
| const char* vsHLSLEnd = |
| static_cast<const char*>(memchr(vsHLSL, '\0', remaining)); |
| if (!vsHLSLEnd) |
| return false; |
| // Parse: fsHLSL (rest of blob) |
| const char* fsHLSL = vsHLSLEnd + 1; |
| uint32_t fsHLSLSize = |
| blobSize - static_cast<uint32_t>(fsHLSL - blobData); |
| |
| // Create vertex module with HLSL source for runtime D3DCompile. |
| { |
| ShaderModuleDesc vtxDesc; |
| vtxDesc.stage = ShaderStage::vertex; |
| vtxDesc.hlslSource = vsHLSL; |
| vtxDesc.hlslSourceSize = static_cast<uint32_t>(vsHLSLEnd - vsHLSL); |
| vtxDesc.hlslEntryPoint = vsEntry; |
| vtxDesc.bindingMapBytes = bindingMapBytes; |
| vtxDesc.bindingMapSize = bindingMapSize; |
| out->module = oreCtx->makeShaderModule(vtxDesc); |
| } |
| // Create fragment module with HLSL source for runtime D3DCompile. |
| { |
| ShaderModuleDesc fragDesc; |
| fragDesc.stage = ShaderStage::fragment; |
| fragDesc.hlslSource = fsHLSL; |
| fragDesc.hlslSourceSize = fsHLSLSize; |
| fragDesc.hlslEntryPoint = fsEntry; |
| fragDesc.bindingMapBytes = bindingMapBytes; |
| fragDesc.bindingMapSize = bindingMapSize; |
| out->fragmentModule = oreCtx->makeShaderModule(fragDesc); |
| } |
| if (out->module) |
| propagatePairs(asset, out); |
| return out->module != nullptr; |
| } |
| |
| // Check for a null separator inside the blob — indicates split |
| // vertex/fragment GLSL (one GLSL source per entry point). |
| const char* sep = |
| static_cast<const char*>(memchr(blobData, '\0', blobSize - 1)); |
| |
| if (sep != nullptr) |
| { |
| // Split blob: vertex before separator, fragment after. |
| uint32_t vtxSize = static_cast<uint32_t>(sep - blobData); |
| const char* fragData = sep + 1; |
| uint32_t fragSize = blobSize - vtxSize - 1; |
| |
| ShaderModuleDesc vtxDesc; |
| vtxDesc.code = blobData; |
| vtxDesc.codeSize = vtxSize; |
| vtxDesc.stage = ShaderStage::vertex; |
| vtxDesc.bindingMapBytes = bindingMapBytes; |
| vtxDesc.bindingMapSize = bindingMapSize; |
| vtxDesc.glFixupBytes = vsGLFixupBytes; |
| vtxDesc.glFixupSize = vsGLFixupSize; |
| out->module = oreCtx->makeShaderModule(vtxDesc); |
| |
| ShaderModuleDesc fragDesc; |
| fragDesc.code = fragData; |
| fragDesc.codeSize = fragSize; |
| fragDesc.stage = ShaderStage::fragment; |
| fragDesc.bindingMapBytes = bindingMapBytes; |
| fragDesc.bindingMapSize = bindingMapSize; |
| fragDesc.glFixupBytes = fsGLFixupBytes; |
| fragDesc.glFixupSize = fsGLFixupSize; |
| out->fragmentModule = oreCtx->makeShaderModule(fragDesc); |
| |
| if (out->module && out->fragmentModule) |
| propagatePairs(asset, out); |
| return out->module != nullptr && out->fragmentModule != nullptr; |
| } |
| |
| // Single combined blob (Metal, WGSL, etc.) |
| ShaderModuleDesc desc; |
| desc.code = blobData; |
| desc.codeSize = blobSize; |
| desc.bindingMapBytes = bindingMapBytes; |
| desc.bindingMapSize = bindingMapSize; |
| out->module = oreCtx->makeShaderModule(desc); |
| if (out->module) |
| propagatePairs(asset, out); |
| return out->module != nullptr; |
| } |
| |
| /// Public wrapper: build a ScriptedShader from raw RSTB bytes. Used by the |
| /// runtime `loadShader` fallback path for legacy .riv files where WGSL |
| /// shaders were packed into ScriptAsset containers (pre-ShaderAsset). |
| bool lua_gpu_make_shader_from_rstb(ScriptedShader* out, |
| ScriptingContext* context, |
| const uint8_t* data, |
| uint32_t len) |
| { |
| Context* oreCtx = static_cast<Context*>( |
| context != nullptr ? context->oreContext() : nullptr); |
| return makeShaderFromRstb(oreCtx, data, len, out); |
| } |
| |
| /// 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. |
| // Use the same RSTB decoding as the editor path so that GLSL |
| // vertex/fragment splitting and texture-sampler pair propagation |
| // are handled correctly on all backends. |
| if (fileAsset != nullptr && oreCtx != nullptr) |
| { |
| uint8_t target = currentShaderTarget(); |
| auto blob = fileAsset->findShader(0, target); |
| if (!blob.empty()) |
| { |
| const char* blobData = reinterpret_cast<const char*>(blob.data()); |
| uint32_t blobSize = static_cast<uint32_t>(blob.size()); |
| |
| // Sidecar lookup (mirrors makeShaderFromRstb above). The |
| // sidecar is mandatory; absence here would assert in |
| // `makeShaderModule::applyBindingMapFromDesc`. |
| auto bmTarget = [](uint8_t t) -> uint8_t { |
| switch (t) |
| { |
| case 0: |
| return 16; |
| case 1: |
| return 11; |
| case 2: |
| return 10; |
| case 3: |
| return 12; |
| case 5: |
| return 13; |
| default: |
| return 255; |
| } |
| }(target); |
| auto bmBlob = (bmTarget == 255) |
| ? Span<const uint8_t>{} |
| : fileAsset->findShader(0, bmTarget); |
| const uint8_t* bindingMapBytes = |
| bmBlob.empty() ? nullptr : bmBlob.data(); |
| uint32_t bindingMapSize = static_cast<uint32_t>(bmBlob.size()); |
| auto vsFixupBlob = (target == 1) ? fileAsset->findShader(0, 14) |
| : Span<const uint8_t>{}; |
| auto fsFixupBlob = (target == 1) ? fileAsset->findShader(0, 15) |
| : Span<const uint8_t>{}; |
| const uint8_t* vsFixupBytes = |
| vsFixupBlob.empty() ? nullptr : vsFixupBlob.data(); |
| uint32_t vsFixupSize = static_cast<uint32_t>(vsFixupBlob.size()); |
| const uint8_t* fsFixupBytes = |
| fsFixupBlob.empty() ? nullptr : fsFixupBlob.data(); |
| uint32_t fsFixupSize = static_cast<uint32_t>(fsFixupBlob.size()); |
| |
| // HLSL SM5 (target 3): split vsEntry/fsEntry/vsHLSL/fsHLSL. |
| if (target == 3) |
| { |
| const char* vsEntry = blobData; |
| const char* vsEnd = |
| static_cast<const char*>(memchr(vsEntry, '\0', blobSize)); |
| if (!vsEnd) |
| return false; |
| const char* fsEntry = vsEnd + 1; |
| uint32_t remaining = |
| blobSize - static_cast<uint32_t>(fsEntry - blobData); |
| const char* fsEnd = |
| static_cast<const char*>(memchr(fsEntry, '\0', remaining)); |
| if (!fsEnd) |
| return false; |
| const char* vsHLSL = fsEnd + 1; |
| remaining = blobSize - static_cast<uint32_t>(vsHLSL - blobData); |
| const char* vsHLSLEnd = |
| static_cast<const char*>(memchr(vsHLSL, '\0', remaining)); |
| if (!vsHLSLEnd) |
| return false; |
| const char* fsHLSL = vsHLSLEnd + 1; |
| uint32_t fsHLSLSize = |
| blobSize - static_cast<uint32_t>(fsHLSL - blobData); |
| |
| ShaderModuleDesc vtxDesc; |
| vtxDesc.stage = ShaderStage::vertex; |
| vtxDesc.hlslSource = vsHLSL; |
| vtxDesc.hlslSourceSize = |
| static_cast<uint32_t>(vsHLSLEnd - vsHLSL); |
| vtxDesc.hlslEntryPoint = vsEntry; |
| vtxDesc.bindingMapBytes = bindingMapBytes; |
| vtxDesc.bindingMapSize = bindingMapSize; |
| out->module = oreCtx->makeShaderModule(vtxDesc); |
| |
| ShaderModuleDesc fragDesc; |
| fragDesc.stage = ShaderStage::fragment; |
| fragDesc.hlslSource = fsHLSL; |
| fragDesc.hlslSourceSize = fsHLSLSize; |
| fragDesc.hlslEntryPoint = fsEntry; |
| fragDesc.bindingMapBytes = bindingMapBytes; |
| fragDesc.bindingMapSize = bindingMapSize; |
| out->fragmentModule = oreCtx->makeShaderModule(fragDesc); |
| } |
| // GLSL ES3 (target 1): split vertex\0fragment. |
| else if (target == 1 && blobSize > 1) |
| { |
| const char* sep = static_cast<const char*>( |
| memchr(blobData, '\0', blobSize - 1)); |
| if (sep) |
| { |
| uint32_t vtxSize = static_cast<uint32_t>(sep - blobData); |
| const char* fragData = sep + 1; |
| uint32_t fragSize = blobSize - vtxSize - 1; |
| |
| ShaderModuleDesc vtxDesc; |
| vtxDesc.code = blobData; |
| vtxDesc.codeSize = vtxSize; |
| vtxDesc.stage = ShaderStage::vertex; |
| vtxDesc.bindingMapBytes = bindingMapBytes; |
| vtxDesc.bindingMapSize = bindingMapSize; |
| vtxDesc.glFixupBytes = vsFixupBytes; |
| vtxDesc.glFixupSize = vsFixupSize; |
| out->module = oreCtx->makeShaderModule(vtxDesc); |
| |
| ShaderModuleDesc fragDesc; |
| fragDesc.code = fragData; |
| fragDesc.codeSize = fragSize; |
| fragDesc.stage = ShaderStage::fragment; |
| fragDesc.bindingMapBytes = bindingMapBytes; |
| fragDesc.bindingMapSize = bindingMapSize; |
| fragDesc.glFixupBytes = fsFixupBytes; |
| fragDesc.glFixupSize = fsFixupSize; |
| out->fragmentModule = oreCtx->makeShaderModule(fragDesc); |
| } |
| else |
| { |
| // No separator — single module (unusual). |
| ShaderModuleDesc desc; |
| desc.code = blobData; |
| desc.codeSize = blobSize; |
| desc.bindingMapBytes = bindingMapBytes; |
| desc.bindingMapSize = bindingMapSize; |
| out->module = oreCtx->makeShaderModule(desc); |
| } |
| } |
| // Single combined blob (MSL, WGSL, SPIR-V). |
| else |
| { |
| ShaderModuleDesc desc; |
| desc.code = blobData; |
| desc.codeSize = blobSize; |
| desc.bindingMapBytes = bindingMapBytes; |
| desc.bindingMapSize = bindingMapSize; |
| out->module = oreCtx->makeShaderModule(desc); |
| } |
| |
| // Propagate texture-sampler pairs from the RSTB. |
| if (out->module) |
| { |
| auto pairs = fileAsset->textureSamplerPairs(); |
| if (!pairs.empty()) |
| { |
| std::vector<ShaderModule::TextureSamplerPair> vec; |
| vec.reserve(pairs.size()); |
| for (size_t i = 0; i < pairs.size(); i++) |
| { |
| vec.push_back({pairs[i].texGroup, |
| pairs[i].texBinding, |
| pairs[i].sampGroup, |
| pairs[i].sampBinding}); |
| } |
| out->module->m_textureSamplerPairs = vec; |
| if (out->fragmentModule) |
| out->fragmentModule->m_textureSamplerPairs = vec; |
| } |
| } |
| |
| return out->module != nullptr; |
| } |
| } |
| |
| return false; |
| } |
| |
| int lua_gpu_push_shader_by_name(lua_State* L, const char* name) |
| { |
| auto* context = static_cast<ScriptingContext*>(lua_getthreaddata(L)); |
| auto* scripted = lua_newrive<ScriptedShader>(L); |
| if (!lua_gpu_load_shader_by_name(scripted, context, name, nullptr)) |
| { |
| lua_pop(L, 1); |
| return 0; |
| } |
| return 1; |
| } |
| |
| // Optional WGSL→native compiler, set by the host via |
| // luaopen_rive_gpu_set_wgsl_compiler(). When set, shader_construct() routes |
| // WGSL source through this function before passing to makeShaderModule(). |
| // Returns true and fills |outSource| on success; returns false and fills |
| // |outError| on failure. Not set in Flutter runtime builds (shaders arrive |
| // as pre-compiled backend source from ShaderAsset). |
| #ifdef WITH_RIVE_TOOLS |
| static bool (*g_wgslCompilerFn)(const char* wgsl, |
| uint32_t len, |
| std::string* outSource, |
| std::string* outError) = nullptr; |
| |
| void luaopen_rive_gpu_set_wgsl_compiler( |
| bool (*fn)(const char*, uint32_t, std::string*, std::string*)) |
| { |
| g_wgslCompilerFn = fn; |
| } |
| #endif |
| |
| static int shader_construct(lua_State* L) |
| { |
| // Argument 1 is the name of a pre-compiled WGSL shader asset that was |
| // bundled with the Rive file (set via the "wgslAssetName" field in the |
| // editor). Raw WGSL source strings are NOT accepted at runtime; shaders |
| // must be pre-compiled and embedded as assets. |
| const char* name = luaL_checkstring(L, 1); |
| if (lua_gpu_push_shader_by_name(L, name) == 0) |
| { |
| luaL_error(L, "Shader.new: no shader asset named '%s' found", name); |
| } |
| return 1; |
| } |
| |
| // ============================================================================ |
| // GPUBuffer |
| // ============================================================================ |
| |
| // `GPUBuffer.new(size, usage [, options])` where `options` is an optional |
| // table of: |
| // - `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. |
| static int gpubuffer_construct(lua_State* L) |
| { |
| if (getOreContext(L) == nullptr) |
| { |
| luaL_error(L, "GPU context not initialized"); |
| } |
| |
| uint32_t size = static_cast<uint32_t>(luaL_checkunsigned(L, 1)); |
| BufferUsage usage = lua_tobufferusage(L, 2); |
| |
| BufferDesc desc; |
| desc.size = size; |
| desc.usage = usage; |
| |
| // Optional `options` table with initial data + immutable flag. |
| if (lua_istable(L, 3)) |
| { |
| int optsIdx = 3; |
| desc.immutable = |
| lua_getoptionalboolfield(L, optsIdx, "immutable", false); |
| |
| lua_getfield(L, optsIdx, "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' option must be a Luau " |
| "buffer"); |
| } |
| if (dataLen != size) |
| { |
| luaL_error(L, |
| "GPUBuffer.new: data length (%zu) must equal " |
| "size (%u)", |
| dataLen, |
| size); |
| } |
| desc.data = dataPtr; |
| } |
| lua_pop(L, 1); |
| |
| if (desc.immutable && desc.data == nullptr) |
| { |
| luaL_error(L, |
| "GPUBuffer.new: immutable=true requires 'data' " |
| "option (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 int gpubuffer_index(lua_State* L) |
| { |
| auto* self = lua_torive<ScriptedGPUBuffer>(L, 1); |
| const char* key = luaL_checkstring(L, 2); |
| if (strcmp(key, "size") == 0) |
| { |
| lua_pushnumber(L, self->buffer->size()); |
| return 1; |
| } |
| luaL_error(L, "%s is not a valid property 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); |
| 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 int gputexture_index(lua_State* L) |
| { |
| auto* self = lua_torive<ScriptedGPUTexture>(L, 1); |
| const char* key = luaL_checkstring(L, 2); |
| if (strcmp(key, "width") == 0) |
| { |
| lua_pushnumber(L, self->texture->width()); |
| return 1; |
| } |
| if (strcmp(key, "height") == 0) |
| { |
| lua_pushnumber(L, self->texture->height()); |
| return 1; |
| } |
| if (strcmp(key, "format") == 0) |
| { |
| lua_pushstring(L, lua_totextureformatstring(self->texture->format())); |
| return 1; |
| } |
| luaL_error(L, "%s is not a valid property of GPUTexture", key); |
| return 0; |
| } |
| |
| static int gputextureview_index(lua_State* L) |
| { |
| auto* self = lua_torive<ScriptedGPUTextureView>(L, 1); |
| const char* key = luaL_checkstring(L, 2); |
| if (strcmp(key, "format") == 0) |
| { |
| 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 property 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() |
| { |
| // If the BindGroup has a context, defer its destruction until after |
| // endFrame() so the GPU is done with any command buffers referencing it. |
| // The rcp<> is moved into the context's deferred queue, keeping the |
| // BindGroup alive. When endFrame() clears the queue, refcount drops |
| // to zero and onRefCntReachedZero() does a simple delete. |
| if (bindGroup && bindGroup->context()) |
| { |
| bindGroup->context()->deferBindGroupDestroy(std::move(bindGroup)); |
| return; |
| } |
| // No context (or null) — drop immediately; rcp destructor handles it. |
| } |
| |
| // ============================================================================ |
| // 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->module) |
| 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 |
| // ============================================================================ |
| |
| 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 shader (required) |
| lua_getfield(L, descIdx, "vertex"); |
| auto* vs = lua_torive<ScriptedShader>(L, -1); |
| desc.vertexModule = vs->vertexMod(); |
| lua_pop(L, 1); |
| |
| // fragment shader. Three cases: |
| // * explicit `fragment` Shader → use its fragment module. |
| // * absent + at least one colorTarget → fall back to vs's fragment |
| // module (combined-shader file with both vs_main and fs_main). |
| // * absent + no colorTargets → depth-only pipeline, no fragment. |
| // Case 3 resolution is deferred until after colorTargets is parsed. |
| bool explicitFragment = false; |
| lua_getfield(L, descIdx, "fragment"); |
| if (!lua_isnil(L, -1)) |
| { |
| auto* fs = lua_torive<ScriptedShader>(L, -1); |
| desc.fragmentModule = fs->fragmentMod(); |
| 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 fragment module (combined file). |
| if (!explicitFragment && desc.colorCount > 0) |
| desc.fragmentModule = vs->fragmentMod(); |
| |
| // 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 = vs->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, |
| vs->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) |
| { |
| if (self->m_finished || self->pass == nullptr) |
| { |
| luaL_error(L, "render pass expired — call finish() only once"); |
| } |
| } |
| |
| 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 (= ascending `@binding` per RFC §3.6). |
| // 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 (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) |
| 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() |
| { |
| delete m_activePass; |
| m_activePass = nullptr; |
| if (m_L != nullptr && m_imageRef != LUA_NOREF) |
| { |
| lua_unref(m_L, m_imageRef); |
| } |
| } |
| |
| ScriptedGPURenderPass::~ScriptedGPURenderPass() |
| { |
| if (m_L != nullptr && m_canvasRef != LUA_NOREF) |
| { |
| lua_unref(m_L, m_canvasRef); |
| } |
| } |
| |
| 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; |
| } |
| |
| static int gpucanvashandle_beginrenderpass(lua_State* L) |
| { |
| auto* self = lua_torive<ScriptedGPUCanvas>(L, 1); |
| if (!self->oreColorView) |
| { |
| luaL_error(L, "GPUCanvas has no color view"); |
| } |
| if (getOreContext(L) == nullptr) |
| { |
| luaL_error(L, "GPU context not initialized"); |
| } |
| |
| RenderPassDesc passDesc{}; |
| passDesc.colorCount = 1; |
| // Default: if the canvas has an MSAA color texture, render into it and |
| // resolve to the 1× platform backing; otherwise render directly to it. |
| if (self->oreMSAAColorView) |
| { |
| passDesc.colorAttachments[0].view = self->oreMSAAColorView.get(); |
| passDesc.colorAttachments[0].resolveTarget = self->oreColorView.get(); |
| passDesc.colorAttachments[0].storeOp = StoreOp::discard; |
| } |
| else |
| { |
| passDesc.colorAttachments[0].view = self->oreColorView.get(); |
| } |
| passDesc.colorAttachments[0].loadOp = LoadOp::clear; |
| |
| uint32_t passSampleCount = |
| self->oreMSAAColorView ? self->oreMSAAColorTexture->sampleCount() : 1; |
| |
| // Parse optional descriptor table |
| if (lua_gettop(L) >= 2 && lua_istable(L, 2)) |
| { |
| lua_getfield(L, 2, "color"); |
| if (lua_isnil(L, -1)) |
| { |
| // Descriptor provided but no 'color' key — caller wants a |
| // depth-only pass (e.g. shadow map). Drop the default canvas |
| // color attachment so the render pass matches pipelines that |
| // have no color outputs. |
| passDesc.colorCount = 0; |
| // Fall through to the lua_pop(L, 1) below (don't pop here). |
| } |
| else if (lua_istable(L, -1)) |
| { |
| // color is an array of attachment descriptor tables. Each entry |
| // Each entry is an attachment descriptor table. Supported fields: |
| // view: GPUTextureView? — overrides the default canvas view |
| // resolveTarget: GPUTextureView? — 1× resolve (for MSAA) |
| // loadOp: LoadOp? |
| // storeOp: StoreOp? — use 'discard' on MSAA color |
| // clearColor: {r, g, b, a}? |
| passDesc.colorCount = 0; |
| 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++; |
| |
| // Optional explicit TextureView override |
| lua_getfield(L, -1, "view"); |
| if (!lua_isnil(L, -1)) |
| { |
| auto* tv = lua_torive<ScriptedGPUTextureView>(L, -1); |
| if (tv && tv->view) |
| { |
| passDesc.colorAttachments[slot].view = tv->view.get(); |
| if (slot == 0) |
| passSampleCount = |
| tv->view->texture()->sampleCount(); |
| } |
| } |
| lua_pop(L, 1); // view |
| |
| // Default view for slot 0 is the canvas itself; other slots |
| // require an explicit view from the descriptor. This |
| // mirrors the depthStencil-attachment shape below — both |
| // depth and color slot 1+ surface a clear error when no |
| // view is supplied, so a malformed `colorAttachments` |
| // table can't silently truncate the pass to fewer |
| // attachments than declared. |
| if (passDesc.colorAttachments[slot].view == nullptr) |
| { |
| if (slot == 0) |
| { |
| passDesc.colorAttachments[0].view = |
| self->oreColorView.get(); |
| } |
| else |
| { |
| luaL_error(L, |
| "beginRenderPass: colorAttachments[%d] " |
| "has no `view` field — slot %d requires " |
| "an explicit GPUTextureView (only slot 0 " |
| "defaults to the canvas color view)", |
| slot + 1, |
| slot); |
| } |
| } |
| |
| // resolveTarget (optional — for MSAA) |
| lua_getfield(L, -1, "resolveTarget"); |
| if (!lua_isnil(L, -1)) |
| { |
| auto* tv = lua_torive<ScriptedGPUTextureView>(L, -1); |
| if (tv && tv->view) |
| { |
| // Validate: resolveTarget format must match the MSAA |
| // attachment format. Metal/Vulkan/D3D all require this; |
| // mismatches produce silent corruption (channel swaps, |
| // broken alpha). bgra8 canvas vs rgba8 MSAA is the |
| // classic footgun — catch it here. |
| auto* msaaTex = passDesc.colorAttachments[slot].view |
| ? passDesc.colorAttachments[slot] |
| .view->texture() |
| : nullptr; |
| if (msaaTex && |
| tv->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( |
| tv->view->texture()->format()), |
| lua_totextureformatstring(msaaTex->format())); |
| } |
| passDesc.colorAttachments[slot].resolveTarget = |
| tv->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)); |
| } |
| 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 |
| } |
| // If no explicit entries were found, fall back to the canvas view |
| if (passDesc.colorCount == 0) |
| { |
| passDesc.colorCount = 1; |
| passDesc.colorAttachments[0].view = self->oreColorView.get(); |
| } |
| } |
| lua_pop(L, 1); // color |
| |
| lua_getfield(L, 2, "depthStencil"); |
| if (lua_istable(L, -1)) |
| { |
| // Prefer an explicit view from the descriptor; fall back to the |
| // canvas's own depth view if available. |
| lua_getfield(L, -1, "view"); |
| if (!lua_isnil(L, -1)) |
| { |
| auto* tv = lua_torive<ScriptedGPUTextureView>(L, -1); |
| if (tv && tv->view) |
| passDesc.depthStencil.view = tv->view.get(); |
| } |
| else if (self->oreDepthView) |
| { |
| passDesc.depthStencil.view = self->oreDepthView.get(); |
| } |
| lua_pop(L, 1); // view |
| |
| // Require an explicit view, matching WebGPU validation. Without |
| // one, GL silently drops depth testing while Metal/TBDR implicitly |
| // allocates tile memory — making the same script behave differently |
| // across backends. |
| if (!passDesc.depthStencil.view) |
| { |
| luaL_error(L, |
| "beginRenderPass: depthStencil.view is required — " |
| "pass GPUTexture:view()"); |
| } |
| |
| if (passDesc.depthStencil.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 |
| } |
| |
| // Delete any previous active pass (ore::RenderPass destructor handles |
| // cleanup) |
| // Metal (and other backends) only allow one active encoder per command |
| // buffer. If another canvas handle (or another VM sharing the same |
| // oreCtx) left a pass open, finish it before opening a new encoder. |
| Context* oreCtx = getOreContext(L); |
| if (oreCtx->activeRenderPass() && !oreCtx->activeRenderPass()->isFinished()) |
| { |
| oreCtx->activeRenderPass()->finish(); |
| } |
| |
| delete self->m_activePass; |
| self->m_activePass = nullptr; |
| |
| ore::RenderPass raw = oreCtx->beginRenderPass(passDesc); |
| self->m_activePass = new ore::RenderPass(std::move(raw)); |
| self->m_activePassLabel = passDesc.label ? passDesc.label : ""; |
| oreCtx->setActiveRenderPass(self->m_activePass); |
| |
| auto* rp = lua_newrive<ScriptedGPURenderPass>(L); |
| rp->pass = self->m_activePass; |
| rp->m_finished = false; |
| rp->sampleCount = passSampleCount; |
| rp->label = self->m_activePassLabel; |
| rp->drawCallCount = 0; |
| // Hold a Lua ref to the owning canvas so it can't be GC'd while the |
| // pass userdata is reachable. Without this, `~ScriptedGPUCanvas` |
| // deletes `m_activePass` (which `rp->pass` aliases) and any |
| // subsequent method on the pass dereferences a freed pointer. |
| rp->m_L = L; |
| lua_pushvalue(L, 1); // canvas userdata is the `self` argument. |
| rp->m_canvasRef = lua_ref(L, -1); |
| lua_pop(L, 1); |
| 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. |
| 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"); |
| } |
| |
| 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"); |
| } |
| |
| // Update the image ref to point to the new canvas |
| 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); |
| |
| // Rebuild MSAA color texture if canvas was created with sampleCount > 1. |
| if (self->oreMSAAColorTexture) |
| { |
| uint32_t sc = self->oreMSAAColorTexture->sampleCount(); |
| ore::TextureDesc msaaDesc; |
| msaaDesc.width = w; |
| msaaDesc.height = h; |
| msaaDesc.format = self->oreColorView->texture()->format(); |
| msaaDesc.renderTarget = true; |
| msaaDesc.sampleCount = sc; |
| msaaDesc.label = "GPUCanvasMSAAColor"; |
| self->oreMSAAColorTexture = oreCtx->makeTexture(msaaDesc); |
| ore::TextureViewDesc msaaViewDesc; |
| msaaViewDesc.texture = self->oreMSAAColorTexture.get(); |
| self->oreMSAAColorView = oreCtx->makeTextureView(msaaViewDesc); |
| |
| ore::TextureDesc depthDesc; |
| depthDesc.width = w; |
| depthDesc.height = h; |
| depthDesc.format = ore::TextureFormat::depth32float; |
| depthDesc.renderTarget = true; |
| depthDesc.sampleCount = sc; |
| depthDesc.label = "GPUCanvasMSAADepth"; |
| self->oreDepthTexture = oreCtx->makeTexture(depthDesc); |
| ore::TextureViewDesc depthViewDesc; |
| depthViewDesc.texture = self->oreDepthTexture.get(); |
| self->oreDepthView = oreCtx->makeTextureView(depthViewDesc); |
| } |
| else |
| { |
| self->oreMSAAColorTexture = nullptr; |
| self->oreMSAAColorView = nullptr; |
| self->oreDepthTexture = nullptr; |
| self->oreDepthView = nullptr; |
| } |
| |
| 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) |
| { |
| lua_pushnil(L); |
| return 1; |
| } |
| auto* tv = lua_newrive<ScriptedGPUTextureView>(L); |
| tv->view = self->oreColorView; |
| return 1; |
| } |
| |
| static int gpucanvashandle_index(lua_State* L) |
| { |
| auto* self = lua_torive<ScriptedGPUCanvas>(L, 1); |
| const char* key = luaL_checkstring(L, 2); |
| if (strcmp(key, "image") == 0) |
| { |
| if (self->m_imageRef != LUA_NOREF) |
| { |
| rive_lua_pushRef(L, self->m_imageRef); |
| return 1; |
| } |
| lua_pushnil(L); |
| return 1; |
| } |
| if (strcmp(key, "width") == 0 && self->canvas) |
| { |
| lua_pushnumber(L, self->canvas->width()); |
| return 1; |
| } |
| if (strcmp(key, "height") == 0 && self->canvas) |
| { |
| lua_pushnumber(L, self->canvas->height()); |
| return 1; |
| } |
| if (strcmp(key, "sampleCount") == 0) |
| { |
| uint32_t sc = self->oreMSAAColorTexture |
| ? self->oreMSAAColorTexture->sampleCount() |
| : 1; |
| lua_pushnumber(L, sc); |
| return 1; |
| } |
| if (strcmp(key, "hasDepth") == 0) |
| { |
| lua_pushboolean(L, self->oreDepthView != nullptr); |
| return 1; |
| } |
| if (strcmp(key, "format") == 0) |
| { |
| if (self->oreColorView && self->oreColorView->texture()) |
| lua_pushstring(L, |
| lua_totextureformatstring( |
| self->oreColorView->texture()->format())); |
| else |
| lua_pushnil(L); |
| return 1; |
| } |
| luaL_error(L, "%s is not a valid property 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::beginRenderPass: |
| return gpucanvashandle_beginrenderpass(L); |
| case (int)LuaAtoms::colorView: |
| return gpucanvashandle_colorview(L); |
| case (int)LuaAtoms::resize: |
| return gpucanvashandle_resize(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(). |
| 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"); |
| } |
| |
| 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"); |
| } |
| if (self->m_state != CanvasState::Idle) |
| { |
| luaL_error(L, "Canvas:beginFrame() called during an active frame"); |
| } |
| if (!self->canvas) |
| { |
| luaL_error(L, "Canvas:beginFrame() — canvas is null"); |
| } |
| |
| 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 int canvashandle_index(lua_State* L) |
| { |
| auto* self = lua_torive<ScriptedCanvas>(L, 1); |
| const char* key = luaL_checkstring(L, 2); |
| if (strcmp(key, "image") == 0) |
| { |
| if (self->m_imageRef != LUA_NOREF) |
| { |
| rive_lua_pushRef(L, self->m_imageRef); |
| return 1; |
| } |
| lua_pushnil(L); |
| return 1; |
| } |
| if (strcmp(key, "width") == 0 && self->canvas) |
| { |
| lua_pushnumber(L, self->canvas->width()); |
| return 1; |
| } |
| if (strcmp(key, "height") == 0 && self->canvas) |
| { |
| lua_pushnumber(L, self->canvas->height()); |
| return 1; |
| } |
| luaL_error(L, "%s is not a valid property 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) |
| { |
| static const luaL_Reg shaderStatics[] = {{"new", shader_construct}, |
| {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); |
| } |
| |
| // 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); |
| } |
| |
| // 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); |
| } |
| |
| // 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); |
| } |
| |
| 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; |
| } |
| |
| #endif // RIVE_CANVAS && RIVE_ORE |
| #endif // WITH_RIVE_SCRIPTING |