feat(scripting): 3D Vector ops, Mat4 lookAt/ortho, Vector buffer writes, GPUBuffer write source range (#13003) ef0e100413 * feat(scripting): 3D Vector ops, Mat4 lookAt/ortho, Vector buffer writes, GPUBuffer write source range * test(scripting): pin Vector fastcall and C binding paths bit-identical, align lerp endpoint Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head index bbf97f1..a0c0b4f 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -eeb280d7f906f2e47bb77c02600a301dda7e600d +ef0e10041348832ffb0d398b9bb20df97a1d00b8
diff --git a/include/rive/lua/rive_lua_libs.hpp b/include/rive/lua/rive_lua_libs.hpp index e9d7209..eb838ab 100644 --- a/include/rive/lua/rive_lua_libs.hpp +++ b/include/rive/lua/rive_lua_libs.hpp
@@ -347,6 +347,9 @@ writeToBuffer, invertAffine, + // Vector + writeVec4, + // Gamepad axes, gamepadMapping,
diff --git a/include/rive/math/mat4.hpp b/include/rive/math/mat4.hpp index 341ab2b..26d5212 100644 --- a/include/rive/math/mat4.hpp +++ b/include/rive/math/mat4.hpp
@@ -178,6 +178,73 @@ return m; } + // Right-handed view matrix looking from `eye` toward `center` (GLM + // convention). `up` need not be normalized. + static Mat4 lookAt(const float eye[3], + const float center[3], + const float up[3]) + { + float f[3] = {center[0] - eye[0], + center[1] - eye[1], + center[2] - eye[2]}; + float fInv = 1.f / std::sqrt(f[0] * f[0] + f[1] * f[1] + f[2] * f[2]); + f[0] *= fInv; + f[1] *= fInv; + f[2] *= fInv; + float s[3] = {f[1] * up[2] - f[2] * up[1], + f[2] * up[0] - f[0] * up[2], + f[0] * up[1] - f[1] * up[0]}; + float sInv = 1.f / std::sqrt(s[0] * s[0] + s[1] * s[1] + s[2] * s[2]); + s[0] *= sInv; + s[1] *= sInv; + s[2] *= sInv; + float u[3] = {s[1] * f[2] - s[2] * f[1], + s[2] * f[0] - s[0] * f[2], + s[0] * f[1] - s[1] * f[0]}; + Mat4 m; + m.m_buffer[0] = s[0]; + m.m_buffer[1] = u[0]; + m.m_buffer[2] = -f[0]; + m.m_buffer[4] = s[1]; + m.m_buffer[5] = u[1]; + m.m_buffer[6] = -f[1]; + m.m_buffer[8] = s[2]; + m.m_buffer[9] = u[2]; + m.m_buffer[10] = -f[2]; + m.m_buffer[12] = -(s[0] * eye[0] + s[1] * eye[1] + s[2] * eye[2]); + m.m_buffer[13] = -(u[0] * eye[0] + u[1] * eye[1] + u[2] * eye[2]); + m.m_buffer[14] = f[0] * eye[0] + f[1] * eye[1] + f[2] * eye[2]; + return m; + } + + // Right-handed orthographic projection. Maps view-space z=[-near, -far] + // to NDC z in either [0, 1] (default, depthZeroToOne=true) or [-1, 1]. + static Mat4 ortho(float left, + float right, + float bottom, + float top, + float near_, + float far_, + bool depthZeroToOne = true) + { + Mat4 m; + m.m_buffer[0] = 2.f / (right - left); + m.m_buffer[5] = 2.f / (top - bottom); + m.m_buffer[12] = -(right + left) / (right - left); + m.m_buffer[13] = -(top + bottom) / (top - bottom); + if (depthZeroToOne) + { + m.m_buffer[10] = -1.f / (far_ - near_); + m.m_buffer[14] = -near_ / (far_ - near_); + } + else + { + m.m_buffer[10] = -2.f / (far_ - near_); + m.m_buffer[14] = -(far_ + near_) / (far_ - near_); + } + return m; + } + // SIMD: out = lhs * rhs. Both column-major. static Mat4 multiply(const Mat4& lhs, const Mat4& rhs) {
diff --git a/scripting/premake5.lua b/scripting/premake5.lua index 4c049ce..ce47969 100755 --- a/scripting/premake5.lua +++ b/scripting/premake5.lua
@@ -1,5 +1,5 @@ local dependency = require('dependency') -local luau = dependency.github('luigi-rosso/luau', 'rive_0_728') +local luau = dependency.github('luigi-rosso/luau', 'rive_0_728_vec3') local libhydrogen = dependency.github('luigi-rosso/libhydrogen', 'rive_0_2') dofile('rive_build_config.lua')
diff --git a/src/lua/math/lua_mat4.cpp b/src/lua/math/lua_mat4.cpp index ca764f4..8527655 100644 --- a/src/lua/math/lua_mat4.cpp +++ b/src/lua/math/lua_mat4.cpp
@@ -90,6 +90,27 @@ return 1; } +static int mat4_lookAt(lua_State* L) +{ + const float* eye = luaL_checkvector(L, 1); + const float* center = luaL_checkvector(L, 2); + const float* up = luaL_checkvector(L, 3); + lua_pushmat4(L, Mat4::lookAt(eye, center, up)); + return 1; +} + +static int mat4_ortho(lua_State* L) +{ + float l = float(luaL_checknumber(L, 1)); + float r = float(luaL_checknumber(L, 2)); + float b = float(luaL_checknumber(L, 3)); + float t = float(luaL_checknumber(L, 4)); + float n = float(luaL_checknumber(L, 5)); + float f = float(luaL_checknumber(L, 6)); + lua_pushmat4(L, Mat4::ortho(l, r, b, t, n, f, /*zeroToOne=*/true)); + return 1; +} + // In-place: Mat4.multiply(out, a, b) -> out = a * b. Returns out. // Avoids per-call userdata allocation in tight loops. static int mat4_static_multiply(lua_State* L) @@ -379,6 +400,8 @@ {"fromRotationZ", mat4_fromRotationZ}, {"perspective", mat4_perspective}, {"perspectiveReverseZ", mat4_perspectiveReverseZ}, + {"lookAt", mat4_lookAt}, + {"ortho", mat4_ortho}, {"multiply", mat4_static_multiply}, {"multiplyAffine", mat4_static_multiplyAffine}, {"invert", mat4_static_invert},
diff --git a/src/lua/math/lua_vec2d.cpp b/src/lua/math/lua_vec2d.cpp index d9cdd01..a38b0f3 100644 --- a/src/lua/math/lua_vec2d.cpp +++ b/src/lua/math/lua_vec2d.cpp
@@ -2,6 +2,8 @@ #ifdef WITH_RIVE_SCRIPTING #include "rive/lua/rive_lua_libs.hpp" #include "lualib.h" +#include <cmath> +#include <cstring> using namespace rive; @@ -40,49 +42,59 @@ return 0; } +// All magnitude and interpolation ops read all 3 components. For genuine 2D +// vectors z is 0, so results are identical to the old 2D math. +static inline float dot3(const float* a, const float* b) +{ + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + static int vector_length(lua_State* L) { - auto vec = lua_checkvec2d(L, 1); - lua_pushnumber(L, vec->length()); + const float* vec = luaL_checkvector(L, 1); + lua_pushnumber(L, std::sqrt(dot3(vec, vec))); return 1; } static int vector_normalized(lua_State* L) { - auto vec = lua_checkvec2d(L, 1); - lua_pushvec2d(L, vec->normalized()); + const float* vec = luaL_checkvector(L, 1); + float len2 = dot3(vec, vec); + float scale = len2 > 0 ? 1.f / std::sqrt(len2) : 1.f; + lua_pushvector(L, vec[0] * scale, vec[1] * scale, vec[2] * scale); return 1; } static int vector_lengthSquared(lua_State* L) { - auto vec = lua_checkvec2d(L, 1); - lua_pushnumber(L, vec->lengthSquared()); - return 1; -} - -static int vector_distance(lua_State* L) -{ - auto lhs = lua_checkvec2d(L, 1); - auto rhs = lua_checkvec2d(L, 2); - lua_pushnumber(L, Vec2D::distance(*lhs, *rhs)); + const float* vec = luaL_checkvector(L, 1); + lua_pushnumber(L, dot3(vec, vec)); return 1; } static int vector_distanceSquared(lua_State* L) { - auto lhs = lua_checkvec2d(L, 1); - auto rhs = lua_checkvec2d(L, 2); - lua_pushnumber(L, Vec2D::distanceSquared(*lhs, *rhs)); + const float* lhs = luaL_checkvector(L, 1); + const float* rhs = luaL_checkvector(L, 2); + float d[3] = {rhs[0] - lhs[0], rhs[1] - lhs[1], rhs[2] - lhs[2]}; + lua_pushnumber(L, dot3(d, d)); + return 1; +} + +static int vector_distance(lua_State* L) +{ + const float* lhs = luaL_checkvector(L, 1); + const float* rhs = luaL_checkvector(L, 2); + float d[3] = {rhs[0] - lhs[0], rhs[1] - lhs[1], rhs[2] - lhs[2]}; + lua_pushnumber(L, std::sqrt(dot3(d, d))); return 1; } static int vector_dot(lua_State* L) { - auto lhs = lua_checkvec2d(L, 1); - auto rhs = lua_checkvec2d(L, 2); - - lua_pushnumber(L, Vec2D::dot(*lhs, *rhs)); + const float* lhs = luaL_checkvector(L, 1); + const float* rhs = luaL_checkvector(L, 2); + lua_pushnumber(L, dot3(lhs, rhs)); return 1; } @@ -94,33 +106,98 @@ return 1; } +// 3D cross product. Distinct from `cross`, which returns the scalar 2D +// perp-dot. +static int vector_cross3(lua_State* L) +{ + const float* a = luaL_checkvector(L, 1); + const float* b = luaL_checkvector(L, 2); + lua_pushvector(L, + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0]); + return 1; +} + static int vector_scaleAndAdd(lua_State* L) { - auto a = lua_checkvec2d(L, 1); - auto b = lua_checkvec2d(L, 2); + const float* a = luaL_checkvector(L, 1); + const float* b = luaL_checkvector(L, 2); float scale = float(luaL_checknumber(L, 3)); - lua_pushvec2d(L, Vec2D::scaleAndAdd(*a, *b, scale)); + lua_pushvector(L, + a[0] + b[0] * scale, + a[1] + b[1] * scale, + a[2] + b[2] * scale); return 1; } static int vector_scaleAndSub(lua_State* L) { - auto a = lua_checkvec2d(L, 1); - auto b = lua_checkvec2d(L, 2); + const float* a = luaL_checkvector(L, 1); + const float* b = luaL_checkvector(L, 2); float scale = float(luaL_checknumber(L, 3)); - lua_pushvec2d(L, *a - *b * scale); + lua_pushvector(L, + a[0] - b[0] * scale, + a[1] - b[1] * scale, + a[2] - b[2] * scale); return 1; } +// Exact at t=1, mirroring the VM fastcall's luai_lerpf so both dispatch +// paths return bit-identical results. +static inline float lerpf(float a, float b, float t) +{ + return t == 1.f ? b : a + (b - a) * t; +} + static int vector_lerp(lua_State* L) { - auto lhs = lua_checkvec2d(L, 1); - auto rhs = lua_checkvec2d(L, 2); - float factor = float(luaL_checknumber(L, 3)); - lua_pushvec2d(L, Vec2D::lerp(*lhs, *rhs, factor)); + const float* a = luaL_checkvector(L, 1); + const float* b = luaL_checkvector(L, 2); + float t = float(luaL_checknumber(L, 3)); + lua_pushvector(L, + lerpf(a[0], b[0], t), + lerpf(a[1], b[1], t), + lerpf(a[2], b[2], t)); return 1; } +// vec:writeToBuffer(buf, byteOffset) — 12 bytes (x, y, z as float32). +static int vector_writeToBuffer(lua_State* L) +{ + const float* vec = luaL_checkvector(L, 1); + size_t bufLen = 0; + void* buf = luaL_checkbuffer(L, 2, &bufLen); + int off = int(luaL_checkinteger(L, 3)); + if (off < 0 || size_t(off) + 12 > bufLen) + { + luaL_error(L, "Vector:writeToBuffer offset out of range"); + return 0; + } + std::memcpy(static_cast<uint8_t*>(buf) + off, vec, 12); + return 0; +} + +// vec:writeVec4(buf, byteOffset, w) — 16 bytes (x, y, z, w as float32), +// matching a vec4 uniform-buffer slot. +static int vector_writeVec4(lua_State* L) +{ + const float* vec = luaL_checkvector(L, 1); + size_t bufLen = 0; + void* buf = luaL_checkbuffer(L, 2, &bufLen); + int off = int(luaL_checkinteger(L, 3)); + float w = float(luaL_checknumber(L, 4)); + if (off < 0 || size_t(off) + 16 > bufLen) + { + luaL_error(L, "Vector:writeVec4 offset out of range"); + return 0; + } + uint8_t* dst = static_cast<uint8_t*>(buf) + off; + std::memcpy(dst, vec, 12); + std::memcpy(dst + 12, &w, 4); + return 0; +} + static int vector_xy(lua_State* L) { float x = (float)lua_tonumber(L, 1); @@ -130,6 +207,16 @@ return 1; } +static int vector_xyz(lua_State* L) +{ + float x = (float)lua_tonumber(L, 1); + float y = (float)lua_tonumber(L, 2); + float z = (float)lua_tonumber(L, 3); + + lua_pushvector(L, x, y, z); + return 1; +} + static int vector_origin(lua_State* L) { lua_pushvector2(L, 0.0f, 0.0f); @@ -158,6 +245,10 @@ return vector_dot(L); case (int)LuaAtoms::lerp: return vector_lerp(L); + case (int)LuaAtoms::writeToBuffer: + return vector_writeToBuffer(L); + case (int)LuaAtoms::writeVec4: + return vector_writeVec4(L); } } @@ -170,10 +261,12 @@ {"distanceSquared", vector_distanceSquared}, {"dot", vector_dot}, {"cross", vector_cross}, + {"cross3", vector_cross3}, {"scaleAndAdd", vector_scaleAndAdd}, {"scaleAndSub", vector_scaleAndSub}, {"lerp", vector_lerp}, {"xy", vector_xy}, + {"xyz", vector_xyz}, {"origin", vector_origin}, {"length", vector_length}, {"lengthSquared", vector_lengthSquared},
diff --git a/src/lua/renderer/lua_gpu.cpp b/src/lua/renderer/lua_gpu.cpp index e82cb4d..a32069b 100644 --- a/src/lua/renderer/lua_gpu.cpp +++ b/src/lua/renderer/lua_gpu.cpp
@@ -873,21 +873,45 @@ { luaL_typeerror(L, 2, "buffer"); } - uint32_t offset = + 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 (offset + len > self->buffer->size()) + 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)", - offset, - (uint32_t)len, - (uint32_t)(offset + len), + dstOffset, + byteLength, + (uint32_t)(dstOffset + byteLength), self->buffer->size()); } - self->buffer->update(data, static_cast<uint32_t>(len), offset); + self->buffer->update(static_cast<const uint8_t*>(data) + srcOffset, + byteLength, + dstOffset); return 0; }
diff --git a/src/lua/rive_lua_libs.cpp b/src/lua/rive_lua_libs.cpp index 2687043..b7356b0 100644 --- a/src/lua/rive_lua_libs.cpp +++ b/src/lua/rive_lua_libs.cpp
@@ -295,6 +295,8 @@ {"transformVec4", (int16_t)LuaAtoms::transformVec4}, {"writeToBuffer", (int16_t)LuaAtoms::writeToBuffer}, {"invertAffine", (int16_t)LuaAtoms::invertAffine}, + // Vector + {"writeVec4", (int16_t)LuaAtoms::writeVec4}, }; static const luaL_Reg lualibs[] = {
diff --git a/tests/unit_tests/runtime/scripting/scripting_mat4_test.cpp b/tests/unit_tests/runtime/scripting/scripting_mat4_test.cpp index 0b4fe1f..d72e62c 100644 --- a/tests/unit_tests/runtime/scripting/scripting_mat4_test.cpp +++ b/tests/unit_tests/runtime/scripting/scripting_mat4_test.cpp
@@ -58,6 +58,66 @@ CHECK(lua_tonumber(t.state(), -1) == 33.0); } +TEST_CASE("Mat4.lookAt builds a view matrix", "[scripting]") +{ + // Eye at (0,0,5) looking at the origin: pure -5 z translation. + const char* src = + "local view = Mat4.lookAt(Vector.xyz(0, 0, 5), Vector.origin(),\n" + " Vector.xyz(0, 1, 0))\n" + "local v = view:transformPoint(0, 0, 0)\n" + "return v.x, v.y, v.z, view.m11, view.m22, view.m33\n"; + auto t = ScriptingTest(src, 6); + CHECK(lua_tonumber(t.state(), -6) == 0.0); + CHECK(lua_tonumber(t.state(), -5) == 0.0); + CHECK(lua_tonumber(t.state(), -4) == -5.0); + CHECK(lua_tonumber(t.state(), -3) == 1.0); + CHECK(lua_tonumber(t.state(), -2) == 1.0); + CHECK(lua_tonumber(t.state(), -1) == 1.0); + + // Eye on +x looking at the origin: world +x maps to view -z. + const char* sideSrc = + "local view = Mat4.lookAt(Vector.xyz(5, 0, 0), Vector.origin(),\n" + " Vector.xyz(0, 1, 0))\n" + "local v = view:transformPoint(1, 0, 0)\n" + "return v.x, v.y, v.z\n"; + auto side = ScriptingTest(sideSrc, 3); + CHECK(lua_tonumber(side.state(), -3) == Approx(0.0).margin(1e-6)); + CHECK(lua_tonumber(side.state(), -2) == Approx(0.0).margin(1e-6)); + CHECK(lua_tonumber(side.state(), -1) == Approx(-4.0)); +} + +TEST_CASE("Mat4.ortho maps depth to [0, 1]", "[scripting]") +{ + const char* src = "local proj = Mat4.ortho(-2, 2, -1, 1, 0, 10)\n" + "local near = proj:transformPoint(2, 1, 0)\n" + "local far = proj:transformPoint(-2, -1, -10)\n" + "return near.x, near.y, near.z, far.x, far.y, far.z\n"; + auto t = ScriptingTest(src, 6); + CHECK(lua_tonumber(t.state(), -6) == 1.0); // right -> +1 + CHECK(lua_tonumber(t.state(), -5) == 1.0); // top -> +1 + CHECK(lua_tonumber(t.state(), -4) == 0.0); // near -> 0 + CHECK(lua_tonumber(t.state(), -3) == -1.0); // left -> -1 + CHECK(lua_tonumber(t.state(), -2) == -1.0); // bottom -> -1 + CHECK(lua_tonumber(t.state(), -1) == 1.0); // far -> 1 +} + +TEST_CASE("Mat4 ortho * lookAt round-trips a point", "[scripting]") +{ + // Camera at +z looking at the origin; a point at world z=-5 sits 10 in + // front of the eye, the middle of the [5, 15] depth range. + const char* src = + "local view = Mat4.lookAt(Vector.xyz(0, 0, 5), Vector.origin(),\n" + " Vector.xyz(0, 1, 0))\n" + "local proj = Mat4.ortho(-4, 4, -4, 4, 5, 15)\n" + "local vp = Mat4.multiply(Mat4.identity(), proj, view)\n" + "local v = vp:transformPoint(2, -2, -5)\n" + "return v.x, v.y, v.z\n"; + auto t = ScriptingTest(src, 3); + CHECK(lua_tonumber(t.state(), -3) == Approx(0.5)); + CHECK(lua_tonumber(t.state(), -2) == Approx(-0.5)); + CHECK(lua_tonumber(t.state(), -1) == Approx(0.5)); +} + TEST_CASE("Mat4 transformVec4 returns homogeneous components", "[scripting]") { // No perspective divide: w is preserved as the final return value.
diff --git a/tests/unit_tests/runtime/scripting/scripting_vector_test.cpp b/tests/unit_tests/runtime/scripting/scripting_vector_test.cpp index e553d5a..895df18 100644 --- a/tests/unit_tests/runtime/scripting/scripting_vector_test.cpp +++ b/tests/unit_tests/runtime/scripting/scripting_vector_test.cpp
@@ -139,6 +139,171 @@ -1) == 0.0f); } +TEST_CASE("vector 3D constructor and cross3 work", "[scripting]") +{ + auto t = ScriptingTest("local v = Vector.xyz(1, 2, 3)\n" + "return v.x, v.y, v.z\n", + 3); + CHECK(lua_tonumber(t.state(), -3) == 1.0f); + CHECK(lua_tonumber(t.state(), -2) == 2.0f); + CHECK(lua_tonumber(t.state(), -1) == 3.0f); + + // x cross y = z + auto c = ScriptingTest( + "local v = Vector.cross3(Vector.xyz(1,0,0), Vector.xyz(0,1,0))\n" + "return v.x, v.y, v.z\n", + 3); + CHECK(lua_tonumber(c.state(), -3) == 0.0f); + CHECK(lua_tonumber(c.state(), -2) == 0.0f); + CHECK(lua_tonumber(c.state(), -1) == 1.0f); + + // y cross x = -z + CHECK(lua_tonumber(ScriptingTest("return Vector.cross3(Vector.xyz(0,1,0), " + "Vector.xyz(1,0,0)).z") + .state(), + -1) == -1.0f); +} + +TEST_CASE("vector magnitude ops use all 3 components", "[scripting]") +{ + CHECK(lua_tonumber( + ScriptingTest("return Vector.length(Vector.xyz(1,2,2))").state(), + -1) == 3.0f); + + CHECK(lua_tonumber( + ScriptingTest("return Vector.lengthSquared(Vector.xyz(1,2,3))") + .state(), + -1) == 14.0f); + + // (1,2,3) . (4,5,6) = 4 + 10 + 18 + CHECK(lua_tonumber(ScriptingTest("return Vector.dot(Vector.xyz(1,2,3), " + "Vector.xyz(4,5,6))") + .state(), + -1) == 32.0f); + + CHECK(lua_tonumber(ScriptingTest("return Vector.distance(" + "Vector.xyz(1,1,1), Vector.xyz(1,1,4))") + .state(), + -1) == 3.0f); + + CHECK(lua_tonumber(ScriptingTest("return Vector.distanceSquared(" + "Vector.xyz(0,0,0), Vector.xyz(1,2,3))") + .state(), + -1) == 14.0f); + + auto n = ScriptingTest("local v = Vector.normalized(Vector.xyz(0,3,4))\n" + "return v.y, v.z\n", + 2); + CHECK(lua_tonumber(n.state(), -2) == Approx(0.6f)); + CHECK(lua_tonumber(n.state(), -1) == Approx(0.8f)); + + // Zero-length normalize stays the zero vector + CHECK(lua_tonumber( + ScriptingTest("return Vector.normalized(Vector.origin()).x") + .state(), + -1) == 0.0f); + + CHECK(lua_tonumber(ScriptingTest("return Vector.lerp(Vector.xyz(0,0,10), " + "Vector.xyz(0,0,20), 0.5).z") + .state(), + -1) == 15.0f); + + CHECK(lua_tonumber( + ScriptingTest("return Vector.scaleAndAdd(Vector.xyz(0,0,1), " + "Vector.xyz(0,0,2), 3).z") + .state(), + -1) == 7.0f); + + CHECK(lua_tonumber( + ScriptingTest("return Vector.scaleAndSub(Vector.xyz(0,0,7), " + "Vector.xyz(0,0,2), 3).z") + .state(), + -1) == 1.0f); +} + +TEST_CASE("vector writeToBuffer and writeVec4 work", "[scripting]") +{ + auto t = ScriptingTest("local buf = buffer.create(16)\n" + "Vector.xyz(1, 2, 3):writeToBuffer(buf, 4)\n" + "return buffer.readf32(buf, 4),\n" + " buffer.readf32(buf, 8),\n" + " buffer.readf32(buf, 12)\n", + 3); + CHECK(lua_tonumber(t.state(), -3) == 1.0f); + CHECK(lua_tonumber(t.state(), -2) == 2.0f); + CHECK(lua_tonumber(t.state(), -1) == 3.0f); + + auto v4 = ScriptingTest("local buf = buffer.create(16)\n" + "Vector.xyz(1, 2, 3):writeVec4(buf, 0, 4)\n" + "return buffer.readf32(buf, 0),\n" + " buffer.readf32(buf, 12)\n", + 2); + CHECK(lua_tonumber(v4.state(), -2) == 1.0f); + CHECK(lua_tonumber(v4.state(), -1) == 4.0f); + + // Out-of-range offsets error rather than corrupt memory + CHECK(!lua_toboolean( + ScriptingTest("local buf = buffer.create(12)\n" + "return (pcall(function()\n" + " Vector.origin():writeToBuffer(buf, 4)\n" + "end))\n") + .state(), + -1)); + + CHECK(!lua_toboolean( + ScriptingTest("local buf = buffer.create(16)\n" + "return (pcall(function()\n" + " Vector.origin():writeVec4(buf, 4, 1)\n" + "end))\n") + .state(), + -1)); +} + +TEST_CASE("vector fastcall and C binding paths agree", "[scripting]") +{ + // Static Vector.op(...) calls compile to Luau-fork fastcalls; v:op(...) + // and indirect calls hit the C bindings in lua_vec2d.cpp. Pins the two + // implementations bit-identical so neither can silently diverge. + const char* src = + "local a = Vector.xyz(1.5, -2.25, 3.75)\n" + "local b = Vector.xyz(-4.5, 5.25, -6.5)\n" + "local function check(name, x, y)\n" + " if x ~= y then\n" + " error(`{name} diverged: {x} vs {y}`)\n" + " end\n" + "end\n" + "-- namecall (C binding) vs static (fastcall)\n" + "check('length', a:length(), Vector.length(a))\n" + "check('lengthSquared', a:lengthSquared(), Vector.lengthSquared(a))\n" + "check('normalized', a:normalized(), Vector.normalized(a))\n" + "check('distance', a:distance(b), Vector.distance(a, b))\n" + "check('distanceSquared', a:distanceSquared(b),\n" + " Vector.distanceSquared(a, b))\n" + "check('dot', a:dot(b), Vector.dot(a, b))\n" + "for _, t in {0, 0.375, 1} do\n" + " check(`lerp t={t}`, a:lerp(b, t), Vector.lerp(a, b, t))\n" + "end\n" + "-- ops with no instance form: indirect call (C binding) vs direct\n" + "-- static (fastcall); a table load can't compile to FASTCALL\n" + "local ind = {\n" + " Vector.scaleAndAdd,\n" + " Vector.scaleAndSub,\n" + " Vector.cross,\n" + " Vector.normalized,\n" + "}\n" + "check('scaleAndAdd', ind[1](a, b, 2.5), Vector.scaleAndAdd(a, b, " + "2.5))\n" + "check('scaleAndSub', ind[2](a, b, 2.5), Vector.scaleAndSub(a, b, " + "2.5))\n" + "check('cross', ind[3](a, b), Vector.cross(a, b))\n" + "-- zero-length normalize stays zero (not NaN) on both paths\n" + "local z = Vector.origin()\n" + "check('normalizedZero', ind[4](z), Vector.normalized(z))\n" + "check('normalizedZeroValue', Vector.normalized(z), z)\n" + "return true\n"; + CHECK(lua_toboolean(ScriptingTest(src).state(), -1)); +} + TEST_CASE("vector indexing work", "[scripting]") { CHECK(lua_tonumber(ScriptingTest("return Vector.xy(19, 27)[1]").state(),