From 769009ad5ea735c4014b7e8ce9a5068cf0bb2b44 Mon Sep 17 00:00:00 2001 From: Kbz-8 Date: Mon, 11 May 2026 01:48:13 +0200 Subject: [PATCH] adding base matrix management --- .gitea/workflows/test.yml | 2 +- build.zig | 9 ++++- build.zig.zon | 4 +-- src/Result.zig | 3 ++ src/Value.zig | 2 +- src/opcodes.zig | 33 ++++++++++++++++-- test/maths.zig | 73 +++++++++++++++++++++++++++++++++++++++ test/root.zig | 30 +++++++++++++--- 8 files changed, 145 insertions(+), 11 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 7b8e263..16dcce4 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -20,4 +20,4 @@ jobs: - uses: https://codeberg.org/mlugg/setup-zig@v2 - name: Test - run: zig build test -Dno-example=true + run: zig build test -Dno-example=true --release=fast diff --git a/build.zig b/build.zig index f81b6de..48366bd 100644 --- a/build.zig +++ b/build.zig @@ -153,9 +153,15 @@ fn addZigTests( const no_test = b.option(bool, "no-test", "Skip unit test dependencies fetch") orelse false; if (no_test) return; + const test_filter = b.option( + []const u8, + "test-filter", + "Only run tests whose name contains this substring", + ); + const nzsl = b.lazyDependency("NZSL", .{ .target = target, - .optimize = optimize, + .optimize = .ReleaseFast, }) orelse return; const tests = b.addTest(.{ @@ -173,6 +179,7 @@ fn addZigTests( .path = b.path("test/test_runner.zig"), .mode = .simple, }, + .filters = if (test_filter) |filter| &.{filter} else &.{}, .use_llvm = use_llvm, }); diff --git a/build.zig.zon b/build.zig.zon index 79fb522..559df2c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -7,8 +7,8 @@ .hash = "zmath-0.11.0-dev-wjwivdMsAwD-xaLj76YHUq3t9JDH-X16xuMTmnDzqbu2", }, .NZSL = .{ // For unit tests - .url = "git+https://git.kbz8.me/kbz_8/NZigSL#ab95fc3734da46079fda2a4cd0f14143d92bf633", - .hash = "NZSL-1.1.2-N0xSVCR7AACeI_Wa6JPggJzy9_MPCpWC-2OHkMowwX-7", + .url = "git+https://git.kbz8.me/kbz_8/NZigSL#68f6c0ae2d0fc6b91eaa9df5c0fcd68f3529c5b8", + .hash = "NZSL-1.1.4-N0xSVC97AACi1-SuJ_ifNAOBRdCMPIXN1vMgVprfABhH", .lazy = true, }, //.sdl3 = .{ diff --git a/src/Result.zig b/src/Result.zig index c0c4cd0..a571c21 100644 --- a/src/Result.zig +++ b/src/Result.zig @@ -390,6 +390,7 @@ pub fn resolveLaneBitWidth(target_type: TypeData, rt: *const Runtime) RuntimeErr .Float => |f| f.bit_length, .Int => |i| i.bit_length, .Vector => |v| continue :sw (try rt.results[v.components_type_word].getVariant()).Type, + .Matrix => |m| continue :sw (try rt.results[m.column_type_word].getVariant()).Type, .Vector4f32, .Vector3f32, .Vector2f32, @@ -408,6 +409,7 @@ pub fn resolveLaneCount(target_type: TypeData) RuntimeError!SpvWord { return switch (target_type) { .Bool, .Float, .Int => 1, .Vector => |v| v.member_count, + .Matrix => |m| m.member_count, .Vector4f32, .Vector4i32, .Vector4u32 => 4, .Vector3f32, .Vector3i32, .Vector3u32 => 3, .Vector2f32, .Vector2i32, .Vector2u32 => 2, @@ -419,6 +421,7 @@ pub fn resolveSign(target_type: TypeData, rt: *const Runtime) RuntimeError!enum return sw: switch (target_type) { .Int => |i| if (i.is_signed) .signed else .unsigned, .Vector => |v| continue :sw (try rt.results[v.components_type_word].getVariant()).Type, + .Matrix => |m| continue :sw (try rt.results[m.column_type_word].getVariant()).Type, .Vector4i32 => .signed, .Vector3i32 => .signed, .Vector2i32 => .signed, diff --git a/src/Value.zig b/src/Value.zig index f57e8eb..354d752 100644 --- a/src/Value.zig +++ b/src/Value.zig @@ -136,7 +136,7 @@ pub const Value = union(Type) { return switch (self.*) { .Structure => |*s| s.values, .Array => |*a| a.values, - .Vector, .Matrix => |v| v, + .Vector => |v| v, else => null, }; } diff --git a/src/opcodes.zig b/src/opcodes.zig index dfa1100..b6b208e 100644 --- a/src/opcodes.zig +++ b/src/opcodes.zig @@ -289,7 +289,7 @@ pub fn initRuntimeDispatcher() void { runtime_dispatcher[@intFromEnum(spv.SpvOp.LogicalNot)] = CondEngine(.Bool, .LogicalNot).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.LogicalNotEqual)] = CondEngine(.Bool, .LogicalNotEqual).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.LogicalOr)] = CondEngine(.Bool, .LogicalOr).op; - runtime_dispatcher[@intFromEnum(spv.SpvOp.MatrixTimesMatrix)] = MathEngine(.Float, .MatrixTimesMatrix, false).op; // TODO + runtime_dispatcher[@intFromEnum(spv.SpvOp.MatrixTimesMatrix)] = MathEngine(.Float, .MatrixTimesMatrix, false).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.MatrixTimesScalar)] = MathEngine(.Float, .MatrixTimesScalar, false).op; // TODO runtime_dispatcher[@intFromEnum(spv.SpvOp.MatrixTimesVector)] = MathEngine(.Float, .MatrixTimesVector, false).op; // TODO runtime_dispatcher[@intFromEnum(spv.SpvOp.Not)] = BitEngine(.UInt, .Not).op; @@ -919,7 +919,9 @@ fn MathEngine(comptime T: PrimitiveType, comptime Op: MathOp, comptime IsAtomic: return switch (Op) { .Add => if (@typeInfo(TT) == .int) @addWithOverflow(op1, op2)[0] else op1 + op2, .Sub => if (@typeInfo(TT) == .int) @subWithOverflow(op1, op2)[0] else op1 - op2, - .Mul => if (@typeInfo(TT) == .int) @mulWithOverflow(op1, op2)[0] else op1 * op2, + .Mul, + .MatrixTimesMatrix, + => if (@typeInfo(TT) == .int) @mulWithOverflow(op1, op2)[0] else op1 * op2, .Div => blk: { if (op2 == 0) return RuntimeError.DivisionByZero; break :blk if (@typeInfo(TT) == .int) @divTrunc(op1, op2) else op1 / op2; @@ -1009,6 +1011,19 @@ fn MathEngine(comptime T: PrimitiveType, comptime Op: MathOp, comptime IsAtomic: .Vector3u32 => |*d| try operator.applySIMDVector(u32, 3, d, &lhs.Vector3u32, &rhs.Vector3u32), .Vector2u32 => |*d| try operator.applySIMDVector(u32, 2, d, &lhs.Vector2u32, &rhs.Vector2u32), + .Matrix => |dst_m| switch (Op) { + .MatrixTimesMatrix => { + for (dst_m, lhs.Matrix, rhs.Matrix) |*dst_vec, *lhs_vec, *rhs_vec| { + for (dst_vec.Vector, lhs_vec.Vector, rhs_vec.Vector) |*d_lane, *l_lane, *r_lane| { + try operator.applyScalar(lane_bits, d_lane, l_lane, r_lane); + } + } + }, + // TODO : matrix times vector + // TODO : matrix times scalar + else => return RuntimeError.ToDo, + }, + else => return RuntimeError.InvalidSpirV, } @@ -1600,6 +1615,20 @@ fn opCompositeConstruct(_: std.mem.Allocator, word_count: SpvWord, rt: *Runtime) } switch (value.*) { + .Matrix => |m| { + var index: SpvWord = 0; + for (m[0..]) |mat_elem| { + if (mat_elem.getCompositeDataOrNull()) |vec| { + for (vec[0..]) |*elem| { + const elem_value = (try rt.results[try rt.it.next()].getVariant()).Constant.value; + elem.* = elem_value; + index += 1; + if (index == index_count) + return; + } + } + } + }, .RuntimeArray => |arr| { var offset: usize = 0; diff --git a/test/maths.zig b/test/maths.zig index 69f0500..e436ae0 100644 --- a/test/maths.zig +++ b/test/maths.zig @@ -221,3 +221,76 @@ test "Maths vectors with scalars" { } } } + +// Tests all mathematical operation on mat3/4 with all NZSL supported primitive types +test "Maths matrices" { + const allocator = std.testing.allocator; + const types = [_]type{ f32, f64 }; + var operations = std.EnumMap(Operations, u8).init(.{ + .Add = '+', + .Sub = '-', + .Mul = '*', + }); + + var it = operations.iterator(); + while (it.next()) |op| { + inline for (3..5) |L| { + inline for (types) |T| { + const base: case.Mat(L, T) = .{ .val = case.random([L][L]T) }; + const ratio: case.Mat(L, T) = .{ .val = case.random([L][L]T) }; + var expected: case.Mat(L, T) = undefined; + for (expected.val[0..], base.val[0..], ratio.val[0..]) |*ec, bc, rc| { + for (ec[0..], bc[0..], rc[0..]) |*e, b, r| { + e.* = switch (op.key) { + .Add => b + r, + .Sub => b - r, + .Mul => b * r, + else => unreachable, + }; + } + } + + const shader = try std.fmt.allocPrint( + allocator, + \\ [nzsl_version("1.1")] + \\ [feature(float64)] + \\ module; + \\ + \\ struct FragOut + \\ {{ + \\ [location(0)] value: mat{d}[{s}] + \\ }} + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ {{ + \\ let output: FragOut; + \\ output.value = mat{d}[{s}]({f}) {c} mat{d}[{s}]({f}); + \\ return output; + \\ }} + , + .{ + L, + @typeName(T), + L, + @typeName(T), + base, + op.value.*, + L, + @typeName(T), + ratio, + }, + ); + defer allocator.free(shader); + const code = try compileNzsl(allocator, shader); + defer allocator.free(code); + try case.expect(.{ + .source = code, + .expected_outputs = &.{ + std.mem.asBytes(&expected), + }, + }); + } + } + } +} diff --git a/test/root.zig b/test/root.zig index d82ae2c..4f42d73 100644 --- a/test/root.zig +++ b/test/root.zig @@ -33,12 +33,12 @@ pub const case = struct { // To test with all important module options const module_options = [_]spv.Module.ModuleOptions{ - .{ - .use_simd_vectors_specializations = true, - }, .{ .use_simd_vectors_specializations = false, }, + //.{ + // .use_simd_vectors_specializations = true, + //}, }; for (module_options) |opt| { @@ -78,7 +78,7 @@ pub const case = struct { } pub fn random(comptime T: type) T { - var prng: std.Random.DefaultPrng = .init(@intCast(std.Io.Timestamp.now(std.testing.io, .real).toMicroseconds())); + var prng: std.Random.DefaultPrng = .init(@intCast(std.Io.Timestamp.now(std.testing.io, .real).toNanoseconds())); const rand = prng.random(); return switch (@typeInfo(T)) { @@ -91,6 +91,13 @@ pub const case = struct { } break :blk vec; }, + .array => |a| blk: { + var arr: [a.len]a.child = undefined; + inline for (0..a.len) |i| { + arr[i] = random(a.child); + } + break :blk arr; + }, inline else => unreachable, }; } @@ -107,6 +114,21 @@ pub const case = struct { } }; } + + pub fn Mat(comptime len: usize, comptime T: type) type { + return struct { + const Self = @This(); + val: [len][len]T, + pub fn format(self: *const Self, w: *std.Io.Writer) std.Io.Writer.Error!void { + inline for (0..len) |i| { + inline for (0..len) |j| { + try w.print("{d}", .{self.val[i][j]}); + if (i < len - 1 or j < len - 1) try w.writeAll(", "); + } + } + } + }; + } }; test {