From 7c876e4bd44e71a59f4f2d80349ff1797e743201 Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Sat, 13 Dec 2025 04:28:15 +0000 Subject: [PATCH 1/5] Convery globals uuid and quaternion to tables, to match vector's implementation Signed-off-by: WolfGangS --- VM/src/lapi.cpp | 8 +- VM/src/llsl.cpp | 221 +++++++++++++++++++++++++++++++++++-- tests/conformance/uuid.lua | 4 + 3 files changed, 217 insertions(+), 16 deletions(-) diff --git a/VM/src/lapi.cpp b/VM/src/lapi.cpp index 484e3210..639218db 100644 --- a/VM/src/lapi.cpp +++ b/VM/src/lapi.cpp @@ -2092,13 +2092,13 @@ CLANG_NOOPT void GCC_NOOPT lua_fixallcollectable(lua_State *L) if (key_str) { - if (!strcmp(key_str, "vector")) + if (!strcmp(key_str, "vector") || !strcmp(key_str, "quaternion") || !strcmp(key_str, "rotation") || !strcmp(key_str, "uuid")) { - // vector has a special metatable with `__call` which should be fixable. + // vector quaternion rotation and uuid have a special metatable with `__call` which should be fixable. if (ttistable(global_val)) { - LuaTable *vector_mt = hvalue(global_val)->metatable; - ASSERT_IN_DBG(try_fix_table(vector_mt)); + LuaTable *mt = hvalue(global_val)->metatable; + ASSERT_IN_DBG(try_fix_table(mt)); } } } diff --git a/VM/src/llsl.cpp b/VM/src/llsl.cpp index 8620b7a4..f2e75e95 100644 --- a/VM/src/llsl.cpp +++ b/VM/src/llsl.cpp @@ -1437,6 +1437,198 @@ static void make_weak_uuid_table(lua_State *L) lua_setmetatable(L, -2); } +// ServerLua: callable quaternion module +static int quaternion_call(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + lua_remove(L, 1); + return lsl_quaternion_ctor(L); +} + +static inline float quaternion_dot(const float* a, const float* b) { + return ((a)[0] * (b)[0] + (a)[1] * (b)[1] + (a)[2] * (b)[2] + (a)[3] * (b)[3]); +} + +static int lua_quaternion_normalize(lua_State *L) +{ + const float* quat = luaSL_checkquaternion(L, 1); + float invNorm = 1.0f / sqrtf(quaternion_dot(quat, quat)); + luaSL_pushquaternion(L, quat[0] * invNorm, quat[1] * invNorm, quat[2] * invNorm, quat[3] * invNorm); + return 1; +} + +static int lua_quaternion_magnitude(lua_State *L) +{ + const float* quat = luaSL_checkquaternion(L, 1); + lua_pushnumber(L, sqrtf(quaternion_dot(quat, quat))); + return 1; +} + + +static int lua_quaternion_dot(lua_State *L) +{ + const float* a = luaSL_checkquaternion(L, 1); + const float* b = luaSL_checkquaternion(L, 2); + lua_pushnumber(L, quaternion_dot(a, b)); + return 1; +} + +static int lua_quaternion_slerp(lua_State *L) +{ + const float* a = luaSL_checkquaternion(L, 1); + const float* b = luaSL_checkquaternion(L, 2); + const float t = luaL_checknumber(L, 3); + + float b_to[4] = {b[0], b[1], b[2], b[3]}; + float cosom = quaternion_dot(a, b_to); + if (cosom < 0.0f) + { + cosom = -cosom; + b_to[0] = -b_to[0]; + b_to[1] = -b_to[1]; + b_to[2] = -b_to[2]; + b_to[3] = -b_to[3]; + } + + // calculate coefficients + float omega = acosf(cosom); + float sinom = sinf(omega); + float scale0 = sinf((1.0f - t) * omega) / sinom; + float scale1 = sinf(t * omega) / sinom; + // calculate final values + luaSL_pushquaternion(L, + scale0 * a[0] + scale1 * b_to[0], + scale0 * a[1] + scale1 * b_to[1], + scale0 * a[2] + scale1 * b_to[2], + scale0 * a[3] + scale1 * b_to[3]); + return 1; +} + +static int lua_quaternion_conjugate(lua_State *L) +{ + const float* quat = luaSL_checkquaternion(L, 1); + luaSL_pushquaternion(L, -quat[0], -quat[1], -quat[2], quat[3]); + return 1; +} + + +static inline void quaternion_to(lua_State *L, const float* vec) { + const float* quat = luaSL_checkquaternion(L, 1); + float res[3] = {0.0f}; + rot_vec(vec, quat, res); + lua_pushvector(L, res[0], res[1], res[2]); +} + +static int lua_quaternion_tofwd(lua_State *L) +{ + const float vec[3] = {1.0f, 0.0f, 0.0f}; + quaternion_to(L, vec); + return 1; +} + +static int lua_quaternion_toleft(lua_State *L) +{ + const float vec[3] = {0.0f, 1.0f, 0.0f}; + quaternion_to(L, vec); + return 1; +} + +static int lua_quaternion_toup(lua_State *L) +{ + const float vec[3] = {0.0f, 0.0f, 1.0f}; + quaternion_to(L, vec); + return 1; +} + +static const luaL_Reg quaternionlib[] = { + {"create", lsl_quaternion_ctor}, + {"normalize", lua_quaternion_normalize}, + {"magnitude", lua_quaternion_magnitude}, + {"dot", lua_quaternion_dot}, + {"slerp", lua_quaternion_slerp}, + {"conjugate", lua_quaternion_conjugate}, + {"tofwd", lua_quaternion_tofwd}, + {"toleft", lua_quaternion_toleft}, + {"toup", lua_quaternion_toup}, + {NULL, NULL}, +}; + +int luaopen_sl_quaternion(lua_State* L, const char* name) +{ + [[maybe_unused]] int old_top = lua_gettop(L); + lua_newtable(L); + luaL_register(L, NULL, quaternionlib); + + luaSL_pushquaternion(L, 0.0, 0.0, 0.0, 1.0); + lua_setfield(L, -2, "identity"); + + // ServerLua: `quaternion()` is an alias to `quaternion.create()`, so we need to add a metatable + // to the quaternion module which allows calling it. + lua_newtable(L); + lua_pushcfunction(L, quaternion_call, "__call"); + lua_setfield(L, -2, "__call"); + + // We need to override __iter so generalized iteration doesn't try to use __call. + lua_rawgetfield(L, LUA_BASEGLOBALSINDEX, "pairs"); + // This is confusing at first, but we want a unique function identity + // when this shows up anywhere other than globals, otherwise we can + // muck up Ares serialization. + luau_dupcclosure(L, -1, "__iter"); + lua_replace(L, -2); + lua_rawsetfield(L, -2, "__iter"); + + lua_setreadonly(L, -1, true); + lua_setmetatable(L, -2); + + lua_setglobal(L, name); + + LUAU_ASSERT(lua_gettop(L) == old_top); + return 1; +} + +// ServerLua: callable uuid module +static int uuid_call(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + lua_remove(L, 1); + return lua_uuid_ctor(L); +} + +static const luaL_Reg uuidlib[] = { + {"create", lua_uuid_ctor}, + {NULL, NULL}, +}; + +int luaopen_sl_uuid(lua_State* L) +{ + [[maybe_unused]] int old_top = lua_gettop(L); + lua_newtable(L); + luaL_register(L, NULL, uuidlib); + + // ServerLua: `uuid()` is an alias to `uuid.create()`, so we need to add a metatable + // to the uuid module which allows calling it. + lua_newtable(L); + lua_pushcfunction(L, uuid_call, "__call"); + lua_setfield(L, -2, "__call"); + + // We need to override __iter so generalized iteration doesn't try to use __call. + lua_rawgetfield(L, LUA_BASEGLOBALSINDEX, "pairs"); + // This is confusing at first, but we want a unique function identity + // when this shows up anywhere other than globals, otherwise we can + // muck up Ares serialization. + luau_dupcclosure(L, -1, "__iter"); + lua_replace(L, -2); + lua_rawsetfield(L, -2, "__iter"); + + lua_setreadonly(L, -1, true); + lua_setmetatable(L, -2); + + lua_setglobal(L, "uuid"); + + LUAU_ASSERT(lua_gettop(L) == old_top); + return 1; +} + int luaopen_sl(lua_State* L, int expose_internal_funcs) { if (!LUAU_IS_SL_VM(L)) @@ -1447,23 +1639,14 @@ int luaopen_sl(lua_State* L, int expose_internal_funcs) int top = lua_gettop(L); // Load these into the global namespace - lua_pushcfunction(L, lsl_quaternion_ctor, "quaternion"); - luau_dupcclosure(L, -1, "rotation"); - // Alias it as "rotation" - lua_setglobal(L, "rotation"); - lua_setglobal(L, "quaternion"); if (LUAU_IS_LSL_VM(L)) { lua_pushcfunction(L, lsl_key_ctor, "uuid"); + luau_dupcclosure(L, -1, "touuid"); + lua_setglobal(L, "touuid"); + lua_setglobal(L, "uuid"); } - else - { - lua_pushcfunction(L, lua_uuid_ctor, "uuid"); - } - luau_dupcclosure(L, -1, "touuid"); - lua_setglobal(L, "touuid"); - lua_setglobal(L, "uuid"); lua_pushcfunction(L, lsl_to_vector, "tovector"); lua_setglobal(L, "tovector"); @@ -1524,6 +1707,15 @@ int luaopen_sl(lua_State* L, int expose_internal_funcs) lua_pop(L, 1); LUAU_ASSERT(lua_gettop(L) == top); + if (!LUAU_IS_LSL_VM(L)) + { + // Create uuid module table + luaopen_sl_uuid(L); + LUAU_ASSERT(lua_gettop(L) == top); + lua_pushcfunction(L, lua_uuid_ctor, "touuid"); + lua_setglobal(L, "touuid"); + } + ////// /// Quaternions ////// @@ -1570,6 +1762,11 @@ int luaopen_sl(lua_State* L, int expose_internal_funcs) lua_pop(L, 1); LUAU_ASSERT(lua_gettop(L) == top); + // Create quaternion module table + luaopen_sl_quaternion(L, "quaternion"); + luaopen_sl_quaternion(L, "rotation"); + LUAU_ASSERT(lua_gettop(L) == top); + ////// /// DetectedEvent ////// diff --git a/tests/conformance/uuid.lua b/tests/conformance/uuid.lua index de33a3a0..2b371ba7 100644 --- a/tests/conformance/uuid.lua +++ b/tests/conformance/uuid.lua @@ -7,6 +7,9 @@ assert(key == expected_key) assert(tostring(key) == expected_str) assert(#expected_key.bytes == 16) +local to_key = touuid(expected_str) +assert(to_key == key) + local expected_key_clone = uuid(buffer.fromstring(expected_key.bytes)) -- This should end up with the same UUID identity because of UUID interning. assert(expected_key_clone == expected_key) @@ -21,6 +24,7 @@ assert(tab[expected_key] == 2) -- Invalid values result in nil assert(uuid('foo') == nil) +assert(touuid('foo') == nil) -- But the blank string is special-cased to mean `NULL_KEY` assert(uuid('') == uuid('00000000-0000-0000-0000-000000000000')) From c77562fa8e0feaf47426d214218f1e3e000edb09 Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Sat, 13 Dec 2025 05:57:10 +0000 Subject: [PATCH 2/5] Add quaternion tests Signed-off-by: WolfGangS --- tests/SLConformance.test.cpp | 14 +++++++++++++ tests/conformance/quaternion.lua | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/conformance/quaternion.lua diff --git a/tests/SLConformance.test.cpp b/tests/SLConformance.test.cpp index d969f3e4..121d8c18 100644 --- a/tests/SLConformance.test.cpp +++ b/tests/SLConformance.test.cpp @@ -345,6 +345,20 @@ TEST_CASE("Push UUID string") }); } +static int give_quaternion(lua_State *L) +{ + luaSL_pushquaternion(L, 1.0, 2.0, 3.0, 4.0); + return 1; +} + +TEST_CASE("Push Quaternion") +{ + runConformance("quaternion.lua", nullptr, [](lua_State *L) { + lua_pushcfunction(L, give_quaternion, "give_quaternion"); + lua_setglobal(L, "give_quaternion"); + }); +} + static int get_num_table_keys(lua_State *L, int idx) { lua_pushnil(L); diff --git a/tests/conformance/quaternion.lua b/tests/conformance/quaternion.lua new file mode 100644 index 00000000..de714ab6 --- /dev/null +++ b/tests/conformance/quaternion.lua @@ -0,0 +1,35 @@ +local quat = give_quaternion() +local nintey = quaternion(0,0,0.7071068,0.7071068) + +local modules = {quaternion = quaternion, rotation = rotation} + +for _, module in modules do + + assert(quat == module(1.0, 2.0, 3.0, 4.0)) + + assert(module.create(1, 2, 3, 4) == module(1, 2, 3, 4)) + + + assert(`{module.normalize(module(3, 5, 2, 1))}` == "<0.480384, 0.800641, 0.320256, 0.160128>") + assert(module.normalize(module(3, 5, 2, 1)) == module(0.4803844690322876, 0.8006408214569092, 0.3202563226222992, 0.1601281613111496)) + + assert(module.magnitude(module(0, 0, 0, 1)) == 1) + + assert(module.dot(module(1, 2, 3, 4),module(0, 0, 0, 1)) == 4) + + assert(module.conjugate(nintey) == module(-nintey.x, -nintey.y, -nintey.z, nintey.s)) + + assert(`{module.slerp(nintey, module(0, 0, 0, 1), 0.5)}` == "<0, 0, 0.382683, 0.92388>") + + --string cast to deal with precision + assert(`{module.tofwd(nintey)}` == "<0, 1, 0>") + assert(`{module.toleft(nintey)}` == "<-1, 0, 0>") + assert(`{module.toup(nintey)}` == "<0, 0, 1>") + +end +-- Check rotation module has same implementation as quaternion module +for k,_ in quaternion do + assert(rotation[k] ~= nil) +end + +return "OK" \ No newline at end of file From 0488d592caacb00224d1e4a7acf35c84851b320a Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Sun, 14 Dec 2025 14:40:23 +0000 Subject: [PATCH 3/5] Review fixes: tests and normalize to... functions Signed-off-by: WolfGangS --- VM/src/llsl.cpp | 11 +++++----- tests/conformance/quaternion.lua | 36 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/VM/src/llsl.cpp b/VM/src/llsl.cpp index f2e75e95..fb94de6b 100644 --- a/VM/src/llsl.cpp +++ b/VM/src/llsl.cpp @@ -1512,31 +1512,32 @@ static int lua_quaternion_conjugate(lua_State *L) } -static inline void quaternion_to(lua_State *L, const float* vec) { +static inline void push_rotated_vector(lua_State *L, const float* vec) { const float* quat = luaSL_checkquaternion(L, 1); float res[3] = {0.0f}; rot_vec(vec, quat, res); - lua_pushvector(L, res[0], res[1], res[2]); + float invSqrt = 1.0f / sqrtf(res[0] * res[0] + res[1] * res[1] + res[2] * res[2]); + lua_pushvector(L, res[0] * invSqrt, res[1] * invSqrt, res[2] * invSqrt); } static int lua_quaternion_tofwd(lua_State *L) { const float vec[3] = {1.0f, 0.0f, 0.0f}; - quaternion_to(L, vec); + push_rotated_vector(L, vec); return 1; } static int lua_quaternion_toleft(lua_State *L) { const float vec[3] = {0.0f, 1.0f, 0.0f}; - quaternion_to(L, vec); + push_rotated_vector(L, vec); return 1; } static int lua_quaternion_toup(lua_State *L) { const float vec[3] = {0.0f, 0.0f, 1.0f}; - quaternion_to(L, vec); + push_rotated_vector(L, vec); return 1; } diff --git a/tests/conformance/quaternion.lua b/tests/conformance/quaternion.lua index de714ab6..2c49f592 100644 --- a/tests/conformance/quaternion.lua +++ b/tests/conformance/quaternion.lua @@ -1,32 +1,32 @@ local quat = give_quaternion() -local nintey = quaternion(0,0,0.7071068,0.7071068) +local yaw_ninety = quaternion(0,0,0.7071068,0.7071068) -local modules = {quaternion = quaternion, rotation = rotation} +assert(quat == quaternion(1, 2, 3, 4)) -for _, module in modules do +-- Test string cast is expected format and precision +assert(`{quat}` == "<1, 2, 3, 4>") +assert(`{yaw_ninety}` == "<0, 0, 0.707107, 0.707107>") - assert(quat == module(1.0, 2.0, 3.0, 4.0)) +-- Test both quaternion and rotation modules exist and can be used for creation the same way +assert(quaternion(1, 2, 3, 4) == quaternion(1, 2, 3, 4)) +assert(quaternion.create(4, 3, 2, 1) == rotation.create(4, 3, 2, 1)) - assert(module.create(1, 2, 3, 4) == module(1, 2, 3, 4)) +-- Test quaternion module functions +assert(quaternion.normalize(quaternion(3, 5, 2, 1)) == quaternion(0.4803844690322876, 0.8006408214569092, 0.3202563226222992, 0.1601281613111496)) +assert(quaternion.magnitude(quaternion(0, 0, 0, 1)) == 1) - assert(`{module.normalize(module(3, 5, 2, 1))}` == "<0.480384, 0.800641, 0.320256, 0.160128>") - assert(module.normalize(module(3, 5, 2, 1)) == module(0.4803844690322876, 0.8006408214569092, 0.3202563226222992, 0.1601281613111496)) +assert(quaternion.dot(quaternion(1, 2, 3, 4),quaternion(0, 0, 0, 1)) == 4) - assert(module.magnitude(module(0, 0, 0, 1)) == 1) +assert(quaternion.conjugate(quat) == quaternion(-quat.x, -quat.y, -quat.z, quat.s)) - assert(module.dot(module(1, 2, 3, 4),module(0, 0, 0, 1)) == 4) +assert(`{quaternion.slerp(yaw_ninety, quaternion(0, 0, 0, 1), 0.5)}` == "<0, 0, 0.382683, 0.92388>") - assert(module.conjugate(nintey) == module(-nintey.x, -nintey.y, -nintey.z, nintey.s)) +-- string cast to deal with precision +assert(`{quaternion.tofwd(yaw_ninety)}` == "<0, 1, 0>") +assert(`{quaternion.toleft(yaw_ninety)}` == "<-1, 0, 0>") +assert(`{quaternion.toup(yaw_ninety)}` == "<0, 0, 1>") - assert(`{module.slerp(nintey, module(0, 0, 0, 1), 0.5)}` == "<0, 0, 0.382683, 0.92388>") - - --string cast to deal with precision - assert(`{module.tofwd(nintey)}` == "<0, 1, 0>") - assert(`{module.toleft(nintey)}` == "<-1, 0, 0>") - assert(`{module.toup(nintey)}` == "<0, 0, 1>") - -end -- Check rotation module has same implementation as quaternion module for k,_ in quaternion do assert(rotation[k] ~= nil) From 34fbe3dbd64699a046d1d64e9316cd3f8291fbde Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Mon, 15 Dec 2025 16:38:34 +0000 Subject: [PATCH 4/5] Review fixes: Change slerp to use the same methodology as the secondlife viewers slerp function Signed-off-by: WolfGangS --- VM/src/llsl.cpp | 55 ++++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/VM/src/llsl.cpp b/VM/src/llsl.cpp index fb94de6b..a98098db 100644 --- a/VM/src/llsl.cpp +++ b/VM/src/llsl.cpp @@ -1477,30 +1477,43 @@ static int lua_quaternion_slerp(lua_State *L) { const float* a = luaSL_checkquaternion(L, 1); const float* b = luaSL_checkquaternion(L, 2); - const float t = luaL_checknumber(L, 3); + const float u = luaL_checknumber(L, 3); float b_to[4] = {b[0], b[1], b[2], b[3]}; - float cosom = quaternion_dot(a, b_to); - if (cosom < 0.0f) + float cos_t = quaternion_dot(a, b_to); + + bool bflip = false; + if (cos_t < 0.0f) + { + cos_t = -cos_t; + bflip = true; + } + + float alpha; + float beta; + if(1.0f - cos_t < 0.00001f) + { + beta = 1.0f - u; + alpha = u; + } + else + { + float theta = acosf(cos_t); + float sin_t = sinf(theta); + beta = sinf(theta - u*theta) / sin_t; + alpha = sinf(u*theta) / sin_t; + } + + if (bflip) { - cosom = -cosom; - b_to[0] = -b_to[0]; - b_to[1] = -b_to[1]; - b_to[2] = -b_to[2]; - b_to[3] = -b_to[3]; - } - - // calculate coefficients - float omega = acosf(cosom); - float sinom = sinf(omega); - float scale0 = sinf((1.0f - t) * omega) / sinom; - float scale1 = sinf(t * omega) / sinom; - // calculate final values - luaSL_pushquaternion(L, - scale0 * a[0] + scale1 * b_to[0], - scale0 * a[1] + scale1 * b_to[1], - scale0 * a[2] + scale1 * b_to[2], - scale0 * a[3] + scale1 * b_to[3]); + beta = -beta; + } + + luaSL_pushquaternion(L, + beta*a[0] + alpha*b[0], + beta*a[1] + alpha*b[1], + beta*a[2] + alpha*b[2], + beta*a[3] + alpha*b[3]); return 1; } From bce1bbd962f9ab9eab1b201416eb41a23b81b3a2 Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Mon, 15 Dec 2025 16:41:05 +0000 Subject: [PATCH 5/5] remove pointless extra variable Signed-off-by: WolfGangS --- VM/src/llsl.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/VM/src/llsl.cpp b/VM/src/llsl.cpp index a98098db..e42e2893 100644 --- a/VM/src/llsl.cpp +++ b/VM/src/llsl.cpp @@ -1479,8 +1479,7 @@ static int lua_quaternion_slerp(lua_State *L) const float* b = luaSL_checkquaternion(L, 2); const float u = luaL_checknumber(L, 3); - float b_to[4] = {b[0], b[1], b[2], b[3]}; - float cos_t = quaternion_dot(a, b_to); + float cos_t = quaternion_dot(a, b); bool bflip = false; if (cos_t < 0.0f)