[Soft] adding array length support to ir interpreter
Mirror Gitea refs to GitHub / mirror (push) Successful in 24s
Build and Test / build (push) Successful in 1m14s
Build and Test / build (push) Successful in 1m29s
Docs and IR / build (push) Successful in 2m1s
Build and Test / build (push) Successful in 2m40s

This commit is contained in:
2026-09-05 21:51:24 +02:00
parent 51b299681e
commit bb8aa75b63
8 changed files with 169 additions and 81 deletions
+19 -1
View File
@@ -43,6 +43,7 @@ stage: module_ir.Stage,
entry_pc: u32, entry_pc: u32,
register_count: usize, register_count: usize,
scratch_count: usize, scratch_count: usize,
array_lengths: []const bc.ArrayLength,
code: []const bc.Instruction, code: []const bc.Instruction,
edges: []const bc.Edge, edges: []const bc.Edge,
copies: []const bc.Copy, copies: []const bc.Copy,
@@ -66,6 +67,7 @@ pub fn compile(backing_allocator: std.mem.Allocator, module: *const module_ir.Mo
.entry_pc = lowerer.entry_pc, .entry_pc = lowerer.entry_pc,
.register_count = lowerer.register_count, .register_count = lowerer.register_count,
.scratch_count = lowerer.scratch_count, .scratch_count = lowerer.scratch_count,
.array_lengths = lowerer.array_lengths.items,
.code = lowerer.code.items, .code = lowerer.code.items,
.edges = lowerer.edges.items, .edges = lowerer.edges.items,
.copies = lowerer.copies.items, .copies = lowerer.copies.items,
@@ -107,6 +109,7 @@ const Lowerer = struct {
register_count: usize = 0, register_count: usize = 0,
scratch_count: usize = 0, scratch_count: usize = 0,
entry_pc: u32 = 0, entry_pc: u32 = 0,
array_lengths: std.ArrayList(bc.ArrayLength) = .empty,
code: std.ArrayList(bc.Instruction) = .empty, code: std.ArrayList(bc.Instruction) = .empty,
edges: std.ArrayList(bc.Edge) = .empty, edges: std.ArrayList(bc.Edge) = .empty,
copies: std.ArrayList(bc.Copy) = .empty, copies: std.ArrayList(bc.Copy) = .empty,
@@ -387,7 +390,22 @@ const Lowerer = struct {
try self.emit(.store_buffer, src.components, src.base, byte_offset, bc.invalid_register, bc.invalid_register, @intFromEnum(op.resource)); try self.emit(.store_buffer, src.components, src.base, byte_offset, bc.invalid_register, bc.invalid_register, @intFromEnum(op.resource));
}, },
.call => return CompileError.UnsupportedOperation, .call => return CompileError.UnsupportedOperation,
.array_length => return CompileError.UnsupportedOperation, .array_length => |op| {
const dst = result orelse return CompileError.InvalidOperation;
const byte_offset = try self.bufferOffset(op.byte_offset);
_ = try self.storageBuffer(op.resource);
if (dst.components != 1 or dst.kind != .unsigned_integer)
return CompileError.InvalidOperation;
const metadata_index = try u32Index(self.array_lengths.items.len);
try self.array_lengths.append(self.allocator, .{
.resource = @intFromEnum(op.resource),
.stride = op.stride,
});
try self.emit(.array_length, 1, dst.base, byte_offset, bc.invalid_register, bc.invalid_register, metadata_index);
},
} }
} }
+65 -37
View File
@@ -98,45 +98,13 @@ pub fn run(self: *Self, program: *const Program, options: RunOptions) RuntimeErr
pc += 1; pc += 1;
switch (instruction.opcode) { switch (instruction.opcode) {
.copy => self.copy(instruction), .@"unreachable" => return RuntimeError.UnreachableExecuted,
.negate_i32 => self.unaryInt(instruction, .negate), .array_length => try self.arrayLength(program, options.resource_buffers, instruction),
.negate_f32 => self.unaryFloat(instruction),
.logical_not => self.unaryInt(instruction, .logical_not),
.bitwise_not => self.unaryInt(instruction, .bitwise_not),
.integer_add => try self.binaryInt(instruction, .add),
.integer_subtract => try self.binaryInt(instruction, .subtract),
.integer_multiply => try self.binaryInt(instruction, .multiply),
.unsigned_divide => try self.binaryInt(instruction, .unsigned_divide),
.signed_divide => try self.binaryInt(instruction, .signed_divide),
.unsigned_modulo => try self.binaryInt(instruction, .unsigned_modulo),
.signed_modulo => try self.binaryInt(instruction, .signed_modulo),
.shift_left => try self.binaryInt(instruction, .shift_left),
.logical_shift_right => try self.binaryInt(instruction, .logical_shift_right),
.arithmetic_shift_right => try self.binaryInt(instruction, .arithmetic_shift_right), .arithmetic_shift_right => try self.binaryInt(instruction, .arithmetic_shift_right),
.bitwise_and => try self.binaryInt(instruction, .bitwise_and), .bitwise_and => try self.binaryInt(instruction, .bitwise_and),
.bitwise_not => self.unaryInt(instruction, .bitwise_not),
.bitwise_or => try self.binaryInt(instruction, .bitwise_or), .bitwise_or => try self.binaryInt(instruction, .bitwise_or),
.bitwise_xor => try self.binaryInt(instruction, .bitwise_xor), .bitwise_xor => try self.binaryInt(instruction, .bitwise_xor),
.logical_and => try self.binaryInt(instruction, .logical_and),
.logical_or => try self.binaryInt(instruction, .logical_or),
.float_add => self.binaryFloat(instruction, .add),
.float_subtract => self.binaryFloat(instruction, .subtract),
.float_multiply => self.binaryFloat(instruction, .multiply),
.float_divide => self.binaryFloat(instruction, .divide),
.float_modulo => self.binaryFloat(instruction, .modulo),
.compare_equal => self.compareInt(instruction, .equal),
.compare_not_equal => self.compareInt(instruction, .not_equal),
.compare_unsigned_less => self.compareInt(instruction, .unsigned_less),
.compare_signed_less => self.compareInt(instruction, .signed_less),
.compare_ordered_float_equal => self.compareFloat(instruction, .ordered_equal),
.compare_unordered_float_equal => self.compareFloat(instruction, .unordered_equal),
.compare_ordered_float_not_equal => self.compareFloat(instruction, .ordered_not_equal),
.compare_unordered_float_not_equal => self.compareFloat(instruction, .unordered_not_equal),
.compare_ordered_float_less => self.compareFloat(instruction, .ordered_less),
.compare_unordered_float_less => self.compareFloat(instruction, .unordered_less),
.select => self.select(instruction),
.load_buffer => try self.loadBuffer(program, options.resource_buffers, instruction),
.store_buffer => try self.storeBuffer(program, options.resource_buffers, instruction),
.jump_edge => pc = try self.applyEdge(program, instruction.immediate),
.branch => { .branch => {
if (instruction.immediate >= program.branches.len) if (instruction.immediate >= program.branches.len)
return RuntimeError.InvalidBytecode; return RuntimeError.InvalidBytecode;
@@ -144,9 +112,42 @@ pub fn run(self: *Self, program: *const Program, options: RunOptions) RuntimeErr
const branch = program.branches[instruction.immediate]; const branch = program.branches[instruction.immediate];
pc = try self.applyEdge(program, if (self.registers[instruction.a] != 0) branch.true_edge else branch.false_edge); pc = try self.applyEdge(program, if (self.registers[instruction.a] != 0) branch.true_edge else branch.false_edge);
}, },
.return_void => return .returned, .compare_equal => self.compareInt(instruction, .equal),
.compare_not_equal => self.compareInt(instruction, .not_equal),
.compare_ordered_float_equal => self.compareFloat(instruction, .ordered_equal),
.compare_ordered_float_less => self.compareFloat(instruction, .ordered_less),
.compare_ordered_float_not_equal => self.compareFloat(instruction, .ordered_not_equal),
.compare_signed_less => self.compareInt(instruction, .signed_less),
.compare_unordered_float_equal => self.compareFloat(instruction, .unordered_equal),
.compare_unordered_float_less => self.compareFloat(instruction, .unordered_less),
.compare_unordered_float_not_equal => self.compareFloat(instruction, .unordered_not_equal),
.compare_unsigned_less => self.compareInt(instruction, .unsigned_less),
.copy => self.copy(instruction),
.discard => return .discarded, .discard => return .discarded,
.@"unreachable" => return RuntimeError.UnreachableExecuted, .float_add => self.binaryFloat(instruction, .add),
.float_divide => self.binaryFloat(instruction, .divide),
.float_modulo => self.binaryFloat(instruction, .modulo),
.float_multiply => self.binaryFloat(instruction, .multiply),
.float_subtract => self.binaryFloat(instruction, .subtract),
.integer_add => try self.binaryInt(instruction, .add),
.integer_multiply => try self.binaryInt(instruction, .multiply),
.integer_subtract => try self.binaryInt(instruction, .subtract),
.jump_edge => pc = try self.applyEdge(program, instruction.immediate),
.load_buffer => try self.loadBuffer(program, options.resource_buffers, instruction),
.logical_and => try self.binaryInt(instruction, .logical_and),
.logical_not => self.unaryInt(instruction, .logical_not),
.logical_or => try self.binaryInt(instruction, .logical_or),
.logical_shift_right => try self.binaryInt(instruction, .logical_shift_right),
.negate_f32 => self.unaryFloat(instruction),
.negate_i32 => self.unaryInt(instruction, .negate),
.return_void => return .returned,
.select => self.select(instruction),
.shift_left => try self.binaryInt(instruction, .shift_left),
.signed_divide => try self.binaryInt(instruction, .signed_divide),
.signed_modulo => try self.binaryInt(instruction, .signed_modulo),
.store_buffer => try self.storeBuffer(program, options.resource_buffers, instruction),
.unsigned_divide => try self.binaryInt(instruction, .unsigned_divide),
.unsigned_modulo => try self.binaryInt(instruction, .unsigned_modulo),
} }
} }
} }
@@ -173,6 +174,33 @@ const BinaryFloat = enum { add, subtract, multiply, divide, modulo };
const CompareInt = enum { equal, not_equal, unsigned_less, signed_less }; const CompareInt = enum { equal, not_equal, unsigned_less, signed_less };
const CompareFloat = enum { ordered_equal, unordered_equal, ordered_not_equal, unordered_not_equal, ordered_less, unordered_less }; const CompareFloat = enum { ordered_equal, unordered_equal, ordered_not_equal, unordered_not_equal, ordered_less, unordered_less };
fn arrayLength(self: *Self, program: *const Program, resource_buffers: []const ?[]u8, instruction: bc.Instruction) RuntimeError!void {
if (instruction.components != 1)
return RuntimeError.InvalidBytecode;
if (instruction.a >= self.registers.len or instruction.b >= self.registers.len)
return RuntimeError.InvalidBytecode;
if (instruction.immediate >= program.array_lengths.len)
return RuntimeError.InvalidBytecode;
const metadata = program.array_lengths[instruction.immediate];
if (metadata.stride == 0)
return RuntimeError.InvalidBytecode;
const buffer = try resourceBuffer(program, resource_buffers, metadata.resource);
const byte_offset: usize = self.registers[instruction.b];
if (byte_offset > buffer.len)
return RuntimeError.BufferOutOfBounds;
const byte_length = buffer.len - byte_offset;
const element_count = byte_length / metadata.stride;
self.registers[instruction.a] = std.math.cast(u32, element_count) orelse return RuntimeError.IntegerOverflow;
}
fn copy(self: *Self, instruction: bc.Instruction) void { fn copy(self: *Self, instruction: bc.Instruction) void {
for (0..instruction.components) |component| for (0..instruction.components) |component|
self.registers[@as(usize, instruction.a) + component] = self.registers[@as(usize, instruction.b) + component]; self.registers[@as(usize, instruction.a) + component] = self.registers[@as(usize, instruction.b) + component];
+1 -3
View File
@@ -54,9 +54,7 @@ pub fn compile(allocator: std.mem.Allocator, module: *SoftShaderModule, stage: *
}; };
errdefer program.deinit(); errdefer program.deinit();
if (!hasCompatibleInterface(&program, expected_stage) or if (!hasCompatibleInterface(&program, expected_stage) or (expected_stage == .compute and module_ir.execution_modes.workgroup_size == null)) {
(expected_stage == .compute and module_ir.execution_modes.workgroup_size == null))
{
std.log.scoped(.IrInterpreter).err("unsupported stage interface or execution modes", .{}); std.log.scoped(.IrInterpreter).err("unsupported stage interface or execution modes", .{});
return VkError.ValidationFailed; return VkError.ValidationFailed;
} }
+39 -33
View File
@@ -36,49 +36,50 @@ comptime {
} }
pub const Opcode = enum(u16) { pub const Opcode = enum(u16) {
copy, @"unreachable",
negate_i32, array_length,
negate_f32,
logical_not,
bitwise_not,
integer_add,
integer_subtract,
integer_multiply,
unsigned_divide,
signed_divide,
unsigned_modulo,
signed_modulo,
float_add,
float_subtract,
float_multiply,
float_divide,
float_modulo,
shift_left,
logical_shift_right,
arithmetic_shift_right, arithmetic_shift_right,
bitwise_and, bitwise_and,
bitwise_not,
bitwise_or, bitwise_or,
bitwise_xor, bitwise_xor,
logical_and, branch,
logical_or,
compare_equal, compare_equal,
compare_not_equal, compare_not_equal,
compare_unsigned_less,
compare_signed_less,
compare_ordered_float_equal, compare_ordered_float_equal,
compare_unordered_float_equal,
compare_ordered_float_not_equal,
compare_unordered_float_not_equal,
compare_ordered_float_less, compare_ordered_float_less,
compare_ordered_float_not_equal,
compare_signed_less,
compare_unordered_float_equal,
compare_unordered_float_less, compare_unordered_float_less,
select, compare_unordered_float_not_equal,
load_buffer, compare_unsigned_less,
store_buffer, copy,
jump_edge,
branch,
return_void,
discard, discard,
@"unreachable", float_add,
float_divide,
float_modulo,
float_multiply,
float_subtract,
integer_add,
integer_multiply,
integer_subtract,
jump_edge,
load_buffer,
logical_and,
logical_not,
logical_or,
logical_shift_right,
negate_f32,
negate_i32,
return_void,
select,
shift_left,
signed_divide,
signed_modulo,
store_buffer,
unsigned_divide,
unsigned_modulo,
}; };
pub const Copy = struct { pub const Copy = struct {
@@ -99,3 +100,8 @@ pub const Branch = struct {
true_edge: u32, true_edge: u32,
false_edge: u32, false_edge: u32,
}; };
pub const ArrayLength = struct {
resource: u32,
stride: u32,
};
+1 -1
View File
@@ -14,7 +14,7 @@ fn bitsF32(value: u32) f32 {
return @bitCast(value); return @bitCast(value);
} }
test "[interpreter] vector floating-point arithmetic" { test "[interpreter] vector float arithmetic" {
var module = try ir.parser.parseString(std.testing.allocator, var module = try ir.parser.parseString(std.testing.allocator,
\\ shader vertex @main \\ shader vertex @main
\\ { \\ {
+1 -1
View File
@@ -6,7 +6,7 @@ const Runtime = @import("../Runtime.zig");
const ir = shader_ir.ir; const ir = shader_ir.ir;
test "[interpreter] loop back edges copy block arguments in parallel" { test "[interpreter] loop copy block arguments in parallel" {
var module = try ir.parser.parseString(std.testing.allocator, var module = try ir.parser.parseString(std.testing.allocator,
\\ shader compute @main \\ shader compute @main
\\ { \\ {
@@ -64,7 +64,26 @@ const bounds_shader =
\\ } \\ }
; ;
test "[interpreter] storage-buffer vector load and store use portable little-endian words" { const array_length_shader =
\\ shader compute @main
\\ {
\\ @source: runtime_array[u32] = storage_buffer[set(0), binding(0)]
\\ @destination: u32 = storage_buffer[set(0), binding(1)]
\\
\\ %zero: constant u32 = 0
\\ %offset: constant u32 = 8
\\
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %length: u32 = array_length @source, %offset, stride 4
\\ store_buffer @destination, %zero, %length
\\ return
\\ }
\\ }
;
test "[interpreter] ssbo vector load/store use portable little-endian words" {
var module = try ir.parser.parseString(std.testing.allocator, copy_shader); var module = try ir.parser.parseString(std.testing.allocator, copy_shader);
defer module.deinit(); defer module.deinit();
@@ -92,7 +111,7 @@ test "[interpreter] storage-buffer vector load and store use portable little-end
try std.testing.expectEqual(@as(u8, 0xcc), destination[19]); try std.testing.expectEqual(@as(u8, 0xcc), destination[19]);
} }
test "[interpreter] storage-buffer scalar load and store interpret little-endian words" { test "[interpreter] ssbo scalar load/store interpret little-endian words" {
var module = try ir.parser.parseString(std.testing.allocator, scalar_shader); var module = try ir.parser.parseString(std.testing.allocator, scalar_shader);
defer module.deinit(); defer module.deinit();
@@ -108,7 +127,7 @@ test "[interpreter] storage-buffer scalar load and store interpret little-endian
try std.testing.expectEqualSlices(u8, &[_]u8{ 0x79, 0x56, 0x34, 0x12 }, &destination); try std.testing.expectEqualSlices(u8, &[_]u8{ 0x79, 0x56, 0x34, 0x12 }, &destination);
} }
test "[interpreter] storage-buffer accesses report unbound and out-of-bounds resources" { test "[interpreter] ssbo access oob resources" {
var module = try ir.parser.parseString(std.testing.allocator, bounds_shader); var module = try ir.parser.parseString(std.testing.allocator, bounds_shader);
defer module.deinit(); defer module.deinit();
@@ -125,3 +144,22 @@ test "[interpreter] storage-buffer accesses report unbound and out-of-bounds res
const unchanged = [_]u8{0xa5} ** 8; const unchanged = [_]u8{0xa5} ** 8;
try std.testing.expectEqualSlices(u8, &unchanged, &buffer); try std.testing.expectEqualSlices(u8, &unchanged, &buffer);
} }
test "[interpreter] ssbo array length" {
var module = try ir.parser.parseString(std.testing.allocator, array_length_shader);
defer module.deinit();
var program = try Program.compile(std.testing.allocator, &module);
defer program.deinit();
var runtime = try Runtime.init(std.testing.allocator, &program);
defer runtime.deinit();
var source = [_]u8{ 0xff, 0x78, 0x56, 0x34, 0x12, 0xef, 0xcd, 0xab, 0x90, 0x04, 0x03, 0x02, 0x01, 0xdd, 0xcc, 0xbb, 0xaa };
var destination = [_]u8{0xcc} ** 20;
const resources = [_]?[]u8{ source[0..], destination[0..] };
try std.testing.expectEqual(Runtime.Outcome.returned, try runtime.run(&program, .{ .resource_buffers = &resources }));
try std.testing.expectEqualSlices(u8, &[_]u8{ 2, 0, 0, 0 }, destination[0..4]);
const unchanged = [_]u8{0xcc} ** 16;
try std.testing.expectEqualSlices(u8, &unchanged, destination[4..]);
}
@@ -6,7 +6,7 @@ const Runtime = @import("../Runtime.zig");
const ir = shader_ir.ir; const ir = shader_ir.ir;
test "[interpreter] fragment discard is an execution outcome" { test "[interpreter] fragment discard" {
var module = try ir.parser.parseString(std.testing.allocator, var module = try ir.parser.parseString(std.testing.allocator,
\\ shader fragment @main \\ shader fragment @main
\\ { \\ {
@@ -27,7 +27,7 @@ test "[interpreter] fragment discard is an execution outcome" {
try std.testing.expectEqual(Runtime.Outcome.discarded, try runtime.run(&program, .{})); try std.testing.expectEqual(Runtime.Outcome.discarded, try runtime.run(&program, .{}));
} }
test "[interpreter] execution budget stops an infinite loop" { test "[interpreter] execution budget stops infinite loop" {
var module = try ir.parser.parseString(std.testing.allocator, var module = try ir.parser.parseString(std.testing.allocator,
\\ shader compute @main \\ shader compute @main
\\ { \\ {