From 6932085df817ef0394992ebb105b1b9267cb943c Mon Sep 17 00:00:00 2001 From: Janne Hellsten Date: Sun, 7 Jan 2024 12:56:22 +0200 Subject: [PATCH] Expose Luau built-in float vector support The Luau VM supports native f32 3- or 4-vectors so that typical linear algebra operations are fast in game code. Both the 3- and 4-vector flavors are supported. Use -Dluau_vector_size=N to choose which. This must be configured at build time, as the native Luau VM must be re-compiled for this setting. --- build.zig | 13 +++++++--- src/libluau.zig | 34 ++++++++++++++++++++++++ src/tests.zig | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 6a90819..9fbd0ff 100644 --- a/build.zig +++ b/build.zig @@ -20,7 +20,7 @@ pub fn build(b: *Build) void { const lang = b.option(Language, "lang", "Lua language version to build") orelse .lua54; const shared = b.option(bool, "shared", "Build shared library instead of static") orelse false; - + const luau_use_4_vector = b.option(bool, "luau_use_4_vector", "Build Luau to use 4-vectors instead of the default 3-vector.") orelse false; const upstream = b.dependency(@tagName(lang), .{}); // Zig module @@ -37,10 +37,16 @@ pub fn build(b: *Build) void { // Expose build configuration to the ziglua module const config = b.addOptions(); config.addOption(Language, "lang", lang); + config.addOption(bool, "luau_use_4_vector", luau_use_4_vector); ziglua.addOptions("config", config); + if (lang == .luau) { + const vector_size: usize = if (luau_use_4_vector) 4 else 3; + ziglua.addCMacro("LUA_VECTOR_SIZE", b.fmt("{}", .{vector_size})); + } + const lib = switch (lang) { - .luau => buildLuau(b, target, optimize, upstream, shared), + .luau => buildLuau(b, target, optimize, upstream, shared, luau_use_4_vector), else => buildLua(b, target, optimize, upstream, lang, shared), }; @@ -152,7 +158,7 @@ fn buildLua(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Optim } /// Luau has diverged enough from Lua (C++, project structure, ...) that it is easier to separate the build logic -fn buildLuau(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, upstream: *Build.Dependency, shared: bool) *Step.Compile { +fn buildLuau(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, upstream: *Build.Dependency, shared: bool, luau_use_4_vector: bool) *Step.Compile { const lib_opts = .{ .name = "luau", .target = target, @@ -174,6 +180,7 @@ fn buildLuau(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Opti "-DLUA_API=extern\"C\"", "-DLUACODE_API=extern\"C\"", "-DLUACODEGEN_API=extern\"C\"", + if (luau_use_4_vector) "-DLUA_VECTOR_SIZE=4" else "", }; for (luau_source_files) |file| { diff --git a/src/libluau.zig b/src/libluau.zig index 169129a..83c170d 100644 --- a/src/libluau.zig +++ b/src/libluau.zig @@ -12,6 +12,9 @@ const c = @cImport({ const config = @import("config"); pub const lang = config.lang; +/// The length of Luau vector values, either 3 or 4. +pub const luau_vector_size = if (config.luau_use_4_vector) 4 else 3; + /// This function is defined in luau.cpp and must be called to define the assertion printer extern "c" fn zig_registerAssertionHandler() void; @@ -139,6 +142,7 @@ pub const LuaType = enum(i5) { boolean = c.LUA_TBOOLEAN, light_userdata = c.LUA_TLIGHTUSERDATA, number = c.LUA_TNUMBER, + vector = c.LUA_TVECTOR, string = c.LUA_TSTRING, table = c.LUA_TTABLE, function = c.LUA_TFUNCTION, @@ -520,6 +524,11 @@ pub const Lua = struct { return c.lua_isuserdata(lua.state, index) != 0; } + /// Returns true if the value at the given index is a vector + pub fn isVector(lua: *Lua, index: i32) bool { + return c.lua_isvector(lua.state, index); + } + /// Returns true if the value at index1 is smaller than the value at index2, following the /// semantics of the Lua < operator. /// See https://www.lua.org/manual/5.4/manual.html#lua_lessthan @@ -713,6 +722,17 @@ pub const Lua = struct { c.lua_pushvalue(lua.state, index); } + fn pushVector3(lua: *Lua, x: f32, y: f32, z: f32) void { + c.lua_pushvector(lua.state, x, y, z); + } + + fn pushVector4(lua: *Lua, x: f32, y: f32, z: f32, w: f32) void { + c.lua_pushvector(lua.state, x, y, z, w); + } + + /// Pushes a floating point 3-vector (or 4-vector if configured) `v` onto the stack + pub const pushVector = if (luau_vector_size == 3) pushVector3 else pushVector4; + /// Returns true if the two values in indices `index1` and `index2` are primitively equal /// Bypasses __eq metamethods /// Returns false if not equal, or if any index is invalid @@ -927,6 +947,20 @@ pub const Lua = struct { return error.Fail; } + /// Converts the Lua value at the given `index` to a 3- or 4-vector. + /// The Lua value must be a vector. + pub fn toVector(lua: *Lua, index: i32) ![luau_vector_size]f32 { + const res = c.lua_tovector(lua.state, index); + if (res) |r| { + switch (luau_vector_size) { + 3 => return [_]f32{ r[0], r[1], r[2] }, + 4 => return [_]f32{ r[0], r[1], r[2], r[3] }, + else => @compileError("invalid luau_vector_size - should not happen"), + } + } + return error.Fail; + } + /// Returns the `LuaType` of the value at the given index /// Note that this is equivalent to lua_type but because type is a Zig primitive it is renamed to `typeOf` /// See https://www.lua.org/manual/5.1/manual.html#lua_type diff --git a/src/tests.zig b/src/tests.zig index ad02123..7a232e6 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -381,6 +381,10 @@ test "typenames" { try expectEqualStrings("function", lua.typeName(.function)); try expectEqualStrings("userdata", lua.typeName(.userdata)); try expectEqualStrings("thread", lua.typeName(.thread)); + + if (ziglua.lang == .luau) { + try expectEqualStrings("vector", lua.typeName(.vector)); + } } test "unsigned" { @@ -2249,3 +2253,68 @@ test "tagged userdata" { lua.pushInteger(13); try expectError(error.Fail, lua.userdataTag(-1)); } + +fn vectorCtor(l: *Lua) i32 { + const x = l.toNumber(1) catch unreachable; + const y = l.toNumber(2) catch unreachable; + const z = l.toNumber(3) catch unreachable; + if (ziglua.luau_vector_size == 4) { + const w = l.optNumber(4, 0); + l.pushVector(@floatCast(x), @floatCast(y), @floatCast(z), @floatCast(w)); + } else { + l.pushVector(@floatCast(x), @floatCast(y), @floatCast(z)); + } + return 1; +} + +test "luau vectors" { + if (ziglua.lang != .luau) return; + + var lua = try Lua.init(testing.allocator); + defer lua.deinit(); + lua.openLibs(); + lua.register("vector", ziglua.wrap(vectorCtor)); + + try lua.doString( + \\function test() + \\ local a = vector(1, 2, 3) + \\ local b = vector(4, 5, 6) + \\ local c = (a + b) * vector(2, 2, 2) + \\ return c + \\end + ); + _ = try lua.getGlobal("test"); + try lua.protectedCall(0, 1, 0); + var v = try lua.toVector(-1); + try testing.expectEqualSlices(f32, &[3]f32{ 10, 14, 18 }, v[0..3]); + + if (ziglua.luau_vector_size == 3) lua.pushVector(1, 2, 3) else lua.pushVector(1, 2, 3, 4); + try expect(lua.isVector(-1)); + v = try lua.toVector(-1); + const expected = if (ziglua.luau_vector_size == 3) [3]f32{ 1, 2, 3 } else [4]f32{ 1, 2, 3, 4 }; + try expectEqual(expected, v); + try expectEqualStrings("vector", lua.typeNameIndex(-1)); + + lua.pushInteger(5); + try expect(!lua.isVector(-1)); +} + +test "luau 4-vectors" { + if (ziglua.lang != .luau) return; + + var lua = try Lua.init(testing.allocator); + defer lua.deinit(); + lua.openLibs(); + lua.register("vector", ziglua.wrap(vectorCtor)); + + // More specific 4-vector tests + if (ziglua.luau_vector_size == 4) { + try lua.doString( + \\local a = vector(1, 2, 3, 4) + \\local b = vector(5, 6, 7, 8) + \\return a + b + ); + const vec4 = try lua.toVector(-1); + try expectEqual([4]f32{ 6, 8, 10, 12 }, vec4); + } +}