From fcf303a0c62a66ff32fc92fe9c92ca9294f72caf Mon Sep 17 00:00:00 2001 From: Kbz-8 Date: Mon, 29 Jun 2026 14:30:16 +0200 Subject: [PATCH] adding missing opcodes, fixing runtime array pointer windows --- src/GLSL_std_450/opcodes.zig | 23 +- src/Module.zig | 2 + src/Result.zig | 1 - src/Runtime.zig | 407 +++++++++++++++++++++++++--- src/Value.zig | 123 ++++++--- src/opcodes.zig | 510 +++++++++++++++++++++++++---------- 6 files changed, 854 insertions(+), 212 deletions(-) diff --git a/src/GLSL_std_450/opcodes.zig b/src/GLSL_std_450/opcodes.zig index 098f327..bf9c73d 100644 --- a/src/GLSL_std_450/opcodes.zig +++ b/src/GLSL_std_450/opcodes.zig @@ -258,10 +258,25 @@ fn readIntegralLaneAsI32(src: *const Value, lane_index: usize) RuntimeError!i32 const lane_bits = try src.resolveLaneBitWidth(); const sign = try src.resolveSign(); return switch (lane_bits) { - inline 8, 16, 32, 64 => |bits| if (sign == .signed) - @intCast(try Value.readLane(.SInt, bits, src, lane_index)) - else - @intCast(try Value.readLane(.UInt, bits, src, lane_index)), + inline 8, 16, 32, 64 => |bits| blk: { + if (sign == .signed) { + const value = try Value.readLane(.SInt, bits, src, lane_index); + if (bits > 32) { + break :blk std.math.cast(i32, value) orelse if (value < 0) + std.math.minInt(i32) + else + std.math.maxInt(i32); + } + break :blk @intCast(value); + } else { + const value = try Value.readLane(.UInt, bits, src, lane_index); + const bits32: u32 = if (bits > 32) + std.math.cast(u32, value) orelse std.math.maxInt(u32) + else + @intCast(value); + break :blk @bitCast(bits32); + } + }, else => RuntimeError.InvalidSpirV, }; } diff --git a/src/Module.zig b/src/Module.zig index 5d9f25f..1a034d8 100644 --- a/src/Module.zig +++ b/src/Module.zig @@ -54,6 +54,8 @@ pub const ReflectionInfos = struct { needs_derivatives: bool, has_control_barriers: bool, + has_atomics: bool, + early_fragment_tests: bool, }; options: ModuleOptions, diff --git a/src/Result.zig b/src/Result.zig index 5768411..19e79f4 100644 --- a/src/Result.zig +++ b/src/Result.zig @@ -482,7 +482,6 @@ pub fn getMemberCounts(self: *const Self) usize { pub inline fn flushPtr(self: *Self, allocator: std.mem.Allocator) RuntimeError!void { if (self.variant) |*variant| switch (variant.*) { - .Variable => |*v| try v.value.flushPtr(allocator), .AccessChain => |*a| { if (!std.mem.allEqual(u8, std.mem.asBytes(&a.value), 0xaa)) try a.value.flushPtr(allocator); diff --git a/src/Runtime.zig b/src/Runtime.zig index cf2fee8..d44078c 100644 --- a/src/Runtime.zig +++ b/src/Runtime.zig @@ -47,6 +47,11 @@ pub const SpecializationEntry = struct { size: usize, }; +pub const WorkgroupMemory = struct { + result: SpvWord, + bytes: []u8, +}; + pub const Derivative = struct { dx: Value, dy: Value, @@ -118,14 +123,16 @@ function_stack: std.ArrayList(Function), current_label: ?SpvWord, previous_label: ?SpvWord, +active_entry_point: ?SpvWord, specialization_constants: std.AutoHashMapUnmanaged(u32, []const u8), derivatives: std.AutoHashMapUnmanaged(SpvWord, Derivative), +phi_values: std.AutoHashMapUnmanaged(SpvWord, Value), image_api: ImageAPI, pub fn init(allocator: std.mem.Allocator, module: *Module, image_api: ImageAPI) RuntimeError!Self { - return .{ + var self: Self = .{ .mod = module, .it = module.it, .results = blk: { @@ -150,10 +157,15 @@ pub fn init(allocator: std.mem.Allocator, module: *Module, image_api: ImageAPI) .function_stack = .empty, .current_label = null, .previous_label = null, + .active_entry_point = null, .specialization_constants = .empty, .derivatives = .empty, + .phi_values = .empty, .image_api = image_api, }; + errdefer self.deinit(allocator); + try self.refreshValueLayouts(); + return self; } pub fn initFrom(allocator: std.mem.Allocator, other: *const Self, image_api: ImageAPI) RuntimeError!Self { @@ -180,13 +192,16 @@ pub fn initFrom(allocator: std.mem.Allocator, other: *const Self, image_api: Ima .function_stack = .empty, .current_label = null, .previous_label = null, + .active_entry_point = other.active_entry_point, .specialization_constants = .empty, .derivatives = .empty, + .phi_values = .empty, .image_api = image_api, }; errdefer self.deinit(allocator); try self.copySpecializationConstantsFrom(allocator, other); + try self.refreshValueLayouts(); return self; } @@ -207,6 +222,9 @@ pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { entry.value_ptr.deinit(allocator); } self.derivatives.deinit(allocator); + + self.clearPhiValues(allocator); + self.phi_values.deinit(allocator); } pub fn addSpecializationInfo(self: *Self, allocator: std.mem.Allocator, entry: SpecializationEntry, data: []const u8) RuntimeError!void { @@ -225,6 +243,169 @@ pub fn copySpecializationConstantsFrom(self: *Self, allocator: std.mem.Allocator } } +fn applyValueLayout(results: []Result, value: *Value, type_word: SpvWord) RuntimeError!void { + const resolved_type_word = results[type_word].resolveTypeWordOrNull() orelse type_word; + const type_data = (try results[resolved_type_word].getConstVariant()).Type; + + switch (value.*) { + .Structure => |*structure| switch (type_data) { + .Structure => |type_structure| { + @memcpy(@constCast(structure.offsets), type_structure.members_offsets); + @memcpy(@constCast(structure.matrix_strides), type_structure.members_matrix_strides); + @memcpy(@constCast(structure.row_major), type_structure.members_row_major); + for (structure.values, type_structure.members_type_word, 0..) |*member_value, member_type_word, member_index| { + if (member_value.* == .RuntimeArray) { + member_value.RuntimeArray.matrix_stride = type_structure.members_matrix_strides[member_index]; + member_value.RuntimeArray.row_major = type_structure.members_row_major[member_index]; + } + try applyValueLayout(results, member_value, member_type_word); + } + }, + else => {}, + }, + .Array => |array| switch (type_data) { + .Array => |type_array| for (array.values) |*element| { + try applyValueLayout(results, element, type_array.components_type_word); + }, + else => {}, + }, + .Matrix => |columns| switch (type_data) { + .Matrix => |type_matrix| for (columns) |*column| { + try applyValueLayout(results, column, type_matrix.column_type_word); + }, + else => {}, + }, + .Vector => |elements| switch (type_data) { + .Vector => |type_vector| for (elements) |*element| { + try applyValueLayout(results, element, type_vector.components_type_word); + }, + else => {}, + }, + else => {}, + } +} + +fn refreshValueLayouts(self: *Self) RuntimeError!void { + for (self.results) |*result| { + if (result.variant) |*variant| switch (variant.*) { + .Variable => |*v| switch (v.storage_class) { + .StorageBuffer, + .Uniform, + .PushConstant, + .Workgroup, + => try applyValueLayout(self.results, &v.value, v.type_word), + else => {}, + }, + else => {}, + }; + } +} + +fn typePlainMemorySize(self: *const Self, type_word: SpvWord) RuntimeError!usize { + const resolved_word = self.results[type_word].resolveTypeWordOrNull() orelse type_word; + const target_type = (try self.results[resolved_word].getConstVariant()).Type; + + return switch (target_type) { + .Array => |a| blk: { + const stride: usize = if (a.stride != 0) + @intCast(a.stride) + else + try self.typePlainMemorySize(a.components_type_word); + break :blk stride * @as(usize, @intCast(a.member_count)); + }, + .RuntimeArray => return RuntimeError.InvalidValueType, + .Structure => |s| blk: { + var size: usize = 0; + for (s.members_type_word, 0..) |member_type_word, i| { + const member_offset: usize = @intCast(s.members_offsets[i] orelse size); + size = @max(size, member_offset + try self.typePlainMemorySize(member_type_word)); + } + break :blk size; + }, + else => target_type.getSize(self.results), + }; +} + +pub fn createWorkgroupMemory(self: *Self, allocator: std.mem.Allocator) RuntimeError![]WorkgroupMemory { + var count: usize = 0; + for (self.results) |result| { + if (result.variant) |variant| switch (variant) { + .Variable => |v| { + if (v.storage_class == .Workgroup) count += 1; + }, + else => {}, + }; + } + + const memories = allocator.alloc(WorkgroupMemory, count) catch return RuntimeError.OutOfMemory; + errdefer allocator.free(memories); + + var index: usize = 0; + for (self.results, 0..) |result, result_id| { + if (result.variant) |variant| switch (variant) { + .Variable => |v| { + if (v.storage_class == .Workgroup) { + const size = try self.typePlainMemorySize(v.type_word); + const bytes = allocator.alloc(u8, size) catch return RuntimeError.OutOfMemory; + @memset(bytes, 0); + memories[index] = .{ + .result = @intCast(result_id), + .bytes = bytes, + }; + index += 1; + } + }, + else => {}, + }; + } + + return memories; +} + +pub fn destroyWorkgroupMemory(_: *Self, allocator: std.mem.Allocator, memories: []WorkgroupMemory) void { + for (memories) |memory| allocator.free(memory.bytes); + allocator.free(memories); +} + +pub fn bindWorkgroupMemory(self: *Self, memories: []const WorkgroupMemory) RuntimeError!void { + for (memories) |memory| { + _ = try (try self.results[memory.result].getValue()).write(memory.bytes); + } +} + +fn clearPhiValues(self: *Self, allocator: std.mem.Allocator) void { + var it = self.phi_values.iterator(); + while (it.next()) |entry| { + entry.value_ptr.deinit(allocator); + } + self.phi_values.clearRetainingCapacity(); +} + +pub fn snapshotPhiValues(self: *Self, allocator: std.mem.Allocator) RuntimeError!void { + self.clearPhiValues(allocator); + + for (self.results, 0..) |*result, result_id| { + const value = switch (result.variant orelse continue) { + .Constant => |*constant| &constant.value, + .FunctionParameter => |*parameter| parameter.value_ptr orelse continue, + else => continue, + }; + if (std.meta.activeTag(value.*) == .Pointer) continue; + const snapshot = try value.dupe(allocator); + const gop = self.phi_values.getOrPut(allocator, @intCast(result_id)) catch { + var tmp = snapshot; + tmp.deinit(allocator); + return RuntimeError.OutOfMemory; + }; + if (gop.found_existing) gop.value_ptr.deinit(allocator); + gop.value_ptr.* = snapshot; + } +} + +pub fn getPhiValueSnapshot(self: *Self, id: SpvWord) ?*const Value { + return self.phi_values.getPtr(id); +} + pub fn setDerivative(self: *Self, allocator: std.mem.Allocator, result: SpvWord, dx: *const Value, dy: *const Value) RuntimeError!void { const derivative: Derivative = .{ .dx = try dx.dupe(allocator), @@ -283,33 +464,70 @@ pub fn copyDerivative(self: *Self, allocator: std.mem.Allocator, dst: SpvWord, s pub fn applySpecializationLayout(self: *Self, allocator: std.mem.Allocator) RuntimeError!void { self.reset(); + try self.applySpecializationConstants(allocator); + try self.applySpecializationDependentLayout(allocator); +} + +pub fn applySpecializationInvocationLayout(self: *Self, allocator: std.mem.Allocator) RuntimeError!void { + self.reset(); + if (self.specialization_constants.count() != 0) + try self.applySpecializationConstants(allocator); + try self.applySpecializationDependentLayout(allocator); +} + +fn applySpecializationConstants(self: *Self, allocator: std.mem.Allocator) RuntimeError!void { try self.pass(allocator, .initMany(&.{ .SpecConstantTrue, .SpecConstantFalse, .SpecConstantComposite, + .ConstantNull, .SpecConstant, .SpecConstantOp, + })); +} + +fn applySpecializationDependentLayout(self: *Self, allocator: std.mem.Allocator) RuntimeError!void { + try self.pass(allocator, .initMany(&.{ .TypeArray, .Variable, })); } pub fn getEntryPointByName(self: *const Self, name: []const u8) RuntimeError!SpvWord { + if (self.active_entry_point) |entry_point| { + if (entryPointNameMatches(self.mod.entry_points.items[entry_point].name, name)) + return entry_point; + } + for (self.mod.entry_points.items, 0..) |entry_point, i| { - if (blk: { - // Not using std.mem.eql as entry point names may have longer size than their content - for (0..@min(name.len, entry_point.name.len)) |j| { - if (name[j] != entry_point.name[j]) - break :blk false; - } - if (entry_point.name.len != name.len and entry_point.name[name.len] != 0) - break :blk false; - break :blk true; - }) return @intCast(i); + if (entryPointNameMatches(entry_point.name, name)) return @intCast(i); } return RuntimeError.NotFound; } +pub fn getEntryPointByNameAndExecutionModel(self: *const Self, name: []const u8, execution_model: spv.SpvExecutionModel) RuntimeError!SpvWord { + for (self.mod.entry_points.items, 0..) |entry_point, i| { + if (entry_point.exec_model == execution_model and entryPointNameMatches(entry_point.name, name)) + return @intCast(i); + } + return RuntimeError.NotFound; +} + +fn entryPointNameMatches(entry_point_name: []const u8, name: []const u8) bool { + // Not using std.mem.eql as entry point names may have longer size than their content. + for (0..@min(name.len, entry_point_name.len)) |j| { + if (name[j] != entry_point_name[j]) + return false; + } + return entry_point_name.len == name.len or entry_point_name[name.len] == 0; +} + +pub fn selectEntryPoint(self: *Self, entry_point_index: SpvWord) RuntimeError!void { + if (entry_point_index >= self.mod.entry_points.items.len) + return RuntimeError.InvalidEntryPoint; + self.active_entry_point = entry_point_index; +} + pub fn getResultByName(self: *const Self, name: []const u8) RuntimeError!SpvWord { for (self.results, 0..) |result, i| { if (result.name) |result_name| { @@ -332,17 +550,76 @@ pub inline fn getResultByLocation(self: *const Self, location: SpvWord, kind: Lo } pub fn getResultByLocationComponent(self: *const Self, location: SpvWord, component: SpvWord, kind: LocationKind) RuntimeError!SpvWord { - switch (kind) { - .input => if (location < self.mod.input_locations.len and component < 4 and self.mod.input_locations[location][component] != 0) { - return self.mod.input_locations[location][component]; - }, - .output => if (location < self.mod.output_locations.len and component < 4 and self.mod.output_locations[location][component] != 0) { - return self.mod.output_locations[location][component]; - }, + const locations = switch (kind) { + .input => &self.mod.input_locations, + .output => &self.mod.output_locations, + }; + if (location >= locations.len or component >= 4) + return RuntimeError.NotFound; + + const result = locations[location][component]; + if (result != 0 and self.resultIsInActiveInterface(result)) + return result; + + if (self.active_entry_point) |entry_point_index| { + const entry_point = self.mod.entry_points.items[entry_point_index]; + for (entry_point.globals) |global| { + if (global >= self.results.len) + continue; + + const variant = self.results[global].variant orelse continue; + const variable = switch (variant) { + .Variable => |v| v, + else => continue, + }; + const storage_class_matches = switch (kind) { + .input => variable.storage_class == .Input, + .output => variable.storage_class == .Output, + }; + if (!storage_class_matches) + continue; + + for (self.results[global].decorations.items) |decoration| { + if (decoration.rtype == .Location and + decoration.literal_1 == location and + self.resultComponent(global) == component) + { + return global; + } + } + } } return RuntimeError.NotFound; } +fn resultIsInActiveInterface(self: *const Self, result: SpvWord) bool { + const entry_point_index = self.active_entry_point orelse return true; + const global = self.resultGlobal(result) orelse return false; + return std.mem.indexOfScalar(SpvWord, self.mod.entry_points.items[entry_point_index].globals, global) != null; +} + +fn resultGlobal(self: *const Self, result: SpvWord) ?SpvWord { + if (result >= self.results.len) + return null; + + const variant = self.results[result].variant orelse return result; + return switch (variant) { + .AccessChain => |access_chain| access_chain.base, + else => result, + }; +} + +fn resultComponent(self: *const Self, result: SpvWord) SpvWord { + if (result >= self.results.len) + return 0; + + for (self.results[result].decorations.items) |decoration| { + if (decoration.rtype == .Component) + return decoration.literal_1; + } + return 0; +} + pub fn getResultPrimitiveType(self: *const Self, result: SpvWord) RuntimeError!PrimitiveType { if (result >= self.results.len) return RuntimeError.OutOfBounds; @@ -354,6 +631,7 @@ pub fn getWorkgroupSize(self: *Self, allocator: std.mem.Allocator) RuntimeError! .SpecConstantTrue, .SpecConstantFalse, .SpecConstantComposite, + .ConstantNull, .SpecConstant, .SpecConstantOp, })); @@ -410,13 +688,8 @@ pub fn beginEntryPoint(self: *Self, allocator: std.mem.Allocator, entry_point_in return RuntimeError.InvalidEntryPoint; // Spec constants pass - try self.pass(allocator, .initMany(&.{ - .SpecConstantTrue, - .SpecConstantFalse, - .SpecConstantComposite, - .SpecConstant, - .SpecConstantOp, - })); + if (self.specialization_constants.count() != 0) + try self.applySpecializationConstants(allocator); { const entry_point_desc = &self.mod.entry_points.items[entry_point_index]; @@ -681,20 +954,22 @@ const InputLocationTarget = struct { }; fn resolveInputLocationTarget(self: *const Self, location: SpvWord) RuntimeError!InputLocationTarget { - if (location < self.mod.input_locations.len and - self.mod.input_locations[location][0] != 0 and - self.results[self.mod.input_locations[location][0]].variant != null) - { - const result = self.mod.input_locations[location][0]; + if (self.getResultByLocationComponent(location, 0, .input)) |result| { + if (self.results[result].variant == null) + return RuntimeError.NotFound; + const value = try self.results[result].getConstValue(); switch (value.*) { .Array => |arr| { if (arr.values.len == 0) return RuntimeError.OutOfBounds; - return .{ .result = result, .array_element = 0 }; + return .{ .result = result }; }, .Matrix => return .{ .result = result, .matrix_column = 0 }, else => return .{ .result = result }, } + } else |err| switch (err) { + RuntimeError.NotFound => {}, + else => return err, } for (self.results, 0..) |*result, id| { @@ -736,11 +1011,10 @@ fn resolveInputLocationTarget(self: *const Self, location: SpvWord) RuntimeError while (base_location > 0) { base_location -= 1; - const result = if (base_location < self.mod.input_locations.len) - self.mod.input_locations[base_location][0] - else - 0; - if (result == 0) continue; + const result = self.getResultByLocationComponent(base_location, 0, .input) catch |err| switch (err) { + RuntimeError.NotFound => continue, + else => return err, + }; const location_offset: usize = @intCast(location - base_location); const value = try self.results[result].getConstValue(); @@ -830,7 +1104,7 @@ pub fn readOutput(self: *const Self, output: []u8, result: SpvWord) RuntimeError } pub fn readBuiltIn(self: *const Self, output: []u8, builtin: spv.SpvBuiltIn) RuntimeError!void { - if (self.mod.builtins.get(builtin)) |result| { + if (self.getBuiltinResult(builtin)) |result| { try self.readResultValue(output, result); } else { return RuntimeError.NotFound; @@ -865,13 +1139,34 @@ pub fn writeInputLocation(self: *const Self, input: []const u8, location: SpvWor } pub fn writeBuiltIn(self: *const Self, allocator: std.mem.Allocator, input: []const u8, builtin: spv.SpvBuiltIn) RuntimeError!void { - if (self.mod.builtins.get(builtin)) |result| { + if (self.getBuiltinResult(builtin)) |result| { try self.writeResultValue(allocator, input, result); } else { return RuntimeError.NotFound; } } +fn getBuiltinResult(self: *const Self, builtin: spv.SpvBuiltIn) ?SpvWord { + if (self.mod.builtins.get(builtin)) |result| { + if (self.resultIsInActiveInterface(result)) + return result; + } + + const entry_point_index = self.active_entry_point orelse return null; + const entry_point = self.mod.entry_points.items[entry_point_index]; + for (entry_point.globals) |global| { + if (global >= self.results.len) + continue; + + for (self.results[global].decorations.items) |decoration| { + if (decoration.rtype == .BuiltIn and decoration.literal_1 == @intFromEnum(builtin)) + return global; + } + } + + return null; +} + pub fn flushDescriptorSets(self: *const Self, allocator: std.mem.Allocator) RuntimeError!void { for (self.results) |*result| { try result.flushPtr(allocator); @@ -902,12 +1197,48 @@ pub fn hasResultDecoration(self: *const Self, result: SpvWord, decoration: spv.S return false; } +pub fn hasResultOrMemberDecoration(self: *const Self, result: SpvWord, decoration: spv.SpvDecoration) bool { + if (self.hasResultDecoration(result, decoration)) + return true; + + if (result >= self.results.len) + return false; + + const type_word = switch ((self.results[result].variant orelse return false)) { + .Variable => |variable| variable.type_word, + else => return false, + }; + const target_type_word = switch ((self.results[type_word].variant orelse return false)) { + .Type => |t| switch (t) { + .Pointer => |ptr| ptr.target, + else => type_word, + }, + else => return false, + }; + const target_type = self.results[target_type_word].variant orelse return false; + switch (target_type) { + .Type => |t| switch (t) { + .Structure => { + for (self.results[target_type_word].decorations.items) |member_decoration| { + if (member_decoration.rtype == decoration) + return true; + } + }, + else => {}, + }, + else => {}, + } + + return false; +} + pub fn resetInvocation(self: *Self, allocator: std.mem.Allocator) void { var derivatives = self.derivatives.iterator(); while (derivatives.next()) |entry| { entry.value_ptr.deinit(allocator); } self.derivatives.clearRetainingCapacity(); + self.clearPhiValues(allocator); for (self.results) |*result| { if (result.variant) |*variant| { diff --git a/src/Value.zig b/src/Value.zig index 4b26f4e..aa619f2 100644 --- a/src/Value.zig +++ b/src/Value.zig @@ -264,19 +264,6 @@ pub const Value = union(Type) { break :blk self; }, .Array => |a| blk: { - // If an array is in externally visible storage we treat it as a runtime array - if (is_externally_visible) { - break :blk .{ - .RuntimeArray = .{ - .type_word = a.components_type_word, - .stride = a.stride, - .data = &.{}, - .matrix_stride = null, - .row_major = false, - }, - }; - } - const self: Self = .{ .Array = .{ .stride = a.stride, @@ -389,6 +376,17 @@ pub const Value = union(Type) { }; }, }, + .Pointer => |p| .{ + .Pointer = .{ + .ptr = p.ptr, + .image_texel = p.image_texel, + .uniform_slice_window = p.uniform_slice_window, + .uniform_backing_value = p.uniform_backing_value, + .owns_uniform_backing_value = false, + .matrix_stride = p.matrix_stride, + .matrix_row_major = p.matrix_row_major, + }, + }, else => self.*, }; } @@ -479,7 +477,7 @@ pub const Value = union(Type) { var offset: usize = 0; for (arr.values) |v| { _ = try v.read(output[offset..]); - offset += arr.stride; + offset += if (arr.stride != 0) arr.stride else try v.getPlainMemorySize(); } return offset; }, @@ -488,10 +486,15 @@ pub const Value = union(Type) { for (s.values, 0..) |v, i| { const member_offset: usize = @intCast(s.offsets[i] orelse end_offset); if (member_offset > output.len) return RuntimeError.OutOfBounds; + const member_output = if (v == .RuntimeArray and i + 1 < s.values.len and s.offsets[i + 1] != null) blk: { + const next_offset: usize = @intCast(s.offsets[i + 1].?); + if (next_offset < member_offset or next_offset > output.len) return RuntimeError.OutOfBounds; + break :blk output[member_offset..next_offset]; + } else output[member_offset..]; const read_size = if (s.matrix_strides[i]) |matrix_stride| - try v.readWithMatrixLayout(output[member_offset..], matrix_stride, s.row_major[i]) + try v.readWithMatrixLayout(member_output, matrix_stride, s.row_major[i]) else - try v.read(output[member_offset..]); + try v.read(member_output); end_offset = @max(end_offset, member_offset + read_size); } return end_offset; @@ -624,7 +627,7 @@ pub const Value = union(Type) { var offset: usize = 0; for (arr.values) |*value| { _ = try value.readWithMatrixLayout(output[offset..], matrix_stride, row_major); - offset += arr.stride; + offset += if (arr.stride != 0) arr.stride else try value.getPlainMemorySize(); } break :blk offset; }, @@ -671,7 +674,7 @@ pub const Value = union(Type) { var offset: usize = 0; for (arr.values) |*value| { _ = try value.writeWithMatrixLayout(input[offset..], matrix_stride, row_major); - offset += arr.stride; + offset += if (arr.stride != 0) arr.stride else try value.getPlainMemorySize(); } break :blk offset; }, @@ -771,7 +774,7 @@ pub const Value = union(Type) { var offset: usize = 0; for (arr.values) |*v| { _ = try v.write(input[offset..]); - offset += arr.stride; + offset += if (arr.stride != 0) arr.stride else try v.getPlainMemorySize(); } return offset; }, @@ -780,17 +783,25 @@ pub const Value = union(Type) { for (s.values, 0..) |*v, i| { const member_offset: usize = @intCast(s.offsets[i] orelse end_offset); if (member_offset > input.len) return RuntimeError.OutOfBounds; + const member_input = if (v.* == .RuntimeArray and i + 1 < s.values.len and s.offsets[i + 1] != null) blk: { + const next_offset: usize = @intCast(s.offsets[i + 1].?); + if (next_offset < member_offset or next_offset > input.len) return RuntimeError.OutOfBounds; + break :blk input[member_offset..next_offset]; + } else input[member_offset..]; const write_size = if (s.matrix_strides[i]) |matrix_stride| - try v.writeWithMatrixLayout(input[member_offset..], matrix_stride, s.row_major[i]) + try v.writeWithMatrixLayout(member_input, matrix_stride, s.row_major[i]) else - try v.write(input[member_offset..]); + try v.write(member_input); end_offset = @max(end_offset, member_offset + write_size); } if (end_offset > input.len) return RuntimeError.OutOfBounds; s.external_data = @constCast(input[0..end_offset]); return end_offset; }, - .RuntimeArray => |*arr| arr.data = @constCast(input[0..]), + .RuntimeArray => |*arr| { + arr.data = @constCast(input[0..]); + return input.len; + }, .Image => |*img| { if (input.len < @sizeOf(usize)) return RuntimeError.OutOfBounds; img.driver_image = @ptrFromInt(std.mem.bytesToValue(usize, input[0..@sizeOf(usize)])); @@ -840,7 +851,13 @@ pub const Value = union(Type) { } break :blk size; }, - .Array => |arr| arr.stride * arr.values.len, + .Array => |arr| blk: { + var size: usize = 0; + for (arr.values) |v| { + size += if (arr.stride != 0) arr.stride else try v.getPlainMemorySize(); + } + break :blk size; + }, .Structure => |s| blk: { var size: usize = 0; for (s.values, 0..) |v, i| { @@ -959,14 +976,54 @@ pub const Value = union(Type) { } } + fn readScalarLane(comptime T: PrimitiveType, comptime bits: u32, v: *const Value) RuntimeError!getPrimitiveFieldType(T, bits) { + const TT = getPrimitiveFieldType(T, bits); + + return switch (v.*) { + .Bool => |b| blk: { + if (T != .Bool or bits != 8) return RuntimeError.InvalidSpirV; + break :blk @as(TT, b); + }, + .Int => |i| blk: { + if (i.bit_count != bits) return RuntimeError.InvalidSpirV; + break :blk switch (T) { + .SInt => switch (bits) { + 8 => @as(TT, if (i.is_signed) i.value.sint8 else @bitCast(i.value.uint8)), + 16 => @as(TT, if (i.is_signed) i.value.sint16 else @bitCast(i.value.uint16)), + 32 => @as(TT, if (i.is_signed) i.value.sint32 else @bitCast(i.value.uint32)), + 64 => @as(TT, if (i.is_signed) i.value.sint64 else @bitCast(i.value.uint64)), + else => return RuntimeError.InvalidSpirV, + }, + .UInt => switch (bits) { + 8 => @as(TT, if (i.is_signed) @bitCast(i.value.sint8) else i.value.uint8), + 16 => @as(TT, if (i.is_signed) @bitCast(i.value.sint16) else i.value.uint16), + 32 => @as(TT, if (i.is_signed) @bitCast(i.value.sint32) else i.value.uint32), + 64 => @as(TT, if (i.is_signed) @bitCast(i.value.sint64) else i.value.uint64), + else => return RuntimeError.InvalidSpirV, + }, + else => return RuntimeError.InvalidSpirV, + }; + }, + .Float => |f| blk: { + if (T != .Float or f.bit_count != bits) return RuntimeError.InvalidSpirV; + break :blk switch (bits) { + 16 => @as(TT, f.value.float16), + 32 => @as(TT, f.value.float32), + 64 => @as(TT, f.value.float64), + else => return RuntimeError.InvalidSpirV, + }; + }, + else => return RuntimeError.InvalidSpirV, + }; + } + pub inline fn readLane(comptime T: PrimitiveType, comptime bits: u32, v: *const Value, lane_index: usize) RuntimeError!getPrimitiveFieldType(T, bits) { const TT = getPrimitiveFieldType(T, bits); return switch (v.*) { - .Int => (try getPrimitiveField(T, bits, @constCast(v))).*, - .Float => (try getPrimitiveField(T, bits, @constCast(v))).*, + .Bool, .Int, .Float => try readScalarLane(T, bits, v), - .Vector => |lanes| (try getPrimitiveField(T, bits, &lanes[lane_index])).*, + .Vector => |lanes| try readScalarLane(T, bits, &lanes[lane_index]), .Vector2f32 => |*vec| switch (lane_index) { inline 0...1 => |i| blk: { @@ -1001,7 +1058,7 @@ pub const Value = union(Type) { .Vector2i32 => |*vec| switch (lane_index) { inline 0...1 => |i| blk: { - if (bits == 32) { + if (comptime (T == .SInt or T == .UInt) and bits == 32) { break :blk @as(TT, @bitCast(vec[i])); } else { return RuntimeError.InvalidSpirV; @@ -1011,7 +1068,7 @@ pub const Value = union(Type) { }, .Vector3i32 => |*vec| switch (lane_index) { inline 0...2 => |i| blk: { - if (bits == 32) { + if (comptime (T == .SInt or T == .UInt) and bits == 32) { break :blk @as(TT, @bitCast(vec[i])); } else { return RuntimeError.InvalidSpirV; @@ -1021,7 +1078,7 @@ pub const Value = union(Type) { }, .Vector4i32 => |*vec| switch (lane_index) { inline 0...3 => |i| blk: { - if (bits == 32) { + if (comptime (T == .SInt or T == .UInt) and bits == 32) { break :blk @as(TT, @bitCast(vec[i])); } else { return RuntimeError.InvalidSpirV; @@ -1032,7 +1089,7 @@ pub const Value = union(Type) { .Vector2u32 => |*vec| switch (lane_index) { inline 0...1 => |i| blk: { - if (bits == 32) { + if (comptime (T == .SInt or T == .UInt) and bits == 32) { break :blk @as(TT, @bitCast(vec[i])); } else { return RuntimeError.InvalidSpirV; @@ -1042,7 +1099,7 @@ pub const Value = union(Type) { }, .Vector3u32 => |*vec| switch (lane_index) { inline 0...2 => |i| blk: { - if (bits == 32) { + if (comptime (T == .SInt or T == .UInt) and bits == 32) { break :blk @as(TT, @bitCast(vec[i])); } else { return RuntimeError.InvalidSpirV; @@ -1052,7 +1109,7 @@ pub const Value = union(Type) { }, .Vector4u32 => |*vec| switch (lane_index) { inline 0...3 => |i| blk: { - if (bits == 32) { + if (comptime (T == .SInt or T == .UInt) and bits == 32) { break :blk @as(TT, @bitCast(vec[i])); } else { return RuntimeError.InvalidSpirV; @@ -1185,7 +1242,7 @@ pub const Value = union(Type) { else => unreachable, }, } }, - else => return RuntimeError.InvalidSpirV, + .Bool => .{ .Bool = v }, }; }, else => return RuntimeError.InvalidSpirV, diff --git a/src/opcodes.zig b/src/opcodes.zig index 1fd946b..c19bcdc 100644 --- a/src/opcodes.zig +++ b/src/opcodes.zig @@ -114,21 +114,22 @@ pub const SetupDispatcher = block: { .AccessChain = setupAccessChain, .All = autoSetupConstant, .Any = autoSetupConstant, - .AtomicAnd = autoSetupConstant, - .AtomicCompareExchange = autoSetupConstant, - .AtomicExchange = autoSetupConstant, - .AtomicIAdd = autoSetupConstant, - .AtomicIDecrement = autoSetupConstant, - .AtomicIIncrement = autoSetupConstant, - .AtomicISub = autoSetupConstant, - .AtomicLoad = autoSetupConstant, - .AtomicOr = autoSetupConstant, - .AtomicSMax = autoSetupConstant, - .AtomicSMin = autoSetupConstant, - .AtomicStore = autoSetupConstant, - .AtomicUMax = autoSetupConstant, - .AtomicUMin = autoSetupConstant, - .AtomicXor = autoSetupConstant, + .ArrayLength = autoSetupConstant, + .AtomicAnd = setupAtomic, + .AtomicCompareExchange = setupAtomic, + .AtomicExchange = setupAtomic, + .AtomicIAdd = setupAtomic, + .AtomicIDecrement = setupAtomic, + .AtomicIIncrement = setupAtomic, + .AtomicISub = setupAtomic, + .AtomicLoad = setupAtomic, + .AtomicOr = setupAtomic, + .AtomicSMax = setupAtomic, + .AtomicSMin = setupAtomic, + .AtomicStore = setupAtomicStore, + .AtomicUMax = setupAtomic, + .AtomicUMin = setupAtomic, + .AtomicXor = setupAtomic, .BitCount = autoSetupConstant, .BitFieldInsert = autoSetupConstant, .BitFieldSExtract = autoSetupConstant, @@ -144,14 +145,16 @@ pub const SetupDispatcher = block: { .Constant = opConstant, .ConstantComposite = opConstantComposite, .ConstantFalse = opConstantFalse, + .ConstantNull = opConstantNull, .ConstantTrue = opConstantTrue, - .ControlBarrier = opControlBarrier, + .ControlBarrier = opControlBarrierSetup, .ConvertFToS = autoSetupConstant, .ConvertFToU = autoSetupConstant, .ConvertPtrToU = autoSetupConstant, .ConvertSToF = autoSetupConstant, .ConvertUToF = autoSetupConstant, .ConvertUToPtr = autoSetupConstant, + .CopyObject = autoSetupConstant, .DPdx = opDerivativeSetup, .DPdxCoarse = opDerivativeSetup, .DPdxFine = opDerivativeSetup, @@ -321,6 +324,7 @@ pub fn initRuntimeDispatcher() void { runtime_dispatcher[@intFromEnum(spv.SpvOp.AtomicUMax)] = AtomicEngine(.MaxUnsigned).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.AtomicUMin)] = AtomicEngine(.MinUnsigned).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.AtomicXor)] = AtomicEngine(.Xor).op; + runtime_dispatcher[@intFromEnum(spv.SpvOp.ArrayLength)] = opArrayLength; runtime_dispatcher[@intFromEnum(spv.SpvOp.BitCount)] = BitEngine(.UInt, .BitCount).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.BitFieldInsert)] = BitEngine(.UInt, .BitFieldInsert).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.BitFieldSExtract)] = BitEngine(.SInt, .BitFieldSExtract).op; @@ -335,11 +339,13 @@ pub fn initRuntimeDispatcher() void { runtime_dispatcher[@intFromEnum(spv.SpvOp.CompositeConstruct)] = opCompositeConstruct; runtime_dispatcher[@intFromEnum(spv.SpvOp.CompositeExtract)] = opCompositeExtract; runtime_dispatcher[@intFromEnum(spv.SpvOp.CompositeInsert)] = opCompositeInsert; + runtime_dispatcher[@intFromEnum(spv.SpvOp.ConstantNull)] = opConstantNull; runtime_dispatcher[@intFromEnum(spv.SpvOp.ControlBarrier)] = opControlBarrier; runtime_dispatcher[@intFromEnum(spv.SpvOp.ConvertFToS)] = ConversionEngine(.Float, .SInt).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.ConvertFToU)] = ConversionEngine(.Float, .UInt).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.ConvertSToF)] = ConversionEngine(.SInt, .Float).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.ConvertUToF)] = ConversionEngine(.UInt, .Float).op; + runtime_dispatcher[@intFromEnum(spv.SpvOp.CopyObject)] = opCopyObject; runtime_dispatcher[@intFromEnum(spv.SpvOp.CopyMemory)] = opCopyMemory; runtime_dispatcher[@intFromEnum(spv.SpvOp.DPdx)] = DerivativeEngine(.x).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.DPdxCoarse)] = DerivativeEngine(.x).op; @@ -580,15 +586,17 @@ fn BitEngine(comptime T: PrimitiveType, comptime Op: BitOp) type { const bits: u32 = info.int.bits; const U = std.meta.Int(.unsigned, bits); - if (count == 0) return base; + if (count == 0 or offset >= bits) return base; + + const actual_count: u64 = @min(count, @as(u64, bits) - offset); const base_u: U = @bitCast(base); const insert_u: U = @bitCast(insert); - const field_mask: U = if (count == bits) + const field_mask: U = if (actual_count == bits) ~@as(U, 0) else - (@as(U, 1) << @intCast(count)) - 1; + (@as(U, 1) << @intCast(actual_count)) - 1; const shift: std.math.Log2Int(U) = @truncate(offset); @@ -604,23 +612,26 @@ fn BitEngine(comptime T: PrimitiveType, comptime Op: BitOp) type { const bits: u32 = info.int.bits; - if (count == 0) return @as(TT, 0); + if (count == 0 or offset >= bits) return @as(TT, 0); + + const actual_count: u64 = @min(count, @as(u64, bits) - offset); const U = std.meta.Int(.unsigned, bits); const base_u: U = @bitCast(base); + const shift: std.math.Log2Int(U) = @truncate(offset); - const field: U = if (count == bits) + const field: U = if (actual_count == bits) base_u else - (base_u >> @intCast(offset)) & - ((@as(U, 1) << @intCast(count)) - 1); + (base_u >> shift) & + ((@as(U, 1) << @intCast(actual_count)) - 1); - const result: U = if (!signed_result or count == bits) blk: { + const result: U = if (!signed_result or actual_count == bits) blk: { break :blk field; } else blk: { - const sign_bit: U = @as(U, 1) << @intCast(count - 1); + const sign_bit: U = @as(U, 1) << @intCast(actual_count - 1); if ((field & sign_bit) != 0) { - break :blk field | (~@as(U, 0) << @intCast(count)); + break :blk field | (~@as(U, 0) << @intCast(actual_count)); } break :blk field; }; @@ -647,34 +658,57 @@ fn BitEngine(comptime T: PrimitiveType, comptime Op: BitOp) type { .BitwiseAnd => op1 & op2, .BitwiseOr => op1 | op2, .BitwiseXor => op1 ^ op2, - .ShiftLeft => op1 << @intCast(op2), - .ShiftRight, .ShiftRightArithmetic => op1 >> @intCast(op2), else => RuntimeError.InvalidSpirV, }; } - inline fn operationTernary(comptime TT: type, op1: TT, op2: TT, op3: *const Value) RuntimeError!TT { + inline fn operationShift(comptime TT: type, op1: TT, amount: u64) RuntimeError!TT { + if (amount >= @bitSizeOf(TT)) return @as(TT, 0); + const shift: std.math.Log2Int(TT) = @intCast(amount); + return switch (Op) { + .ShiftLeft => op1 << shift, + .ShiftRight, .ShiftRightArithmetic => op1 >> shift, + else => RuntimeError.InvalidSpirV, + }; + } + + inline fn operationTernary(comptime TT: type, op1: TT, op2: u64, op3: u64) RuntimeError!TT { return switch (Op) { .BitFieldSExtract => blk: { if (T != .SInt) return RuntimeError.InvalidSpirV; - break :blk bitExtract(TT, true, op1, @intCast(op2), op3.Int.value.uint64); + break :blk bitExtract(TT, true, op1, op2, op3); }, .BitFieldUExtract => blk: { if (T != .UInt) return RuntimeError.InvalidSpirV; - break :blk bitExtract(TT, false, op1, @intCast(op2), op3.Int.value.uint64); + break :blk bitExtract(TT, false, op1, op2, op3); }, else => RuntimeError.InvalidSpirV, }; } - inline fn operationQuaternary(comptime TT: type, op1: TT, op2: TT, op3: *const Value, op4: *const Value) RuntimeError!TT { + inline fn operationQuaternary(comptime TT: type, op1: TT, op2: TT, op3: u64, op4: u64) RuntimeError!TT { return switch (Op) { - .BitFieldInsert => bitInsert(TT, op1, op2, op3.Int.value.uint64, op4.Int.value.uint64), + .BitFieldInsert => bitInsert(TT, op1, op2, op3, op4), else => RuntimeError.InvalidSpirV, }; } + fn readIntegerLaneAsU64(value: *const Value, lane_index: usize) RuntimeError!u64 { + const lane_bits = try value.resolveLaneBitWidth(); + const sign = try value.resolveSign(); + return switch (lane_bits) { + inline 8, 16, 32, 64 => |bits| blk: { + if (sign == .signed) { + const lane = try Value.readLane(.SInt, bits, value, lane_index); + break :blk std.math.cast(u64, lane) orelse return RuntimeError.OutOfBounds; + } + break :blk @intCast(try Value.readLane(.UInt, bits, value, lane_index)); + }, + else => return RuntimeError.InvalidSpirV, + }; + } + fn applyScalarBits(bit_count: SpvWord, dst: *Value, ops: [max_operator_count]?*const Value) RuntimeError!void { switch (bit_count) { inline 8, 16, 32, 64 => |bits| { @@ -685,11 +719,25 @@ fn BitEngine(comptime T: PrimitiveType, comptime Op: BitOp) type { if (comptime isUnaryOp()) break :blk try operationUnary(TT, a); - const b = try Value.readLane(T, bits, ops[1].?, 0); - - if (comptime isBinaryOp()) break :blk try operationBinary(TT, a, b); - if (comptime isTernaryOp()) break :blk try operationTernary(TT, a, b, ops[2].?); - if (comptime isQuaternaryOp()) break :blk try operationQuaternary(TT, a, b, ops[2].?, ops[3].?); + if (comptime isBinaryOp()) { + if (comptime Op == .ShiftLeft or Op == .ShiftRight or Op == .ShiftRightArithmetic) { + const amount = try readIntegerLaneAsU64(ops[1].?, 0); + break :blk try operationShift(TT, a, amount); + } + const b = try Value.readLane(T, bits, ops[1].?, 0); + break :blk try operationBinary(TT, a, b); + } + if (comptime isTernaryOp()) { + const offset = try readIntegerLaneAsU64(ops[1].?, 0); + const count = try readIntegerLaneAsU64(ops[2].?, 0); + break :blk try operationTernary(TT, a, offset, count); + } + if (comptime isQuaternaryOp()) { + const b = try Value.readLane(T, bits, ops[1].?, 0); + const offset = try readIntegerLaneAsU64(ops[2].?, 0); + const count = try readIntegerLaneAsU64(ops[3].?, 0); + break :blk try operationQuaternary(TT, a, b, offset, count); + } }; try Value.writeLane(T, bits, dst, 0, out); @@ -711,11 +759,25 @@ fn BitEngine(comptime T: PrimitiveType, comptime Op: BitOp) type { if (comptime isUnaryOp()) break :blk try operationUnary(TT, a); - const b = try Value.readLane(T, bits, ops[1].?, if (ops[1].?.isVector()) i else 0); - - if (comptime isBinaryOp()) break :blk try operationBinary(TT, a, b); - if (comptime isTernaryOp()) break :blk try operationTernary(TT, a, b, ops[2].?); - if (comptime isQuaternaryOp()) break :blk try operationQuaternary(TT, a, b, ops[2].?, ops[3].?); + if (comptime isBinaryOp()) { + if (comptime Op == .ShiftLeft or Op == .ShiftRight or Op == .ShiftRightArithmetic) { + const amount = try readIntegerLaneAsU64(ops[1].?, if (ops[1].?.isVector()) i else 0); + break :blk try operationShift(TT, a, amount); + } + const b = try Value.readLane(T, bits, ops[1].?, if (ops[1].?.isVector()) i else 0); + break :blk try operationBinary(TT, a, b); + } + if (comptime isTernaryOp()) { + const offset = try readIntegerLaneAsU64(ops[1].?, if (ops[1].?.isVector()) i else 0); + const count = try readIntegerLaneAsU64(ops[2].?, if (ops[2].?.isVector()) i else 0); + break :blk try operationTernary(TT, a, offset, count); + } + if (comptime isQuaternaryOp()) { + const b = try Value.readLane(T, bits, ops[1].?, if (ops[1].?.isVector()) i else 0); + const offset = try readIntegerLaneAsU64(ops[2].?, if (ops[2].?.isVector()) i else 0); + const count = try readIntegerLaneAsU64(ops[3].?, if (ops[3].?.isVector()) i else 0); + break :blk try operationQuaternary(TT, a, b, offset, count); + } }; try Value.writeLane(T, bits, dst, i, out); @@ -809,18 +871,36 @@ fn CondEngine(comptime T: PrimitiveType, comptime Op: CondOp) type { } fn applyScalarBits(bit_count: SpvWord, dst_bool: *Value, a_v: *const Value, b_v: ?*const Value) RuntimeError!void { + if (comptime T == .Bool) { + const a = switch (a_v.*) { + .Bool => |value| value, + else => return RuntimeError.InvalidSpirV, + }; + if (comptime isUnaryOp()) { + dst_bool.Bool = try operationUnary(bool, a); + } else { + const b_ptr = b_v orelse return RuntimeError.InvalidSpirV; + const b = switch (b_ptr.*) { + .Bool => |value| value, + else => return RuntimeError.InvalidSpirV, + }; + dst_bool.Bool = try operationBinary(bool, a, b); + } + return; + } + switch (bit_count) { inline 8, 16, 32, 64 => |bits| { if (bits == 8 and T == .Float) return RuntimeError.InvalidSpirV; const TT = Value.getPrimitiveFieldType(T, bits); - const a = (try Value.getPrimitiveField(T, bits, @constCast(a_v))).*; + const a = try Value.readLane(T, bits, a_v, 0); if (comptime isUnaryOp()) { dst_bool.Bool = try operationUnary(TT, a); } else { const b_ptr = b_v orelse return RuntimeError.InvalidSpirV; - const b = (try Value.getPrimitiveField(T, bits, @constCast(b_ptr))).*; + const b = try Value.readLane(T, bits, b_ptr, 0); dst_bool.Bool = try operationBinary(TT, a, b); } }, @@ -912,57 +992,20 @@ fn CondEngine(comptime T: PrimitiveType, comptime Op: CondOp) type { .Bool => try applyScalarBits(lane_bits, dst, op1_value, op2_value), .Vector => |dst_vec| { - switch (op1_value.*) { - .Vector => |op1_vec| for (dst_vec, op1_vec, 0..) |*d_lane, a_lane, i| { - const b_ptr = laneRhsPtr(op2_value, i); - try applyScalarBits(lane_bits, d_lane, &a_lane, b_ptr); + switch (lane_bits) { + inline 8, 16, 32, 64 => |bits| { + if (bits == 8 and T == .Float) return RuntimeError.InvalidSpirV; + const TT = Value.getPrimitiveFieldType(T, bits); + for (dst_vec, 0..) |*d_lane, i| { + const a = try Value.readLane(T, bits, op1_value, i); + d_lane.Bool = if (comptime isUnaryOp()) blk: { + break :blk try operationUnary(TT, a); + } else blk: { + const b = try Value.readLane(T, bits, op2_value.?, i); + break :blk try operationBinary(TT, a, b); + }; + } }, - - .Vector4f32 => |*op1_vec| if (comptime isUnaryOp()) - try applyFixedVectorUnary(f32, 4, dst_vec, op1_vec) - else - try applyFixedVectorBinary(f32, 4, dst_vec, op1_vec, op2_value.?), - - .Vector3f32 => |*op1_vec| if (comptime isUnaryOp()) - try applyFixedVectorUnary(f32, 3, dst_vec, op1_vec) - else - try applyFixedVectorBinary(f32, 3, dst_vec, op1_vec, op2_value.?), - - .Vector2f32 => |*op1_vec| if (comptime isUnaryOp()) - try applyFixedVectorUnary(f32, 2, dst_vec, op1_vec) - else - try applyFixedVectorBinary(f32, 2, dst_vec, op1_vec, op2_value.?), - - .Vector4i32 => |*op1_vec| if (comptime isUnaryOp()) - try applyFixedVectorUnary(i32, 4, dst_vec, op1_vec) - else - try applyFixedVectorBinary(i32, 4, dst_vec, op1_vec, op2_value.?), - - .Vector3i32 => |*op1_vec| if (comptime isUnaryOp()) - try applyFixedVectorUnary(i32, 3, dst_vec, op1_vec) - else - try applyFixedVectorBinary(i32, 3, dst_vec, op1_vec, op2_value.?), - - .Vector2i32 => |*op1_vec| if (comptime isUnaryOp()) - try applyFixedVectorUnary(i32, 2, dst_vec, op1_vec) - else - try applyFixedVectorBinary(i32, 2, dst_vec, op1_vec, op2_value.?), - - .Vector4u32 => |*op1_vec| if (comptime isUnaryOp()) - try applyFixedVectorUnary(u32, 4, dst_vec, op1_vec) - else - try applyFixedVectorBinary(u32, 4, dst_vec, op1_vec, op2_value.?), - - .Vector3u32 => |*op1_vec| if (comptime isUnaryOp()) - try applyFixedVectorUnary(u32, 3, dst_vec, op1_vec) - else - try applyFixedVectorBinary(u32, 3, dst_vec, op1_vec, op2_value.?), - - .Vector2u32 => |*op1_vec| if (comptime isUnaryOp()) - try applyFixedVectorUnary(u32, 2, dst_vec, op1_vec) - else - try applyFixedVectorBinary(u32, 2, dst_vec, op1_vec, op2_value.?), - else => return RuntimeError.InvalidSpirV, } }, @@ -980,6 +1023,20 @@ fn ConversionEngine(comptime from_kind: PrimitiveType, comptime to_kind: Primiti inline 8, 16, 32, 64 => |bits| blk: { if (bits == 8 and from_kind == .Float) return RuntimeError.InvalidSpirV; // No f8 const v = try Value.readLane(from_kind, bits, from, lane_index); + if (comptime from_kind != .Float and to_kind != .Float) { + const to_bits = @bitSizeOf(ToT); + const FromUInt = std.meta.Int(.unsigned, bits); + const ToUInt = std.meta.Int(.unsigned, to_bits); + + const src_bits: FromUInt = @bitCast(v); + const dst_bits: ToUInt = if (to_bits < bits) + @truncate(src_bits) + else if (from_kind == .SInt) + @bitCast(@as(std.meta.Int(.signed, to_bits), @intCast(v))) + else + @intCast(src_bits); + break :blk @bitCast(dst_bits); + } break :blk std.math.lossyCast(ToT, v); }, else => return RuntimeError.InvalidSpirV, @@ -1001,11 +1058,9 @@ fn ConversionEngine(comptime from_kind: PrimitiveType, comptime to_kind: Primiti const target_type = (try rt.results[try rt.it.next()].getVariant()).Type; const dst_value = try rt.results[try rt.it.next()].getValue(); - const src_result = &rt.results[try rt.it.next()]; - const src_type_word = try src_result.getValueTypeWord(); - const src_value = try src_result.getValue(); + const src_value = try rt.results[try rt.it.next()].getValue(); - const from_bits = try Result.resolveLaneBitWidth((try rt.results[src_type_word].getVariant()).Type, rt); + const from_bits = try src_value.resolveLaneBitWidth(); const to_bits = try Result.resolveLaneBitWidth(target_type, rt); const dst_lane_count = try dst_value.resolveLaneCount(); @@ -1730,6 +1785,10 @@ fn ImageEngine(comptime Op: ImageOp) type { defer dy.deinit(allocator); const result_type = (try rt.results[result_type_word].getVariant()).Type; + if (!try typeHasFloatLanes(rt, result_type)) { + rt.clearDerivative(allocator, result_id); + return; + } const lane_bits = try Result.resolveLaneBitWidth(result_type, rt); const lane_count = try Result.resolveLaneCount(result_type); @@ -1749,6 +1808,19 @@ fn ImageEngine(comptime Op: ImageOp) type { try rt.setDerivative(allocator, result_id, &dx, &dy); } + fn typeHasFloatLanes(rt: *Runtime, target_type: Result.TypeData) RuntimeError!bool { + return switch (target_type) { + .Float, + .Vector2f32, + .Vector3f32, + .Vector4f32, + => true, + .Vector => |v| typeHasFloatLanes(rt, (try rt.results[v.components_type_word].getVariant()).Type), + .Matrix => |m| typeHasFloatLanes(rt, (try rt.results[m.column_type_word].getVariant()).Type), + else => false, + }; + } + fn sampleImageExplicitLod(rt: *Runtime, dst: *Value, driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32, offset: Runtime.ImageOffset) RuntimeError!void { switch (dst.*) { .Vector4f32, @@ -3078,6 +3150,44 @@ fn autoSetupConstant(allocator: std.mem.Allocator, _: SpvWord, rt: *Runtime) Run _ = try setupConstant(allocator, rt); } +fn setupAtomic(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime) RuntimeError!void { + rt.mod.reflection_infos.has_atomics = true; + try autoSetupConstant(allocator, word_count, rt); +} + +fn setupAtomicStore(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { + rt.mod.reflection_infos.has_atomics = true; +} + +fn opArrayLength(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { + _ = try rt.it.next(); // result type + const id = try rt.it.next(); + const structure = try rt.results[try rt.it.next()].getValue(); + const member_index = try rt.it.next(); + + const structure_value = switch (structure.*) { + .Pointer => |p| switch (p.ptr) { + .common => |value| value, + else => return RuntimeError.InvalidValueType, + }, + else => structure, + }; + + const length: u32 = switch (structure_value.*) { + .Structure => |s| blk: { + if (member_index >= s.values.len) return RuntimeError.OutOfBounds; + break :blk switch (s.values[member_index]) { + .RuntimeArray => |arr| @as(u32, @intCast(arr.getLen())), + else => return RuntimeError.InvalidValueType, + }; + }, + .RuntimeArray => |arr| @intCast(arr.getLen()), + else => return RuntimeError.InvalidValueType, + }; + + try Value.writeLane(.UInt, 32, try rt.results[id].getValue(), 0, length); +} + fn setupAccessChain(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime) RuntimeError!void { const var_type = try rt.it.next(); const id = try rt.it.next(); @@ -3264,26 +3374,53 @@ fn copyValue(dst: *Value, src: *const Value) RuntimeError!void { switch (src.*) { .Vector, .Matrix => |src_slice| { - const dst_slice = helpers.getDstSlice(dst); - try helpers.copySlice(dst_slice.?, src_slice); + if (dst.* == .RuntimeArray) { + const size = try src.getPlainMemorySize(); + if (size > dst.RuntimeArray.data.len) return RuntimeError.OutOfBounds; + _ = try src.read(dst.RuntimeArray.data[0..size]); + return; + } + const dst_slice = helpers.getDstSlice(dst) orelse return RuntimeError.InvalidSpirV; + try helpers.copySlice(dst_slice, src_slice); }, .Array => |a| { - const dst_slice = helpers.getDstSlice(dst); - try helpers.copySlice(dst_slice.?, a.values); + if (dst.* == .RuntimeArray) { + const size = try src.getPlainMemorySize(); + if (size > dst.RuntimeArray.data.len) return RuntimeError.OutOfBounds; + _ = try src.read(dst.RuntimeArray.data[0..size]); + return; + } + const dst_slice = helpers.getDstSlice(dst) orelse return RuntimeError.InvalidSpirV; + try helpers.copySlice(dst_slice, a.values); }, .Structure => |s| { + if (s.external_data) |src_data| { + if (dst.* == .Structure) { + if (dst.Structure.external_data) |dst_data| { + if (src_data.len > dst_data.len) return RuntimeError.OutOfBounds; + @memcpy(dst_data[0..src_data.len], src_data); + _ = try dst.write(dst_data[0..src_data.len]); + return; + } + } + } + if (dst.* == .RuntimeArray) { + const size = try src.getPlainMemorySize(); + if (size > dst.RuntimeArray.data.len) return RuntimeError.OutOfBounds; + _ = try src.read(dst.RuntimeArray.data[0..size]); + return; + } if (dst.* == .Structure) { @memcpy(@constCast(dst.Structure.offsets), s.offsets); @memcpy(@constCast(dst.Structure.matrix_strides), s.matrix_strides); @memcpy(@constCast(dst.Structure.row_major), s.row_major); } - const dst_slice = helpers.getDstSlice(dst); - try helpers.copySlice(dst_slice.?, s.values); + const dst_slice = helpers.getDstSlice(dst) orelse return RuntimeError.InvalidSpirV; + try helpers.copySlice(dst_slice, s.values); }, .Pointer => |ptr| switch (ptr.ptr) { .common => |src_val_ptr| { if (ptr.uniform_slice_window) |window| { - try copyValue(dst, src_val_ptr); try helpers.readWindow(dst, window, ptr.matrix_stride, ptr.matrix_row_major); } else { try copyValue(dst, src_val_ptr); @@ -3312,6 +3449,11 @@ fn copyValue(dst: *Value, src: *const Value) RuntimeError!void { }, }, .RuntimeArray => |src_arr| switch (dst.*) { + .Array => { + const size = try dst.getPlainMemorySize(); + if (size > src_arr.data.len) return RuntimeError.OutOfBounds; + _ = try dst.write(src_arr.data[0..size]); + }, .RuntimeArray => |dst_arr| @memcpy(dst_arr.data, src_arr.data), .Pointer => |dst_ptr| switch (dst_ptr.ptr) { .common => |dst_ptr_ptr| switch (dst_ptr_ptr.*) { @@ -3326,6 +3468,16 @@ fn copyValue(dst: *Value, src: *const Value) RuntimeError!void { } } +fn intValueToIndex(i: @TypeOf(@as(Value, undefined).Int)) RuntimeError!usize { + return switch (i.bit_count) { + 8 => if (i.is_signed) std.math.cast(usize, i.value.sint8) orelse RuntimeError.OutOfBounds else @as(usize, i.value.uint8), + 16 => if (i.is_signed) std.math.cast(usize, i.value.sint16) orelse RuntimeError.OutOfBounds else @as(usize, i.value.uint16), + 32 => if (i.is_signed) std.math.cast(usize, i.value.sint32) orelse RuntimeError.OutOfBounds else @as(usize, i.value.uint32), + 64 => if (i.is_signed) std.math.cast(usize, i.value.sint64) orelse RuntimeError.OutOfBounds else std.math.cast(usize, i.value.uint64) orelse RuntimeError.OutOfBounds, + else => RuntimeError.InvalidSpirV, + }; +} + fn opAccessChain(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime) RuntimeError!void { const var_type = try rt.it.next(); const id = try rt.it.next(); @@ -3433,7 +3585,7 @@ fn opAccessChain(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime } } - const component_index: usize = @intCast(i.value.uint32); + const component_index = try intValueToIndex(i); switch (value_ptr.*) { .Vector, .Matrix => |v| { @@ -3465,7 +3617,8 @@ fn opAccessChain(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime } }, .Array => |a| { - if (component_index >= a.values.len) return RuntimeError.OutOfBounds; + if (component_index >= a.values.len) + return RuntimeError.OutOfBounds; uniform_slice_window = try helpers.advanceWindow(uniform_slice_window, component_index * a.stride); value_ptr = &a.values[component_index]; switch (value_ptr.*) { @@ -3494,6 +3647,15 @@ fn opAccessChain(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime value_ptr = &s.values[component_index]; matrix_stride = s.matrix_strides[component_index]; matrix_row_major = s.row_major[component_index]; + if (uniform_slice_window) |window| { + if (value_ptr.* == .RuntimeArray) { + value_ptr.RuntimeArray.data = window; + if (matrix_stride) |stride| { + value_ptr.RuntimeArray.matrix_stride = stride; + value_ptr.RuntimeArray.row_major = matrix_row_major; + } + } + } }, .RuntimeArray => |*arr| { if (component_index >= arr.getLen()) return RuntimeError.OutOfBounds; @@ -3703,8 +3865,9 @@ fn opBitcast(allocator: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeErro _ = try to_value.write(bytes); } -fn opBranch(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { +fn opBranch(allocator: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { const id = try rt.it.next(); + try rt.snapshotPhiValues(allocator); rt.previous_label = rt.current_label; _ = rt.it.jumpToSourceLocation(switch ((try rt.results[id].getVariant()).*) { .Label => |l| l.source_location, @@ -3712,7 +3875,7 @@ fn opBranch(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { }); } -fn opBranchConditional(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { +fn opBranchConditional(allocator: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { const cond_value = try rt.results[try rt.it.next()].getValue(); const true_branch = switch ((try rt.results[try rt.it.next()].getVariant()).*) { .Label => |l| l.source_location, @@ -3722,6 +3885,7 @@ fn opBranchConditional(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeEr .Label => |l| l.source_location, else => return RuntimeError.InvalidSpirV, }; + try rt.snapshotPhiValues(allocator); rt.previous_label = rt.current_label; if (cond_value.Bool) { _ = rt.it.jumpToSourceLocation(true_branch); @@ -3872,7 +4036,7 @@ fn opCompositeExtract(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Ru else => return RuntimeError.InvalidSpirV, }, .value = blk: { - var composite = (try rt.results[composite_id].getVariant()).Constant.value; + var composite = (try rt.results[composite_id].getValue()).*; for (0..index_count) |_| { const member_id = try rt.it.next(); if (composite.getCompositeDataOrNull()) |v| { @@ -4049,6 +4213,36 @@ fn opConstantTrue(allocator: std.mem.Allocator, _: SpvWord, rt: *Runtime) Runtim } } +fn zeroValue(value: *Value) RuntimeError!void { + switch (value.*) { + .Void => {}, + .Bool => |*b| b.* = false, + .Int => |*i| i.value.uint64 = 0, + .Float => |*f| f.value.float64 = 0, + .Vector => |lanes| for (lanes) |*lane| try zeroValue(lane), + .Vector4f32 => |*v| v.* = .{ 0, 0, 0, 0 }, + .Vector3f32 => |*v| v.* = .{ 0, 0, 0 }, + .Vector2f32 => |*v| v.* = .{ 0, 0 }, + .Vector4i32 => |*v| v.* = .{ 0, 0, 0, 0 }, + .Vector3i32 => |*v| v.* = .{ 0, 0, 0 }, + .Vector2i32 => |*v| v.* = .{ 0, 0 }, + .Vector4u32 => |*v| v.* = .{ 0, 0, 0, 0 }, + .Vector3u32 => |*v| v.* = .{ 0, 0, 0 }, + .Vector2u32 => |*v| v.* = .{ 0, 0 }, + .Matrix => |columns| for (columns) |*column| try zeroValue(column), + .Array => |*a| for (a.values) |*element| try zeroValue(element), + .RuntimeArray => |*arr| @memset(arr.data, 0), + .Structure => |*s| for (s.values) |*field| try zeroValue(field), + .Image, .Sampler, .SampledImage, .Pointer => {}, + .Function => unreachable, + } +} + +fn opConstantNull(allocator: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { + const target = try setupConstant(allocator, rt); + try zeroValue(&target.variant.?.Constant.value); +} + fn opConstant(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime) RuntimeError!void { const target = try setupConstant(allocator, rt); switch (target.variant.?.Constant.value) { @@ -4937,6 +5131,20 @@ fn opCopyMemory(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!voi try copyValue(try rt.results[target].getValue(), try rt.results[source].getValue()); } +fn opCopyObject(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { + _ = try rt.it.next(); // result type + const target = try rt.it.next(); + const source = try rt.it.next(); + const target_value = try rt.results[target].getValue(); + const source_value = try rt.results[source].getValue(); + if (source_value.* == .Pointer) { + target_value.* = source_value.*; + target_value.Pointer.owns_uniform_backing_value = false; + return; + } + try copyValue(target_value, source_value); +} + fn opDecorate(allocator: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { const target = try rt.it.next(); const decoration_type = try rt.it.nextAs(spv.SpvDecoration); @@ -5118,6 +5326,7 @@ fn opExecutionMode(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError! .OutputLineStrip, .OutputTriangleStrip, => rt.mod.reflection_infos.geometry_output = @intFromEnum(mode), + .EarlyFragmentTests => rt.mod.reflection_infos.early_fragment_tests = true, else => {}, } } @@ -5323,7 +5532,8 @@ fn opPhi(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime) Runtim const parent_label_id = try rt.it.next(); if (parent_label_id == predecessor) { - try copyValue(try rt.results[id].getValue(), try rt.results[value_id].getValue()); + const value = rt.getPhiValueSnapshot(value_id) orelse try rt.results[value_id].getValue(); + try copyValue(try rt.results[id].getValue(), value); try rt.copyDerivative(allocator, id, value_id); return; } @@ -5332,12 +5542,12 @@ fn opPhi(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime) Runtim } fn quantizeF32ToF16(value: f32) f32 { - const rounded = @as(f32, @floatCast(@as(f16, @floatCast(value)))); - if (@abs(rounded) != 0.0 and @abs(rounded) < @as(f32, 0x1p-14)) { - const sign = @as(u32, @bitCast(value)) & 0x80000000; - return @bitCast(sign); + const quantized = @as(f32, @floatCast(@as(f16, @floatCast(value)))); + if (quantized != 0.0 and @abs(quantized) < 0x1.0p-14) { + const sign = @as(u32, @bitCast(quantized)) & 0x8000_0000; + return @as(f32, @bitCast(sign)); } - return rounded; + return quantized; } fn quantizeToF16Value(target_type: Result.TypeData, rt: *Runtime, dst: *Value, src: *const Value) RuntimeError!void { @@ -5410,13 +5620,13 @@ fn opSelect(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { const obj2_val = try rt.results[obj2].getValue(); if (target_val.getCompositeDataOrNull()) |*targets| { - for ( - targets.*, - cond_val.getCompositeDataOrNull().?, - obj1_val.getCompositeDataOrNull().?, - obj2_val.getCompositeDataOrNull().?, - ) |*t, c, o1, o2| { - try copyValue(t, if (c.Bool) &o1 else &o2); + for (targets.*, 0..) |*t, lane_index| { + const condition = try readVectorLaneAsValue(cond_val, lane_index); + if (condition != .Bool) + return RuntimeError.InvalidValueType; + + const selected = try readVectorLaneAsValue(if (condition.Bool) obj1_val else obj2_val, lane_index); + try copyValue(t, &selected); } return; } @@ -5518,6 +5728,7 @@ fn opTypeArray(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void var target = &rt.results[id]; const components_type_word = try rt.it.next(); const components_type_data = &((try rt.results[components_type_word].getVariant()).*).Type; + const length_word = try rt.it.next(); target.variant = .{ .Type = .{ .Array = .{ @@ -5526,16 +5737,7 @@ fn opTypeArray(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void .Type => |t| @as(Result.Type, t), else => return RuntimeError.InvalidSpirV, }, - .member_count = switch ((try rt.results[try rt.it.next()].getValue()).*) { - .Int => |i| if (!i.is_signed) @intCast(i.value.uint64) else switch (i.bit_count) { - 8 => @intCast(i.value.sint8), - 16 => @intCast(i.value.sint8), - 32 => @intCast(i.value.sint8), - 64 => @intCast(i.value.sint8), - else => return RuntimeError.InvalidSpirV, - }, - else => return RuntimeError.InvalidSpirV, - }, + .member_count = try arrayMemberCount(rt, length_word), .stride = blk: { for (target.decorations.items) |decoration| { if (decoration.rtype == .ArrayStride) @@ -5548,6 +5750,31 @@ fn opTypeArray(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void }; } +fn arrayMemberCount(rt: *Runtime, length_word: SpvWord) RuntimeError!SpvWord { + for (rt.results[length_word].decorations.items) |decoration| { + if (decoration.rtype != .SpecId) + continue; + + if (rt.specialization_constants.get(decoration.literal_1)) |data| { + if (data.len < @sizeOf(u32)) + return RuntimeError.OutOfBounds; + + return @intCast(std.mem.bytesToValue(u32, data[0..@sizeOf(u32)])); + } + } + + return switch ((try rt.results[length_word].getValue()).*) { + .Int => |i| if (!i.is_signed) @intCast(i.value.uint64) else switch (i.bit_count) { + 8 => @intCast(i.value.sint8), + 16 => @intCast(i.value.sint16), + 32 => @intCast(i.value.sint32), + 64 => @intCast(i.value.sint64), + else => return RuntimeError.InvalidSpirV, + }, + else => return RuntimeError.InvalidSpirV, + }; +} + fn opTypeBool(_: std.mem.Allocator, _: SpvWord, rt: *Runtime) RuntimeError!void { const id = try rt.it.next(); rt.results[id].variant = .{ @@ -5838,20 +6065,31 @@ fn opVariable(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime) R .Uniform, .StorageBuffer, .PushConstant, + .Workgroup, }; const is_externally_visible = std.mem.containsAtLeastScalar(spv.SpvStorageClass, &externally_visible_data_storages, 1, storage_class); + const use_external_storage = is_externally_visible and (storage_class == .Workgroup or resolved_type != .Array); + + if (target.variant) |*variant| { + switch (variant.*) { + .Variable => |*variable| variable.value.deinit(allocator), + else => {}, + } + } target.variant = .{ .Variable = .{ .storage_class = storage_class, .type_word = resolved_word, .type = resolved_type, - .value = try Value.init(allocator, rt.results, resolved_word, is_externally_visible and resolved_type != .Array), + .value = try Value.init(allocator, rt.results, resolved_word, use_external_storage), }, }; - _ = initializer; + if (initializer) |initializer_id| { + try copyValue(try target.getValue(), try rt.results[initializer_id].getValue()); + } } fn readDynamicVectorIndex(index_value: *const Value) RuntimeError!usize {