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